人员岗位补负责人/线路维护,并同步后台人员、机器人回复与看板页改动。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
/**
|
||||
* 员工管理:对接主账 /inquiry/staffRole。
|
||||
* 企微 userid 可手工维护,也可点「同步企业微信」由主账 upsert(姓名/手机/部门)。
|
||||
* 浏览器禁止直连企微;岗位/线路仍手工改。
|
||||
* 浏览器禁止直连企微;岗位/负责人/线路仍手工改。
|
||||
* 产品·航线:负责人(是/否)+ 负责线路多选,选项照搬旧项目 admin-staff。
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
@@ -33,6 +34,8 @@ type StaffRow = {
|
||||
role: StaffRole;
|
||||
roleLabel: string;
|
||||
routes: string;
|
||||
/** 产品/航线是否负责人;销售为 null */
|
||||
isLeader: boolean | null;
|
||||
notifyEventException: boolean;
|
||||
createdAt: string;
|
||||
status: "active" | "disabled";
|
||||
@@ -48,6 +51,48 @@ const roleLabelMap: Record<StaffRole, string> = {
|
||||
land: "产品·陆运",
|
||||
};
|
||||
|
||||
/** 按岗位的负责线路选项(与原型 admin-staff.html ROUTES_BY_ROLE 一致) */
|
||||
const ROUTES_BY_ROLE: Record<Exclude<StaffRole, "sales">, string[]> = {
|
||||
air: [
|
||||
"CGO-KUL",
|
||||
"SZX-KUL",
|
||||
"HKG-KUL",
|
||||
"NKG-KUL",
|
||||
"HKG-DEL",
|
||||
"CGO-MAA",
|
||||
"CGO-BLR",
|
||||
"ZUH-CRK",
|
||||
"WNZ-CGK",
|
||||
"BKK",
|
||||
"外采-空运",
|
||||
"全线",
|
||||
],
|
||||
sea: ["海运全线"],
|
||||
land: ["中港/中亚/中欧", "国内长途/零担", "国内城配/拖车/打包", "东南亚"],
|
||||
};
|
||||
|
||||
const ALL_ROUTE_FILTER_OPTIONS = [
|
||||
...ROUTES_BY_ROLE.air,
|
||||
...ROUTES_BY_ROLE.sea,
|
||||
...ROUTES_BY_ROLE.land,
|
||||
];
|
||||
|
||||
function isProductOrAir(role: StaffRole) {
|
||||
return role === "air" || role === "sea" || role === "land";
|
||||
}
|
||||
|
||||
function parseRouteList(raw: string): string[] {
|
||||
if (!raw || raw === "-") return [];
|
||||
return raw
|
||||
.split(/[/·,、]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function joinRoutes(list: string[]) {
|
||||
return list.map((s) => s.trim()).filter(Boolean).join(",");
|
||||
}
|
||||
|
||||
const rows = ref<StaffRow[]>([]);
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
@@ -76,14 +121,32 @@ const form = reactive({
|
||||
phone: "",
|
||||
dept: "",
|
||||
role: "sales" as StaffRole,
|
||||
routes: "",
|
||||
/** 空字符串=未选;true/false 字符串对齐旧项目下拉 */
|
||||
isLeader: "" as "" | "true" | "false",
|
||||
/** 负责线路多选;销售岗位不展示 */
|
||||
routeList: [] as string[],
|
||||
notifyEventException: false,
|
||||
status: "active" as StaffRow["status"],
|
||||
});
|
||||
|
||||
/** 当前岗位可选线路;销售为空 */
|
||||
const routeOptions = computed(() => {
|
||||
if (!isProductOrAir(form.role)) return [] as string[];
|
||||
return ROUTES_BY_ROLE[form.role];
|
||||
});
|
||||
|
||||
const showLeaderAndRoutes = computed(() => isProductOrAir(form.role));
|
||||
|
||||
const filtered = computed(() => rows.value);
|
||||
|
||||
/**
|
||||
* 部门下拉过滤:点开/聚焦时展示全量树;只有用户继续打字才按关键字筛。
|
||||
* 为何:编辑时 form.dept 已是「武汉友通达」等完整名,若直接用它过滤会只剩 1 条。
|
||||
*/
|
||||
const deptFilterActive = ref(false);
|
||||
|
||||
const filteredDeptOptions = computed(() => {
|
||||
if (!deptFilterActive.value) return deptOptions.value;
|
||||
const q = (form.dept || "").trim().toLowerCase();
|
||||
if (!q) return deptOptions.value;
|
||||
return deptOptions.value.filter((d) => d.name.toLowerCase().includes(q));
|
||||
@@ -97,6 +160,9 @@ function colorFor(name: string) {
|
||||
|
||||
function mapRow(r: any): StaffRow {
|
||||
const role = (r.roleCode || "sales") as StaffRole;
|
||||
let isLeader: boolean | null = null;
|
||||
if (r.isLeader === 1 || r.isLeader === true) isLeader = true;
|
||||
else if (r.isLeader === 0 || r.isLeader === false) isLeader = false;
|
||||
return {
|
||||
id: String(r.id || ""),
|
||||
name: r.name || "",
|
||||
@@ -106,6 +172,7 @@ function mapRow(r: any): StaffRow {
|
||||
role: ["sales", "air", "sea", "land"].includes(role) ? role : "sales",
|
||||
roleLabel: r.roleLabel || roleLabelMap[role] || role,
|
||||
routes: r.routes || "-",
|
||||
isLeader,
|
||||
notifyEventException: r.notifyEventException === 1 || r.notifyEventException === true,
|
||||
createdAt: (r.createTime || "").toString().slice(0, 10) || "—",
|
||||
status: r.status === "disabled" ? "disabled" : "active",
|
||||
@@ -264,6 +331,7 @@ let deptBlurTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function pickDept(d: DeptOption) {
|
||||
form.dept = d.name;
|
||||
deptFilterActive.value = false;
|
||||
deptOpen.value = false;
|
||||
}
|
||||
|
||||
@@ -272,20 +340,45 @@ function onDeptFocus() {
|
||||
clearTimeout(deptBlurTimer);
|
||||
deptBlurTimer = null;
|
||||
}
|
||||
// 聚焦先看全量,避免当前部门名把列表滤成一项
|
||||
deptFilterActive.value = false;
|
||||
deptOpen.value = true;
|
||||
ensureDeptOptions();
|
||||
}
|
||||
|
||||
function onDeptInput() {
|
||||
deptFilterActive.value = true;
|
||||
deptOpen.value = true;
|
||||
}
|
||||
|
||||
function onDeptBlur() {
|
||||
deptBlurTimer = setTimeout(() => {
|
||||
deptOpen.value = false;
|
||||
deptFilterActive.value = false;
|
||||
}, 180);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => modalOpen.value,
|
||||
(open) => {
|
||||
if (!open) deptOpen.value = false;
|
||||
if (!open) {
|
||||
deptOpen.value = false;
|
||||
deptFilterActive.value = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/** 换岗位时:销售清空负责人/线路;产品航线只保留本岗可选线路 */
|
||||
watch(
|
||||
() => form.role,
|
||||
(role) => {
|
||||
if (!isProductOrAir(role)) {
|
||||
form.isLeader = "";
|
||||
form.routeList = [];
|
||||
return;
|
||||
}
|
||||
const valid = new Set(ROUTES_BY_ROLE[role]);
|
||||
form.routeList = form.routeList.filter((r) => valid.has(r));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -297,7 +390,8 @@ function openCreate() {
|
||||
phone: "",
|
||||
dept: "",
|
||||
role: "sales",
|
||||
routes: "",
|
||||
isLeader: "",
|
||||
routeList: [],
|
||||
notifyEventException: false,
|
||||
status: "active",
|
||||
});
|
||||
@@ -308,13 +402,16 @@ function openCreate() {
|
||||
|
||||
function openEdit(row: StaffRow) {
|
||||
editing.value = row;
|
||||
const valid = isProductOrAir(row.role) ? new Set(ROUTES_BY_ROLE[row.role]) : null;
|
||||
const routes = parseRouteList(row.routes).filter((r) => (valid ? valid.has(r) : true));
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
wecomId: row.wecomId,
|
||||
phone: row.phoneMasked === "未填写" ? "" : row.phoneMasked,
|
||||
dept: row.dept === "未分配" ? "" : row.dept,
|
||||
role: row.role,
|
||||
routes: row.routes === "-" ? "" : row.routes,
|
||||
isLeader: row.isLeader === true ? "true" : row.isLeader === false ? "false" : "",
|
||||
routeList: routes,
|
||||
notifyEventException: Boolean(row.notifyEventException),
|
||||
status: row.status,
|
||||
});
|
||||
@@ -328,6 +425,16 @@ async function saveStaff() {
|
||||
message.warning("请填写姓名与企业微信 ID");
|
||||
return;
|
||||
}
|
||||
if (isProductOrAir(form.role)) {
|
||||
if (form.isLeader !== "true" && form.isLeader !== "false") {
|
||||
message.warning("请选择是否负责人");
|
||||
return;
|
||||
}
|
||||
if (!form.routeList.length) {
|
||||
message.warning("请选择负责线路");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const body = {
|
||||
id: editing.value?.id,
|
||||
name: form.name.trim(),
|
||||
@@ -336,7 +443,8 @@ async function saveStaff() {
|
||||
departName: form.dept.trim() || undefined,
|
||||
roleCode: form.role,
|
||||
roleLabel: roleLabelMap[form.role],
|
||||
routes: form.routes.trim() || undefined,
|
||||
routes: isProductOrAir(form.role) ? joinRoutes(form.routeList) : undefined,
|
||||
isLeader: isProductOrAir(form.role) ? (form.isLeader === "true" ? 1 : 0) : null,
|
||||
notifyEventException: form.notifyEventException ? 1 : 0,
|
||||
status: form.status,
|
||||
};
|
||||
@@ -383,11 +491,11 @@ async function syncWecom() {
|
||||
}
|
||||
|
||||
function routeTags(routes: string) {
|
||||
if (!routes || routes === "-") return [];
|
||||
return routes
|
||||
.split(/[/·,、]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return parseRouteList(routes);
|
||||
}
|
||||
|
||||
function filterRouteOption(input: string, option: any) {
|
||||
return String(option?.label ?? "").includes(input);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -438,15 +546,7 @@ function routeTags(routes: string) {
|
||||
</select>
|
||||
<select v-model="filters.route">
|
||||
<option value="">负责线路</option>
|
||||
<option value="CGO-KUL">CGO-KUL</option>
|
||||
<option value="HKG-KUL">HKG-KUL</option>
|
||||
<option value="全线">全线</option>
|
||||
<option value="外采-空运">外采-空运</option>
|
||||
<option value="海运全线">海运全线</option>
|
||||
<option value="中港/中亚/中欧">中港/中亚/中欧</option>
|
||||
<option value="国内长途/零担">国内长途/零担</option>
|
||||
<option value="国内城配/拖车/打包">国内城配/拖车/打包</option>
|
||||
<option value="东南亚">东南亚</option>
|
||||
<option v-for="r in ALL_ROUTE_FILTER_OPTIONS" :key="r" :value="r">{{ r }}</option>
|
||||
</select>
|
||||
<select v-model="filters.status">
|
||||
<option value="">状态</option>
|
||||
@@ -558,7 +658,7 @@ function routeTags(routes: string) {
|
||||
:disabled="deptLoading"
|
||||
:placeholder="deptLoading ? '加载部门…' : '请选择或输入部门'"
|
||||
@focus="onDeptFocus"
|
||||
@input="deptOpen = true"
|
||||
@input="onDeptInput"
|
||||
@blur="onDeptBlur"
|
||||
/>
|
||||
<span class="dept-combo-arrow" aria-hidden="true">▾</span>
|
||||
@@ -585,9 +685,13 @@ function routeTags(routes: string) {
|
||||
<option value="land">产品·陆运</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">负责线路</label>
|
||||
<input v-model="form.routes" class="field" type="text" placeholder="销售可留空" />
|
||||
<div v-if="showLeaderAndRoutes" class="form-row">
|
||||
<label class="lbl">负责人 <span class="req">*</span></label>
|
||||
<select v-model="form.isLeader" class="field">
|
||||
<option value="">请选择是否负责人</option>
|
||||
<option value="true">是</option>
|
||||
<option value="false">否</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label class="lbl">消息提醒</label>
|
||||
@@ -611,6 +715,19 @@ function routeTags(routes: string) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showLeaderAndRoutes" class="form-row">
|
||||
<label class="lbl">负责线路 <span class="req">*</span></label>
|
||||
<a-select
|
||||
v-model:value="form.routeList"
|
||||
mode="multiple"
|
||||
allow-clear
|
||||
show-search
|
||||
class="staff-route-select"
|
||||
placeholder="请选择负责线路,可多选"
|
||||
:options="routeOptions.map((r) => ({ label: r, value: r }))"
|
||||
:filter-option="filterRouteOption"
|
||||
/>
|
||||
</div>
|
||||
</AdminModal>
|
||||
</template>
|
||||
|
||||
@@ -698,4 +815,16 @@ function routeTags(routes: string) {
|
||||
color: #94a3b8;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.staff-route-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.staff-route-select :deep(.ant-select-selector) {
|
||||
min-height: 34px;
|
||||
border-radius: 7px !important;
|
||||
border-color: #cbd5e1 !important;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user