This commit is contained in:
xiao12feng8 2025-12-30 19:00:25 +08:00
commit 37ee807c8a
19 changed files with 2892 additions and 0 deletions

View File

@ -0,0 +1,93 @@
import request from '@/utils/request';
/**
* 缘池管理 API
*/
// ========== 话题管理 ==========
// 话题列表
export function fatePoolTopicListApi(params) {
return request({
url: '/admin/fate/pool/topic/list',
method: 'get',
params,
});
}
// 话题详情
export function fatePoolTopicInfoApi(id) {
return request({
url: `/admin/fate/pool/topic/info/${id}`,
method: 'get',
});
}
// 新增话题
export function fatePoolTopicSaveApi(data) {
return request({
url: '/admin/fate/pool/topic/save',
method: 'post',
data,
});
}
// 更新话题
export function fatePoolTopicUpdateApi(data) {
return request({
url: '/admin/fate/pool/topic/update',
method: 'post',
data,
});
}
// 删除话题
export function fatePoolTopicDeleteApi(id) {
return request({
url: `/admin/fate/pool/topic/delete/${id}`,
method: 'post',
});
}
// 修改话题状态
export function fatePoolTopicStatusApi(id) {
return request({
url: `/admin/fate/pool/topic/status/${id}`,
method: 'post',
});
}
// ========== 话题用户发布信息管理 ==========
// 发布信息列表
export function fatePoolPostListApi(params) {
return request({
url: '/admin/fate/pool/post/list',
method: 'get',
params,
});
}
// 发布信息详情
export function fatePoolPostInfoApi(id) {
return request({
url: `/admin/fate/pool/post/info/${id}`,
method: 'get',
});
}
// 删除发布信息
export function fatePoolPostDeleteApi(id) {
return request({
url: `/admin/fate/pool/post/delete/${id}`,
method: 'post',
});
}
// 修改发布信息状态
export function fatePoolPostStatusApi(id) {
return request({
url: `/admin/fate/pool/post/status/${id}`,
method: 'post',
});
}

View File

@ -0,0 +1,146 @@
import request from '@/utils/request';
/**
* 许愿树管理 API
*/
// ========== 许愿树管理 ==========
// 许愿树列表
export function wishTreeListApi(params) {
return request({
url: '/admin/wish/tree/list',
method: 'get',
params,
});
}
// 许愿树详情(含节点)
export function wishTreeInfoApi(id) {
return request({
url: `/admin/wish/tree/info/${id}`,
method: 'get',
});
}
// 新增许愿树(含节点)
export function wishTreeSaveApi(data) {
return request({
url: '/admin/wish/tree/save',
method: 'post',
data,
});
}
// 更新许愿树(含节点)
export function wishTreeUpdateApi(data) {
return request({
url: '/admin/wish/tree/update',
method: 'post',
data,
});
}
// 删除许愿树
export function wishTreeDeleteApi(id) {
return request({
url: `/admin/wish/tree/delete/${id}`,
method: 'post',
});
}
// 启用/停用许愿树
export function wishTreeActivateApi(id) {
return request({
url: `/admin/wish/tree/activate/${id}`,
method: 'post',
});
}
// ========== 节点管理 ==========
// 节点列表
export function wishTreeNodeListApi(params) {
return request({
url: '/admin/wish/tree/node/list',
method: 'get',
params,
});
}
// 节点详情
export function wishTreeNodeInfoApi(id) {
return request({
url: `/admin/wish/tree/node/info/${id}`,
method: 'get',
});
}
// 新增节点
export function wishTreeNodeSaveApi(data) {
return request({
url: '/admin/wish/tree/node/save',
method: 'post',
data,
});
}
// 更新节点
export function wishTreeNodeUpdateApi(data) {
return request({
url: '/admin/wish/tree/node/update',
method: 'post',
data,
});
}
// 删除节点
export function wishTreeNodeDeleteApi(id) {
return request({
url: `/admin/wish/tree/node/delete/${id}`,
method: 'post',
});
}
// 修改节点状态
export function wishTreeNodeStatusApi(id) {
return request({
url: `/admin/wish/tree/node/status/${id}`,
method: 'post',
});
}
// ========== 用户留言管理 ==========
// 留言列表(按许愿树查询)
export function wishTreeMessageListApi(params) {
return request({
url: '/admin/wish/tree/message/list',
method: 'get',
params,
});
}
// 留言详情
export function wishTreeMessageInfoApi(id) {
return request({
url: `/admin/wish/tree/message/info/${id}`,
method: 'get',
});
}
// 删除留言
export function wishTreeMessageDeleteApi(id) {
return request({
url: `/admin/wish/tree/message/delete/${id}`,
method: 'post',
});
}
// 修改留言状态
export function wishTreeMessageStatusApi(id) {
return request({
url: `/admin/wish/tree/message/status/${id}`,
method: 'post',
});
}

View File

@ -31,6 +31,8 @@ import contentManageRouter from './modules/contentManage'; // 内容管理
import feedbackManageRouter from './modules/feedbackManage'; // 用户反馈
import agentManageRouter from './modules/agentManage'; // 代理管理
import systemSettingRouter from './modules/systemSetting'; // 系统设置
import fatePoolRouter from './modules/fatePool'; // 缘池
import wishTreeRouter from './modules/wishTree'; // 许愿树
/**
* Note: sub-menu only appear when route children.length >= 1
@ -84,6 +86,10 @@ export const constantRoutes = [
liveManageRouter,
// 4. 社交互动
socialManageRouter,
// 4.5 缘池
fatePoolRouter,
// 4.6 许愿树
wishTreeRouter,
// 5. 礼物打赏
giftManageRouter,
// 6. 虚拟道具

View File

@ -0,0 +1,39 @@
import Layout from '@/layout';
/**
* 缘池路由
*/
const fatePoolRouter = {
path: '/fatePool',
component: Layout,
redirect: '/fatePool/topic',
name: 'FatePool',
alwaysShow: true,
meta: {
title: '缘池',
icon: 'el-icon-connection',
},
children: [
{
path: '/fatePool/topic',
component: () => import('@/views/fatePool/topic/index'),
name: 'FatePoolTopic',
meta: { title: '话题管理', icon: '' },
},
{
path: '/fatePool/topicUser',
component: () => import('@/views/fatePool/topicUser/index'),
name: 'FatePoolTopicUser',
meta: { title: '用户发布', icon: '' },
},
{
path: '/fatePool/topic/post/:topicId',
component: () => import('@/views/fatePool/topic/post'),
name: 'FatePoolTopicPost',
meta: { title: '用户发布', icon: '' },
hidden: true,
},
],
};
export default fatePoolRouter;

View File

@ -0,0 +1,45 @@
import Layout from '@/layout';
/**
* 许愿树路由
*/
const wishTreeRouter = {
path: '/wishTree',
component: Layout,
redirect: '/wishTree/tree',
name: 'WishTree',
alwaysShow: true,
meta: {
title: '许愿树',
icon: 'el-icon-magic-stick',
},
children: [
{
path: '/wishTree/tree',
component: () => import('@/views/wishTree/tree/index'),
name: 'WishTreeList',
meta: { title: '许愿树列表', icon: '' },
},
{
path: '/wishTree/node',
component: () => import('@/views/wishTree/node/index'),
name: 'WishTreeNode',
meta: { title: '留言节点', icon: '' },
},
{
path: '/wishTree/message',
component: () => import('@/views/wishTree/message/index'),
name: 'WishTreeMessage',
meta: { title: '用户留言', icon: '' },
},
{
path: '/wishTree/tree/detail/:treeId',
component: () => import('@/views/wishTree/tree/detail'),
name: 'WishTreeDetail',
meta: { title: '许愿树详情', icon: '' },
hidden: true,
},
],
};
export default wishTreeRouter;

View File

@ -0,0 +1,196 @@
<template>
<div class="divBox">
<el-card class="box-card">
<!-- 搜索区域 -->
<div class="clearfix" slot="header">
<el-form :inline="true" :model="queryParams" size="small">
<el-form-item label="话题标题">
<el-input v-model="queryParams.title" placeholder="请输入话题标题" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="success" @click="handleAdd">新增话题</el-button>
</el-form-item>
</el-form>
</div>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column label="封面" width="100" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.cover_image" :src="scope.row.cover_image" :preview-src-list="[scope.row.cover_image]" style="width: 60px; height: 60px" fit="cover" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="title" label="话题标题" min-width="150" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="post_count" label="发布数" width="100" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="160" align="center" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleViewPosts(scope.row)">查看发布</el-button>
<el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" @close="handleDialogClose">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="话题标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入话题标题" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="话题描述" prop="description">
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入话题描述" maxlength="500" show-word-limit />
</el-form-item>
<el-form-item label="封面图片" prop="cover_image">
<el-input v-model="formData.cover_image" placeholder="请输入封面图片URL" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { fatePoolTopicListApi, fatePoolTopicSaveApi, fatePoolTopicUpdateApi, fatePoolTopicDeleteApi, fatePoolTopicStatusApi } from '@/api/fatePool';
export default {
name: 'FatePoolTopic',
data() {
return {
loading: false,
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, title: '', status: '' },
dialogVisible: false,
dialogTitle: '新增话题',
formData: { id: null, title: '', description: '', cover_image: '', sort: 0, status: 1 },
formRules: { title: [{ required: true, message: '请输入话题标题', trigger: 'blur' }] },
};
},
created() {
this.getList();
},
methods: {
async getList() {
this.loading = true;
try {
const res = await fatePoolTopicListApi(this.queryParams);
// data
this.tableData = res.list || [];
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, title: '', status: '' };
this.getList();
},
handleAdd() {
this.dialogTitle = '新增话题';
this.formData = { id: null, title: '', description: '', cover_image: '', sort: 0, status: 1 };
this.dialogVisible = true;
},
handleEdit(row) {
this.dialogTitle = '编辑话题';
this.formData = { ...row };
this.dialogVisible = true;
},
handleViewPosts(row) {
this.$router.push({ path: `/fatePool/topic/post/${row.id}`, params: { topicId: row.id } });
},
handleDelete(row) {
this.$confirm('确定要删除该话题吗?删除后该话题下的所有用户发布信息也会被删除。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await fatePoolTopicDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleStatusChange(row) {
try {
await fatePoolTopicStatusApi(row.id);
this.$message.success('状态修改成功');
} catch (error) {
row.status = row.status === 1 ? 0 : 1;
console.error(error);
}
},
handleSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) {
this.$message.warning('请填写必填项');
return;
}
try {
const api = this.formData.id ? fatePoolTopicUpdateApi : fatePoolTopicSaveApi;
await api(this.formData);
this.$message.success(this.formData.id ? '编辑成功' : '新增成功');
this.dialogVisible = false;
this.getList();
} catch (error) {
console.error(error);
this.$message.error('操作失败');
}
});
},
handleDialogClose() {
this.$refs.formRef && this.$refs.formRef.resetFields();
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,179 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div class="clearfix" slot="header">
<el-button type="text" icon="el-icon-back" @click="$router.back()">返回</el-button>
<span style="margin-left: 10px; font-size: 16px; font-weight: bold;">{{ topicTitle }} - 用户发布信息</span>
</div>
<el-form :inline="true" :model="queryParams" size="small" style="margin-bottom: 20px;">
<el-form-item label="用户昵称">
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="显示" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column label="头像" width="80" align="center">
<template slot-scope="scope">
<el-avatar :src="scope.row.user_avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="user_nickname" label="昵称" width="120" />
<el-table-column prop="content" label="发布内容" min-width="250" show-overflow-tooltip />
<el-table-column label="图片" width="100" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.images && scope.row.images.length" type="text" @click="handleViewImages(scope.row)">查看({{ scope.row.images.length }})</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="发布时间" width="160" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleView(scope.row)">详情</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<el-dialog title="发布详情" :visible.sync="detailVisible" width="600px">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户昵称">{{ detailData.user_nickname }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ detailData.create_time }}</el-descriptions-item>
<el-descriptions-item label="发布内容" :span="2">{{ detailData.content }}</el-descriptions-item>
</el-descriptions>
<div v-if="detailData.images && detailData.images.length" style="margin-top: 20px">
<div style="margin-bottom: 10px; font-weight: bold">图片</div>
<el-image v-for="(img, index) in detailData.images" :key="index" :src="img" :preview-src-list="detailData.images" style="width: 100px; height: 100px; margin-right: 10px" fit="cover" />
</div>
</el-dialog>
<el-dialog title="图片预览" :visible.sync="imageVisible" width="800px">
<div style="text-align: center">
<el-image v-for="(img, index) in previewImages" :key="index" :src="img" :preview-src-list="previewImages" style="width: 150px; height: 150px; margin: 10px" fit="cover" />
</div>
</el-dialog>
</div>
</template>
<script>
import { fatePoolTopicInfoApi, fatePoolPostListApi, fatePoolPostDeleteApi, fatePoolPostStatusApi } from '@/api/fatePool';
export default {
name: 'FatePoolTopicPost',
data() {
return {
loading: false,
topicId: null,
topicTitle: '',
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, topic_id: '', nickname: '', status: '' },
detailVisible: false,
detailData: {},
imageVisible: false,
previewImages: [],
};
},
created() {
this.topicId = this.$route.params.topicId;
this.queryParams.topic_id = this.topicId;
this.getTopicInfo();
this.getList();
},
methods: {
async getTopicInfo() {
try {
const res = await fatePoolTopicInfoApi(this.topicId);
this.topicTitle = res.title || '';
} catch (error) {
console.error(error);
}
},
async getList() {
this.loading = true;
try {
const res = await fatePoolPostListApi(this.queryParams);
this.tableData = (res.list || []).map(item => ({
...item,
images: item.images ? (typeof item.images === 'string' ? JSON.parse(item.images) : item.images) : [],
}));
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, topic_id: this.topicId, nickname: '', status: '' };
this.getList();
},
handleView(row) {
this.detailData = row;
this.detailVisible = true;
},
handleViewImages(row) {
this.previewImages = row.images;
this.imageVisible = true;
},
handleDelete(row) {
this.$confirm('确定要删除该发布信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await fatePoolPostDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleStatusChange(row) {
try {
await fatePoolPostStatusApi(row.id);
this.$message.success('状态修改成功');
} catch (error) {
row.status = row.status === 1 ? 0 : 1;
console.error(error);
}
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,181 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div class="clearfix" slot="header">
<el-form :inline="true" :model="queryParams" size="small">
<el-form-item label="话题">
<el-select v-model="queryParams.topic_id" placeholder="请选择话题" clearable>
<el-option v-for="item in topicList" :key="item.id" :label="item.title" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="显示" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="topic_title" label="所属话题" width="150" />
<el-table-column label="头像" width="80" align="center">
<template slot-scope="scope">
<el-avatar :src="scope.row.user_avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="user_nickname" label="昵称" width="120" />
<el-table-column prop="content" label="发布内容" min-width="200" show-overflow-tooltip />
<el-table-column label="图片" width="100" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.images && scope.row.images.length" type="text" @click="handleViewImages(scope.row)">查看({{ scope.row.images.length }})</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="发布时间" width="160" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleView(scope.row)">详情</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<el-dialog title="发布详情" :visible.sync="detailVisible" width="600px">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户昵称">{{ detailData.user_nickname }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ detailData.create_time }}</el-descriptions-item>
<el-descriptions-item label="所属话题">{{ detailData.topic_title }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.status === 1 ? '显示' : '隐藏' }}</el-descriptions-item>
<el-descriptions-item label="发布内容" :span="2">{{ detailData.content }}</el-descriptions-item>
</el-descriptions>
<div v-if="detailData.images && detailData.images.length" style="margin-top: 20px">
<div style="margin-bottom: 10px; font-weight: bold">图片</div>
<el-image v-for="(img, index) in detailData.images" :key="index" :src="img" :preview-src-list="detailData.images" style="width: 100px; height: 100px; margin-right: 10px" fit="cover" />
</div>
</el-dialog>
<el-dialog title="图片预览" :visible.sync="imageVisible" width="800px">
<div style="text-align: center">
<el-image v-for="(img, index) in previewImages" :key="index" :src="img" :preview-src-list="previewImages" style="width: 150px; height: 150px; margin: 10px" fit="cover" />
</div>
</el-dialog>
</div>
</template>
<script>
import { fatePoolTopicListApi, fatePoolPostListApi, fatePoolPostDeleteApi, fatePoolPostStatusApi } from '@/api/fatePool';
export default {
name: 'FatePoolTopicUser',
data() {
return {
loading: false,
topicList: [],
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, topic_id: '', nickname: '', status: '' },
detailVisible: false,
detailData: {},
imageVisible: false,
previewImages: [],
};
},
created() {
this.getTopicList();
this.getList();
},
methods: {
async getTopicList() {
try {
const res = await fatePoolTopicListApi({ page: 1, limit: 1000, status: 1 });
this.topicList = res.list || [];
} catch (error) {
console.error(error);
}
},
async getList() {
this.loading = true;
try {
const res = await fatePoolPostListApi(this.queryParams);
this.tableData = (res.list || []).map(item => ({
...item,
images: item.images ? (typeof item.images === 'string' ? JSON.parse(item.images) : item.images) : [],
}));
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, topic_id: '', nickname: '', status: '' };
this.getList();
},
handleView(row) {
this.detailData = row;
this.detailVisible = true;
},
handleViewImages(row) {
this.previewImages = row.images;
this.imageVisible = true;
},
handleDelete(row) {
this.$confirm('确定要删除该发布信息吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await fatePoolPostDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleStatusChange(row) {
try {
await fatePoolPostStatusApi(row.id);
this.$message.success('状态修改成功');
} catch (error) {
row.status = row.status === 1 ? 0 : 1;
console.error(error);
}
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,213 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div class="clearfix" slot="header">
<el-form :inline="true" :model="queryParams" size="small">
<el-form-item label="许愿树">
<el-select v-model="queryParams.tree_id" placeholder="请选择许愿树" clearable @change="handleTreeChange">
<el-option v-for="item in treeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="节点">
<el-select v-model="queryParams.node_id" placeholder="请选择节点" clearable>
<el-option v-for="item in nodeList" :key="item.id" :label="item.title" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="显示" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="node_title" label="所属节点" width="120" />
<el-table-column label="头像" width="80" align="center">
<template slot-scope="scope">
<el-avatar :src="scope.row.user_avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="user_nickname" label="昵称" width="100">
<template slot-scope="scope">
<span>{{ scope.row.is_anonymous === 1 ? '匿名用户' : scope.row.user_nickname }}</span>
</template>
</el-table-column>
<el-table-column prop="content" label="留言内容" min-width="250" show-overflow-tooltip />
<el-table-column label="图片" width="100" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.images && scope.row.images.length" type="text" @click="handleViewImages(scope.row)">查看({{ scope.row.images.length }})</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="匿名" width="80" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.is_anonymous === 1" type="warning" size="small"></el-tag>
<el-tag v-else type="info" size="small"></el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="留言时间" width="160" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleView(scope.row)">详情</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<el-dialog title="留言详情" :visible.sync="detailVisible" width="600px">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户昵称">{{ detailData.is_anonymous === 1 ? '匿名用户' : detailData.user_nickname }}</el-descriptions-item>
<el-descriptions-item label="是否匿名">{{ detailData.is_anonymous === 1 ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="所属节点">{{ detailData.node_title }}</el-descriptions-item>
<el-descriptions-item label="留言时间">{{ detailData.create_time }}</el-descriptions-item>
<el-descriptions-item label="留言内容" :span="2">{{ detailData.content }}</el-descriptions-item>
</el-descriptions>
<div v-if="detailData.images && detailData.images.length" style="margin-top: 20px">
<div style="margin-bottom: 10px; font-weight: bold">图片</div>
<el-image v-for="(img, index) in detailData.images" :key="index" :src="img" :preview-src-list="detailData.images" style="width: 100px; height: 100px; margin-right: 10px" fit="cover" />
</div>
</el-dialog>
<el-dialog title="图片预览" :visible.sync="imageVisible" width="800px">
<div style="text-align: center">
<el-image v-for="(img, index) in previewImages" :key="index" :src="img" :preview-src-list="previewImages" style="width: 150px; height: 150px; margin: 10px" fit="cover" />
</div>
</el-dialog>
</div>
</template>
<script>
import { wishTreeListApi, wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
export default {
name: 'WishTreeMessage',
data() {
return {
loading: false,
treeList: [],
nodeList: [],
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, tree_id: '', node_id: '', nickname: '', status: '' },
detailVisible: false,
detailData: {},
imageVisible: false,
previewImages: [],
};
},
created() {
this.getTreeList();
this.getList();
},
methods: {
async getTreeList() {
try {
const res = await wishTreeListApi({ page: 1, limit: 1000 });
this.treeList = res.list || [];
} catch (error) {
console.error(error);
}
},
async getNodeList(treeId) {
try {
const res = await wishTreeInfoApi(treeId);
this.nodeList = res.nodes || [];
} catch (error) {
console.error(error);
}
},
async getList() {
this.loading = true;
try {
const res = await wishTreeMessageListApi(this.queryParams);
this.tableData = (res.list || []).map(item => ({
...item,
images: item.images ? (typeof item.images === 'string' ? JSON.parse(item.images) : item.images) : [],
}));
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleTreeChange(val) {
this.queryParams.node_id = '';
this.nodeList = [];
if (val) {
this.getNodeList(val);
}
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, tree_id: '', node_id: '', nickname: '', status: '' };
this.nodeList = [];
this.getList();
},
handleView(row) {
this.detailData = row;
this.detailVisible = true;
},
handleViewImages(row) {
this.previewImages = row.images;
this.imageVisible = true;
},
handleDelete(row) {
this.$confirm('确定要删除该留言吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await wishTreeMessageDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleStatusChange(row) {
try {
await wishTreeMessageStatusApi(row.id);
this.$message.success('状态修改成功');
} catch (error) {
row.status = row.status === 1 ? 0 : 1;
console.error(error);
}
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,209 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div class="clearfix" slot="header">
<el-form :inline="true" :model="queryParams" size="small">
<el-form-item label="许愿树">
<el-select v-model="queryParams.tree_id" placeholder="请选择许愿树" clearable>
<el-option v-for="item in treeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="节点标题">
<el-input v-model="queryParams.title" placeholder="请输入标题" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="success" @click="handleAdd">新增节点</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="tree_name" label="所属许愿树" width="150" />
<el-table-column prop="title" label="节点标题" min-width="150" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column label="开启时间" width="180" align="center">
<template slot-scope="scope">
<span v-if="scope.row.open_time">{{ scope.row.open_time }}</span>
<el-tag v-else type="success" size="small">立即开启</el-tag>
</template>
</el-table-column>
<el-table-column prop="message_count" label="留言数" width="80" align="center" />
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="160" align="center" />
<el-table-column label="操作" width="150" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="600px" @close="handleDialogClose">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="许愿树" prop="tree_id">
<el-select v-model="formData.tree_id" placeholder="请选择许愿树" style="width: 100%">
<el-option v-for="item in treeList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="节点标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入节点标题" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入描述" maxlength="500" show-word-limit />
</el-form-item>
<el-form-item label="图标URL" prop="icon">
<el-input v-model="formData.icon" placeholder="请输入图标URL" />
</el-form-item>
<el-form-item label="开启时间" prop="open_time">
<el-date-picker v-model="formData.open_time" type="datetime" placeholder="不设置则立即开启" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { wishTreeListApi, wishTreeNodeListApi, wishTreeNodeSaveApi, wishTreeNodeUpdateApi, wishTreeNodeDeleteApi, wishTreeNodeStatusApi } from '@/api/wishTree';
export default {
name: 'WishTreeNode',
data() {
return {
loading: false,
treeList: [],
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, tree_id: '', title: '', status: '' },
dialogVisible: false,
dialogTitle: '新增节点',
formData: { id: null, tree_id: '', title: '', description: '', icon: '', open_time: null, sort: 0, status: 1 },
formRules: {
tree_id: [{ required: true, message: '请选择许愿树', trigger: 'change' }],
title: [{ required: true, message: '请输入节点标题', trigger: 'blur' }],
},
};
},
created() {
this.getTreeList();
this.getList();
},
methods: {
async getTreeList() {
try {
const res = await wishTreeListApi({ page: 1, limit: 1000 });
this.treeList = res.list || [];
} catch (error) {
console.error(error);
}
},
async getList() {
this.loading = true;
try {
const res = await wishTreeNodeListApi(this.queryParams);
this.tableData = res.list || [];
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, tree_id: '', title: '', status: '' };
this.getList();
},
handleAdd() {
this.dialogTitle = '新增节点';
this.formData = { id: null, tree_id: '', title: '', description: '', icon: '', open_time: null, sort: 0, status: 1 };
this.dialogVisible = true;
},
handleEdit(row) {
this.dialogTitle = '编辑节点';
this.formData = { ...row };
this.dialogVisible = true;
},
handleDelete(row) {
this.$confirm('确定要删除该节点吗?删除后该节点下的所有留言也会被删除。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await wishTreeNodeDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleStatusChange(row) {
try {
await wishTreeNodeStatusApi(row.id);
this.$message.success('状态修改成功');
} catch (error) {
row.status = row.status === 1 ? 0 : 1;
console.error(error);
}
},
handleSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) return;
try {
const api = this.formData.id ? wishTreeNodeUpdateApi : wishTreeNodeSaveApi;
await api(this.formData);
this.$message.success(this.formData.id ? '编辑成功' : '新增成功');
this.dialogVisible = false;
this.getList();
} catch (error) {
console.error(error);
}
});
},
handleDialogClose() {
this.$refs.formRef && this.$refs.formRef.resetFields();
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,199 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div class="clearfix" slot="header">
<el-button type="text" icon="el-icon-back" @click="$router.back()">返回</el-button>
<span style="margin-left: 10px; font-size: 16px; font-weight: bold;">{{ treeName }} - 用户留言</span>
</div>
<el-form :inline="true" :model="queryParams" size="small" style="margin-bottom: 20px;">
<el-form-item label="节点">
<el-select v-model="queryParams.node_id" placeholder="全部节点" clearable>
<el-option v-for="item in nodeList" :key="item.id" :label="item.title" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="用户昵称">
<el-input v-model="queryParams.nickname" placeholder="请输入昵称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="显示" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="node_title" label="所属节点" width="120" />
<el-table-column label="头像" width="80" align="center">
<template slot-scope="scope">
<el-avatar :src="scope.row.user_avatar" :size="40" />
</template>
</el-table-column>
<el-table-column prop="user_nickname" label="昵称" width="100">
<template slot-scope="scope">
<span>{{ scope.row.is_anonymous === 1 ? '匿名用户' : scope.row.user_nickname }}</span>
</template>
</el-table-column>
<el-table-column prop="content" label="留言内容" min-width="250" show-overflow-tooltip />
<el-table-column label="图片" width="100" align="center">
<template slot-scope="scope">
<el-button v-if="scope.row.images && scope.row.images.length" type="text" @click="handleViewImages(scope.row)">查看({{ scope.row.images.length }})</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="匿名" width="80" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.is_anonymous === 1" type="warning" size="small"></el-tag>
<el-tag v-else type="info" size="small"></el-tag>
</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" @change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column prop="create_time" label="留言时间" width="160" align="center" />
<el-table-column label="操作" width="120" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleView(scope.row)">详情</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<el-dialog title="留言详情" :visible.sync="detailVisible" width="600px">
<el-descriptions :column="2" border>
<el-descriptions-item label="用户昵称">{{ detailData.is_anonymous === 1 ? '匿名用户' : detailData.user_nickname }}</el-descriptions-item>
<el-descriptions-item label="是否匿名">{{ detailData.is_anonymous === 1 ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="所属节点">{{ detailData.node_title }}</el-descriptions-item>
<el-descriptions-item label="留言时间">{{ detailData.create_time }}</el-descriptions-item>
<el-descriptions-item label="留言内容" :span="2">{{ detailData.content }}</el-descriptions-item>
</el-descriptions>
<div v-if="detailData.images && detailData.images.length" style="margin-top: 20px">
<div style="margin-bottom: 10px; font-weight: bold">图片</div>
<el-image v-for="(img, index) in detailData.images" :key="index" :src="img" :preview-src-list="detailData.images" style="width: 100px; height: 100px; margin-right: 10px" fit="cover" />
</div>
</el-dialog>
<el-dialog title="图片预览" :visible.sync="imageVisible" width="800px">
<div style="text-align: center">
<el-image v-for="(img, index) in previewImages" :key="index" :src="img" :preview-src-list="previewImages" style="width: 150px; height: 150px; margin: 10px" fit="cover" />
</div>
</el-dialog>
</div>
</template>
<script>
import { wishTreeInfoApi, wishTreeMessageListApi, wishTreeMessageDeleteApi, wishTreeMessageStatusApi } from '@/api/wishTree';
export default {
name: 'WishTreeDetail',
data() {
return {
loading: false,
treeId: null,
treeName: '',
nodeList: [],
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, tree_id: '', node_id: '', nickname: '', status: '' },
detailVisible: false,
detailData: {},
imageVisible: false,
previewImages: [],
};
},
created() {
this.treeId = this.$route.params.treeId;
this.queryParams.tree_id = this.treeId;
this.getTreeInfo();
this.getList();
},
methods: {
async getTreeInfo() {
try {
const res = await wishTreeInfoApi(this.treeId);
this.treeName = res.name || '';
this.nodeList = res.nodes || [];
} catch (error) {
console.error(error);
}
},
async getList() {
this.loading = true;
try {
const res = await wishTreeMessageListApi(this.queryParams);
this.tableData = (res.list || []).map(item => ({
...item,
images: item.images ? (typeof item.images === 'string' ? JSON.parse(item.images) : item.images) : [],
}));
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, tree_id: this.treeId, node_id: '', nickname: '', status: '' };
this.getList();
},
handleView(row) {
this.detailData = row;
this.detailVisible = true;
},
handleViewImages(row) {
this.previewImages = row.images;
this.imageVisible = true;
},
handleDelete(row) {
this.$confirm('确定要删除该留言吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await wishTreeMessageDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleStatusChange(row) {
try {
await wishTreeMessageStatusApi(row.id);
this.$message.success('状态修改成功');
} catch (error) {
row.status = row.status === 1 ? 0 : 1;
console.error(error);
}
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,226 @@
<template>
<div class="divBox">
<el-card class="box-card">
<div class="clearfix" slot="header">
<el-form :inline="true" :model="queryParams" size="small">
<el-form-item label="许愿树名称">
<el-input v-model="queryParams.name" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="success" @click="handleAdd">新增许愿树</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="loading" :data="tableData" border style="width: 100%">
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column label="封面" width="100" align="center">
<template slot-scope="scope">
<el-image v-if="scope.row.cover_image" :src="scope.row.cover_image" :preview-src-list="[scope.row.cover_image]" style="width: 60px; height: 60px" fit="cover" />
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="name" label="许愿树名称" min-width="150" />
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="node_count" label="节点数" width="80" align="center" />
<el-table-column prop="message_count" label="留言数" width="80" align="center" />
<el-table-column label="启用状态" width="100" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.is_active === 1" type="success">已启用</el-tag>
<el-tag v-else type="info">已停用</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="160" align="center" />
<el-table-column label="操作" width="250" align="center" fixed="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleViewMessages(scope.row)">查看留言</el-button>
<el-button type="text" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button v-if="scope.row.is_active !== 1" type="text" size="small" style="color: #67c23a" @click="handleActivate(scope.row)">启用</el-button>
<el-button v-else type="text" size="small" style="color: #e6a23c" @click="handleActivate(scope.row)">停用</el-button>
<el-button type="text" size="small" style="color: #f56c6c" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top: 20px; text-align: right" @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="queryParams.page" :page-sizes="[10, 20, 50, 100]" :page-size="queryParams.limit" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</el-card>
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="800px" @close="handleDialogClose">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入许愿树名称" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" type="textarea" :rows="2" placeholder="请输入描述" maxlength="500" show-word-limit />
</el-form-item>
<el-form-item label="封面图片">
<el-input v-model="formData.cover_image" placeholder="请输入封面图片URL" />
</el-form-item>
<el-form-item label="背景图片">
<el-input v-model="formData.background_image" placeholder="请输入背景图片URL" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
</el-form-item>
<el-divider content-position="left">节点设置</el-divider>
<el-button type="primary" size="small" @click="addNode" style="margin-bottom: 15px;">添加节点</el-button>
<el-table :data="formData.nodes" border size="small" style="margin-bottom: 20px;">
<el-table-column prop="title" label="节点标题" min-width="120">
<template slot-scope="scope">
<el-input v-model="scope.row.title" size="small" placeholder="节点标题" />
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="150">
<template slot-scope="scope">
<el-input v-model="scope.row.description" size="small" placeholder="节点描述" />
</template>
</el-table-column>
<el-table-column prop="open_time" label="开启时间" width="200">
<template slot-scope="scope">
<el-date-picker v-model="scope.row.open_time" type="datetime" size="small" placeholder="不设置则立即开启" value-format="yyyy-MM-dd HH:mm:ss" style="width: 100%;" />
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="100">
<template slot-scope="scope">
<el-input-number v-model="scope.row.sort" size="small" :min="0" :max="999" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template slot-scope="scope">
<el-button type="text" size="small" style="color: #f56c6c" @click="removeNode(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { wishTreeListApi, wishTreeInfoApi, wishTreeSaveApi, wishTreeUpdateApi, wishTreeDeleteApi, wishTreeActivateApi } from '@/api/wishTree';
export default {
name: 'WishTreeList',
data() {
return {
loading: false,
tableData: [],
total: 0,
queryParams: { page: 1, limit: 10, name: '' },
dialogVisible: false,
dialogTitle: '新增许愿树',
formData: { id: null, name: '', description: '', cover_image: '', background_image: '', sort: 0, nodes: [] },
formRules: { name: [{ required: true, message: '请输入许愿树名称', trigger: 'blur' }] },
};
},
created() {
this.getList();
},
methods: {
async getList() {
this.loading = true;
try {
const res = await wishTreeListApi(this.queryParams);
this.tableData = res.list || [];
this.total = res.total || 0;
} catch (error) {
console.error(error);
}
this.loading = false;
},
handleSearch() {
this.queryParams.page = 1;
this.getList();
},
handleReset() {
this.queryParams = { page: 1, limit: 10, name: '' };
this.getList();
},
handleAdd() {
this.dialogTitle = '新增许愿树';
this.formData = { id: null, name: '', description: '', cover_image: '', background_image: '', sort: 0, nodes: [] };
this.dialogVisible = true;
},
async handleEdit(row) {
this.dialogTitle = '编辑许愿树';
try {
const res = await wishTreeInfoApi(row.id);
this.formData = { ...res, nodes: res.nodes || [] };
this.dialogVisible = true;
} catch (error) {
console.error(error);
}
},
handleViewMessages(row) {
this.$router.push({ path: `/wishTree/tree/detail/${row.id}` });
},
addNode() {
this.formData.nodes.push({ title: '', description: '', open_time: null, sort: 0, status: 1 });
},
removeNode(index) {
this.formData.nodes.splice(index, 1);
},
handleDelete(row) {
this.$confirm('确定要删除该许愿树吗?删除后所有节点和用户留言也会被删除。', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
try {
await wishTreeDeleteApi(row.id);
this.$message.success('删除成功');
this.getList();
} catch (error) {
console.error(error);
}
});
},
async handleActivate(row) {
const action = row.is_active === 1 ? '停用' : '启用';
try {
await wishTreeActivateApi(row.id);
this.$message.success(`${action}成功`);
this.getList();
} catch (error) {
console.error(error);
}
},
handleSubmit() {
this.$refs.formRef.validate(async (valid) => {
if (!valid) return;
try {
const api = this.formData.id ? wishTreeUpdateApi : wishTreeSaveApi;
await api(this.formData);
this.$message.success(this.formData.id ? '编辑成功' : '新增成功');
this.dialogVisible = false;
this.getList();
} catch (error) {
console.error(error);
}
});
},
handleDialogClose() {
this.$refs.formRef && this.$refs.formRef.resetFields();
},
handleSizeChange(val) {
this.queryParams.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.queryParams.page = val;
this.getList();
},
},
};
</script>
<style scoped>
.divBox { padding: 20px; }
</style>

View File

@ -0,0 +1,130 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 缘池话题用户发布信息管理 Controller
*/
@Slf4j
@RestController
@RequestMapping("api/admin/fate/pool/post")
@Api(tags = "缘池话题发布信息管理")
@Validated
public class FatePoolPostController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 发布信息列表
*/
@ApiOperation(value = "发布信息列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public CommonResult<CommonPage<Map<String, Object>>> getList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit,
@RequestParam(value = "topic_id", required = false) Integer topicId,
@RequestParam(value = "nickname", required = false) String nickname,
@RequestParam(value = "status", required = false) Integer status) {
try {
StringBuilder whereSql = new StringBuilder(" WHERE 1=1");
List<Object> params = new ArrayList<>();
if (topicId != null) {
whereSql.append(" AND topic_id = ?");
params.add(topicId);
}
if (nickname != null && !nickname.isEmpty()) {
whereSql.append(" AND user_nickname LIKE ?");
params.add("%" + nickname + "%");
}
if (status != null) {
whereSql.append(" AND status = ?");
params.add(status);
}
String countSql = "SELECT COUNT(*) FROM eb_fate_pool_topic_post" + whereSql;
Integer total = jdbcTemplate.queryForObject(countSql, params.toArray(), Integer.class);
// 关联查询话题标题
String sql = "SELECT p.*, t.title as topic_title FROM eb_fate_pool_topic_post p " +
"LEFT JOIN eb_fate_pool_topic t ON p.topic_id = t.id" +
whereSql.toString().replace("topic_id", "p.topic_id").replace("user_nickname", "p.user_nickname").replace("status", "p.status") +
" ORDER BY p.id DESC LIMIT ?, ?";
params.add((page - 1) * limit);
params.add(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, params.toArray());
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取发布信息列表失败", e);
return CommonResult.failed("获取列表失败:" + e.getMessage());
}
}
/**
* 发布信息详情
*/
@ApiOperation(value = "发布信息详情")
@RequestMapping(value = "/info/{id}", method = RequestMethod.GET)
public CommonResult<Map<String, Object>> getInfo(@PathVariable Integer id) {
try {
String sql = "SELECT * FROM eb_fate_pool_topic_post WHERE id = ?";
Map<String, Object> data = jdbcTemplate.queryForMap(sql, id);
return CommonResult.success(data);
} catch (Exception e) {
log.error("获取发布信息详情失败", e);
return CommonResult.failed("获取详情失败:" + e.getMessage());
}
}
/**
* 删除发布信息
*/
@ApiOperation(value = "删除发布信息")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
public CommonResult<String> delete(@PathVariable Integer id) {
try {
jdbcTemplate.update("DELETE FROM eb_fate_pool_topic_post WHERE id = ?", id);
return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除发布信息失败", e);
return CommonResult.failed("删除失败:" + e.getMessage());
}
}
/**
* 修改发布信息状态
*/
@ApiOperation(value = "修改发布信息状态")
@RequestMapping(value = "/status/{id}", method = RequestMethod.POST)
public CommonResult<String> updateStatus(@PathVariable Integer id) {
try {
String sql = "UPDATE eb_fate_pool_topic_post SET status = IF(status = 1, 0, 1), update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql, id);
return CommonResult.success("状态修改成功");
} catch (Exception e) {
log.error("修改发布信息状态失败", e);
return CommonResult.failed("状态修改失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,169 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 缘池话题管理 Controller
*/
@Slf4j
@RestController
@RequestMapping("api/admin/fate/pool/topic")
@Api(tags = "缘池话题管理")
@Validated
public class FatePoolTopicController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 话题列表
*/
@ApiOperation(value = "话题列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public CommonResult<CommonPage<Map<String, Object>>> getList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit,
@RequestParam(value = "title", required = false) String title,
@RequestParam(value = "status", required = false) Integer status) {
try {
StringBuilder whereSql = new StringBuilder(" WHERE 1=1");
List<Object> params = new ArrayList<>();
if (title != null && !title.isEmpty()) {
whereSql.append(" AND title LIKE ?");
params.add("%" + title + "%");
}
if (status != null) {
whereSql.append(" AND status = ?");
params.add(status);
}
String countSql = "SELECT COUNT(*) FROM eb_fate_pool_topic" + whereSql;
Integer total = jdbcTemplate.queryForObject(countSql, params.toArray(), Integer.class);
// 查询列表包含发布数统计
String sql = "SELECT t.*, (SELECT COUNT(*) FROM eb_fate_pool_topic_post WHERE topic_id = t.id) as post_count " +
"FROM eb_fate_pool_topic t" + whereSql + " ORDER BY t.sort DESC, t.id DESC LIMIT ?, ?";
params.add((page - 1) * limit);
params.add(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, params.toArray());
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取话题列表失败", e);
return CommonResult.failed("获取列表失败:" + e.getMessage());
}
}
/**
* 话题详情
*/
@ApiOperation(value = "话题详情")
@RequestMapping(value = "/info/{id}", method = RequestMethod.GET)
public CommonResult<Map<String, Object>> getInfo(@PathVariable Integer id) {
try {
String sql = "SELECT * FROM eb_fate_pool_topic WHERE id = ?";
Map<String, Object> data = jdbcTemplate.queryForMap(sql, id);
return CommonResult.success(data);
} catch (Exception e) {
log.error("获取话题详情失败", e);
return CommonResult.failed("获取详情失败:" + e.getMessage());
}
}
/**
* 新增话题
*/
@ApiOperation(value = "新增话题")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public CommonResult<String> save(@RequestBody Map<String, Object> data) {
try {
String sql = "INSERT INTO eb_fate_pool_topic (title, description, cover_image, sort, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, NOW(), NOW())";
jdbcTemplate.update(sql,
data.get("title"),
data.get("description"),
data.get("cover_image"),
data.get("sort") != null ? data.get("sort") : 0,
data.get("status") != null ? data.get("status") : 1);
return CommonResult.success("新增成功");
} catch (Exception e) {
log.error("新增话题失败", e);
return CommonResult.failed("新增失败:" + e.getMessage());
}
}
/**
* 更新话题
*/
@ApiOperation(value = "更新话题")
@RequestMapping(value = "/update", method = RequestMethod.POST)
public CommonResult<String> update(@RequestBody Map<String, Object> data) {
try {
String sql = "UPDATE eb_fate_pool_topic SET title = ?, description = ?, cover_image = ?, sort = ?, status = ?, update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql,
data.get("title"),
data.get("description"),
data.get("cover_image"),
data.get("sort"),
data.get("status"),
data.get("id"));
return CommonResult.success("更新成功");
} catch (Exception e) {
log.error("更新话题失败", e);
return CommonResult.failed("更新失败:" + e.getMessage());
}
}
/**
* 删除话题
*/
@ApiOperation(value = "删除话题")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
public CommonResult<String> delete(@PathVariable Integer id) {
try {
// 先删除话题下的所有发布信息
jdbcTemplate.update("DELETE FROM eb_fate_pool_topic_post WHERE topic_id = ?", id);
// 再删除话题
jdbcTemplate.update("DELETE FROM eb_fate_pool_topic WHERE id = ?", id);
return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除话题失败", e);
return CommonResult.failed("删除失败:" + e.getMessage());
}
}
/**
* 修改话题状态
*/
@ApiOperation(value = "修改话题状态")
@RequestMapping(value = "/status/{id}", method = RequestMethod.POST)
public CommonResult<String> updateStatus(@PathVariable Integer id) {
try {
String sql = "UPDATE eb_fate_pool_topic SET status = IF(status = 1, 0, 1), update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql, id);
return CommonResult.success("状态修改成功");
} catch (Exception e) {
log.error("修改话题状态失败", e);
return CommonResult.failed("状态修改失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,224 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 许愿树管理 Controller
*/
@Slf4j
@RestController
@RequestMapping("api/admin/wish/tree")
@Api(tags = "许愿树管理")
@Validated
public class WishTreeController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 许愿树列表
*/
@ApiOperation(value = "许愿树列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public CommonResult<CommonPage<Map<String, Object>>> getList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit,
@RequestParam(value = "name", required = false) String name) {
try {
StringBuilder whereSql = new StringBuilder(" WHERE 1=1");
List<Object> params = new ArrayList<>();
if (name != null && !name.isEmpty()) {
whereSql.append(" AND name LIKE ?");
params.add("%" + name + "%");
}
String countSql = "SELECT COUNT(*) FROM eb_wish_tree" + whereSql;
Integer total = jdbcTemplate.queryForObject(countSql, params.toArray(), Integer.class);
// 查询列表包含节点数和留言数统计
String sql = "SELECT t.*, " +
"(SELECT COUNT(*) FROM eb_wish_tree_node WHERE tree_id = t.id) as node_count, " +
"(SELECT COUNT(*) FROM eb_wish_tree_message WHERE tree_id = t.id) as message_count " +
"FROM eb_wish_tree t" + whereSql + " ORDER BY t.sort DESC, t.id DESC LIMIT ?, ?";
params.add((page - 1) * limit);
params.add(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, params.toArray());
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取许愿树列表失败", e);
return CommonResult.failed("获取列表失败:" + e.getMessage());
}
}
/**
* 许愿树详情含节点
*/
@ApiOperation(value = "许愿树详情")
@RequestMapping(value = "/info/{id}", method = RequestMethod.GET)
public CommonResult<Map<String, Object>> getInfo(@PathVariable Integer id) {
try {
String sql = "SELECT * FROM eb_wish_tree WHERE id = ?";
Map<String, Object> data = jdbcTemplate.queryForMap(sql, id);
// 查询节点列表
String nodeSql = "SELECT * FROM eb_wish_tree_node WHERE tree_id = ? ORDER BY sort DESC, id ASC";
List<Map<String, Object>> nodes = jdbcTemplate.queryForList(nodeSql, id);
data.put("nodes", nodes);
return CommonResult.success(data);
} catch (Exception e) {
log.error("获取许愿树详情失败", e);
return CommonResult.failed("获取详情失败:" + e.getMessage());
}
}
/**
* 新增许愿树含节点
*/
@ApiOperation(value = "新增许愿树")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public CommonResult<String> save(@RequestBody Map<String, Object> data) {
try {
// 插入许愿树
String sql = "INSERT INTO eb_wish_tree (name, description, cover_image, background_image, sort, is_active, create_time, update_time) VALUES (?, ?, ?, ?, ?, 0, NOW(), NOW())";
jdbcTemplate.update(sql,
data.get("name"),
data.get("description"),
data.get("cover_image"),
data.get("background_image"),
data.get("sort") != null ? data.get("sort") : 0);
// 获取新插入的ID
Integer treeId = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Integer.class);
// 插入节点
@SuppressWarnings("unchecked")
List<Map<String, Object>> nodes = (List<Map<String, Object>>) data.get("nodes");
if (nodes != null && !nodes.isEmpty()) {
String nodeSql = "INSERT INTO eb_wish_tree_node (tree_id, title, description, icon, open_time, sort, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, 1, NOW(), NOW())";
for (Map<String, Object> node : nodes) {
jdbcTemplate.update(nodeSql,
treeId,
node.get("title"),
node.get("description"),
node.get("icon"),
node.get("open_time"),
node.get("sort") != null ? node.get("sort") : 0);
}
}
return CommonResult.success("新增成功");
} catch (Exception e) {
log.error("新增许愿树失败", e);
return CommonResult.failed("新增失败:" + e.getMessage());
}
}
/**
* 更新许愿树含节点
*/
@ApiOperation(value = "更新许愿树")
@RequestMapping(value = "/update", method = RequestMethod.POST)
public CommonResult<String> update(@RequestBody Map<String, Object> data) {
try {
Integer treeId = (Integer) data.get("id");
// 更新许愿树
String sql = "UPDATE eb_wish_tree SET name = ?, description = ?, cover_image = ?, background_image = ?, sort = ?, update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql,
data.get("name"),
data.get("description"),
data.get("cover_image"),
data.get("background_image"),
data.get("sort"),
treeId);
// 删除旧节点重新插入
jdbcTemplate.update("DELETE FROM eb_wish_tree_node WHERE tree_id = ?", treeId);
@SuppressWarnings("unchecked")
List<Map<String, Object>> nodes = (List<Map<String, Object>>) data.get("nodes");
if (nodes != null && !nodes.isEmpty()) {
String nodeSql = "INSERT INTO eb_wish_tree_node (tree_id, title, description, icon, open_time, sort, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, 1, NOW(), NOW())";
for (Map<String, Object> node : nodes) {
jdbcTemplate.update(nodeSql,
treeId,
node.get("title"),
node.get("description"),
node.get("icon"),
node.get("open_time"),
node.get("sort") != null ? node.get("sort") : 0);
}
}
return CommonResult.success("更新成功");
} catch (Exception e) {
log.error("更新许愿树失败", e);
return CommonResult.failed("更新失败:" + e.getMessage());
}
}
/**
* 删除许愿树
*/
@ApiOperation(value = "删除许愿树")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
public CommonResult<String> delete(@PathVariable Integer id) {
try {
// 删除留言
jdbcTemplate.update("DELETE FROM eb_wish_tree_message WHERE tree_id = ?", id);
// 删除节点
jdbcTemplate.update("DELETE FROM eb_wish_tree_node WHERE tree_id = ?", id);
// 删除许愿树
jdbcTemplate.update("DELETE FROM eb_wish_tree WHERE id = ?", id);
return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除许愿树失败", e);
return CommonResult.failed("删除失败:" + e.getMessage());
}
}
/**
* 启用/停用许愿树
*/
@ApiOperation(value = "启用/停用许愿树")
@RequestMapping(value = "/activate/{id}", method = RequestMethod.POST)
public CommonResult<String> activate(@PathVariable Integer id) {
try {
// 查询当前状态
Integer currentStatus = jdbcTemplate.queryForObject("SELECT is_active FROM eb_wish_tree WHERE id = ?", Integer.class, id);
if (currentStatus != null && currentStatus == 1) {
// 当前是启用状态改为停用
jdbcTemplate.update("UPDATE eb_wish_tree SET is_active = 0, update_time = NOW() WHERE id = ?", id);
} else {
// 当前是停用状态改为启用
jdbcTemplate.update("UPDATE eb_wish_tree SET is_active = 1, update_time = NOW() WHERE id = ?", id);
}
return CommonResult.success("操作成功");
} catch (Exception e) {
log.error("启用/停用许愿树失败", e);
return CommonResult.failed("操作失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,136 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 许愿树用户留言管理 Controller
*/
@Slf4j
@RestController
@RequestMapping("api/admin/wish/tree/message")
@Api(tags = "许愿树用户留言管理")
@Validated
public class WishTreeMessageController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 留言列表
*/
@ApiOperation(value = "留言列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public CommonResult<CommonPage<Map<String, Object>>> getList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit,
@RequestParam(value = "tree_id", required = false) Integer treeId,
@RequestParam(value = "node_id", required = false) Integer nodeId,
@RequestParam(value = "nickname", required = false) String nickname,
@RequestParam(value = "status", required = false) Integer status) {
try {
StringBuilder whereSql = new StringBuilder(" WHERE 1=1");
List<Object> params = new ArrayList<>();
if (treeId != null) {
whereSql.append(" AND m.tree_id = ?");
params.add(treeId);
}
if (nodeId != null) {
whereSql.append(" AND m.node_id = ?");
params.add(nodeId);
}
if (nickname != null && !nickname.isEmpty()) {
whereSql.append(" AND m.user_nickname LIKE ?");
params.add("%" + nickname + "%");
}
if (status != null) {
whereSql.append(" AND m.status = ?");
params.add(status);
}
String countSql = "SELECT COUNT(*) FROM eb_wish_tree_message m" + whereSql;
Integer total = jdbcTemplate.queryForObject(countSql, params.toArray(), Integer.class);
// 查询列表关联节点标题
String sql = "SELECT m.*, n.title as node_title " +
"FROM eb_wish_tree_message m " +
"LEFT JOIN eb_wish_tree_node n ON m.node_id = n.id" +
whereSql + " ORDER BY m.id DESC LIMIT ?, ?";
params.add((page - 1) * limit);
params.add(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, params.toArray());
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取留言列表失败", e);
return CommonResult.failed("获取列表失败:" + e.getMessage());
}
}
/**
* 留言详情
*/
@ApiOperation(value = "留言详情")
@RequestMapping(value = "/info/{id}", method = RequestMethod.GET)
public CommonResult<Map<String, Object>> getInfo(@PathVariable Integer id) {
try {
String sql = "SELECT m.*, n.title as node_title FROM eb_wish_tree_message m " +
"LEFT JOIN eb_wish_tree_node n ON m.node_id = n.id WHERE m.id = ?";
Map<String, Object> data = jdbcTemplate.queryForMap(sql, id);
return CommonResult.success(data);
} catch (Exception e) {
log.error("获取留言详情失败", e);
return CommonResult.failed("获取详情失败:" + e.getMessage());
}
}
/**
* 删除留言
*/
@ApiOperation(value = "删除留言")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
public CommonResult<String> delete(@PathVariable Integer id) {
try {
jdbcTemplate.update("DELETE FROM eb_wish_tree_message WHERE id = ?", id);
return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除留言失败", e);
return CommonResult.failed("删除失败:" + e.getMessage());
}
}
/**
* 修改留言状态
*/
@ApiOperation(value = "修改留言状态")
@RequestMapping(value = "/status/{id}", method = RequestMethod.POST)
public CommonResult<String> updateStatus(@PathVariable Integer id) {
try {
String sql = "UPDATE eb_wish_tree_message SET status = IF(status = 1, 0, 1), update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql, id);
return CommonResult.success("状态修改成功");
} catch (Exception e) {
log.error("修改留言状态失败", e);
return CommonResult.failed("状态修改失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,181 @@
package com.zbkj.admin.controller;
import com.zbkj.common.page.CommonPage;
import com.zbkj.common.result.CommonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 许愿树节点管理 Controller
*/
@Slf4j
@RestController
@RequestMapping("api/admin/wish/tree/node")
@Api(tags = "许愿树节点管理")
@Validated
public class WishTreeNodeController {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 节点列表
*/
@ApiOperation(value = "节点列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public CommonResult<CommonPage<Map<String, Object>>> getList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "limit", defaultValue = "10") Integer limit,
@RequestParam(value = "tree_id", required = false) Integer treeId,
@RequestParam(value = "title", required = false) String title,
@RequestParam(value = "status", required = false) Integer status) {
try {
StringBuilder whereSql = new StringBuilder(" WHERE 1=1");
List<Object> params = new ArrayList<>();
if (treeId != null) {
whereSql.append(" AND n.tree_id = ?");
params.add(treeId);
}
if (title != null && !title.isEmpty()) {
whereSql.append(" AND n.title LIKE ?");
params.add("%" + title + "%");
}
if (status != null) {
whereSql.append(" AND n.status = ?");
params.add(status);
}
String countSql = "SELECT COUNT(*) FROM eb_wish_tree_node n" + whereSql;
Integer total = jdbcTemplate.queryForObject(countSql, params.toArray(), Integer.class);
String sql = "SELECT n.*, t.name as tree_name, " +
"(SELECT COUNT(*) FROM eb_wish_tree_message WHERE node_id = n.id) as message_count " +
"FROM eb_wish_tree_node n " +
"LEFT JOIN eb_wish_tree t ON n.tree_id = t.id" +
whereSql + " ORDER BY n.tree_id, n.sort DESC, n.id ASC LIMIT ?, ?";
params.add((page - 1) * limit);
params.add(limit);
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, params.toArray());
CommonPage<Map<String, Object>> result = new CommonPage<>();
result.setList(list);
result.setTotal(total != null ? total.longValue() : 0L);
result.setPage(page);
result.setLimit(limit);
result.setTotalPage((int) Math.ceil((double) (total != null ? total : 0) / limit));
return CommonResult.success(result);
} catch (Exception e) {
log.error("获取节点列表失败", e);
return CommonResult.failed("获取列表失败:" + e.getMessage());
}
}
/**
* 节点详情
*/
@ApiOperation(value = "节点详情")
@RequestMapping(value = "/info/{id}", method = RequestMethod.GET)
public CommonResult<Map<String, Object>> getInfo(@PathVariable Integer id) {
try {
String sql = "SELECT n.*, t.name as tree_name FROM eb_wish_tree_node n " +
"LEFT JOIN eb_wish_tree t ON n.tree_id = t.id WHERE n.id = ?";
Map<String, Object> data = jdbcTemplate.queryForMap(sql, id);
return CommonResult.success(data);
} catch (Exception e) {
log.error("获取节点详情失败", e);
return CommonResult.failed("获取详情失败:" + e.getMessage());
}
}
/**
* 新增节点
*/
@ApiOperation(value = "新增节点")
@RequestMapping(value = "/save", method = RequestMethod.POST)
public CommonResult<String> save(@RequestBody Map<String, Object> data) {
try {
String sql = "INSERT INTO eb_wish_tree_node (tree_id, title, description, icon, open_time, sort, status, create_time, update_time) VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW())";
jdbcTemplate.update(sql,
data.get("tree_id"),
data.get("title"),
data.get("description"),
data.get("icon"),
data.get("open_time"),
data.get("sort") != null ? data.get("sort") : 0,
data.get("status") != null ? data.get("status") : 1);
return CommonResult.success("新增成功");
} catch (Exception e) {
log.error("新增节点失败", e);
return CommonResult.failed("新增失败:" + e.getMessage());
}
}
/**
* 更新节点
*/
@ApiOperation(value = "更新节点")
@RequestMapping(value = "/update", method = RequestMethod.POST)
public CommonResult<String> update(@RequestBody Map<String, Object> data) {
try {
String sql = "UPDATE eb_wish_tree_node SET tree_id = ?, title = ?, description = ?, icon = ?, open_time = ?, sort = ?, status = ?, update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql,
data.get("tree_id"),
data.get("title"),
data.get("description"),
data.get("icon"),
data.get("open_time"),
data.get("sort"),
data.get("status"),
data.get("id"));
return CommonResult.success("更新成功");
} catch (Exception e) {
log.error("更新节点失败", e);
return CommonResult.failed("更新失败:" + e.getMessage());
}
}
/**
* 删除节点
*/
@ApiOperation(value = "删除节点")
@RequestMapping(value = "/delete/{id}", method = RequestMethod.POST)
public CommonResult<String> delete(@PathVariable Integer id) {
try {
// 先删除节点下的所有留言
jdbcTemplate.update("DELETE FROM eb_wish_tree_message WHERE node_id = ?", id);
// 再删除节点
jdbcTemplate.update("DELETE FROM eb_wish_tree_node WHERE id = ?", id);
return CommonResult.success("删除成功");
} catch (Exception e) {
log.error("删除节点失败", e);
return CommonResult.failed("删除失败:" + e.getMessage());
}
}
/**
* 修改节点状态
*/
@ApiOperation(value = "修改节点状态")
@RequestMapping(value = "/status/{id}", method = RequestMethod.POST)
public CommonResult<String> updateStatus(@PathVariable Integer id) {
try {
String sql = "UPDATE eb_wish_tree_node SET status = IF(status = 1, 0, 1), update_time = NOW() WHERE id = ?";
jdbcTemplate.update(sql, id);
return CommonResult.success("状态修改成功");
} catch (Exception e) {
log.error("修改节点状态失败", e);
return CommonResult.failed("状态修改失败:" + e.getMessage());
}
}
}

View File

@ -0,0 +1,320 @@
# 缘池与许愿树管理端功能总结
> 更新日期2025-12-30
---
## 一、文件结构
### 1.1 前端文件
```
Zhibo/admin/src/
├── api/
│ ├── fatePool.js # 缘池API
│ └── wishTree.js # 许愿树API
├── router/modules/
│ ├── fatePool.js # 缘池路由
│ └── wishTree.js # 许愿树路由
└── views/
├── fatePool/ # 缘池页面
│ ├── topic/
│ │ ├── index.vue # 话题管理
│ │ └── post.vue # 话题发布详情
│ └── topicUser/
│ └── index.vue # 用户发布列表
└── wishTree/ # 许愿树页面
├── tree/
│ ├── index.vue # 许愿树列表
│ └── detail.vue # 许愿树留言详情
├── node/
│ └── index.vue # 节点管理
└── message/
└── index.vue # 用户留言列表
```
### 1.2 后端文件
```
Zhibo/zhibo-h/crmeb-admin/src/main/java/com/zbkj/admin/controller/
├── FatePoolTopicController.java # 缘池话题管理
├── FatePoolPostController.java # 缘池用户发布管理
├── WishTreeController.java # 许愿树管理
├── WishTreeNodeController.java # 许愿树节点管理
└── WishTreeMessageController.java # 许愿树留言管理
```
### 1.3 数据库表
```
eb_fate_pool_topic # 缘池话题表
eb_fate_pool_topic_post # 缘池用户发布表
eb_wish_tree # 许愿树表
eb_wish_tree_node # 许愿树节点表
eb_wish_tree_message # 许愿树留言表
```
---
## 二、缘池功能
### 2.1 话题管理
**页面路径:** `/fatePool/topic`
**页面文件:** `views/fatePool/topic/index.vue`
| 功能 | 说明 |
|-----|------|
| 话题列表 | 分页展示话题,显示封面、标题、描述、发布数 |
| 搜索 | 按标题模糊搜索、按状态筛选 |
| 新增话题 | 填写标题、描述、封面图URL、排序、状态 |
| 编辑话题 | 修改话题信息 |
| 删除话题 | 删除话题及其下所有发布信息 |
| 状态切换 | 启用/禁用话题 |
| 查看发布 | 跳转到该话题的用户发布列表 |
**接口列表:**
| 接口 | 方法 | 路径 | 说明 |
|-----|------|------|------|
| 话题列表 | GET | `/api/admin/fate/pool/topic/list` | 分页查询 |
| 话题详情 | GET | `/api/admin/fate/pool/topic/info/{id}` | 获取单个 |
| 新增话题 | POST | `/api/admin/fate/pool/topic/save` | 创建 |
| 更新话题 | POST | `/api/admin/fate/pool/topic/update` | 修改 |
| 删除话题 | POST | `/api/admin/fate/pool/topic/delete/{id}` | 删除 |
| 修改状态 | POST | `/api/admin/fate/pool/topic/status/{id}` | 切换状态 |
---
### 2.2 用户发布管理
**页面路径:** `/fatePool/topicUser`
**页面文件:** `views/fatePool/topicUser/index.vue`
| 功能 | 说明 |
|-----|------|
| 发布列表 | 分页展示用户发布信息 |
| 搜索 | 按话题筛选、按昵称搜索、按状态筛选 |
| 查看详情 | 弹窗展示完整内容和图片 |
| 删除发布 | 删除用户发布信息 |
| 状态切换 | 显示/隐藏发布信息 |
**接口列表:**
| 接口 | 方法 | 路径 | 说明 |
|-----|------|------|------|
| 发布列表 | GET | `/api/admin/fate/pool/post/list` | 分页查询 |
| 发布详情 | GET | `/api/admin/fate/pool/post/info/{id}` | 获取单个 |
| 删除发布 | POST | `/api/admin/fate/pool/post/delete/{id}` | 删除 |
| 修改状态 | POST | `/api/admin/fate/pool/post/status/{id}` | 切换状态 |
---
## 三、许愿树功能
### 3.1 许愿树管理
**页面路径:** `/wishTree/tree`
**页面文件:** `views/wishTree/tree/index.vue`
| 功能 | 说明 |
|-----|------|
| 许愿树列表 | 分页展示许愿树,显示封面、名称、节点数、留言数 |
| 搜索 | 按名称模糊搜索 |
| 新增许愿树 | 填写名称、描述、封面图、背景图、排序,可同时添加节点 |
| 编辑许愿树 | 修改许愿树信息和节点 |
| 删除许愿树 | 删除许愿树及其所有节点和留言 |
| 启用/停用 | 设置许愿树启用状态 |
| 查看留言 | 跳转到该许愿树的留言列表 |
**接口列表:**
| 接口 | 方法 | 路径 | 说明 |
|-----|------|------|------|
| 许愿树列表 | GET | `/api/admin/wish/tree/list` | 分页查询 |
| 许愿树详情 | GET | `/api/admin/wish/tree/info/{id}` | 获取详情(含节点) |
| 新增许愿树 | POST | `/api/admin/wish/tree/save` | 创建(含节点) |
| 更新许愿树 | POST | `/api/admin/wish/tree/update` | 修改(含节点) |
| 删除许愿树 | POST | `/api/admin/wish/tree/delete/{id}` | 删除 |
| 启用/停用 | POST | `/api/admin/wish/tree/activate/{id}` | 切换启用状态 |
---
### 3.2 节点管理
**页面路径:** `/wishTree/node`
**页面文件:** `views/wishTree/node/index.vue`
| 功能 | 说明 |
|-----|------|
| 节点列表 | 分页展示所有节点,显示所属许愿树、标题、开启时间、留言数 |
| 搜索 | 按许愿树筛选、按标题搜索、按状态筛选 |
| 新增节点 | 选择许愿树、填写标题、描述、图标、开启时间、排序 |
| 编辑节点 | 修改节点信息 |
| 删除节点 | 删除节点及其所有留言 |
| 状态切换 | 启用/禁用节点 |
**接口列表:**
| 接口 | 方法 | 路径 | 说明 |
|-----|------|------|------|
| 节点列表 | GET | `/api/admin/wish/tree/node/list` | 分页查询 |
| 节点详情 | GET | `/api/admin/wish/tree/node/info/{id}` | 获取单个 |
| 新增节点 | POST | `/api/admin/wish/tree/node/save` | 创建 |
| 更新节点 | POST | `/api/admin/wish/tree/node/update` | 修改 |
| 删除节点 | POST | `/api/admin/wish/tree/node/delete/{id}` | 删除 |
| 修改状态 | POST | `/api/admin/wish/tree/node/status/{id}` | 切换状态 |
---
### 3.3 用户留言管理
**页面路径:** `/wishTree/message`
**页面文件:** `views/wishTree/message/index.vue`
| 功能 | 说明 |
|-----|------|
| 留言列表 | 分页展示用户留言,显示节点、用户信息、内容、匿名状态 |
| 搜索 | 按许愿树/节点筛选、按昵称搜索、按状态筛选 |
| 查看详情 | 弹窗展示完整内容和图片 |
| 删除留言 | 删除用户留言 |
| 状态切换 | 显示/隐藏留言 |
**接口列表:**
| 接口 | 方法 | 路径 | 说明 |
|-----|------|------|------|
| 留言列表 | GET | `/api/admin/wish/tree/message/list` | 分页查询 |
| 留言详情 | GET | `/api/admin/wish/tree/message/info/{id}` | 获取单个 |
| 删除留言 | POST | `/api/admin/wish/tree/message/delete/{id}` | 删除 |
| 修改状态 | POST | `/api/admin/wish/tree/message/status/{id}` | 切换状态 |
---
## 四、接口参数说明
### 4.1 通用分页参数
| 参数 | 类型 | 必填 | 说明 |
|-----|------|------|------|
| page | Integer | 否 | 页码默认1 |
| limit | Integer | 否 | 每页数量默认10 |
### 4.2 缘池话题接口
**列表查询参数:**
```json
{
"page": 1,
"limit": 10,
"title": "关键词",
"status": 1
}
```
**新增/更新参数:**
```json
{
"id": 1,
"title": "话题标题",
"description": "话题描述",
"cover_image": "https://xxx.com/cover.jpg",
"sort": 100,
"status": 1
}
```
### 4.3 许愿树接口
**新增/更新参数(含节点):**
```json
{
"id": 1,
"name": "许愿树名称",
"description": "描述",
"cover_image": "https://xxx.com/cover.jpg",
"background_image": "https://xxx.com/bg.jpg",
"sort": 100,
"nodes": [
{
"title": "节点标题",
"description": "节点描述",
"icon": "https://xxx.com/icon.png",
"open_time": "2026-01-01 00:00:00",
"sort": 0
}
]
}
```
### 4.4 节点接口
**列表查询参数:**
```json
{
"page": 1,
"limit": 10,
"tree_id": 1,
"title": "关键词",
"status": 1
}
```
**新增/更新参数:**
```json
{
"id": 1,
"tree_id": 1,
"title": "节点标题",
"description": "节点描述",
"icon": "https://xxx.com/icon.png",
"open_time": "2026-01-01 00:00:00",
"sort": 0,
"status": 1
}
```
### 4.5 留言接口
**列表查询参数:**
```json
{
"page": 1,
"limit": 10,
"tree_id": 1,
"node_id": 1,
"nickname": "昵称",
"status": 1
}
```
---
## 五、菜单入口
管理后台左侧菜单:
```
├── 缘池
│ ├── 话题管理 /fatePool/topic
│ └── 用户发布 /fatePool/topicUser
└── 许愿树
├── 许愿树列表 /wishTree/tree
├── 留言节点 /wishTree/node
└── 用户留言 /wishTree/message
```
---
## 六、注意事项
1. **响应格式**:后端返回 `{code: 200, message: null, data: ...}`,前端 axios 拦截器会直接返回 `data` 部分
2. **图片字段**`images` 字段存储为 JSON 字符串,前端需要解析
3. **开启时间**:节点的 `open_time` 为空表示立即开启
4. **级联删除**
- 删除话题会同时删除该话题下所有发布信息
- 删除许愿树会同时删除所有节点和留言
- 删除节点会同时删除该节点下所有留言