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