完善报价单填表与币种回填,并补齐账号末次登录、角色按钮权限与陆运全线匹配。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -43,6 +43,19 @@ export function roleTagFor(index: number) {
|
||||
return ROLE_TAG_CYCLE[index % ROLE_TAG_CYCLE.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后登录展示为 `yyyy-MM-dd HH:mm:ss`;无记录显示 —。
|
||||
* 兼容接口偶发 ISO(带 T)或已格式化字符串。
|
||||
*/
|
||||
export function formatLastLogin(raw: unknown): string {
|
||||
if (raw == null || raw === '') return '—';
|
||||
const s = String(raw).trim();
|
||||
if (!s) return '—';
|
||||
const m = s.match(/^(\d{4}-\d{2}-\d{2})[T\s](\d{2}:\d{2}:\d{2})/);
|
||||
if (m) return `${m[1]} ${m[2]}`;
|
||||
return s;
|
||||
}
|
||||
|
||||
/** username=admin 或角色码/名含超级管理员 → 内置超管 */
|
||||
export function isSuperAdminUser(u: { username?: string; roleLabel?: string; roleCode?: string }) {
|
||||
const name = String(u.username || '').trim().toLowerCase();
|
||||
@@ -71,7 +84,8 @@ export function mapUserToAccount(
|
||||
roleLabel: superAdmin ? '超级管理员' : roleLabel,
|
||||
roleTag: superAdmin ? 'tag-violet' : roleTag,
|
||||
phoneMasked: u.phone || '未填写',
|
||||
lastLogin: u.lastLoginTime || u.createTime || '—',
|
||||
// 只用真实最后登录;禁止回退 createTime(Jeecg 种子常为 2019,会误导成「最后登录」)
|
||||
lastLogin: formatLastLogin(u.lastLoginTime),
|
||||
status: Number(u.status) === 1 ? 'active' : 'disabled',
|
||||
dashboardSelfOnly: Boolean(u.dashboardSelfOnly === true || u.dashboardSelfOnly === 1),
|
||||
isSuperAdmin: superAdmin,
|
||||
|
||||
@@ -361,9 +361,8 @@ function removeAccount(row: AccountRow) {
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<span v-else class="account-locked" title="超级管理员不可在后台编辑">不可编辑</span>
|
||||
<!-- 超管只隐藏编辑/删除;重置密码仍可用。不展示「不可编辑」占位文案 -->
|
||||
<button
|
||||
v-if="!row.isSuperAdmin"
|
||||
class="link-action link-muted"
|
||||
type="button"
|
||||
@click="resetPassword(row)"
|
||||
@@ -520,11 +519,6 @@ function removeAccount(row: AccountRow) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.account-locked {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.account-self-only {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 角色管理:对接 Jeecg /sys/role 与菜单权限树。
|
||||
* 数据权限用线路标签写入角色 description,避免伪造第二套 RBAC。
|
||||
* 数据权限按 Excel 线路目录树勾选,写入角色 description 的 [dataPerms:…],不另造 RBAC。
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
@@ -15,6 +15,14 @@ import {
|
||||
saveRolePermission,
|
||||
type RoleRow,
|
||||
} from "/@/views/inquiry/role/role.api";
|
||||
import {
|
||||
DATA_PERM_TREE,
|
||||
SYSTEM_EXCEPTION_NOTIFY_KEY,
|
||||
SYSTEM_EXCEPTION_NOTIFY_TITLE,
|
||||
dataPermTitle,
|
||||
leafDataPermKeys,
|
||||
normalizeDataPermKeys,
|
||||
} from "/@/views/inquiry/shared/dataPermCatalog";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
|
||||
@@ -30,19 +38,12 @@ const form = reactive({
|
||||
status: "active" as RoleRow["status"],
|
||||
checkedKeys: [] as string[],
|
||||
dataPerms: [] as string[],
|
||||
/** 旧后台独立项:系统异常消息通知 */
|
||||
systemExceptionNotify: false,
|
||||
});
|
||||
|
||||
const dataOptions = [
|
||||
"空运·全部线路",
|
||||
"空运·CGO-KUL",
|
||||
"空运·HKG-KUL",
|
||||
"海运·全部线路",
|
||||
"海运·海运全线",
|
||||
"陆运·全部线路",
|
||||
"陆运·中港/中亚/中欧",
|
||||
"陆运·东南亚",
|
||||
"系统异常·消息通知",
|
||||
];
|
||||
const dataPermTree = DATA_PERM_TREE;
|
||||
const systemExceptionNotifyTitle = SYSTEM_EXCEPTION_NOTIFY_TITLE;
|
||||
|
||||
const modalTitle = computed(() => (editing.value ? "编辑角色" : "新增角色"));
|
||||
const modalOkText = computed(() => "保存");
|
||||
@@ -68,15 +69,29 @@ function parseDataPerms(desc: string | undefined): string[] {
|
||||
if (!desc) return [];
|
||||
const m = desc.match(/\[dataPerms:(.+?)\]/);
|
||||
if (!m) return [];
|
||||
return m[1]
|
||||
.split("|")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return normalizeDataPermKeys(
|
||||
m[1]
|
||||
.split("|")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
);
|
||||
}
|
||||
|
||||
function buildDescription(dataPerms: string[]) {
|
||||
if (!dataPerms.length) return "";
|
||||
return `[dataPerms:${dataPerms.join("|")}]`;
|
||||
function buildDescription(dataPerms: string[], systemExceptionNotify: boolean) {
|
||||
const keys = leafDataPermKeys(dataPerms);
|
||||
if (systemExceptionNotify && !keys.includes(SYSTEM_EXCEPTION_NOTIFY_KEY)) {
|
||||
keys.push(SYSTEM_EXCEPTION_NOTIFY_KEY);
|
||||
}
|
||||
if (!keys.length) return "";
|
||||
return `[dataPerms:${keys.join("|")}]`;
|
||||
}
|
||||
|
||||
function displayDataPerms(keys: string[]): string[] {
|
||||
return keys.map((k) => dataPermTitle(k));
|
||||
}
|
||||
|
||||
function routeDataPermsOnly(keys: string[]): string[] {
|
||||
return keys.filter((k) => k !== SYSTEM_EXCEPTION_NOTIFY_KEY);
|
||||
}
|
||||
|
||||
async function loadMenuTree() {
|
||||
@@ -143,18 +158,21 @@ function openCreate() {
|
||||
status: "active",
|
||||
checkedKeys: [],
|
||||
dataPerms: [],
|
||||
systemExceptionNotify: false,
|
||||
});
|
||||
modalOpen.value = true;
|
||||
}
|
||||
|
||||
async function openEdit(row: RoleRow) {
|
||||
editing.value = row;
|
||||
const allPerms = [...row.dataPerms];
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
roleCode: row.roleCode,
|
||||
status: row.status,
|
||||
checkedKeys: [] as string[],
|
||||
dataPerms: [...row.dataPerms],
|
||||
dataPerms: routeDataPermsOnly(allPerms),
|
||||
systemExceptionNotify: allPerms.includes(SYSTEM_EXCEPTION_NOTIFY_KEY),
|
||||
});
|
||||
try {
|
||||
const perms: any = await queryRolePermission({ roleId: row.id });
|
||||
@@ -179,7 +197,7 @@ async function save() {
|
||||
id: editing.value?.id,
|
||||
roleName: form.name.trim(),
|
||||
roleCode,
|
||||
description: buildDescription(form.dataPerms),
|
||||
description: buildDescription(form.dataPerms, form.systemExceptionNotify),
|
||||
},
|
||||
Boolean(editing.value),
|
||||
);
|
||||
@@ -214,6 +232,12 @@ function removeRole(row: RoleRow) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** a-tree 可能回传对象或数组;统一成叶子 key 列表 */
|
||||
function onDataPermCheck(checked: string[] | { checked: string[] }) {
|
||||
const keys = Array.isArray(checked) ? checked : checked?.checked || [];
|
||||
form.dataPerms = leafDataPermKeys(keys.map(String));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -252,7 +276,7 @@ function removeRole(row: RoleRow) {
|
||||
</td>
|
||||
<td>
|
||||
<div class="role-perm-summary">
|
||||
<span v-for="d in row.dataPerms" :key="d" class="tag">{{ d }}</span>
|
||||
<span v-for="d in displayDataPerms(row.dataPerms)" :key="d" class="tag">{{ d }}</span>
|
||||
<span v-if="!row.dataPerms.length" style="color: #94a3b8">—</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -306,14 +330,20 @@ function removeRole(row: RoleRow) {
|
||||
|
||||
<div class="admin-modal-field">
|
||||
<label class="admin-modal-label">数据权限</label>
|
||||
<a-select
|
||||
v-model:value="form.dataPerms"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
:options="dataOptions.map((d) => ({ value: d, label: d }))"
|
||||
placeholder="选择线路 / 通知范围"
|
||||
:max-tag-count="3"
|
||||
/>
|
||||
<div class="perm-tree-box">
|
||||
<a-tree
|
||||
:checkedKeys="form.dataPerms"
|
||||
checkable
|
||||
default-expand-all
|
||||
:tree-data="dataPermTree"
|
||||
:field-names="{ title: 'title', key: 'key', children: 'children' }"
|
||||
@check="onDataPermCheck"
|
||||
/>
|
||||
</div>
|
||||
<label class="notify-check">
|
||||
<input v-model="form.systemExceptionNotify" type="checkbox" />
|
||||
{{ systemExceptionNotifyTitle }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="admin-modal-field">
|
||||
@@ -340,4 +370,14 @@ function removeRole(row: RoleRow) {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notify-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: #0f172a;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 运营后台数据权限 / 员工负责线路 单一目录。
|
||||
*
|
||||
* 来源:《询价智能体运营后台数据权限下拉选项.xlsx》(2026-09)。
|
||||
* key = 写入 sys_role.description / inq_staff_role.routes 的稳定码;
|
||||
* title = 界面展示文案。
|
||||
*/
|
||||
|
||||
export type DataPermNode = {
|
||||
key: string;
|
||||
title: string;
|
||||
children?: DataPermNode[];
|
||||
};
|
||||
|
||||
/** 空运具体线路(不含全线) */
|
||||
export const AIR_ROUTE_ITEMS: { key: string; title: string }[] = [
|
||||
{ key: "CGO-KUL", title: "郑州-吉隆坡(CGO-KUL)" },
|
||||
{ key: "SZX-KUL", title: "深圳-吉隆坡(SZX-KUL)" },
|
||||
{ key: "HKG-KUL", title: "香港-吉隆坡(HKG-KUL)" },
|
||||
{ key: "NKG-KUL", title: "南京-吉隆坡(NKG-KUL)" },
|
||||
{ key: "HKG-DEL", title: "香港-新德里(HKG-DEL)" },
|
||||
{ key: "CGO-DEL", title: "郑州-新德里(CGO-DEL)" },
|
||||
{ key: "CGO-BLR", title: "郑州-班加罗尔(CGO-BLR)" },
|
||||
{ key: "CGO-MAA", title: "郑州-金奈(CGO-MAA)" },
|
||||
{ key: "CTU-MAA", title: "成都-金奈(CTU-MAA)" },
|
||||
{ key: "CKG-BOM", title: "重庆-孟买(CKG-BOM)" },
|
||||
{ key: "ZUH-CRK", title: "珠海-克拉克(ZUH-CRK)" },
|
||||
{ key: "SZX-CRK", title: "深圳-克拉克(SZX-CRK)" },
|
||||
{ key: "EHU-ORD", title: "鄂州-芝加哥(EHU-ORD)" },
|
||||
{ key: "WNZ-CGK", title: "温州-雅加达(WNZ-CGK)" },
|
||||
{ key: "国内-曼谷(BKK 外采)", title: "国内-曼谷(BKK 外采)" },
|
||||
{ key: "国外-国外(外采-空运)", title: "国外-国外(外采-空运)" },
|
||||
{ key: "其他线路和快递双清包税(外采-空运)", title: "其他线路和快递双清包税(外采-空运)" },
|
||||
];
|
||||
|
||||
export const LAND_ROUTE_ITEMS: { key: string; title: string }[] = [
|
||||
{ key: "中港/中亚/中欧", title: "中港/中亚/中欧" },
|
||||
{ key: "国内长途/零担", title: "国内长途/零担" },
|
||||
{ key: "国内城配/拖车/打包", title: "国内城配/拖车/打包" },
|
||||
{ key: "东南亚", title: "东南亚" },
|
||||
];
|
||||
|
||||
/** 角色弹窗数据权限树(父节点仅作分组,不写入 description;文案对齐旧后台「空运数据」) */
|
||||
export const DATA_PERM_TREE: DataPermNode[] = [
|
||||
{
|
||||
key: "group:air",
|
||||
title: "空运数据",
|
||||
children: [{ key: "空运全线", title: "空运全线" }, ...AIR_ROUTE_ITEMS],
|
||||
},
|
||||
{
|
||||
key: "group:sea",
|
||||
title: "海运数据",
|
||||
children: [{ key: "海运全线", title: "海运全线" }],
|
||||
},
|
||||
{
|
||||
key: "group:land",
|
||||
title: "陆运数据",
|
||||
children: [{ key: "陆运全线", title: "陆运全线" }, ...LAND_ROUTE_ITEMS],
|
||||
},
|
||||
];
|
||||
|
||||
/** 旧后台独立勾选项:系统异常消息通知(企微) */
|
||||
export const SYSTEM_EXCEPTION_NOTIFY_KEY = "系统异常·消息通知";
|
||||
|
||||
export const SYSTEM_EXCEPTION_NOTIFY_TITLE = "系统异常消息通知(企微)";
|
||||
|
||||
/** 员工岗位 → 可选负责线路 key(与角色数据权限同源) */
|
||||
export const ROUTES_BY_ROLE: Record<"air" | "sea" | "land", string[]> = {
|
||||
air: ["空运全线", ...AIR_ROUTE_ITEMS.map((x) => x.key)],
|
||||
sea: ["海运全线"],
|
||||
land: ["陆运全线", ...LAND_ROUTE_ITEMS.map((x) => x.key)],
|
||||
};
|
||||
|
||||
const TITLE_BY_KEY: Record<string, string> = (() => {
|
||||
const m: Record<string, string> = {
|
||||
空运全线: "空运全线",
|
||||
海运全线: "海运全线",
|
||||
陆运全线: "陆运全线",
|
||||
[SYSTEM_EXCEPTION_NOTIFY_KEY]: SYSTEM_EXCEPTION_NOTIFY_TITLE,
|
||||
};
|
||||
for (const x of AIR_ROUTE_ITEMS) m[x.key] = x.title;
|
||||
for (const x of LAND_ROUTE_ITEMS) m[x.key] = x.title;
|
||||
return m;
|
||||
})();
|
||||
|
||||
/** 已知稳定码集合(不含分组父节点) */
|
||||
export const KNOWN_DATA_PERM_KEYS = new Set(Object.keys(TITLE_BY_KEY));
|
||||
|
||||
/**
|
||||
* 旧写法 → 新稳定码。
|
||||
* 无法识别时返回 trim 后原文(可能仍展示,但不进勾选树)。
|
||||
*/
|
||||
const ALIAS_TO_KEY: Record<string, string> = {
|
||||
空运全线: "空运全线",
|
||||
空运全部线路: "空运全线",
|
||||
"空运·全部线路": "空运全线",
|
||||
"空运·全线": "空运全线",
|
||||
全线: "空运全线",
|
||||
海运全线: "海运全线",
|
||||
"海运·全部线路": "海运全线",
|
||||
"海运·海运全线": "海运全线",
|
||||
陆运全线: "陆运全线",
|
||||
"陆运·全部线路": "陆运全线",
|
||||
"陆运·中港/中亚/中欧": "中港/中亚/中欧",
|
||||
"陆运·国内长途/零担": "国内长途/零担",
|
||||
"陆运·国内城配/拖车/打包": "国内城配/拖车/打包",
|
||||
"陆运·东南亚": "东南亚",
|
||||
BKK: "国内-曼谷(BKK 外采)",
|
||||
"外采-空运": "国外-国外(外采-空运)",
|
||||
[SYSTEM_EXCEPTION_NOTIFY_KEY]: SYSTEM_EXCEPTION_NOTIFY_KEY,
|
||||
系统异常消息通知: SYSTEM_EXCEPTION_NOTIFY_KEY,
|
||||
"系统异常消息通知(企微)": SYSTEM_EXCEPTION_NOTIFY_KEY,
|
||||
};
|
||||
|
||||
for (const item of AIR_ROUTE_ITEMS) {
|
||||
ALIAS_TO_KEY[item.key] = item.key;
|
||||
ALIAS_TO_KEY[item.title] = item.key;
|
||||
ALIAS_TO_KEY[`空运·${item.key}`] = item.key;
|
||||
}
|
||||
for (const item of LAND_ROUTE_ITEMS) {
|
||||
ALIAS_TO_KEY[item.key] = item.key;
|
||||
ALIAS_TO_KEY[`陆运·${item.key}`] = item.key;
|
||||
}
|
||||
|
||||
export function normalizeDataPermKey(raw: string): string {
|
||||
const s = String(raw || "").trim();
|
||||
if (!s) return "";
|
||||
if (ALIAS_TO_KEY[s]) return ALIAS_TO_KEY[s];
|
||||
// 空运·XXX / 陆运·XXX 兜底剥前缀
|
||||
const m = s.match(/^(?:空运|海运|陆运)[·•](.+)$/);
|
||||
if (m) {
|
||||
const inner = m[1].trim();
|
||||
if (ALIAS_TO_KEY[inner]) return ALIAS_TO_KEY[inner];
|
||||
if (inner === "全部线路" || inner === "全线") {
|
||||
if (s.startsWith("空运")) return "空运全线";
|
||||
if (s.startsWith("海运")) return "海运全线";
|
||||
if (s.startsWith("陆运")) return "陆运全线";
|
||||
}
|
||||
if (KNOWN_DATA_PERM_KEYS.has(inner)) return inner;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 批量归一;去重保序;只保留已知码(角色勾选回显用) */
|
||||
export function normalizeDataPermKeys(rawList: string[]): string[] {
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const raw of rawList || []) {
|
||||
const k = normalizeDataPermKey(raw);
|
||||
if (!k || !KNOWN_DATA_PERM_KEYS.has(k) || seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
out.push(k);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 员工 routes:归一后保留已知码;未知旧值也保留以免丢数据 */
|
||||
export function normalizeStaffRouteKeys(rawList: string[]): string[] {
|
||||
const out: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const raw of rawList || []) {
|
||||
const k = normalizeDataPermKey(raw);
|
||||
if (!k || seen.has(k)) continue;
|
||||
seen.add(k);
|
||||
out.push(k);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function dataPermTitle(key: string): string {
|
||||
const k = normalizeDataPermKey(key);
|
||||
return TITLE_BY_KEY[k] || key;
|
||||
}
|
||||
|
||||
/** Ant Design Tree 用:过滤掉分组父 key,只交叶子 */
|
||||
export function leafDataPermKeys(checked: string[]): string[] {
|
||||
return (checked || []).filter((k) => KNOWN_DATA_PERM_KEYS.has(k));
|
||||
}
|
||||
@@ -20,6 +20,11 @@ import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
import AdminPagination from "/@/views/inquiry/components/AdminPagination.vue";
|
||||
import { joinRoutes, parseRouteList } from "/@/views/inquiry/staffRole/staffRoutes";
|
||||
import {
|
||||
ROUTES_BY_ROLE,
|
||||
dataPermTitle,
|
||||
normalizeStaffRouteKeys,
|
||||
} from "/@/views/inquiry/shared/dataPermCatalog";
|
||||
|
||||
type StaffRole = "sales" | "air" | "sea" | "land";
|
||||
|
||||
@@ -52,26 +57,7 @@ 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: ["中港/中亚/中欧", "国内长途/零担", "国内城配/拖车/打包", "东南亚"],
|
||||
};
|
||||
|
||||
/** 负责线路与角色数据权限同源(Excel《数据权限下拉选项》) */
|
||||
const ALL_ROUTE_FILTER_OPTIONS = [
|
||||
...ROUTES_BY_ROLE.air,
|
||||
...ROUTES_BY_ROLE.sea,
|
||||
@@ -120,8 +106,11 @@ const form = reactive({
|
||||
|
||||
/** 当前岗位可选线路;销售为空 */
|
||||
const routeOptions = computed(() => {
|
||||
if (!isProductOrAir(form.role)) return [] as string[];
|
||||
return ROUTES_BY_ROLE[form.role];
|
||||
if (!isProductOrAir(form.role)) return [] as { label: string; value: string }[];
|
||||
return ROUTES_BY_ROLE[form.role].map((key) => ({
|
||||
value: key,
|
||||
label: dataPermTitle(key),
|
||||
}));
|
||||
});
|
||||
|
||||
const showLeaderAndRoutes = computed(() => isProductOrAir(form.role));
|
||||
@@ -367,7 +356,7 @@ watch(
|
||||
return;
|
||||
}
|
||||
const valid = new Set(ROUTES_BY_ROLE[role]);
|
||||
form.routeList = form.routeList.filter((r) => valid.has(r));
|
||||
form.routeList = normalizeStaffRouteKeys(form.routeList).filter((r) => valid.has(r));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -392,7 +381,10 @@ 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));
|
||||
// 旧 routes(全线/BKK/外采-空运 等)归一到 Excel 码后再过滤本岗选项
|
||||
const routes = normalizeStaffRouteKeys(parseRouteList(row.routes)).filter((r) =>
|
||||
valid ? valid.has(r) : true,
|
||||
);
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
wecomId: row.wecomId,
|
||||
@@ -432,7 +424,9 @@ async function saveStaff() {
|
||||
departName: form.dept.trim() || undefined,
|
||||
roleCode: form.role,
|
||||
roleLabel: roleLabelMap[form.role],
|
||||
routes: isProductOrAir(form.role) ? joinRoutes(form.routeList) : undefined,
|
||||
routes: isProductOrAir(form.role)
|
||||
? joinRoutes(normalizeStaffRouteKeys(form.routeList))
|
||||
: undefined,
|
||||
isLeader: isProductOrAir(form.role) ? (form.isLeader === "true" ? 1 : 0) : null,
|
||||
notifyEventException: form.notifyEventException ? 1 : 0,
|
||||
status: form.status,
|
||||
@@ -480,7 +474,7 @@ async function syncWecom() {
|
||||
}
|
||||
|
||||
function routeTags(routes: string) {
|
||||
return parseRouteList(routes);
|
||||
return normalizeStaffRouteKeys(parseRouteList(routes)).map((k) => dataPermTitle(k));
|
||||
}
|
||||
|
||||
function filterRouteOption(input: string, option: any) {
|
||||
@@ -535,7 +529,7 @@ function filterRouteOption(input: string, option: any) {
|
||||
</select>
|
||||
<select v-model="filters.route">
|
||||
<option value="">负责线路</option>
|
||||
<option v-for="r in ALL_ROUTE_FILTER_OPTIONS" :key="r" :value="r">{{ r }}</option>
|
||||
<option v-for="r in ALL_ROUTE_FILTER_OPTIONS" :key="r" :value="r">{{ dataPermTitle(r) }}</option>
|
||||
</select>
|
||||
<select v-model="filters.status">
|
||||
<option value="">状态</option>
|
||||
@@ -713,7 +707,7 @@ function filterRouteOption(input: string, option: any) {
|
||||
show-search
|
||||
class="staff-route-select"
|
||||
placeholder="请选择负责线路,可多选"
|
||||
:options="routeOptions.map((r) => ({ label: r, value: r }))"
|
||||
:options="routeOptions"
|
||||
:filter-option="filterRouteOption"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 员工管理「负责线路」拆合。
|
||||
*
|
||||
* 只按逗号/中文逗号/顿号拆整词,禁止把「中港/中亚/中欧」「国内长途/零担」按斜杠切开。
|
||||
* 陆运没有「陆运全线」快捷项。
|
||||
* 选项与角色数据权限同源(含空运全线 / 海运全线 / 陆运全线)。
|
||||
*/
|
||||
|
||||
/** 把库里的逗号串还原成多选项。空或占位「-」当没有。 */
|
||||
|
||||
@@ -434,7 +434,7 @@ async function toggleStatus(row: TemplateRow) {
|
||||
|
||||
/**
|
||||
* 重新提交 AI 映射解析(已解析也可点;解析中禁用)。
|
||||
* 未换文件:主账带 mergeExisting,回写时保留已有字段只补缺。
|
||||
* 未换文件:对齐标签的旧坐标保留,对不上的改用新坐标;缺的字段仍补上。
|
||||
* 副作用:主账入队;列表进入解析中并轮询。
|
||||
*/
|
||||
async function reparseRow(row: TemplateRow) {
|
||||
@@ -691,7 +691,7 @@ async function downloadPreviewOrOriginal() {
|
||||
:title="
|
||||
row.parseStatus === '解析中'
|
||||
? '解析中,请稍候'
|
||||
: '重新解析:保留已有映射,只补缺;换文件请用编辑上传'
|
||||
: '重新解析:对齐的保留,对不上的改坐标;换文件请用编辑上传'
|
||||
"
|
||||
@click.stop="reparseRow(row)"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user