refactor(plans): 重构一下计划管理页面

This commit is contained in:
2025-08-30 17:03:22 +08:00
parent 7db6fcbcc8
commit c15c74895b
2 changed files with 515 additions and 358 deletions

View File

@@ -28,7 +28,7 @@ export function usePlanApi() {
try {
const params: PlanCreateIn = { type }
const response = await Service.addPlanApiPlanAddPost(params)
message.success('创建计划成功')
// message.success('创建计划成功')
return response
} catch (error) {
console.error('创建计划失败:', error)

View File

@@ -1,21 +1,49 @@
<template>
<div v-if="loading" class="loading-box">
<a-spin tip="加载中,请稍候..." size="large" />
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<a-spin size="large" tip="加载中,请稍候..." />
</div>
<!-- 有计划时显示 -->
<div v-else class="plans-content">
<!-- 计划头部 -->
<!-- 主要内容 -->
<div v-else class="plans-main">
<!-- 页面头部 -->
<div class="plans-header">
<div class="header-title">
<h1>计划管理</h1>
<div class="header-left">
<h1 class="page-title">计划管理</h1>
</div>
<div class="header-actions">
<a-space size="middle">
<a-button type="primary" size="large" @click="handleAddPlan">
<a-button
type="primary"
size="large"
@click="handleAddPlan"
v-if="planList.length > 0 || currentPlanData"
>
<template #icon>
<PlusOutlined />
</template>
新建计划
</a-button>
<a-popconfirm
v-if="planList.length > 0"
title="确定要删除这个计划吗?"
ok-text="确定"
cancel-text="取消"
@confirm="handleRemovePlan(activePlanId)"
>
<a-button
danger
size="large"
:disabled="!activePlanId"
>
<template #icon>
<DeleteOutlined />
</template>
删除当前计划
</a-button>
</a-popconfirm>
<a-button size="large" @click="handleRefresh">
<template #icon>
<ReloadOutlined />
@@ -24,80 +52,126 @@
</a-button>
</a-space>
</div>
</div>
<!-- 如果没有计划显示占位符 -->
<div v-if="!planList.length || !currentPlanData" class="placeholder-container">
<div class="placeholder-content">
<h2>当前没有计划</h2>
<p>您还没有创建任何计划点击下方按钮来创建您的第一个计划</p>
<!-- 空状态 -->
<div v-if="!planList.length || !currentPlanData" class="empty-state">
<a-empty
image="https://gw.alipayobjects.com/zos/antfincdn/ZHrcdLPrvN/empty.svg"
:image-style="{ height: '120px' }"
description="当前没有计划"
>
<template #description>
<span class="empty-description">
您还没有创建任何计划点击下方按钮来创建您的第一个计划
</span>
</template>
<a-button type="primary" size="large" @click="handleAddPlan">
<template #icon>
<PlusOutlined />
</template>
新建计划
</a-button>
</div>
</a-empty>
</div>
<!-- 计划内容 -->
<div v-else class="plans-content">
<!-- 计划选择卡片 -->
<a-card class="plan-selector-card" :bordered="false">
<template #title>
<div class="card-title">
<span>计划选择</span>
<a-tag :color="planList.length > 0 ? 'success' : 'default'">
{{ planList.length }} 个计划
</a-tag>
</div>
</template>
<div class="plan-content" v-else v-if="currentPlanData">
<!-- 计划选择器 -->
<div class="plan-selector">
<a-tabs
v-model:activeKey="activePlanId"
type="editable-card"
@edit="onTabEdit"
@change="onPlanChange"
class="plan-tabs"
>
<a-tab-pane
<div class="plan-selection-container">
<!-- 计划按钮组 -->
<div class="plan-buttons-container">
<a-space wrap size="middle">
<a-button
v-for="plan in planList"
:key="plan.id"
:tab="plan.name"
:closable="planList.length > 0"
/>
</a-tabs>
</div>
<!-- MAA计划配置 -->
<div class="maa-config-section">
<div class="section-header">
<div class="section-title">
<div class="plan-name-editor">
<a-input
v-model:value="currentPlanName"
placeholder="请输入计划名称"
:type="activePlanId === plan.id ? 'primary' : 'default'"
size="large"
class="plan-name-input"
@blur="onPlanNameBlur"
@pressEnter="onPlanNameBlur"
/>
</div>
</div>
<div class="section-controls">
<a-space>
<span class="mode-label">模式</span>
<a-radio-group v-model:value="currentMode" @change="onModeChange" size="default">
<a-radio-button value="ALL">全局</a-radio-button>
<a-radio-button value="Weekly">周计划</a-radio-button>
</a-radio-group>
@click="onPlanChange(plan.id)"
class="plan-button"
>
{{ plan.name }}
</a-button>
</a-space>
</div>
</div>
</a-card>
<!-- 使用 Ant Design 表格组件 -->
<!-- 计划配置卡片 -->
<a-card class="plan-config-card" :bordered="false">
<template #title>
<div class="plan-title-container">
<div v-if="!isEditingPlanName" class="plan-title-display">
<span class="plan-title-text">{{ currentPlanName || '计划配置' }}</span>
<a-button
type="text"
size="small"
@click="startEditPlanName"
class="plan-edit-btn"
>
<template #icon>
<EditOutlined />
</template>
</a-button>
</div>
<div v-else class="plan-title-edit">
<a-input
v-model:value="currentPlanName"
placeholder="请输入计划名称"
size="small"
class="plan-title-input"
@blur="finishEditPlanName"
@pressEnter="finishEditPlanName"
:maxlength="50"
ref="planNameInputRef"
/>
</div>
</div>
</template>
<template #extra>
<a-space>
<span class="mode-label">执行模式</span>
<a-segmented
v-model:value="currentMode"
@change="onModeChange"
:options="[
{ label: '全局模式', value: 'ALL' },
{ label: '周计划模式', value: 'Weekly' }
]"
/>
</a-space>
</template>
<!-- 配置表格 -->
<div class="config-table-container">
<a-table
:columns="dynamicTableColumns"
:data-source="tableData"
:pagination="false"
:scroll="{ x: 1000 }"
class="plan-table"
class="config-table"
size="middle"
:bordered="true"
:scroll="{ x: false }"
>
<template #bodyCell="{ column, record }">
<template #bodyCell="{ column, record, index }">
<template v-if="column.key === 'taskName'">
<div class="task-name">
<div class="task-name-cell">
<a-tag
:color="getTaskTagColor(record.taskName)"
class="task-tag"
>
{{ record.taskName }}
</a-tag>
</div>
</template>
<template v-else-if="record.taskName === '吃理智药'">
@@ -107,7 +181,8 @@
:min="0"
:max="999"
:placeholder="getPlaceholder(column.key, record.taskName)"
class="config-input"
class="config-input-number"
:controls="false"
/>
</template>
<template v-else>
@@ -118,31 +193,22 @@
:placeholder="getPlaceholder(column.key, record.taskName)"
class="config-select"
allow-clear
:show-search="true"
:filter-option="filterOption"
/>
</template>
</template>
</a-table>
</div>
</a-card>
</div>
</div>
<!-- 悬浮保存按钮 -->
<a-float-button
type="primary"
@click="handleSave"
class="float-button"
:style="{ right: '24px' }"
>
<template #icon>
<SaveOutlined />
</template>
</a-float-button>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons-vue'
import { PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons-vue'
import { usePlanApi } from '../composables/usePlanApi'
// API 相关
@@ -156,6 +222,10 @@ const currentPlanData = ref<Record<string, any> | null>(null)
// 当前计划的名称和模式
const currentPlanName = ref<string>('')
const currentMode = ref<'ALL' | 'Weekly'>('ALL')
// 计划名称编辑状态
const isEditingPlanName = ref<boolean>(false)
// 显示名称提示
const showNameTip = ref<boolean>(false)
const loading = ref(true)
@@ -423,7 +493,7 @@ const onModeChange = () => {
// 计划名称编辑失焦处理
const onPlanNameBlur = () => {
// 当用户编辑完计划名称后,更新标签页显示的名称
// 当用户编辑完计划名称后,更新按钮显示的名称
if (activePlanId.value) {
const currentPlan = planList.value.find(plan => plan.id === activePlanId.value)
if (currentPlan) {
@@ -432,6 +502,25 @@ const onPlanNameBlur = () => {
}
}
// 开始编辑计划名称
const startEditPlanName = () => {
isEditingPlanName.value = true
// 使用 nextTick 确保 DOM 更新后再获取焦点
setTimeout(() => {
const input = document.querySelector('.plan-title-input input') as HTMLInputElement
if (input) {
input.focus()
input.select()
}
}, 100)
}
// 完成编辑计划名称
const finishEditPlanName = () => {
isEditingPlanName.value = false
onPlanNameBlur()
}
// 手动保存处理
const handleSave = async () => {
if (!activePlanId.value) {
@@ -440,26 +529,17 @@ const handleSave = async () => {
}
try {
await savePlanData()
message.success('保存成功')
// message.success('保存成功')
} catch (error) {
message.error('保存失败')
}
}
// 标签页编辑处理
const onTabEdit = async (targetKey: string | MouseEvent, action: 'add' | 'remove') => {
if (action === 'add') {
await handleAddPlan()
} else if (action === 'remove' && typeof targetKey === 'string') {
await handleRemovePlan(targetKey)
}
}
// 添加计划
const handleAddPlan = async () => {
try {
const response = await createPlan('MaaPlan')
const defaultName = `计划 ${planList.value.length + 1}`
const defaultName = '新MAA计划表'
const newPlan = {
id: response.planId,
name: defaultName,
@@ -471,6 +551,9 @@ const handleAddPlan = async () => {
currentPlanName.value = defaultName
await loadPlanData(newPlan.id)
// 显示名称修改提示
message.info('已创建新的MAA计划表建议您修改为更有意义的名称', 3)
} catch (error) {
console.error('添加计划失败:', error)
}
@@ -487,6 +570,8 @@ const handleRemovePlan = async (planId: string) => {
activePlanId.value = planList.value[0]?.id || ''
if (activePlanId.value) {
await loadPlanData(activePlanId.value)
} else {
currentPlanData.value = null
}
}
}
@@ -497,6 +582,8 @@ const handleRemovePlan = async (planId: string) => {
// 计划切换
const onPlanChange = async (planId: string) => {
// 立即更新activePlanId以确保按钮高亮切换
activePlanId.value = planId
await loadPlanData(planId)
}
@@ -633,76 +720,78 @@ const handleRefresh = async () => {
// message.success('刷新成功')
}
// 自动保存功能
watch(
() => [currentPlanName.value, currentMode.value, tableData.value],
async () => {
// 使用nextTick确保DOM更新后再保存
await nextTick()
handleSave()
},
{ deep: true }
)
// 移除自动保存功能,改为手动保存
// 用户需要点击悬浮按钮才能保存数据
onMounted(() => {
initPlans()
})
// 新增方法:获取任务标签颜色
const getTaskTagColor = (taskName: string) => {
const colorMap: Record<string, string> = {
'吃理智药': 'blue',
'连战次数': 'green',
'关卡选择': 'orange',
'备选-1': 'purple',
'备选-2': 'purple',
'备选-3': 'purple',
'剩余理智': 'cyan'
}
return colorMap[taskName] || 'default'
}
// 新增方法:选择器过滤
const filterOption = (input: string, option: any) => {
return option.label.toLowerCase().includes(input.toLowerCase())
}
</script>
<style scoped>
/* 空状态样式 */
.empty-state {
flex: 1;
.plans-container {
min-height: 100vh;
background: var(--ant-color-bg-layout);
padding: 24px;
}
.loading-container {
display: flex;
align-items: center;
justify-content: center;
align-items: center;
min-height: 400px;
}
.empty-content {
text-align: center;
padding: 48px;
background: var(--ant-color-bg-container);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid var(--ant-color-border-secondary);
.plans-main {
max-width: 1400px;
margin: 0 auto;
}
.empty-icon {
font-size: 64px;
color: var(--ant-color-text-tertiary);
margin-bottom: 24px;
}
.empty-content h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin: 0 0 8px 0;
}
.empty-content p {
font-size: 14px;
color: var(--ant-color-text-secondary);
margin: 0 0 32px 0;
}
/* 计划内容区域 */
.plans-content {
flex: 1;
display: flex;
flex-direction: column;
background: var(--ant-color-bg-container);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid var(--ant-color-border-secondary);
overflow: hidden;
}
/* 计划头部样式 */
/* 页面头部 */
.plans-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px 32px;
background: var(--ant-color-bg-container);
border-bottom: 1px solid var(--ant-color-border-secondary);
margin-bottom: 5px;
align-items: flex-end;
margin-bottom: 24px;
padding: 0 4px;
}
.header-title h1 {
margin: 0;
.header-left {
flex: 1;
}
.page-title {
margin: 0 0 8px 0;
font-size: 32px;
font-weight: 700;
color: var(--ant-color-text);
@@ -712,38 +801,81 @@ onMounted(() => {
background-clip: text;
}
/* 计划选择器 */
.plan-selector {
padding: 0 32px;
.page-description {
margin: 0;
font-size: 16px;
color: var(--ant-color-text-secondary);
line-height: 1.5;
}
.header-actions {
flex-shrink: 0;
}
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: var(--ant-color-bg-container);
border-bottom: 1px solid var(--ant-color-border-secondary);
border-radius: 12px;
border: 1px solid var(--ant-color-border-secondary);
}
.empty-description {
color: var(--ant-color-text-secondary);
font-size: 16px;
margin-bottom: 16px;
display: block;
}
/* 计划内容 */
.plan-content {
flex: 1;
padding: 24px 32px;
overflow: auto;
}
.section-header {
.plans-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
flex-direction: column;
gap: 24px;
}
.section-title h3 {
margin: 0;
color: var(--ant-color-text);
/* 计划选择卡片 */
.plan-selector-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 12px;
border: 1px solid var(--ant-color-border-secondary);
}
.card-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
}
.section-controls {
.plan-selection-container {
padding: 16px;
}
/* 计划按钮组 */
.plan-buttons-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.plan-button {
flex: 1 1 120px;
border-radius: 8px;
transition: all 0.2s ease;
}
/* 计划配置卡片 */
.plan-config-card {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 12px;
border: 1px solid var(--ant-color-border-secondary);
min-height: 600px;
}
.mode-label {
@@ -752,71 +884,175 @@ onMounted(() => {
font-weight: 500;
}
/* 计划名称编辑器样式 */
.plan-name-editor {
/* 计划名称编辑 */
.plan-title-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.plan-title-display {
display: flex;
align-items: center;
gap: 8px;
}
.plan-title-text {
font-size: 18px;
font-weight: 600;
color: var(--ant-color-text);
}
.plan-edit-btn {
color: var(--ant-color-primary);
padding: 0;
}
/* 计划名称输入框 */
.plan-title-input {
flex: 1;
max-width: 400px;
border-radius: 8px;
transition: all 0.2s ease;
}
/* 配置表格 */
.config-table-container {
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--ant-color-border-secondary);
}
.config-table {
margin: 0;
}
/* 任务名称单元格 */
.task-name-cell {
display: flex;
justify-content: center;
align-items: center;
}
.plan-name-input {
max-width: 300px;
font-size: 18px;
font-weight: 600;
}
.plan-name-input :deep(.ant-input) {
border: 1px solid transparent;
background: transparent;
color: var(--ant-color-text);
font-size: 18px;
font-weight: 600;
padding: 4px 8px;
.task-tag {
margin: 0;
padding: 4px 12px;
border-radius: 6px;
transition: all 0.3s ease;
}
.plan-name-input :deep(.ant-input:hover) {
border-color: var(--ant-color-border);
}
.plan-name-input :deep(.ant-input:focus) {
border-color: var(--ant-color-primary);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 表格样式 */
.plan-table {
background: var(--ant-color-bg-container);
}
.plan-table :deep(.ant-table-thead > tr > th) {
border-bottom: 2px solid var(--ant-color-border);
font-weight: 600;
color: var(--ant-color-text);
text-align: center;
}
.plan-table :deep(.ant-table-tbody > tr > td) {
border-bottom: 1px solid var(--ant-color-border-secondary);
text-align: center;
padding: 12px 8px;
}
.task-name {
font-weight: 600;
color: var(--ant-color-text);
text-align: center;
font-weight: 500;
font-size: 13px;
}
/* 配置输入组件 */
.config-select {
width: 100%;
min-width: 100px;
}
.config-input-number {
width: 100%;
min-width: 100px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.plans-container {
padding: 16px;
}
.plans-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.page-title {
font-size: 28px;
}
.config-table-container {
overflow-x: auto;
}
}
@media (max-width: 768px) {
.plans-container {
padding: 12px;
}
.page-title {
font-size: 24px;
}
.page-description {
font-size: 14px;
}
.plan-title-input {
max-width: 100%;
}
.header-actions {
width: 100%;
display: flex;
justify-content: center;
}
}
/* 深度样式使用全局CSS变量 */
.plan-selector-card :deep(.ant-card-head) {
border-bottom: 1px solid var(--ant-color-border-secondary);
padding: 16px 24px;
}
.plan-config-card :deep(.ant-card-head) {
border-bottom: 1px solid var(--ant-color-border-secondary);
padding: 16px 24px;
}
.plan-config-card :deep(.ant-card-head-title) {
font-size: 18px;
font-weight: 600;
}
.plan-title-container :deep(.ant-form-item-label) {
font-weight: 600;
color: var(--ant-color-text);
}
.plan-title-input :deep(.ant-input) {
font-size: 16px;
font-weight: 500;
}
.plan-title-input :deep(.ant-input:focus) {
box-shadow: 0 0 0 2px var(--ant-color-primary-bg-hover);
}
.config-table :deep(.ant-table-thead > tr > th) {
background: var(--ant-color-bg-container-disabled);
border-bottom: 2px solid var(--ant-color-border);
font-weight: 600;
color: var(--ant-color-text);
text-align: center;
padding: 16px 12px;
font-size: 14px;
}
.config-table :deep(.ant-table-tbody > tr > td) {
border-bottom: 1px solid var(--ant-color-border-secondary);
text-align: center;
padding: 12px 8px;
vertical-align: middle;
}
.config-table :deep(.ant-table-tbody > tr:hover > td) {
background: var(--ant-color-bg-container-disabled);
}
.config-select :deep(.ant-select-selector) {
border-radius: 6px;
border: 1px solid var(--ant-color-border);
transition: all 0.2s ease;
}
.config-select :deep(.ant-select-selector:hover) {
@@ -825,112 +1061,33 @@ onMounted(() => {
.config-select :deep(.ant-select-focused .ant-select-selector) {
border-color: var(--ant-color-primary);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
box-shadow: 0 0 0 2px var(--ant-color-primary-bg-hover);
}
/* 输入框样式 */
.config-input {
.config-input-number :deep(.ant-input-number) {
border-radius: 6px;
width: 100%;
min-width: 100px;
}
.config-input :deep(.ant-input-number-focused) {
.config-input-number :deep(.ant-input-number-focused) {
border-color: var(--ant-color-primary);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
box-shadow: 0 0 0 2px var(--ant-color-primary-bg-hover);
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.maa-config-section {
border-color: var(--ant-color-border-secondary);
border-radius: 16px;
.plan-tabs :deep(.ant-tabs-tab) {
padding: 12px 20px;
font-size: 14px;
font-weight: 500;
border-radius: 8px 8px 0 0;
transition: all 0.2s ease;
}
.section-header {
border-color: var(--ant-color-border-secondary);
border-radius: 16px;
}
.plan-tabs :deep(.ant-tabs-tab-active) {
background: var(--ant-color-primary-bg);
color: var(--ant-color-primary);
}
.float-button {
width: 60px;
height: 60px;
}
.empty-content-fancy {
transition:
box-shadow 0.3s,
transform 0.2s;
border: none;
border-radius: 24px;
}
.empty-icon {
font-size: 80px;
margin-bottom: 32px;
border-radius: 50%;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
.empty-content-fancy h3 {
font-size: 26px;
font-weight: 700;
margin: 0 0 12px 0;
letter-spacing: 1px;
}
.empty-content-fancy p {
font-size: 16px;
border-radius: 8px;
padding: 8px 16px;
margin: 0 0 12px 0;
display: inline-block;
}
.loading-box {
min-height: 400px;
display: flex;
justify-content: center;
align-items: center;
}
.placeholder-container {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.placeholder-content {
text-align: center;
max-width: 500px;
}
.placeholder-content h2 {
font-size: 24px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 16px;
}
.placeholder-content p {
font-size: 16px;
color: var(--ant-color-text-secondary);
margin-bottom: 24px;
line-height: 1.5;
}
.empty-content-fancy h2 {
font-size: 26px;
font-weight: 700;
margin: 0 0 12px 0;
letter-spacing: 1px;
color: var(--ant-color-text);
:deep(.ant-float-btn-group .ant-float-btn-group-circle-wrapper) {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
}
</style>