优化小程序部分

This commit is contained in:
hero920103 committed 2026-10-31 19:17:37 +08:00
1 parent 727319a6eb
commit d4e89788c5
46 files changed
+4906 -63

No files matched your search

+88
View File
@@ -39,3 +39,91 @@ export function deleteMiniApp(id) {
method: "post",
});
}
/**
* 消息推送配置(对应小程序后台「开发管理 → 开发设置 → 消息推送」)
*/
/** 获取指定小程序的推送配置(含回调地址,aes_key 掩码回显) */
export function getMiniAppPush(appId) {
return request({
url: "/platform/wechatMini/app/push",
method: "get",
params: { app_id: appId },
});
}
/**
* 保存推送配置
* @param {Object} data
* @param {string} data.app_id 小程序 AppID
* @param {string} [data.token] 令牌(3-32 位字母数字)
* @param {string} [data.aes_key] 消息加解密密钥(43 位,掩码/空串 = 不修改原值)
* @param {string} [data.encrypt_mode] plain / compatible / safe
* @param {string} [data.data_format] xml / json
* @param {string} [data.push_reply] 客服消息自动回复文案
*/
export function saveMiniAppPush(data) {
return request({
url: "/platform/wechatMini/app/push/save",
method: "post",
data,
});
}
/** 生成随机 Token 与 EncodingAESKey */
export function generateMiniAppPushSecret() {
return request({
url: "/platform/wechatMini/app/push/generate",
method: "post",
});
}
/**
* 订阅消息模板(小程序后台「功能 → 订阅消息」选用模板后登记)
*/
/** 模板列表 */
export function listMiniTemplates(appId) {
return request({
url: "/platform/wechatMini/templates",
method: "get",
params: { app_id: appId },
});
}
/**
* 保存模板
* @param {Object} data
* @param {string} data.app_id 小程序 AppID
* @param {string} data.code 场景码,如 notice
* @param {string} data.template_id 微信模板 ID
* @param {string} [data.title] 场景名称
* @param {string} [data.page] 点击消息跳转的小程序页面
* @param {string} [data.fields] JSON:微信字段键 → 语义变量名,如 {"thing1":"title"}
* @param {number} data.enabled 1=启用 0=停用
*/
export function saveMiniTemplate(data) {
return request({
url: "/platform/wechatMini/template/save",
method: "post",
data,
});
}
/** 删除模板 */
export function deleteMiniTemplate(id) {
return request({
url: `/platform/wechatMini/template/delete/${id}`,
method: "post",
});
}
/** 给指定租户用户发一条测试订阅消息 */
export function testMiniTemplate(data) {
return request({
url: "/platform/wechatMini/template/test",
method: "post",
data,
});
}
@@ -61,6 +61,13 @@
<span v-else class="empty-tip">未上传</span>
</template>
</el-table-column>
<el-table-column label="消息推送" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.token ? 'success' : 'info'" size="small">
{{ scope.row.token ? "已配置" : "未配置" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="启用" width="80" align="center">
<template #default="scope">
<el-switch
@@ -197,6 +204,157 @@
placeholder="选填"
/>
</el-form-item>
<!-- 消息推送配置(对应小程序后台「开发管理 → 开发设置 → 消息推送」) -->
<el-divider content-position="left">
<span class="divider-text">消息推送</span>
</el-divider>
<el-alert
type="info"
:closable="false"
show-icon
class="push-alert"
title="把下面的「回调地址 / Token / EncodingAESKey」原样填到小程序后台「开发管理 → 开发设置 → 消息推送」,加解密方式与数据格式需与后台保持一致。"
/>
<el-form-item label="回调地址">
<div class="callback-field">
<el-input
v-model="pushForm.callback_url"
readonly
placeholder="保存后由服务端生成"
/>
<el-button size="small" @click="copyText(pushForm.callback_url)">
复制
</el-button>
</div>
<div class="form-tip block">
必须以 http:// 或 https:// 开头(80 / 443 端口),且微信服务器能直接访问。
若显示为相对路径,请自行补上域名,如
<code>https://api.yunzer.cn/api/wechat/mini/callback</code>。
</div>
</el-form-item>
<el-form-item label="Token">
<div class="callback-field">
<el-input
v-model="pushForm.token"
placeholder="3-32 位字母数字"
maxlength="32"
/>
<el-button size="small" @click="generateSecrets">
随机生成
</el-button>
</div>
</el-form-item>
<el-form-item label="EncodingAESKey">
<el-input
v-model="pushForm.aes_key"
type="password"
show-password
:placeholder="
pushConfigured ? '已配置,留空 / 掩码表示不修改' : '43 位字母数字'
"
maxlength="43"
/>
<div class="form-tip block">
安全 / 兼容模式必填,与小程序后台填写的 EncodingAESKey 必须完全一致。
</div>
</el-form-item>
<el-form-item label="加解密方式">
<el-radio-group v-model="pushForm.encrypt_mode">
<el-radio value="plain">明文模式</el-radio>
<el-radio value="compatible">兼容模式</el-radio>
<el-radio value="safe">安全模式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="数据格式">
<el-radio-group v-model="pushForm.data_format">
<el-radio value="xml">XML</el-radio>
<el-radio value="json">JSON</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="自动回复文案">
<el-input
v-model="pushForm.push_reply"
type="textarea"
:rows="2"
maxlength="500"
placeholder="选填。用户发来客服消息时,5 秒内自动回复该文案;留空表示只接收不回复。"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="pushSaving"
:disabled="!form.id"
@click="handleSavePush"
>
保存消息推送配置
</el-button>
<span v-if="!form.id" class="form-tip">请先保存小程序,再配置消息推送</span>
</el-form-item>
<!-- 订阅消息模板 -->
<el-divider content-position="left">
<span class="divider-text">订阅消息模板</span>
</el-divider>
<el-alert
type="info"
:closable="false"
show-icon
class="push-alert"
title="要主动给用户发提示(审批待办、站内通知等),需要用「订阅消息」:先在小程序后台「功能 → 订阅消息 → 公共模板库」选一个模板,把模板 ID 和字段填到下面。"
/>
<el-table :data="templates" size="small" style="width: 100%">
<el-table-column prop="code" label="场景码" width="110" align="center" />
<el-table-column prop="title" label="场景名称" min-width="120" align="center">
<template #default="{ row }">{{ row.title || "-" }}</template>
</el-table-column>
<el-table-column
prop="template_id"
label="模板 ID"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column label="启用" width="70" align="center">
<template #default="{ row }">
<el-tag :type="row.enabled === 1 ? 'success' : 'info'" size="small">
{{ row.enabled === 1 ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="175" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openTemplate(row)">
编辑
</el-button>
<el-button size="small" type="success" link @click="sendTestTemplate(row)">
测试
</el-button>
<el-button size="small" type="danger" link @click="removeTemplate(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-button
size="small"
style="margin-top: 10px"
:disabled="!form.id"
@click="openTemplate()"
>
新增模板
</el-button>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -205,6 +363,65 @@
</el-button>
</template>
</el-dialog>
<!-- 订阅消息模板编辑对话框 -->
<el-dialog
v-model="templateDialogVisible"
title="订阅消息模板"
width="560px"
append-to-body
>
<el-form :model="templateForm" label-width="100px">
<el-form-item label="场景码">
<el-input
v-model="templateForm.code"
placeholder="如 notice(站内通知)"
:disabled="!!templateForm.id"
/>
<div class="form-tip block">
业务侧按场景码触发:notice = 站内信 / 审批待办等通用提醒
</div>
</el-form-item>
<el-form-item label="场景名称">
<el-input v-model="templateForm.title" placeholder="如 站内通知" maxlength="64" />
</el-form-item>
<el-form-item label="模板 ID">
<el-input
v-model="templateForm.template_id"
placeholder="小程序后台订阅消息模板的 ID"
/>
</el-form-item>
<el-form-item label="跳转页面">
<el-input
v-model="templateForm.page"
placeholder="如 pages/index/index(点击消息进入的小程序页面)"
/>
</el-form-item>
<el-form-item label="字段映射">
<el-input
v-model="templateForm.fields"
type="textarea"
:rows="3"
placeholder='{"thing1":"title","phrase2":"content","time3":"time"}'
/>
<div class="form-tip block">
左边是微信模板的字段名,右边是平台这边的变量名。可用变量:
<code>title</code>(标题)、<code>content</code>(内容)、<code>time</code>(时间)
</div>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="templateForm.enabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="templateForm.remark" type="textarea" :rows="2" maxlength="200" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="templateDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="templateSaving" @click="handleSaveTemplate">
保存
</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -212,7 +429,18 @@
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus } from "@element-plus/icons-vue";
import { listMiniApps, saveMiniApp, deleteMiniApp } from "@/api/miniApp";
import {
listMiniApps,
saveMiniApp,
deleteMiniApp,
getMiniAppPush,
saveMiniAppPush,
generateMiniAppPushSecret,
listMiniTemplates,
saveMiniTemplate,
deleteMiniTemplate,
testMiniTemplate,
} from "@/api/miniApp";
import { formatDateTime } from "@/utils/datetime";
import { smartUpload } from "@/utils/qiniuUpload";
@@ -228,10 +456,15 @@ const previewUrl = (url) => {
const loading = ref(false);
const saving = ref(false);
const qrcodeUploading = ref(false);
const pushSaving = ref(false);
const templateSaving = ref(false);
const templateDialogVisible = ref(false);
const templates = ref([]);
const apps = ref([]);
const dialogVisible = ref(false);
const formRef = ref(null);
const secretConfigured = ref(false);
const pushConfigured = ref(false);
const emptyForm = () => ({
id: 0,
@@ -244,7 +477,31 @@ const emptyForm = () => ({
remark: "",
});
const emptyPushForm = () => ({
app_id: "",
token: "",
aes_key: "",
encrypt_mode: "plain",
data_format: "xml",
push_reply: "",
callback_url: "",
});
const emptyTemplateForm = () => ({
id: 0,
app_id: "",
code: "notice",
template_id: "",
title: "站内通知",
page: "",
fields: '{"thing1":"title","phrase2":"content","time3":"time"}',
enabled: 1,
remark: "",
});
const form = ref(emptyForm());
const pushForm = ref(emptyPushForm());
const templateForm = ref(emptyTemplateForm());
const rules = {
name: [{ required: true, message: "请输入应用名称", trigger: "blur" }],
@@ -273,6 +530,9 @@ const fetchApps = async () => {
const openAdd = () => {
form.value = emptyForm();
secretConfigured.value = false;
pushConfigured.value = false;
pushForm.value = emptyPushForm();
templates.value = [];
dialogVisible.value = true;
};
@@ -289,6 +549,199 @@ const openEdit = (row) => {
};
secretConfigured.value = !!row.configured;
dialogVisible.value = true;
fetchPushConfig(row.app_id);
fetchTemplates(row.app_id);
};
// 读取订阅消息模板列表
const fetchTemplates = async (appId) => {
if (!appId) return;
try {
const res = await listMiniTemplates(appId);
if (res.code === 200) {
templates.value = res.data?.list || [];
}
} catch (error) {
ElMessage.error(error?.message || "获取模板列表失败");
}
};
// 新增 / 编辑模板
const openTemplate = (row) => {
if (row) {
templateForm.value = { ...row };
} else {
templateForm.value = {
...emptyTemplateForm(),
app_id: form.value.app_id,
};
}
templateDialogVisible.value = true;
};
// 保存模板
const handleSaveTemplate = async () => {
if (!templateForm.value.code || !templateForm.value.template_id) {
ElMessage.warning("请填写场景码和模板 ID");
return;
}
templateSaving.value = true;
try {
const res = await saveMiniTemplate({ ...templateForm.value });
if (res.code === 200) {
ElMessage.success(res.msg || "已保存");
templateDialogVisible.value = false;
fetchTemplates(form.value.app_id);
} else {
ElMessage.error(res.msg || "保存失败");
}
} catch (error) {
ElMessage.error(error?.message || "保存失败");
} finally {
templateSaving.value = false;
}
};
// 删除模板
const removeTemplate = async (row) => {
try {
await ElMessageBox.confirm(`确认删除模板「${row.title || row.code}」?`, "删除模板", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
} catch {
return;
}
try {
const res = await deleteMiniTemplate(row.id);
if (res.code === 200) {
ElMessage.success("已删除");
fetchTemplates(form.value.app_id);
} else {
ElMessage.error(res.msg || "删除失败");
}
} catch (error) {
ElMessage.error(error?.message || "删除失败");
}
};
// 测试发送:需要指定一个租户用户(该用户需已授权订阅)
const sendTestTemplate = async (row) => {
let userId = "";
try {
const { value } = await ElMessageBox.prompt(
"请输入测试用的租户用户 ID(该用户必须已用小程序登录并授权过订阅)",
"发送测试消息",
{
confirmButtonText: "发送",
cancelButtonText: "取消",
inputPattern: /^\d+$/,
inputErrorMessage: "请输入数字 ID",
type: "info",
},
);
userId = value;
} catch {
return;
}
try {
const res = await testMiniTemplate({
tenant_user_id: Number(userId),
app_id: form.value.app_id,
code: row.code,
});
if (res.code === 200) {
ElMessage.success(res.msg || "已发送");
} else {
ElMessage.error(res.msg || "发送失败");
}
} catch (error) {
ElMessage.error(error?.message || "发送失败");
}
};
// 读取消息推送配置(回调地址由服务端生成,Token 直接回显,AESKey 掩码)
const fetchPushConfig = async (appId) => {
if (!appId) return;
pushForm.value = emptyPushForm();
pushForm.value.app_id = appId;
try {
const res = await getMiniAppPush(appId);
if (res.code === 200) {
const data = res.data || {};
pushForm.value = {
app_id: appId,
token: data.token || "",
aes_key: data.aes_key || "",
encrypt_mode: data.encrypt_mode || "plain",
data_format: data.data_format || "xml",
push_reply: data.push_reply || "",
callback_url: data.callback_url || "",
};
pushConfigured.value = !!data.configured;
} else {
ElMessage.error(res.msg || "获取消息推送配置失败");
}
} catch (error) {
ElMessage.error(error?.message || "获取消息推送配置失败");
}
};
// 随机生成 Token 与 EncodingAESKey
const generateSecrets = async () => {
try {
const res = await generateMiniAppPushSecret();
if (res.code === 200) {
pushForm.value.token = res.data?.token || "";
pushForm.value.aes_key = res.data?.aes_key || "";
ElMessage.success("已生成,记得保存");
} else {
ElMessage.error(res.msg || "生成失败");
}
} catch (error) {
ElMessage.error(error?.message || "生成失败");
}
};
// 保存消息推送配置
const handleSavePush = async () => {
if (!pushForm.value.app_id) {
ElMessage.warning("请先保存小程序");
return;
}
pushSaving.value = true;
try {
const res = await saveMiniAppPush({ ...pushForm.value });
if (res.code === 200) {
ElMessage.success(res.msg || "消息推送配置已保存");
if (res.data?.callback_url) {
pushForm.value.callback_url = res.data.callback_url;
}
pushConfigured.value = true;
fetchApps();
} else {
ElMessage.error(res.msg || "保存失败");
}
} catch (error) {
ElMessage.error(error?.message || "保存失败");
} finally {
pushSaving.value = false;
}
};
// 复制文本
const copyText = async (text) => {
if (!text) {
ElMessage.warning("无可复制内容");
return;
}
try {
await navigator.clipboard.writeText(text);
ElMessage.success("已复制");
} catch {
ElMessage.error("复制失败,请手动选择复制");
}
};
// 二维码上传:上传成功后先写入表单,保存时随应用一起落库
@@ -422,6 +875,26 @@ onMounted(() => {
margin-bottom: 16px;
}
.push-alert {
margin-bottom: 14px;
}
.divider-text {
font-size: 13px;
color: #5a6478;
}
.callback-field {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
:deep(.el-input) {
flex: 1;
}
}
.form-tip {
margin-left: 10px;
color: #8a94ad;