feat: 重构服务支持双模式并接入 MongoDB 数据库

- 新增 MongoDB 存储层实现英雄、预设和版本数据的持久化
- 新增 push-meta 和 migrate-to-mongo 脚本支持数据同步迁移
- 支持开发版和线上设计版双模式运行
- 新增版本闸门机制,线上设计版落后时进入只读模式
- 优化前端 API 配置适配不同部署环境
- 新增线上设计版静态资源托管和路由回退
This commit is contained in:
panFD
2026-10-03 12:02:47 +08:00
parent 1bb275607a
commit 9c9cc99e64
14 changed files with 656 additions and 48 deletions

88
put/web/dist/assets/index-CZTxENXL.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
put/web/dist/index.html vendored Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>英雄配置编辑器</title>
<script type="module" crossorigin src="/assets/index-CZTxENXL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dzv_EDk3.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@@ -1,18 +1,33 @@
<template>
<div class="hero-app">
<header class="hero-header">
<h1>英雄配置编辑器</h1>
<h1>
英雄配置编辑器
<el-tag size="small" :type="isDev ? 'success' : 'warning'" effect="plain">
{{ isDev ? '项目开发版' : '线上设计版' }} v{{ meta?.schemaVersion ?? '-' }}
</el-tag>
</h1>
<div class="actions">
<el-radio-group v-model="view" size="small">
<el-radio-button value="heroes">英雄列表</el-radio-button>
<el-radio-button value="templates">预设库</el-radio-button>
</el-radio-group>
<el-button size="small" @click="refreshMeta" :loading="loadingMeta">刷新配置数据</el-button>
<el-button size="small" type="success" @click="doSave" :disabled="!heroes.length">保存到服务器</el-button>
<el-button size="small" type="primary" @click="doApply" :disabled="!heroes.length">写入 heroSet.ts</el-button>
<el-button size="small" type="danger" @click="doExportCsv" :disabled="!heroes.length">导出英雄设计表.csv</el-button>
<el-button size="small" type="success" @click="doSave" :disabled="!heroes.length || outdated">保存到数据库</el-button>
<template v-if="isDev">
<el-button size="small" type="primary" @click="doApply" :disabled="!heroes.length">写入 heroSet.ts</el-button>
<el-button size="small" type="danger" @click="doExportCsv" :disabled="!heroes.length">导出英雄设计表.csv</el-button>
</template>
</div>
</header>
<el-alert
v-if="outdated"
type="error"
:closable="false"
show-icon
:title="`数据库结构已升级到 v${meta.dbSchemaVersion},当前线上设计版(v${meta.schemaVersion})已切换为只读,请更新部署后再修改`"
style="margin-bottom: 8px"
/>
<div class="hero-main">
<!-- 预设库视图 -->
<TemplateManager
@@ -252,6 +267,10 @@ const editVisible = ref(false)
const current = computed(() => heroes.value[activeIdx.value])
// 版本/模式:isDev 区分开发版全功能;outdated 时线上设计版只读
const isDev = computed(() => meta.value?.mode === 'local')
const outdated = computed(() => (meta.value?.dbSchemaVersion ?? 0) > (meta.value?.schemaVersion ?? 0))
// 首页列表排序:none 原始顺序 / role 按职业枚举序(坦克→辅助)/ rarity 按星级
const sortKey = ref('none')
const sortOrder = ref('desc')
@@ -510,7 +529,7 @@ async function loadTemplates() {
async function doSave() {
try {
const r = await api.saveHeroes(heroes.value)
ElMessage.success(`已保存 ${r.count} 个英雄 → ${r.path}`)
ElMessage.success(`已保存 ${r.count} 个英雄到数据库`)
} catch (e) {
ElMessage.error('保存失败: ' + (e?.response?.data?.error || e.message))
}

View File

@@ -1,12 +1,16 @@
import axios from 'axios'
// 开发环境走 vite 代理(留空即可);部署静态站点时用 VITE_API_BASE 指向后端服务
const baseURL = import.meta.env.VITE_API_BASE || ''
const http = axios.create({ baseURL })
export const api = {
meta: () => axios.get('/api/meta').then((r) => r.data),
refresh: () => axios.post('/api/refresh').then((r) => r.data),
fetchHeroes: () => axios.get('/api/heroes').then((r) => r.data),
saveHeroes: (heroes) => axios.post('/api/heroes', { heroes }).then((r) => r.data),
applyHeroes: (heroes) => axios.post('/api/apply', { heroes }).then((r) => r.data),
exportCsv: (heroes) => axios.post('/api/export-csv', { heroes }).then((r) => r.data),
fetchTemplates: () => axios.get('/api/templates').then((r) => r.data),
saveTemplates: (templates) => axios.post('/api/templates', { templates }).then((r) => r.data),
meta: () => http.get('/api/meta').then((r) => r.data),
refresh: () => http.post('/api/refresh').then((r) => r.data),
fetchHeroes: () => http.get('/api/heroes').then((r) => r.data),
saveHeroes: (heroes) => http.post('/api/heroes', { heroes }).then((r) => r.data),
applyHeroes: (heroes) => http.post('/api/apply', { heroes }).then((r) => r.data),
exportCsv: (heroes) => http.post('/api/export-csv', { heroes }).then((r) => r.data),
fetchTemplates: () => http.get('/api/templates').then((r) => r.data),
saveTemplates: (templates) => http.post('/api/templates', { templates }).then((r) => r.data),
}