feat(plan):优化计划保存与切换逻辑,提升性能与用户体验- 在计划组件中引入防抖机制,避免频繁保存操作
- 实现异步保存队列,确保计划切换时数据不丢失 - 优化计划切换逻辑,支持后台保存并提升响应速度 - 在组件卸载前确保所有 pending 保存操作完成 - 修复 MaaPlanTable 中响应式丢失问题,优化选项缓存逻辑 - 为 PlanSelector 添加点击防抖,防止重复触发计划切换- 重构数据同步逻辑,提高表格与计划数据的同步效率
This commit is contained in:
@@ -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> = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user