feat(smoke): add fa_smoke admin CRUD console page (#9)
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
// 戒烟记录(fa_smoke_log)
|
||||
export function listSmokeLogs(params) {
|
||||
return request({ url: '/api/admin/smoke/logs', method: 'get', params })
|
||||
}
|
||||
export function createSmokeLog(data) {
|
||||
return request({ url: '/api/admin/smoke/logs', method: 'post', data })
|
||||
}
|
||||
export function updateSmokeLog(id, data) {
|
||||
return request({ url: `/api/admin/smoke/logs/${id}`, method: 'put', data })
|
||||
}
|
||||
export function deleteSmokeLog(id) {
|
||||
return request({ url: `/api/admin/smoke/logs/${id}`, method: 'delete' })
|
||||
}
|
||||
|
||||
// 用户画像(fa_smoke_user_profile)
|
||||
export function listSmokeProfiles(params) {
|
||||
return request({ url: '/api/admin/smoke/profiles', method: 'get', params })
|
||||
}
|
||||
export function createSmokeProfile(data) {
|
||||
return request({ url: '/api/admin/smoke/profiles', method: 'post', data })
|
||||
}
|
||||
export function updateSmokeProfile(id, data) {
|
||||
return request({ url: `/api/admin/smoke/profiles/${id}`, method: 'put', data })
|
||||
}
|
||||
export function deleteSmokeProfile(id) {
|
||||
return request({ url: `/api/admin/smoke/profiles/${id}`, method: 'delete' })
|
||||
}
|
||||
|
||||
// AI 建议(fa_smoke_ai_advice)
|
||||
export function listSmokeAIAdvices(params) {
|
||||
return request({ url: '/api/admin/smoke/ai-advices', method: 'get', params })
|
||||
}
|
||||
export function createSmokeAIAdvice(data) {
|
||||
return request({ url: '/api/admin/smoke/ai-advices', method: 'post', data })
|
||||
}
|
||||
export function updateSmokeAIAdvice(id, data) {
|
||||
return request({ url: `/api/admin/smoke/ai-advices/${id}`, method: 'put', data })
|
||||
}
|
||||
export function deleteSmokeAIAdvice(id) {
|
||||
return request({ url: `/api/admin/smoke/ai-advices/${id}`, method: 'delete' })
|
||||
}
|
||||
|
||||
// AI 解锁(fa_smoke_ai_advice_unlocks)
|
||||
export function listSmokeAIUnlocks(params) {
|
||||
return request({ url: '/api/admin/smoke/ai-unlocks', method: 'get', params })
|
||||
}
|
||||
export function createSmokeAIUnlock(data) {
|
||||
return request({ url: '/api/admin/smoke/ai-unlocks', method: 'post', data })
|
||||
}
|
||||
export function updateSmokeAIUnlock(id, data) {
|
||||
return request({ url: `/api/admin/smoke/ai-unlocks/${id}`, method: 'put', data })
|
||||
}
|
||||
export function deleteSmokeAIUnlock(id) {
|
||||
return request({ url: `/api/admin/smoke/ai-unlocks/${id}`, method: 'delete' })
|
||||
}
|
||||
|
||||
// AI 下次抽烟节点(fa_smoke_ai_next_smoke)
|
||||
export function listSmokeAINexts(params) {
|
||||
return request({ url: '/api/admin/smoke/ai-next-smokes', method: 'get', params })
|
||||
}
|
||||
export function createSmokeAINext(data) {
|
||||
return request({ url: '/api/admin/smoke/ai-next-smokes', method: 'post', data })
|
||||
}
|
||||
export function updateSmokeAINext(id, data) {
|
||||
return request({ url: `/api/admin/smoke/ai-next-smokes/${id}`, method: 'put', data })
|
||||
}
|
||||
export function deleteSmokeAINext(id) {
|
||||
return request({ url: `/api/admin/smoke/ai-next-smokes/${id}`, method: 'delete' })
|
||||
}
|
||||
|
||||
// 激励语模板(fa_smoke_motivation_quote)
|
||||
export function listSmokeMotivations(params) {
|
||||
return request({ url: '/api/admin/smoke/motivation-quotes', method: 'get', params })
|
||||
}
|
||||
export function createSmokeMotivation(data) {
|
||||
return request({ url: '/api/admin/smoke/motivation-quotes', method: 'post', data })
|
||||
}
|
||||
export function updateSmokeMotivation(id, data) {
|
||||
return request({ url: `/api/admin/smoke/motivation-quotes/${id}`, method: 'put', data })
|
||||
}
|
||||
export function deleteSmokeMotivation(id) {
|
||||
return request({ url: `/api/admin/smoke/motivation-quotes/${id}`, method: 'delete' })
|
||||
}
|
||||
@@ -96,6 +96,12 @@ const routes = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'smoke',
|
||||
name: 'Smoke',
|
||||
component: () => import('../views/smoke/index.vue'),
|
||||
meta: { title: '戒烟小程序', icon: 'Opportunity' }
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'Settings',
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="header-row">
|
||||
<span>戒烟小程序数据管理(fa_smoke)</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tab-pane label="抽烟记录" name="logs" />
|
||||
<el-tab-pane label="用户画像" name="profiles" />
|
||||
<el-tab-pane label="AI建议" name="aiAdvices" />
|
||||
<el-tab-pane label="AI解锁" name="aiUnlocks" />
|
||||
<el-tab-pane label="AI节点" name="aiNexts" />
|
||||
<el-tab-pane label="激励语模板" name="motivations" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="可选:按 uid 过滤(仅数字)"
|
||||
style="width: 260px"
|
||||
clearable
|
||||
@keyup.enter="loadActiveData"
|
||||
@clear="loadActiveData"
|
||||
/>
|
||||
<el-button type="primary" @click="loadActiveData">刷新</el-button>
|
||||
<el-button type="success" @click="openCreate">新增</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="rows" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="UID" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.uid ?? '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据预览" min-width="360">
|
||||
<template #default="{ row }">
|
||||
<pre class="json-preview">{{ formatRow(row) }}</pre>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="loadActiveData"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="780px">
|
||||
<div class="dialog-tip">请填写 JSON(字段需符合当前 Tab 对应接口要求)</div>
|
||||
<el-input
|
||||
v-model="jsonForm"
|
||||
type="textarea"
|
||||
:rows="16"
|
||||
placeholder='例如:{"uid": 1, "remark": "测试"}'
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
listSmokeLogs,
|
||||
createSmokeLog,
|
||||
updateSmokeLog,
|
||||
deleteSmokeLog,
|
||||
listSmokeProfiles,
|
||||
createSmokeProfile,
|
||||
updateSmokeProfile,
|
||||
deleteSmokeProfile,
|
||||
listSmokeAIAdvices,
|
||||
createSmokeAIAdvice,
|
||||
updateSmokeAIAdvice,
|
||||
deleteSmokeAIAdvice,
|
||||
listSmokeAIUnlocks,
|
||||
createSmokeAIUnlock,
|
||||
updateSmokeAIUnlock,
|
||||
deleteSmokeAIUnlock,
|
||||
listSmokeAINexts,
|
||||
createSmokeAINext,
|
||||
updateSmokeAINext,
|
||||
deleteSmokeAINext,
|
||||
listSmokeMotivations,
|
||||
createSmokeMotivation,
|
||||
updateSmokeMotivation,
|
||||
deleteSmokeMotivation
|
||||
} from '../../api/smoke'
|
||||
|
||||
// 每个 Tab 绑定对应的 CRUD API,便于统一维护。
|
||||
const tabMap = {
|
||||
logs: { list: listSmokeLogs, create: createSmokeLog, update: updateSmokeLog, remove: deleteSmokeLog },
|
||||
profiles: { list: listSmokeProfiles, create: createSmokeProfile, update: updateSmokeProfile, remove: deleteSmokeProfile },
|
||||
aiAdvices: { list: listSmokeAIAdvices, create: createSmokeAIAdvice, update: updateSmokeAIAdvice, remove: deleteSmokeAIAdvice },
|
||||
aiUnlocks: { list: listSmokeAIUnlocks, create: createSmokeAIUnlock, update: updateSmokeAIUnlock, remove: deleteSmokeAIUnlock },
|
||||
aiNexts: { list: listSmokeAINexts, create: createSmokeAINext, update: updateSmokeAINext, remove: deleteSmokeAINext },
|
||||
motivations: { list: listSmokeMotivations, create: createSmokeMotivation, update: updateSmokeMotivation, remove: deleteSmokeMotivation }
|
||||
}
|
||||
|
||||
const activeTab = ref('logs')
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const rows = ref([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const keyword = ref('')
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogMode = ref('create')
|
||||
const editingId = ref(null)
|
||||
const jsonForm = ref(`{\n "uid": 1\n}`)
|
||||
|
||||
const dialogTitle = computed(() => (dialogMode.value === 'create' ? '新增记录' : `编辑记录 #${editingId.value}`))
|
||||
|
||||
const formatRow = (row) => JSON.stringify(row, null, 2)
|
||||
|
||||
const buildParams = () => {
|
||||
const params = {
|
||||
page: page.value,
|
||||
page_size: pageSize.value
|
||||
}
|
||||
|
||||
const uid = Number(keyword.value)
|
||||
if (keyword.value && !Number.isNaN(uid) && uid > 0) {
|
||||
params.uid = uid
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
const getApi = () => tabMap[activeTab.value]
|
||||
|
||||
const loadActiveData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getApi().list(buildParams())
|
||||
const payload = res.data || {}
|
||||
rows.value = payload.list || []
|
||||
total.value = payload.total || 0
|
||||
page.value = payload.page || page.value
|
||||
pageSize.value = payload.page_size || pageSize.value
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTabChange = () => {
|
||||
page.value = 1
|
||||
loadActiveData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
page.value = 1
|
||||
loadActiveData()
|
||||
}
|
||||
|
||||
const openCreate = () => {
|
||||
dialogMode.value = 'create'
|
||||
editingId.value = null
|
||||
jsonForm.value = '{\n "uid": 1\n}'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEdit = (row) => {
|
||||
dialogMode.value = 'edit'
|
||||
editingId.value = row.id
|
||||
jsonForm.value = JSON.stringify(row, null, 2)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const parseJsonForm = () => {
|
||||
try {
|
||||
return JSON.parse(jsonForm.value)
|
||||
} catch (error) {
|
||||
throw new Error('JSON 格式错误,请检查后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
let payload
|
||||
try {
|
||||
payload = parseJsonForm()
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message)
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (dialogMode.value === 'create') {
|
||||
await getApi().create(payload)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await getApi().update(editingId.value, payload)
|
||||
ElMessage.success('更新成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await loadActiveData()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
await ElMessageBox.confirm('删除后不可恢复,确定继续吗?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await getApi().remove(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadActiveData()
|
||||
}
|
||||
|
||||
loadActiveData()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.json-preview {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
background: #f8fafc;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-tip {
|
||||
margin-bottom: 8px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user