refactor: 重构组件逻辑与样式优化

- 在 App.vue 中重构站点信息的获取与应用逻辑,提升代码可读性与维护性。
- 更新 Search.vue 组件,优化搜索框的样式与结构,增强用户体验。
- 在 element.scss 中注释掉不再使用的样式,并新增确认收货按钮的样式。
- 改进多个页面的布局与样式,确保响应式设计的一致性与美观性。
- 更新订单相关组件的按钮样式,提升视觉一致性与可用性。
This commit is contained in:
田香琪
2026-06-30 21:09:28 +08:00
parent ec948b5496
commit 28a857249a
121 changed files with 5122 additions and 2182 deletions

View File

@@ -0,0 +1,484 @@
<template>
<div class="search">
<el-card>
<div class="toolbar">
<div class="toolbar-title">消息模板管理</div>
<div>
<el-button size="small" style="margin-right: 8px" @click="reloadCurrent">刷新</el-button>
<el-button size="small" style="margin-right: 8px" :loading="syncOaLoading" @click="syncOaTemplates">
同步服务号模板
</el-button>
<el-button size="small" :loading="syncMpLoading" @click="syncMpTemplates">同步小程序模板</el-button>
</div>
</div>
<el-tabs v-model="activeTab">
<el-tab-pane label="会员消息模板" name="member">
<el-alert
type="info"
show-icon
:closable="false"
style="margin-bottom: 10px"
title="支持维护站内信、微信服务号消息、微信小程序订阅消息开关。未匹配模板的渠道会显示禁用。"
/>
<el-table v-loading="loading" border :data="memberRows" style="width: 100%">
<el-table-column prop="index" label="序号" width="80" align="center" />
<el-table-column prop="noticeNode" label="模板应用场景" min-width="180" show-overflow-tooltip />
<el-table-column label="站内信" width="110" align="center">
<template #default="{ row }">
<el-switch
:model-value="isOpen(row.noticeStatus)"
@change="(v) => toggleNoticeChannel(v, row)"
/>
</template>
</el-table-column>
<el-table-column label="微信服务号消息" width="140" align="center">
<template #default="{ row }">
<el-switch
:model-value="isOpen(row.oaStatus)"
:disabled="!row.oaId"
@change="(v) => toggleOaChannel(v, row)"
/>
</template>
</el-table-column>
<el-table-column label="微信小程序订阅消息" width="160" align="center">
<template #default="{ row }">
<el-switch
:model-value="isOpen(row.mpStatus)"
:disabled="!row.mpId"
@change="(v) => toggleMpChannel(v, row)"
/>
</template>
</el-table-column>
<el-table-column label="邮箱" width="120" align="center">
<template #default="{ row }">
<el-switch
:model-value="isOpen(row.emailStatus)"
:disabled="!row.emailContent"
@change="(v) => toggleEmailChannel(v, row)"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="400" align="center" fixed="right">
<template #default="{ row }">
<div class="ops">
<a class="link-text" @click="openNoticeModal(row)">站内信</a>
<span class="op-split">|</span>
<a
:class="row.oaId ? 'link-text' : 'link-disabled'"
@click="row.oaId && openOaModal(row)"
>
微信服务号消息
</a>
<span class="op-split">|</span>
<a
:class="row.mpId ? 'link-text' : 'link-disabled'"
@click="row.mpId && openMpModal(row)"
>
微信小程序订阅消息
</a>
<span class="op-split">|</span>
<a class="link-text" @click="openEmailModal(row)">邮箱</a>
</div>
</template>
</el-table-column>
</el-table>
<div class="mt_10" style="display: flex; justify-content: flex-end">
<el-pagination
v-model:current-page="searchForm.pageNumber"
v-model:page-size="searchForm.pageSize"
:total="total"
:page-sizes="[20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
size="small"
@current-change="loadMemberPage"
@size-change="changePageSize"
/>
</div>
</el-tab-pane>
<el-tab-pane label="商户消息模板" name="store">
<el-alert type="info" :closable="false" title="商户消息模板待扩展,后续可复用会员模板能力。" />
</el-tab-pane>
<el-tab-pane label="平台消息模板" name="platform">
<el-alert type="info" :closable="false" title="平台消息模板待扩展,后续可复用会员模板能力。" />
</el-tab-pane>
<el-tab-pane label="邮箱消息模板" name="email">
<el-alert type="info" :closable="false" title="邮箱消息模板待扩展,后续可按场景映射邮箱模板。" />
</el-tab-pane>
</el-tabs>
</el-card>
<el-dialog v-model="noticeModalVisible" title="站内信模板" width="700px">
<el-form label-width="120px">
<el-form-item label="模板应用场景">
<el-input :model-value="currentRow.noticeNode || '-'" readonly />
</el-form-item>
<el-form-item label="启用">
<el-switch
:model-value="isOpen(currentRow.noticeStatus)"
@change="(v) => toggleNoticeChannel(v, currentRow)"
/>
</el-form-item>
<el-form-item label="模板内容">
<el-input :model-value="noticeDetailText" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="noticeModalVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="oaModalVisible" title="微信服务号消息模板" width="700px">
<el-form label-width="140px">
<el-form-item label="模板应用场景">
<el-input :model-value="currentRow.noticeNode || '-'" readonly />
</el-form-item>
<el-form-item label="启用">
<el-switch
:model-value="isOpen(currentRow.oaStatus)"
:disabled="!currentRow.oaId"
@change="(v) => toggleOaChannel(v, currentRow)"
/>
</el-form-item>
<el-form-item label="模板名称">
<el-input :model-value="currentRow.oaName || '-'" readonly />
</el-form-item>
<el-form-item label="微信模板ID">
<el-input :model-value="currentRow.oaCode || '-'" readonly />
</el-form-item>
<el-form-item label="模板内容">
<el-input
:model-value="currentRow.oaContent || '-'"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
readonly
/>
</el-form-item>
<el-alert v-if="!currentRow.oaId" type="warning" :closable="false" title="当前场景未匹配到微信服务号模板。" />
</el-form>
<template #footer>
<el-button @click="oaModalVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="emailModalVisible" title="邮箱消息模板" width="700px">
<el-form label-width="120px">
<el-form-item label="模板应用场景">
<el-input :model-value="currentRow.noticeNode || '-'" readonly />
</el-form-item>
<el-form-item label="启用">
<el-switch
:model-value="isOpen(currentRow.emailStatus)"
@change="(v) => toggleEmailChannel(v, currentRow)"
/>
</el-form-item>
<el-form-item label="模板内容">
<el-input :model-value="emailDetailText" type="textarea" :autosize="{ minRows: 4, maxRows: 8 }" readonly />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="emailModalVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="mpModalVisible" title="微信小程序订阅消息模板" width="700px">
<el-form label-width="150px">
<el-form-item label="模板应用场景">
<el-input :model-value="currentRow.noticeNode || '-'" readonly />
</el-form-item>
<el-form-item label="启用">
<el-switch
:model-value="isOpen(currentRow.mpStatus)"
:disabled="!currentRow.mpId"
@change="(v) => toggleMpChannel(v, currentRow)"
/>
</el-form-item>
<el-form-item label="模板名称">
<el-input :model-value="currentRow.mpName || '-'" readonly />
</el-form-item>
<el-form-item label="公共模板库ID">
<el-input :model-value="currentRow.mpTemplateId || '-'" readonly />
</el-form-item>
<el-form-item label="订阅消息模板ID">
<el-input :model-value="currentRow.mpCode || '-'" readonly />
</el-form-item>
<el-form-item label="模板内容">
<el-input
:model-value="currentRow.mpContent || '-'"
type="textarea"
:autosize="{ minRows: 4, maxRows: 8 }"
readonly
/>
</el-form-item>
<el-alert v-if="!currentRow.mpId" type="warning" :closable="false" title="当前场景未匹配到微信小程序模板。" />
</el-form>
<template #footer>
<el-button @click="mpModalVisible = false">关闭</el-button>
</template>
</el-dialog>
</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: "",
};
},
methods: {
isOpen(status) {
return String(status || "").toUpperCase() === "OPEN";
},
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;
}
},
changePageSize() {
this.searchForm.pageNumber = 1;
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.memberRows[idx][key] = val;
if (this.currentRow && this.currentRow.id === row.id) 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));
const params = { id: row.oaId, enable: v };
try {
const res = await API_Setting.editWechatMessageTemplate(row.oaId, params);
if (!(res && res.success)) {
this.updateRowStatus(row, "oaStatus", old);
this.$Message.error((res && res.message) || "服务号开关更新失败");
} else {
this.$Message.success("服务号开关更新成功");
}
} catch (e) {
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>
<style lang="scss" scoped>
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.toolbar-title {
font-weight: 600;
}
.ops a {
color: #409eff;
cursor: pointer;
text-decoration: none;
}
.link-disabled {
color: #c0c4cc;
cursor: not-allowed;
text-decoration: none;
}
.op-split {
display: inline-block;
margin: 0 8px;
color: #dcdfe6;
}
</style>

View File

@@ -57,7 +57,7 @@
<div v-if="row" class="ops">
<a class="link-text" @click="detail(row)">详细</a>
<span class="op-split">|</span>
<a class="link-text" @click="delete(row.id)">删除</a>
<a class="link-text" @click="removeMessage(row.id)">删除</a>
</div>
</template>
</el-table-column>
@@ -207,16 +207,21 @@
</el-select>
</el-form-item>
<el-form-item v-if="memberShow" label="选择会员" prop="scopeType">
<el-button type="primary" plain @click="addVip">选择会员</el-button>
<div v-if="messageSendForm.messageClient == 'member'" style="margin-top: 24px">
<el-table border :data="selectedMember" style="width: 100%">
<el-table-column prop="nickName" label="用户名称" min-width="120" />
<el-table-column label="手机号" min-width="120">
<div class="member-select-wrap">
<el-button type="primary" plain @click="addVip">选择会员</el-button>
<el-table
v-if="messageSendForm.messageClient == 'member'"
border
:data="selectedMember"
class="member-select-table"
>
<el-table-column prop="nickName" label="用户名称" min-width="140" show-overflow-tooltip />
<el-table-column label="手机号" min-width="140">
<template #default="{ row }">
<span v-if="row">{{ row.mobile || "暂未填写" }}</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="50" align="center">
<el-table-column label="操作" width="80" align="center">
<template #default="{ row, $index }">
<a v-if="row" class="link-text" @click="delUser($index)">删除</a>
</template>
@@ -235,9 +240,14 @@
<userList
v-if="checkUserList"
ref="memberLayout"
:selectedMember="true"
:selectedList="selectedMember"
@callback="callbackSelectUser"
/>
<template #footer>
<el-button @click="cancelMemberSelect">取消</el-button>
<el-button type="primary" @click="confirmMemberSelect">确定</el-button>
</template>
</el-dialog>
<el-dialog
@@ -359,6 +369,7 @@ export default {
return {
activeTab: "MESSAGE",
checkUserList: false,
memberSelectSnapshot: [],
selectedMember: [],
loading: true,
modalVisible: false,
@@ -455,10 +466,17 @@ export default {
this.loading = false;
},
addVip() {
this.memberSelectSnapshot = JSON.parse(JSON.stringify(this.selectedMember));
this.checkUserList = true;
this.$nextTick(() => {
this.$refs.memberLayout.selectedMember = true;
});
},
cancelMemberSelect() {
this.selectedMember = JSON.parse(JSON.stringify(this.memberSelectSnapshot));
this.reSelectMember();
this.checkUserList = false;
},
confirmMemberSelect() {
this.reSelectMember();
this.checkUserList = false;
},
paneChange(v) {
if (v == "SETTING") {
@@ -526,7 +544,7 @@ export default {
return shop ? shop.storeName : "";
});
},
delete(id) {
removeMessage(id) {
this.$Modal.confirm({
title: "确认删除",
content: "您确认删除此站内信 ?",
@@ -545,6 +563,7 @@ export default {
this.messageModalTitle = "发送站内信";
this.shopShow = false;
this.memberShow = false;
this.selectedMember = [];
this.messageSendForm = {
messageRange: "ALL",
messageClient: "member",
@@ -719,4 +738,11 @@ export default {
margin: 0 8px;
color: #dcdfe6;
}
.member-select-wrap {
width: 100%;
}
.member-select-table {
width: 100%;
margin-top: 12px;
}
</style>

View File

@@ -83,15 +83,27 @@ methodItem h4 {
border-radius: 6px;
}
}
.choose-member-item {
width: 100%;
.el-form-item__content {
margin-left: 0 !important;
width: 100%;
}
}
.choose-member {
display: flex;
width: 100%;
min-width: 580px;
height: 400px;
border: 1px solid #eee;
margin-bottom: 20px;
}
.source-member {
width: 50%;
flex: 3;
min-width: 0;
border-right: 1px solid #eee;
padding: 10px;
}
@@ -116,7 +128,8 @@ methodItem h4 {
white-space: nowrap;
}
.traget-member {
width: 50%;
flex: 2;
min-width: 0;
padding: 10px;
}
.scroll-card {

View File

@@ -184,7 +184,7 @@
<el-dialog
v-model="sendSmsModal"
:title="sendSmsModalTitle"
width="900px"
width="1100px"
:close-on-click-modal="false"
destroy-on-close
>
@@ -234,7 +234,7 @@
<el-radio value="2">自定义选择</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="customSms">
<el-form-item v-if="customSms" label-width="0" class="choose-member-item">
<div class="choose-member">
<div class="source-member">
<el-input