1
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
// 下拉/标签映射(与 biz/consts 对齐)
|
||||
export const eTypeMap = { 1: '场景', 2: '人物', 3: '道具' }
|
||||
export const nodeTypeMap = { 1: '决策', 2: '终局' }
|
||||
export const interactionMap = { 1: '选项选择', 2: '道具选择', 3: '步骤排序', 4: '动作过关', 5: '拖拽放置', 6: '接取收集', 7: '找线索', 8: '连线配对' }
|
||||
export const resultMap = { 0: '非终局', 1: '失败', 2: '良好', 3: '最佳' }
|
||||
export const pTypeMap = { 1: '虚拟', 2: '实物' }
|
||||
export const condTypeMap = { 1: '集卡数', 2: '连续签到', 3: '通关数', 4: '积分', 5: '完美关卡', 6: '复习完成' }
|
||||
export const redeemStatusMap = { 1: '待领取', 2: '待发货', 3: '已发货', 4: '已取消', 5: '已领取' }
|
||||
|
||||
export const statusTag = (status) => (status === 1 ? 'success' : 'info')
|
||||
export const statusText = (status) => (status === 1 ? '启用' : '禁用')
|
||||
@@ -2,18 +2,24 @@
|
||||
<el-container class="layout">
|
||||
<el-aside width="200px">
|
||||
<div class="logo">三十六计·后台</div>
|
||||
<el-menu :default-active="$route.path" router background-color="#1f2d3d" text-color="#c0c4cc" active-text-color="#409eff">
|
||||
<el-menu-item index="/strategy"><span>计策管理</span></el-menu-item>
|
||||
<el-menu-item index="/level"><span>关卡管理</span></el-menu-item>
|
||||
<el-menu-item index="/scene-node"><span>场景节点</span></el-menu-item>
|
||||
<el-menu-item index="/node-option"><span>分支选项</span></el-menu-item>
|
||||
<el-menu-item index="/element"><span>元素库</span></el-menu-item>
|
||||
<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="/parent"><span>家长管理</span></el-menu-item>
|
||||
<el-menu-item index="/child"><span>孩子管理</span></el-menu-item>
|
||||
<el-menu-item index="/redemption"><span>兑换管理</span></el-menu-item>
|
||||
<el-menu :default-active="$route.path" :default-openeds="['content', 'ops', 'users']" router background-color="#1f2d3d" text-color="#c0c4cc" active-text-color="#409eff">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="/stats"><span>关卡统计</span></el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
@@ -8,12 +8,9 @@ const router = createRouter({
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/strategy',
|
||||
redirect: '/content',
|
||||
children: [
|
||||
{ path: 'strategy', component: () => import('../views/Strategy.vue'), meta: { title: '计策管理' } },
|
||||
{ path: 'level', component: () => import('../views/Level.vue'), meta: { title: '关卡管理' } },
|
||||
{ path: 'scene-node', component: () => import('../views/SceneNode.vue'), meta: { title: '场景节点' } },
|
||||
{ path: 'node-option', component: () => import('../views/NodeOption.vue'), meta: { title: '分支选项' } },
|
||||
{ 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: '徽章管理' } },
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-button type="primary" @click="openCreate({ cond_type: 1, cond_value: 1 })">新增徽章</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column label="图标" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.icon" :src="row.icon" style="width: 40px; height: 40px" fit="cover" :preview-src-list="[row.icon]" preview-teleported />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="条件类型" width="110">
|
||||
<template #default="{ row }">{{ condTypeMap[row.cond_type] }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cond_value" label="条件值" width="90" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row, EDIT_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑徽章' : '新增徽章'" width="520px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="图标"><UploadField v-model="form.icon" kind="image" label="图标" /></el-form-item>
|
||||
<el-form-item label="条件类型" required>
|
||||
<el-select v-model="form.cond_type" style="width: 100%">
|
||||
<el-option v-for="(v, k) in condTypeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件值" required><el-input-number v-model="form.cond_value" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCrud } from '../composables/useCrud'
|
||||
import { condTypeMap, statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
|
||||
const EDIT_KEYS = ['id', 'name', 'icon', 'cond_type', 'cond_value']
|
||||
const crud = useCrud('/badge')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
load()
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-form-item label="家长ID">
|
||||
<el-input-number v-model="filterParent" :min="0" style="width: 140px" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="parent_id" label="家长ID" width="80" />
|
||||
<el-table-column prop="nickname" label="昵称" min-width="120" />
|
||||
<el-table-column label="头像" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-avatar v-if="row.avatar" :src="row.avatar" :size="36" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="age_group" label="年龄段" width="80" />
|
||||
<el-table-column prop="points" label="积分" width="70" />
|
||||
<el-table-column prop="daily_limit_minutes" label="每日上限(分)" width="110" />
|
||||
<el-table-column prop="perfect_count" label="完美关" width="80" />
|
||||
<el-table-column label="成长等级" width="140">
|
||||
<template #default="{ row }">{{ row.level_title }}(Lv{{ row.level }})</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="showDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="孩子详情" width="720px">
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions-item label="昵称">{{ detail.child.nickname }}</el-descriptions-item>
|
||||
<el-descriptions-item label="积分">{{ detail.child.points }}</el-descriptions-item>
|
||||
<el-descriptions-item label="成长等级">{{ detail.child.level_title }}</el-descriptions-item>
|
||||
<el-descriptions-item label="通关">{{ detail.passed }}</el-descriptions-item>
|
||||
<el-descriptions-item label="完美">{{ detail.perfect }}</el-descriptions-item>
|
||||
<el-descriptions-item label="星星">{{ detail.stars }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-table :data="detail.progress" border size="small" style="margin-top: 12px" max-height="400">
|
||||
<el-table-column prop="level_title" label="关卡" min-width="150" />
|
||||
<el-table-column prop="strategy_name" label="计策" min-width="120" />
|
||||
<el-table-column prop="stars" label="星星" width="70" />
|
||||
<el-table-column label="完美" width="70">
|
||||
<template #default="{ row }">{{ row.perfect ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="content_version" label="内容版本" width="90" />
|
||||
<el-table-column prop="completed_at" label="完成时间" width="170" />
|
||||
</el-table>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import request from '../request'
|
||||
import { statusTag, statusText } from '../constants'
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const filterParent = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await request.get('/child/list', { params: filterParent.value ? { parent_id: filterParent.value } : {} })
|
||||
list.value = (d && d.list) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
load()
|
||||
|
||||
const showDetail = async (row) => {
|
||||
detail.value = null
|
||||
dialogVisible.value = true
|
||||
detail.value = await request.get('/child/detail', { params: { child_id: row.id } })
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<el-container class="content-page">
|
||||
<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"
|
||||
node-key="id"
|
||||
:props="{ label: 'label', isLeaf: 'isLeaf' }"
|
||||
lazy
|
||||
:load="loadNode"
|
||||
highlight-current
|
||||
:expand-on-click-node="false"
|
||||
@node-click="onNodeClick"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<span class="tree-label">
|
||||
<span>{{ data.label }}</span>
|
||||
<el-tag v-if="data.status === 0" type="info" size="small">禁用</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-aside>
|
||||
|
||||
<el-main class="detail-panel">
|
||||
<div class="detail-head">
|
||||
<div class="crumb">
|
||||
<span v-if="current" class="crumb-label">当前位置</span>
|
||||
<span v-else class="crumb-label">请在左侧选择计策 / 关卡 / 决策节点</span>
|
||||
<el-breadcrumb v-if="current" separator="/">
|
||||
<el-breadcrumb-item v-for="(p, i) in current.path" :key="i">{{ p }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<template v-if="current && current.type === 'strategy'">
|
||||
<el-button type="primary" @click="levelCrud.openCreate({ strategy_id: current.rawId, age_group: '4-6', sort_order: 1 })">新增关卡</el-button>
|
||||
<el-button @click="strategyCrud.openEdit(current.raw, STRATEGY_KEYS)">编辑计策</el-button>
|
||||
<el-button :type="current.raw.status === 1 ? 'danger' : 'success'" @click="toggleStrategy">{{ current.raw.status === 1 ? '下架计策' : '上架计策' }}</el-button>
|
||||
</template>
|
||||
<template v-else-if="current && current.type === 'level'">
|
||||
<el-button type="primary" @click="nodeCrud.openCreate({ level_id: current.rawId, node_type: 1, sort_order: 1 })">新增节点</el-button>
|
||||
<el-button @click="levelCrud.openEdit(current.raw, LEVEL_KEYS)">编辑关卡</el-button>
|
||||
</template>
|
||||
<template v-else-if="current && current.type === 'node'">
|
||||
<el-button v-if="current.node_type === 1" type="primary" @click="optionCrud.openCreate({ node_id: current.rawId, sort_order: 1 })">新增选项</el-button>
|
||||
<el-button @click="nodeCrud.openEdit(current.raw, NODE_KEYS)">编辑节点</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关卡列表(选中计策) -->
|
||||
<el-table v-if="current && current.type === 'strategy'" :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="title" label="标题" min-width="160" />
|
||||
<el-table-column prop="scene_name" label="场景" width="100" />
|
||||
<el-table-column prop="age_group" label="年龄" width="80" />
|
||||
<el-table-column label="版本" width="80">
|
||||
<template #default="{ row }"><el-tag size="small">v{{ row.content_version }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="node_count" label="节点数" width="80" />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="levelCrud.openEdit(row, LEVEL_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleLevelRow(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 节点列表(选中关卡) -->
|
||||
<el-table v-else-if="current && current.type === 'level'" :data="list" v-loading="loading" border stripe>
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" width="120" />
|
||||
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="互动" width="110">
|
||||
<template #default="{ row }">{{ interactionMap[row.interaction_type] || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="终局评级" width="90">
|
||||
<template #default="{ row }">{{ resultMap[row.result_type] }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="character_name" label="角色" width="80" />
|
||||
<el-table-column label="入口" width="60">
|
||||
<template #default="{ row }">{{ row.is_entry === 1 ? '是' : '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="option_count" label="选项数" width="80" />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="nodeCrud.openEdit(row, NODE_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleNodeRow(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 选项列表(选中决策节点) -->
|
||||
<el-table v-else-if="current && current.type === 'node' && current.node_type === 1" :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="text" label="选项文本" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="prop_name" label="道具" width="90" />
|
||||
<el-table-column prop="next_node_title" label="下一节点" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="feedback" label="反馈" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="optionCrud.openEdit(row, OPTION_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleOptionRow(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty v-else-if="current && current.type === 'node'" description="终局节点,无分支选项" />
|
||||
|
||||
<!-- 计策对话框 -->
|
||||
<el-dialog v-model="strategyCrud.state.dialogVisible" :title="strategyCrud.state.isEdit ? '编辑计策' : '新增计策'" 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>
|
||||
<el-form-item label="组名"><el-input v-model="strategyCrud.state.form.group_name" /></el-form-item>
|
||||
<el-form-item label="释义"><el-input v-model="strategyCrud.state.form.meaning" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="教学内容"><el-input v-model="strategyCrud.state.form.teach_content" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="教学图"><UploadField v-model="strategyCrud.state.form.teach_image" kind="image" label="教学图" /></el-form-item>
|
||||
<el-form-item label="教学音频"><UploadField v-model="strategyCrud.state.form.teach_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="总结问题"><el-input v-model="strategyCrud.state.form.summary_q" /></el-form-item>
|
||||
<el-form-item label="总结选项"><el-input v-model="strategyCrud.state.form.summary_options" placeholder="多个选项用 | 分隔" /></el-form-item>
|
||||
<el-form-item label="总结音频"><UploadField v-model="strategyCrud.state.form.summary_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="图标"><UploadField v-model="strategyCrud.state.form.icon" kind="image" label="图标" /></el-form-item>
|
||||
<el-form-item label="解锁前置"><el-input-number v-model="strategyCrud.state.form.unlock_before" :min="0" /></el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="strategyCrud.state.form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="strategyCrud.state.dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveStrategy">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 关卡对话框 -->
|
||||
<el-dialog v-model="levelCrud.state.dialogVisible" :title="levelCrud.state.isEdit ? '编辑关卡' : '新增关卡'" width="680px">
|
||||
<el-form :model="levelCrud.state.form" label-width="100px">
|
||||
<el-form-item v-if="levelCrud.state.isEdit" label="计策" required>
|
||||
<el-select v-model="levelCrud.state.form.strategy_id" style="width: 100%">
|
||||
<el-option v-for="s in strategies" :key="s.id" :label="s.name" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" required><el-input v-model="levelCrud.state.form.title" /></el-form-item>
|
||||
<el-form-item label="场景元素ID"><el-input-number v-model="levelCrud.state.form.scene_id" :min="0" /></el-form-item>
|
||||
<el-form-item label="场景文案"><el-input v-model="levelCrud.state.form.scene_content" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="场景图"><UploadField v-model="levelCrud.state.form.scene_image" kind="image" label="场景图" /></el-form-item>
|
||||
<el-form-item label="场景音频"><UploadField v-model="levelCrud.state.form.scene_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="年龄段" required>
|
||||
<el-radio-group v-model="levelCrud.state.form.age_group">
|
||||
<el-radio value="4-6">4-6 岁</el-radio>
|
||||
<el-radio value="6-8">6-8 岁</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="levelCrud.state.form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="levelCrud.state.dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveLevel">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 节点对话框 -->
|
||||
<el-dialog v-model="nodeCrud.state.dialogVisible" :title="nodeCrud.state.isEdit ? '编辑节点' : '新增节点'" width="680px">
|
||||
<el-form :model="nodeCrud.state.form" label-width="100px">
|
||||
<el-form-item label="标题"><el-input v-model="nodeCrud.state.form.title" /></el-form-item>
|
||||
<el-form-item label="内容" required><el-input v-model="nodeCrud.state.form.content" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="节点类型" required>
|
||||
<el-radio-group v-model="nodeCrud.state.form.node_type">
|
||||
<el-radio :value="1">决策</el-radio>
|
||||
<el-radio :value="2">终局</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="互动形态">
|
||||
<el-select v-model="nodeCrud.state.form.interaction_type" style="width: 100%" :disabled="nodeCrud.state.form.node_type === 2">
|
||||
<el-option v-for="(v, k) in interactionMap" :key="k" :label="v" :value="Number(k)" />
|
||||
<el-option label="无" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="终局评级">
|
||||
<el-select v-model="nodeCrud.state.form.result_type" style="width: 100%" :disabled="nodeCrud.state.form.node_type === 1">
|
||||
<el-option v-for="(v, k) in resultMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色ID"><el-input-number v-model="nodeCrud.state.form.character_id" :min="0" /></el-form-item>
|
||||
<el-form-item label="入口节点">
|
||||
<el-radio-group v-model="nodeCrud.state.form.is_entry">
|
||||
<el-radio :value="0">否</el-radio>
|
||||
<el-radio :value="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片"><UploadField v-model="nodeCrud.state.form.image" kind="image" label="图片" /></el-form-item>
|
||||
<el-form-item label="音频"><UploadField v-model="nodeCrud.state.form.audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="配置"><el-input v-model="nodeCrud.state.form.config" type="textarea" :rows="2" placeholder="动作过关等子模式配置,JSON 或文本" /></el-form-item>
|
||||
<el-form-item label="剧本"><el-input v-model="nodeCrud.state.form.script" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="nodeCrud.state.form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="nodeCrud.state.dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveNode">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 选项对话框 -->
|
||||
<el-dialog v-model="optionCrud.state.dialogVisible" :title="optionCrud.state.isEdit ? '编辑选项' : '新增选项'" width="640px">
|
||||
<el-form :model="optionCrud.state.form" label-width="100px">
|
||||
<el-form-item label="选项文本" required><el-input v-model="optionCrud.state.form.text" /></el-form-item>
|
||||
<el-form-item label="下一节点ID" required><el-input-number v-model="optionCrud.state.form.next_node_id" :min="1" /></el-form-item>
|
||||
<el-form-item label="道具ID"><el-input-number v-model="optionCrud.state.form.prop_id" :min="0" /></el-form-item>
|
||||
<el-form-item label="选项音频"><UploadField v-model="optionCrud.state.form.audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="反馈文本"><el-input v-model="optionCrud.state.form.feedback" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="反馈音频"><UploadField v-model="optionCrud.state.form.feedback_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="optionCrud.state.form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="optionCrud.state.dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveOption">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nodeTypeMap, interactionMap, resultMap, statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
import request from '../request'
|
||||
|
||||
const STRATEGY_KEYS = ['id', 'name', 'group_no', 'group_name', 'meaning', 'teach_content', 'teach_image', 'teach_audio', 'summary_q', 'summary_options', 'summary_audio', 'icon', 'sort_order', 'unlock_before']
|
||||
const LEVEL_KEYS = ['id', 'strategy_id', 'title', 'scene_id', 'scene_content', 'scene_image', 'scene_audio', 'age_group', 'sort_order']
|
||||
const NODE_KEYS = ['id', 'level_id', 'title', 'character_id', 'content', 'image', 'audio', 'node_type', 'interaction_type', 'config', 'script', 'result_type', 'is_entry', 'sort_order']
|
||||
const OPTION_KEYS = ['id', 'node_id', 'text', 'prop_id', 'audio', 'next_node_id', 'feedback', 'feedback_audio', 'sort_order']
|
||||
|
||||
const treeRef = ref()
|
||||
const current = ref(null) // 选中的树节点数据 { id, type, rawId, label, status, raw, ... }
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const strategies = ref([])
|
||||
let autoSelectRoot = true
|
||||
|
||||
// 每类资源的 CRUD 状态与操作(保存/上下架后回调刷新右侧列表与树)
|
||||
const makeCrud = (prefix) => {
|
||||
const state = reactive({ dialogVisible: false, isEdit: false, form: {} })
|
||||
const openCreate = (blank) => {
|
||||
Object.keys(state.form).forEach((k) => delete state.form[k])
|
||||
Object.assign(state.form, blank)
|
||||
state.isEdit = false
|
||||
state.dialogVisible = true
|
||||
}
|
||||
const openEdit = (row, keys) => {
|
||||
Object.keys(state.form).forEach((k) => delete state.form[k])
|
||||
keys.forEach((k) => {
|
||||
state.form[k] = row[k] ?? ''
|
||||
})
|
||||
state.isEdit = true
|
||||
state.dialogVisible = true
|
||||
}
|
||||
const save = async (after) => {
|
||||
await request.post(state.isEdit ? `${prefix}/update` : `${prefix}/create`, { ...state.form })
|
||||
ElMessage.success('保存成功')
|
||||
state.dialogVisible = false
|
||||
after && after()
|
||||
}
|
||||
const toggleStatus = async (row, after) => {
|
||||
const target = row.status === 1 ? 'disable' : 'enable'
|
||||
await request.post(`${prefix}/${target}`, { id: row.id })
|
||||
ElMessage.success(row.status === 1 ? '已下架' : '已上架')
|
||||
after && after()
|
||||
}
|
||||
return { state, openCreate, openEdit, save, toggleStatus }
|
||||
}
|
||||
const strategyCrud = makeCrud('/strategy')
|
||||
const levelCrud = makeCrud('/level')
|
||||
const nodeCrud = makeCrud('/scene-node')
|
||||
const optionCrud = makeCrud('/node-option')
|
||||
|
||||
// 强制重载某个树节点的子节点(lazy 树),加载完成后回调
|
||||
const reloadTree = (node, after) => {
|
||||
if (!treeRef.value || !node) {
|
||||
after && after()
|
||||
return
|
||||
}
|
||||
node.loaded = false
|
||||
treeRef.value.loadNode(node, () => {
|
||||
after && after()
|
||||
})
|
||||
}
|
||||
|
||||
// ---------- 树 ----------
|
||||
const loadNode = async (node, resolve) => {
|
||||
if (node.level === 0) {
|
||||
const d = await request.get('/strategy/list')
|
||||
const items = ((d && d.list) || []).map((s) => ({ id: `s-${s.id}`, type: 'strategy', rawId: s.id, label: s.name, status: s.status, raw: s }))
|
||||
resolve(items)
|
||||
if (autoSelectRoot && items.length) {
|
||||
autoSelectRoot = false
|
||||
nextTick(() => {
|
||||
const treeNode = treeRef.value.getNode(items[0].id)
|
||||
if (treeNode) {
|
||||
treeNode.expanded = true
|
||||
onNodeClick(treeNode.data, treeNode)
|
||||
}
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (node.data.type === 'strategy') {
|
||||
const d = await request.get('/level/list', { params: { strategy_id: node.data.rawId } })
|
||||
resolve(((d && d.list) || []).map((l) => ({ id: `l-${l.id}`, type: 'level', rawId: l.id, label: l.title, status: l.status, raw: l })))
|
||||
return
|
||||
}
|
||||
if (node.data.type === 'level') {
|
||||
const d = await request.get('/scene-node/list', { params: { level_id: node.data.rawId } })
|
||||
resolve(((d && d.list) || []).map((n) => ({ id: `n-${n.id}`, type: 'node', rawId: n.id, label: n.title || n.content.slice(0, 10), status: n.status, node_type: n.node_type, isLeaf: true, raw: n })))
|
||||
return
|
||||
}
|
||||
resolve([])
|
||||
}
|
||||
|
||||
const loadList = async (api, params) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await request.get(api, { params })
|
||||
list.value = (d && d.list) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 从树节点向上收集祖先链,得到「计策 / 关卡 / 节点」路径
|
||||
const buildPath = (data, treeNode) => {
|
||||
const path = []
|
||||
let p = treeNode ? treeNode.parent : null
|
||||
while (p && p.level > 0) {
|
||||
path.unshift(p.data.label)
|
||||
p = p.parent
|
||||
}
|
||||
path.push(data.label)
|
||||
return path
|
||||
}
|
||||
|
||||
const onNodeClick = (data, treeNode) => {
|
||||
current.value = { ...data, path: buildPath(data, treeNode) }
|
||||
parentNodeRef.value = treeNode ? treeNode.parent : null
|
||||
if (data.type === 'strategy') {
|
||||
loadList('/level/list', { strategy_id: data.rawId })
|
||||
} else if (data.type === 'level') {
|
||||
loadList('/scene-node/list', { level_id: data.rawId })
|
||||
} else if (data.type === 'node' && data.node_type === 1) {
|
||||
loadList('/node-option/list', { node_id: data.rawId })
|
||||
} else {
|
||||
list.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 保存/上下架后的联动刷新:右侧列表 + 树中父节点子项
|
||||
const parentNodeRef = ref(null)
|
||||
const refreshTree = () => {
|
||||
reloadTree(parentNodeRef.value, () => {
|
||||
// 树重建后让 current 指向最新节点数据(标签/状态可能已变),path 保持
|
||||
const n = treeRef.value.getNode(current.value.id)
|
||||
if (n) current.value = { ...n.data, path: current.value.path }
|
||||
})
|
||||
}
|
||||
const refreshAfter = (api, params) => {
|
||||
loadList(api, params)
|
||||
refreshTree()
|
||||
}
|
||||
|
||||
// 计策保存:重载根节点(树数据变了),右侧无需(无列表)
|
||||
const saveStrategy = async () => {
|
||||
await strategyCrud.save(() => {
|
||||
reloadTree(treeRef.value.store.root, () => {
|
||||
const n = treeRef.value.getNode(current.value.id)
|
||||
if (n) current.value = { ...n.data, path: current.value.path }
|
||||
})
|
||||
})
|
||||
}
|
||||
const toggleStrategy = async () => {
|
||||
const target = current.value.raw.status === 1 ? 'disable' : 'enable'
|
||||
await request.post(`/strategy/${target}`, { id: current.value.rawId })
|
||||
ElMessage.success(current.value.raw.status === 1 ? '已下架' : '已上架')
|
||||
current.value.status = current.value.status === 1 ? 0 : 1
|
||||
current.value.raw.status = current.value.status
|
||||
reloadTree(treeRef.value.store.root, () => {
|
||||
const n = treeRef.value.getNode(current.value.id)
|
||||
if (n) current.value = { ...n.data, path: current.value.path }
|
||||
})
|
||||
}
|
||||
|
||||
// 关卡:保存/上下架后重载计策树节点 + 关卡列表
|
||||
const saveLevel = async () => {
|
||||
await levelCrud.save(() => refreshAfter('/level/list', { strategy_id: current.value.rawId }))
|
||||
}
|
||||
|
||||
// 节点:保存/上下架后重载关卡树节点 + 节点列表
|
||||
const saveNode = async () => {
|
||||
await nodeCrud.save(() => refreshAfter('/scene-node/list', { level_id: current.value.rawId }))
|
||||
}
|
||||
|
||||
// 选项:只刷新右侧列表
|
||||
const saveOption = async () => {
|
||||
await optionCrud.save(() => loadList('/node-option/list', { node_id: current.value.rawId }))
|
||||
}
|
||||
|
||||
// 关卡/节点的行内上下架同样联动树
|
||||
const toggleLevelRow = async (row) => {
|
||||
await levelCrud.toggleStatus(row, () => refreshAfter('/level/list', { strategy_id: current.value.rawId }))
|
||||
}
|
||||
const toggleNodeRow = async (row) => {
|
||||
await nodeCrud.toggleStatus(row, () => refreshAfter('/scene-node/list', { level_id: current.value.rawId }))
|
||||
}
|
||||
const toggleOptionRow = async (row) => {
|
||||
await optionCrud.toggleStatus(row, () => loadList('/node-option/list', { node_id: current.value.rawId }))
|
||||
}
|
||||
|
||||
// 关卡编辑对话框需要计策下拉
|
||||
request.get('/strategy/list').then((d) => {
|
||||
strategies.value = (d && d.list) || []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-page { height: calc(100vh - 60px); }
|
||||
.tree-panel { border-right: 1px solid #e4e7ed; overflow: auto; padding: 10px; }
|
||||
.tree-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; font-weight: 600; }
|
||||
.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; }
|
||||
.crumb { display: flex; align-items: center; gap: 8px; }
|
||||
.crumb-label { color: #909399; }
|
||||
</style>
|
||||
@@ -1,81 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-form-item label="计策">
|
||||
<el-select v-model="filterStrategy" style="width: 200px" @change="load({ strategy_id: filterStrategy })">
|
||||
<el-option v-for="s in strategies" :key="s.id" :label="`${s.name} (#${s.id})`" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" :disabled="!filterStrategy" @click="openCreate({ strategy_id: filterStrategy, age_group: '4-6', sort_order: 1 })">新增关卡</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="title" label="标题" min-width="160" />
|
||||
<el-table-column prop="scene_name" label="场景" width="100" />
|
||||
<el-table-column prop="age_group" label="年龄" width="80" />
|
||||
<el-table-column label="版本" width="80">
|
||||
<template #default="{ row }"><el-tag size="small">v{{ row.content_version }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="node_count" label="节点数" width="80" />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row, EDIT_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑关卡' : '新增关卡'" width="680px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="计策" required>
|
||||
<el-select v-model="form.strategy_id" style="width: 100%">
|
||||
<el-option v-for="s in strategies" :key="s.id" :label="s.name" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题" required><el-input v-model="form.title" /></el-form-item>
|
||||
<el-form-item label="场景元素ID"><el-input-number v-model="form.scene_id" :min="0" /></el-form-item>
|
||||
<el-form-item label="场景文案"><el-input v-model="form.scene_content" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="场景图"><UploadField v-model="form.scene_image" kind="image" label="场景图" /></el-form-item>
|
||||
<el-form-item label="场景音频"><UploadField v-model="form.scene_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="年龄段" required>
|
||||
<el-radio-group v-model="form.age_group">
|
||||
<el-radio value="4-6">4-6 岁</el-radio>
|
||||
<el-radio value="6-8">6-8 岁</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useCrud } from '../composables/useCrud'
|
||||
import { statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
import request from '../request'
|
||||
|
||||
const EDIT_KEYS = ['id', 'strategy_id', 'title', 'scene_id', 'scene_content', 'scene_image', 'scene_audio', 'age_group', 'sort_order']
|
||||
const strategies = ref([])
|
||||
const filterStrategy = ref(0)
|
||||
const crud = useCrud('/level')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
|
||||
request.get('/strategy/list').then((d) => {
|
||||
strategies.value = (d && d.list) || []
|
||||
if (strategies.value.length) {
|
||||
filterStrategy.value = strategies.value[0].id
|
||||
load({ strategy_id: filterStrategy.value })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,107 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-form-item label="计策">
|
||||
<el-select v-model="filterStrategy" style="width: 180px" @change="onStrategyChange">
|
||||
<el-option v-for="s in strategies" :key="s.id" :label="s.name" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关卡">
|
||||
<el-select v-model="filterLevel" style="width: 200px" @change="onLevelChange">
|
||||
<el-option v-for="l in levels" :key="l.id" :label="`${l.title} (#${l.id})`" :value="l.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="节点">
|
||||
<el-select v-model="filterNode" style="width: 180px" @change="load({ node_id: filterNode })">
|
||||
<el-option v-for="n in nodes" :key="n.id" :label="`${n.title || n.content.slice(0, 10)} (#${n.id})`" :value="n.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" :disabled="!filterNode" @click="openCreate({ node_id: filterNode, sort_order: 1 })">新增选项</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="node_title" label="所属节点" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="text" label="选项文本" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="prop_name" label="道具" width="90" />
|
||||
<el-table-column prop="next_node_title" label="下一节点" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="feedback" label="反馈" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row, EDIT_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑选项' : '新增选项'" width="640px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="选项文本" required><el-input v-model="form.text" /></el-form-item>
|
||||
<el-form-item label="下一节点ID" required><el-input-number v-model="form.next_node_id" :min="1" /></el-form-item>
|
||||
<el-form-item label="道具ID"><el-input-number v-model="form.prop_id" :min="0" /></el-form-item>
|
||||
<el-form-item label="选项音频"><UploadField v-model="form.audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="反馈文本"><el-input v-model="form.feedback" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="反馈音频"><UploadField v-model="form.feedback_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useCrud } from '../composables/useCrud'
|
||||
import { statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
import request from '../request'
|
||||
|
||||
const EDIT_KEYS = ['id', 'node_id', 'text', 'prop_id', 'audio', 'next_node_id', 'feedback', 'feedback_audio', 'sort_order']
|
||||
const strategies = ref([])
|
||||
const levels = ref([])
|
||||
const nodes = ref([])
|
||||
const filterStrategy = ref(0)
|
||||
const filterLevel = ref(0)
|
||||
const filterNode = ref(0)
|
||||
const crud = useCrud('/node-option')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
|
||||
const onStrategyChange = async () => {
|
||||
filterLevel.value = 0
|
||||
filterNode.value = 0
|
||||
levels.value = []
|
||||
nodes.value = []
|
||||
const d = await request.get('/level/list', { params: { strategy_id: filterStrategy.value } })
|
||||
levels.value = (d && d.list) || []
|
||||
if (levels.value.length) {
|
||||
filterLevel.value = levels.value[0].id
|
||||
await onLevelChange()
|
||||
}
|
||||
}
|
||||
|
||||
const onLevelChange = async () => {
|
||||
filterNode.value = 0
|
||||
nodes.value = []
|
||||
const d = await request.get('/scene-node/list', { params: { level_id: filterLevel.value } })
|
||||
nodes.value = (d && d.list) || []
|
||||
if (nodes.value.length) {
|
||||
filterNode.value = nodes.value[0].id
|
||||
load({ node_id: filterNode.value })
|
||||
}
|
||||
}
|
||||
|
||||
request.get('/strategy/list').then((d) => {
|
||||
strategies.value = (d && d.list) || []
|
||||
if (strategies.value.length) {
|
||||
filterStrategy.value = strategies.value[0].id
|
||||
onStrategyChange()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-form-item label="计策">
|
||||
<el-select v-model="filterStrategy" style="width: 180px" @change="onStrategyChange">
|
||||
<el-option v-for="s in strategies" :key="s.id" :label="s.name" :value="s.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关卡">
|
||||
<el-select v-model="filterLevel" style="width: 200px" @change="load({ level_id: filterLevel })">
|
||||
<el-option v-for="l in levels" :key="l.id" :label="`${l.title} (#${l.id})`" :value="l.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" :disabled="!filterLevel" @click="openCreate({ level_id: filterLevel, node_type: 1, sort_order: 1 })">新增节点</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" width="120" />
|
||||
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="互动" width="110">
|
||||
<template #default="{ row }">{{ interactionMap[row.interaction_type] || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="终局评级" width="90">
|
||||
<template #default="{ row }">{{ resultMap[row.result_type] }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="character_name" label="角色" width="80" />
|
||||
<el-table-column label="入口" width="60">
|
||||
<template #default="{ row }">{{ row.is_entry === 1 ? '是' : '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="option_count" label="选项数" width="80" />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row, EDIT_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑节点' : '新增节点'" width="680px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
|
||||
<el-form-item label="内容" required><el-input v-model="form.content" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="节点类型" required>
|
||||
<el-radio-group v-model="form.node_type">
|
||||
<el-radio :value="1">决策</el-radio>
|
||||
<el-radio :value="2">终局</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="互动形态">
|
||||
<el-select v-model="form.interaction_type" style="width: 100%" :disabled="form.node_type === 2">
|
||||
<el-option v-for="(v, k) in interactionMap" :key="k" :label="v" :value="Number(k)" />
|
||||
<el-option label="无" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="终局评级">
|
||||
<el-select v-model="form.result_type" style="width: 100%" :disabled="form.node_type === 1">
|
||||
<el-option v-for="(v, k) in resultMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色ID"><el-input-number v-model="form.character_id" :min="0" /></el-form-item>
|
||||
<el-form-item label="入口节点">
|
||||
<el-radio-group v-model="form.is_entry">
|
||||
<el-radio :value="0">否</el-radio>
|
||||
<el-radio :value="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片"><UploadField v-model="form.image" kind="image" label="图片" /></el-form-item>
|
||||
<el-form-item label="音频"><UploadField v-model="form.audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="配置"><el-input v-model="form.config" type="textarea" :rows="2" placeholder="动作过关等子模式配置,JSON 或文本" /></el-form-item>
|
||||
<el-form-item label="剧本"><el-input v-model="form.script" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useCrud } from '../composables/useCrud'
|
||||
import { nodeTypeMap, interactionMap, resultMap, statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
import request from '../request'
|
||||
|
||||
const EDIT_KEYS = ['id', 'level_id', 'title', 'character_id', 'content', 'image', 'audio', 'node_type', 'interaction_type', 'config', 'script', 'result_type', 'is_entry', 'sort_order']
|
||||
const strategies = ref([])
|
||||
const levels = ref([])
|
||||
const filterStrategy = ref(0)
|
||||
const filterLevel = ref(0)
|
||||
const crud = useCrud('/scene-node')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
|
||||
const onStrategyChange = async () => {
|
||||
filterLevel.value = 0
|
||||
levels.value = []
|
||||
const d = await request.get('/level/list', { params: { strategy_id: filterStrategy.value } })
|
||||
levels.value = (d && d.list) || []
|
||||
if (levels.value.length) {
|
||||
filterLevel.value = levels.value[0].id
|
||||
load({ level_id: filterLevel.value })
|
||||
}
|
||||
}
|
||||
|
||||
request.get('/strategy/list').then((d) => {
|
||||
strategies.value = (d && d.list) || []
|
||||
if (strategies.value.length) {
|
||||
filterStrategy.value = strategies.value[0].id
|
||||
onStrategyChange()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,61 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-button type="primary" @click="openCreate({ group_no: 1, sort_order: 1 })">新增计策</el-button>
|
||||
</el-form>
|
||||
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="名称" width="140" />
|
||||
<el-table-column prop="pinyin" label="拼音" min-width="150" />
|
||||
<el-table-column prop="group_no" label="组" width="60" />
|
||||
<el-table-column prop="group_name" label="组名" width="100" />
|
||||
<el-table-column prop="meaning" label="释义" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="unlock_before" label="前置" width="70" />
|
||||
<el-table-column prop="level_count" label="关卡数" width="80" />
|
||||
<el-table-column prop="sort_order" label="序号" width="70" />
|
||||
<el-table-column label="状态" width="70">
|
||||
<template #default="{ row }"><el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row, EDIT_KEYS)">编辑</el-button>
|
||||
<el-button link :type="row.status === 1 ? 'danger' : 'success'" @click="toggleStatus(row)">{{ row.status === 1 ? '下架' : '上架' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑计策' : '新增计策'" width="680px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="分组编号" required><el-input-number v-model="form.group_no" :min="1" :max="6" /></el-form-item>
|
||||
<el-form-item label="组名"><el-input v-model="form.group_name" /></el-form-item>
|
||||
<el-form-item label="释义"><el-input v-model="form.meaning" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="教学内容"><el-input v-model="form.teach_content" type="textarea" :rows="3" /></el-form-item>
|
||||
<el-form-item label="教学图"><UploadField v-model="form.teach_image" kind="image" label="教学图" /></el-form-item>
|
||||
<el-form-item label="教学音频"><UploadField v-model="form.teach_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="总结问题"><el-input v-model="form.summary_q" /></el-form-item>
|
||||
<el-form-item label="总结选项"><el-input v-model="form.summary_options" placeholder="多个选项用 | 分隔" /></el-form-item>
|
||||
<el-form-item label="总结音频"><UploadField v-model="form.summary_audio" kind="audio" label="音频" /></el-form-item>
|
||||
<el-form-item label="图标"><UploadField v-model="form.icon" kind="image" label="图标" /></el-form-item>
|
||||
<el-form-item label="解锁前置"><el-input-number v-model="form.unlock_before" :min="0" /></el-form-item>
|
||||
<el-form-item label="序号"><el-input-number v-model="form.sort_order" :min="1" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useCrud } from '../composables/useCrud'
|
||||
import { statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
|
||||
const EDIT_KEYS = ['id', 'name', 'group_no', 'group_name', 'meaning', 'teach_content', 'teach_image', 'teach_audio', 'summary_q', 'summary_options', 'summary_audio', 'icon', 'sort_order', 'unlock_before']
|
||||
const crud = useCrud('/strategy')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
load()
|
||||
</script>
|
||||
Reference in New Issue
Block a user