feat(plan): 优化计划页面结构与加载逻辑
- 调整 template 结构,将加载状态与主要内容分离,提升可读性 - 引入 optionsLoaded 控制选项加载时机,避免初始渲染阻塞 - 增加 onMounted 和 watch逻辑确保异步数据正确处理- 统一计划类型为 MaaPlanConfig,兼容旧类型 MaaPlan- 优化 PlanHeader 新建计划按钮交互,支持动态显示按钮文本- 改进 PlanSelector 中计划类型标签显示逻辑,仅在必要时展示
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '连战次数':
|
||||
|
||||
Reference in New Issue
Block a user