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-item> -->
</a-menu> </a-menu>
</template> </template>
<a-button type="primary" size="large"> <a-button type="primary" size="large" @click="handleAddPlan">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
</template> </template>
新建计划 {{ getPlanButtonText }}
<DownOutlined /> <DownOutlined />
</a-button> </a-button>
</a-dropdown> </a-dropdown>
@@ -54,6 +54,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { DeleteOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons-vue' import { DeleteOutlined, DownOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { ref, computed } from 'vue'
interface Plan { interface Plan {
id: string id: string
@@ -68,15 +69,36 @@ interface Props {
interface Emits { interface Emits {
(e: 'add-plan', planType: string): void (e: 'add-plan', planType: string): void
(e: 'remove-plan', planId: string): void (e: 'remove-plan', planId: string): void
} }
defineProps<Props>() defineProps<Props>()
const emit = defineEmits<Emits>() 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 }) => { const handleMenuClick = ({ key }: { key: string }) => {
emit('add-plan', key) selectedPlanType.value = key
}
// 点击主按钮创建计划
const handleAddPlan = () => {
emit('add-plan', selectedPlanType.value)
} }
</script> </script>
@@ -131,4 +153,4 @@ const handleMenuClick = ({ key }: { key: string }) => {
justify-content: center; justify-content: center;
} }
} }
</style> </style>

View File

@@ -22,7 +22,12 @@
class="plan-button" class="plan-button"
> >
<span class="plan-name">{{ plan.name }}</span> <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) }} {{ getPlanTypeLabel(plan.type) }}
</a-tag> </a-tag>
</a-button> </a-button>
@@ -33,7 +38,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
interface Plan { interface Plan {
id: string id: string
name: string name: string
@@ -66,13 +70,37 @@ const handlePlanClick = debounce((planId: string) => {
emit('plan-change', planId) emit('plan-change', planId)
}, 100) }, 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) => { const getPlanTypeLabel = (planType: string) => {
// 统一使用 MaaPlanConfig 作为默认类型
const normalizedPlanType = planType === 'MaaPlan' ? 'MaaPlanConfig' : planType
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
MaaPlan: 'MAA', MaaPlanConfig: 'MAA',
GeneralPlan: '通用', GeneralPlan: '通用',
CustomPlan: '自定义', CustomPlan: '自定义',
} }
return labelMap[planType] || planType return labelMap[normalizedPlanType] || normalizedPlanType
} }
</script> </script>

View File

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

View File

@@ -178,7 +178,7 @@
</template> </template>
<script setup lang="ts"> <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 { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue' import { PlusOutlined } from '@ant-design/icons-vue'
@@ -203,13 +203,44 @@ interface Props {
viewMode: 'config' | 'simple' viewMode: 'config' | 'simple'
} }
// 添加一个新的prop用于控制选项是否已加载
interface ExtendedProps extends Props {
optionsLoaded?: boolean
}
interface Emits { interface Emits {
(e: 'update-table-data', value: Record<string, any>): void (e: 'update-table-data', value: Record<string, any>): void
} }
const props = defineProps<Props>() const props = defineProps<ExtendedProps>()
const emit = defineEmits<Emits>() 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的辅助组件 // 渲染VNode的辅助组件
const VNodeRenderer = defineComponent({ const VNodeRenderer = defineComponent({
name: 'VNodeRenderer', name: 'VNodeRenderer',
@@ -415,7 +446,14 @@ const addCustomStage = (rowKey: string, columnKey: string) => {
// 缓存计算属性,避免重复计算 // 缓存计算属性,避免重复计算
const stageOptionsCache = ref(new Map<string, any[]>()) const stageOptionsCache = ref(new Map<string, any[]>())
// 修改stageOptions计算属性仅在选项已加载时才计算
const stageOptions = computed(() => { const stageOptions = computed(() => {
// 如果通过props传入了optionsLoaded且为true或者本地状态表示已加载则计算选项
const optionsReady = props.optionsLoaded || localOptionsLoaded.value
if (!optionsReady) {
return []
}
const cacheKey = 'base_stage_options' const cacheKey = 'base_stage_options'
if (!stageOptionsCache.value.has(cacheKey)) { if (!stageOptionsCache.value.has(cacheKey)) {
const baseOptions = STAGE_DAILY_INFO.map(stage => ({ const baseOptions = STAGE_DAILY_INFO.map(stage => ({
@@ -433,15 +471,25 @@ const stageOptions = computed(() => {
return stageOptionsCache.value.get(cacheKey) || [] return stageOptionsCache.value.get(cacheKey) || []
}) })
// 优化getSelectOptions函数添加缓存 // 修改getSelectOptions函数添加选项未加载时的默认处理
const getSelectOptions = (columnKey: string, taskName: string, currentValue?: string) => { 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 || ''}` const cacheKey = `${columnKey}_${taskName}_${currentValue || ''}`
if (stageOptionsCache.value.has(cacheKey)) { if (stageOptionsCache.value.has(cacheKey)) {
return stageOptionsCache.value.get(cacheKey) return stageOptionsCache.value.get(cacheKey)
} }
let options: any[] = [] let options: any[]
switch (taskName) { switch (taskName) {
case '连战次数': case '连战次数':