Merge remote-tracking branch 'origin/dev/message-oa' into feature/invoice

# Conflicts:
#	apps/web-antd/src/views/system/system-config/index.vue
This commit is contained in:
李琦
2026-07-28 08:25:45 +08:00
73 changed files with 15613 additions and 37 deletions

View File

@@ -28,6 +28,7 @@ import { createAdminGridOptions } from './table-config';
import { getRoleMeta } from './role-meta';
import DoctorCardModal from '#/views/doctor/doctor/components/DoctorCardModal.vue';
import PromoterInfoCard from '#/components/promoter/PromoterInfoCard.vue';
import OaAccountModal from '#/views/system/admin/_shared/components/oa-account-modal.vue';
import { resolveAvatarUrl } from '#/views/system/store-input/utils/inputUser';
function formatAdminRegion(row: {
@@ -121,6 +122,20 @@ const openDoctorCard = (row: { doctor_id?: number }) => {
DoctorCardModalApi.open();
};
/** 员工 OA 账号弹窗:每个员工可配置各平台的 @ 标识 */
const [OaAccountModals, OaAccountModalApi] = useVbenModal({
connectedComponent: OaAccountModal,
});
/** 打开员工 OA 账号弹窗 */
const openOaAccount = (row: { id: number; nick_name?: string; name?: string }) => {
OaAccountModalApi.setData({
id: row.id,
name: row.nick_name || row.name,
});
OaAccountModalApi.open();
};
const gridEvents: VxeGridListeners<any> = {
checkboxChange() {
const records = gridApi.grid.getCheckboxRecords();
@@ -210,6 +225,7 @@ const copyToClipboard = async (text: string) => {
<Form />
</Modal>
<DoctorCardModals />
<OaAccountModals />
<Grid>
<template #toolbar-buttons>
<TableAction
@@ -368,6 +384,13 @@ const copyToClipboard = async (text: string) => {
confirm: resetPasswordApi.bind(null, row.id),
},
},
{
label: 'OA 账号',
type: 'link',
icon: 'ant-design:notification-outlined',
size: 'small',
onClick: openOaAccount.bind(null, row),
},
]"
/>
</template>

View File

@@ -0,0 +1,235 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Button, Empty, message, Spin, Tabs } from 'ant-design-vue';
import { getOaPlatformList } from '#/views/system/oa-platform/api';
import {
getAdminOaAccounts,
saveAdminOaAccounts,
} from '#/views/system/admin/_shared/oa-account/api';
/**
* 员工 OA 账号管理弹窗
*
* 用途:在员工管理页的「更多操作」中点击「OA 账号」打开,
* 为该员工配置每个平台的 @ 标识(企业微信 userid、钉钉 userid、飞书 open_id 等)。
*
* 设计要点:
* - 每个启用平台一个 Tab,Tab 内可以添加多条账号(一个员工在一个平台可有多个账号)
* - 账号标识和昵称两个输入框:account_id 用于 @,account_name 仅用于展示
* - 保存时全量覆盖该员工的所有账号(先软删旧的,再插入新的)
*/
const loading = ref(false);
const saving = ref(false);
const platforms = ref<any[]>([]);
const activeTab = ref('');
const adminId = ref(0);
const adminName = ref('');
/** 按平台分组的账号列表:[platform_code => [{account_id, account_name, key}]] */
const accountsByPlatform = ref<Record<string, Array<{ account_id: string; account_name: string; key: number }>>>({});
let keyCounter = 0;
function genKey() {
keyCounter += 1;
return keyCounter;
}
const [Modal, modalApi] = useVbenModal({
fullscreenButton: false,
draggable: true,
onCancel() {
modalApi.close();
},
onConfirm: async () => {
if (!adminId.value) {
message.error('员工 ID 不能为空');
return;
}
saving.value = true;
try {
// 提交时去除 key 字段(后端不需要),过滤空 account_id
const payload: Record<string, any[]> = {};
for (const [platformCode, accounts] of Object.entries(accountsByPlatform.value)) {
const filtered = accounts
.filter((a) => a.account_id.trim() !== '')
.map((a) => ({
account_id: a.account_id.trim(),
account_name: a.account_name,
}));
if (filtered.length > 0) {
payload[platformCode] = filtered;
}
}
await saveAdminOaAccounts(adminId.value, payload);
message.success('保存成功');
modalApi.close();
} catch (e: any) {
message.error(e.message || '保存失败');
} finally {
saving.value = false;
}
},
onOpenChange(isOpen: boolean) {
if (isOpen) {
const data = modalApi.getData<{ id: number; name?: string }>();
adminId.value = data?.id || 0;
adminName.value = data?.name || '';
loadData();
}
},
});
/**
* 加载平台列表 + 该员工的已有账号
*/
async function loadData() {
if (!adminId.value) return;
loading.value = true;
try {
const [platformList, accountList] = await Promise.all([
getOaPlatformList(),
getAdminOaAccounts(adminId.value),
]);
const platformRows: any[] = Array.isArray(platformList)
? platformList
: platformList?.data || [];
platforms.value = platformRows.sort(
(a: any, b: any) => (b.sort || 0) - (a.sort || 0),
);
activeTab.value = platforms.value[0]?.platform_code || '';
// 把后端返回的按平台分组的账号填充到表单
const grouped: typeof accountsByPlatform.value = {};
for (const platform of platforms.value) {
const code = platform.platform_code;
const accounts: any[] = (accountList?.[code] || accountList?.data?.[code]) || [];
grouped[code] = accounts.map((a) => ({
account_id: String(a.account_id || ''),
account_name: String(a.account_name || ''),
key: genKey(),
}));
// 至少留一条空行,便于新增
if (grouped[code].length === 0) {
grouped[code] = [{ account_id: '', account_name: '', key: genKey() }];
}
}
accountsByPlatform.value = grouped;
} catch (e: any) {
message.error(e.message || '加载数据失败');
} finally {
loading.value = false;
}
}
/**
* 添加一行账号输入框
*/
function addAccount(platformCode: string) {
if (!accountsByPlatform.value[platformCode]) {
accountsByPlatform.value[platformCode] = [];
}
accountsByPlatform.value[platformCode].push({
account_id: '',
account_name: '',
key: genKey(),
});
}
/**
* 删除一行账号
*/
function removeAccount(platformCode: string, key: number) {
accountsByPlatform.value[platformCode] = (
accountsByPlatform.value[platformCode] || []
).filter((a) => a.key !== key);
}
/**
* 平台编码 → 显示名
*/
function platformName(code: string): string {
return platforms.value.find((p) => p.platform_code === code)?.name || code;
}
/**
* 平台编码 → 该平台 @ 标识的提示文案
*/
function platformTip(code: string): string {
if (code === 'work_wechat') return '企业微信 userid(在通讯录中查看,组织内唯一)';
if (code === 'work_wechat_app')
return '企业微信 userid(应用 API @ 群成员可选;员工 @ 主路径仍用手机号)';
if (code === 'dingtalk') return '钉钉 userid(管理后台 → 通讯录 → 成员详情)';
if (code === 'feishu') return '飞书 open_id(ou_ 开头)';
return '该平台的账号标识';
}
</script>
<template>
<Modal
:title="`员工 OA 账号 - ${adminName || `#${adminId}`}`"
class="w-[50%]"
>
<Spin :spinning="loading || saving">
<div v-if="platforms.length === 0 && !loading" class="py-6">
<Empty description="暂无启用的 OA 平台" />
</div>
<Tabs v-else v-model:active-key="activeTab">
<Tabs.TabPane
v-for="platform in platforms"
:key="platform.platform_code"
:tab="platform.name"
>
<div class="oa-account-tab">
<div class="mb-3 text-sm text-gray-500">
{{ platformTip(platform.platform_code) }}
</div>
<div
v-for="account in accountsByPlatform[platform.platform_code] || []"
:key="account.key"
class="mb-2 flex items-center gap-2"
>
<input
v-model="account.account_id"
class="ant-input ant-input-sm flex-1"
placeholder="账号标识(用于 @)"
/>
<input
v-model="account.account_name"
class="ant-input ant-input-sm flex-1"
placeholder="账号昵称(仅用于展示,可选)"
/>
<Button
type="link"
size="small"
danger
@click="removeAccount(platform.platform_code, account.key)"
>
删除
</Button>
</div>
<Button
type="dashed"
size="small"
block
@click="addAccount(platform.platform_code)"
>
+ 添加{{ platformName(platform.platform_code) }}账号
</Button>
</div>
</Tabs.TabPane>
</Tabs>
</Spin>
</Modal>
</template>
<style scoped>
.oa-account-tab {
padding: 8px;
max-height: 360px;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,23 @@
import { requestClient } from '#/api/request';
/**
* 员工 OA 账号管理 API
* 路由前缀:admin-oa-account/
*/
const prefix = 'admin-oa-account/';
/**
* 获取指定员工的所有 OA 账号(按平台分组)
* 返回结构:['work_wechat' => [{id, account_id, account_name}, ...], ...]
*/
export async function getAdminOaAccounts(adminId: number) {
return requestClient.get<any>(`${prefix}list`, { params: { admin_id: adminId } });
}
/**
* 保存员工的 OA 账号配置(全量同步)
* accounts 结构:{ platform_code: [{account_id, account_name}, ...] }
*/
export async function saveAdminOaAccounts(adminId: number, accounts: Record<string, any[]>) {
return requestClient.post<any>(`${prefix}save`, { admin_id: adminId, accounts });
}