feat: 钉钉、企业微信webbook、企业微信应用api,问题:交互
This commit is contained in:
34
apps/web-antd/src/views/system/oa-chat/api/org.ts
Normal file
34
apps/web-antd/src/views/system/oa-chat/api/org.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 企微组织架构 API(OA 群聊页「员工」Tab)
|
||||
* 路由前缀:oa-ww-org/
|
||||
*/
|
||||
const prefix = 'oa-ww-org/';
|
||||
|
||||
/**
|
||||
* 从企微同步组织架构(部门 + 成员)
|
||||
*/
|
||||
export async function syncOaWwOrg(data: { platform_code?: string } = {}) {
|
||||
return requestClient.post<any>(`${prefix}sync-org`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地部门树(antd Tree)
|
||||
*/
|
||||
export async function getOaWwDeptTree(params: { platform_code?: string } = {}) {
|
||||
return requestClient.get<any>(`${prefix}department-tree`, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* 员工分页列表
|
||||
*/
|
||||
export async function getOaWwUserList(params: {
|
||||
platform_code?: string;
|
||||
dept_id?: number;
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}) {
|
||||
return requestClient.get<any>(`${prefix}user-list`, { params });
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 创建企微应用群聊弹窗
|
||||
* 调 appchat/create,成功后回写本地 xk_oa_chat(chat_kind=应用群,source=接口创建)
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createOaAppChat } from '../api';
|
||||
|
||||
const gridApi = ref();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: [
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'name',
|
||||
label: '群名称',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '应用群聊名称' },
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'owner',
|
||||
label: '群主 userid',
|
||||
rules: 'required',
|
||||
componentProps: { placeholder: '企微成员 userid' },
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
fieldName: 'userlist',
|
||||
label: '成员 userid',
|
||||
rules: 'selectRequired',
|
||||
help: '至少 2 人(含群主);可输入后回车添加',
|
||||
componentProps: {
|
||||
mode: 'tags',
|
||||
placeholder: '输入 userid 后回车',
|
||||
tokenSeparators: [',', ' '],
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'chatid',
|
||||
label: '指定 chatid',
|
||||
help: '选填;不填则由企微自动生成',
|
||||
componentProps: { placeholder: '选填' },
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
const userlist = Array.isArray(values.userlist)
|
||||
? values.userlist
|
||||
: String(values.userlist || '')
|
||||
.split(/[,,\s]+/)
|
||||
.filter(Boolean);
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
try {
|
||||
await createOaAppChat({
|
||||
platform_code: 'work_wechat_app',
|
||||
name: values.name,
|
||||
owner: values.owner,
|
||||
userlist,
|
||||
chatid: values.chatid || '',
|
||||
});
|
||||
message.success('应用群创建成功');
|
||||
gridApi.value?.reload?.() ?? gridApi.value?.query?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
formApi.resetForm();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal title="创建应用群聊" class="w-[40%]">
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -0,0 +1,244 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* OA 群聊页「员工」面板
|
||||
* 左侧企微部门树自动分类,右侧员工列表;支持同步组织架构与关键词搜索
|
||||
*/
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
InputSearch,
|
||||
message,
|
||||
Pagination,
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
Tree,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getOaWwDeptTree,
|
||||
getOaWwUserList,
|
||||
syncOaWwOrg,
|
||||
} from '../api/org';
|
||||
|
||||
const syncLoading = ref(false);
|
||||
const treeLoading = ref(false);
|
||||
const tableLoading = ref(false);
|
||||
const treeData = ref<any[]>([]);
|
||||
const selectedDeptId = ref<number | null>(null);
|
||||
const keyword = ref('');
|
||||
const users = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
|
||||
/**
|
||||
* 同步企微组织架构
|
||||
*/
|
||||
async function handleSync() {
|
||||
syncLoading.value = true;
|
||||
try {
|
||||
const res = await syncOaWwOrg({ platform_code: 'work_wechat_app' });
|
||||
const data = res?.data ?? res ?? {};
|
||||
message.success(
|
||||
`同步完成:部门 ${data.dept_count ?? 0},成员 ${data.user_count ?? 0}`,
|
||||
);
|
||||
selectedDeptId.value = null;
|
||||
page.value = 1;
|
||||
await Promise.all([loadTree(), loadUsers()]);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '同步失败,请确认平台凭证与通讯录权限');
|
||||
} finally {
|
||||
syncLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载部门树
|
||||
*/
|
||||
async function loadTree() {
|
||||
treeLoading.value = true;
|
||||
try {
|
||||
const res = await getOaWwDeptTree({ platform_code: 'work_wechat_app' });
|
||||
const data = res?.data ?? res ?? [];
|
||||
treeData.value = Array.isArray(data) ? data : [];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
treeData.value = [];
|
||||
} finally {
|
||||
treeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载员工列表
|
||||
*/
|
||||
async function loadUsers() {
|
||||
tableLoading.value = true;
|
||||
try {
|
||||
const res = await getOaWwUserList({
|
||||
platform_code: 'work_wechat_app',
|
||||
dept_id: selectedDeptId.value || 0,
|
||||
keyword: keyword.value.trim(),
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
});
|
||||
const data = res?.data ?? res ?? {};
|
||||
users.value = Array.isArray(data.items) ? data.items : [];
|
||||
total.value = Number(data.total || 0);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
tableLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中部门节点
|
||||
*/
|
||||
function onSelectDept(keys: (string | number)[]) {
|
||||
const key = keys[0];
|
||||
selectedDeptId.value = key !== undefined && key !== null ? Number(key) : null;
|
||||
page.value = 1;
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
page.value = 1;
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
function onPageChange(p: number, ps: number) {
|
||||
page.value = p;
|
||||
pageSize.value = ps;
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: 'userid', dataIndex: 'userid', key: 'userid', width: 140 },
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
||||
{ title: '手机号', dataIndex: 'mobile', key: 'mobile', width: 120 },
|
||||
{
|
||||
title: '主部门',
|
||||
dataIndex: 'main_department_name',
|
||||
key: 'main_department_name',
|
||||
width: 140,
|
||||
},
|
||||
{ title: '职位', dataIndex: 'position', key: 'position', width: 120 },
|
||||
{
|
||||
title: '匹配系统员工',
|
||||
key: 'matched',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 90,
|
||||
},
|
||||
];
|
||||
|
||||
function statusLabel(status: number) {
|
||||
const map: Record<number, string> = {
|
||||
1: '已激活',
|
||||
2: '已禁用',
|
||||
4: '未激活',
|
||||
5: '退出企业',
|
||||
};
|
||||
return map[status] || String(status);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadTree();
|
||||
await loadUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="employee-panel flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button type="primary" :loading="syncLoading" @click="handleSync">
|
||||
同步组织架构
|
||||
</Button>
|
||||
<InputSearch
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="搜索姓名 / userid / 手机号"
|
||||
class="w-[280px]"
|
||||
@search="onSearch"
|
||||
/>
|
||||
<span class="text-xs text-gray-400">
|
||||
需平台凭证且应用已开通通讯录读权限;匹配仅展示,不自动绑定
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<!-- 左侧部门树:固定最大高度,内部滚动,避免撑高整页 -->
|
||||
<aside
|
||||
class="flex w-[260px] shrink-0 flex-col rounded-lg border border-border bg-card p-3"
|
||||
style="max-height: 560px"
|
||||
>
|
||||
<div class="mb-2 text-sm font-medium">组织架构</div>
|
||||
<Spin :spinning="treeLoading">
|
||||
<div class="overflow-auto" style="max-height: 500px">
|
||||
<Empty
|
||||
v-if="!treeLoading && treeData.length === 0"
|
||||
description="暂无部门,请先同步"
|
||||
:image="Empty.PRESENTED_IMAGE_SIMPLE"
|
||||
/>
|
||||
<Tree
|
||||
v-else
|
||||
:tree-data="treeData"
|
||||
:selected-keys="
|
||||
selectedDeptId !== null ? [String(selectedDeptId)] : []
|
||||
"
|
||||
default-expand-all
|
||||
@select="onSelectDept"
|
||||
/>
|
||||
</div>
|
||||
</Spin>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧员工表:不设自适应高度,内容自然撑开 + 横向滚动 -->
|
||||
<div class="min-w-0 flex-1 rounded-lg border border-border bg-card p-3">
|
||||
<Table
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data-source="users"
|
||||
:loading="tableLoading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
:scroll="{ x: 900 }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'matched'">
|
||||
<Tag v-if="record.matched_admin_id" color="success">
|
||||
{{ record.matched_admin_name || `#${record.matched_admin_id}` }}
|
||||
</Tag>
|
||||
<span v-else class="text-gray-400">未匹配</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag :color="record.status === 1 ? 'success' : 'default'">
|
||||
{{ statusLabel(Number(record.status)) }}
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<div class="mt-3 flex justify-end">
|
||||
<Pagination
|
||||
:current="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
show-size-changer
|
||||
:page-size-options="['10', '20', '50', '100']"
|
||||
@change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,184 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 企业微信应用 API「改凭证」弹窗
|
||||
* 仅保存凭证策略(平台默认 / 自定义 corp/secret/agent);测试请到列表「测试」弹窗选人/选群
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Alert, message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { updateOaRobotAppCredential } from '#/views/system/oa-robot/api';
|
||||
import {
|
||||
isUsePlatformCredential,
|
||||
showCustomAppCredential,
|
||||
} from '#/views/system/oa-robot/config/form';
|
||||
|
||||
const gridApi = ref();
|
||||
const robotId = ref(0);
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
wrapperClass: 'grid-cols-12',
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-12',
|
||||
componentProps: { class: 'w-full' },
|
||||
},
|
||||
layout: 'horizontal',
|
||||
showDefaultActions: false,
|
||||
schema: [
|
||||
{
|
||||
component: 'RadioGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '使用平台默认凭证', value: 1 },
|
||||
{ label: '自定义凭证', value: 0 },
|
||||
],
|
||||
},
|
||||
defaultValue: 1,
|
||||
fieldName: 'use_platform_credential',
|
||||
label: '凭证来源',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'corp_id',
|
||||
label: '企业 ID',
|
||||
componentProps: { placeholder: 'corpid', autocomplete: 'off' },
|
||||
dependencies: {
|
||||
triggerFields: ['use_platform_credential'],
|
||||
show: (values: { use_platform_credential?: number }) =>
|
||||
showCustomAppCredential({
|
||||
platform_code: 'work_wechat_app',
|
||||
use_platform_credential: values.use_platform_credential,
|
||||
}),
|
||||
rules: (values: { use_platform_credential?: number }) =>
|
||||
showCustomAppCredential({
|
||||
platform_code: 'work_wechat_app',
|
||||
use_platform_credential: values.use_platform_credential,
|
||||
})
|
||||
? 'required'
|
||||
: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInputPassword',
|
||||
fieldName: 'secret',
|
||||
label: '应用 Secret',
|
||||
componentProps: {
|
||||
placeholder: '留空表示不修改;填写则覆盖',
|
||||
autocomplete: 'new-password',
|
||||
},
|
||||
dependencies: {
|
||||
triggerFields: ['use_platform_credential'],
|
||||
show: (values: { use_platform_credential?: number }) =>
|
||||
showCustomAppCredential({
|
||||
platform_code: 'work_wechat_app',
|
||||
use_platform_credential: values.use_platform_credential,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
fieldName: 'agent_id',
|
||||
label: '应用 AgentId',
|
||||
componentProps: { placeholder: '企业应用 agentid', min: 1, class: 'w-full' },
|
||||
dependencies: {
|
||||
triggerFields: ['use_platform_credential'],
|
||||
show: (values: { use_platform_credential?: number }) =>
|
||||
showCustomAppCredential({
|
||||
platform_code: 'work_wechat_app',
|
||||
use_platform_credential: values.use_platform_credential,
|
||||
}),
|
||||
rules: (values: { use_platform_credential?: number }) =>
|
||||
showCustomAppCredential({
|
||||
platform_code: 'work_wechat_app',
|
||||
use_platform_credential: values.use_platform_credential,
|
||||
})
|
||||
? 'required'
|
||||
: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'chat_id',
|
||||
label: '默认群聊 ID',
|
||||
componentProps: {
|
||||
placeholder: '可选;场景选群为主。填写后可同步成员',
|
||||
autocomplete: 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
const usePlatform = Number(values.use_platform_credential ?? 1);
|
||||
// 自定义凭证时 secret 首次必填;编辑留空表示不改
|
||||
if (
|
||||
!isUsePlatformCredential({
|
||||
platform_code: 'work_wechat_app',
|
||||
use_platform_credential: usePlatform,
|
||||
}) &&
|
||||
!String(values.secret || '').trim()
|
||||
) {
|
||||
// 编辑场景允许留空不改;若库中本无 secret 会由后端校验
|
||||
}
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
await updateOaRobotAppCredential({
|
||||
id: robotId.value,
|
||||
use_platform_credential: usePlatform,
|
||||
corp_id: String(values.corp_id || ''),
|
||||
secret: String(values.secret || ''),
|
||||
agent_id: Number(values.agent_id || 0) || undefined,
|
||||
chat_id: String(values.chat_id || ''),
|
||||
});
|
||||
message.success('凭证已更新');
|
||||
gridApi.value?.query?.() || gridApi.value?.reload?.();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) return;
|
||||
const data = modalApi.getData<{ row: any; gridApi: any }>() ?? {};
|
||||
gridApi.value = data.gridApi;
|
||||
const row = data.row || {};
|
||||
robotId.value = Number(row.id || 0);
|
||||
formApi.setValues({
|
||||
use_platform_credential: Number(row.use_platform_credential ?? 1),
|
||||
corp_id: row.corp_id || '',
|
||||
secret: '',
|
||||
agent_id: Number(row.agent_id || 0) || undefined,
|
||||
chat_id: row.chat_id || '',
|
||||
});
|
||||
modalApi.setState({
|
||||
title: `改凭证 - ${row.name || ''}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[40%]">
|
||||
<Alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="本弹窗仅保存凭证"
|
||||
description="冒烟测试请关闭后在列表点击「测试」,并选择接收员工或群聊后再发送。"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<Form />
|
||||
</Modal>
|
||||
</template>
|
||||
1131
apps/web-antd/src/views/system/oa-scene/components/form-page.vue
Normal file
1131
apps/web-antd/src/views/system/oa-scene/components/form-page.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,355 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 企微应用 API 场景投递目标面板
|
||||
* 支持:选人(多选)+ 选群(多选)+ 每群独立 @员工 / @userid
|
||||
* 客户群必须配置「确认发送员工」sender_userid(add_msg_template.sender)
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
CollapsePanel,
|
||||
Select,
|
||||
Tag,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getOaChatListByPlatform } from '#/views/system/oa-chat/api';
|
||||
import { getOaWwUserList } from '#/views/system/oa-chat/api/org';
|
||||
|
||||
export interface SceneTargetItem {
|
||||
target_type: 1 | 2;
|
||||
platform_code: string;
|
||||
admin_id?: number;
|
||||
chat_id?: number;
|
||||
at_admin_ids?: number[];
|
||||
at_userids?: string[];
|
||||
/** 客户群群发确认人 userid */
|
||||
sender_userid?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: SceneTargetItem[];
|
||||
adminList: Array<{ id: number; name: string }>;
|
||||
platformCode?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: SceneTargetItem[]): void;
|
||||
}>();
|
||||
|
||||
const platformCode = computed(() => props.platformCode || 'work_wechat_app');
|
||||
|
||||
/** 该平台下的群聊选项 */
|
||||
const chatOptions = ref<
|
||||
Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
chat_kind: number;
|
||||
external_id: string;
|
||||
}>
|
||||
>([]);
|
||||
|
||||
/** 企微组织架构员工(确认发送人 / 可作参考) */
|
||||
const wwUserOptions = ref<{ label: string; value: string }[]>([]);
|
||||
|
||||
const personAdminIds = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
.filter((t) => t.target_type === 1 && (t.admin_id || 0) > 0)
|
||||
.map((t) => Number(t.admin_id));
|
||||
},
|
||||
set(ids: number[]) {
|
||||
rebuild(ids, groupChatIds.value);
|
||||
},
|
||||
});
|
||||
|
||||
const groupChatIds = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
.filter((t) => t.target_type === 2 && (t.chat_id || 0) > 0)
|
||||
.map((t) => Number(t.chat_id));
|
||||
},
|
||||
set(ids: number[]) {
|
||||
rebuild(personAdminIds.value, ids);
|
||||
},
|
||||
});
|
||||
|
||||
/** 按 chat_id 取群目标行(含 @ / 确认人配置) */
|
||||
function groupTargetOf(chatId: number): SceneTargetItem {
|
||||
return (
|
||||
props.modelValue.find(
|
||||
(t) => t.target_type === 2 && Number(t.chat_id) === chatId,
|
||||
) || {
|
||||
target_type: 2,
|
||||
platform_code: platformCode.value,
|
||||
chat_id: chatId,
|
||||
at_admin_ids: [],
|
||||
at_userids: [],
|
||||
sender_userid: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function chatMeta(chatId: number) {
|
||||
return chatOptions.value.find((c) => c.id === chatId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建 targets:保留已有群的 @ / sender 配置,新增群默认空
|
||||
*/
|
||||
function rebuild(adminIds: number[], chatIds: number[]) {
|
||||
const next: SceneTargetItem[] = [];
|
||||
for (const id of adminIds) {
|
||||
next.push({
|
||||
target_type: 1,
|
||||
platform_code: platformCode.value,
|
||||
admin_id: id,
|
||||
});
|
||||
}
|
||||
for (const chatId of chatIds) {
|
||||
const prev = groupTargetOf(chatId);
|
||||
next.push({
|
||||
target_type: 2,
|
||||
platform_code: platformCode.value,
|
||||
chat_id: chatId,
|
||||
at_admin_ids: [...(prev.at_admin_ids || [])],
|
||||
at_userids: [...(prev.at_userids || [])],
|
||||
sender_userid: prev.sender_userid || '',
|
||||
});
|
||||
}
|
||||
emit('update:modelValue', next);
|
||||
}
|
||||
|
||||
function updateGroupAt(
|
||||
chatId: number,
|
||||
patch: {
|
||||
at_admin_ids?: number[];
|
||||
at_userids?: string[];
|
||||
sender_userid?: string;
|
||||
},
|
||||
) {
|
||||
const next = props.modelValue.map((t) => {
|
||||
if (t.target_type !== 2 || Number(t.chat_id) !== chatId) return t;
|
||||
return {
|
||||
...t,
|
||||
at_admin_ids: patch.at_admin_ids ?? t.at_admin_ids ?? [],
|
||||
at_userids: patch.at_userids ?? t.at_userids ?? [],
|
||||
sender_userid:
|
||||
patch.sender_userid !== undefined
|
||||
? patch.sender_userid
|
||||
: t.sender_userid || '',
|
||||
};
|
||||
});
|
||||
emit('update:modelValue', next);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前校验:任一客户群缺确认发送员工则拦截
|
||||
* @returns 错误文案,通过返回空串
|
||||
*/
|
||||
function validateBeforeSubmit(): string {
|
||||
for (const chatId of groupChatIds.value) {
|
||||
const meta = chatMeta(chatId);
|
||||
if (Number(meta?.chat_kind) !== 2) continue;
|
||||
const sender = String(groupTargetOf(chatId).sender_userid || '').trim();
|
||||
if (!sender) {
|
||||
return `客户群「${meta?.name || chatId}」请选择确认发送员工`;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
defineExpose({ validateBeforeSubmit });
|
||||
|
||||
async function loadChats() {
|
||||
try {
|
||||
const res = await getOaChatListByPlatform();
|
||||
const grouped = res?.data ?? res ?? {};
|
||||
const list = grouped[platformCode.value] || [];
|
||||
chatOptions.value = (Array.isArray(list) ? list : []).map((c: any) => ({
|
||||
id: Number(c.id),
|
||||
name: String(c.name || ''),
|
||||
chat_kind: Number(c.chat_kind ?? 2),
|
||||
external_id: String(c.external_id || ''),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
message.error('加载群聊列表失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWwUsers() {
|
||||
try {
|
||||
const userRes = await getOaWwUserList({
|
||||
platform_code: platformCode.value,
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
});
|
||||
const userData = userRes?.data ?? userRes ?? {};
|
||||
const items = Array.isArray(userData.items) ? userData.items : [];
|
||||
wwUserOptions.value = items.map((u: any) => ({
|
||||
label: u.name ? `${u.name}(${u.userid})` : String(u.userid),
|
||||
value: String(u.userid),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
wwUserOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadChats();
|
||||
loadWwUsers();
|
||||
});
|
||||
watch(platformCode, () => {
|
||||
loadChats();
|
||||
loadWwUsers();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scene-targets-panel">
|
||||
<div class="mb-3 text-xs text-gray-500">
|
||||
同一场景可同时选人、多群;客户群发送为群发任务,需指定确认员工并在企微客户端确认(非实时)
|
||||
</div>
|
||||
|
||||
<!-- 发送给个人 -->
|
||||
<div class="mb-4">
|
||||
<div class="section-title">发送给个人</div>
|
||||
<Checkbox.Group
|
||||
v-model:value="personAdminIds"
|
||||
class="flex flex-wrap gap-2"
|
||||
>
|
||||
<Checkbox
|
||||
v-for="admin in adminList"
|
||||
:key="admin.id"
|
||||
:value="admin.id"
|
||||
>
|
||||
{{ admin.name }}
|
||||
</Checkbox>
|
||||
</Checkbox.Group>
|
||||
<div v-if="adminList.length === 0" class="text-gray-400">暂无员工可选</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送给群 -->
|
||||
<div class="mb-2">
|
||||
<div class="section-title">发送给群</div>
|
||||
<Select
|
||||
v-model:value="groupChatIds"
|
||||
mode="multiple"
|
||||
allow-clear
|
||||
class="w-full"
|
||||
placeholder="选择群聊(可多选)"
|
||||
:options="
|
||||
chatOptions.map((c) => ({
|
||||
label: `${c.name}${c.chat_kind === 2 ? '(客户群·需确认)' : '(应用群)'}`,
|
||||
value: c.id,
|
||||
}))
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Collapse v-if="groupChatIds.length > 0" ghost>
|
||||
<CollapsePanel
|
||||
v-for="chatId in groupChatIds"
|
||||
:key="chatId"
|
||||
:header="chatMeta(chatId)?.name || `群#${chatId}`"
|
||||
>
|
||||
<template #extra>
|
||||
<Tag
|
||||
:color="chatMeta(chatId)?.chat_kind === 1 ? 'blue' : 'orange'"
|
||||
@click.stop
|
||||
>
|
||||
{{
|
||||
chatMeta(chatId)?.chat_kind === 1
|
||||
? '应用群·实时'
|
||||
: '客户群·需确认'
|
||||
}}
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 仅客户群:确认发送员工必填 -->
|
||||
<div
|
||||
v-if="chatMeta(chatId)?.chat_kind === 2"
|
||||
class="mb-3"
|
||||
>
|
||||
<div class="mb-1 text-xs">
|
||||
确认发送员工 <span class="text-red-500">*</span>
|
||||
</div>
|
||||
<Select
|
||||
show-search
|
||||
allow-clear
|
||||
class="w-full"
|
||||
placeholder="选择在企微客户端确认群发的员工 userid"
|
||||
:value="groupTargetOf(chatId).sender_userid || undefined"
|
||||
:options="wwUserOptions"
|
||||
:filter-option="
|
||||
(input: string, option: any) =>
|
||||
String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase())
|
||||
"
|
||||
@change="
|
||||
(v: string) => updateGroupAt(chatId, { sender_userid: v || '' })
|
||||
"
|
||||
/>
|
||||
<div class="mt-1 text-xs text-gray-400">
|
||||
对应企微 add_msg_template.sender,该员工需在客户端点确认后消息才会发出
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2 text-xs text-gray-500">本群 @ 配置</div>
|
||||
<div class="mb-2">
|
||||
<div class="mb-1 text-xs">@ 系统员工(手机号)</div>
|
||||
<Select
|
||||
mode="multiple"
|
||||
class="w-full"
|
||||
placeholder="选择要 @ 的员工"
|
||||
:value="groupTargetOf(chatId).at_admin_ids || []"
|
||||
:options="
|
||||
adminList.map((a) => ({ label: a.name, value: a.id }))
|
||||
"
|
||||
@change="
|
||||
(v: number[]) => updateGroupAt(chatId, { at_admin_ids: v })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-1 text-xs">@ 群成员 userid</div>
|
||||
<Select
|
||||
mode="tags"
|
||||
class="w-full"
|
||||
placeholder="输入 userid 回车添加"
|
||||
:token-separators="[',', ' ']"
|
||||
:value="groupTargetOf(chatId).at_userids || []"
|
||||
@change="
|
||||
(v: string[]) => updateGroupAt(chatId, { at_userids: v })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</CollapsePanel>
|
||||
</Collapse>
|
||||
|
||||
<Button
|
||||
v-if="chatOptions.length === 0"
|
||||
type="link"
|
||||
size="small"
|
||||
class="px-0"
|
||||
@click="loadChats"
|
||||
>
|
||||
刷新群聊列表
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section-title {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user