1
This commit is contained in:
@@ -1,35 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="layout">
|
<el-container class="layout">
|
||||||
<el-aside width="200px">
|
<el-header class="header">
|
||||||
<div class="logo">三十六计·后台</div>
|
<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">
|
<el-sub-menu index="content">
|
||||||
<template #title><span>内容管理</span></template>
|
<template #title>内容管理</template>
|
||||||
<el-menu-item index="/content"><span>内容管理</span></el-menu-item>
|
<el-menu-item index="/content">内容管理</el-menu-item>
|
||||||
<el-menu-item index="/element"><span>元素库</span></el-menu-item>
|
<el-menu-item index="/element">元素库</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
<el-sub-menu index="ops">
|
<el-sub-menu index="ops">
|
||||||
<template #title><span>运营中心</span></template>
|
<template #title>运营中心</template>
|
||||||
<el-menu-item index="/prize"><span>奖品管理</span></el-menu-item>
|
<el-menu-item index="/prize">奖品管理</el-menu-item>
|
||||||
<el-menu-item index="/badge"><span>徽章管理</span></el-menu-item>
|
<el-menu-item index="/badge">徽章管理</el-menu-item>
|
||||||
<el-menu-item index="/life-task"><span>生活任务</span></el-menu-item>
|
<el-menu-item index="/life-task">生活任务</el-menu-item>
|
||||||
<el-menu-item index="/redemption"><span>兑换管理</span></el-menu-item>
|
<el-menu-item index="/redemption">兑换管理</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
<el-sub-menu index="users">
|
<el-sub-menu index="users">
|
||||||
<template #title><span>用户管理</span></template>
|
<template #title>用户管理</template>
|
||||||
<el-menu-item index="/parent"><span>家长管理</span></el-menu-item>
|
<el-menu-item index="/parent">家长管理</el-menu-item>
|
||||||
<el-menu-item index="/child"><span>孩子管理</span></el-menu-item>
|
<el-menu-item index="/child">孩子管理</el-menu-item>
|
||||||
</el-sub-menu>
|
</el-sub-menu>
|
||||||
<el-menu-item index="/stats"><span>关卡统计</span></el-menu-item>
|
<el-menu-item index="/stats">关卡统计</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
<div class="right">
|
||||||
<el-container>
|
|
||||||
<el-header class="header">
|
|
||||||
<span>{{ $route.meta.title }}</span>
|
|
||||||
<el-button link type="primary" @click="logout">退出登录</el-button>
|
<el-button link type="primary" @click="logout">退出登录</el-button>
|
||||||
</el-header>
|
</div>
|
||||||
<el-main><router-view /></el-main>
|
</el-header>
|
||||||
</el-container>
|
<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>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,8 +57,20 @@ const logout = () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.layout { height: 100vh; }
|
.layout { height: 100vh; }
|
||||||
.logo { height: 60px; line-height: 60px; text-align: center; color: #fff; font-weight: 600; background: #1f2d3d; }
|
.header {
|
||||||
.el-aside { background: #1f2d3d; }
|
display: flex;
|
||||||
.el-menu { border-right: none; }
|
align-items: center;
|
||||||
.header { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e4e7ed; }
|
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>
|
</style>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
import './styles.css'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ const router = createRouter({
|
|||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/content',
|
redirect: '/content',
|
||||||
children: [
|
children: [
|
||||||
{ path: 'content', component: () => import('../views/Content.vue'), meta: { title: '内容管理' } },
|
{ path: 'content', component: () => import('../views/Content.vue'), meta: { title: '内容管理', group: '内容管理' } },
|
||||||
{ path: 'element', component: () => import('../views/Element.vue'), meta: { title: '元素库' } },
|
{ path: 'element', component: () => import('../views/Element.vue'), meta: { title: '元素库', group: '内容管理' } },
|
||||||
{ path: 'prize', component: () => import('../views/Prize.vue'), meta: { title: '奖品管理' } },
|
{ path: 'prize', component: () => import('../views/Prize.vue'), meta: { title: '奖品管理', group: '运营中心' } },
|
||||||
{ path: 'badge', component: () => import('../views/Badge.vue'), meta: { title: '徽章管理' } },
|
{ path: 'badge', component: () => import('../views/Badge.vue'), meta: { title: '徽章管理', group: '运营中心' } },
|
||||||
{ path: 'life-task', component: () => import('../views/LifeTask.vue'), meta: { title: '生活任务' } },
|
{ path: 'life-task', component: () => import('../views/LifeTask.vue'), meta: { title: '生活任务', group: '运营中心' } },
|
||||||
{ path: 'parent', component: () => import('../views/Parent.vue'), meta: { title: '家长管理' } },
|
{ path: 'parent', component: () => import('../views/Parent.vue'), meta: { title: '家长管理', group: '用户管理' } },
|
||||||
{ path: 'child', component: () => import('../views/Child.vue'), meta: { title: '孩子管理' } },
|
{ path: 'child', component: () => import('../views/Child.vue'), meta: { title: '孩子管理', group: '用户管理' } },
|
||||||
{ path: 'redemption', component: () => import('../views/Redemption.vue'), meta: { title: '兑换管理' } },
|
{ path: 'redemption', component: () => import('../views/Redemption.vue'), meta: { title: '兑换管理', group: '运营中心' } },
|
||||||
{ path: 'stats', component: () => import('../views/Stats.vue'), meta: { title: '关卡统计' } },
|
{ path: 'stats', component: () => import('../views/Stats.vue'), meta: { title: '关卡统计' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
html, body, #app {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
<el-aside width="330px" class="tree-panel">
|
<el-aside width="330px" class="tree-panel">
|
||||||
<div class="tree-head">
|
<div class="tree-head">
|
||||||
<span>内容结构</span>
|
<span>内容结构</span>
|
||||||
<el-button size="small" type="primary" plain @click="strategyCrud.openCreate({ group_no: 1, sort_order: 1 })">新增计策</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
@@ -18,6 +17,7 @@
|
|||||||
<template #default="{ data }">
|
<template #default="{ data }">
|
||||||
<span class="tree-label">
|
<span class="tree-label">
|
||||||
<span>{{ data.label }}</span>
|
<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>
|
<el-tag v-if="data.status === 0" type="info" size="small">禁用</el-tag>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -73,7 +73,24 @@
|
|||||||
</el-table>
|
</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 prop="id" label="ID" width="70" />
|
||||||
<el-table-column label="类型" 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>
|
<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-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 :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 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>
|
<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 nodeCrud = makeCrud('/scene-node')
|
||||||
const optionCrud = makeCrud('/node-option')
|
const optionCrud = makeCrud('/node-option')
|
||||||
|
|
||||||
// 强制重载某个树节点的子节点(lazy 树),加载完成后回调
|
// 强制重载某个树节点的子节点(lazy 树):置 loaded=false 后走 Node.expand 触发 loadData
|
||||||
|
// (expanded 是普通属性,直接赋值不会触发懒加载;loadNode 也不在树组件公开方法中)
|
||||||
const reloadTree = (node, after) => {
|
const reloadTree = (node, after) => {
|
||||||
if (!treeRef.value || !node) {
|
if (!treeRef.value || !node) {
|
||||||
after && after()
|
after && after()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node.loaded = false
|
node.loaded = false
|
||||||
treeRef.value.loadNode(node, () => {
|
node.expand(() => {
|
||||||
after && after()
|
after && after()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -313,7 +331,7 @@ const loadNode = async (node, resolve) => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const treeNode = treeRef.value.getNode(items[0].id)
|
const treeNode = treeRef.value.getNode(items[0].id)
|
||||||
if (treeNode) {
|
if (treeNode) {
|
||||||
treeNode.expanded = true
|
treeNode.expand()
|
||||||
onNodeClick(treeNode.data, treeNode)
|
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 buildPath = (data, treeNode) => {
|
||||||
const path = []
|
const path = []
|
||||||
@@ -443,6 +473,11 @@ request.get('/strategy/list').then((d) => {
|
|||||||
.tree-label { display: inline-flex; align-items: center; gap: 6px; }
|
.tree-label { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.detail-panel { padding: 10px 16px; }
|
.detail-panel { padding: 10px 16px; }
|
||||||
.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; flex-wrap: wrap; }
|
.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 { display: flex; align-items: center; gap: 8px; }
|
||||||
.crumb-label { color: #909399; }
|
.crumb-label { color: #909399; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+1300
-66
File diff suppressed because it is too large
Load Diff
+1302
-66
File diff suppressed because it is too large
Load Diff
+1300
-66
File diff suppressed because it is too large
Load Diff
+1301
-66
File diff suppressed because it is too large
Load Diff
+1302
-66
File diff suppressed because it is too large
Load Diff
+1301
-66
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user