537 lines
23 KiB
Markdown
537 lines
23 KiB
Markdown
# 关于我们模块 + 工厂重置 — 收尾实施计划
|
||
|
||
## 摘要
|
||
|
||
本计划承接上一会话已完成的工作,完成剩余的前端改造与新增页面、编译验证、服务重启。整体目标:
|
||
|
||
1. 前台"关于我们"页面去掉"联系我们"模块,并改为从后端动态加载内容(已基本完成)
|
||
2. 后台新增"关于我们"管理模块:控制前台是否显示该页 + 全部内容**内联所见即所得**编辑
|
||
3. 在系统设置页新增"危险操作"卡片,提供恢复出厂设置入口(清库 + 清 COS)
|
||
|
||
## 当前状态分析
|
||
|
||
### 已完成(上一会话)
|
||
|
||
**后端(编译通过,0 错误):**
|
||
|
||
* [FileStorageService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/Infrastructure/FileStorageService.cs) — 新增 `ListAllObjectsAsync()` 与 `DeleteObjectsAsync(IEnumerable<string>)`,支持 COS 分页列出 + 批量删除(每批 1000),Local 模式回退到 Directory.GetFiles
|
||
|
||
* [Dtos.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Common/Dtos/Dtos.cs) — `PublicSettingsDto` 加 `ShowAbout`;新增 `AboutStatDto`/`AboutMissionCardDto`/`AboutTimelineItemDto`/`AboutContentDto`/`AboutSettingsDto`/`FactoryResetRequest`/`FactoryResetResultDto`
|
||
|
||
* [SystemService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/SystemService.cs) — `GetPublicSettingsAsync` 加 `ShowAbout`;新增 `GetAboutSettingsAsync`、`UpdateAboutSettingsAsync(AboutSettingsDto)`、`FactoryResetAsync(FactoryResetRequest)`
|
||
|
||
* [ISystemService.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Services/ISystemService.cs) — 新增 3 个方法签名
|
||
|
||
* [SystemController.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Controllers/SystemController.cs) — 新增 `GET /api/system/about`(AllowAnonymous)、`PUT /api/system/about`(Admin)、`POST /api/system/factory-reset`(Admin)
|
||
|
||
* [DbSeeder.cs](file:///d:/TraeProject/MilliSim4/backend/MilliSim.Api/Data/DbSeeder.cs) — allSettings 新增 `ShowAbout=true` 与 `AboutContent=JSON(默认内容)`;工厂重置后重新种子可恢复默认数据
|
||
|
||
**前端(部分完成):**
|
||
|
||
* [types/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/types/index.ts) — `PublicSettings` 加 `showAbout`;新增 `AboutStat`/`AboutMissionCard`/`AboutTimelineItem`/`AboutContent`/`AboutSettings`/`FactoryResetResult` 接口
|
||
|
||
* [api/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/api/index.ts) — systemApi 加 `getAbout()`、`updateAbout(data)`、`factoryReset(confirmText)`
|
||
|
||
* [stores/system.ts](file:///d:/TraeProject/MilliSim4/frontend/src/stores/system.ts) — 加 `aboutContent` ref、`aboutLoaded` ref、`loadAbout()` 方法
|
||
|
||
* [About.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/frontend/About.vue) — 已重写,去掉"联系我们"模块,所有内容从 `systemStore.aboutContent` 加载,骨架屏 loading
|
||
|
||
### 待完成
|
||
|
||
* **B 收尾**:[router/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/router/index.ts) 与 [FrontendLayout.vue](file:///d:/TraeProject/MilliSim4/frontend/src/layouts/FrontendLayout.vue) 改造未完成
|
||
|
||
* **C**:[AboutSettings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/AboutSettings.vue) 与配套 CSS 文件尚未创建
|
||
|
||
* **D**:[Settings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Settings.vue) 危险操作卡片未添加
|
||
|
||
* **E**:编译验证 + 重启前后端
|
||
|
||
## 关键决策(用户已确认)
|
||
|
||
1. **数据库重置范围**:保留种子数据(DbSeeder.SeedAsync 重新补回分类/平台/标签/示例项目/轮播图/合作伙伴;admin 账户保留,密码 admin123)
|
||
2. **编辑交互方式**:内联所见即所得(非表单式),编辑样式与前台 About 页一致,每个内容可直接编辑
|
||
|
||
## 提议变更
|
||
|
||
### 阶段 B 收尾 — 路由 + 前台导航
|
||
|
||
#### 文件 1:[frontend/src/router/index.ts](file:///d:/TraeProject/MilliSim4/frontend/src/router/index.ts)
|
||
|
||
**变更 1(L28)**:About 路由 meta 增加 `requiresAbout: true`:
|
||
|
||
```ts
|
||
{ path: 'about', name: 'About', component: () => import('@/views/frontend/About.vue'), meta: { title: '关于我们', requiresAbout: true } },
|
||
```
|
||
|
||
**变更 2(L84 后,notifications 路由前)**:admin children 增加"关于我们"管理路由:
|
||
|
||
```ts
|
||
{ path: 'system/about', name: 'AdminAboutSettings', component: () => import('@/views/admin/AboutSettings.vue'), meta: { title: '关于我们', admin: true } },
|
||
```
|
||
|
||
**变更 3(L103-139 beforeEach 守卫)**:守卫改为 async,并在权限校验前加 `showAbout` 检查。完整守卫:
|
||
|
||
```ts
|
||
router.beforeEach(async (to, _from, next) => {
|
||
document.title = `${to.meta.title || ''} - 毫秒数仿`
|
||
const userStore = useUserStore()
|
||
const systemStore = useSystemStore()
|
||
|
||
// 确保 systemStore 已加载(用于 showAbout 判断)
|
||
await systemStore.ensureLoaded()
|
||
|
||
// 校验关于我们页面是否对外显示
|
||
if (to.meta.requiresAbout && !systemStore.settings.showAbout) {
|
||
next('/')
|
||
return
|
||
}
|
||
|
||
// 校验登录状态
|
||
if (to.meta.auth && !userStore.isLoggedIn) {
|
||
next(to.path.startsWith('/admin') ? '/admin/login' : '/login')
|
||
return
|
||
}
|
||
// ... 其余 staff/admin/pm/sales 校验保持不变
|
||
next()
|
||
})
|
||
```
|
||
|
||
> 注意:原守卫是同步函数,需改为 `async`,因为 `ensureLoaded()` 返回 Promise。系统已在 main.ts 启动时调 `systemStore.load()`,此处 `ensureLoaded` 通常是 no-op,不会引入明显延迟。
|
||
|
||
#### 文件 2:[frontend/src/layouts/FrontendLayout.vue](file:///d:/TraeProject/MilliSim4/frontend/src/layouts/FrontendLayout.vue)
|
||
|
||
**变更 1(L192-198 menuItems)**:改为 computed,让"关于我们"项根据 `systemStore.settings.showAbout` 过滤:
|
||
|
||
```ts
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
// ...
|
||
const allMenuItems = [
|
||
{ path: '/', label: '首页', icon: 'home' },
|
||
{ path: '/projects', label: '项目中心', icon: 'grid' },
|
||
{ path: '/tech', label: '技术能力', icon: 'zap' },
|
||
{ path: '/about', label: '关于我们', icon: 'building' },
|
||
{ path: '/contact', label: '联系咨询', icon: 'message' },
|
||
]
|
||
const menuItems = computed(() =>
|
||
systemStore.settings.showAbout
|
||
? allMenuItems
|
||
: allMenuItems.filter(i => i.path !== '/about')
|
||
)
|
||
```
|
||
|
||
**变更 2(L132-138 footer 关于我们 列)**:footer 中"公司介绍"链接加 `v-if="systemStore.settings.showAbout"`:
|
||
|
||
```html
|
||
<li v-if="systemStore.settings.showAbout"><router-link to="/about">公司介绍</router-link></li>
|
||
```
|
||
|
||
### 阶段 C — 后台内联编辑页 AboutSettings.vue
|
||
|
||
#### 文件 3:[frontend/src/views/admin/AboutSettings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/AboutSettings.vue)(新建)
|
||
|
||
**目标**:内联所见即所得编辑器,编辑样式尽量与前台 About 一致,每个文本内容可直接点击编辑。
|
||
|
||
**结构**:
|
||
|
||
* 顶部页头卡片(与其他 admin 页一致),含"显示开关"和"保存"按钮
|
||
|
||
* 内容区按 Hero / 公司简介 / 使命愿景 / 发展历程分块,每块都是可编辑卡片
|
||
|
||
* 列表型数据(introParagraphs / stats / missionCards / timeline)支持增删和上下移动
|
||
|
||
* 使用 `el-input`(无边框 + autosize)实现内联编辑,hover/focus 时显示虚线边框提示可编辑
|
||
|
||
**关键交互**:
|
||
|
||
* `showAbout` 开关:el-switch,change 时立即调 `updateAbout` 保存(不依赖主保存按钮)
|
||
|
||
* 主"保存"按钮:保存全部内容到 `AboutContent`
|
||
|
||
* 列表增删:每项右侧悬浮显示"+ 添加 / 删除 / 上移 / 下移"图标按钮
|
||
|
||
* 保存后调 `systemStore.loadAbout()` 刷新 store,使前台立即生效
|
||
|
||
**核心代码骨架**:
|
||
|
||
```vue
|
||
<template>
|
||
<div class="admin-page about-settings-page">
|
||
<!-- 页头 -->
|
||
<div class="page-header-card">
|
||
<div class="page-title-area">
|
||
<div class="page-title-icon"><SvgIcon name="building" :size="20" color="#fff" /></div>
|
||
<div class="page-title-text">
|
||
<h2>关于我们管理</h2>
|
||
<p>控制前台"关于我们"页面是否显示,并编辑全部内容(所见即所得)</p>
|
||
</div>
|
||
</div>
|
||
<div class="page-header-actions">
|
||
<el-switch v-model="form.showAbout" active-text="前台显示" @change="onToggleShowAbout" />
|
||
<el-button type="primary" :loading="saving" @click="saveAll">
|
||
<SvgIcon name="save" :size="14" color="#fff" /><span>保存全部内容</span>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-skeleton v-if="loading" :rows="12" animated />
|
||
|
||
<template v-else>
|
||
<!-- Hero 块 -->
|
||
<div class="editor-card hero-editor">
|
||
<div class="editor-section-title">主视觉 Hero</div>
|
||
<div class="hero-preview">
|
||
<el-input v-model="form.content.heroTag" class="inline-input hero-tag-input" placeholder="标签" />
|
||
<el-input v-model="form.content.heroTitle" class="inline-input hero-title-input" placeholder="主标题" />
|
||
<el-input v-model="form.content.heroDesc" type="textarea" :autosize="{ minRows: 2 }" class="inline-input hero-desc-input" placeholder="副标题描述" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 公司简介 -->
|
||
<div class="editor-card">
|
||
<div class="editor-section-title">
|
||
<span>公司简介</span>
|
||
<el-button text @click="addIntroParagraph"><SvgIcon name="plus" :size="14" /> 段落</el-button>
|
||
</div>
|
||
<el-input v-model="form.content.introTitle" class="inline-input block-title-input" placeholder="板块标题" />
|
||
<div v-for="(p, i) in form.content.introParagraphs" :key="i" class="list-item">
|
||
<el-input v-model="form.content.introParagraphs[i]" type="textarea" :autosize="{ minRows: 2 }" class="inline-input" />
|
||
<div class="list-actions">
|
||
<button @click="moveItem(form.content.introParagraphs, i, -1)" :disabled="i === 0"><SvgIcon name="arrowUp" :size="14" /></button>
|
||
<button @click="moveItem(form.content.introParagraphs, i, 1)" :disabled="i === form.content.introParagraphs.length - 1"><SvgIcon name="arrowDown" :size="14" /></button>
|
||
<button @click="form.content.introParagraphs.splice(i, 1)"><SvgIcon name="trash" :size="14" /></button>
|
||
</div>
|
||
</div>
|
||
<div class="editor-section-subtitle">数据统计</div>
|
||
<div v-for="(s, i) in form.content.stats" :key="i" class="stat-row list-item">
|
||
<el-input v-model="form.content.stats[i].number" placeholder="数字" />
|
||
<el-input v-model="form.content.stats[i].label" placeholder="标签" />
|
||
<div class="list-actions">...</div>
|
||
</div>
|
||
<el-button text @click="form.content.stats.push({ number: '', label: '' })"><SvgIcon name="plus" :size="14" /> 统计项</el-button>
|
||
</div>
|
||
|
||
<!-- 使命愿景 -->
|
||
<div class="editor-card">
|
||
<div class="editor-section-title">
|
||
<span>使命与愿景</span>
|
||
<el-button text @click="form.content.missionCards.push({ title: '', icon: 'rocket', desc: '' })"><SvgIcon name="plus" :size="14" /> 卡片</el-button>
|
||
</div>
|
||
<el-input v-model="form.content.missionTitle" class="inline-input block-title-input" placeholder="板块标题" />
|
||
<div v-for="(c, i) in form.content.missionCards" :key="i" class="mission-card list-item">
|
||
<div class="mission-card-head">
|
||
<el-input v-model="form.content.missionCards[i].title" placeholder="卡片标题" />
|
||
<el-input v-model="form.content.missionCards[i].icon" placeholder="图标名 (如 rocket)" />
|
||
<div class="list-actions">...</div>
|
||
</div>
|
||
<el-input v-model="form.content.missionCards[i].desc" type="textarea" :autosize="{ minRows: 2 }" placeholder="描述" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 发展历程 -->
|
||
<div class="editor-card">
|
||
<div class="editor-section-title">
|
||
<span>发展历程</span>
|
||
<el-button text @click="form.content.timeline.push({ year: '', title: '', desc: '' })"><SvgIcon name="plus" :size="14" /> 时间线项</el-button>
|
||
</div>
|
||
<el-input v-model="form.content.timelineTitle" class="inline-input block-title-input" placeholder="板块标题" />
|
||
<div v-for="(t, i) in form.content.timeline" :key="i" class="timeline-row list-item">
|
||
<el-input v-model="form.content.timeline[i].year" placeholder="年份" />
|
||
<el-input v-model="form.content.timeline[i].title" placeholder="标题" />
|
||
<el-input v-model="form.content.timeline[i].desc" type="textarea" :autosize="{ minRows: 1 }" placeholder="描述" />
|
||
<div class="list-actions">...</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { systemApi } from '@/api'
|
||
import { useSystemStore } from '@/stores/system'
|
||
import SvgIcon from '@/components/SvgIcon.vue'
|
||
import { showActionConfirm } from '@/utils/confirm'
|
||
import type { AboutSettings, AboutContent } from '@/types'
|
||
|
||
const systemStore = useSystemStore()
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
|
||
const form = reactive<AboutSettings>({
|
||
showAbout: true,
|
||
content: { heroTag: '', heroTitle: '', heroDesc: '', introTitle: '', introParagraphs: [], stats: [], missionTitle: '', missionCards: [], timelineTitle: '', timeline: [] },
|
||
})
|
||
|
||
async function fetchAbout() {
|
||
loading.value = true
|
||
try {
|
||
const res = await systemApi.getAbout()
|
||
if (res.data) {
|
||
form.showAbout = res.data.showAbout !== false
|
||
form.content = res.data.content || form.content
|
||
}
|
||
} finally { loading.value = false }
|
||
}
|
||
|
||
function moveItem<T>(arr: T[], i: number, dir: number) {
|
||
const j = i + dir
|
||
if (j < 0 || j >= arr.length) return
|
||
;[arr[i], arr[j]] = [arr[j], arr[i]]
|
||
}
|
||
|
||
async function onToggleShowAbout(val: boolean) {
|
||
// 开关独立保存,不影响未保存的内容编辑
|
||
try {
|
||
await systemApi.updateAbout({ showAbout: val, content: form.content })
|
||
await systemStore.loadAbout()
|
||
ElMessage.success(val ? '已开启前台显示' : '已关闭前台显示')
|
||
} catch { form.showAbout = !val }
|
||
}
|
||
|
||
async function saveAll() {
|
||
if (saving.value) return
|
||
saving.value = true
|
||
try {
|
||
await systemApi.updateAbout({ showAbout: form.showAbout, content: form.content })
|
||
await systemStore.loadAbout()
|
||
ElMessage.success('保存成功')
|
||
} finally { saving.value = false }
|
||
}
|
||
|
||
function addIntroParagraph() {
|
||
form.content.introParagraphs.push('')
|
||
}
|
||
|
||
onMounted(fetchAbout)
|
||
</script>
|
||
|
||
<style scoped src="./AboutSettings.css"></style>
|
||
```
|
||
|
||
#### 文件 4:[frontend/src/views/admin/AboutSettings.css](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/AboutSettings.css)(新建)
|
||
|
||
**目标**:内联编辑样式 — 让 `el-input` 看起来像前台 About 页的文字,但 hover/focus 时显示虚线边框提示可编辑。
|
||
|
||
**关键样式**:
|
||
|
||
```css
|
||
/* 内联输入:默认无边框、透明背景,模拟前台文本外观 */
|
||
.inline-input :deep(.el-input__wrapper),
|
||
.inline-input :deep(.el-textarea__inner) {
|
||
background: transparent;
|
||
box-shadow: none !important;
|
||
border: 1px dashed transparent;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
transition: border-color .2s;
|
||
}
|
||
.inline-input :deep(.el-input__wrapper):hover,
|
||
.inline-input :deep(.el-textarea__inner):hover {
|
||
border-color: var(--color-primary-light);
|
||
}
|
||
.inline-input :deep(.el-input__wrapper).is-focus,
|
||
.inline-input :deep(.el-textarea__inner):focus {
|
||
border-color: var(--color-primary);
|
||
background: rgba(22, 119, 255, 0.04);
|
||
}
|
||
|
||
/* Hero 区编辑样式(白字) */
|
||
.hero-preview {
|
||
background: var(--gradient-hero);
|
||
padding: 40px 24px;
|
||
border-radius: 12px;
|
||
text-align: center;
|
||
}
|
||
.hero-preview .hero-tag-input :deep(.el-input__inner),
|
||
.hero-preview .hero-title-input :deep(.el-input__inner),
|
||
.hero-preview .hero-desc-input :deep(.el-textarea__inner) {
|
||
color: #fff;
|
||
text-align: center;
|
||
}
|
||
.hero-title-input :deep(.el-input__inner) { font-size: 32px; font-weight: 700; }
|
||
|
||
/* 列表项容器 */
|
||
.list-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.list-item > .el-input,
|
||
.list-item > .el-textarea { flex: 1; }
|
||
.list-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding-top: 4px;
|
||
}
|
||
.list-actions button {
|
||
background: transparent;
|
||
border: none;
|
||
padding: 4px;
|
||
cursor: pointer;
|
||
color: var(--color-text-secondary);
|
||
border-radius: 4px;
|
||
}
|
||
.list-actions button:hover:not(:disabled) {
|
||
background: var(--color-bg-elevated);
|
||
color: var(--color-primary);
|
||
}
|
||
.list-actions button:disabled { opacity: .3; cursor: not-allowed; }
|
||
```
|
||
|
||
### 阶段 D — Settings.vue 危险操作卡片
|
||
|
||
#### 文件 5:[frontend/src/views/admin/Settings.vue](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Settings.vue)
|
||
|
||
在"上传限制"卡片之后追加"危险操作"卡片:
|
||
|
||
```html
|
||
<!-- ===== 6. 危险操作卡片 ===== -->
|
||
<div class="form-card danger-card">
|
||
<div class="form-section-title danger-title">
|
||
<SvgIcon name="alert" :size="16" color="#E5484D" />
|
||
<span>危险操作</span>
|
||
</div>
|
||
<div class="danger-body">
|
||
<div class="danger-item">
|
||
<div class="danger-info">
|
||
<div class="danger-name">恢复出厂设置</div>
|
||
<div class="danger-desc">
|
||
清空所有业务数据(仅保留 admin 账户)并删除 COS 全部资源。系统会先自动创建数据备份,再重新写入种子数据(分类/平台/标签/示例项目/轮播图/合作伙伴)。
|
||
</div>
|
||
</div>
|
||
<el-button type="danger" @click="openFactoryReset">
|
||
<SvgIcon name="trash" :size="14" color="#fff" />
|
||
<span>恢复出厂设置</span>
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 恢复出厂设置确认弹窗 -->
|
||
<el-dialog v-model="resetDialogVisible" title="恢复出厂设置确认" width="480px" :close-on-click-modal="false">
|
||
<el-alert type="error" :closable="false" show-icon title="此操作不可逆" description="将清空所有业务数据并删除 COS 全部资源。系统会先自动创建数据备份。" style="margin-bottom: 16px" />
|
||
<p style="margin-bottom: 8px">请输入 <code>RESET</code> 确认:</p>
|
||
<el-input v-model="resetConfirmText" placeholder="请输入 RESET" />
|
||
<template #footer>
|
||
<el-button @click="resetDialogVisible = false">取消</el-button>
|
||
<el-button type="danger" :loading="resetting" :disabled="resetConfirmText !== 'RESET'" @click="confirmFactoryReset">
|
||
确认重置
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
```
|
||
|
||
**script 增加**:
|
||
|
||
```ts
|
||
import { systemApi } from '@/api'
|
||
// ...
|
||
const resetDialogVisible = ref(false)
|
||
const resetConfirmText = ref('')
|
||
const resetting = ref(false)
|
||
|
||
function openFactoryReset() {
|
||
resetConfirmText.value = ''
|
||
resetDialogVisible.value = true
|
||
}
|
||
|
||
async function confirmFactoryReset() {
|
||
if (resetting.value) return
|
||
resetting.value = true
|
||
try {
|
||
const res = await systemApi.factoryReset(resetConfirmText.value)
|
||
if (res.data) {
|
||
ElMessage.success(`重置成功:已清空 ${res.data.deletedTables} 张表,删除 ${res.data.deletedCosObjects} 个 COS 对象`)
|
||
resetDialogVisible.value = false
|
||
// 刷新 systemStore(admin 头像被清空、Logo 等可能变化)
|
||
await systemStore.load()
|
||
await systemStore.loadAbout()
|
||
}
|
||
} catch {
|
||
// 错误已由拦截器处理
|
||
} finally {
|
||
resetting.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
**关于 FactoryResetResultDto 字段**:后端 DTO 已定义 `DeletedTables`、`DeletedCosObjects`、`BackupId` 等字段(与前端 `FactoryResetResult` 接口对应)。
|
||
|
||
#### 文件 6:[frontend/src/views/admin/Settings.css](file:///d:/TraeProject/MilliSim4/frontend/src/views/admin/Settings.css)(追加样式)
|
||
|
||
追加:
|
||
|
||
```css
|
||
.danger-card { border: 1px solid #E5484D; }
|
||
.danger-title { color: #E5484D; }
|
||
.danger-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
.danger-info { flex: 1; }
|
||
.danger-name { font-weight: 600; color: var(--color-text-primary); margin-bottom: 4px; }
|
||
.danger-desc { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
|
||
```
|
||
|
||
### 阶段 E — 编译验证 + 重启
|
||
|
||
1. **前端 HMR 验证**:保存所有改动后,Vite dev server(5173 端口)应自动 HMR;检查控制台无 TypeScript 错误
|
||
2. **后端编译验证**:
|
||
|
||
* `taskkill /F /PID <pid>` 杀掉运行中的 MilliSim.Api.exe(如 PID 13788/22680,但需 `tasklist | findstr MilliSim` 确认当前 PID)
|
||
|
||
* `cd d:\TraeProject\MilliSim4\backend\MilliSim.Api && dotnet build`(预期 0 错误,仅有 2 个预存警告)
|
||
|
||
* 后台启动 `dotnet run --project d:\TraeProject\MilliSim4\backend\MilliSim.Api\MilliSim.Api.csproj`(run\_in\_background=true)
|
||
3. **功能验证清单**(不强制要求用户操作,但建议):
|
||
|
||
* 访问 `/admin/system/about` → 加载默认内容 → 修改 Hero 标题 → 保存 → 访问前台 `/about` 看到新内容
|
||
|
||
* 切换"前台显示"开关为关闭 → 前台导航不再显示"关于我们" → 直接访问 `/about` 重定向到首页
|
||
|
||
* 在 Settings 页点"恢复出厂设置" → 输入 RESET → 确认 → 看到"重置成功"提示 → 数据库业务表清空,admin 仍可登录
|
||
|
||
## 假设与决策
|
||
|
||
1. **AboutSettings.vue 列表交互**:使用数组 `splice`/`push` 直接操作(reactive 深层响应),不引入额外的拖拽库;上下移动用图标按钮
|
||
2. **图标名输入**:missionCards 的 `icon` 字段直接用文本输入框(让管理员填 `rocket`/`globe`/`heart` 等 SvgIcon 名称),不引入图标选择器,保持简单
|
||
3. **保存策略**:`showAbout` 开关 change 即时保存(独立于内容);主"保存全部内容"按钮保存 `AboutContent` JSON。这样切换开关不会丢失未保存的内容编辑(因为 updateAbout 同时发送 showAbout + content)
|
||
4. **路由守卫 async 化**:原 `beforeEach` 是同步,改为 async 会让首次路由跳转等待一次 `systemStore.load()`(main.ts 已先调过,通常是 no-op)
|
||
5. **不修改 main.ts**:保持 main.ts 已有的 `systemStore.load()` 调用,不在启动时调 `loadAbout()`(About.vue 自己 onMounted 调即可,避免首次启动多一次请求)
|
||
6. **危险操作不放在独立路由**:直接在 Settings.vue 末尾加卡片,符合"系统设置集中"的产品语义;不新建 `DangerZone.vue` 页
|
||
7. **FactoryResetResult 字段命名**:后端 DTO 已使用 PascalCase(`DeletedTables`、`DeletedCosObjects`),前端 ApiResponse 拦截器已统一转 camelCase(`deletedTables`、`deletedCosObjects`),按前端命名规范使用
|
||
|
||
## 验证步骤
|
||
|
||
实施完成后:
|
||
|
||
1. 前端 Vite 控制台无 TS 报错,`/admin/system/about` 路由可访问
|
||
2. 后端 `dotnet build` 输出 `Build succeeded, 0 Error`
|
||
3. 访问后台"系统设置"侧边栏看到"关于我们"子菜单
|
||
4. AboutSettings.vue 加载默认内容、修改保存、开关切换均生效
|
||
5. 前台 `/about` 显示修改后的内容;关闭开关后导航隐藏、直接访问重定向到首页
|
||
6. Settings.vue 危险操作卡片显示,输入 RESET 后调用工厂重置成功
|
||
|
||
## 任务清单(执行顺序)
|
||
|
||
* [ ] B1: 修改 router/index.ts(加 requiresAbout meta + admin 路由 + 守卫 showAbout 检查)
|
||
|
||
* [ ] B2: 修改 FrontendLayout.vue(menuItems 改 computed + footer 链接加 v-if)
|
||
|
||
* [ ] C1: 创建 AboutSettings.vue(内联所见即所得编辑器)
|
||
|
||
* [ ] C2: 创建 AboutSettings.css(内联编辑样式)
|
||
|
||
* [ ] D1: Settings.vue 追加危险操作卡片 + 工厂重置弹窗 + script 逻辑
|
||
|
||
* [ ] D2: Settings.css 追加 danger-card 样式
|
||
|
||
* [ ] E1: 杀掉后端进程 + dotnet build 验证
|
||
|
||
* [ ] E2: 重启后端 + 前端 HMR 验证
|
||
|