feat(admin): complete post management, finance csv export, and wechat message template config
这个提交包含在:
@@ -52,6 +52,8 @@ export default {
|
||||
'menu.banner': 'Banner & Operations',
|
||||
'menu.banner.manage': 'Banner Management',
|
||||
'menu.banner.navCategory': 'Category Navigation',
|
||||
'menu.community': 'Community & Content',
|
||||
'menu.community.postManage': 'Post & Comment Audit',
|
||||
'menu.server.dashboard': 'Dashboard-Server',
|
||||
'menu.server.workplace': 'Workplace-Server',
|
||||
'menu.server.monitor': 'Monitor-Server',
|
||||
|
||||
@@ -52,6 +52,8 @@ export default {
|
||||
'menu.banner': '首页轮播与运营',
|
||||
'menu.banner.manage': '轮播图管理',
|
||||
'menu.banner.navCategory': '金刚区导航品类',
|
||||
'menu.community': '友圈与内容管理',
|
||||
'menu.community.postManage': '动态与评论监管',
|
||||
'menu.server.dashboard': '仪表盘-服务端',
|
||||
'menu.server.workplace': '工作台-服务端',
|
||||
'menu.server.monitor': '实时监控-服务端',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import { AppRouteRecordRaw } from '../types';
|
||||
|
||||
const COMMUNITY: AppRouteRecordRaw = {
|
||||
path: '/community',
|
||||
name: 'community',
|
||||
component: DEFAULT_LAYOUT,
|
||||
meta: {
|
||||
locale: 'menu.community',
|
||||
requiresAuth: true,
|
||||
icon: 'icon-message',
|
||||
order: 7,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'post-manage',
|
||||
name: 'PostManage',
|
||||
component: () => import('@/views/community/post-manage/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.community.postManage',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default COMMUNITY;
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item>内容与社区管理</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>友圈动态管理</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
|
||||
<a-card class="general-card" title="友圈广场动态审核与内容管控中心">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="fetchData">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新数据
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<!-- 搜索筛选栏 -->
|
||||
<a-row :gutter="16" style="margin-bottom: 16px">
|
||||
<a-col :span="6">
|
||||
<a-input-search
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索作者昵称、动态正文或商圈地点"
|
||||
allow-clear
|
||||
@search="fetchData"
|
||||
@press-enter="fetchData"
|
||||
/>
|
||||
</a-col>
|
||||
<a-col :span="5">
|
||||
<a-select
|
||||
v-model="selectedCategory"
|
||||
placeholder="动态品类筛选"
|
||||
allow-clear
|
||||
@change="fetchData"
|
||||
>
|
||||
<a-option value="all">全部分类</a-option>
|
||||
<a-option value="partner">同城搭子</a-option>
|
||||
<a-option value="game">开黑电竞</a-option>
|
||||
<a-option value="food">美食探店</a-option>
|
||||
<a-option value="ktv">K歌欢唱</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="5">
|
||||
<a-select
|
||||
v-model="selectedStatus"
|
||||
placeholder="状态筛选"
|
||||
allow-clear
|
||||
@change="fetchData"
|
||||
>
|
||||
<a-option value="all">全部状态</a-option>
|
||||
<a-option value="正常">正常显示</a-option>
|
||||
<a-option value="已隐藏">违规隐藏</a-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<a-table
|
||||
:data="postList"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 10, showTotal: true }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="作者" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<a-space>
|
||||
<a-avatar :size="36" shape="circle">
|
||||
<img :src="record.avatar" alt="avatar" />
|
||||
</a-avatar>
|
||||
<div>
|
||||
<div style="font-weight: 600">{{ record.author }}</div>
|
||||
<div style="font-size: 12px; color: var(--color-text-3)">UID: {{ record.userId }}</div>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="分类与话题" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<div style="margin-bottom: 4px">
|
||||
<a-tag color="arcoblue">{{ record.tag || '同城搭子' }}</a-tag>
|
||||
</div>
|
||||
<div v-if="record.topics && record.topics.length > 0" class="topics-wrap">
|
||||
<a-tag
|
||||
v-for="(tp, idx) in record.topics"
|
||||
:key="idx"
|
||||
size="small"
|
||||
color="gray"
|
||||
style="margin-right: 4px; margin-bottom: 2px"
|
||||
>
|
||||
#{{ tp }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="动态正文内容" :width="320">
|
||||
<template #cell="{ record }">
|
||||
<div class="post-content-cell">{{ record.content }}</div>
|
||||
<div class="post-location-tag" v-if="record.location">
|
||||
<icon-location style="margin-right: 4px" />
|
||||
{{ record.location }}
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="配图" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<a-image-preview-group v-if="record.images && record.images.length > 0">
|
||||
<a-space wrap :size="4">
|
||||
<a-image
|
||||
v-for="(img, idx) in record.images"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
width="44"
|
||||
height="44"
|
||||
fit="cover"
|
||||
style="border-radius: 4px; cursor: pointer"
|
||||
/>
|
||||
</a-space>
|
||||
</a-image-preview-group>
|
||||
<span v-else style="color: var(--color-text-4); font-size: 12px">纯文本无配图</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="互动数据" :width="150">
|
||||
<template #cell="{ record }">
|
||||
<div class="stats-row">
|
||||
<span>点赞: <b>{{ record.likes || 0 }}</b></span>
|
||||
<span style="margin-left: 8px">评论: <b>{{ record.comments || 0 }}</b></span>
|
||||
</div>
|
||||
<div class="stats-row" style="margin-top: 2px; color: var(--color-text-3)">
|
||||
<span>浏览量: {{ record.views || 0 }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="状态 / 置顶" :width="130">
|
||||
<template #cell="{ record }">
|
||||
<div style="margin-bottom: 4px">
|
||||
<a-tag v-if="record.isTop" color="red">
|
||||
<template #icon><icon-star /></template>
|
||||
官方置顶
|
||||
</a-tag>
|
||||
</div>
|
||||
<a-badge
|
||||
:status="record.status === '正常' ? 'success' : 'danger'"
|
||||
:text="record.status || '正常'"
|
||||
/>
|
||||
</template>
|
||||
</a-table-column>
|
||||
|
||||
<a-table-column title="发布时间" data-index="createdAt" :width="160" />
|
||||
|
||||
<a-table-column title="管理操作" :width="200" fixed="right">
|
||||
<template #cell="{ record }">
|
||||
<a-space :size="8">
|
||||
<!-- 置顶切换 -->
|
||||
<a-button
|
||||
size="small"
|
||||
:type="record.isTop ? 'outline' : 'text'"
|
||||
:status="record.isTop ? 'warning' : 'normal'"
|
||||
@click="handleToggleTop(record)"
|
||||
>
|
||||
{{ record.isTop ? '取消置顶' : '置顶' }}
|
||||
</a-button>
|
||||
|
||||
<!-- 显示/隐藏切换 -->
|
||||
<a-button
|
||||
size="small"
|
||||
type="text"
|
||||
:status="record.status === '正常' ? 'warning' : 'success'"
|
||||
@click="handleToggleStatus(record)"
|
||||
>
|
||||
{{ record.status === '正常' ? '隐藏' : '显示' }}
|
||||
</a-button>
|
||||
|
||||
<!-- 评论管理 -->
|
||||
<a-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="openCommentsDrawer(record)"
|
||||
>
|
||||
评论
|
||||
</a-button>
|
||||
|
||||
<!-- 违规删除 -->
|
||||
<a-popconfirm
|
||||
content="确定彻底删除该条动态及其所有评论吗?此操作不可逆。"
|
||||
type="warning"
|
||||
ok-text="确认删除"
|
||||
cancel-text="取消"
|
||||
@ok="handleDeletePost(record.id)"
|
||||
>
|
||||
<a-button size="small" type="text" status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 评论互动抽屉 (Drawer) -->
|
||||
<a-drawer
|
||||
v-model:visible="commentsDrawerVisible"
|
||||
:width="520"
|
||||
:title="`动态评论管理 - ${currentPost ? currentPost.author : ''}`"
|
||||
:footer="false"
|
||||
>
|
||||
<div v-if="loadingComments" style="text-align: center; padding: 40px 0">
|
||||
<a-spin />
|
||||
</div>
|
||||
<div v-else-if="commentsList.length > 0">
|
||||
<a-list :bordered="false">
|
||||
<a-list-item v-for="cmt in commentsList" :key="cmt.id">
|
||||
<a-list-item-meta
|
||||
:title="cmt.author"
|
||||
:description="cmt.createdAt"
|
||||
>
|
||||
<template #avatar>
|
||||
<a-avatar :size="32">
|
||||
<img :src="cmt.avatar" alt="avatar" />
|
||||
</a-avatar>
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<div class="comment-content-body">{{ cmt.content }}</div>
|
||||
<template #actions>
|
||||
<a-popconfirm
|
||||
content="确定删除该条不当评论吗?"
|
||||
type="warning"
|
||||
ok-text="确认"
|
||||
cancel-text="取消"
|
||||
@ok="handleDeleteComment(cmt.id)"
|
||||
>
|
||||
<a-button size="mini" type="text" status="danger">删除评论</a-button>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
<div v-else style="text-align: center; padding: 60px 0; color: var(--color-text-3)">
|
||||
该动态暂无任何评论互动
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const postList = ref<any[]>([]);
|
||||
const searchKeyword = ref('');
|
||||
const selectedCategory = ref('all');
|
||||
const selectedStatus = ref('all');
|
||||
|
||||
// 评论抽屉
|
||||
const commentsDrawerVisible = ref(false);
|
||||
const currentPost = ref<any>(null);
|
||||
const loadingComments = ref(false);
|
||||
const commentsList = ref<any[]>([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params: any = {};
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value;
|
||||
if (selectedCategory.value && selectedCategory.value !== 'all') params.category = selectedCategory.value;
|
||||
if (selectedStatus.value && selectedStatus.value !== 'all') params.status = selectedStatus.value;
|
||||
|
||||
const res: any = await axios.get('/api/admin/posts', { params });
|
||||
if (res && res.data) {
|
||||
postList.value = res.data;
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error('获取动态列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleTop = async (record: any) => {
|
||||
try {
|
||||
const nextTop = !record.isTop;
|
||||
const res: any = await axios.post(`/api/admin/posts/${record.id}/top`, { isTop: nextTop });
|
||||
Message.success(res.data?.message || (nextTop ? '置顶成功' : '已取消置顶'));
|
||||
record.isTop = nextTop;
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
Message.error('置顶状态修改失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleStatus = async (record: any) => {
|
||||
try {
|
||||
const nextStatus = record.status === '正常' ? '已隐藏' : '正常';
|
||||
const res: any = await axios.post(`/api/admin/posts/${record.id}/status`, { status: nextStatus });
|
||||
Message.success(res.data?.message || '状态更新成功');
|
||||
record.status = nextStatus;
|
||||
} catch (err: any) {
|
||||
Message.error('状态更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePost = async (postId: string) => {
|
||||
try {
|
||||
const res: any = await axios.delete(`/api/admin/posts/${postId}`);
|
||||
Message.success(res.data?.message || '动态已删除');
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
Message.error('删除动态失败');
|
||||
}
|
||||
};
|
||||
|
||||
const openCommentsDrawer = async (record: any) => {
|
||||
currentPost.value = record;
|
||||
commentsDrawerVisible.value = true;
|
||||
loadingComments.value = true;
|
||||
try {
|
||||
const res: any = await axios.get(`/api/posts/${record.id}/comments`);
|
||||
commentsList.value = res.data || [];
|
||||
} catch (err) {
|
||||
commentsList.value = [];
|
||||
} finally {
|
||||
loadingComments.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteComment = async (commentId: string) => {
|
||||
try {
|
||||
const res: any = await axios.delete(`/api/admin/posts/comments/${commentId}`);
|
||||
Message.success(res.data?.message || '评论已删除');
|
||||
if (currentPost.value) {
|
||||
openCommentsDrawer(currentPost.value);
|
||||
}
|
||||
fetchData();
|
||||
} catch (err: any) {
|
||||
Message.error('删除评论失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.breadcrumb {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.post-content-cell {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-1);
|
||||
word-break: break-all;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.post-location-tag {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.topics-wrap {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.stats-row {
|
||||
font-size: 12px;
|
||||
}
|
||||
.comment-content-body {
|
||||
padding: 6px 0 12px 40px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,10 @@
|
||||
<a-card class="general-card" title="平台全量交易与分账流水账本">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="outline" status="success" @click="exportCSV">
|
||||
<template #icon><icon-download /></template>
|
||||
导出对账明细 (CSV)
|
||||
</a-button>
|
||||
<a-button @click="fetchData">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新流水
|
||||
@@ -95,6 +99,38 @@
|
||||
return 'arcoblue';
|
||||
};
|
||||
|
||||
const exportCSV = () => {
|
||||
if (!rawList.value || rawList.value.length === 0) {
|
||||
Message.warning('暂无流水数据可导出');
|
||||
return;
|
||||
}
|
||||
const headers = ['流水号', '交易类型', '金额(元)', '资金方向', '交易对手方', '关联业务ID', '记账时间'];
|
||||
const rows = rawList.value.map(item => [
|
||||
item.recordNo || '',
|
||||
item.type || '',
|
||||
Number(item.amount || 0).toFixed(2),
|
||||
item.direction === 'IN' ? '收入' : '支出',
|
||||
item.counterparty || '',
|
||||
item.relatedId || '',
|
||||
item.createdAt || ''
|
||||
]);
|
||||
|
||||
const csvContent = '\uFEFF' + [headers, ...rows]
|
||||
.map(e => e.map(val => `"${String(val).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `平台交易资金对账流水_${new Date().toISOString().slice(0, 10)}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
Message.success('财务对账流水已成功导出');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
<a-card class="general-card" title="大神收益提现人工审核与打款通道">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button type="outline" status="success" @click="exportCSV">
|
||||
<template #icon><icon-download /></template>
|
||||
导出提现明细 (CSV)
|
||||
</a-button>
|
||||
<a-button @click="fetchData">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新提现列表
|
||||
@@ -164,6 +168,40 @@
|
||||
}
|
||||
};
|
||||
|
||||
const exportCSV = () => {
|
||||
if (!rawList.value || rawList.value.length === 0) {
|
||||
Message.warning('暂无提现审核记录可导出');
|
||||
return;
|
||||
}
|
||||
const headers = ['提现单号', '申请大神', '大神ID', '提现金额(元)', '账户余额(元)', '结算渠道', '申请时间', '审核状态', '驳回说明'];
|
||||
const rows = rawList.value.map(item => [
|
||||
item.withdrawNo || '',
|
||||
item.companionName || '',
|
||||
item.companionId || '',
|
||||
Number(item.amount || 0).toFixed(2),
|
||||
Number(item.balance || 0).toFixed(2),
|
||||
item.channel || '',
|
||||
item.applyTime || '',
|
||||
item.status || '',
|
||||
item.rejectReason || item.reason || ''
|
||||
]);
|
||||
|
||||
const csvContent = '\uFEFF' + [headers, ...rows]
|
||||
.map(e => e.map(val => `"${String(val).replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `大神收益提现审核对账表_${new Date().toISOString().slice(0, 10)}.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
Message.success('提现审核对账表已成功导出');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
@@ -594,6 +594,117 @@
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- Tab 11: 微信服务通知与模板消息 -->
|
||||
<a-tab-pane key="message_template_rules" title="微信服务通知与模板">
|
||||
<div class="tab-pane-content">
|
||||
<a-alert type="info" style="margin-bottom: 20px">
|
||||
在此维护微信小程序官方订阅消息(模板消息)的 Template ID 与推送总开关。用于在订单履约状态变更、组局活动到场、佣金提现到账及现场出示4位核销码时,向微信用户推送官方服务通知。
|
||||
</a-alert>
|
||||
|
||||
<a-card title="微信小程序模板消息配置" :bordered="false" style="background: var(--color-fill-2); margin-bottom: 20px;">
|
||||
<a-form :model="messageTemplateForm.valueJson" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="全局订阅消息推送开关">
|
||||
<a-switch
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
v-model="messageTemplateForm.valueJson.enablePush"
|
||||
checked-text="开启微信通知"
|
||||
unchecked-text="停用通知"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="配置描述说明">
|
||||
<a-input v-model="messageTemplateForm.valueJson.remark" placeholder="如:微信小程序订阅消息与服务通知模版ID配置" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="1. 订单履约状态变动通知模板 ID (接单/开始/完成)">
|
||||
<a-input v-model="messageTemplateForm.valueJson.orderStatusTemplateId" placeholder="例如:tmpl_order_status_update_01" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="2. 组局搭子报名与成局通知模板 ID">
|
||||
<a-input v-model="messageTemplateForm.valueJson.activityJoinTemplateId" placeholder="例如:tmpl_activity_joined_02" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="3. 大神佣金提现打款到账通知模板 ID">
|
||||
<a-input v-model="messageTemplateForm.valueJson.withdrawSuccessTemplateId" placeholder="例如:tmpl_withdraw_success_03" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="4. 现场 4 位数字核销码签到提醒模板 ID">
|
||||
<a-input v-model="messageTemplateForm.valueJson.verifyCodeTemplateId" placeholder="例如:tmpl_verify_code_notice_04" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<div style="margin-top: 16px">
|
||||
<a-button type="primary" :loading="saving" @click="saveMessageTemplateConfig">
|
||||
保存微信模板消息配置
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 模拟测试推送控制台 -->
|
||||
<a-card title="微信订阅通知模拟测试推送" :bordered="false" style="background: var(--color-fill-2)">
|
||||
<a-form :model="testMsgForm" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-form-item label="测试模板类型">
|
||||
<a-select v-model="testMsgForm.templateType" placeholder="选择要测试的消息类型">
|
||||
<a-option value="order_status">订单履约状态变动通知</a-option>
|
||||
<a-option value="activity_join">组局搭子成局/报名通知</a-option>
|
||||
<a-option value="withdraw_success">提现零钱打款到账通知</a-option>
|
||||
<a-option value="verify_code">现场 4 位核销码签到提醒</a-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="目标测试用户 ID (UID)">
|
||||
<a-input-number v-model="testMsgForm.targetUserId" placeholder="例如:1001" :min="1" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<a-form-item label="消息通知标题">
|
||||
<a-input v-model="testMsgForm.previewTitle" placeholder="例如:订单履约开始提醒" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="通知文本详情预览">
|
||||
<a-textarea v-model="testMsgForm.previewDetail" placeholder="输入模拟推送给用户的详细内容" :auto-size="{ minRows: 2, maxRows: 4 }" />
|
||||
</a-form-item>
|
||||
|
||||
<div>
|
||||
<a-space>
|
||||
<a-button type="primary" status="success" :loading="testSending" @click="sendTestMessage">
|
||||
<template #icon><icon-send /></template>
|
||||
发送模拟微信服务通知
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div v-if="testResult" style="margin-top: 16px;">
|
||||
<a-alert type="success" :title="`测试推送发送成功 [消息ID: ${testResult.wechatMsgId}]`">
|
||||
推送时间: {{ testResult.sendTime }} | 目标UID: {{ testResult.targetUserId }} | 状态: {{ testResult.status }}
|
||||
</a-alert>
|
||||
</div>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
@@ -891,6 +1002,56 @@ const saveActivityRulesConfig = () => {
|
||||
saveConfig('activity_rules', activityRulesForm);
|
||||
};
|
||||
|
||||
// Tab 11 微信服务通知与模板消息
|
||||
const messageTemplateForm = reactive({
|
||||
name: '微信服务通知与模板消息配置',
|
||||
description: '微信订阅消息通知与状态模板配置',
|
||||
status: 1,
|
||||
valueJson: {
|
||||
orderStatusTemplateId: 'tmpl_order_status_update_01',
|
||||
activityJoinTemplateId: 'tmpl_activity_joined_02',
|
||||
withdrawSuccessTemplateId: 'tmpl_withdraw_success_03',
|
||||
verifyCodeTemplateId: 'tmpl_verify_code_notice_04',
|
||||
enablePush: 1,
|
||||
remark: '微信小程序订阅消息与服务通知模版ID配置'
|
||||
}
|
||||
});
|
||||
|
||||
const testSending = ref(false);
|
||||
const testResult = ref<any>(null);
|
||||
const testMsgForm = reactive({
|
||||
templateType: 'order_status',
|
||||
targetUserId: 1001,
|
||||
previewTitle: '订单履约开始提醒',
|
||||
previewDetail: '您的同城生活陪伴订单已开始服务,请出示4位核销码完成现场确认。'
|
||||
});
|
||||
|
||||
const saveMessageTemplateConfig = () => {
|
||||
saveConfig('message_template_rules', messageTemplateForm);
|
||||
};
|
||||
|
||||
const sendTestMessage = async () => {
|
||||
testSending.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
templateType: testMsgForm.templateType,
|
||||
targetUserId: testMsgForm.targetUserId,
|
||||
previewData: {
|
||||
title: testMsgForm.previewTitle,
|
||||
detail: testMsgForm.previewDetail
|
||||
}
|
||||
};
|
||||
const res: any = await axios.post('/api/admin/message/test-send', payload);
|
||||
const data = res?.data?.data || res?.data || res;
|
||||
testResult.value = data;
|
||||
Message.success('测试服务通知推送成功');
|
||||
} catch (err: any) {
|
||||
Message.error(err.response?.data?.message || err.message || '测试推送异常');
|
||||
} finally {
|
||||
testSending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchConfigs = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -959,6 +1120,11 @@ const fetchConfigs = async () => {
|
||||
if (Array.isArray(activityRulesForm.valueJson.presetTotalFees)) {
|
||||
presetTotalFeesText.value = activityRulesForm.valueJson.presetTotalFees.join(', ');
|
||||
}
|
||||
} else if (item.configKey === 'message_template_rules') {
|
||||
messageTemplateForm.name = item.name;
|
||||
messageTemplateForm.description = item.description;
|
||||
messageTemplateForm.status = item.status;
|
||||
messageTemplateForm.valueJson = { ...messageTemplateForm.valueJson, ...(item.valueJson || {}) };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户