refactor frontend (not done)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch, computed } from 'vue'
|
||||
import { NConfigProvider, darkTheme, NMessageProvider } from 'naive-ui'
|
||||
import { NConfigProvider, darkTheme, NMessageProvider, createDiscreteApi } from 'naive-ui'
|
||||
import { useWorldStore } from './stores/world'
|
||||
import { useUiStore } from './stores/ui'
|
||||
import { useSocketStore } from './stores/socket'
|
||||
@@ -23,14 +23,22 @@ const initStatus = ref<InitStatusDTO | null>(null)
|
||||
const gameInitialized = ref(false)
|
||||
const mapPreloaded = ref(false)
|
||||
let pollInterval: ReturnType<typeof setInterval> | null = null
|
||||
const canCloseMenu = ref(true)
|
||||
const { message } = createDiscreteApi(['message'], {
|
||||
configProviderProps: {
|
||||
theme: darkTheme
|
||||
}
|
||||
})
|
||||
|
||||
// 根据 spec: showLoading = initStatus !== 'ready'
|
||||
// 注意:
|
||||
// 1. initStatus 为 null 时显示加载界面(还没获取到状态)
|
||||
// 2. initStatus 不是 ready 时显示加载界面
|
||||
// 3. 前端还没初始化完成时也要显示加载界面
|
||||
// 2. initStatus 不是 ready 且不是 idle 时显示加载界面
|
||||
// 3. 前端还没初始化完成时也要显示加载界面(如果后端 ready)
|
||||
const showLoading = computed(() => {
|
||||
if (initStatus.value === null) return true
|
||||
// idle 状态下,不显示全屏 loading,而是显示菜单供用户配置
|
||||
if (initStatus.value.status === 'idle') return false
|
||||
if (initStatus.value.status !== 'ready') return true
|
||||
if (!gameInitialized.value) return true
|
||||
return false
|
||||
@@ -38,7 +46,7 @@ const showLoading = computed(() => {
|
||||
|
||||
const showMenu = ref(false)
|
||||
const isManualPaused = ref(true)
|
||||
const menuDefaultTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm'>('load')
|
||||
const menuDefaultTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm' | 'start'>('load')
|
||||
|
||||
// 可以提前加载地图的阶段(宗门初始化后地图数据就 ready 了)。
|
||||
const MAP_READY_PHASES = ['initializing_sects', 'generating_avatars', 'checking_llm', 'generating_initial_events']
|
||||
@@ -54,6 +62,12 @@ async function pollInitStatus() {
|
||||
const prevStatus = initStatus.value?.status
|
||||
initStatus.value = res
|
||||
|
||||
// 如果是从 null -> idle,或者一直保持 idle 但菜单没打开
|
||||
// 这里我们只在第一次检测到 idle 时执行启动检查
|
||||
if (res.status === 'idle' && !showMenu.value && prevStatus !== 'idle') {
|
||||
performStartupCheck()
|
||||
}
|
||||
|
||||
// 提前加载地图:当进入特定阶段且还没预加载过时。
|
||||
if (!mapPreloaded.value && MAP_READY_PHASES.includes(res.phase_name)) {
|
||||
mapPreloaded.value = true
|
||||
@@ -72,12 +86,60 @@ async function pollInitStatus() {
|
||||
await initializeGame()
|
||||
// ready 后停止轮询
|
||||
stopPolling()
|
||||
// 游戏准备就绪,关闭菜单
|
||||
showMenu.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch init status:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function performStartupCheck() {
|
||||
try {
|
||||
const res = await gameApi.fetchLLMStatus()
|
||||
|
||||
if (!res.configured) {
|
||||
// 未配置 -> 强制进入 LLM 配置,禁止关闭
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'llm'
|
||||
canCloseMenu.value = false
|
||||
message.warning('检测到 LLM 未配置,请先完成设置')
|
||||
} else {
|
||||
// 已配置 -> 验证连通性
|
||||
try {
|
||||
const configRes = await gameApi.fetchLLMConfig()
|
||||
await gameApi.testLLMConnection(configRes)
|
||||
|
||||
// 测试通过 -> 允许进入开始游戏
|
||||
menuDefaultTab.value = 'start'
|
||||
canCloseMenu.value = true
|
||||
// 确保菜单显示在 Start 页
|
||||
showMenu.value = true
|
||||
} catch (connErr) {
|
||||
// 连接失败 -> 强制进入配置
|
||||
console.error('LLM Connection check failed:', connErr)
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'llm'
|
||||
canCloseMenu.value = false
|
||||
message.error('LLM 连接测试失败,请重新配置')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to check LLM status:', e)
|
||||
// Fallback
|
||||
showMenu.value = true
|
||||
menuDefaultTab.value = 'llm'
|
||||
canCloseMenu.value = false
|
||||
message.error('无法获取系统状态')
|
||||
}
|
||||
}
|
||||
|
||||
function handleLLMReady() {
|
||||
canCloseMenu.value = true
|
||||
menuDefaultTab.value = 'start'
|
||||
message.success('LLM 配置成功,请开始游戏')
|
||||
}
|
||||
|
||||
async function initializeGame() {
|
||||
if (gameInitialized.value) {
|
||||
// 重新加载存档时,重新初始化
|
||||
@@ -141,6 +203,10 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
uiStore.clearSelection()
|
||||
} else {
|
||||
showMenu.value = !showMenu.value
|
||||
// 如果打开菜单,默认切到 load (或者保持上一次的状态,这里暂定 load)
|
||||
if (showMenu.value) {
|
||||
menuDefaultTab.value = 'load'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,7 +292,10 @@ watch([showMenu, isManualPaused], ([menuVisible, manualPaused]) => {
|
||||
<SystemMenu
|
||||
:visible="showMenu"
|
||||
:default-tab="menuDefaultTab"
|
||||
:game-initialized="gameInitialized"
|
||||
:closable="canCloseMenu"
|
||||
@close="handleMenuClose"
|
||||
@llm-ready="handleLLMReady"
|
||||
/>
|
||||
</div>
|
||||
</n-message-provider>
|
||||
@@ -316,4 +385,4 @@ watch([showMenu, isManualPaused], ([menuVisible, manualPaused]) => {
|
||||
flex-direction: column;
|
||||
z-index: 20;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -66,6 +66,24 @@ export interface LLMConfigDTO {
|
||||
mode: string;
|
||||
}
|
||||
|
||||
export interface GameStartConfigDTO {
|
||||
init_npc_num: number;
|
||||
sect_num: number;
|
||||
protagonist: string;
|
||||
npc_awakening_rate_per_month: number;
|
||||
}
|
||||
|
||||
export interface CurrentConfigDTO {
|
||||
game: {
|
||||
init_npc_num: number;
|
||||
sect_num: number;
|
||||
npc_awakening_rate_per_month: number;
|
||||
};
|
||||
avatar: {
|
||||
protagonist: string;
|
||||
};
|
||||
}
|
||||
|
||||
// --- Events Pagination ---
|
||||
|
||||
export interface EventDTO {
|
||||
@@ -117,7 +135,8 @@ export const gameApi = {
|
||||
},
|
||||
|
||||
fetchAvatarMeta() {
|
||||
return httpClient.get<{ males: number[]; females: number[] }>('/api/meta/avatars');
|
||||
// Add timestamp to prevent caching
|
||||
return httpClient.get<{ males: number[]; females: number[] }>(`/api/meta/avatars?t=${Date.now()}`);
|
||||
},
|
||||
|
||||
fetchPhenomenaList() {
|
||||
@@ -205,6 +224,10 @@ export const gameApi = {
|
||||
saveLLMConfig(config: LLMConfigDTO) {
|
||||
return httpClient.post<{ status: string; message: string }>('/api/config/llm/save', config);
|
||||
},
|
||||
|
||||
fetchLLMStatus() {
|
||||
return httpClient.get<{ configured: boolean }>('/api/config/llm/status');
|
||||
},
|
||||
|
||||
// --- Events Pagination ---
|
||||
|
||||
@@ -233,10 +256,21 @@ export const gameApi = {
|
||||
},
|
||||
|
||||
startNewGame() {
|
||||
// Legacy: replaced by startGame logic usually, but kept for compatibility if needed
|
||||
return httpClient.post<{ status: string; message: string }>('/api/game/new', {});
|
||||
},
|
||||
|
||||
reinitGame() {
|
||||
return httpClient.post<{ status: string; message: string }>('/api/control/reinit', {});
|
||||
},
|
||||
|
||||
// --- Game Start Config ---
|
||||
|
||||
fetchCurrentConfig() {
|
||||
return httpClient.get<CurrentConfigDTO>('/api/config/current');
|
||||
},
|
||||
|
||||
startGame(config: GameStartConfigDTO) {
|
||||
return httpClient.post<{ status: string; message: string }>('/api/game/start', config);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,17 +42,19 @@ const tips = [
|
||||
'天灵根角色在任何洞府修行,都事半功倍',
|
||||
'改变天地灵机,不仅会影响加成,还会微妙调整角色行事风格',
|
||||
'偶尔会有修仙小说中的主角穿越进此方世界',
|
||||
'每个角色都有自己的思考和情绪',
|
||||
'每个角色都有自己的真实思考和情绪',
|
||||
'除了修炼,炼丹和练气也很重要',
|
||||
'参加拍卖会可能捡漏,但要小心恶人的衔尾追杀',
|
||||
'江湖同道会根据你的行为取一个绰号',
|
||||
'双修虽好,还请克制',
|
||||
'在宗门驻地回血,可以回满HP',
|
||||
'境界之间,战力差距极大,越阶挑战难于登天',
|
||||
'不同境界之间,战力差距极大,越阶挑战难于登天',
|
||||
'天命之子特质的角色,好运连连,奇遇不断',
|
||||
'现代世界的穿越者,只想回到现实世界,但这是不可能的',
|
||||
'丹药有生效的时间限制',
|
||||
'由于大模型需要思考,游戏启动可能耗时较久',
|
||||
'模拟世界对大模型token消耗较大,请注意',
|
||||
'设定开局历史,世界也会随之而改变',
|
||||
]
|
||||
|
||||
const currentTip = ref(tips[Math.floor(Math.random() * tips.length)])
|
||||
|
||||
@@ -4,17 +4,21 @@ import SaveLoadPanel from './game/panels/system/SaveLoadPanel.vue'
|
||||
import CreateAvatarPanel from './game/panels/system/CreateAvatarPanel.vue'
|
||||
import DeleteAvatarPanel from './game/panels/system/DeleteAvatarPanel.vue'
|
||||
import LLMConfigPanel from './game/panels/system/LLMConfigPanel.vue'
|
||||
import GameStartPanel from './game/panels/system/GameStartPanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
defaultTab?: 'save' | 'load' | 'create' | 'delete' | 'llm'
|
||||
defaultTab?: 'save' | 'load' | 'create' | 'delete' | 'llm' | 'start'
|
||||
gameInitialized: boolean
|
||||
closable?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void
|
||||
(e: 'llm-ready'): void
|
||||
}>()
|
||||
|
||||
const activeTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm'>(props.defaultTab || 'load')
|
||||
const activeTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm' | 'start'>(props.defaultTab || 'load')
|
||||
|
||||
function switchTab(tab: typeof activeTab.value) {
|
||||
activeTab.value = tab
|
||||
@@ -40,10 +44,18 @@ watch(() => props.visible, (val) => {
|
||||
<div class="system-menu">
|
||||
<div class="menu-header">
|
||||
<h2>系统菜单</h2>
|
||||
<button class="close-btn" @click="emit('close')">×</button>
|
||||
<!-- 只有在游戏未开始且处于 start/llm 界面时才可能无法关闭(如果是强制引导) -->
|
||||
<!-- 但为了用户体验,通常保留关闭按钮,用户如果没配置好就关闭,也只是回到 idle 状态的空界面 -->
|
||||
<button class="close-btn" @click="emit('close')" v-if="closable !== false">×</button>
|
||||
</div>
|
||||
|
||||
<div class="menu-tabs">
|
||||
<button
|
||||
:class="{ active: activeTab === 'start' }"
|
||||
@click="switchTab('start')"
|
||||
>
|
||||
开始游戏
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: activeTab === 'load' }"
|
||||
@click="switchTab('load')"
|
||||
@@ -53,18 +65,21 @@ watch(() => props.visible, (val) => {
|
||||
<button
|
||||
:class="{ active: activeTab === 'save' }"
|
||||
@click="switchTab('save')"
|
||||
:disabled="!gameInitialized"
|
||||
>
|
||||
保存游戏
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: activeTab === 'create' }"
|
||||
@click="switchTab('create')"
|
||||
:disabled="!gameInitialized"
|
||||
>
|
||||
新建角色
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: activeTab === 'delete' }"
|
||||
@click="switchTab('delete')"
|
||||
:disabled="!gameInitialized"
|
||||
>
|
||||
删除角色
|
||||
</button>
|
||||
@@ -77,8 +92,13 @@ watch(() => props.visible, (val) => {
|
||||
</div>
|
||||
|
||||
<div class="menu-content">
|
||||
<GameStartPanel
|
||||
v-if="activeTab === 'start'"
|
||||
:readonly="gameInitialized"
|
||||
/>
|
||||
|
||||
<SaveLoadPanel
|
||||
v-if="activeTab === 'save' || activeTab === 'load'"
|
||||
v-else-if="activeTab === 'save' || activeTab === 'load'"
|
||||
:mode="activeTab === 'save' ? 'save' : 'load'"
|
||||
@close="emit('close')"
|
||||
/>
|
||||
@@ -87,12 +107,13 @@ watch(() => props.visible, (val) => {
|
||||
v-else-if="activeTab === 'create'"
|
||||
@created="switchTab('create')"
|
||||
/>
|
||||
<!-- Note: @created callback could switch to list or stay,
|
||||
currently it stays to allow creating more or just refreshes internal list -->
|
||||
|
||||
<DeleteAvatarPanel v-else-if="activeTab === 'delete'" />
|
||||
|
||||
<LLMConfigPanel v-else-if="activeTab === 'llm'" />
|
||||
<LLMConfigPanel
|
||||
v-else-if="activeTab === 'llm'"
|
||||
@config-saved="emit('llm-ready')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,10 +186,15 @@ watch(() => props.visible, (val) => {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.menu-tabs button:hover {
|
||||
.menu-tabs button:hover:not(:disabled) {
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.menu-tabs button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.menu-tabs button.active {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
|
||||
@@ -32,19 +32,46 @@ export function useTextures() {
|
||||
|
||||
// 基础纹理加载(地图块、角色)
|
||||
const loadBaseTextures = async () => {
|
||||
if (isLoaded.value) return
|
||||
|
||||
// Load Avatar Meta first
|
||||
// 1. 获取最新的 Avatar Meta 并检查是否有变化
|
||||
let metaChanged = false
|
||||
try {
|
||||
const meta = await gameApi.fetchAvatarMeta()
|
||||
if (meta.males) availableAvatars.value.males = meta.males
|
||||
if (meta.females) availableAvatars.value.females = meta.females
|
||||
console.log('Avatar meta loaded:', availableAvatars.value)
|
||||
|
||||
// 对比当前缓存的列表和新获取的列表
|
||||
const newMalesStr = JSON.stringify(meta.males || [])
|
||||
const curMalesStr = JSON.stringify(availableAvatars.value.males)
|
||||
if (meta.males && newMalesStr !== curMalesStr) {
|
||||
availableAvatars.value.males = meta.males
|
||||
metaChanged = true
|
||||
}
|
||||
|
||||
const newFemalesStr = JSON.stringify(meta.females || [])
|
||||
const curFemalesStr = JSON.stringify(availableAvatars.value.females)
|
||||
if (meta.females && newFemalesStr !== curFemalesStr) {
|
||||
availableAvatars.value.females = meta.females
|
||||
metaChanged = true
|
||||
}
|
||||
|
||||
if (metaChanged) {
|
||||
console.log('Avatar meta updated:', availableAvatars.value)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load avatar meta, using default range', e)
|
||||
// Fallback
|
||||
availableAvatars.value.males = Array.from({length: 47}, (_, i) => i + 1)
|
||||
availableAvatars.value.females = Array.from({length: 41}, (_, i) => i + 1)
|
||||
// Fallback: 只有在列表为空时才使用默认值
|
||||
if (availableAvatars.value.males.length === 0) {
|
||||
availableAvatars.value.males = Array.from({length: 47}, (_, i) => i + 1)
|
||||
availableAvatars.value.females = Array.from({length: 41}, (_, i) => i + 1)
|
||||
metaChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果已经加载过,且元数据没有变化,则跳过
|
||||
// 注意:如果 metaChanged 为 true,即使 isLoaded 为 true 也要重新执行加载逻辑(Pixi Assets 会处理去重)
|
||||
if (isLoaded.value && !metaChanged) {
|
||||
// Double check if textures are actually loaded for current avatars
|
||||
// This handles the case where meta didn't change (e.g. was fallback) but textures weren't loaded
|
||||
const missingTexture = availableAvatars.value.males.some(id => !textures.value[`male_${id}`])
|
||||
if (!missingTexture) return
|
||||
}
|
||||
|
||||
const manifest: Record<string, string> = {
|
||||
|
||||
152
web/src/components/game/panels/system/GameStartPanel.vue
Normal file
152
web/src/components/game/panels/system/GameStartPanel.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { NForm, NFormItem, NInputNumber, NSelect, NButton, useMessage } from 'naive-ui'
|
||||
import { gameApi } from '../../../../api/game'
|
||||
|
||||
const props = defineProps<{
|
||||
readonly: boolean
|
||||
}>()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 配置表单数据
|
||||
const config = ref({
|
||||
init_npc_num: 12,
|
||||
sect_num: 3,
|
||||
protagonist: 'none',
|
||||
npc_awakening_rate_per_month: 0.01
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 选项
|
||||
const protagonistOptions = [
|
||||
{ label: '不引入主角', value: 'none' },
|
||||
{ label: '随机引入主角', value: 'random' },
|
||||
{ label: '全部引入主角', value: 'all' }
|
||||
]
|
||||
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await gameApi.fetchCurrentConfig()
|
||||
config.value = {
|
||||
init_npc_num: res.game.init_npc_num,
|
||||
sect_num: res.game.sect_num,
|
||||
protagonist: res.avatar.protagonist,
|
||||
npc_awakening_rate_per_month: res.game.npc_awakening_rate_per_month
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('加载配置失败')
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
try {
|
||||
loading.value = true
|
||||
await gameApi.startGame(config.value)
|
||||
message.success('配置已保存,正在初始化世界...')
|
||||
// 父组件会通过 polling 检测到状态变化,从而自动关闭菜单并显示 loading
|
||||
} catch (e) {
|
||||
message.error('开始游戏失败')
|
||||
console.error(e)
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="game-start-panel">
|
||||
<div class="panel-header">
|
||||
<h3>开始游戏</h3>
|
||||
<p class="description">设定世界的初始状态。注意:游戏开始后,这些设定将无法更改。</p>
|
||||
</div>
|
||||
|
||||
<n-form
|
||||
label-placement="left"
|
||||
label-width="160"
|
||||
require-mark-placement="right-hanging"
|
||||
:disabled="readonly"
|
||||
>
|
||||
<n-form-item label="初始修士数量" path="init_npc_num">
|
||||
<n-input-number v-model:value="config.init_npc_num" :min="0" :max="100" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="活跃宗门数量" path="sect_num">
|
||||
<n-input-number v-model:value="config.sect_num" :min="0" :max="10" />
|
||||
</n-form-item>
|
||||
<div class="tip-text" style="margin-top: -12px;">
|
||||
宗门数量建议少于角色数量的一半
|
||||
</div>
|
||||
|
||||
<n-form-item label="主角引入模式" path="protagonist">
|
||||
<n-select v-model:value="config.protagonist" :options="protagonistOptions" />
|
||||
</n-form-item>
|
||||
|
||||
<div class="tip-text" v-if="config.protagonist !== 'none'">
|
||||
<span v-if="config.protagonist === 'random'">
|
||||
随机引入:每次生成角色时,有 5% 的概率使用预设的“小说主角”模板。
|
||||
</span>
|
||||
<span v-if="config.protagonist === 'all'">
|
||||
全部引入:开局时强制生成所有预设的“小说主角”。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<n-form-item label="每月新生修士概率" path="npc_awakening_rate_per_month">
|
||||
<n-input-number
|
||||
v-model:value="config.npc_awakening_rate_per_month"
|
||||
:min="0"
|
||||
:max="1"
|
||||
:step="0.001"
|
||||
:format="(val: number) => `${(val * 100).toFixed(1)}%`"
|
||||
:parse="(val: string) => parseFloat(val) / 100"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<div class="actions" v-if="!readonly">
|
||||
<n-button type="primary" size="large" @click="startGame" :loading="loading">
|
||||
开始
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.game-start-panel {
|
||||
color: #eee;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
margin-bottom: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #888;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
margin-left: 160px; /* offset label width */
|
||||
margin-bottom: 24px;
|
||||
color: #aaa;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2em;
|
||||
}
|
||||
</style>
|
||||
@@ -69,6 +69,10 @@ function applyPreset(preset: typeof presets[0]) {
|
||||
message.info(`已应用 ${preset.name} 预设 (请填写 API Key)`)
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'config-saved'): void
|
||||
}>()
|
||||
|
||||
async function handleTestAndSave() {
|
||||
if (!config.value.api_key) {
|
||||
message.warning('请填写 API Key')
|
||||
@@ -88,6 +92,7 @@ async function handleTestAndSave() {
|
||||
// 2. 保存配置
|
||||
await gameApi.saveLLMConfig(config.value)
|
||||
message.success('配置已保存')
|
||||
emit('config-saved')
|
||||
} catch (e: any) {
|
||||
message.error('测试或保存失败: ' + (e.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user