This commit is contained in:
2026-08-15 10:30:53 +08:00
parent 2cb52cd849
commit 0573cd8f40
11 changed files with 7910 additions and 435 deletions
+49 -25
View File
@@ -1,35 +1,47 @@
<template>
<el-container class="layout">
<el-aside width="200px">
<el-header class="header">
<div class="logo">三十六计·后台</div>
<el-menu :default-active="$route.path" :default-openeds="['content', 'ops', 'users']" router background-color="#1f2d3d" text-color="#c0c4cc" active-text-color="#409eff">
<el-menu
mode="horizontal"
:default-active="$route.path"
router
background-color="#1f2d3d"
text-color="#c0c4cc"
active-text-color="#409eff"
class="top-menu"
>
<el-sub-menu index="content">
<template #title><span>内容管理</span></template>
<el-menu-item index="/content"><span>内容管理</span></el-menu-item>
<el-menu-item index="/element"><span>元素库</span></el-menu-item>
<template #title>内容管理</template>
<el-menu-item index="/content">内容管理</el-menu-item>
<el-menu-item index="/element">元素库</el-menu-item>
</el-sub-menu>
<el-sub-menu index="ops">
<template #title><span>运营中心</span></template>
<el-menu-item index="/prize"><span>奖品管理</span></el-menu-item>
<el-menu-item index="/badge"><span>徽章管理</span></el-menu-item>
<el-menu-item index="/life-task"><span>生活任务</span></el-menu-item>
<el-menu-item index="/redemption"><span>兑换管理</span></el-menu-item>
<template #title>运营中心</template>
<el-menu-item index="/prize">奖品管理</el-menu-item>
<el-menu-item index="/badge">徽章管理</el-menu-item>
<el-menu-item index="/life-task">生活任务</el-menu-item>
<el-menu-item index="/redemption">兑换管理</el-menu-item>
</el-sub-menu>
<el-sub-menu index="users">
<template #title><span>用户管理</span></template>
<el-menu-item index="/parent"><span>家长管理</span></el-menu-item>
<el-menu-item index="/child"><span>孩子管理</span></el-menu-item>
<template #title>用户管理</template>
<el-menu-item index="/parent">家长管理</el-menu-item>
<el-menu-item index="/child">孩子管理</el-menu-item>
</el-sub-menu>
<el-menu-item index="/stats"><span>关卡统计</span></el-menu-item>
<el-menu-item index="/stats">关卡统计</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<span>{{ $route.meta.title }}</span>
<div class="right">
<el-button link type="primary" @click="logout">退出登录</el-button>
</el-header>
<el-main><router-view /></el-main>
</el-container>
</div>
</el-header>
<div class="breadcrumb-bar">
<el-breadcrumb separator="/">
<el-breadcrumb-item>三十六计·后台</el-breadcrumb-item>
<el-breadcrumb-item v-if="$route.meta.group">{{ $route.meta.group }}</el-breadcrumb-item>
<el-breadcrumb-item>{{ $route.meta.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<el-main><router-view /></el-main>
</el-container>
</template>
@@ -45,8 +57,20 @@ const logout = () => {
<style scoped>
.layout { height: 100vh; }
.logo { height: 60px; line-height: 60px; text-align: center; color: #fff; font-weight: 600; background: #1f2d3d; }
.el-aside { background: #1f2d3d; }
.el-menu { border-right: none; }
.header { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e4e7ed; }
.header {
display: flex;
align-items: center;
padding: 0 16px;
background: #1f2d3d;
border-bottom: none;
}
.logo { color: #fff; font-weight: 600; font-size: 16px; padding: 0 16px 0 0; white-space: nowrap; }
.el-menu--horizontal { border-bottom: none; }
.top-menu { flex: 1; min-width: 0; overflow-x: auto; }
.right { margin-left: auto; display: flex; align-items: center; gap: 12px; }
.breadcrumb-bar {
padding: 10px 16px;
background: #fff;
border-bottom: 1px solid #e4e7ed;
}
</style>
+1
View File
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import 'element-plus/dist/index.css'
import './styles.css'
import App from './App.vue'
import router from './router'
+8 -8
View File
@@ -10,14 +10,14 @@ const router = createRouter({
component: Layout,
redirect: '/content',
children: [
{ path: 'content', component: () => import('../views/Content.vue'), meta: { title: '内容管理' } },
{ path: 'element', component: () => import('../views/Element.vue'), meta: { title: '元素库' } },
{ path: 'prize', component: () => import('../views/Prize.vue'), meta: { title: '奖品管理' } },
{ path: 'badge', component: () => import('../views/Badge.vue'), meta: { title: '徽章管理' } },
{ path: 'life-task', component: () => import('../views/LifeTask.vue'), meta: { title: '生活任务' } },
{ path: 'parent', component: () => import('../views/Parent.vue'), meta: { title: '家长管理' } },
{ path: 'child', component: () => import('../views/Child.vue'), meta: { title: '孩子管理' } },
{ path: 'redemption', component: () => import('../views/Redemption.vue'), meta: { title: '兑换管理' } },
{ path: 'content', component: () => import('../views/Content.vue'), meta: { title: '内容管理', group: '内容管理' } },
{ path: 'element', component: () => import('../views/Element.vue'), meta: { title: '元素库', group: '内容管理' } },
{ path: 'prize', component: () => import('../views/Prize.vue'), meta: { title: '奖品管理', group: '运营中心' } },
{ path: 'badge', component: () => import('../views/Badge.vue'), meta: { title: '徽章管理', group: '运营中心' } },
{ path: 'life-task', component: () => import('../views/LifeTask.vue'), meta: { title: '生活任务', group: '运营中心' } },
{ path: 'parent', component: () => import('../views/Parent.vue'), meta: { title: '家长管理', group: '用户管理' } },
{ path: 'child', component: () => import('../views/Child.vue'), meta: { title: '孩子管理', group: '用户管理' } },
{ path: 'redemption', component: () => import('../views/Redemption.vue'), meta: { title: '兑换管理', group: '运营中心' } },
{ path: 'stats', component: () => import('../views/Stats.vue'), meta: { title: '关卡统计' } },
],
},
+5
View File
@@ -0,0 +1,5 @@
html, body, #app {
margin: 0;
padding: 0;
height: 100%;
}
+41 -6
View File
@@ -3,7 +3,6 @@
<el-aside width="330px" class="tree-panel">
<div class="tree-head">
<span>内容结构</span>
<el-button size="small" type="primary" plain @click="strategyCrud.openCreate({ group_no: 1, sort_order: 1 })">新增计策</el-button>
</div>
<el-tree
ref="treeRef"
@@ -18,6 +17,7 @@
<template #default="{ data }">
<span class="tree-label">
<span>{{ data.label }}</span>
<el-tag v-if="data.type === 'node' && data.node_type === 2" type="warning" size="small">终局</el-tag>
<el-tag v-if="data.status === 0" type="info" size="small">禁用</el-tag>
</span>
</template>
@@ -73,7 +73,24 @@
</el-table>
<!-- 节点列表选中关卡 -->
<el-table v-else-if="current && current.type === 'level'" :data="list" v-loading="loading" border stripe>
<el-table v-else-if="current && current.type === 'level'" :data="list" v-loading="loading" border stripe @expand-change="onNodeExpand">
<el-table-column type="expand">
<template #default="{ row }">
<div v-if="row.node_type === 1" class="node-opts">
<div v-if="row._optionsLoading" class="opt-hint">加载中</div>
<template v-else>
<div v-for="o in row._options || []" :key="o.id" class="opt-row">
<el-tag size="small" :type="o.status === 1 ? 'success' : 'info'">{{ o.status === 1 ? '上架' : '下架' }}</el-tag>
<span class="opt-text">{{ o.text }}</span>
<el-tag v-if="o.prop_name" size="small" type="warning">{{ o.prop_name }}</el-tag>
<span class="opt-next"> {{ o.next_node_title }}</span>
</div>
<div v-if="!row._options || !row._options.length" class="opt-hint">该节点暂无选项</div>
</template>
</div>
<div v-else class="opt-hint">终局节点无分支选项</div>
</template>
</el-table-column>
<el-table-column prop="id" label="ID" width="70" />
<el-table-column label="类型" width="70">
<template #default="{ row }"><el-tag :type="row.node_type === 1 ? 'primary' : 'warning'" size="small">{{ nodeTypeMap[row.node_type] }}</el-tag></template>
@@ -125,7 +142,7 @@
<el-empty v-else-if="current && current.type === 'node'" description="终局节点,无分支选项" />
<!-- 计策对话框 -->
<el-dialog v-model="strategyCrud.state.dialogVisible" :title="strategyCrud.state.isEdit ? '编辑计策' : '新增计策'" width="680px">
<el-dialog v-model="strategyCrud.state.dialogVisible" title="编辑计策" width="680px">
<el-form :model="strategyCrud.state.form" label-width="100px">
<el-form-item label="名称" required><el-input v-model="strategyCrud.state.form.name" /></el-form-item>
<el-form-item label="分组编号" required><el-input-number v-model="strategyCrud.state.form.group_no" :min="1" :max="6" /></el-form-item>
@@ -290,14 +307,15 @@ const levelCrud = makeCrud('/level')
const nodeCrud = makeCrud('/scene-node')
const optionCrud = makeCrud('/node-option')
// 强制重载某个树节点的子节点(lazy 树),加载完成后回调
// 强制重载某个树节点的子节点(lazy 树):置 loaded=false 后走 Node.expand 触发 loadData
// (expanded 是普通属性,直接赋值不会触发懒加载;loadNode 也不在树组件公开方法中)
const reloadTree = (node, after) => {
if (!treeRef.value || !node) {
after && after()
return
}
node.loaded = false
treeRef.value.loadNode(node, () => {
node.expand(() => {
after && after()
})
}
@@ -313,7 +331,7 @@ const loadNode = async (node, resolve) => {
nextTick(() => {
const treeNode = treeRef.value.getNode(items[0].id)
if (treeNode) {
treeNode.expanded = true
treeNode.expand()
onNodeClick(treeNode.data, treeNode)
}
})
@@ -343,6 +361,18 @@ const loadList = async (api, params) => {
}
}
// 展开节点行时懒加载该节点的分支选项
const onNodeExpand = async (row, expanded) => {
if (!expanded || row.node_type !== 1 || row._options) return
row._optionsLoading = true
try {
const d = await request.get('/node-option/list', { params: { node_id: row.id } })
row._options = (d && d.list) || []
} finally {
row._optionsLoading = false
}
}
// 从树节点向上收集祖先链,得到「计策 / 关卡 / 节点」路径
const buildPath = (data, treeNode) => {
const path = []
@@ -443,6 +473,11 @@ request.get('/strategy/list').then((d) => {
.tree-label { display: inline-flex; align-items: center; gap: 6px; }
.detail-panel { padding: 10px 16px; }
.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
.node-opts { padding: 8px 24px; display: flex; flex-direction: column; gap: 8px; }
.opt-row { display: flex; align-items: center; gap: 10px; }
.opt-text { font-weight: 600; color: #5b4636; }
.opt-next { color: #909399; }
.opt-hint { color: #a08c74; padding: 8px 24px; }
.crumb { display: flex; align-items: center; gap: 8px; }
.crumb-label { color: #909399; }
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff