1
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>三十六计·管理后台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1929
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "admin-src",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"element-plus": "^2.9.5",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="upload-field">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:http-request="doUpload"
|
||||
:accept="accept"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<el-button size="small" :loading="loading">{{ label }}:上传</el-button>
|
||||
</el-upload>
|
||||
<span v-if="modelValue" class="preview">
|
||||
<el-image v-if="kind === 'image'" :src="modelValue" style="width: 56px; height: 56px" fit="cover" />
|
||||
<el-button v-else link type="primary" @click="playAudio">试听</el-button>
|
||||
<el-button link type="danger" @click="$emit('update:modelValue', '')">清除</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../request'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
kind: { type: String, default: 'image' }, // image | audio
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const loading = ref(false)
|
||||
const accept = props.kind === 'image' ? 'image/*' : 'audio/*'
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const max = props.kind === 'image' ? 10 * 1024 * 1024 : 20 * 1024 * 1024
|
||||
if (file.size > max) {
|
||||
ElMessage.error('文件过大')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const doUpload = async ({ file }) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
const data = await request.post('/upload', fd)
|
||||
emit('update:modelValue', data.url)
|
||||
ElMessage.success('上传成功')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const playAudio = () => {
|
||||
const a = new Audio(props.modelValue)
|
||||
a.play()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-field { display: flex; align-items: center; gap: 8px; }
|
||||
.preview { display: inline-flex; align-items: center; gap: 4px; }
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../request'
|
||||
|
||||
// 通用五件套(list/create/update/disable/enable)页面状态与操作
|
||||
export function useCrud(prefix) {
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({})
|
||||
|
||||
const load = async (params = {}) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await request.get(`${prefix}/list`, { params })
|
||||
list.value = (data && data.list) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.keys(form).forEach((k) => delete form[k])
|
||||
}
|
||||
|
||||
const openCreate = (blank) => {
|
||||
resetForm()
|
||||
Object.assign(form, blank)
|
||||
isEdit.value = false
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEdit = (row, keys) => {
|
||||
resetForm()
|
||||
keys.forEach((k) => {
|
||||
form[k] = row[k] ?? ''
|
||||
})
|
||||
isEdit.value = true
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
await request.post(isEdit.value ? `${prefix}/update` : `${prefix}/create`, { ...form })
|
||||
ElMessage.success('保存成功')
|
||||
dialogVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
const toggleStatus = async (row) => {
|
||||
const target = row.status === 1 ? 'disable' : 'enable'
|
||||
await request.post(`${prefix}/${target}`, { id: row.id })
|
||||
ElMessage.success(row.status === 1 ? '已下架' : '已上架')
|
||||
load()
|
||||
}
|
||||
|
||||
return { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<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-item index="/stats"><span>关卡统计</span></el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<span>{{ $route.meta.title }}</span>
|
||||
<el-button link type="primary" @click="logout">退出登录</el-button>
|
||||
</el-header>
|
||||
<el-main><router-view /></el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const logout = () => {
|
||||
localStorage.removeItem('admin_token')
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<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; }
|
||||
</style>
|
||||
@@ -0,0 +1,11 @@
|
||||
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 App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,34 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import router from './router'
|
||||
|
||||
const request = axios.create({ baseURL: '/api/admin', timeout: 15000 })
|
||||
|
||||
request.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('admin_token')
|
||||
if (token) config.headers.Authorization = `Bearer ${token}`
|
||||
return config
|
||||
})
|
||||
|
||||
request.interceptors.response.use(
|
||||
(resp) => {
|
||||
const data = resp.data
|
||||
if (data.code !== 0) {
|
||||
ElMessage.error(data.message || '请求失败')
|
||||
return Promise.reject(new Error(data.message))
|
||||
}
|
||||
return data.data
|
||||
},
|
||||
(err) => {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('admin_token')
|
||||
router.push('/login')
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
} else {
|
||||
ElMessage.error(err.response?.data?.message || err.message || '网络错误')
|
||||
}
|
||||
return Promise.reject(err)
|
||||
},
|
||||
)
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import Layout from '../layout/index.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: [
|
||||
{ path: '/login', component: () => import('../views/Login.vue') },
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/strategy',
|
||||
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: '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: 'stats', component: () => import('../views/Stats.vue'), meta: { title: '关卡统计' } },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.path !== '/login' && !localStorage.getItem('admin_token')) {
|
||||
return '/login'
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="filterEType" style="width: 140px" @change="load()">
|
||||
<el-option label="全部" :value="0" />
|
||||
<el-option v-for="(v, k) in eTypeMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" @click="openCreate({ e_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="80">
|
||||
<template #default="{ row }">{{ eTypeMap[row.e_type] }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column prop="name_pinyin" label="拼音" min-width="160" />
|
||||
<el-table-column label="图片" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.image" :src="row.image" style="width: 40px; height: 40px" fit="cover" :preview-src-list="[row.image]" preview-teleported />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="180" 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, ['id', 'e_type', 'name', 'image', 'audio', 'description', 'sort_order'])">编辑</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="560px">
|
||||
<el-form :model="form" label-width="80px">
|
||||
<el-form-item label="类型" required>
|
||||
<el-radio-group v-model="form.e_type">
|
||||
<el-radio v-for="(v, k) in eTypeMap" :key="k" :value="Number(k)">{{ v }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" required><el-input v-model="form.name" /></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.description" type="textarea" :rows="3" /></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 { eTypeMap, statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
|
||||
const filterEType = ref(0)
|
||||
const crud = useCrud('/element')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
load({ e_type: 0 })
|
||||
</script>
|
||||
@@ -0,0 +1,81 @@
|
||||
<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>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-button type="primary" @click="openCreate({ reward_points: 5 })">新增任务</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="strategy_name" label="关联计策" min-width="140" />
|
||||
<el-table-column prop="title" label="标题" min-width="180" />
|
||||
<el-table-column prop="reward_points" label="奖励积分" width="90" />
|
||||
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="guide" label="引导" min-width="160" show-overflow-tooltip />
|
||||
<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="600px">
|
||||
<el-form :model="form" label-width="90px">
|
||||
<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="描述"><el-input v-model="form.description" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="引导"><el-input v-model="form.guide" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="奖励积分"><el-input-number v-model="form.reward_points" :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 request from '../request'
|
||||
|
||||
const EDIT_KEYS = ['id', 'strategy_id', 'title', 'description', 'guide', 'reward_points']
|
||||
const strategies = ref([])
|
||||
const crud = useCrud('/life-task')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
load()
|
||||
request.get('/strategy/list').then((d) => {
|
||||
strategies.value = (d && d.list) || []
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="login-wrap">
|
||||
<el-card class="login-card">
|
||||
<h2>三十六计·管理后台</h2>
|
||||
<el-form :model="form" @keyup.enter="submit">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.username" placeholder="用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" show-password />
|
||||
</el-form-item>
|
||||
<el-button type="primary" style="width: 100%" :loading="loading" @click="submit">登 录</el-button>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../request'
|
||||
|
||||
const router = useRouter()
|
||||
const form = reactive({ username: '', password: '' })
|
||||
const loading = ref(false)
|
||||
|
||||
const submit = async () => {
|
||||
if (!form.username || !form.password) {
|
||||
ElMessage.warning('请输入用户名和密码')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await request.post('/login', form)
|
||||
localStorage.setItem('admin_token', data.token)
|
||||
router.push('/')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrap { display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5; }
|
||||
.login-card { width: 360px; padding: 12px 8px; }
|
||||
.login-card h2 { text-align: center; margin: 8px 0 24px; }
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<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>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="list" v-loading="loading" border stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="phone" label="手机号" width="130" />
|
||||
<el-table-column prop="nickname" label="昵称" min-width="140" />
|
||||
<el-table-column label="头像" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-avatar v-if="row.avatar" :src="row.avatar" :size="40" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="child_count" label="孩子数" width="80" />
|
||||
<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 prop="created_at" label="注册时间" width="180" />
|
||||
</el-table>
|
||||
</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 load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await request.get('/parent/list')
|
||||
list.value = (d && d.list) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
load()
|
||||
</script>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-button type="primary" @click="openCreate({ p_type: 1, points_cost: 10, stock: -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="名称" min-width="140" />
|
||||
<el-table-column label="类型" width="80">
|
||||
<template #default="{ row }"><el-tag :type="row.p_type === 1 ? 'primary' : 'warning'" size="small">{{ pTypeMap[row.p_type] }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="points_cost" label="所需积分" width="90" />
|
||||
<el-table-column label="库存" width="80">
|
||||
<template #default="{ row }">{{ row.stock < 0 ? '不限量' : row.stock }}</template>
|
||||
</el-table-column>
|
||||
<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 prop="description" 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="560px">
|
||||
<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="类型" required>
|
||||
<el-radio-group v-model="form.p_type">
|
||||
<el-radio :value="1">虚拟</el-radio>
|
||||
<el-radio :value="2">实物</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="所需积分" required><el-input-number v-model="form.points_cost" :min="1" /></el-form-item>
|
||||
<el-form-item label="库存"><el-input-number v-model="form.stock" :min="-1" />(-1 = 不限量)</el-form-item>
|
||||
<el-form-item label="图标"><UploadField v-model="form.icon" kind="image" label="图标" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="form.description" 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 { useCrud } from '../composables/useCrud'
|
||||
import { pTypeMap, statusTag, statusText } from '../constants'
|
||||
import UploadField from '../components/UploadField.vue'
|
||||
|
||||
const EDIT_KEYS = ['id', 'name', 'description', 'icon', 'p_type', 'points_cost', 'stock', 'sort_order']
|
||||
const crud = useCrud('/prize')
|
||||
const { list, loading, dialogVisible, isEdit, form, load, openCreate, openEdit, save, toggleStatus } = crud
|
||||
load()
|
||||
</script>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form inline>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="filterStatus" style="width: 130px" @change="load()">
|
||||
<el-option label="全部" :value="0" />
|
||||
<el-option v-for="(v, k) in redeemStatusMap" :key="k" :label="v" :value="Number(k)" />
|
||||
</el-select>
|
||||
</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="child_nickname" label="孩子" min-width="110" />
|
||||
<el-table-column prop="prize_name" label="奖品" min-width="130" />
|
||||
<el-table-column prop="points_cost" label="积分" width="70" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="redeemTag(row.status)" size="small">{{ redeemStatusMap[row.status] }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="code" label="兑换码" min-width="150" />
|
||||
<el-table-column prop="created_at" label="兑换时间" width="170" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 1 || row.status === 2" link type="primary" @click="openShip(row)">发货</el-button>
|
||||
<el-button v-if="row.status === 3" link type="success" @click="receive(row)">确认领取</el-button>
|
||||
<el-button v-if="row.status === 1 || row.status === 2" link type="danger" @click="cancel(row)">取消(退积分)</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="shipVisible" title="兑换发货" width="480px">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="兑换码">
|
||||
<el-input v-model="shipCode" placeholder="留空自动生成(36ZH- + 8位大写随机)" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="shipVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="ship">确认发货</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../request'
|
||||
import { redeemStatusMap } from '../constants'
|
||||
|
||||
const list = ref([])
|
||||
const loading = ref(false)
|
||||
const filterStatus = ref(0)
|
||||
const shipVisible = ref(false)
|
||||
const shipId = ref(0)
|
||||
const shipCode = ref('')
|
||||
|
||||
const redeemTag = (s) => ({ 1: 'primary', 2: 'warning', 3: 'success', 4: 'info', 5: '' })[s] || 'info'
|
||||
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const d = await request.get('/redemption/list', { params: filterStatus.value ? { status: filterStatus.value } : {} })
|
||||
list.value = (d && d.list) || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
load()
|
||||
|
||||
const openShip = (row) => {
|
||||
shipId.value = row.id
|
||||
shipCode.value = ''
|
||||
shipVisible.value = true
|
||||
}
|
||||
|
||||
const ship = async () => {
|
||||
await request.post('/redemption/ship', { id: shipId.value, code: shipCode.value })
|
||||
ElMessage.success('发货成功')
|
||||
shipVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
const receive = async (row) => {
|
||||
await request.post('/redemption/receive', { id: row.id })
|
||||
ElMessage.success('已确认领取')
|
||||
load()
|
||||
}
|
||||
|
||||
const cancel = async (row) => {
|
||||
await ElMessageBox.confirm(`确认取消该兑换?积分 ${row.points_cost} 将退回孩子账户`, '提示', { type: 'warning' })
|
||||
await request.post('/redemption/cancel', { id: row.id })
|
||||
ElMessage.success('已取消,积分已退回')
|
||||
load()
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<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>
|
||||
@@ -0,0 +1,110 @@
|
||||
<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: 220px" @change="loadStats">
|
||||
<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>
|
||||
|
||||
<template v-if="stats">
|
||||
<el-row :gutter="16" style="margin-bottom: 16px">
|
||||
<el-col :span="8">
|
||||
<el-card><el-statistic title="参与孩子数" :value="stats.players" /></el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card><el-statistic title="完美通关数" :value="stats.perfect_count" /></el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card><el-statistic title="完美率" :value="stats.perfect_rate" suffix="%" /></el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card style="margin-bottom: 16px">
|
||||
<template #header>节点统计</template>
|
||||
<el-table :data="stats.nodes" border size="small" max-height="360">
|
||||
<el-table-column prop="node_id" label="节点ID" width="80" />
|
||||
<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="content" label="内容" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column prop="reach_count" label="到达次数" width="90" />
|
||||
<el-table-column label="离开(非终局)" width="110">
|
||||
<template #default="{ row }">{{ row.none }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败终局" width="90">
|
||||
<template #default="{ row }"><span style="color: #f56c6c">{{ row.fail }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="良好终局" width="90">
|
||||
<template #default="{ row }"><span style="color: #e6a23c">{{ row.good }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最佳终局" width="90">
|
||||
<template #default="{ row }"><span style="color: #67c23a">{{ row.best }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<template #header>选项统计</template>
|
||||
<el-table :data="stats.options" border size="small" max-height="360">
|
||||
<el-table-column prop="node_id" label="节点ID" width="80" />
|
||||
<el-table-column prop="option_id" label="选项ID" width="80" />
|
||||
<el-table-column prop="text" label="选项" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="choose_count" label="选择次数" width="90" />
|
||||
<el-table-column label="失败终局" width="90">
|
||||
<template #default="{ row }"><span style="color: #f56c6c">{{ row.fail }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="良好终局" width="90">
|
||||
<template #default="{ row }"><span style="color: #e6a23c">{{ row.good }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最佳终局" width="90">
|
||||
<template #default="{ row }"><span style="color: #67c23a">{{ row.best }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
<el-empty v-else description="请选择关卡查看统计" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import request from '../request'
|
||||
import { nodeTypeMap } from '../constants'
|
||||
|
||||
const strategies = ref([])
|
||||
const levels = ref([])
|
||||
const filterStrategy = ref(0)
|
||||
const filterLevel = ref(0)
|
||||
const stats = ref(null)
|
||||
|
||||
const onStrategyChange = async () => {
|
||||
filterLevel.value = 0
|
||||
stats.value = null
|
||||
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
|
||||
loadStats()
|
||||
}
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
stats.value = await request.get('/stats/level', { params: { 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>
|
||||
@@ -0,0 +1,61 @@
|
||||
<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>
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// base 与生产托管对齐:后端 AddStaticPath("/admin", "admin-src/dist")
|
||||
export default defineConfig({
|
||||
base: '/admin/',
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/api': { target: 'http://127.0.0.1:8080', changeOrigin: true },
|
||||
},
|
||||
},
|
||||
build: { outDir: 'dist' },
|
||||
})
|
||||
Reference in New Issue
Block a user