推送功能添加
deploy-userbase-front / build-and-deploy (push) Successful in 2m50s

This commit is contained in:
chenxing
2026-08-14 09:08:01 +08:00
parent 4077ef64d8
commit 85619785f6
92 changed files with 862 additions and 15442 deletions
+13
View File
@@ -35,6 +35,9 @@
<!-- 排序管理 -->
<AppSort v-model="sortModalVisible" :categoryId="dataParams.categoryId" :categoryName="currentCategoryName"
@saved="onSortSaved" />
<!-- 推送设置 -->
<PushSettingModal v-model="pushSettingVisible" :appId="currentApp.id" :appName="currentApp.name" />
</div>
</template>
@@ -46,6 +49,7 @@ import useCurrentInstance from "@/utils/hook/useCurrentInstance";
import MyConfirm from "@/components/MyConfirm";
import AddAppContainer from "./AddAppContainer.vue";
import EditAppContainer from "./EditAppContainer.vue";
import PushSettingModal from "./PushSettingModal.vue";
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import { OperatorEnum } from '@/types';
import AppSort from './AppSort.vue';
@@ -56,6 +60,8 @@ const { $utils } = useCurrentInstance();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const sortModalVisible = ref(false);
const pushSettingVisible = ref(false);
const currentApp = ref<{ id: number; name: string }>({ id: 0, name: "" });
const keyword = ref("");
const dataParams = ref<{
@@ -150,6 +156,7 @@ const columns = [
return [
$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row)),
$utils.operatingTextHandle(h, '模块管理', '', () => goModuleManage(row)),
$utils.operatingTextHandle(h, '推送设置', '', () => openPushSetting(row)),
$utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)),
];
}
@@ -210,6 +217,12 @@ async function handleDelete(row: any) {
});
}
/* ---------------- 推送设置 ---------------- */
function openPushSetting(row: any) {
currentApp.value = { id: row.id, name: row.name };
pushSettingVisible.value = true;
}
/* ---------------- 导航 ---------------- */
const goAppType = () => {
router.push("/app-manage/category");
+215
View File
@@ -0,0 +1,215 @@
<template>
<Modal v-model="show" :mask-closable="false" width="680" title="推送设置" @on-visible-change="onVisibleChange">
<div v-if="loading" class="loading-tip">加载中...</div>
<Form v-else :label-width="110" class="push-setting-form">
<FormItem label="应用">
<span>{{ appName || '-' }}</span>
</FormItem>
<FormItem label="推送总开关">
<i-switch v-model="form.pushEnabled" />
<span class="switch-tip">{{ form.pushEnabled ? '开启' : '关闭' }}</span>
</FormItem>
<FormItem label="推送地址" required>
<Input v-model="form.pushBaseUrl" placeholder="如 http://host/notice/api/base/(不含接口后缀)" />
</FormItem>
<FormItem label="订阅接口" required>
<Select v-model="form.pushEventTypes" multiple filterable placeholder="请选择要订阅的接口(必选)">
<Option v-for="opt in interfaceOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
</FormItem>
<FormItem label="关联学校">
<Select v-model="form.pushSchoolIds" multiple filterable placeholder="留空=全局(监听所有学校变化)">
<Option v-for="opt in schoolOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
</FormItem>
<FormItem label="推送角色">
<Select v-model="form.pushRoleIds" multiple filterable placeholder="留空=全部角色">
<Option v-for="opt in roleOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
</FormItem>
<FormItem label="签名密钥">
<Input v-model="form.pushSecret" type="password" password placeholder="留空=复用应用密钥 appSecret;清空则改为复用" />
<p class="field-tip">不修改请保持原值;清空输入框表示改用应用密钥。</p>
</FormItem>
</Form>
<template #footer>
<Button @click="show = false">取消</Button>
<Button type="primary" :loading="saving" @click="handleSave">保存</Button>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { Message } from 'view-ui-plus';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
const props = defineProps<{
modelValue: boolean;
appId: number;
appName?: string;
}>();
const emit = defineEmits(['update:modelValue', 'saved']);
const { $utils } = useCurrentInstance();
const show = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
});
const interfaceOptions = [
{ value: 'group/SyncGroup', label: '组全量同步' },
{ value: 'user/SyncUser', label: '用户全量同步' },
{ value: 'group/AddGroup', label: '添加组' },
{ value: 'group/UpdateGroup', label: '更新组' },
{ value: 'group/DeleteGroup', label: '删除组' },
{ value: 'user/AddUser', label: '添加用户' },
{ value: 'user/UpdateUser', label: '更新用户' },
{ value: 'user/DeleteUser', label: '删除用户' },
];
const roleOptions = [
{ value: 1, label: '老师' },
{ value: 2, label: '校领导' },
{ value: 3, label: '学生' },
{ value: 4, label: '家长' },
{ value: 5, label: '局领导' },
{ value: 6, label: '工作号' },
{ value: 7, label: '管理员' },
];
const schoolOptions = ref<{ value: number; label: string }[]>([]);
const loading = ref(false);
const saving = ref(false);
const form = ref({
pushEnabled: false,
pushBaseUrl: '',
pushEventTypes: [] as string[],
pushSchoolIds: [] as number[],
pushRoleIds: [] as number[],
pushSecret: '',
});
const originalSecret = ref<string>('');
function flattenSchools(nodes: any[], prefix = ''): { value: number; label: string }[] {
const out: { value: number; label: string }[] = [];
(nodes || []).forEach((n) => {
const label = prefix ? `${prefix} / ${n.name}` : n.name;
out.push({ value: Number(n.id), label });
if (Array.isArray(n.children) && n.children.length) {
out.push(...flattenSchools(n.children, label));
}
});
return out;
}
async function loadSchools() {
try {
const data = await (Apis as any).SysGroup.getSchoolTree({ cacheFor: null });
schoolOptions.value = flattenSchools(Array.isArray(data) ? data : []);
} catch (e: any) {
schoolOptions.value = [];
}
}
async function loadSettings() {
loading.value = true;
try {
const data = await (Apis as any).AppSettings.getPushSetting({
pathParams: { id: props.appId },
cacheFor: null,
});
const d = data || {};
form.value.pushEnabled = d.pushEnabled === 1;
form.value.pushBaseUrl = d.pushBaseUrl || '';
form.value.pushEventTypes = d.pushEventTypes ? String(d.pushEventTypes).split(',').filter(Boolean) : [];
form.value.pushSchoolIds = d.pushSchoolIds
? String(d.pushSchoolIds).split(',').filter(Boolean).map(Number)
: [];
form.value.pushRoleIds = d.pushRoleIds
? String(d.pushRoleIds).split(',').filter(Boolean).map(Number)
: [];
form.value.pushSecret = d.pushSecret || '';
originalSecret.value = d.pushSecret || '';
} catch (e: any) {
Message.error(e?.message || '加载推送设置失败');
} finally {
loading.value = false;
}
}
watch(
() => props.modelValue,
(val) => {
if (val && props.appId) {
loadSchools();
loadSettings();
}
}
);
function onVisibleChange(val: boolean) {
if (!val) emit('update:modelValue', false);
}
function handleSave() {
if (!form.value.pushBaseUrl.trim()) {
Message.error('请填写推送地址');
return;
}
if (!form.value.pushEventTypes.length) {
Message.error('请至少选择一个订阅接口');
return;
}
saving.value = true;
const data: any = {
pushBaseUrl: form.value.pushBaseUrl.trim(),
pushEnabled: form.value.pushEnabled ? 1 : 0,
pushEventTypes: form.value.pushEventTypes.join(','),
pushSchoolIds: form.value.pushSchoolIds.length ? form.value.pushSchoolIds.join(',') : null,
pushRoleIds: form.value.pushRoleIds.length ? form.value.pushRoleIds.join(',') : null,
// 与原值相同=不修改(传 null 保留);清空=复用应用密钥(传 "")
pushSecret: form.value.pushSecret === originalSecret.value ? null : form.value.pushSecret,
};
(Apis as any)
.AppSettings.savePushSetting({
pathParams: { id: props.appId },
data,
cacheFor: null,
})
.then(() => {
Message.success('保存成功');
show.value = false;
emit('saved');
})
.catch((e: any) => Message.error(e?.message || '保存失败'))
.finally(() => (saving.value = false));
}
</script>
<style lang="scss" scoped>
.push-setting-form {
:deep(.ivu-form-item) {
margin-bottom: 18px;
}
.switch-tip {
margin-left: 8px;
color: #808695;
}
.field-tip {
margin: 4px 0 0;
color: #999;
font-size: 12px;
}
}
.loading-tip {
padding: 24px 0;
text-align: center;
color: #999;
}
</style>
+3 -3
View File
@@ -16,9 +16,9 @@
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight">
<template #default="{ checkList }">
<Button type="primary" ghost @click="openAdjust(checkList)">调整组</Button>
<Button type="warning" class="ml-15" :disabled="!checkList?.length" @click="openBatchStatus(checkList)">设置状态</Button>
<Button type="error" class="ml-15" :disabled="!checkList?.length" @click="openBatchDelete(checkList)">批量删除</Button>
<Button type="primary" ghost :disabled="!checkList?.length" @click="openAdjust(checkList)">调整组</Button>
<Button type="warning" ghost class="ml-15" :disabled="!checkList?.length" @click="openBatchStatus(checkList)">设置状态</Button>
<Button type="error" ghost class="ml-15" :disabled="!checkList?.length" @click="openBatchDelete(checkList)">批量删除</Button>
</template>
</TablePageAlovaNew>
+227
View File
@@ -0,0 +1,227 @@
<template>
<div class="main-wrapper">
<div class="main-container">
<div class="search-wrap fix">
<Select v-model="dataParams.appKey" class="ml-12" clearable filterable placeholder="应用" style="width: 180px"
@on-change="handleSearch">
<Option v-for="opt in appOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
<Select v-model="dataParams.interfacePath" class="ml-12" clearable placeholder="接口" style="width: 160px"
@on-change="handleSearch">
<Option v-for="opt in interfaceOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
<Select v-model="dataParams.success" class="ml-12" clearable placeholder="调用结果" style="width: 120px"
@on-change="handleSearch">
<Option :value="1">成功</Option>
<Option :value="0">失败</Option>
</Select>
<DatePicker v-model="startDate" type="date" placeholder="开始时间" format="yyyy-MM-dd" :editable="false"
:clearable="true" class="ml-12" style="width: 140px" @on-change="v => onStartChange(v)" />
<DatePicker v-model="endDate" type="date" placeholder="结束时间" format="yyyy-MM-dd" :editable="false"
:clearable="true" class="ml-12" style="width: 140px" @on-change="v => onEndChange(v)" />
<Button type="primary" class="ml-12" @click="handleSearch">查询</Button>
</div>
<TablePageAlovaNew ref="tableRef" :dataApi="tableApi" :columns="columns" :tableHeight="tableHeight" />
</div>
<PushLogDetailModal v-model="detailVisible" :row="currentRow" />
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { Message } from 'view-ui-plus';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
import TablePageAlovaNew from '@/components/TablePageAlovaNew.vue';
import Utils from '@/utils';
import PushLogDetailModal from './PushLogDetailModal.vue';
defineOptions({ name: 'PushLog' });
const { $utils } = useCurrentInstance();
const tableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const interfaceOptions = [
{ value: 'group/SyncGroup', label: '组全量同步' },
{ value: 'user/SyncUser', label: '用户全量同步' },
{ value: 'group/AddGroup', label: '添加组' },
{ value: 'group/UpdateGroup', label: '更新组' },
{ value: 'group/DeleteGroup', label: '删除组' },
{ value: 'user/AddUser', label: '添加用户' },
{ value: 'user/UpdateUser', label: '更新用户' },
{ value: 'user/DeleteUser', label: '删除用户' },
];
const appOptions = ref<{ value: number; label: string }[]>([]);
interface DataParams {
appKey?: number;
interfacePath?: string;
success?: number;
startTime?: string;
endTime?: string;
}
const dataParams = ref<DataParams>({
appKey: undefined,
interfacePath: undefined,
success: undefined,
startTime: undefined,
endTime: undefined,
});
const startDate = ref<Date | null>(null);
const endDate = ref<Date | null>(null);
function onStartChange(val: string) {
dataParams.value.startTime = val ? `${val} 00:00:00` : undefined;
handleSearch();
}
function onEndChange(val: string) {
dataParams.value.endTime = val ? `${val} 23:59:59` : undefined;
handleSearch();
}
function handleSearch() {
tableRef.value?.refreshData();
}
function clean(obj: Record<string, any>) {
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== ''));
}
const tableApi = (params: { pageIndex: number; pageSize: number }) => {
return (Apis as any).Push.recordList({
params: clean({
appKey: dataParams.value.appKey,
interfacePath: dataParams.value.interfacePath,
success: dataParams.value.success,
startTime: dataParams.value.startTime,
endTime: dataParams.value.endTime,
pageIndex: params.pageIndex,
pageSize: params.pageSize,
}),
cacheFor: null,
});
};
const tableHeight = ref($utils.useTableHeight({ isOperat: false, isPage: true, isSearch: true }));
const columns = [
{ type: 'index', title: '序号', width: 70, align: 'center' },
{
title: '日志时间',
key: 'createdAt',
width: 170,
render: (h: any, { row }: any) =>
h('span', row.createdAt ? Utils.dateFormat(row.createdAt, 'yyyy-MM-dd HH:mm:ss') : '-'),
},
{ title: '应用', key: 'appName', width: 160, ellipsis: true, tooltip: true },
{
title: '接口',
key: 'interfaceLabel',
minWidth: 180,
render: (h: any, { row }: any) =>
h('span', { attrs: { title: row.interfacePath } }, row.interfaceLabel || row.interfacePath || '-'),
},
{
title: '调用结果',
key: 'success',
width: 90,
align: 'center',
render: (h: any, { row }: any) =>
h('span', { class: row.success === 1 ? 'text-success' : 'text-error' }, row.success === 1 ? '成功' : '失败'),
},
{
title: '重试',
key: 'attemptNo',
width: 80,
align: 'center',
render: (h: any, { row }: any) => h('span', `第${row.attemptNo || 1}次`),
},
{
title: '耗时(ms)',
key: 'costMs',
width: 100,
align: 'right',
render: (h: any, { row }: any) => h('span', row.costMs != null ? `${row.costMs}` : '-'),
},
{
title: '操作',
key: 'action',
width: 130,
align: 'center',
fixed: 'right' as const,
render: (h: any, { row }: any) => {
const actions = [
h('a', { style: { color: '#577afb', marginRight: '12px' }, on: { click: () => openDetail(row) } }, '详情'),
];
if (row.success !== 1) {
actions.push(
h('a', { style: { color: '#ed4014' }, on: { click: () => handleRetry(row) } }, '重试')
);
}
return h('span', actions);
},
},
];
const detailVisible = ref(false);
const currentRow = ref<Record<string, any> | null>(null);
function openDetail(row: any) {
currentRow.value = row;
detailVisible.value = true;
}
async function handleRetry(row: any) {
try {
await (Apis as any).Push.retry({ pathParams: { id: row.id }, cacheFor: null });
Message.success('已重新投递,稍后在列表中查看结果');
tableRef.value?.refreshData();
} catch (e: any) {
Message.error(e?.message || '重试失败');
}
}
async function loadApps() {
try {
const res = await (Apis as any).AppSettings.appList({
params: { pageIndex: 1, pageSize: 9999 },
cacheFor: null,
});
const list = (res && res.content) || [];
appOptions.value = list.map((a: any) => ({ value: a.appKey, label: `${a.name}(${a.appKey})` }));
} catch (e: any) {
appOptions.value = [];
}
}
loadApps();
</script>
<style lang="scss" scoped>
.search-wrap {
.ivu-select,
.ivu-input-wrapper,
.ivu-date-picker {
vertical-align: middle;
}
}
.text-success {
color: #19be6b;
}
.text-error {
color: #ed4014;
}
</style>
+144
View File
@@ -0,0 +1,144 @@
<template>
<Modal v-model="show" :mask-closable="false" width="720" title="推送日志详情" @on-visible-change="onVisibleChange">
<Form :label-width="110" class="push-log-detail-form">
<FormItem label="日志ID:">
<span>{{ row?.id ?? '-' }}</span>
</FormItem>
<FormItem label="应用:">
<span>{{ row?.appName ?? '-' }}</span>
<span class="sub-text">(appKey {{ row?.subscriberAppKey ?? '-' }})</span>
</FormItem>
<FormItem label="接口:">
<span>{{ row?.interfaceLabel ?? '-' }}</span>
<span class="sub-text">{{ row?.interfacePath ?? '' }}</span>
</FormItem>
<FormItem label="请求地址:">
<span class="break-all">{{ row?.requestUrl || '-' }}</span>
</FormItem>
<FormItem label="调用结果:">
<span :class="resultClass">{{ resultText }}</span>
</FormItem>
<FormItem label="触发类型:">
<span>{{ triggerText }}</span>
</FormItem>
<FormItem label="HTTP状态:">
<span :class="row?.httpStatus === 200 ? 'text-success' : 'text-error'">{{ row?.httpStatus ?? '-' }}</span>
</FormItem>
<FormItem label="业务码:">
<span>{{ row?.respCode ?? '-' }}</span>
</FormItem>
<FormItem label="业务消息:">
<span class="break-all">{{ row?.respMsg || '-' }}</span>
</FormItem>
<FormItem label="重试次数:">
<span>第 {{ row?.attemptNo ?? 1 }} 次</span>
</FormItem>
<FormItem label="耗时:">
<span>{{ row?.costMs != null ? row.costMs + ' ms' : '-' }}</span>
</FormItem>
<FormItem label="签名时间 t:">
<span>{{ row?.signT || '-' }}</span>
</FormItem>
<FormItem label="创建时间:">
<span>{{ formatTime(row?.createdAt) }}</span>
</FormItem>
<FormItem label="请求体:">
<pre class="json-pre">{{ formatJson(row?.payload) }}</pre>
</FormItem>
</Form>
<template #footer>
<Button type="primary" @click="show = false">确定</Button>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import Utils from '@/utils';
interface Props {
modelValue: boolean;
row?: Record<string, any> | null;
}
const props = withDefaults(defineProps<Props>(), { row: null });
const emit = defineEmits(['update:modelValue']);
const show = computed({
get: () => props.modelValue,
set: val => emit('update:modelValue', val),
});
const resultText = computed(() => (props.row?.success === 1 ? '成功' : '失败'));
const resultClass = computed(() => (props.row?.success === 1 ? 'text-success' : 'text-error'));
const triggerMap: Record<string, string> = {
manual: '手动测试',
auto: '自动触发',
retry: '失败重试',
};
const triggerText = computed(() => (props.row?.triggerType ? (triggerMap[props.row.triggerType] || props.row.triggerType) : '-'));
function onVisibleChange(val: boolean) {
if (!val) emit('update:modelValue', false);
}
function formatJson(val: any): string {
if (!val) return '-';
try {
return JSON.stringify(JSON.parse(val), null, 2);
} catch {
return String(val);
}
}
function formatTime(val: any): string {
if (!val) return '-';
return Utils.dateFormat(val, 'yyyy-MM-dd HH:mm:ss');
}
</script>
<style lang="scss" scoped>
.push-log-detail-form {
:deep(.ivu-form-item) {
margin-bottom: 14px;
}
:deep(.ivu-form-item-label) {
color: #515a6e;
font-weight: 500;
}
.text-success {
color: #19be6b;
}
.text-error {
color: #ed4014;
}
.sub-text {
margin-left: 8px;
color: #999;
font-size: 12px;
}
.break-all {
word-break: break-all;
}
.json-pre {
margin: 0;
padding: 10px 12px;
background: #f7f8fa;
border: 1px solid #e8eaec;
border-radius: 4px;
max-height: 240px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
}
}
</style>
+242
View File
@@ -0,0 +1,242 @@
<template>
<div class="main-wrapper">
<div class="main-container">
<div class="test-card">
<h3 class="card-title">测试推送</h3>
<p class="card-desc">
手动触发一次推送(投递消息队列,由订阅方异步接收)。下方将展示本次请求预览(URL / 头 / 体),
实际响应结果与耗时请在「推送日志」中查看。
</p>
<Form :label-width="100" class="test-form">
<FormItem label="订阅方应用" required>
<Select v-model="form.appId" filterable placeholder="选择要推送的订阅方应用" style="width: 360px">
<Option v-for="opt in appOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
</FormItem>
<FormItem label="订阅接口" required>
<Select v-model="form.interfacePath" filterable placeholder="选择接口" style="width: 360px"
@on-change="onInterfaceChange">
<Option v-for="opt in interfaceOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</Option>
</Select>
</FormItem>
<FormItem label="目标ID" :required="!isSync">
<Input v-model="targetIdsText" :disabled="isSync" :placeholder="isSync ? '全量同步无需指定' : '可选,逗号分隔,如 10,11,12'"
style="width: 360px" />
</FormItem>
<FormItem>
<Button type="primary" :loading="sending" @click="handleSend">发送测试推送</Button>
</FormItem>
</Form>
</div>
<div v-if="preview" class="result-card">
<div class="result-head">
<h3 class="card-title">请求预览</h3>
<Button type="text" @click="goLog">查看推送日志</Button>
</div>
<Alert v-if="preview.batch && preview.batch > 1" type="info" show-icon>
本次将拆分为 {{ preview.batch }} 条消息依次发送
</Alert>
<Form :label-width="100" class="result-form">
<FormItem label="请求URL:">
<span class="break-all">{{ preview.url }}</span>
</FormItem>
<FormItem label="请求方法:">
<span>{{ preview.method }}</span>
</FormItem>
<FormItem label="请求头:">
<pre class="kv-pre">{{ formatHeaders(preview.headers) }}</pre>
</FormItem>
<FormItem label="请求体:">
<pre class="json-pre">{{ formatJson(preview.body) }}</pre>
</FormItem>
</Form>
<Alert type="success" show-icon>{{ sendMessage }}</Alert>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { Message } from 'view-ui-plus';
import useCurrentInstance from '@/utils/hook/useCurrentInstance';
defineOptions({ name: 'PushTest' });
const router = useRouter();
const { $utils } = useCurrentInstance();
const interfaceOptions = [
{ value: 'group/SyncGroup', label: '组全量同步', sync: true },
{ value: 'user/SyncUser', label: '用户全量同步', sync: true },
{ value: 'group/AddGroup', label: '添加组', sync: false },
{ value: 'group/UpdateGroup', label: '更新组', sync: false },
{ value: 'group/DeleteGroup', label: '删除组', sync: false },
{ value: 'user/AddUser', label: '添加用户', sync: false },
{ value: 'user/UpdateUser', label: '更新用户', sync: false },
{ value: 'user/DeleteUser', label: '删除用户', sync: false },
];
const appOptions = ref<{ value: number; label: string }[]>([]);
const form = ref({ appId: undefined as number | undefined, interfacePath: '' });
const targetIdsText = ref('');
const sending = ref(false);
const preview = ref<Record<string, any> | null>(null);
const sendMessage = ref('');
const isSync = computed(() => {
const opt = interfaceOptions.find((o) => o.value === form.value.interfacePath);
return !!opt?.sync;
});
function onInterfaceChange() {
if (isSync.value) targetIdsText.value = '';
}
function goLog() {
router.push('/push-log');
}
function formatHeaders(headers: Record<string, string> | undefined): string {
if (!headers) return '-';
return Object.entries(headers)
.map(([k, v]) => `${k}: ${v}`)
.join('\n');
}
function formatJson(val: any): string {
if (val == null) return '-';
if (typeof val === 'string') {
try {
return JSON.stringify(JSON.parse(val), null, 2);
} catch {
return val;
}
}
return JSON.stringify(val, null, 2);
}
function parseTargetIds(): number[] | null {
if (isSync.value) return null;
const txt = targetIdsText.value.trim();
if (!txt) return null;
const ids = txt
.split(/[,,\s]+/)
.map((s) => Number(s.trim()))
.filter((n) => Number.isFinite(n));
return ids.length ? ids : null;
}
function handleSend() {
if (form.value.appId == null) {
Message.error('请选择订阅方应用');
return;
}
if (!form.value.interfacePath) {
Message.error('请选择订阅接口');
return;
}
sending.value = true;
preview.value = null;
sendMessage.value = '';
(Apis as any)
.Push.test({
data: {
appId: form.value.appId,
interfacePath: form.value.interfacePath,
targetIds: parseTargetIds(),
},
cacheFor: null,
})
.then((res: any) => {
preview.value = res?.preview || null;
sendMessage.value = res?.message || '已发送';
})
.catch((e: any) => Message.error(e?.message || '发送失败'))
.finally(() => (sending.value = false));
}
async function loadApps() {
try {
const res = await (Apis as any).AppSettings.appList({
params: { pageIndex: 1, pageSize: 9999 },
cacheFor: null,
});
const list = (res && res.content) || [];
appOptions.value = list.map((a: any) => ({ value: a.id, label: `${a.name}(${a.appKey})` }));
} catch (e: any) {
appOptions.value = [];
}
}
loadApps();
</script>
<style lang="scss" scoped>
.main-wrapper {
height: 100%;
}
.main-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.test-card,
.result-card {
background: #fff;
border-radius: 4px;
padding: 16px 20px;
}
.card-title {
margin: 0 0 6px;
font-size: 15px;
font-weight: 600;
}
.card-desc {
margin: 0 0 12px;
color: #808695;
font-size: 13px;
line-height: 1.6;
}
.test-form {
margin-top: 4px;
}
.result-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.break-all {
word-break: break-all;
}
.kv-pre,
.json-pre {
margin: 0;
padding: 10px 12px;
background: #f7f8fa;
border: 1px solid #e8eaec;
border-radius: 4px;
max-height: 280px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
font-size: 13px;
line-height: 1.6;
}
.kv-pre {
max-height: 140px;
}
</style>