feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互
This commit is contained in:
@@ -18,12 +18,17 @@ import { getOaChatListByPlatform } from '#/views/system/oa-chat/api';
|
||||
import { getOaPlatformList } from '#/views/system/oa-platform/api';
|
||||
import {
|
||||
createOaRobot,
|
||||
getOaRobotGroupedByPlatform,
|
||||
getOaRobotInfo,
|
||||
syncOaRobotChatMembers,
|
||||
testSendOaRobot,
|
||||
updateOaRobot,
|
||||
} from '#/views/system/oa-robot/api';
|
||||
import {
|
||||
createFormSchema,
|
||||
editFormSchema,
|
||||
isUsePlatformCredential,
|
||||
isWorkWechatApp,
|
||||
modalFormProps,
|
||||
} from '#/views/system/oa-robot/config/form';
|
||||
|
||||
@@ -35,6 +40,7 @@ import {
|
||||
* (禁止事后 updateSchema,避免与 setState(schema) 竞态导致所属平台选项为空)
|
||||
* - 用 update 标志区分新增/编辑:新增走 createFormSchema(含 webhook/secret),编辑走 editFormSchema
|
||||
* - 「绑定群聊」复选框组:按当前选中的 platform_code 过滤,仅展示该平台下的群聊
|
||||
* - 应用 API:凭证来源 Radio(平台默认 / 自定义)+ 可选「克隆机器人」回填
|
||||
* - 弹窗内有「测试发送」按钮,循环跑该平台支持的所有消息类型
|
||||
*/
|
||||
|
||||
@@ -66,6 +72,19 @@ const chatsByPlatform = ref<Record<string, ChatOption[]>>({});
|
||||
const selectedChatIds = ref<number[]>([]);
|
||||
/** 当前选中的平台编码(控制群聊列表过滤) */
|
||||
const currentPlatformCode = ref('');
|
||||
/** 按平台分组的机器人列表(克隆源下拉用) */
|
||||
const robotsByPlatform = ref<Record<string, { id: number; name: string }[]>>(
|
||||
{},
|
||||
);
|
||||
/** 同步群成员 loading */
|
||||
const syncingMembers = ref(false);
|
||||
/** 最近一次同步的成员数量提示 */
|
||||
const memberSyncHint = ref('');
|
||||
|
||||
/** 应用 API 平台不展示 N:N 群聊绑定 */
|
||||
const showChatBind = computed(
|
||||
() => currentPlatformCode.value !== '' && !isWorkWechatApp(currentPlatformCode.value),
|
||||
);
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
@@ -111,9 +130,16 @@ const [Modal, modalApi] = useVbenModal({
|
||||
selectedChatIds.value = Array.isArray(values.chat_ids)
|
||||
? values.chat_ids.map((id: any) => Number(id))
|
||||
: [];
|
||||
const memberCount = Array.isArray(values.members)
|
||||
? values.members.length
|
||||
: 0;
|
||||
memberSyncHint.value = isWorkWechatApp(currentPlatformCode.value)
|
||||
? `已缓存群成员 ${memberCount} 人`
|
||||
: '';
|
||||
} else {
|
||||
currentPlatformCode.value = '';
|
||||
selectedChatIds.value = [];
|
||||
memberSyncHint.value = '';
|
||||
}
|
||||
});
|
||||
loadChatsByPlatform();
|
||||
@@ -140,6 +166,89 @@ function handlePlatformChange(value: string) {
|
||||
selectedChatIds.value = selectedChatIds.value.filter((id) =>
|
||||
allowedIds.has(id),
|
||||
);
|
||||
if (isWorkWechatApp(value)) {
|
||||
selectedChatIds.value = [];
|
||||
// 切换到应用 API 时刷新克隆源下拉(按平台过滤)
|
||||
refreshCloneOptions(value);
|
||||
} else {
|
||||
formApi.setValues({ clone_from_id: undefined });
|
||||
}
|
||||
memberSyncHint.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择克隆源:拉详情回填凭证策略与 chat_id(不覆盖用户已改的名称,除非名称为空)
|
||||
*/
|
||||
async function handleCloneFromChange(cloneId: number | undefined) {
|
||||
if (!cloneId) return;
|
||||
try {
|
||||
const detail = await getOaRobotInfo(Number(cloneId));
|
||||
if (!detail) return;
|
||||
const current = await formApi.getValues();
|
||||
formApi.setValues({
|
||||
platform_code: detail.platform_code,
|
||||
use_platform_credential: Number(detail.use_platform_credential ?? 1),
|
||||
corp_id: detail.corp_id || '',
|
||||
secret: detail.secret || '',
|
||||
agent_id: Number(detail.agent_id || 0) || undefined,
|
||||
chat_id: detail.chat_id || '',
|
||||
remark: detail.remark || '',
|
||||
// 名称:空则用「源名+副本」,已有内容不强制覆盖
|
||||
name:
|
||||
current.name && String(current.name).trim() !== ''
|
||||
? current.name
|
||||
: `${detail.name || '机器人'}副本`,
|
||||
clone_from_id: Number(cloneId),
|
||||
});
|
||||
currentPlatformCode.value = String(detail.platform_code ?? '');
|
||||
} catch (e) {
|
||||
console.error('加载克隆源失败', e);
|
||||
message.error('加载克隆源失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前平台刷新「克隆机器人」下拉 options
|
||||
*/
|
||||
function refreshCloneOptions(platformCode: string) {
|
||||
const options = (robotsByPlatform.value[platformCode] ?? []).map((r) => ({
|
||||
label: r.name,
|
||||
value: r.id,
|
||||
}));
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'clone_from_id',
|
||||
componentProps: {
|
||||
options,
|
||||
placeholder: '可选,选择后回填凭证与群聊(不含强制改名)',
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
onChange: (val: number | undefined) => {
|
||||
handleCloneFromChange(val);
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑态手动同步群成员(需已保存凭证)
|
||||
*/
|
||||
async function handleSyncMembers() {
|
||||
const values = await formApi.getValues();
|
||||
if (!values.id) {
|
||||
message.warning('请先保存机器人后再同步成员');
|
||||
return;
|
||||
}
|
||||
syncingMembers.value = true;
|
||||
try {
|
||||
const data = await syncOaRobotChatMembers({ id: Number(values.id) });
|
||||
memberSyncHint.value = `已同步 ${data?.member_count ?? 0} 人${data?.chat_name ? `(${data.chat_name})` : ''}`;
|
||||
message.success(memberSyncHint.value);
|
||||
} finally {
|
||||
syncingMembers.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,26 +272,48 @@ async function applySchemaWithPlatforms(isEdit: boolean) {
|
||||
} catch (e) {
|
||||
console.error('加载平台列表失败', e);
|
||||
}
|
||||
// 新增态预拉克隆源列表(按平台分组)
|
||||
if (!isEdit) {
|
||||
try {
|
||||
const grouped = await getOaRobotGroupedByPlatform();
|
||||
robotsByPlatform.value = grouped ?? {};
|
||||
} catch (e) {
|
||||
console.error('加载机器人分组失败', e);
|
||||
robotsByPlatform.value = {};
|
||||
}
|
||||
}
|
||||
const base = isEdit ? editFormSchema : createFormSchema;
|
||||
// 深拷贝字段配置,避免污染导出的静态 schema;把 options/onChange 写进 platform_code
|
||||
const schema = base.map((field: any) => {
|
||||
if (field.fieldName !== 'platform_code') {
|
||||
return { ...field };
|
||||
}
|
||||
return {
|
||||
...field,
|
||||
componentProps: {
|
||||
...(field.componentProps || {}),
|
||||
options,
|
||||
// RadioGroup 的 change 事件(antd 原生):e.target.value 取选中值
|
||||
onChange: (e: any) => {
|
||||
const next = e?.target?.value ?? '';
|
||||
if (typeof next === 'string' && next !== '') {
|
||||
handlePlatformChange(next);
|
||||
}
|
||||
if (field.fieldName === 'platform_code') {
|
||||
return {
|
||||
...field,
|
||||
componentProps: {
|
||||
...(field.componentProps || {}),
|
||||
options,
|
||||
// RadioGroup 的 change 事件(antd 原生):e.target.value 取选中值
|
||||
onChange: (e: any) => {
|
||||
const next = e?.target?.value ?? '';
|
||||
if (typeof next === 'string' && next !== '') {
|
||||
handlePlatformChange(next);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
if (field.fieldName === 'clone_from_id') {
|
||||
return {
|
||||
...field,
|
||||
componentProps: {
|
||||
...(field.componentProps || {}),
|
||||
options: [],
|
||||
onChange: (val: number | undefined) => {
|
||||
handleCloneFromChange(val);
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return { ...field };
|
||||
});
|
||||
formApi.setState({ schema });
|
||||
}
|
||||
@@ -216,33 +347,55 @@ async function handleTestSend() {
|
||||
message.warning('请先选择平台');
|
||||
return;
|
||||
}
|
||||
// 新增态校验 webhook_url;编辑态走 id 兜底,不需要表单 webhook_url
|
||||
if (!isUpdate.value && (!values.webhook_url || values.webhook_url === '****')) {
|
||||
message.warning('请填写 webhook 地址');
|
||||
return;
|
||||
const isApp = isWorkWechatApp(values.platform_code);
|
||||
if (!isUpdate.value) {
|
||||
if (isApp) {
|
||||
if (!values.chat_id) {
|
||||
message.warning('请填写群聊 ID');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!isUsePlatformCredential(values) &&
|
||||
(!values.corp_id || !values.secret || !values.agent_id)
|
||||
) {
|
||||
message.warning('自定义凭证请填写 corp_id、Secret、agent_id');
|
||||
return;
|
||||
}
|
||||
} else if (!values.webhook_url || values.webhook_url === '****') {
|
||||
message.warning('请填写 webhook 地址');
|
||||
return;
|
||||
}
|
||||
}
|
||||
testing.value = true;
|
||||
// 进入测试态时清空旧结果,避免用户误以为是上次结果
|
||||
testResults.value = [];
|
||||
testSummary.value = '正在发送测试消息,请稍候(最多 8 种类型)...';
|
||||
try {
|
||||
const result = await testSendOaRobot(
|
||||
isUpdate.value
|
||||
? {
|
||||
// 编辑态:传 id,后端按 id 自动取已加密密钥
|
||||
id: values.id,
|
||||
platform_code: values.platform_code,
|
||||
test_content: '萧康云医 OA 测试消息',
|
||||
}
|
||||
: {
|
||||
// 新增态:传表单填写的 webhook_url/secret
|
||||
platform_code: values.platform_code,
|
||||
webhook_url: values.webhook_url,
|
||||
secret: values.secret || '',
|
||||
test_content: '萧康云医 OA 测试消息',
|
||||
},
|
||||
: isApp
|
||||
? {
|
||||
platform_code: values.platform_code,
|
||||
use_platform_credential: Number(
|
||||
values.use_platform_credential ?? 1,
|
||||
),
|
||||
corp_id: values.corp_id || '',
|
||||
secret: values.secret || '',
|
||||
agent_id: Number(values.agent_id || 0),
|
||||
chat_id: values.chat_id,
|
||||
test_content: '萧康云医 OA 测试消息',
|
||||
}
|
||||
: {
|
||||
platform_code: values.platform_code,
|
||||
webhook_url: values.webhook_url,
|
||||
secret: values.secret || '',
|
||||
test_content: '萧康云医 OA 测试消息',
|
||||
},
|
||||
);
|
||||
// 兼容后端两种返回结构:单结果(旧)/ 多结果数组(新)
|
||||
if (Array.isArray(result?.results)) {
|
||||
testResults.value = result.results;
|
||||
const total = result.total ?? result.results.length;
|
||||
@@ -257,7 +410,6 @@ async function handleTestSend() {
|
||||
message.warning(testSummary.value);
|
||||
}
|
||||
} else if (result?.success !== undefined) {
|
||||
// 兼容老接口:单条结果包装为数组
|
||||
testResults.value = [
|
||||
{
|
||||
message_type: 'text',
|
||||
@@ -283,14 +435,8 @@ async function handleTestSend() {
|
||||
>
|
||||
<Form />
|
||||
|
||||
<!--
|
||||
绑定群聊:去掉自定义背景块改为简单分区
|
||||
- 全部走 antd 原生样式,暗色模式下由 antd 自动保证对比度一致性
|
||||
- 标题用 TypographyTitle,提示用 TypographyText type="secondary"
|
||||
- 必须用具名导入(TypographyTitle/TypographyText),不要用 Typography.Title 命名空间写法
|
||||
(script setup 下命名空间子组件可能解析失败导致整块不渲染)
|
||||
-->
|
||||
<div class="chat-bind-section">
|
||||
<!-- webhook 平台:N:N 绑定群聊;应用 API 目标群即 chat_id,不展示本区 -->
|
||||
<div v-if="showChatBind" class="chat-bind-section">
|
||||
<TypographyTitle :level="5" class="chat-bind-title">
|
||||
绑定群聊
|
||||
</TypographyTitle>
|
||||
@@ -319,6 +465,35 @@ async function handleTestSend() {
|
||||
</Checkbox.Group>
|
||||
</div>
|
||||
|
||||
<!-- 应用 API:同步群成员入口(编辑态有 id 时可用) -->
|
||||
<div
|
||||
v-if="isWorkWechatApp(currentPlatformCode)"
|
||||
class="chat-bind-section"
|
||||
>
|
||||
<TypographyTitle :level="5" class="chat-bind-title">
|
||||
群成员
|
||||
</TypographyTitle>
|
||||
<TypographyText type="secondary" class="chat-bind-hint">
|
||||
保存凭证后可同步 appchat 成员,供测试发送与场景 @ 勾选
|
||||
</TypographyText>
|
||||
<div class="member-sync-row">
|
||||
<Button
|
||||
v-if="isUpdate"
|
||||
size="small"
|
||||
:loading="syncingMembers"
|
||||
@click="handleSyncMembers"
|
||||
>
|
||||
同步群成员
|
||||
</Button>
|
||||
<TypographyText v-if="memberSyncHint" type="secondary">
|
||||
{{ memberSyncHint }}
|
||||
</TypographyText>
|
||||
<TypographyText v-else-if="!isUpdate" type="secondary">
|
||||
首次保存后会自动同步成员
|
||||
</TypographyText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试发送结果展示区:仅在有过测试时显示 -->
|
||||
<div v-if="testResults.length > 0 || testSummary" class="test-results">
|
||||
<Alert
|
||||
@@ -358,8 +533,8 @@ async function handleTestSend() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 测试发送按钮塞在默认「取消/确定」按钮组的左侧 -->
|
||||
<template #prepend-footer>
|
||||
<!-- webhook 平台保留表单内测试;应用 API 请到列表「测试」独立弹窗选人/选群 -->
|
||||
<template v-if="!isWorkWechatApp(currentPlatformCode)" #prepend-footer>
|
||||
<Button type="default" :loading="testing" @click="handleTestSend">
|
||||
测试发送(全部类型)
|
||||
</Button>
|
||||
@@ -399,6 +574,13 @@ async function handleTestSend() {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.member-sync-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.test-results {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
|
||||
@@ -129,7 +129,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="测试发送结果" class="w-[60%]">
|
||||
<Modal title="测试发送结果" class="w-[56%]">
|
||||
<!-- 1. 汇总提示 -->
|
||||
<Alert
|
||||
v-if="testSummary"
|
||||
@@ -182,8 +182,8 @@ const [Modal, modalApi] = useVbenModal({
|
||||
</TypographyTitle>
|
||||
<Timeline v-if="testResults.length > 0">
|
||||
<TimelineItem
|
||||
v-for="item in testResults"
|
||||
:key="item.message_type"
|
||||
v-for="(item, idx) in testResults"
|
||||
:key="`${item.message_type}-${idx}`"
|
||||
:color="item.success ? 'green' : 'red'"
|
||||
>
|
||||
<!-- 标题行:名称 + 类型 Tag + 成功/失败 Tag + 耗时 -->
|
||||
@@ -288,4 +288,22 @@ const [Modal, modalApi] = useVbenModal({
|
||||
background: var(--ant-color-fill-quaternary, rgba(0, 0, 0, 0.02));
|
||||
color: var(--ant-color-text, #1d2129);
|
||||
}
|
||||
|
||||
/* 暗色:强制浅色字,避免 CSS 变量未注入时落入 #1d2129 黑底黑字 */
|
||||
html.dark .payload-pre,
|
||||
.dark .payload-pre {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: rgba(255, 255, 255, 0.16);
|
||||
}
|
||||
|
||||
html.dark .timeline-name,
|
||||
.dark .timeline-name {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
html.dark .timeline-msg:not(.error),
|
||||
.dark .timeline-msg:not(.error) {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
/**
|
||||
* OA 机器人行级测试发送弹窗
|
||||
* work_wechat_app:必须选接收员工或群聊;客户群需再选确认发送员工
|
||||
* webhook:沿用 @ 全员 / 手机号
|
||||
*/
|
||||
import { ref, computed, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message, Select, Switch, Textarea } from 'ant-design-vue';
|
||||
import { Alert, message, Radio, RadioGroup, Select, Switch, Textarea } from 'ant-design-vue';
|
||||
|
||||
import { testSendOaRobot } from '#/views/system/oa-robot/api';
|
||||
|
||||
/**
|
||||
* OA 机器人「行级测试发送」参数弹窗
|
||||
*
|
||||
* 与 test-result-modal.vue 配合:
|
||||
* - 本弹窗:负责输入测试参数(test_content、@all、mobiles)和发起发送
|
||||
* - 测试发送结果弹窗:发送完成后接管结果展示
|
||||
* - 拆分后两个弹窗职责单一:本弹窗专注参数输入,结果弹窗专注结果展示
|
||||
*
|
||||
* 数据流:
|
||||
* 1. index.vue 的 openTestModal 注入 onFinished 回调
|
||||
* 2. 用户点击「发送测试」→ 后端按平台循环跑所有消息类型
|
||||
* 3. 发送完成后调用 onFinished({ results, summary, robotName })
|
||||
* 4. 本弹窗关闭,index.vue 中 onFinished 打开 test-result-modal.vue
|
||||
*
|
||||
* 与表单弹窗内测试的区别:
|
||||
* - 表单弹窗内测试:用于「保存前验证密钥」(无 id,直接走 webhook_url/secret)
|
||||
* - 本弹窗(行级测试):用于「列表中已有机器人」的快速冒烟测试(有 id,后端自动取已加密密钥)
|
||||
*/
|
||||
import { getOaChatListByPlatform } from '#/views/system/oa-chat/api';
|
||||
import { getOaWwUserList } from '#/views/system/oa-chat/api/org';
|
||||
import {
|
||||
getOaRobotBoundScenes,
|
||||
testSendOaRobot,
|
||||
} from '#/views/system/oa-robot/api';
|
||||
import { isWorkWechatApp } from '#/views/system/oa-robot/config/form';
|
||||
|
||||
interface TestResultItem {
|
||||
message_type: string;
|
||||
@@ -41,14 +33,6 @@ interface RobotRow {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** onFinished 回调签名(由 index.vue 注入)
|
||||
*
|
||||
* payload 字段说明:
|
||||
* - results/summary:核心结果数据(必传)
|
||||
* - robotName/robotId/platformCode:机器人上下文(结果弹窗展示用)
|
||||
* - testContent/testAtAll/testMobiles:测试参数(结果弹窗「发送详情」展示用)
|
||||
* - sentAt:发送完成时间戳(秒),结果弹窗「发送时间」展示
|
||||
*/
|
||||
interface OnFinishedPayload {
|
||||
results: TestResultItem[];
|
||||
summary: string;
|
||||
@@ -61,22 +45,37 @@ interface OnFinishedPayload {
|
||||
sentAt?: number;
|
||||
}
|
||||
|
||||
/** 是否飞书平台(飞书 webhook 不支持手机号 @) */
|
||||
const isFeishu = computed(() => currentRobot.value?.platform_code === 'feishu');
|
||||
const isApp = computed(() =>
|
||||
isWorkWechatApp(currentRobot.value?.platform_code),
|
||||
);
|
||||
|
||||
/** 当前测试的机器人信息(由列表行传入) */
|
||||
const currentRobot = ref<RobotRow | null>(null);
|
||||
/** 测试发送 loading */
|
||||
const testing = ref(false);
|
||||
/** 测试内容(默认值预填,用户可改) */
|
||||
const testContent = ref('萧康云医 OA 测试消息');
|
||||
/** @ 所有人开关 */
|
||||
const testAtAll = ref(false);
|
||||
/** @ 手机号列表(antd Select mode='tags',允许用户输入任意手机号) */
|
||||
const testMobiles = ref<string[]>([]);
|
||||
/** onFinished 回调(由外部注入,发送完成后通知父级打开结果弹窗) */
|
||||
/** 应用 API:person | group */
|
||||
const targetMode = ref<'person' | 'group'>('person');
|
||||
const testUserid = ref<string | undefined>(undefined);
|
||||
const testChatId = ref<number | undefined>(undefined);
|
||||
const testSenderUserid = ref<string | undefined>(undefined);
|
||||
const userOptions = ref<{ label: string; value: string }[]>([]);
|
||||
const chatOptions = ref<
|
||||
{ label: string; value: number; chat_kind: number }[]
|
||||
>([]);
|
||||
/** 该机器人已绑定的场景(可选:选中则只发场景配置类型) */
|
||||
const boundScenes = ref<{ id: number; scene_code: string; scene_name: string }[]>(
|
||||
[],
|
||||
);
|
||||
const selectedSceneId = ref<number | undefined>(undefined);
|
||||
const onFinished = ref<((payload: OnFinishedPayload) => void) | null>(null);
|
||||
|
||||
const selectedChatKind = computed(() => {
|
||||
const hit = chatOptions.value.find((c) => c.value === testChatId.value);
|
||||
return hit?.chat_kind ?? 0;
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
@@ -94,26 +93,86 @@ const [Modal, modalApi] = useVbenModal({
|
||||
}>();
|
||||
currentRobot.value = data?.row ?? null;
|
||||
onFinished.value = data?.onFinished ?? null;
|
||||
// 每次打开都重置表单,避免上次状态残留
|
||||
testContent.value = '萧康云医 OA 测试消息';
|
||||
testAtAll.value = false;
|
||||
testMobiles.value = [];
|
||||
// 动态设置标题(让用户看到正在测试哪个机器人)
|
||||
targetMode.value = 'person';
|
||||
testUserid.value = undefined;
|
||||
testChatId.value = undefined;
|
||||
testSenderUserid.value = undefined;
|
||||
selectedSceneId.value = undefined;
|
||||
boundScenes.value = [];
|
||||
if (currentRobot.value) {
|
||||
modalApi.setState({
|
||||
title: `测试发送 - ${currentRobot.value.name}`,
|
||||
confirmLoading: false,
|
||||
});
|
||||
loadBoundScenes(currentRobot.value.id);
|
||||
if (isWorkWechatApp(currentRobot.value.platform_code)) {
|
||||
loadAppOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 发送测试:调用后端 oa-robot/test-send 接口
|
||||
* 后端按 platform_code 循环跑该平台支持的所有消息类型,并注入 @ 配置
|
||||
* 发送完成后调用 onFinished 回调,关闭本弹窗,由父级打开结果弹窗
|
||||
*/
|
||||
watch(testAtAll, (val) => {
|
||||
if (val) {
|
||||
testMobiles.value = [];
|
||||
}
|
||||
});
|
||||
|
||||
watch(targetMode, () => {
|
||||
testUserid.value = undefined;
|
||||
testChatId.value = undefined;
|
||||
testSenderUserid.value = undefined;
|
||||
});
|
||||
|
||||
async function loadBoundScenes(robotId: number) {
|
||||
try {
|
||||
const res = await getOaRobotBoundScenes(robotId);
|
||||
const list = Array.isArray(res) ? res : (res as any)?.data || [];
|
||||
boundScenes.value = (Array.isArray(list) ? list : []).map((s: any) => ({
|
||||
id: Number(s.id),
|
||||
scene_code: String(s.scene_code || ''),
|
||||
scene_name: String(s.scene_name || ''),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
boundScenes.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAppOptions() {
|
||||
try {
|
||||
const [userRes, chatRes] = await Promise.all([
|
||||
getOaWwUserList({
|
||||
platform_code: 'work_wechat_app',
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
}),
|
||||
getOaChatListByPlatform(),
|
||||
]);
|
||||
const userData = userRes?.data ?? userRes ?? {};
|
||||
const items = Array.isArray(userData.items) ? userData.items : [];
|
||||
userOptions.value = items.map((u: any) => ({
|
||||
label: u.name ? `${u.name}(${u.userid})` : u.userid,
|
||||
value: String(u.userid),
|
||||
}));
|
||||
const grouped = chatRes?.data ?? chatRes ?? {};
|
||||
const list = grouped.work_wechat_app || [];
|
||||
chatOptions.value = (Array.isArray(list) ? list : []).map((c: any) => ({
|
||||
label: `${c.name}${Number(c.chat_kind) === 2 ? '(客户群·需确认)' : '(应用群)'}`,
|
||||
value: Number(c.id),
|
||||
chat_kind: Number(c.chat_kind ?? 2),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
userOptions.value = [];
|
||||
chatOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTestSend() {
|
||||
if (!currentRobot.value?.id) {
|
||||
message.warning('机器人信息缺失');
|
||||
@@ -123,19 +182,52 @@ async function handleTestSend() {
|
||||
message.warning('请填写测试内容');
|
||||
return;
|
||||
}
|
||||
if (isApp.value) {
|
||||
// 选了场景时后端优先用场景 targets,可不强制弹窗选人/选群
|
||||
if (!selectedSceneId.value) {
|
||||
if (targetMode.value === 'person' && !testUserid.value) {
|
||||
message.warning('请选择接收员工');
|
||||
return;
|
||||
}
|
||||
if (targetMode.value === 'group' && !testChatId.value) {
|
||||
message.warning('请选择接收群聊');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
targetMode.value === 'group' &&
|
||||
selectedChatKind.value === 2 &&
|
||||
!testSenderUserid.value
|
||||
) {
|
||||
message.warning('客户群请选择确认发送员工');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
testing.value = true;
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const result = await testSendOaRobot({
|
||||
const payload: Record<string, any> = {
|
||||
id: currentRobot.value.id,
|
||||
platform_code: currentRobot.value.platform_code,
|
||||
// 行级测试有 id,后端会自动取已加密密钥;webhook_url/secret 不传,避免 mask 字符串覆盖
|
||||
test_content: testContent.value,
|
||||
test_at_all: testAtAll.value,
|
||||
test_mobiles: isFeishu.value ? [] : testMobiles.value,
|
||||
});
|
||||
};
|
||||
if (selectedSceneId.value) {
|
||||
payload.scene_id = selectedSceneId.value;
|
||||
}
|
||||
if (isApp.value) {
|
||||
if (targetMode.value === 'person') {
|
||||
payload.test_userid = testUserid.value;
|
||||
} else {
|
||||
payload.test_chat_id = testChatId.value;
|
||||
if (selectedChatKind.value === 2) {
|
||||
payload.test_sender_userid = testSenderUserid.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await testSendOaRobot(payload);
|
||||
|
||||
// 构造结果数据
|
||||
let results: TestResultItem[] = [];
|
||||
let summary = '';
|
||||
if (Array.isArray(result?.results)) {
|
||||
@@ -145,9 +237,10 @@ async function handleTestSend() {
|
||||
result.success_count ??
|
||||
result.results.filter((r: any) => r.success).length;
|
||||
const fail = total - succ;
|
||||
summary = `共 ${total} 种类型,成功 ${succ} 种,失败 ${fail} 种`;
|
||||
summary = selectedSceneId.value
|
||||
? `按场景发送:共 ${total} 条,成功 ${succ},失败 ${fail}`
|
||||
: `共 ${total} 种类型,成功 ${succ} 种,失败 ${fail} 种`;
|
||||
} else if (result?.success !== undefined) {
|
||||
// 兜底兼容老接口结构
|
||||
results = [
|
||||
{
|
||||
message_type: 'text',
|
||||
@@ -161,20 +254,17 @@ async function handleTestSend() {
|
||||
summary = result.success ? '测试发送成功' : '测试发送失败';
|
||||
}
|
||||
|
||||
// 发送完成:关闭本弹窗,通过 onFinished 回调打开结果弹窗
|
||||
modalApi.close();
|
||||
if (onFinished.value) {
|
||||
onFinished.value({
|
||||
results,
|
||||
summary,
|
||||
// 上下文字段:结果弹窗「发送详情」展示用
|
||||
robotName: currentRobot.value.name,
|
||||
robotId: currentRobot.value.id,
|
||||
platformCode: currentRobot.value.platform_code,
|
||||
testContent: testContent.value,
|
||||
testAtAll: testAtAll.value,
|
||||
testMobiles: isFeishu.value ? [] : testMobiles.value,
|
||||
// 发送完成时间戳(秒)
|
||||
sentAt: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
@@ -186,8 +276,7 @@ async function handleTestSend() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="测试发送" class="w-[40%]">
|
||||
<!-- 测试参数表单(专注于参数输入,结果展示移交 test-result-modal.vue) -->
|
||||
<Modal title="测试发送" class="w-[42%]">
|
||||
<div class="test-form">
|
||||
<div class="form-item">
|
||||
<label class="form-label">测试内容</label>
|
||||
@@ -198,62 +287,151 @@ async function handleTestSend() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<label class="form-label">@ 所有人</label>
|
||||
<Switch v-model:checked="testAtAll" />
|
||||
<span class="form-hint">
|
||||
开启后所有消息类型都按 @ 全员发送
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!isFeishu" class="form-item">
|
||||
<label class="form-label">@ 手机号</label>
|
||||
<!-- 绑定场景:选中后只发场景配置的消息类型,不循环全类型 -->
|
||||
<div v-if="boundScenes.length > 0" class="form-item">
|
||||
<label class="form-label">按场景发送(可选)</label>
|
||||
<Select
|
||||
v-model:value="testMobiles"
|
||||
mode="tags"
|
||||
:disabled="testAtAll"
|
||||
placeholder="输入手机号后回车添加,可多个(@ 所有人开启时无效)"
|
||||
:token-separators="[',', ' ']"
|
||||
v-model:value="selectedSceneId"
|
||||
allow-clear
|
||||
show-search
|
||||
class="w-full"
|
||||
placeholder="不选则测试该平台全部消息类型"
|
||||
:options="
|
||||
boundScenes.map((s) => ({
|
||||
label: `${s.scene_name}(${s.scene_code})`,
|
||||
value: s.id,
|
||||
}))
|
||||
"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
<div v-if="selectedSceneId" class="form-hint">
|
||||
将只发送该场景已配置的消息类型与内容(可用上方测试内容覆盖正文)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
v-else
|
||||
type="info"
|
||||
show-icon
|
||||
message="飞书 webhook 不支持手机号 @,仅 @ 所有人有效"
|
||||
class="form-alert"
|
||||
/>
|
||||
<!-- 应用 API:必选投递目标(选场景时可作兜底) -->
|
||||
<template v-if="isApp && !selectedSceneId">
|
||||
<div class="form-item">
|
||||
<label class="form-label">接收对象</label>
|
||||
<RadioGroup v-model:value="targetMode">
|
||||
<Radio value="person">员工(个人消息)</Radio>
|
||||
<Radio value="group">群聊</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
<div v-if="targetMode === 'person'" class="form-item">
|
||||
<label class="form-label">接收员工</label>
|
||||
<Select
|
||||
v-model:value="testUserid"
|
||||
show-search
|
||||
allow-clear
|
||||
:options="userOptions"
|
||||
placeholder="选择企微员工 userid"
|
||||
class="w-full"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="form-item">
|
||||
<label class="form-label">接收群聊</label>
|
||||
<Select
|
||||
v-model:value="testChatId"
|
||||
show-search
|
||||
allow-clear
|
||||
:options="chatOptions"
|
||||
placeholder="选择群聊"
|
||||
class="w-full"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="targetMode === 'group' && selectedChatKind === 2" class="form-item">
|
||||
<label class="form-label">确认发送员工</label>
|
||||
<Select
|
||||
v-model:value="testSenderUserid"
|
||||
show-search
|
||||
allow-clear
|
||||
:options="userOptions"
|
||||
placeholder="客户群群发需员工在企微确认"
|
||||
class="w-full"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
"
|
||||
/>
|
||||
<span class="form-hint">对应企微 add_msg_template.sender,非实时</span>
|
||||
</div>
|
||||
<Alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="应用 API 测试请在本弹窗选择接收对象;保存机器人仅配置凭证"
|
||||
class="form-alert"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="form-item">
|
||||
<label class="form-label">@ 所有人</label>
|
||||
<Switch v-model:checked="testAtAll" />
|
||||
<span class="form-hint">开启后所有消息类型都按 @ 全员发送</span>
|
||||
</div>
|
||||
<div v-if="!isFeishu" class="form-item">
|
||||
<label class="form-label">@ 手机号</label>
|
||||
<Select
|
||||
v-model:value="testMobiles"
|
||||
mode="tags"
|
||||
:disabled="testAtAll"
|
||||
placeholder="输入手机号后回车添加"
|
||||
:token-separators="[',', ' ']"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Alert
|
||||
v-if="isFeishu"
|
||||
type="info"
|
||||
show-icon
|
||||
message="飞书 webhook 不支持手机号 @,仅 @ 所有人有效"
|
||||
class="form-alert"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 暗色适配:全部走 antd CSS 变量 */
|
||||
.test-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 500;
|
||||
color: var(--ant-color-text, #1d2129);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
color: var(--ant-color-text-tertiary, #86909c);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.form-alert {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user