暂提 消息模板功能

This commit is contained in:
田香琪
2026-03-31 11:21:43 +08:00
parent 5593ebe574
commit 2829290fdc

View File

@@ -0,0 +1,480 @@
<template>
<div class="search">
<Card>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="font-weight:600;">消息模板管理</div>
<div>
<Button size="small" style="margin-right:8px;" @click="reloadCurrent">刷新</Button>
<Button size="small" style="margin-right:8px;" :loading="syncOaLoading" @click="syncOaTemplates">同步服务号模板</Button>
<Button size="small" :loading="syncMpLoading" @click="syncMpTemplates">同步小程序模板</Button>
</div>
</div>
<Tabs v-model="activeTab">
<TabPane label="会员消息模板" name="member">
<Alert type="info" show-icon style="margin-bottom:10px;">
支持维护站内信微信服务号消息微信小程序订阅消息开关未匹配模板的渠道会显示禁用
</Alert>
<Table :loading="loading" border :columns="memberColumns" :data="memberRows"></Table>
<Row type="flex" justify="end" class="mt_10">
<Page
:current="searchForm.pageNumber"
:total="total"
:page-size="searchForm.pageSize"
@on-change="changePage"
@on-page-size-change="changePageSize"
:page-size-opts="[20, 50, 100]"
size="small"
show-total
show-elevator
show-sizer
></Page>
</Row>
</TabPane>
<TabPane label="商户消息模板" name="store">
<Alert>商户消息模板待扩展后续可复用会员模板能力</Alert>
</TabPane>
<TabPane label="平台消息模板" name="platform">
<Alert>平台消息模板待扩展后续可复用会员模板能力</Alert>
</TabPane>
<TabPane label="邮箱消息模板" name="email">
<Alert>邮箱消息模板待扩展后续可按场景映射邮箱模板</Alert>
</TabPane>
</Tabs>
</Card>
<Modal v-model="noticeModalVisible" title="站内信模板" width="700">
<Form :label-width="120">
<FormItem label="模板应用场景">
<Input :value="currentRow.noticeNode || '-'" readonly />
</FormItem>
<FormItem label="启用">
<i-switch
:value="isOpen(currentRow.noticeStatus)"
@on-change="(v) => toggleNoticeChannel(v, currentRow)"
/>
</FormItem>
<FormItem label="模板内容">
<Input :value="noticeDetailText" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
</FormItem>
</Form>
<div slot="footer">
<Button @click="noticeModalVisible = false">关闭</Button>
</div>
</Modal>
<Modal v-model="oaModalVisible" title="微信服务号消息模板" width="700">
<Form :label-width="140">
<FormItem label="模板应用场景">
<Input :value="currentRow.noticeNode || '-'" readonly />
</FormItem>
<FormItem label="启用">
<i-switch
:value="isOpen(currentRow.oaStatus)"
:disabled="!currentRow.oaId"
@on-change="(v) => toggleOaChannel(v, currentRow)"
/>
</FormItem>
<FormItem label="模板名称">
<Input :value="currentRow.oaName || '-'" readonly />
</FormItem>
<FormItem label="微信模板ID">
<Input :value="currentRow.oaCode || '-'" readonly />
</FormItem>
<FormItem label="模板内容">
<Input :value="currentRow.oaContent || '-'" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
</FormItem>
<Alert v-if="!currentRow.oaId" type="warning">当前场景未匹配到微信服务号模板。</Alert>
</Form>
<div slot="footer">
<Button @click="oaModalVisible = false">关闭</Button>
</div>
</Modal>
<Modal v-model="emailModalVisible" title="邮箱消息模板" width="700">
<Form :label-width="120">
<FormItem label="模板应用场景">
<Input :value="currentRow.noticeNode || '-'" readonly />
</FormItem>
<FormItem label="启用">
<i-switch
:value="isOpen(currentRow.emailStatus)"
@on-change="(v) => toggleEmailChannel(v, currentRow)"
/>
</FormItem>
<FormItem label="模板内容">
<Input :value="emailDetailText" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
</FormItem>
</Form>
<div slot="footer">
<Button @click="emailModalVisible = false">关闭</Button>
</div>
</Modal>
<Modal v-model="mpModalVisible" title="微信小程序订阅消息模板" width="700">
<Form :label-width="150">
<FormItem label="模板应用场景">
<Input :value="currentRow.noticeNode || '-'" readonly />
</FormItem>
<FormItem label="启用">
<i-switch
:value="isOpen(currentRow.mpStatus)"
:disabled="!currentRow.mpId"
@on-change="(v) => toggleMpChannel(v, currentRow)"
/>
</FormItem>
<FormItem label="模板名称">
<Input :value="currentRow.mpName || '-'" readonly />
</FormItem>
<FormItem label="公共模板库ID">
<Input :value="currentRow.mpTemplateId || '-'" readonly />
</FormItem>
<FormItem label="订阅消息模板ID">
<Input :value="currentRow.mpCode || '-'" readonly />
</FormItem>
<FormItem label="模板内容">
<Input :value="currentRow.mpContent || '-'" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
</FormItem>
<Alert v-if="!currentRow.mpId" type="warning">当前场景未匹配到微信小程序模板。</Alert>
</Form>
<div slot="footer">
<Button @click="mpModalVisible = false">关闭</Button>
</div>
</Modal>
</div>
</template>
<script>
import * as API_Setting from "@/api/setting.js";
export default {
name: "messageTemplate",
data() {
return {
loading: false,
activeTab: "member",
syncOaLoading: false,
syncMpLoading: false,
total: 0,
searchForm: {
pageNumber: 1,
pageSize: 20,
},
memberRows: [],
currentRow: {},
noticeDetailText: "",
noticeModalVisible: false,
oaModalVisible: false,
mpModalVisible: false,
emailModalVisible: false,
emailDetailText: "",
memberColumns: [
{ title: "序号", key: "index", width: 80, align: "center" },
{ title: "模板应用场景", key: "noticeNode", minWidth: 180, tooltip: true },
{
title: "站内信",
key: "noticeStatus",
width: 110,
align: "center",
render: (h, params) =>
h("i-switch", {
props: { value: this.isOpen(params.row.noticeStatus) },
on: { "on-change": (v) => this.toggleNoticeChannel(v, params.row) },
}),
},
{
title: "微信服务号消息",
key: "oaStatus",
width: 140,
align: "center",
render: (h, params) =>
h("i-switch", {
props: { value: this.isOpen(params.row.oaStatus), disabled: !params.row.oaId },
on: { "on-change": (v) => this.toggleOaChannel(v, params.row) },
}),
},
{
title: "微信小程序订阅消息",
key: "mpStatus",
width: 160,
align: "center",
render: (h, params) =>
h("i-switch", {
props: { value: this.isOpen(params.row.mpStatus), disabled: !params.row.mpId },
on: { "on-change": (v) => this.toggleMpChannel(v, params.row) },
}),
},
{
title: "邮箱",
key: "emailStatus",
width: 120,
align: "center",
render: (h, params) =>
h("i-switch", {
props: {
value: this.isOpen(params.row.emailStatus),
// email_content 为空则禁用(避免切到无正文模板)
disabled: !params.row.emailContent,
},
on: { "on-change": (v) => this.toggleEmailChannel(v, params.row) },
}),
},
{
title: "操作",
key: "action",
width: 400,
align: "center",
render: (h, params) => {
const linkStyle = { color: "#2d8cf0", cursor: "pointer", margin: "0 8px" };
const disabledStyle = { color: "#c5c8ce", cursor: "not-allowed", margin: "0 8px" };
return h("div", [
h("a", { style: linkStyle, on: { click: () => this.openNoticeModal(params.row) } }, "站内信"),
h(
"a",
{
style: params.row.oaId ? linkStyle : disabledStyle,
on: params.row.oaId ? { click: () => this.openOaModal(params.row) } : {},
},
"微信服务号消息"
),
h(
"a",
{
style: params.row.mpId ? linkStyle : disabledStyle,
on: params.row.mpId ? { click: () => this.openMpModal(params.row) } : {},
},
"微信小程序订阅消息"
),
h("a", { style: linkStyle, on: { click: () => this.openEmailModal(params.row) } }, "邮箱"),
]);
},
},
],
};
},
methods: {
isOpen(status) {
return String(status || "").toUpperCase() === "OPEN";
},
/** 微信实体 Boolean enable 与表格 OPEN/CLOSE 统一 */
wechatEnableToOpenClose(enable) {
if (enable === true || enable === "true" || enable === 1) return "OPEN";
return "CLOSE";
},
toOpenClose(flag) {
return flag ? "OPEN" : "CLOSE";
},
safeText(v) {
return v === undefined || v === null ? "" : String(v);
},
pickByKeys(obj, keys) {
for (let i = 0; i < keys.length; i++) {
const val = obj ? obj[keys[i]] : "";
if (val !== undefined && val !== null && val !== "") return val;
}
return "";
},
formatOaContent(item) {
if (!item) return "";
const first = this.pickByKeys(item, ["first", "title", "name"]);
const remark = this.pickByKeys(item, ["remark", "content"]);
const keywords = Array.isArray(item.keywords)
? item.keywords.map((k) => this.pickByKeys(k, ["name", "value", "label"])).filter(Boolean).join(" / ")
: this.safeText(item.keywordsText || "");
return [first, keywords, remark].filter(Boolean).join("\n");
},
formatMpContent(item) {
if (!item) return "";
const keywordsText = this.safeText(item.keywordsText);
if (keywordsText) return keywordsText;
if (Array.isArray(item.keywords)) {
return item.keywords.map((k) => this.pickByKeys(k, ["name", "value", "label"])).filter(Boolean).join(" / ");
}
return this.safeText(item.content);
},
async loadMemberPage() {
this.loading = true;
try {
const res = await API_Setting.getMessageTemplateAggregatePage(this.searchForm);
if (!(res && res.success && res.result)) {
this.memberRows = [];
this.total = 0;
this.$Message.error((res && res.message) || "加载消息模板失败");
return;
}
const records = res.result.records || [];
this.total = res.result.total || 0;
this.memberRows = records.map((agg, idx) => {
const item = { ...(agg.notice || {}) };
const oa = agg.wechatOa;
const mp = agg.wechatMp;
return {
...item,
emailStatus: item.emailStatus || "CLOSE",
emailContent: item.emailContent || "",
index: (this.searchForm.pageNumber - 1) * this.searchForm.pageSize + idx + 1,
oaId: oa ? oa.id : "",
oaName: oa ? this.pickByKeys(oa, ["name", "templateName", "title"]) : "",
oaCode: oa ? this.pickByKeys(oa, ["code", "templateId"]) : "",
oaStatus: oa ? this.wechatEnableToOpenClose(oa.enable) : "",
oaRaw: oa || null,
oaContent: this.formatOaContent(oa),
mpId: mp ? mp.id : "",
mpName: mp ? this.pickByKeys(mp, ["name", "templateName", "title"]) : "",
mpTemplateId: mp ? this.pickByKeys(mp, ["templateId"]) : "",
mpCode: mp ? this.pickByKeys(mp, ["code"]) : "",
mpStatus: mp ? this.wechatEnableToOpenClose(mp.enable) : "",
mpRaw: mp || null,
mpContent: this.formatMpContent(mp),
};
});
} finally {
this.loading = false;
}
},
reloadCurrent() {
if (this.activeTab === "member") this.loadMemberPage();
},
async syncOaTemplates() {
this.syncOaLoading = true;
try {
const res = await API_Setting.wechatMessageSync();
if (res && res.success) {
await this.loadMemberPage();
this.$Message.success("服务号模板同步成功");
} else {
this.$Message.error((res && res.message) || "服务号模板同步失败");
}
} finally {
this.syncOaLoading = false;
}
},
async syncMpTemplates() {
this.syncMpLoading = true;
try {
const res = await API_Setting.wechatMPMessageSync();
if (res && res.success) {
await this.loadMemberPage();
this.$Message.success("小程序模板同步成功");
} else {
this.$Message.error((res && res.message) || "小程序模板同步失败");
}
} finally {
this.syncMpLoading = false;
}
},
changePage(v) {
this.searchForm.pageNumber = v;
this.loadMemberPage();
},
changePageSize(v) {
this.searchForm.pageNumber = 1;
this.searchForm.pageSize = v;
this.loadMemberPage();
},
async openNoticeModal(row) {
this.currentRow = { ...row };
this.noticeDetailText = row.noticeContent || "";
this.noticeModalVisible = true;
const res = await API_Setting.getNoticeMessageDetail(row.id);
if (res && res.success && res.result) {
const d = res.result;
this.noticeDetailText = [d.noticeTitle, d.noticeContent].filter(Boolean).join("\n");
}
},
openOaModal(row) {
this.currentRow = { ...row };
this.oaModalVisible = true;
},
openMpModal(row) {
this.currentRow = { ...row };
this.mpModalVisible = true;
},
async openEmailModal(row) {
this.currentRow = { ...row };
const fallback = row.emailContent || row.noticeContent || "";
this.emailDetailText = row.noticeTitle ? [row.noticeTitle, fallback].filter(Boolean).join("\n") : fallback;
this.emailModalVisible = true;
const res = await API_Setting.getNoticeMessageDetail(row.id);
if (res && res.success && res.result) {
const d = res.result;
if (d.emailContent) {
this.emailDetailText = d.emailContent;
} else {
this.emailDetailText = [d.noticeTitle, d.noticeContent].filter(Boolean).join("\n");
}
}
},
updateRowStatus(row, key, val) {
const idx = this.memberRows.findIndex((x) => x.id === row.id);
if (idx >= 0) this.$set(this.memberRows[idx], key, val);
if (this.currentRow && this.currentRow.id === row.id) this.$set(this.currentRow, key, val);
},
async toggleNoticeChannel(v, row) {
const old = row.noticeStatus;
this.updateRowStatus(row, "noticeStatus", this.toOpenClose(v));
const res = await API_Setting.updateMessageStatus(row.id, this.toOpenClose(v));
if (!(res && res.success)) {
this.updateRowStatus(row, "noticeStatus", old);
this.$Message.error((res && res.message) || "站内信开关更新失败");
} else {
this.$Message.success("站内信开关更新成功");
}
},
async toggleOaChannel(v, row) {
if (!row.oaId) return;
const old = row.oaStatus;
this.updateRowStatus(row, "oaStatus", this.toOpenClose(v));
// #region agent log: avoid sending full entity (it may include updateTime=null and cause backend bind errors)
const params = { id: row.oaId, enable: v };
// #region agent log: toggleOaChannel request
fetch('http://127.0.0.1:7540/ingest/70859f24-04fe-4b64-8e32-c394f0219fba', {method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'ade6ce'},body:JSON.stringify({sessionId:'ade6ce',location:'messageTemplate.vue:toggleOaChannel:beforeRequest',message:'OA toggle request payload',hypothesisId:'A',data:{oaId:row.oaId,oaRawId:row.oaRaw&&row.oaRaw.id,oaRawType:typeof (row.oaRaw&&row.oaRaw.id),paramsId:params&&params.id,paramsIdType:typeof (params&&params.id),paramsKeys:Object.keys(params||{}),enable:v},timestamp:Date.now()})}).catch(()=>{});
// #endregion
try {
const res = await API_Setting.editWechatMessageTemplate(row.oaId, params);
// #region agent log: toggleOaChannel response
fetch('http://127.0.0.1:7540/ingest/70859f24-04fe-4b64-8e32-c394f0219fba', {method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'ade6ce'},body:JSON.stringify({sessionId:'ade6ce',location:'messageTemplate.vue:toggleOaChannel:afterRequest',message:'OA toggle response',hypothesisId:'C',data:{success:res&&res.success,resMessage:res&&res.message,resCode:res&&res.code},timestamp:Date.now()})}).catch(()=>{});
// #endregion
if (!(res && res.success)) {
this.updateRowStatus(row, "oaStatus", old);
this.$Message.error((res && res.message) || "服务号开关更新失败");
} else {
this.$Message.success("服务号开关更新成功");
}
} catch (e) {
// #region agent log: toggleOaChannel thrown error
fetch('http://127.0.0.1:7540/ingest/70859f24-04fe-4b64-8e32-c394f0219fba', {method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'ade6ce'},body:JSON.stringify({sessionId:'ade6ce',location:'messageTemplate.vue:toggleOaChannel:catch',message:'OA toggle thrown exception',hypothesisId:'B',data:{errorMessage:(e&&e.message)||'',error:(e&&String(e))||''},timestamp:Date.now()})}).catch(()=>{});
// #endregion
this.updateRowStatus(row, "oaStatus", old);
this.$Message.error((e && e.message) || "服务号开关更新失败");
}
},
async toggleMpChannel(v, row) {
if (!row.mpId) return;
const old = row.mpStatus;
this.updateRowStatus(row, "mpStatus", this.toOpenClose(v));
const params = { id: row.mpId, enable: v };
const res = await API_Setting.editWechatMPMessageTemplate(row.mpId, params);
if (!(res && res.success)) {
this.updateRowStatus(row, "mpStatus", old);
this.$Message.error((res && res.message) || "小程序开关更新失败");
} else {
this.$Message.success("小程序开关更新成功");
}
},
async toggleEmailChannel(v, row) {
const old = row.emailStatus || "CLOSE";
this.updateRowStatus(row, "emailStatus", this.toOpenClose(v));
const res = await API_Setting.updateNoticeMessageEmailStatus(row.id, this.toOpenClose(v));
if (!(res && res.success)) {
this.updateRowStatus(row, "emailStatus", old);
this.$Message.error((res && res.message) || "邮箱开关更新失败");
} else {
this.$Message.success("邮箱开关更新成功");
}
},
},
mounted() {
this.loadMemberPage();
},
};
</script>