Files
xk-admin/apps/web-antd/src/views/system/oa-robot/components/webhook-modal.vue
2026-07-20 15:05:34 +08:00

349 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Alert, Button, InputPassword, message, Modal as AModal, Tag } from 'ant-design-vue';
import { testSendOaRobot, updateOaRobotWebhook } from '#/views/system/oa-robot/api';
/**
* OA 机器人「单独编辑推送地址」独立弹窗
*
* 设计意图:把 webhook_url 和 secret 的修改从「编辑基础信息」中剥离,
* 避免用户在编辑名称/备注时无意中触发 webhook 重新加密写入。
*
* 关键交互:
* - 顶部 Alert 警示:修改后立即生效,影响所有绑定该机器人的 OA 场景
* - webhook_url 必填(不允许 mask)
* - secret 选填,placeholder 提示「留空 = 清空加签」
* - 内置「测试发送」按钮:保存前先用新地址测一次,避免改错导致线上通知失效
* - 提交前 Modal.confirm 二次确认,避免误操作
*/
interface TestResultItem {
message_type: string;
name: string;
success: boolean;
message: string;
response: any;
cost_ms: number;
}
interface RobotRow {
id: number;
platform_code: string;
name: string;
}
/** 当前操作的机器人(由列表行传入) */
const currentRobot = ref<RobotRow | null>(null);
/** webhook_url 输入值 */
const webhookUrl = ref('');
/** secret 输入值(空字符串表示清空) */
const secret = ref('');
/** 测试发送 loading */
const testing = ref(false);
/** 最近一次测试发送的结果列表 */
const testResults = ref<TestResultItem[]>([]);
/** 测试发送结果汇总提示 */
const testSummary = ref('');
/**
* 测试发送:用「当前填写」的 webhook_url/secret 临时验证(不依赖数据库)
* 与列表「测试」按钮的区别:列表测试用库里已存的密钥;本弹窗用表单新值
*/
async function handleTestSend() {
if (!currentRobot.value?.id) {
message.warning('机器人信息缺失');
return;
}
if (!currentRobot.value.platform_code) {
message.warning('平台编码缺失');
return;
}
if (!webhookUrl.value || webhookUrl.value === '****') {
message.warning('请填写 webhook 地址');
return;
}
testing.value = true;
testResults.value = [];
testSummary.value = '正在发送测试消息,请稍候(最多 8 种类型)...';
try {
const result = await testSendOaRobot({
// 注意:这里不传 id,强制用表单新值(不走后端兜底取密钥)
platform_code: currentRobot.value.platform_code,
webhook_url: webhookUrl.value,
secret: secret.value || '',
test_content: '萧康云医 OA 测试消息',
});
if (Array.isArray(result?.results)) {
testResults.value = result.results;
const total = result.total ?? result.results.length;
const succ =
result.success_count ??
result.results.filter((r: any) => r.success).length;
const fail = total - succ;
testSummary.value = `共 ${total} 种类型,成功 ${succ} 种,失败 ${fail} 种`;
if (fail === 0) {
message.success(testSummary.value);
} else if (succ === 0) {
message.error(testSummary.value);
} else {
message.warning(testSummary.value);
}
} else if (result?.success !== undefined) {
testResults.value = [
{
message_type: 'text',
name: '文本消息',
success: result.success,
message: result.message || '',
response: result.response,
cost_ms: 0,
},
];
testSummary.value = result.success ? '测试发送成功' : '测试发送失败';
}
} finally {
testing.value = false;
}
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (!currentRobot.value?.id) {
message.warning('机器人信息缺失');
return;
}
if (!webhookUrl.value) {
message.warning('请填写 Webhook 地址');
return;
}
if (webhookUrl.value === '****') {
message.warning('请填写完整的 Webhook 地址(不允许 mask)');
return;
}
// secret 为 mask 时报错(必须重新输入完整密钥或留空清空)
if (secret.value && secret.value === '****') {
message.warning('请填写完整的加签密钥或留空以清空');
return;
}
// 二次确认:避免误改导致线上 OA 通知失效
AModal.confirm({
title: '确认修改推送地址?',
content:
'修改后将立即生效,会影响所有绑定该机器人的 OA 通知场景。是否继续?',
okType: 'danger',
onOk: async () => {
modalApi.setState({ loading: true, confirmLoading: true });
try {
await updateOaRobotWebhook({
id: currentRobot.value!.id,
webhook_url: webhookUrl.value,
secret: secret.value || '',
});
message.success('修改成功');
const grid = modalApi.getData<{ gridApi?: any }>()?.gridApi;
grid?.query?.();
modalApi.close();
} finally {
modalApi.setState({ loading: false, confirmLoading: false });
}
},
});
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{ row: RobotRow; gridApi?: any }>();
currentRobot.value = data?.row ?? null;
// 每次打开都重置表单(不回显原 webhook/secret,强制用户重新输入,避免误操作)
webhookUrl.value = '';
secret.value = '';
testResults.value = [];
testSummary.value = '';
if (currentRobot.value) {
modalApi.setState({
title: `编辑推送地址 - ${currentRobot.value.name}`,
confirmLoading: false,
});
}
}
},
});
</script>
<template>
<Modal title="编辑推送地址" class="w-[40%]">
<!-- 顶部警示:修改后立即生效,影响线上 OA 通知 -->
<Alert
type="warning"
show-icon
message="修改 Webhook 地址后立即生效"
description="会影响所有绑定该机器人的 OA 通知场景。建议先点「测试发送」验证新地址可用后再保存。"
style="margin-bottom: 16px"
/>
<!-- 表单:webhook_url 必填 + secret 选填 -->
<div class="webhook-form">
<div class="form-item">
<label class="form-label">
Webhook 地址 <span class="required-mark">*</span>
</label>
<InputPassword
v-model:value="webhookUrl"
placeholder="请粘贴新的 webhook URL"
autocomplete="new-password"
/>
<div class="form-hint">
为防止误操作,编辑时不回显原地址,需完整粘贴新地址
</div>
</div>
<div class="form-item">
<label class="form-label">加签密钥</label>
<InputPassword
v-model:value="secret"
placeholder="选填,留空表示清空加签密钥"
autocomplete="new-password"
/>
<div class="form-hint">
若原机器人有加签密钥,请重新输入完整密钥;留空则清空原密钥
</div>
</div>
</div>
<!-- 测试结果展示区 -->
<div v-if="testResults.length > 0 || testSummary" class="test-results">
<Alert
v-if="testSummary"
:type="
testResults.length === 0
? 'info'
: testResults.every((r) => r.success)
? 'success'
: testResults.some((r) => r.success)
? 'warning'
: 'error'
"
:message="testSummary"
show-icon
style="margin-bottom: 12px"
/>
<div v-if="testResults.length > 0" class="result-list">
<div
v-for="item in testResults"
:key="item.message_type"
class="result-item"
>
<div class="result-header">
<Tag :color="item.success ? 'success' : 'error'">
{{ item.success ? '成功' : '失败' }}
</Tag>
<span class="result-name">{{ item.name }}</span>
<span class="result-type">({{ item.message_type }})</span>
<span v-if="item.cost_ms" class="result-cost">
{{ item.cost_ms }}ms
</span>
</div>
<div v-if="!item.success && item.message" class="result-error">
{{ item.message }}
</div>
</div>
</div>
</div>
<!-- 底部按钮:取消(默认)/ 测试发送(prepend)/ 确定修改(confirm) -->
<template #prepend-footer>
<Button type="default" :loading="testing" @click="handleTestSend">
测试发送(全部类型)
</Button>
</template>
</Modal>
</template>
<style scoped lang="scss">
/* 暗色适配:全部走 antd CSS 变量 */
.webhook-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-item {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-weight: 500;
color: var(--ant-color-text, #1d2129);
font-size: 13px;
}
.required-mark {
color: var(--ant-color-error, #f53f3f);
margin-left: 4px;
}
.form-hint {
color: var(--ant-color-text-tertiary, #86909c);
font-size: 12px;
}
.test-results {
margin-top: 16px;
padding-top: 12px;
border-top: 1px dashed var(--ant-color-border-secondary, #e5e6eb);
}
.result-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.result-item {
padding: 8px 12px;
border-radius: 4px;
background: var(--ant-color-fill-quaternary, #f7f8fa);
font-size: 12px;
}
.result-header {
display: flex;
align-items: center;
gap: 6px;
}
.result-name {
font-weight: 500;
color: var(--ant-color-text, #1d2129);
}
.result-type {
color: var(--ant-color-text-tertiary, #86909c);
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
}
.result-cost {
margin-left: auto;
color: var(--ant-color-text-tertiary, #86909c);
}
.result-error {
margin-top: 4px;
color: var(--ant-color-error, #f53f3f);
word-break: break-all;
line-height: 1.5;
}
</style>