From 38a25a4b40fabe27b0331b7877652133ecd91252 Mon Sep 17 00:00:00 2001 From: linmianxun <634841751@qq.com> Date: Mon, 14 Sep 2026 21:13:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=BA=E5=91=98=E6=89=80=E5=B1=9E=E9=83=A8?= =?UTF-8?q?=E9=97=A8=E4=B8=8B=E6=8B=89=E5=AF=B9=E9=BD=90=E6=97=A7=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=EF=BC=9A=E4=B8=BB=E8=B4=A6=E6=8F=90=E4=BE=9B=20/depar?= =?UTF-8?q?tments=20=E7=9F=AD=E5=90=8D=E5=85=A8=E9=87=8F=EF=BC=8C=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=8F=AF=E6=90=9C=E5=8F=AF=E9=80=89=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../InquiryStaffRoleController.java | 37 ++- .../inquiry/wecom/WeComAddressBookClient.java | 7 +- .../src/views/inquiry/staffRole/index.vue | 266 +++++++++++++----- .../views/inquiry/staffRole/staffRole.api.ts | 10 +- 4 files changed, 244 insertions(+), 76 deletions(-) diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryStaffRoleController.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryStaffRoleController.java index 31207ac..e903a10 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryStaffRoleController.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/controller/InquiryStaffRoleController.java @@ -22,8 +22,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -147,9 +149,40 @@ public class InquiryStaffRoleController extends JeecgController + * 旧前端:{@code GET /inquiry/staffRole/departments} → {@code string[]}, + * 再 {@code options: names.map(n => ({label:n,value:n}))},showSearch 可滚动全量。 + * 本接口同源:企微 {@code department/list} 短名全量;根公司(id=1)不进下拉。 + * 浏览器禁止直连企微。 */ - @ApiOperation("企微部门列表") + @ApiOperation("员工部门名列表(对齐旧项目 /departments)") + @RequiresPermissions("inquiry:staff:list") + @GetMapping("/departments") + public Result> departments() { + List all = weComAddressBookClient.listDepartments(); + List names = new ArrayList<>(); + LinkedHashSet seen = new LinkedHashSet<>(); + for (WeComDepartment d : all) { + if (d == null || StringUtils.isBlank(d.getName())) { + continue; + } + // 企微根部门(id=1 / parentid=0)是公司全称,员工下拉不展示 + if (d.getId() == 1 || d.getParentId() <= 0) { + continue; + } + String name = d.getName().trim(); + if (seen.add(name)) { + names.add(name); + } + } + return Result.OK(names); + } + + /** + * 企微部门结构列表(含 id/path,供排查)。员工下拉请用 {@link #departments()}。 + */ + @ApiOperation("企微部门结构列表") @RequiresPermissions("inquiry:staff:list") @GetMapping("/wecomDepartments") public Result> wecomDepartments() { diff --git a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/wecom/WeComAddressBookClient.java b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/wecom/WeComAddressBookClient.java index 7e7ce9b..75adbca 100644 --- a/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/wecom/WeComAddressBookClient.java +++ b/inquiry-api/jeecg-module-inquiry/src/main/java/org/jeecg/modules/inquiry/wecom/WeComAddressBookClient.java @@ -127,8 +127,9 @@ public class WeComAddressBookClient { } /** - * 拉取企微部门树(扁平列表,带 path 便于下拉展示)。 + * 拉取企微 {@code department/list} 全量扁平列表(短名 + path)。 *

+ * 员工下拉与旧项目同源:整份通讯录,不截层级。顺序保持企微返回序。 * 副作用:出站 HTTP;失败抛 IllegalStateException。 */ public List listDepartments() { @@ -163,8 +164,8 @@ public class WeComAddressBookClient { String path = buildDepartmentPath(id, byId); result.add(new WeComDepartment(id, name, parentId, path)); } - // 按路径排序,下拉更易扫 - result.sort((a, b) -> StringUtils.defaultString(a.getPath()).compareTo(StringUtils.defaultString(b.getPath()))); + // 保持企微 department/list 原序(LinkedHashMap 插入序 = 接口返回序), + // 不要按 path 重排,也不要截层级;员工下拉与旧项目一样用这份全量短名。 return result; } diff --git a/inquiry-backend/src/views/inquiry/staffRole/index.vue b/inquiry-backend/src/views/inquiry/staffRole/index.vue index 2263a58..c75f64a 100644 --- a/inquiry-backend/src/views/inquiry/staffRole/index.vue +++ b/inquiry-backend/src/views/inquiry/staffRole/index.vue @@ -9,6 +9,7 @@ import { message, Modal } from "ant-design-vue"; import { disableStaff, getStaffCounts, + getStaffDepartments, getStaffList, getWecomDepartments, saveStaff as saveStaffApi, @@ -19,7 +20,8 @@ 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 }; +/** 部门下拉:value 存短名,label 带层级缩进 */ +type DeptOption = { id: number; name: string; label: string; depth: number }; type StaffRow = { id: string; @@ -59,9 +61,11 @@ const filters = reactive({ const modalOpen = ref(false); const editing = ref(null); const syncing = ref(false); -const wecomDepts = ref([]); -const wecomDeptsLoading = ref(false); -const wecomDeptsLoaded = ref(false); +/** 部门树扁平列表:一级不缩进,子级前加空格 */ +const deptOptions = ref([]); +const deptLoading = ref(false); +const deptLoaded = ref(false); +const deptOpen = ref(false); const form = reactive({ name: "", wecomId: "", @@ -75,6 +79,12 @@ const form = reactive({ const filtered = computed(() => rows.value); +const filteredDeptOptions = computed(() => { + const q = (form.dept || "").trim().toLowerCase(); + if (!q) return deptOptions.value; + return deptOptions.value.filter((d) => d.name.toLowerCase().includes(q)); +}); + function colorFor(name: string) { let h = 0; for (let i = 0; i < name.length; i++) h = (h + name.charCodeAt(i) * (i + 1)) % AVATAR_COLORS.length; @@ -148,64 +158,79 @@ watch( onMounted(() => loadList()); +/** 企微根部门公司全称,下拉不展示 */ +const ROOT_DEPT_NAME = "深圳市友通达供应链管理有限公司"; + /** - * 打开弹窗时拉企微部门;失败不挡手工保存,仅提示。 - * 下拉对齐旧项目业务层:只展示「分公司」「旧组织」下的直接子部门 - *(事业二部/元昇/武汉友通达/通多邦…),不要公司根下的一级中心。 + * 按 parentId 排成树再前序展开。 + * 展示:部门1 / 「 」子部门1 / 「 」子部门2 / 部门2(根公司不出现)。 + * 保存仍用短名 name。 */ -async function ensureWecomDepartments() { - if (wecomDeptsLoaded.value || wecomDeptsLoading.value) { +function buildDeptTreeOptions(raw: any[]): DeptOption[] { + const nodes = (raw as any[]) + .map((d) => ({ + id: Number(d?.id), + name: String(d?.name || "").trim(), + parentId: Number(d?.parentId ?? d?.parentid ?? 0), + })) + .filter((d) => d.name && Number.isFinite(d.id) && d.id > 0) + .filter((d) => d.id !== 1 && d.parentId > 0 && d.name !== ROOT_DEPT_NAME); + + const idSet = new Set(nodes.map((n) => n.id)); + const children = new Map(); + for (const n of nodes) { + const pid = idSet.has(n.parentId) ? n.parentId : 0; + const list = children.get(pid) || []; + list.push(n); + children.set(pid, list); + } + + const out: DeptOption[] = []; + const walk = (pid: number, depth: number) => { + for (const n of children.get(pid) || []) { + out.push({ + id: n.id, + name: n.name, + label: "\u00A0".repeat(depth) + n.name, + depth, + }); + walk(n.id, depth + 1); + } + }; + walk(0, 0); + return out; +} + +/** + * 打开弹窗拉企微部门树;失败不挡手工保存。 + * 优先 wecomDepartments(有 parentId 才能缩进);否则退回 departments 扁平短名。 + */ +async function ensureDeptOptions() { + if (deptLoaded.value || deptLoading.value) { ensureCurrentDeptOption(); return; } - wecomDeptsLoading.value = true; + deptLoading.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)); + try { + const res: any = await getWecomDepartments(); + const list = Array.isArray(res) ? res : res?.result || []; + deptOptions.value = buildDeptTreeOptions(list); + } catch { + const res: any = await getStaffDepartments(); + const list = Array.isArray(res) ? res : res?.result || []; + deptOptions.value = (list as any[]) + .map((x, i) => { + const name = String(x || "").trim(); + return { id: -(i + 1), name, label: name, depth: 0 }; + }) + .filter((d) => d.name && d.name !== ROOT_DEPT_NAME); } - - wecomDepts.value = picked - .slice() - .sort((a, b) => a.name.localeCompare(b.name, "zh-CN")) - .map((d) => ({ ...d, depth: 0 })); - wecomDeptsLoaded.value = true; + deptLoaded.value = true; } catch (e: any) { - message.warning(e?.message || "企微部门列表加载失败,可稍后重试"); + message.warning(e?.message || "部门列表加载失败,可稍后重试"); } finally { - wecomDeptsLoading.value = false; + deptLoading.value = false; ensureCurrentDeptOption(); } } @@ -213,17 +238,40 @@ async function ensureWecomDepartments() { 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; - } + if (!deptOptions.value.some((d) => d.name === cur)) { + deptOptions.value = [{ id: -1, name: cur, label: cur, depth: 0 }, ...deptOptions.value]; } } +let deptBlurTimer: ReturnType | null = null; + +function pickDept(d: DeptOption) { + form.dept = d.name; + deptOpen.value = false; +} + +function onDeptFocus() { + if (deptBlurTimer) { + clearTimeout(deptBlurTimer); + deptBlurTimer = null; + } + deptOpen.value = true; + ensureDeptOptions(); +} + +function onDeptBlur() { + deptBlurTimer = setTimeout(() => { + deptOpen.value = false; + }, 180); +} + +watch( + () => modalOpen.value, + (open) => { + if (!open) deptOpen.value = false; + }, +); + function openCreate() { editing.value = null; Object.assign(form, { @@ -237,9 +285,8 @@ function openCreate() { status: "active", }); modalOpen.value = true; - // 每次打开重新拉一级部门,避免会话里还留着旧的「整棵树」缓存 - wecomDeptsLoaded.value = false; - ensureWecomDepartments(); + deptLoaded.value = false; + ensureDeptOptions(); } function openEdit(row: StaffRow) { @@ -255,8 +302,8 @@ function openEdit(row: StaffRow) { status: row.status, }); modalOpen.value = true; - wecomDeptsLoaded.value = false; - ensureWecomDepartments(); + deptLoaded.value = false; + ensureDeptOptions(); } async function saveStaff() { @@ -483,10 +530,32 @@ function routeTags(routes: string) {

- +
+ + +
    +
  • 无匹配部门
  • +
  • + {{ d.name }} +
  • +
+
@@ -553,4 +622,61 @@ function routeTags(routes: string) { accent-color: #155eef; cursor: pointer; } + +.dept-combo { + position: relative; + width: 100%; +} + +.dept-combo .field { + padding-right: 28px; +} + +.dept-combo-arrow { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: #64748b; + font-size: 10px; + line-height: 1; +} + +.dept-combo-list { + position: absolute; + left: 0; + right: 0; + top: calc(100% + 4px); + z-index: 30; + max-height: 220px; + overflow: auto; + margin: 0; + padding: 4px 0; + list-style: none; + background: #fff; + border: 1px solid #cbd5e1; + border-radius: 7px; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); +} + +.dept-combo-list li { + padding: 6px 10px; + font-size: 11px; + font-weight: 600; + color: #172033; + cursor: pointer; + white-space: nowrap; +} + +.dept-combo-list li:hover, +.dept-combo-list li.is-active { + background: #eff6ff; + color: #155eef; +} + +.dept-combo-list li.is-empty { + color: #94a3b8; + cursor: default; +} diff --git a/inquiry-backend/src/views/inquiry/staffRole/staffRole.api.ts b/inquiry-backend/src/views/inquiry/staffRole/staffRole.api.ts index 97ce15f..d010af8 100644 --- a/inquiry-backend/src/views/inquiry/staffRole/staffRole.api.ts +++ b/inquiry-backend/src/views/inquiry/staffRole/staffRole.api.ts @@ -1,6 +1,7 @@ /** * 员工岗位 API:对接主账 /inquiry/staffRole。 * 与 Jeecg /sys/user 分离——本页管企微岗位绑定;同步走主账 /syncWecom,浏览器不直连企微。 + * 部门下拉对齐旧项目:GET /inquiry/staffRole/departments → string[]。 */ import { defHttp } from '/@/utils/http/axios'; @@ -12,6 +13,8 @@ enum Api { disable = '/inquiry/staffRole/disable', queryById = '/inquiry/staffRole/queryById', syncWecom = '/inquiry/staffRole/syncWecom', + /** 旧项目同名接口:部门短名全量 */ + departments = '/inquiry/staffRole/departments', wecomDepartments = '/inquiry/staffRole/wecomDepartments', } @@ -23,5 +26,10 @@ export const disableStaff = (id: string) => defHttp.post({ url: Api.disable, par export const getStaffById = (id: string) => defHttp.get({ url: Api.queryById, params: { id } }); /** 主账拉企微通讯录 upsert;返回 { fetched, created, updated, skipped, message } */ export const syncWecomStaff = () => defHttp.post({ url: Api.syncWecom }); -/** 企微部门下拉(主账转发,禁止浏览器直连企微) */ +/** + * 部门下拉(对齐旧项目):返回 string[] 短名全量,可搜索可滚动。 + * 失败时勿挡手工填部门。 + */ +export const getStaffDepartments = () => defHttp.get({ url: Api.departments }); +/** @deprecated 结构列表;员工下拉请用 getStaffDepartments */ export const getWecomDepartments = () => defHttp.get({ url: Api.wecomDepartments });