feat(plan): 优化计划页面结构与加载逻辑

- 调整 template 结构,将加载状态与主要内容分离,提升可读性
- 引入 optionsLoaded 控制选项加载时机,避免初始渲染阻塞
- 增加 onMounted 和 watch逻辑确保异步数据正确处理- 统一计划类型为 MaaPlanConfig,兼容旧类型 MaaPlan- 优化 PlanHeader 新建计划按钮交互,支持动态显示按钮文本- 改进 PlanSelector 中计划类型标签显示逻辑,仅在必要时展示
This commit is contained in:
MoeSnowyFox
2025-09-23 00:02:38 +08:00
parent 4114e4ffc0
commit 35aa3cc42d
4 changed files with 173 additions and 88 deletions

View File

@@ -24,11 +24,11 @@
</a-menu-item> -->
</a-menu>
</template>
<a-button type="primary" size="large">
<a-button type="primary" size="large" @click="handleAddPlan">
<template #icon>
<PlusOutlined />
</template>
新建计划
{{ getPlanButtonText }}
<DownOutlined />
</a-button>
</a-dropdown>
@@ -54,6 +54,7 @@
<script setup lang="ts">
import { DeleteOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { ref, computed } from 'vue'
interface Plan {
id: string
@@ -68,15 +69,36 @@ interface Props {
interface Emits {
(e: 'add-plan', planType: string): void
(e: 'remove-plan', planId: string): void
}
defineProps<Props>()
const emit = defineEmits<Emits>()
// 默认计划类型
const selectedPlanType = ref('MaaPlan')
// 根据选择的计划类型获取按钮文本
const getPlanButtonText = computed(() => {
switch (selectedPlanType.value) {
case 'MaaPlan':
return '新建 MAA 计划'
case 'GeneralPlan':
return '新建通用计划'
case 'CustomPlan':
return '新建自定义计划'
default:
return '新建计划'
}
})
const handleMenuClick = ({ key }: { key: string }) => {
emit('add-plan', key)
selectedPlanType.value = key
}
// 点击主按钮创建计划
const handleAddPlan = () => {
emit('add-plan', selectedPlanType.value)
}
</script>
@@ -131,4 +153,4 @@ const handleMenuClick = ({ key }: { key: string }) => {
justify-content: center;
}
}
</style>
</style>

View File

@@ -22,7 +22,12 @@
class="plan-button"
>
<span class="plan-name">{{ plan.name }}</span>
<a-tag v-if="plan.type !== 'MaaPlan'" size="small" color="blue" class="plan-type-tag">
<a-tag
v-if="shouldShowPlanTypeTag(plan)"
size="small"
color="blue"
class="plan-type-tag"
>
{{ getPlanTypeLabel(plan.type) }}
</a-tag>
</a-button>
@@ -33,7 +38,6 @@
</template>
<script setup lang="ts">
interface Plan {
id: string
name: string
@@ -66,13 +70,37 @@ const handlePlanClick = debounce((planId: string) => {
emit('plan-change', planId)
}, 100)
// 判断是否需要显示计划类型标签
// 当所有计划的类型都相同时不显示标签,否则显示非默认类型的标签
const shouldShowPlanTypeTag = (plan: Plan) => {
// 如果只有一个计划或没有计划,不显示类型标签
if (props.planList.length <= 1) {
return false
}
// 检查是否所有计划的类型都相同
const firstType = props.planList[0].type
const allSameType = props.planList.every(p => p.type === firstType)
// 如果所有计划类型相同,则不显示任何标签
if (allSameType) {
return false
}
// 如果存在不同类型的计划则只显示非默认类型MaaPlanConfig的标签
const normalizedPlanType = plan.type === 'MaaPlan' ? 'MaaPlanConfig' : plan.type
return normalizedPlanType !== 'MaaPlanConfig'
}
const getPlanTypeLabel = (planType: string) => {
// 统一使用 MaaPlanConfig 作为默认类型
const normalizedPlanType = planType === 'MaaPlan' ? 'MaaPlanConfig' : planType
const labelMap: Record<string, string> = {
MaaPlan: 'MAA',
MaaPlanConfig: 'MAA',
GeneralPlan: '通用',
CustomPlan: '自定义',
}
return labelMap[planType] || planType
return labelMap[normalizedPlanType] || normalizedPlanType
}
</script>

View File

@@ -1,63 +1,66 @@
<template>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" tip="加载中,请稍候..." />
</div>
<!-- 主要内容 -->
<div v-else class="plans-main">
<!-- 页面头部 -->
<PlanHeader
:plan-list="planList"
:active-plan-id="activePlanId"
@add-plan="handleAddPlan"
@remove-plan="handleRemovePlan"
/>
<!-- 空状态 -->
<div v-if="!planList.length || !currentPlanData" class="empty-state">
<div class="empty-content">
<div class="empty-image-container">
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image" />
</div>
<div class="empty-text-content">
<h3 class="empty-title">暂无计划</h3>
<p class="empty-description">您还没有创建任何计划</p>
</div>
</div>
<div>
<div v-if="loading" class="loading-container">
<a-spin size="large" tip="加载中,请稍候..." />
</div>
<!-- 计划内容 -->
<div v-else class="plans-content">
<!-- 计划选择器 -->
<PlanSelector
<!-- 主要内容 -->
<div v-else class="plans-main">
<!-- 页面头部 -->
<PlanHeader
:plan-list="planList"
:active-plan-id="activePlanId"
@plan-change="onPlanChange"
@add-plan="handleAddPlan"
@remove-plan="handleRemovePlan"
/>
<!-- 计划配置 -->
<PlanConfig
:current-plan-name="currentPlanName"
:current-mode="currentMode"
:view-mode="viewMode"
:is-editing-plan-name="isEditingPlanName"
@update:current-plan-name="currentPlanName = $event"
@update:current-mode="currentMode = $event"
@update:view-mode="viewMode = $event"
@start-edit-plan-name="startEditPlanName"
@finish-edit-plan-name="finishEditPlanName"
@mode-change="onModeChange"
>
<!-- 动态渲染不同类型的表格 -->
<component
:is="currentTableComponent"
:table-data="tableData"
<!-- 空状态 -->
<div v-if="!planList.length || !currentPlanData" class="empty-state">
<div class="empty-content">
<div class="empty-image-container">
<img src="@/assets/NoData.png" alt="暂无数据" class="empty-image" />
</div>
<div class="empty-text-content">
<h3 class="empty-title">暂无计划</h3>
<p class="empty-description">您还没有创建任何计划</p>
</div>
</div>
</div>
<!-- 计划内容 -->
<div v-else class="plans-content">
<!-- 计划选择器 -->
<PlanSelector
:plan-list="planList"
:active-plan-id="activePlanId"
@plan-change="onPlanChange"
/>
<!-- 计划配置 -->
<PlanConfig
:current-plan-name="currentPlanName"
:current-mode="currentMode"
:view-mode="viewMode"
@update-table-data="handleTableDataUpdate"
/>
</PlanConfig>
:is-editing-plan-name="isEditingPlanName"
@update:current-plan-name="currentPlanName = $event"
@update:current-mode="currentMode = $event"
@update:view-mode="viewMode = $event"
@start-edit-plan-name="startEditPlanName"
@finish-edit-plan-name="finishEditPlanName"
@mode-change="onModeChange"
>
<!-- 动态渲染不同类型的表格 -->
<component
:is="currentTableComponent"
:table-data="tableData"
:current-mode="currentMode"
:view-mode="viewMode"
:options-loaded="!loading"
@update-table-data="handleTableDataUpdate"
/>
</PlanConfig>
</div>
</div>
</div>
</template>
@@ -103,9 +106,11 @@ const tableData = ref<Record<string, any>>({})
const currentTableComponent = computed(() => {
const currentPlan = planList.value.find(plan => plan.id === activePlanId.value)
const planType = currentPlan?.type || 'MaaPlan'
// 统一使用 MaaPlanConfig 作为默认类型
const planType =
currentPlan?.type === 'MaaPlan' ? 'MaaPlanConfig' : currentPlan?.type || 'MaaPlanConfig'
switch (planType) {
case 'MaaPlan':
case 'MaaPlanConfig':
return MaaPlanTable
default:
return MaaPlanTable
@@ -121,7 +126,7 @@ const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): T =
}) as T
}
const handleAddPlan = async (planType: string = 'MaaPlan') => {
const handleAddPlan = async (planType: string = 'MaaPlanConfig') => {
try {
const response = await createPlan(planType)
const defaultName = getDefaultPlanName(planType)
@@ -176,7 +181,7 @@ const saveInBackground = async (planId: string) => {
const savePromise = (async () => {
try {
const currentPlan = planList.value.find(plan => plan.id === planId)
const planType = currentPlan?.type || 'MaaPlan'
const planType = currentPlan?.type || 'MaaPlanConfig'
// Start from existing tableData, then overwrite Info explicitly
const planData: Record<string, any> = { ...(tableData.value || {}) }
@@ -287,7 +292,7 @@ const initPlans = async () => {
planList.value = response.index.map((item: any) => {
const planId = item.uid
const planData = response.data[planId]
const planType = planData?.Info?.Type || 'MaaPlan'
const planType = item.type === 'MaaPlan' ? 'MaaPlanConfig' : item.type || 'MaaPlanConfig'
const planName = planData?.Info?.Name || getDefaultPlanName(planType)
return { id: planId, name: planName, type: planType }
})
@@ -306,28 +311,10 @@ const initPlans = async () => {
}
}
const savePlanData = async (planId?: string) => {
const targetPlanId = planId || activePlanId.value
if (!targetPlanId) return
try {
const currentPlan = planList.value.find(plan => plan.id === targetPlanId)
const planType = currentPlan?.type || 'MaaPlan'
const planData: Record<string, any> = { ...(tableData.value || {}) }
planData.Info = { Mode: currentMode.value, Name: currentPlanName.value, Type: planType }
await updatePlan(targetPlanId, planData)
} catch (error) {
console.error('保存计划数据失败:', error)
throw error
}
}
const getDefaultPlanName = (planType: string) =>
(
({
MaaPlan: '新 MAA 计划表',
MaaPlanConfig: '新 MAA 计划表',
GeneralPlan: '新通用计划表',
CustomPlan: '新自定义计划表',
}) as Record<string, string>
@@ -335,7 +322,7 @@ const getDefaultPlanName = (planType: string) =>
const getPlanTypeLabel = (planType: string) =>
(
({
MaaPlan: 'MAA计划表',
MaaPlanConfig: 'MAA计划表',
GeneralPlan: '通用计划表',
CustomPlan: '自定义计划表',
}) as Record<string, string>

View File

@@ -178,7 +178,7 @@
</template>
<script setup lang="ts">
import { computed, defineComponent, ref, watch } from 'vue'
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
@@ -203,13 +203,44 @@ interface Props {
viewMode: 'config' | 'simple'
}
// 添加一个新的prop用于控制选项是否已加载
interface ExtendedProps extends Props {
optionsLoaded?: boolean
}
interface Emits {
(e: 'update-table-data', value: Record<string, any>): void
}
const props = defineProps<Props>()
const props = defineProps<ExtendedProps>()
const emit = defineEmits<Emits>()
// 添加一个响应式变量来跟踪选项是否已加载
const localOptionsLoaded = ref(false)
// 在组件挂载后延迟加载选项数据
onMounted(() => {
// 使用setTimeout延迟加载选项让表格先渲染出来
setTimeout(() => {
localOptionsLoaded.value = true
// 清除缓存以确保重新计算选项
stageOptionsCache.value.clear()
}, 0)
})
// 当tableData发生变化且有数据时确保选项已加载
watch(
() => props.tableData,
newData => {
if (newData && Object.keys(newData).length > 0 && !localOptionsLoaded.value) {
localOptionsLoaded.value = true
// 清除缓存以确保重新计算选项
stageOptionsCache.value.clear()
}
},
{ immediate: true }
)
// 渲染VNode的辅助组件
const VNodeRenderer = defineComponent({
name: 'VNodeRenderer',
@@ -415,7 +446,14 @@ const addCustomStage = (rowKey: string, columnKey: string) => {
// 缓存计算属性,避免重复计算
const stageOptionsCache = ref(new Map<string, any[]>())
// 修改stageOptions计算属性仅在选项已加载时才计算
const stageOptions = computed(() => {
// 如果通过props传入了optionsLoaded且为true或者本地状态表示已加载则计算选项
const optionsReady = props.optionsLoaded || localOptionsLoaded.value
if (!optionsReady) {
return []
}
const cacheKey = 'base_stage_options'
if (!stageOptionsCache.value.has(cacheKey)) {
const baseOptions = STAGE_DAILY_INFO.map(stage => ({
@@ -433,15 +471,25 @@ const stageOptions = computed(() => {
return stageOptionsCache.value.get(cacheKey) || []
})
// 优化getSelectOptions函数添加缓存
// 修改getSelectOptions函数添加选项未加载时的默认处理
const getSelectOptions = (columnKey: string, taskName: string, currentValue?: string) => {
// 如果选项未加载,返回包含当前值的简单选项或空数组
const optionsReady = props.optionsLoaded || localOptionsLoaded.value
if (!optionsReady) {
if (currentValue) {
// 如果有当前值,至少返回包含当前值的选项,避免显示空白
return [{ label: currentValue, value: currentValue }]
}
return []
}
const cacheKey = `${columnKey}_${taskName}_${currentValue || ''}`
if (stageOptionsCache.value.has(cacheKey)) {
return stageOptionsCache.value.get(cacheKey)
}
let options: any[] = []
let options: any[]
switch (taskName) {
case '连战次数':