From f4bcb73fe97b651f6711d871ab241d2b1aeab373 Mon Sep 17 00:00:00 2001 From: Zrief Date: Thu, 2 Oct 2025 22:44:17 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A1=E5=88=92=E8=A1=A8=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E3=80=81=E6=B7=BB=E5=8A=A0=E8=AE=A1=E5=88=92=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=8D=8F=E8=B0=83=E5=99=A8=E5=92=8C=E8=AE=A1=E5=88=92=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了usePlanDataCoordinator组件式函数(composable),用于实现计划数据的统一管理与同步。 添加了planNameUtils工具类,用于生成唯一计划名称并进行有效性校验。 重构了plan/index.vue和MaaPlanTable.vue两个页面组件,使其适配上述工具;同时优化了计划切换、名称编辑及自定义阶段管理的功能。 --- .../src/composables/usePlanDataCoordinator.ts | 531 ++++++++++ frontend/src/utils/planNameUtils.ts | 91 ++ frontend/src/views/plan/index.vue | 157 ++- .../src/views/plan/tables/MaaPlanTable.vue | 976 +++++------------- 4 files changed, 1022 insertions(+), 733 deletions(-) create mode 100644 frontend/src/composables/usePlanDataCoordinator.ts create mode 100644 frontend/src/utils/planNameUtils.ts diff --git a/frontend/src/composables/usePlanDataCoordinator.ts b/frontend/src/composables/usePlanDataCoordinator.ts new file mode 100644 index 0000000..1b08b98 --- /dev/null +++ b/frontend/src/composables/usePlanDataCoordinator.ts @@ -0,0 +1,531 @@ +/** + * 计划表数据协调层 + * + * 作为前端架构中的"交通指挥中心",负责: + * 1. 统一管理数据流 + * 2. 协调视图间的同步 + * 3. 处理与后端的通信 + * 4. 提供统一的数据访问接口 + */ + +import { ref, computed } from 'vue' +import type { MaaPlanConfig, MaaPlanConfig_Item } from '@/api' + +// 时间维度常量 +export const TIME_KEYS = ['ALL', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] as const +export type TimeKey = typeof TIME_KEYS[number] + +// 关卡槽位常量 +export const STAGE_SLOTS = ['Stage', 'Stage_1', 'Stage_2', 'Stage_3'] as const +export type StageSlot = typeof STAGE_SLOTS[number] + +// 统一的数据结构 +export interface PlanDataState { + // 基础信息 + info: { + name: string + mode: 'ALL' | 'Weekly' + type: string + } + + // 时间维度的配置数据 + timeConfigs: Record + + // 自定义关卡定义 + customStageDefinitions: { + custom_stage_1: string + custom_stage_2: string + custom_stage_3: string + } +} + +// 关卡可用性信息 +export interface StageAvailability { + value: string + text: string + days: number[] +} + +export const STAGE_DAILY_INFO: StageAvailability[] = [ + { value: '-', text: '当前/上次', days: [1, 2, 3, 4, 5, 6, 7] }, + { value: '1-7', text: '1-7', days: [1, 2, 3, 4, 5, 6, 7] }, + { value: 'R8-11', text: 'R8-11', days: [1, 2, 3, 4, 5, 6, 7] }, + { value: '12-17-HARD', text: '12-17-HARD', days: [1, 2, 3, 4, 5, 6, 7] }, + { value: 'LS-6', text: '经验-6/5', days: [1, 2, 3, 4, 5, 6, 7] }, + { value: 'CE-6', text: '龙门币-6/5', days: [2, 4, 6, 7] }, + { value: 'AP-5', text: '红票-5', days: [1, 4, 6, 7] }, + { value: 'CA-5', text: '技能-5', days: [2, 3, 5, 7] }, + { value: 'SK-5', text: '碳-5', days: [1, 3, 5, 6] }, + { value: 'PR-A-1', text: '奶/盾芯片', days: [1, 4, 5, 7] }, + { value: 'PR-A-2', text: '奶/盾芯片组', days: [1, 4, 5, 7] }, + { value: 'PR-B-1', text: '术/狙芯片', days: [1, 2, 5, 6] }, + { value: 'PR-B-2', text: '术/狙芯片组', days: [1, 2, 5, 6] }, + { value: 'PR-C-1', text: '先/辅芯片', days: [3, 4, 6, 7] }, + { value: 'PR-C-2', text: '先/辅芯片组', days: [3, 4, 6, 7] }, + { value: 'PR-D-1', text: '近/特芯片', days: [2, 3, 6, 7] }, + { value: 'PR-D-2', text: '近/特芯片组', days: [2, 3, 6, 7] }, +] + +/** + * 计划表数据协调器 + */ +export function usePlanDataCoordinator() { + // 当前计划表ID + const currentPlanId = ref('default') + + // localStorage 相关函数 + const CUSTOM_STAGE_KEY_PREFIX = 'maa_custom_stage_definitions_' + + const getStorageKey = (): string => { + return `${CUSTOM_STAGE_KEY_PREFIX}${currentPlanId.value}` + } + + const getDefaultCustomStageDefinitions = () => ({ + custom_stage_1: '', + custom_stage_2: '', + custom_stage_3: '', + }) + + const loadCustomStageDefinitionsFromStorage = () => { + try { + const storageKey = getStorageKey() + const stored = localStorage.getItem(storageKey) + if (stored) { + const parsed = JSON.parse(stored) + // 确保包含所有必需的键 + return { + custom_stage_1: parsed.custom_stage_1 || '', + custom_stage_2: parsed.custom_stage_2 || '', + custom_stage_3: parsed.custom_stage_3 || '', + } + } + } catch (error) { + console.error('[自定义关卡] localStorage 恢复失败:', error) + } + + return getDefaultCustomStageDefinitions() + } + + const saveCustomStageDefinitionsToStorage = (definitions: Record) => { + try { + const storageKey = getStorageKey() + localStorage.setItem(storageKey, JSON.stringify(definitions)) + // 只在开发环境输出详细日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[自定义关卡] 保存到 localStorage (${currentPlanId.value})`) + } + } catch (error) { + console.error('[自定义关卡] localStorage 保存失败:', error) + } + } + + // 单一数据源 + const planData = ref({ + info: { + name: '', + mode: 'ALL', + type: 'MaaPlanConfig' + }, + timeConfigs: {} as Record, + customStageDefinitions: loadCustomStageDefinitionsFromStorage() + }) + + // 初始化时间配置 + const initializeTimeConfigs = () => { + TIME_KEYS.forEach(timeKey => { + planData.value.timeConfigs[timeKey] = { + medicineNumb: 0, + seriesNumb: '0', + stages: { + primary: '-', + backup1: '-', + backup2: '-', + backup3: '-', + remain: '-' + } + } + }) + } + + // 初始化数据 + initializeTimeConfigs() + + // 从API数据转换为内部数据结构 + const fromApiData = (apiData: MaaPlanConfig) => { + // 更新基础信息 + if (apiData.Info) { + planData.value.info.name = apiData.Info.Name || '' + planData.value.info.mode = apiData.Info.Mode || 'ALL' + + // 如果API数据中包含计划表ID信息,更新当前planId + // 注意:这里假设planId通过其他方式传入,API数据本身可能不包含ID + } + + // 更新时间配置 + TIME_KEYS.forEach(timeKey => { + const timeData = apiData[timeKey] as MaaPlanConfig_Item + if (timeData) { + planData.value.timeConfigs[timeKey] = { + medicineNumb: timeData.MedicineNumb || 0, + seriesNumb: timeData.SeriesNumb || '0', + stages: { + primary: timeData.Stage || '-', + backup1: timeData.Stage_1 || '-', + backup2: timeData.Stage_2 || '-', + backup3: timeData.Stage_3 || '-', + remain: timeData.Stage_Remain || '-' + } + } + } + }) + + // 更新自定义关卡定义 + const customStages = (apiData.ALL as any)?.customStageDefinitions + if (customStages && typeof customStages === 'object') { + // 只在开发环境输出详细日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[自定义关卡] 从后端数据恢复 (${currentPlanId.value})`) + } + const newDefinitions = { + custom_stage_1: customStages.custom_stage_1 || '', + custom_stage_2: customStages.custom_stage_2 || '', + custom_stage_3: customStages.custom_stage_3 || '', + } + + // 只有当定义真的不同时才更新和保存 + const hasChanged = JSON.stringify(newDefinitions) !== JSON.stringify(planData.value.customStageDefinitions) + + if (hasChanged) { + planData.value.customStageDefinitions = newDefinitions + // 同步到 localStorage + saveCustomStageDefinitionsToStorage(planData.value.customStageDefinitions) + } + } else { + // 只在开发环境输出日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[自定义关卡] 使用 localStorage 数据 (${currentPlanId.value})`) + } + // 如果后端没有自定义关卡定义,使用 localStorage 中的值 + const storedDefinitions = loadCustomStageDefinitionsFromStorage() + planData.value.customStageDefinitions = storedDefinitions + } + } + + // 转换为API数据格式 + const toApiData = (): MaaPlanConfig => { + const result: MaaPlanConfig = { + Info: { + Name: planData.value.info.name, + Mode: planData.value.info.mode + } + } + + TIME_KEYS.forEach(timeKey => { + const config = planData.value.timeConfigs[timeKey] + result[timeKey] = { + MedicineNumb: config.medicineNumb, + SeriesNumb: config.seriesNumb as any, + Stage: config.stages.primary, + Stage_1: config.stages.backup1, + Stage_2: config.stages.backup2, + Stage_3: config.stages.backup3, + Stage_Remain: config.stages.remain + } + }) + + // 在ALL中包含自定义关卡定义 + if (result.ALL) { + (result.ALL as any).customStageDefinitions = planData.value.customStageDefinitions + } + + return result + } + + // 配置视图数据适配器 + const configViewData = computed(() => { + return [ + { + key: 'MedicineNumb', + taskName: '吃理智药', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.medicineNumb || 0 + ]) + ) + }, + { + key: 'SeriesNumb', + taskName: '连战次数', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.seriesNumb || '0' + ]) + ) + }, + { + key: 'Stage', + taskName: '关卡选择', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.stages.primary || '-' + ]) + ) + }, + { + key: 'Stage_1', + taskName: '备选关卡-1', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.stages.backup1 || '-' + ]) + ) + }, + { + key: 'Stage_2', + taskName: '备选关卡-2', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.stages.backup2 || '-' + ]) + ) + }, + { + key: 'Stage_3', + taskName: '备选关卡-3', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.stages.backup3 || '-' + ]) + ) + }, + { + key: 'Stage_Remain', + taskName: '剩余理智关卡', + ...Object.fromEntries( + TIME_KEYS.map(timeKey => [ + timeKey, + planData.value.timeConfigs[timeKey]?.stages.remain || '-' + ]) + ) + } + ] + }) + + // 简化视图数据适配器 + const simpleViewData = computed(() => { + const result: any[] = [] + + // 添加自定义关卡 + Object.entries(planData.value.customStageDefinitions).forEach(([, stageName]) => { + if (stageName && stageName.trim()) { + const stageStates: Record = {} + TIME_KEYS.forEach(timeKey => { + const config = planData.value.timeConfigs[timeKey] + stageStates[timeKey] = Object.values(config.stages).includes(stageName) + }) + + result.push({ + key: stageName, + taskName: stageName, + isCustom: true, + stageName: stageName, + ...stageStates + }) + } + }) + + // 添加标准关卡 + STAGE_DAILY_INFO.filter(stage => stage.value !== '-').forEach(stage => { + const stageStates: Record = {} + TIME_KEYS.forEach(timeKey => { + const config = planData.value.timeConfigs[timeKey] + stageStates[timeKey] = Object.values(config.stages).includes(stage.value) + }) + + result.push({ + key: stage.value, + taskName: stage.text, + isCustom: false, + stageName: stage.value, + ...stageStates + }) + }) + + return result + }) + + // 更新配置数据 + const updateConfig = (timeKey: TimeKey, field: string, value: any) => { + if (field === 'MedicineNumb') { + planData.value.timeConfigs[timeKey].medicineNumb = value + } else if (field === 'SeriesNumb') { + planData.value.timeConfigs[timeKey].seriesNumb = value + } else if (field === 'Stage') { + planData.value.timeConfigs[timeKey].stages.primary = value + } else if (field === 'Stage_1') { + planData.value.timeConfigs[timeKey].stages.backup1 = value + } else if (field === 'Stage_2') { + planData.value.timeConfigs[timeKey].stages.backup2 = value + } else if (field === 'Stage_3') { + planData.value.timeConfigs[timeKey].stages.backup3 = value + } else if (field === 'Stage_Remain') { + planData.value.timeConfigs[timeKey].stages.remain = value + } + } + + // 切换关卡状态(简化视图用) + const toggleStage = (stageName: string, timeKey: TimeKey, enabled: boolean) => { + const config = planData.value.timeConfigs[timeKey] + const stageSlots = ['primary', 'backup1', 'backup2', 'backup3'] as const + + if (enabled) { + // 找到第一个空槽位 + const emptySlot = stageSlots.find(slot => + !config.stages[slot] || config.stages[slot] === '-' + ) + if (emptySlot) { + config.stages[emptySlot] = stageName + } + // 启用后重新按简化视图顺序排列 + reassignSlotsBySimpleViewOrder(timeKey) + } else { + // 从所有槽位中移除 + stageSlots.forEach(slot => { + if (config.stages[slot] === stageName) { + config.stages[slot] = '-' + } + }) + // 移除后重新按简化视图顺序排列 + reassignSlotsBySimpleViewOrder(timeKey) + } + } + + // 按简化视图顺序重新分配槽位 + const reassignSlotsBySimpleViewOrder = (timeKey: TimeKey) => { + const config = planData.value.timeConfigs[timeKey] + const stageSlots = ['primary', 'backup1', 'backup2', 'backup3'] as const + + // 收集当前已启用的关卡 + const enabledStages = Object.values(config.stages).filter(stage => stage && stage !== '-') + + // 清空所有槽位 + stageSlots.forEach(slot => { + config.stages[slot] = '-' + }) + + // 按简化视图的实际显示顺序重新分配 + const sortedStages: string[] = [] + + // 1. 先添加自定义关卡(按 custom_stage_1, custom_stage_2, custom_stage_3 的顺序) + for (let i = 1; i <= 3; i++) { + const key = `custom_stage_${i}` as keyof typeof planData.value.customStageDefinitions + const stageName = planData.value.customStageDefinitions[key] + if (stageName && stageName.trim() && enabledStages.includes(stageName)) { + sortedStages.push(stageName) + } + } + + // 2. 再添加标准关卡(按STAGE_DAILY_INFO的顺序,跳过'-') + STAGE_DAILY_INFO.filter(stage => stage.value !== '-').forEach(stage => { + if (enabledStages.includes(stage.value)) { + sortedStages.push(stage.value) + } + }) + + // 3. 按顺序分配到槽位:第1个→primary,第2个→backup1,第3个→backup2,第4个→backup3 + sortedStages.forEach((stageName, index) => { + if (index < stageSlots.length) { + config.stages[stageSlots[index]] = stageName + } + }) + + // 只在开发环境输出排序日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[关卡排序] ${timeKey}:`, sortedStages.join(' → ')) + } + } + + + + // 更新自定义关卡定义 + const updateCustomStageDefinition = (index: 1 | 2 | 3, name: string) => { + const key = `custom_stage_${index}` as keyof typeof planData.value.customStageDefinitions + const oldName = planData.value.customStageDefinitions[key] + + // 只在开发环境输出详细日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[自定义关卡] 保存关卡-${index}: "${oldName}" -> "${name}"`) + } + + planData.value.customStageDefinitions[key] = name + + // 保存到 localStorage + saveCustomStageDefinitionsToStorage(planData.value.customStageDefinitions) + + // 如果名称改变了,需要更新所有引用 + if (oldName !== name) { + TIME_KEYS.forEach(timeKey => { + const config = planData.value.timeConfigs[timeKey] + Object.keys(config.stages).forEach(stageKey => { + if (config.stages[stageKey as keyof typeof config.stages] === oldName) { + config.stages[stageKey as keyof typeof config.stages] = name || '-' + } + }) + }) + } + } + + // 更新计划表ID + const updatePlanId = (newPlanId: string) => { + if (currentPlanId.value !== newPlanId) { + // 只在开发环境输出日志 + if (process.env.NODE_ENV === 'development') { + console.log(`[自定义关卡] 计划表切换: ${currentPlanId.value} -> ${newPlanId}`) + } + + currentPlanId.value = newPlanId + + // 重新加载自定义关卡定义 + const newDefinitions = loadCustomStageDefinitionsFromStorage() + planData.value.customStageDefinitions = newDefinitions + } + } + + return { + // 数据 + planData: planData.value, + + // 视图适配器 + configViewData, + simpleViewData, + + // 数据转换 + fromApiData, + toApiData, + + // 数据操作 + updateConfig, + toggleStage, + updateCustomStageDefinition, + updatePlanId, + + // 工具函数 + initializeTimeConfigs + } +} \ No newline at end of file diff --git a/frontend/src/utils/planNameUtils.ts b/frontend/src/utils/planNameUtils.ts new file mode 100644 index 0000000..b1f57cb --- /dev/null +++ b/frontend/src/utils/planNameUtils.ts @@ -0,0 +1,91 @@ +/** + * 计划表名称管理工具函数 + */ + +export interface PlanNameValidationResult { + isValid: boolean + message?: string +} + +/** + * 生成唯一的计划表名称(使用数字后缀) + * @param planType 计划表类型 + * @param existingNames 已存在的名称列表 + * @returns 唯一的计划表名称 + */ +export function generateUniquePlanName(planType: string, existingNames: string[]): string { + const baseNames = { + MaaPlanConfig: '新 MAA 计划表', + GeneralPlan: '新通用计划表', + CustomPlan: '新自定义计划表', + } as Record + + const baseName = baseNames[planType] || '新计划表' + + // 如果基础名称没有被使用,直接返回 + if (!existingNames.includes(baseName)) { + return baseName + } + + // 查找可用的编号 + let counter = 2 + let candidateName = `${baseName} ${counter}` + + while (existingNames.includes(candidateName)) { + counter++ + candidateName = `${baseName} ${counter}` + } + + return candidateName +} + +/** + * 验证计划表名称是否可用 + * @param newName 新名称 + * @param existingNames 已存在的名称列表 + * @param currentName 当前名称(编辑时排除自己) + * @returns 验证结果 + */ +export function validatePlanName( + newName: string, + existingNames: string[], + currentName?: string +): PlanNameValidationResult { + // 检查名称是否为空 + if (!newName || !newName.trim()) { + return { isValid: false, message: '计划表名称不能为空' } + } + + const trimmedName = newName.trim() + + // 检查名称长度 + if (trimmedName.length > 50) { + return { isValid: false, message: '计划表名称不能超过50个字符' } + } + + // 检查是否与其他计划表重名(排除当前名称) + const isDuplicate = existingNames.some(name => + name === trimmedName && name !== currentName + ) + + if (isDuplicate) { + return { isValid: false, message: '计划表名称已存在,请使用其他名称' } + } + + return { isValid: true } +} + +/** + * 获取计划表类型的显示标签 + * @param planType 计划表类型 + * @returns 显示标签 + */ +export function getPlanTypeLabel(planType: string): string { + const labels = { + MaaPlanConfig: 'MAA计划表', + GeneralPlan: '通用计划表', + CustomPlan: '自定义计划表', + } as Record + + return labels[planType] || '计划表' +} \ No newline at end of file diff --git a/frontend/src/views/plan/index.vue b/frontend/src/views/plan/index.vue index e7bccaa..a8cf929 100644 --- a/frontend/src/views/plan/index.vue +++ b/frontend/src/views/plan/index.vue @@ -57,6 +57,7 @@ :current-mode="currentMode" :view-mode="viewMode" :options-loaded="!loading" + :plan-id="activePlanId" @update-table-data="handleTableDataUpdate" /> @@ -70,6 +71,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' import { useRoute } from 'vue-router' import { message } from 'ant-design-vue' import { usePlanApi } from '@/composables/usePlanApi' +import { generateUniquePlanName, validatePlanName, getPlanTypeLabel } from '@/utils/planNameUtils' import PlanHeader from './components/PlanHeader.vue' import PlanSelector from './components/PlanSelector.vue' import PlanConfig from './components/PlanConfig.vue' @@ -100,6 +102,7 @@ const viewMode = ref<'config' | 'simple'>('config') const isEditingPlanName = ref(false) const loading = ref(true) +const switching = ref(false) // 添加切换状态 // Use a record to match child component expectations const tableData = ref>({}) @@ -128,13 +131,18 @@ const debounce = any>(func: T, wait: number): T = const handleAddPlan = async (planType: string = 'MaaPlanConfig') => { try { const response = await createPlan(planType) - const defaultName = getDefaultPlanName(planType) - const newPlan = { id: response.planId, name: defaultName, type: planType } + const uniqueName = getDefaultPlanName(planType) + const newPlan = { id: response.planId, name: uniqueName, type: planType } planList.value.push(newPlan) activePlanId.value = newPlan.id - currentPlanName.value = defaultName + currentPlanName.value = uniqueName await loadPlanData(newPlan.id) - message.info(`已创建新的${getPlanTypeLabel(planType)},建议您修改为更有意义的名称`, 3) + // 如果生成的名称包含数字,说明有重名,提示用户 + if (uniqueName.match(/\s\d+$/)) { + message.info(`已创建新的${getPlanTypeLabel(planType)}:"${uniqueName}",建议您修改为更有意义的名称`, 4) + } else { + message.success(`已创建新的${getPlanTypeLabel(planType)}:"${uniqueName}"`) + } } catch (error) { console.error('添加计划失败:', error) } @@ -186,6 +194,7 @@ const saveInBackground = async (planId: string) => { const planData: Record = { ...(tableData.value || {}) } planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType } + console.log(`[计划表] 保存数据 (${planId}):`, planData) await updatePlan(planId, planData) } catch (error) { console.error('后台保存计划数据失败:', error) @@ -214,20 +223,27 @@ const handleSave = async () => { await debouncedSave() } -// 优化计划切换逻辑 +// 优化计划切换逻辑 - 异步保存,立即切换 const onPlanChange = async (planId: string) => { if (planId === activePlanId.value) return - // 触发当前计划的异步保存,但不等待完成 - if (activePlanId.value) { - saveInBackground(activePlanId.value).catch(error => { - console.warn('切换时保存当前计划失败:', error) - }) - } + switching.value = true + try { + // 异步保存当前计划,不等待完成 + if (activePlanId.value) { + saveInBackground(activePlanId.value).catch(error => { + console.error('切换时保存当前计划失败:', error) + message.warning('保存当前计划时出现问题,请检查数据是否完整') + }) + } - // 立即切换到新计划 - activePlanId.value = planId - await loadPlanData(planId) + // 立即切换到新计划,提升响应速度 + console.log(`[计划表] 切换到新计划: ${planId}`) + activePlanId.value = planId + await loadPlanData(planId) + } finally { + switching.value = false + } } const startEditPlanName = () => { @@ -242,13 +258,29 @@ const startEditPlanName = () => { } const finishEditPlanName = () => { - isEditingPlanName.value = false if (activePlanId.value) { const currentPlan = planList.value.find(plan => plan.id === activePlanId.value) if (currentPlan) { - currentPlan.name = currentPlanName.value || getDefaultPlanName(currentPlan.type) + const newName = currentPlanName.value?.trim() || '' + const existingNames = planList.value.map(plan => plan.name) + + // 验证新名称 + const validation = validatePlanName(newName, existingNames, currentPlan.name) + + if (!validation.isValid) { + // 如果验证失败,显示错误消息并恢复原名称 + message.error(validation.message || '计划表名称无效') + currentPlanName.value = currentPlan.name + } else { + // 如果验证成功,更新名称 + currentPlan.name = newName + currentPlanName.value = newName + // 触发保存操作,确保名称被保存到后端 + handleSave() + } } } + isEditingPlanName.value = false } const onModeChange = () => { @@ -263,20 +295,41 @@ const handleTableDataUpdate = async (newData: Record) => { const loadPlanData = async (planId: string) => { try { - const response = await getPlans(planId) - currentPlanData.value = response.data - if (response.data && response.data[planId]) { - const planData = response.data[planId] as PlanData + // 优化:先检查缓存数据,避免不必要的API调用 + let planData: PlanData | null = null + + if (currentPlanData.value && currentPlanData.value[planId]) { + planData = currentPlanData.value[planId] as PlanData + console.log(`[计划表] 使用缓存数据 (${planId})`) + } else { + const response = await getPlans(planId) + currentPlanData.value = response.data + planData = response.data[planId] as PlanData + console.log(`[计划表] 加载新数据 (${planId})`) + } + + if (planData) { if (planData.Info) { const apiName = planData.Info.Name || '' - if (!apiName && !currentPlanName.value) { - const currentPlan = planList.value.find(plan => plan.id === planId) - if (currentPlan) currentPlanName.value = currentPlan.name + const currentPlan = planList.value.find(plan => plan.id === planId) + + // 优先使用planList中的名称 + if (currentPlan && currentPlan.name) { + currentPlanName.value = currentPlan.name + + if (apiName !== currentPlan.name) { + console.log(`[计划表] 同步名称: API="${apiName}" -> planList="${currentPlan.name}"`) + } } else if (apiName) { currentPlanName.value = apiName + if (currentPlan) { + currentPlan.name = apiName + } } + currentMode.value = planData.Info.Mode || 'ALL' } + tableData.value = planData } } catch (error) { @@ -288,17 +341,47 @@ const initPlans = async () => { try { const response = await getPlans() if (response.index && response.index.length > 0) { + // 优化:预先收集所有名称,避免O(n²)复杂度 + const allPlanNames: string[] = [] + planList.value = response.index.map((item: any) => { const planId = item.uid const planData = response.data[planId] const planType = item.type - const planName = planData?.Info?.Name || getDefaultPlanName(planType) + let planName = planData?.Info?.Name || '' + + // 如果API中没有名称,或者名称是默认的模板名称,则生成唯一名称 + if (!planName || planName === '新 MAA 计划表' || planName === '新通用计划表' || planName === '新自定义计划表') { + planName = generateUniquePlanName(planType, allPlanNames) + } + + allPlanNames.push(planName) return { id: planId, name: planName, type: planType } }) + const queryPlanId = (route.query.planId as string) || '' const target = queryPlanId ? planList.value.find(p => p.id === queryPlanId) : null - activePlanId.value = target ? target.id : planList.value[0].id - await loadPlanData(activePlanId.value) + const selectedPlanId = target ? target.id : planList.value[0].id + + // 优化:直接使用已获取的数据,避免重复API调用 + activePlanId.value = selectedPlanId + const planData = response.data[selectedPlanId] + if (planData) { + currentPlanData.value = response.data + + // 直接设置数据,避免loadPlanData的重复调用 + const selectedPlan = planList.value.find(plan => plan.id === selectedPlanId) + if (selectedPlan) { + currentPlanName.value = selectedPlan.name + } + + if (planData.Info) { + currentMode.value = planData.Info.Mode || 'ALL' + } + + console.log(`[计划表] 初始加载数据 (${selectedPlanId}):`, planData) + tableData.value = planData + } } else { currentPlanData.value = null } @@ -310,22 +393,12 @@ const initPlans = async () => { } } -const getDefaultPlanName = (planType: string) => - ( - ({ - MaaPlanConfig: '新 MAA 计划表', - GeneralPlan: '新通用计划表', - CustomPlan: '新自定义计划表', - }) as Record - )[planType] || '新计划表' -const getPlanTypeLabel = (planType: string) => - ( - ({ - MaaPlanConfig: 'MAA计划表', - GeneralPlan: '通用计划表', - CustomPlan: '自定义计划表', - }) as Record - )[planType] || '计划表' +const getDefaultPlanName = (planType: string) => { + // 保持原来的逻辑,但添加重名检测 + const existingNames = planList.value.map(plan => plan.name) + return generateUniquePlanName(planType, existingNames) +} +// getPlanTypeLabel 现在从 @/utils/planNameUtils 导入,删除本地定义 watch( () => [currentPlanName.value, currentMode.value], diff --git a/frontend/src/views/plan/tables/MaaPlanTable.vue b/frontend/src/views/plan/tables/MaaPlanTable.vue index 12e6847..407dd9b 100644 --- a/frontend/src/views/plan/tables/MaaPlanTable.vue +++ b/frontend/src/views/plan/tables/MaaPlanTable.vue @@ -1,15 +1,43 @@