主账支持企微通讯录同步人员,并完善工单展示与后台人员/工单页联调能力。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-14 20:26:36 +08:00
co-authored by Cursor
parent cea3a6e51e
commit 624ec1b7c4
13 changed files with 1173 additions and 42 deletions
@@ -1,7 +1,8 @@
<script setup lang="ts">
/**
* 员工管理:对接主账 /inquiry/staffRole。
* 企微 userid 由运营手工绑定,不向企微换号;停用走 disable 接口。
* 企微 userid 可手工维护,也可点「同步企业微信」由主账 upsert(姓名/手机/部门)。
* 浏览器禁止直连企微;岗位/线路仍手工改。
*/
import { computed, onMounted, reactive, ref, watch } from "vue";
import { message, Modal } from "ant-design-vue";
@@ -9,13 +10,17 @@ import {
disableStaff,
getStaffCounts,
getStaffList,
getWecomDepartments,
saveStaff as saveStaffApi,
syncWecomStaff,
} from "/@/views/inquiry/staffRole/staffRole.api";
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
type StaffRole = "sales" | "air" | "sea" | "land";
type WecomDeptOption = { id: number; name: string; path: string; parentId: number; depth: number };
type StaffRow = {
id: string;
name: string;
@@ -53,6 +58,10 @@ const filters = reactive({
const modalOpen = ref(false);
const editing = ref<StaffRow | null>(null);
const syncing = ref(false);
const wecomDepts = ref<WecomDeptOption[]>([]);
const wecomDeptsLoading = ref(false);
const wecomDeptsLoaded = ref(false);
const form = reactive({
name: "",
wecomId: "",
@@ -139,6 +148,82 @@ watch(
onMounted(() => loadList());
/**
* 打开弹窗时拉企微部门;失败不挡手工保存,仅提示。
* 下拉对齐旧项目业务层:只展示「分公司」「旧组织」下的直接子部门
*(事业二部/元昇/武汉友通达/通多邦…),不要公司根下的一级中心。
*/
async function ensureWecomDepartments() {
if (wecomDeptsLoaded.value || wecomDeptsLoading.value) {
ensureCurrentDeptOption();
return;
}
wecomDeptsLoading.value = true;
try {
const res: any = await getWecomDepartments();
const list = Array.isArray(res) ? res : res?.result || [];
const all: WecomDeptOption[] = (list as any[])
.map((d) => {
const path = String(d.path || d.name || "");
const name = String(d.name || path);
const parentId = Number(d.parentId ?? d.parentid ?? 0);
const depth = path ? path.split(" / ").length - 1 : 0;
return {
id: Number(d.id),
name,
path,
parentId,
depth: depth < 0 ? 0 : depth,
};
})
.filter((d) => d.name && d.id);
// 旧项目下拉对应层:分公司 + 旧组织 的子节点
const businessParentNames = new Set(["分公司", "旧组织"]);
const businessParentIds = new Set(
all.filter((d) => businessParentNames.has(d.name)).map((d) => d.id),
);
let picked = all.filter((d) => businessParentIds.has(d.parentId));
// 兜底:若企微改名导致找不到父节点,仍回退到根下一级,避免空下拉
if (!picked.length) {
const rootIds = new Set(
all.filter((d) => d.parentId === 0 || d.depth === 0).map((d) => d.id),
);
if (!rootIds.size) {
const minDepth = Math.min(...all.map((d) => d.depth));
all.filter((d) => d.depth === minDepth).forEach((d) => rootIds.add(d.id));
}
picked = all.filter((d) => rootIds.has(d.parentId));
}
wecomDepts.value = picked
.slice()
.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"))
.map((d) => ({ ...d, depth: 0 }));
wecomDeptsLoaded.value = true;
} catch (e: any) {
message.warning(e?.message || "企微部门列表加载失败,可稍后重试");
} finally {
wecomDeptsLoading.value = false;
ensureCurrentDeptOption();
}
}
function ensureCurrentDeptOption() {
const cur = (form.dept || "").trim();
if (!cur) return;
const hit = wecomDepts.value.some((d) => d.name === cur || d.path === cur);
if (!hit) {
wecomDepts.value = [{ id: -1, name: cur, path: cur, parentId: 0, depth: 0 }, ...wecomDepts.value];
} else {
const byPath = wecomDepts.value.find((d) => d.path === cur);
if (byPath && form.dept !== byPath.name) {
form.dept = byPath.name;
}
}
}
function openCreate() {
editing.value = null;
Object.assign(form, {
@@ -152,6 +237,9 @@ function openCreate() {
status: "active",
});
modalOpen.value = true;
// 每次打开重新拉一级部门,避免会话里还留着旧的「整棵树」缓存
wecomDeptsLoaded.value = false;
ensureWecomDepartments();
}
function openEdit(row: StaffRow) {
@@ -167,6 +255,8 @@ function openEdit(row: StaffRow) {
status: row.status,
});
modalOpen.value = true;
wecomDeptsLoaded.value = false;
ensureWecomDepartments();
}
async function saveStaff() {
@@ -213,8 +303,19 @@ function removeStaff(row: StaffRow) {
});
}
function syncWecom() {
message.info("企微通讯录同步尚未开放,请手工维护企微 ID");
async function syncWecom() {
if (syncing.value) return;
syncing.value = true;
try {
const res: any = await syncWecomStaff();
const msg = res?.message || (typeof res === "string" ? res : "同步完成");
message.success(msg);
await loadList();
} catch (e: any) {
message.error(e?.message || "同步失败");
} finally {
syncing.value = false;
}
}
function routeTags(routes: string) {
@@ -229,11 +330,11 @@ function routeTags(routes: string) {
<template>
<Teleport defer to="#page-topbar-actions">
<div class="staff-topbar-actions">
<button type="button" class="btn btn-secondary" @click="syncWecom">
<ProtoIcon name="refresh" :size="16" />同步企业微信
<button type="button" class="btn btn-secondary" :disabled="syncing" @click="syncWecom">
<ProtoIcon name="refresh" :size="16" />{{ syncing ? "同步中…" : "同步企业微信" }}
</button>
<button type="button" class="btn btn-primary" @click="openCreate">
<ProtoIcon name="userPlus" :size="16" />新建员工
<ProtoIcon name="userPlus" :size="16" />新增员工
</button>
</div>
</Teleport>
@@ -363,29 +464,32 @@ function routeTags(routes: string) {
<AdminModal
v-model:open="modalOpen"
:title="editing ? '编辑员工' : '新建员工'"
:title="editing ? '编辑员工' : '新增员工'"
size="md"
ok-text="保存"
ok-text="确认"
@ok="saveStaff"
>
<div class="form-row">
<label class="lbl">姓名 <span class="req">*</span></label>
<input v-model="form.name" class="field" type="text" />
</div>
<div class="form-row">
<label class="lbl">企微 ID <span class="req">*</span></label>
<input v-model="form.wecomId" class="field field-mono" type="text" placeholder="如 zhangmin@yt" />
</div>
<div class="form-row">
<label class="lbl">手机号</label>
<input v-model="form.phone" class="field" type="tel" />
</div>
<div class="form-row">
<label class="lbl">部门</label>
<input v-model="form.dept" class="field" type="text" />
<label class="lbl">企业微信 ID <span class="req">*</span></label>
<input v-model="form.wecomId" class="field field-mono" type="text" placeholder="如 zhangmin@yt" />
</div>
<div class="form-row">
<label class="lbl">岗位</label>
<label class="lbl">所属部门</label>
<select v-model="form.dept" class="field" :disabled="wecomDeptsLoading">
<option value="">{{ wecomDeptsLoading ? "加载企微部门…" : "请选择部门" }}</option>
<option v-for="d in wecomDepts" :key="d.id + '-' + d.name" :value="d.name">{{ d.name }}</option>
</select>
</div>
<div class="form-row">
<label class="lbl">岗位 <span class="req">*</span></label>
<select v-model="form.role" class="field">
<option value="sales">销售</option>
<option value="air">航线·空运</option>