增加微信通知功能
This commit is contained in:
Vendored
+2
@@ -61,6 +61,8 @@ declare module 'vue' {
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElStep: typeof import('element-plus/es')['ElStep']
|
||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/*************************************************
|
||||
* 微信公众号(服务号):配置 / 绑定 / 粉丝 / 推送
|
||||
*************************************************/
|
||||
|
||||
/** 获取服务号配置(敏感字段返回掩码) */
|
||||
export function getWechatMpConfig() {
|
||||
return request({
|
||||
url: "/platform/wechatMp/config",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/** 保存服务号配置(掩码 / 空串表示不修改原值) */
|
||||
export function saveWechatMpConfig(data) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/config",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 连接测试:校验 AppID / AppSecret 是否能换取 access_token */
|
||||
export function testWechatMpConnection() {
|
||||
return request({
|
||||
url: "/platform/wechatMp/test",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
/** 发起绑定:生成带参数二维码(扫码关注后公众号回复验证码) */
|
||||
export function startWechatMpBind() {
|
||||
return request({
|
||||
url: "/platform/wechatMp/bind/start",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
/** 查询绑定会话状态:0待扫码 1已扫码待核销 2已核销 3已过期 */
|
||||
export function getWechatMpBindStatus(scene) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/bind/status",
|
||||
method: "get",
|
||||
params: { scene },
|
||||
});
|
||||
}
|
||||
|
||||
/** 核销验证码完成绑定 */
|
||||
export function confirmWechatMpBind(data) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/bind/confirm",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 当前平台用户的绑定信息 */
|
||||
export function getMyWechatMpBinding() {
|
||||
return request({
|
||||
url: "/platform/wechatMp/my",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/** 解除当前平台用户的微信绑定 */
|
||||
export function unbindWechatMp() {
|
||||
return request({
|
||||
url: "/platform/wechatMp/unbind",
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
/** 粉丝列表 */
|
||||
export function getWechatMpFollowers(params) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/followers",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 设置粉丝备注 */
|
||||
export function setWechatMpFollowerRemark(data) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/follower/remark",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 解除粉丝绑定(保留粉丝记录) */
|
||||
export function unbindWechatMpFollower(data) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/follower/unbind",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量推送(认证后可用) */
|
||||
export function pushWechatMpMessage(data) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/push",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 测试推送:发送给当前账号绑定的微信 */
|
||||
export function pushWechatMpTest(data) {
|
||||
return request({
|
||||
url: "/platform/wechatMp/push/test",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
@@ -317,24 +317,213 @@
|
||||
</span>
|
||||
</template>
|
||||
<div class="tab-pane-content">
|
||||
<!-- 服务号配置 -->
|
||||
<el-card shadow="never" class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>微信公众号/企业微信推送</span>
|
||||
<span>微信公众号(服务号)配置</span>
|
||||
<el-tag v-if="Number(wechatCfg.verified) === 1" type="success" size="small">已认证</el-tag>
|
||||
<el-tag v-else type="warning" size="small" class="ml8">未认证:可关注取码,认证后可推送</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="formData.wechat" label-width="140px" label-position="right">
|
||||
<el-form label-width="140px" label-position="right">
|
||||
<el-form-item label="启用状态">
|
||||
<el-switch v-model="formData.wechat.enabled" />
|
||||
<el-switch v-model="wechatCfg.enabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppID / CorpID">
|
||||
<el-input v-model="formData.wechat.app_id" placeholder="请输入公众号 AppID 或企业微信 CorpID" />
|
||||
<el-form-item label="AppID" required>
|
||||
<el-input v-model="wechatCfg.app_id" placeholder="公众号后台「设置与开发 → 基本配置」中的 AppID" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret / Secret">
|
||||
<el-input v-model="formData.wechat.app_secret" type="password" show-password placeholder="请输入 AppSecret" />
|
||||
<el-form-item label="AppSecret">
|
||||
<el-input v-model="wechatCfg.app_secret" type="password" show-password placeholder="已保存时显示掩码,保持不变则无需修改" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="模版ID / AgentID">
|
||||
<el-input v-model="formData.wechat.template_id" placeholder="请输入模版消息 ID 或企业微信应用 AgentID" />
|
||||
<el-form-item label="服务器 Token">
|
||||
<el-input v-model="wechatCfg.token" placeholder="3-32 位字母/数字,两端保持一致" clearable>
|
||||
<template #append>
|
||||
<el-button @click="wechatCfg.token = randomWechatToken(24)">随机生成</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="EncodingAESKey">
|
||||
<el-input v-model="wechatCfg.aes_key" placeholder="43 位;安全/兼容模式必填" clearable>
|
||||
<template #append>
|
||||
<el-button @click="wechatCfg.aes_key = randomWechatToken(43)">随机生成</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息加解密方式">
|
||||
<el-radio-group v-model="wechatCfg.encrypt_mode">
|
||||
<el-radio label="plain">明文模式</el-radio>
|
||||
<el-radio label="compatible">兼容模式</el-radio>
|
||||
<el-radio label="safe">安全模式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="公众号已认证">
|
||||
<el-switch v-model="wechatCfg.verified" :active-value="1" :inactive-value="0" />
|
||||
<span class="form-tip-inline">认证后才可主动推送(模板消息/订阅通知)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板消息ID">
|
||||
<el-input v-model="wechatCfg.template_id" placeholder="认证后在公众号后台申请,用于提醒/公告推送" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务器地址">
|
||||
<el-input v-model="wechatCfg.callback_url" readonly>
|
||||
<template #append>
|
||||
<el-button @click="copyWechatText(wechatCfg.callback_url)">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip">
|
||||
① 微信后台(公众平台 / 微信开发者平台)「设置与开发 → 基本配置」→ 复制 AppID / AppSecret 填入上方,保存并启用;<br />
|
||||
② 将本地址连同上方 Token / EncodingAESKey 填入微信后台「服务器配置」(加解密方式保持一致)——提交时微信会立即请求本地址校验,需本页已保存并启用;<br />
|
||||
③ 若「连接测试」提示 40164,请在微信后台「基本配置 → IP白名单」加入本服务器出口 IP(服务器执行 curl ifconfig.me 可查看)。
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="wechatSaving" @click="saveWechatConfig">保存配置</el-button>
|
||||
<el-button :loading="wechatTesting" @click="testWechatConfig">连接测试</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 绑定我的微信 -->
|
||||
<el-card shadow="never" class="settings-card bind-card">
|
||||
<template #header>
|
||||
<div class="card-header"><span>绑定我的微信(接收提醒 / 公告)</span></div>
|
||||
</template>
|
||||
<template v-if="wechatBinding.bound">
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="微信昵称">{{ wechatBinding.nickname || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关注状态">
|
||||
<el-tag size="small" :type="Number(wechatBinding.subscribe) === 1 ? 'success' : 'danger'">
|
||||
{{ Number(wechatBinding.subscribe) === 1 ? "已关注" : "已取关" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="绑定时间">
|
||||
{{ wechatBinding.bind_time ? formatTime(wechatBinding.bind_time) : "-" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="openid">{{ wechatBinding.openid }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="bind-actions">
|
||||
<el-button type="primary" plain size="small" :loading="wechatPushTesting" @click="sendWechatTestPush">
|
||||
发送测试推送
|
||||
</el-button>
|
||||
<el-button type="danger" plain size="small" @click="unbindMyWechat">解除绑定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-steps :active="bindStep" simple class="bind-steps">
|
||||
<el-step title="微信扫码关注" />
|
||||
<el-step title="公众号回复验证码" />
|
||||
<el-step title="输入验证码完成绑定" />
|
||||
</el-steps>
|
||||
<div class="bind-area">
|
||||
<div class="bind-qr">
|
||||
<img v-if="bindQrUrl" :src="bindQrUrl" class="qr-img" alt="公众号二维码" />
|
||||
<el-button v-else type="primary" :loading="bindStarting" @click="startBind">获取绑定二维码</el-button>
|
||||
</div>
|
||||
<div class="bind-form">
|
||||
<el-input v-model="bindCode" placeholder="输入公众号回复的验证码" maxlength="8" class="bind-input" />
|
||||
<el-button type="primary" :loading="bindConfirming" @click="confirmBind">完成绑定</el-button>
|
||||
<p class="form-tip">扫码关注公众号后,公众号会自动回复 6 位验证码({{ bindTtlText }})。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 粉丝列表 -->
|
||||
<el-card shadow="never" class="settings-card bind-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>关注用户(粉丝)</span>
|
||||
<div class="follower-tools">
|
||||
<el-input v-model="followerQuery.keyword" size="small" placeholder="昵称 / openid / 备注" clearable @keyup.enter="loadFollowers" />
|
||||
<el-select v-model="followerQuery.bind_type" size="small" placeholder="绑定类型" clearable class="follower-select">
|
||||
<el-option label="平台用户" value="platform_user" />
|
||||
<el-option label="租户用户" value="tenant_user" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="loadFollowers">查询</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="followers" size="small" v-loading="followersLoading">
|
||||
<el-table-column label="用户" min-width="140">
|
||||
<template #default="{ row }">{{ row.nickname || "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="openid" label="openid" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="关注状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="Number(row.subscribe) === 1 ? 'success' : 'info'" effect="plain">
|
||||
{{ Number(row.subscribe) === 1 ? "已关注" : "已取关" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定对象" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.bind_name">{{ row.bind_name }}</span>
|
||||
<span v-else class="muted">未绑定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="关注时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ row.subscribe_time ? formatTime(row.subscribe_time) : "-" }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="170" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="editFollowerRemark(row)">备注</el-button>
|
||||
<el-button v-if="row.bind_name" size="small" type="danger" link @click="unbindFollower(row)">
|
||||
解除绑定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="follower-pager"
|
||||
layout="total, prev, pager, next"
|
||||
:total="followerQuery.total"
|
||||
:current-page="followerQuery.page"
|
||||
:page-size="followerQuery.pageSize"
|
||||
@current-change="handleFollowerPage"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 消息推送 -->
|
||||
<el-card shadow="never" class="settings-card bind-card">
|
||||
<template #header>
|
||||
<div class="card-header"><span>消息推送(提醒 / 公告)</span></div>
|
||||
</template>
|
||||
<el-alert
|
||||
v-if="Number(wechatCfg.verified) !== 1"
|
||||
type="warning"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="push-alert"
|
||||
title="公众号未认证:微信不允许主动推送。认证后配置「模板消息ID」即可使用;当前可先积累关注用户。"
|
||||
/>
|
||||
<el-form label-width="120px" label-position="right">
|
||||
<el-form-item label="推送范围">
|
||||
<el-select v-model="pushForm.bind_type" style="width: 200px">
|
||||
<el-option label="全部已关注粉丝" value="" />
|
||||
<el-option label="平台用户" value="platform_user" />
|
||||
<el-option label="租户用户" value="tenant_user" />
|
||||
</el-select>
|
||||
<span class="form-tip-inline">单次最多 500 人</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题">
|
||||
<el-input v-model="pushForm.title" maxlength="50" placeholder="如:系统维护通知" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="pushForm.content" type="textarea" :rows="3" maxlength="200" placeholder="推送正文" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注 / 跳转">
|
||||
<el-input v-model="pushForm.remark" maxlength="50" placeholder="备注(可选)" />
|
||||
<el-input v-model="pushForm.url" class="push-url" placeholder="点击消息跳转地址(可选,如 https://www.yunzer.cn)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="pushSending"
|
||||
:disabled="Number(wechatCfg.verified) !== 1"
|
||||
@click="sendPush"
|
||||
>
|
||||
发送推送
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -386,8 +575,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { reactive, ref, watch, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import {
|
||||
Message,
|
||||
Cellphone,
|
||||
@@ -403,6 +592,22 @@ import { getSmsInfo, editSmsInfo, sendTestSms } from "@/api/sms";
|
||||
import { getEmailInfo, editEmailInfo, sendTestEmail } from "@/api/email";
|
||||
import { getSiteReminderConfig, saveSiteReminderConfig } from "@/api/sitereminder";
|
||||
import { getBarkConfig, saveBarkConfig, sendTestBark } from "@/api/sitesettings";
|
||||
import { formatTime } from "@/utils/datetime";
|
||||
import {
|
||||
getWechatMpConfig,
|
||||
saveWechatMpConfig,
|
||||
testWechatMpConnection,
|
||||
startWechatMpBind,
|
||||
getWechatMpBindStatus,
|
||||
confirmWechatMpBind,
|
||||
getMyWechatMpBinding,
|
||||
unbindWechatMp,
|
||||
getWechatMpFollowers,
|
||||
setWechatMpFollowerRemark,
|
||||
unbindWechatMpFollower,
|
||||
pushWechatMpMessage,
|
||||
pushWechatMpTest
|
||||
} from "@/api/wechatMp";
|
||||
|
||||
const STORAGE_KEY = "notification_settings_draft";
|
||||
const activeSubTab = ref("email");
|
||||
@@ -458,12 +663,6 @@ const formData = reactive({
|
||||
server_url: "https://api.day.app",
|
||||
device_key: "",
|
||||
},
|
||||
wechat: {
|
||||
enabled: false,
|
||||
app_id: "",
|
||||
app_secret: "",
|
||||
template_id: "",
|
||||
},
|
||||
telegram: {
|
||||
enabled: false,
|
||||
bot_token: "",
|
||||
@@ -548,7 +747,6 @@ const handleReset = () => {
|
||||
webhook: { enabled: false, url: "", method: "POST", token: "" },
|
||||
feishu: { enabled: false, webhook_url: "", secret: "" },
|
||||
bark: { enabled: false, server_url: "https://api.day.app", device_key: "" },
|
||||
wechat: { enabled: false, app_id: "", app_secret: "", template_id: "" },
|
||||
telegram: { enabled: false, bot_token: "", chat_id: "" },
|
||||
};
|
||||
|
||||
@@ -620,6 +818,11 @@ const handleSendTest = async () => {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 微信配置使用独立接口保存(页面内有独立的「保存配置」按钮)
|
||||
if (activeSubTab.value === 'wechat') {
|
||||
await saveWechatConfig();
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
if (activeSubTab.value === 'email') {
|
||||
@@ -727,12 +930,351 @@ const handleSendBarkTest = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
/* ==================== 微信公众号(服务号) ==================== */
|
||||
|
||||
const wechatCfg = reactive({
|
||||
app_id: "",
|
||||
app_secret: "",
|
||||
token: "",
|
||||
aes_key: "",
|
||||
encrypt_mode: "plain",
|
||||
template_id: "",
|
||||
verified: 0,
|
||||
enabled: 0,
|
||||
callback_url: "",
|
||||
configured: false,
|
||||
});
|
||||
const wechatSaving = ref(false);
|
||||
const wechatTesting = ref(false);
|
||||
|
||||
// 生成随机串(字母+数字):Token 取 24 位、EncodingAESKey 取 43 位。
|
||||
// 生成后需原样填入微信后台「服务器配置」,保持两端一致(微信侧提交时会请求回调地址校验)。
|
||||
const randomWechatToken = (len = 43) => {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const buf = new Uint8Array(len);
|
||||
window.crypto.getRandomValues(buf);
|
||||
let out = "";
|
||||
for (let i = 0; i < len; i++) out += chars[buf[i] % chars.length];
|
||||
return out;
|
||||
};
|
||||
|
||||
const wechatBinding = ref({ bound: false });
|
||||
const wechatPushTesting = ref(false);
|
||||
|
||||
const bindScene = ref("");
|
||||
const bindQrUrl = ref("");
|
||||
const bindCode = ref("");
|
||||
const bindStarting = ref(false);
|
||||
const bindConfirming = ref(false);
|
||||
const bindStep = ref(0);
|
||||
const bindTtlText = ref("5 分钟内有效");
|
||||
let bindPollTimer = null;
|
||||
|
||||
const followers = ref([]);
|
||||
const followersLoading = ref(false);
|
||||
const followerQuery = reactive({ keyword: "", bind_type: "", page: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const pushForm = reactive({ bind_type: "", title: "", content: "", remark: "", url: "" });
|
||||
const pushSending = ref(false);
|
||||
|
||||
const loadWechatConfig = async () => {
|
||||
try {
|
||||
const res = await getWechatMpConfig();
|
||||
if (res.code === 200 && res.data) {
|
||||
Object.assign(wechatCfg, res.data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载微信配置失败:", e);
|
||||
}
|
||||
};
|
||||
|
||||
const saveWechatConfig = async () => {
|
||||
if (!wechatCfg.app_id) {
|
||||
ElMessage.warning("请先填写 AppID");
|
||||
return;
|
||||
}
|
||||
wechatSaving.value = true;
|
||||
try {
|
||||
const res = await saveWechatMpConfig({
|
||||
app_id: wechatCfg.app_id,
|
||||
app_secret: wechatCfg.app_secret,
|
||||
token: wechatCfg.token,
|
||||
aes_key: wechatCfg.aes_key,
|
||||
encrypt_mode: wechatCfg.encrypt_mode,
|
||||
template_id: wechatCfg.template_id,
|
||||
verified: Number(wechatCfg.verified) === 1,
|
||||
enabled: Number(wechatCfg.enabled) === 1,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("微信配置保存成功");
|
||||
await loadWechatConfig();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "保存失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "保存失败");
|
||||
} finally {
|
||||
wechatSaving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const testWechatConfig = async () => {
|
||||
wechatTesting.value = true;
|
||||
try {
|
||||
const res = await testWechatMpConnection();
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg || "连接成功");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "连接失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "连接失败");
|
||||
} finally {
|
||||
wechatTesting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const copyWechatText = async (text) => {
|
||||
if (!text) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
ElMessage.success("已复制");
|
||||
} catch {
|
||||
ElMessage.warning("复制失败,请手动选择复制");
|
||||
}
|
||||
};
|
||||
|
||||
const loadWechatBinding = async () => {
|
||||
try {
|
||||
const res = await getMyWechatMpBinding();
|
||||
if (res.code === 200) {
|
||||
wechatBinding.value = res.data || { bound: false };
|
||||
}
|
||||
} catch {
|
||||
wechatBinding.value = { bound: false };
|
||||
}
|
||||
};
|
||||
|
||||
const stopBindPoll = () => {
|
||||
if (bindPollTimer) {
|
||||
clearInterval(bindPollTimer);
|
||||
bindPollTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startBind = async () => {
|
||||
bindStarting.value = true;
|
||||
try {
|
||||
const res = await startWechatMpBind();
|
||||
if (res.code !== 200) {
|
||||
ElMessage.error(res.msg || "获取二维码失败");
|
||||
return;
|
||||
}
|
||||
bindScene.value = res.data?.scene || "";
|
||||
bindQrUrl.value = res.data?.qrcode_url || "";
|
||||
bindStep.value = 0;
|
||||
const ttl = Number(res.data?.expires_in || 300);
|
||||
bindTtlText.value = ttl >= 60 ? `${Math.floor(ttl / 60)} 分钟内有效` : `${ttl} 秒内有效`;
|
||||
stopBindPoll();
|
||||
bindPollTimer = setInterval(async () => {
|
||||
if (!bindScene.value) return;
|
||||
try {
|
||||
const st = await getWechatMpBindStatus(bindScene.value);
|
||||
const status = Number(st?.data?.status ?? 0);
|
||||
if (status === 1) {
|
||||
bindStep.value = 2; // 已扫码,公众号已回复验证码,等待输入
|
||||
}
|
||||
if (status === 3) {
|
||||
stopBindPoll();
|
||||
bindQrUrl.value = "";
|
||||
ElMessage.warning("二维码已过期,请重新获取");
|
||||
}
|
||||
} catch {
|
||||
// 轮询失败忽略
|
||||
}
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "获取二维码失败");
|
||||
} finally {
|
||||
bindStarting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmBind = async () => {
|
||||
if (!bindScene.value) {
|
||||
ElMessage.warning("请先获取绑定二维码");
|
||||
return;
|
||||
}
|
||||
if (!bindCode.value.trim()) {
|
||||
ElMessage.warning("请输入公众号回复的验证码");
|
||||
return;
|
||||
}
|
||||
bindConfirming.value = true;
|
||||
try {
|
||||
const res = await confirmWechatMpBind({ scene: bindScene.value, code: bindCode.value.trim() });
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("绑定成功");
|
||||
stopBindPoll();
|
||||
bindQrUrl.value = "";
|
||||
bindCode.value = "";
|
||||
bindScene.value = "";
|
||||
bindStep.value = 0;
|
||||
await loadWechatBinding();
|
||||
await loadFollowers();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "绑定失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "绑定失败");
|
||||
} finally {
|
||||
bindConfirming.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const unbindMyWechat = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm("确定解除当前账号的微信绑定吗?解除后将无法接收微信推送。", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await unbindWechatMp();
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("已解除绑定");
|
||||
await loadWechatBinding();
|
||||
await loadFollowers();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "解绑失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "解绑失败");
|
||||
}
|
||||
};
|
||||
|
||||
const sendWechatTestPush = async () => {
|
||||
wechatPushTesting.value = true;
|
||||
try {
|
||||
const res = await pushWechatMpTest({});
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg || "测试消息已发送");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "发送失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "发送失败");
|
||||
} finally {
|
||||
wechatPushTesting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadFollowers = async () => {
|
||||
followersLoading.value = true;
|
||||
try {
|
||||
const res = await getWechatMpFollowers({
|
||||
page: followerQuery.page,
|
||||
pageSize: followerQuery.pageSize,
|
||||
keyword: followerQuery.keyword,
|
||||
bind_type: followerQuery.bind_type,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
followers.value = res.data?.list || [];
|
||||
followerQuery.total = Number(res.data?.total || 0);
|
||||
} else {
|
||||
followers.value = [];
|
||||
}
|
||||
} catch {
|
||||
followers.value = [];
|
||||
} finally {
|
||||
followersLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleFollowerPage = (page) => {
|
||||
followerQuery.page = page;
|
||||
loadFollowers();
|
||||
};
|
||||
|
||||
const editFollowerRemark = async (row) => {
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt("设置备注(仅平台可见)", "粉丝备注", {
|
||||
inputValue: row.remark || "",
|
||||
inputPlaceholder: "如:张三(XX公司)",
|
||||
inputValidator: (v) => (v || "").length <= 100 || "备注过长",
|
||||
});
|
||||
const res = await setWechatMpFollowerRemark({ id: row.id, remark: value || "" });
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("已保存");
|
||||
await loadFollowers();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "保存失败");
|
||||
}
|
||||
} catch {
|
||||
// 取消
|
||||
}
|
||||
};
|
||||
|
||||
const unbindFollower = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定解除「${row.bind_name || row.openid}」的绑定吗?(粉丝记录保留)`,
|
||||
"提示",
|
||||
{ type: "warning" }
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await unbindWechatMpFollower({ id: row.id });
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("已解除绑定");
|
||||
await loadFollowers();
|
||||
} else {
|
||||
ElMessage.error(res.msg || "解绑失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "解绑失败");
|
||||
}
|
||||
};
|
||||
|
||||
const sendPush = async () => {
|
||||
if (!pushForm.title && !pushForm.content) {
|
||||
ElMessage.warning("请填写推送标题或内容");
|
||||
return;
|
||||
}
|
||||
pushSending.value = true;
|
||||
try {
|
||||
const res = await pushWechatMpMessage({ ...pushForm });
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg || "推送完成");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "推送失败");
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e?.message || "推送失败");
|
||||
} finally {
|
||||
pushSending.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadDraft();
|
||||
loadSmsConfig();
|
||||
loadEmailConfig();
|
||||
loadSiteReminderConfig();
|
||||
loadBarkConfig();
|
||||
loadWechatConfig();
|
||||
loadWechatBinding();
|
||||
});
|
||||
|
||||
// 切换到「微信配置」页时刷新配置 / 绑定 / 粉丝
|
||||
watch(activeSubTab, (tab) => {
|
||||
if (tab === "wechat") {
|
||||
loadWechatConfig();
|
||||
loadWechatBinding();
|
||||
loadFollowers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -846,4 +1388,88 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
/* ===== 微信公众号(服务号) ===== */
|
||||
.bind-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.bind-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bind-steps {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.bind-area {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bind-qr {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qr-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.bind-form {
|
||||
flex: 1;
|
||||
min-width: 240px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bind-input {
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.follower-tools {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.follower-select {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.follower-pager {
|
||||
margin-top: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.push-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.push-url {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ml8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user