feat(plan):优化计划保存与切换逻辑,提升性能与用户体验- 在计划组件中引入防抖机制,避免频繁保存操作

- 实现异步保存队列,确保计划切换时数据不丢失
- 优化计划切换逻辑,支持后台保存并提升响应速度
- 在组件卸载前确保所有 pending 保存操作完成
- 修复 MaaPlanTable 中响应式丢失问题,优化选项缓存逻辑
- 为 PlanSelector 添加点击防抖,防止重复触发计划切换- 重构数据同步逻辑,提高表格与计划数据的同步效率
This commit is contained in:
MoeSnowyFox
2025-09-21 23:56:27 +08:00
parent 4f1e49ce85
commit 32df65fb65
3 changed files with 219 additions and 93 deletions

View File

@@ -18,7 +18,7 @@
:key="plan.id"
:type="activePlanId === plan.id ? 'primary' : 'default'"
size="large"
@click="$emit('plan-change', plan.id)"
@click="handlePlanClick(plan.id)"
class="plan-button"
>
<span class="plan-name">{{ plan.name }}</span>
@@ -33,6 +33,7 @@
</template>
<script setup lang="ts">
interface Plan {
id: string
name: string
@@ -48,8 +49,22 @@ interface Emits {
(e: 'plan-change', planId: string): void
}
defineProps<Props>()
defineEmits<Emits>()
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
// 防抖点击处理
const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): T => {
let timeout: NodeJS.Timeout | null = null
return ((...args: any[]) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}) as T
}
const handlePlanClick = debounce((planId: string) => {
if (planId === props.activePlanId) return
emit('plan-change', planId)
}, 100)
const getPlanTypeLabel = (planType: string) => {
const labelMap: Record<string, string> = {

View File

@@ -63,7 +63,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, watch } from 'vue'
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'
@@ -112,6 +112,15 @@ const currentTableComponent = computed(() => {
}
})
// 添加防抖工具函数
const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): T => {
let timeout: NodeJS.Timeout | null = null
return ((...args: any[]) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}) as T
}
const handleAddPlan = async (planType: string = 'MaaPlan') => {
try {
const response = await createPlan(planType)
@@ -147,7 +156,72 @@ const handleRemovePlan = async (planId: string) => {
}
}
// 添加异步保存队列和状态管理
const savingQueue = ref(new Set<string>())
const savePromises = ref(new Map<string, Promise<void>>())
// 异步保存函数
const saveInBackground = async (planId: string) => {
// 如果已经在保存队列中,等待现有的保存完成
if (savingQueue.value.has(planId)) {
const existingPromise = savePromises.value.get(planId)
if (existingPromise) {
await existingPromise
}
return
}
savingQueue.value.add(planId)
const savePromise = (async () => {
try {
const currentPlan = planList.value.find(plan => plan.id === planId)
const planType = currentPlan?.type || 'MaaPlan'
// Start from existing tableData, then overwrite Info explicitly
const planData: Record<string, any> = { ...(tableData.value || {}) }
planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType }
await updatePlan(planId, planData)
} catch (error) {
console.error('后台保存计划数据失败:', error)
// 不显示错误消息,避免打断用户操作
} finally {
savingQueue.value.delete(planId)
savePromises.value.delete(planId)
}
})()
savePromises.value.set(planId, savePromise)
return savePromise
}
// 防抖保存函数
const debouncedSave = debounce(async () => {
if (!activePlanId.value) return
await saveInBackground(activePlanId.value)
}, 300)
const handleSave = async () => {
if (!activePlanId.value) {
message.warning('请先选择一个计划')
return
}
await debouncedSave()
}
// 优化计划切换逻辑
const onPlanChange = async (planId: string) => {
if (planId === activePlanId.value) return
// 触发当前计划的异步保存,但不等待完成
if (activePlanId.value) {
saveInBackground(activePlanId.value).catch(error => {
console.warn('切换时保存当前计划失败:', error)
})
}
// 立即切换到新计划
activePlanId.value = planId
await loadPlanData(planId)
}
@@ -232,35 +306,24 @@ const initPlans = async () => {
}
}
const savePlanData = async () => {
if (!activePlanId.value) return
const savePlanData = async (planId?: string) => {
const targetPlanId = planId || activePlanId.value
if (!targetPlanId) return
try {
const currentPlan = planList.value.find(plan => plan.id === activePlanId.value)
const currentPlan = planList.value.find(plan => plan.id === targetPlanId)
const planType = currentPlan?.type || 'MaaPlan'
// Start from existing tableData, then overwrite Info explicitly
const planData: Record<string, any> = { ...(tableData.value || {}) }
planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType }
await updatePlan(activePlanId.value, planData)
await updatePlan(targetPlanId, planData)
} catch (error) {
console.error('保存计划数据失败:', error)
throw error
}
}
const handleSave = async () => {
if (!activePlanId.value) {
message.warning('请先选择一个计划')
return
}
try {
await savePlanData()
} catch (error) {
message.error('保存失败')
}
}
const getDefaultPlanName = (planType: string) =>
(
({
@@ -282,7 +345,7 @@ watch(
() => [currentPlanName.value, currentMode.value],
async () => {
await nextTick()
handleSave()
await debouncedSave()
}
)
@@ -298,8 +361,24 @@ watch(
}
)
// 在组件卸载前确保所有保存操作完成
const ensureAllSaved = async () => {
const pendingPromises = Array.from(savePromises.value.values())
if (pendingPromises.length > 0) {
await Promise.allSettled(pendingPromises)
}
}
onMounted(() => {
initPlans()
// 监听页面卸载
window.addEventListener('beforeunload', ensureAllSaved)
})
onUnmounted(() => {
window.removeEventListener('beforeunload', ensureAllSaved)
ensureAllSaved()
})
</script>

View File

@@ -178,7 +178,7 @@
</template>
<script setup lang="ts">
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { computed, defineComponent, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
@@ -219,6 +219,7 @@ const VNodeRenderer = defineComponent({
},
})
// 改回使用普通的ref确保响应式正常工作
const rows = ref<TableRow[]>([
{
key: 'MedicineNumb',
@@ -411,38 +412,40 @@ const addCustomStage = (rowKey: string, columnKey: string) => {
message.success('关卡添加成功')
}
// 缓存计算属性,避免重复计算
const stageOptionsCache = ref(new Map<string, any[]>())
const stageOptions = computed(() => {
const baseOptions = STAGE_DAILY_INFO.map(stage => ({
label: stage.text,
value: stage.value,
isCustom: false,
}))
const customOptions = Object.keys(customStageNames.value).map(key => ({
label: customStageNames.value[key],
value: key,
isCustom: true,
}))
return [...baseOptions, ...customOptions]
const cacheKey = 'base_stage_options'
if (!stageOptionsCache.value.has(cacheKey)) {
const baseOptions = STAGE_DAILY_INFO.map(stage => ({
label: stage.text,
value: stage.value,
isCustom: false,
}))
const customOptions = Object.keys(customStageNames.value).map(key => ({
label: customStageNames.value[key],
value: key,
isCustom: true,
}))
stageOptionsCache.value.set(cacheKey, [...baseOptions, ...customOptions])
}
return stageOptionsCache.value.get(cacheKey) || []
})
const isCustomStage = (value: string, columnKey: string) => {
if (!value || value === '-') return false
const dayNumber = getDayNumber(columnKey)
let availableStages: string[]
if (dayNumber === 0) {
availableStages = STAGE_DAILY_INFO.map(stage => stage.value)
} else {
availableStages = STAGE_DAILY_INFO.filter(stage => stage.days.includes(dayNumber)).map(
stage => stage.value
)
}
return !availableStages.includes(value)
}
// 优化getSelectOptions函数添加缓存
const getSelectOptions = (columnKey: string, taskName: string, currentValue?: string) => {
const cacheKey = `${columnKey}_${taskName}_${currentValue || ''}`
if (stageOptionsCache.value.has(cacheKey)) {
return stageOptionsCache.value.get(cacheKey)
}
let options: any[] = []
switch (taskName) {
case '连战次数':
return [
options = [
{ label: 'AUTO', value: '0' },
{ label: '1', value: '1' },
{ label: '2', value: '2' },
@@ -452,6 +455,7 @@ const getSelectOptions = (columnKey: string, taskName: string, currentValue?: st
{ label: '6', value: '6' },
{ label: '不切换', value: '-1' },
]
break
case '关卡选择':
case '备选关卡-1':
case '备选关卡-2':
@@ -486,11 +490,30 @@ const getSelectOptions = (columnKey: string, taskName: string, currentValue?: st
value: customStageNames.value[key],
isCustom: true,
}))
return [...baseOptions, ...customOptions]
options = [...baseOptions, ...customOptions]
break
}
default:
return []
options = []
}
// 缓存结果
stageOptionsCache.value.set(cacheKey, options)
return options
}
const isCustomStage = (value: string, columnKey: string) => {
if (!value || value === '-') return false
const dayNumber = getDayNumber(columnKey)
let availableStages: string[]
if (dayNumber === 0) {
availableStages = STAGE_DAILY_INFO.map(stage => stage.value)
} else {
availableStages = STAGE_DAILY_INFO.filter(stage => stage.days.includes(dayNumber)).map(
stage => stage.value
)
}
return !availableStages.includes(value)
}
const getPlaceholder = (taskName: string) => {
@@ -549,9 +572,11 @@ const isStageEnabled = (stageValue: string, columnKey: string) => {
const toggleStage = (stageValue: string, columnKey: string, checked: boolean) => {
const stageSlots = ['Stage', 'Stage_1', 'Stage_2', 'Stage_3']
const newRows = [...rows.value]
if (checked) {
for (const slot of stageSlots) {
const row = rows.value.find(r => r.key === slot) as TableRow | undefined
const row = newRows.find(r => r.key === slot) as TableRow | undefined
if (row && ((row as any)[columnKey] === '-' || (row as any)[columnKey] === '')) {
;(row as any)[columnKey] = stageValue
break
@@ -559,12 +584,14 @@ const toggleStage = (stageValue: string, columnKey: string, checked: boolean) =>
}
} else {
for (const slot of stageSlots) {
const row = rows.value.find(r => r.key === slot) as TableRow | undefined
const row = newRows.find(r => r.key === slot) as TableRow | undefined
if (row && (row as any)[columnKey] === stageValue) {
;(row as any)[columnKey] = '-'
}
}
}
rows.value = newRows
}
const getSimpleTaskTagColor = (taskName: string) => {
@@ -626,29 +653,6 @@ const disableAllStages = (stageValue: string) => {
})
}
// 将 props.tableData 映射到 rows
const applyPlanDataToRows = (plan: Record<string, any> | null | undefined) => {
if (!plan) return
const timeKeys = [
'ALL',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
]
rows.value.forEach(row => {
const fieldKey = row.key
timeKeys.forEach(timeKey => {
if (plan[timeKey] && plan[timeKey][fieldKey] !== undefined) {
;(row as any)[timeKey] = plan[timeKey][fieldKey]
}
})
})
}
// 从 rows 组装为 API 所需结构
const buildPlanDataFromRows = (): Record<string, any> => {
const planData: Record<string, any> = {}
@@ -671,6 +675,48 @@ const buildPlanDataFromRows = (): Record<string, any> => {
return planData
}
// 优化数据同步,但保持响应式
const applyPlanDataToRows = (plan: Record<string, any> | null | undefined) => {
if (!plan) return
const timeKeys = [
'ALL',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
]
rows.value.forEach(row => {
const fieldKey = row.key
timeKeys.forEach(timeKey => {
if (plan[timeKey] && plan[timeKey][fieldKey] !== undefined) {
;(row as any)[timeKey] = plan[timeKey][fieldKey]
}
})
})
// 清除缓存以重新计算选项
stageOptionsCache.value.clear()
}
// 简化的防抖函数
const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): T => {
let timeout: NodeJS.Timeout | null = null
return ((...args: any[]) => {
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}) as T
}
const debouncedEmitUpdate = debounce(() => {
emit('update-table-data', buildPlanDataFromRows())
}, 150)
// 恢复正常的watch监听
watch(
() => props.tableData,
newVal => {
@@ -679,28 +725,14 @@ watch(
{ immediate: true, deep: true }
)
// 监听rows变化并触发更新
watch(
() =>
rows.value.map(r => ({
key: r.key,
ALL: r.ALL,
Monday: r.Monday,
Tuesday: r.Tuesday,
Wednesday: r.Wednesday,
Thursday: r.Thursday,
Friday: r.Friday,
Saturday: r.Saturday,
Sunday: r.Sunday,
})),
rows,
() => {
emit('update-table-data', buildPlanDataFromRows())
debouncedEmitUpdate()
},
{ deep: true }
)
onMounted(() => {
applyPlanDataToRows(props.tableData || {})
})
</script>
<style scoped>