优化小程序部分
This commit is contained in:
1 parent
727319a6eb
commit
d4e89788c5
46 files changed
+4906
-63
No files matched your search
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user