人员所属部门下拉对齐旧项目:主账提供 /departments 短名全量,后台可搜可选。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+35
-2
@@ -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<InquiryStaffRole
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微部门下拉选项。浏览器禁止直连企微,只打本接口。
|
||||
* 员工「所属部门」下拉(对齐旧项目 admin-rel26)。
|
||||
* <p>
|
||||
* 旧前端:{@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<List<String>> departments() {
|
||||
List<WeComDepartment> all = weComAddressBookClient.listDepartments();
|
||||
List<String> names = new ArrayList<>();
|
||||
LinkedHashSet<String> 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<List<WeComDepartment>> wecomDepartments() {
|
||||
|
||||
+4
-3
@@ -127,8 +127,9 @@ public class WeComAddressBookClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取企微部门树(扁平列表,带 path 便于下拉展示)。
|
||||
* 拉取企微 {@code department/list} 全量扁平列表(短名 + path)。
|
||||
* <p>
|
||||
* 员工下拉与旧项目同源:整份通讯录,不截层级。顺序保持企微返回序。
|
||||
* 副作用:出站 HTTP;失败抛 IllegalStateException。
|
||||
*/
|
||||
public List<WeComDepartment> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<StaffRow | null>(null);
|
||||
const syncing = ref(false);
|
||||
const wecomDepts = ref<WecomDeptOption[]>([]);
|
||||
const wecomDeptsLoading = ref(false);
|
||||
const wecomDeptsLoaded = ref(false);
|
||||
/** 部门树扁平列表:一级不缩进,子级前加空格 */
|
||||
const deptOptions = ref<DeptOption[]>([]);
|
||||
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<number, typeof nodes>();
|
||||
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<typeof setTimeout> | 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) {
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<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 class="dept-combo">
|
||||
<input
|
||||
v-model="form.dept"
|
||||
class="field"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="deptLoading"
|
||||
:placeholder="deptLoading ? '加载部门…' : '请选择或输入部门'"
|
||||
@focus="onDeptFocus"
|
||||
@input="deptOpen = true"
|
||||
@blur="onDeptBlur"
|
||||
/>
|
||||
<span class="dept-combo-arrow" aria-hidden="true">▾</span>
|
||||
<ul v-show="deptOpen" class="dept-combo-list">
|
||||
<li v-if="!filteredDeptOptions.length" class="is-empty">无匹配部门</li>
|
||||
<li
|
||||
v-for="d in filteredDeptOptions"
|
||||
:key="d.id"
|
||||
:class="{ 'is-active': d.name === form.dept }"
|
||||
:style="{ paddingLeft: 10 + d.depth * 12 + 'px' }"
|
||||
@mousedown.prevent="pickDept(d)"
|
||||
>
|
||||
{{ d.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">岗位 <span class="req">*</span></label>
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user