账号支持只看本账号看板;模板重新解析改为保留原映射并多轮补缺。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-09-19 10:38:53 +08:00
co-authored by Cursor
parent 6cd6803147
commit 9bb69ae72e
19 changed files with 807 additions and 114 deletions
@@ -31,6 +31,10 @@ export type AccountRow = {
phoneMasked: string;
lastLogin: string;
status: 'active' | 'disabled';
/** 只看本账号看板数据 */
dashboardSelfOnly: boolean;
/** 内置超管:列表禁编、展示固定为超级管理员 */
isSuperAdmin: boolean;
};
const ROLE_TAG_CYCLE = ['tag-violet', 'tag-blue', 'tag-teal', 'tag-amber'];
@@ -39,6 +43,16 @@ export function roleTagFor(index: number) {
return ROLE_TAG_CYCLE[index % ROLE_TAG_CYCLE.length];
}
/** username=admin 或角色码/名含超级管理员 → 内置超管 */
export function isSuperAdminUser(u: { username?: string; roleLabel?: string; roleCode?: string }) {
const name = String(u.username || '').trim().toLowerCase();
if (name === 'admin') return true;
const code = String(u.roleCode || '').trim().toLowerCase();
if (code === 'admin') return true;
const label = String(u.roleLabel || '');
return label.includes('超级管理员');
}
/** Jeecg status:1 正常 → active;其余视为禁用。 */
export function mapUserToAccount(
u: any,
@@ -46,16 +60,20 @@ export function mapUserToAccount(
roleLabel = '未绑定角色',
roleTag = 'tag-blue',
): AccountRow {
const username = u.username || '';
const superAdmin = isSuperAdminUser({ username, roleLabel });
return {
id: String(u.id || ''),
username: u.username || '',
username,
name: u.realname || u.username || '',
wecomUserId: u.workNo || u.wecomId || u.thirdId || '',
roleId,
roleLabel,
roleTag,
roleLabel: superAdmin ? '超级管理员' : roleLabel,
roleTag: superAdmin ? 'tag-violet' : roleTag,
phoneMasked: u.phone || '未填写',
lastLogin: u.lastLoginTime || u.createTime || '—',
status: Number(u.status) === 1 ? 'active' : 'disabled',
dashboardSelfOnly: Boolean(u.dashboardSelfOnly === true || u.dashboardSelfOnly === 1),
isSuperAdmin: superAdmin,
};
}
@@ -25,7 +25,7 @@ const loading = ref(false);
const total = ref(0);
const pageNo = ref(1);
const pageSize = ref(10);
const roleOptions = ref<{ value: string; label: string; tag: string }[]>([]);
const roleOptions = ref<{ value: string; label: string; tag: string; roleCode?: string }[]>([]);
const filters = reactive({ keyword: "", status: "" as "" | "active" | "disabled" });
const modalOpen = ref(false);
const editing = ref<AccountRow | null>(null);
@@ -37,6 +37,8 @@ const form = reactive({
phone: "",
status: "active" as AccountRow["status"],
password: "",
/** 只看本账号看板数据(对齐原项目 dashboardSelfOnly) */
dashboardSelfOnly: false,
});
const modalTitle = computed(() => (editing.value ? "编辑账号" : "新建账号"));
@@ -52,9 +54,11 @@ async function loadRoles() {
value: String(r.id),
label: r.roleName || r.roleCode || r.id,
tag: roleTagFor(i),
roleCode: String(r.roleCode || ""),
}));
if (!form.roleId && roleOptions.value.length) {
form.roleId = roleOptions.value.find((r) => r.label.includes("运营"))?.value || roleOptions.value[0].value;
form.roleId =
roleOptions.value.find((r) => r.label.includes("运营"))?.value || roleOptions.value[0].value;
}
}
@@ -97,6 +101,23 @@ async function loadList() {
const hit = roleOptions.value.find((r) => r.value === roleId);
roleLabel = hit?.label || roleLabel;
roleTag = hit?.tag || roleTag;
// 用角色码辅助识别超管
if (hit?.roleCode === "admin" || (hit?.label || "").includes("超级管理员")) {
roleLabel = hit?.label || "超级管理员";
}
} else if (String(u.username || "").toLowerCase() === "admin") {
// 库里未绑角色时仍展示超管,并尝试选中 admin 角色以便保存时能补绑
const adminRole = roleOptions.value.find(
(r) => r.roleCode === "admin" || r.label.includes("超级管理员"),
);
if (adminRole) {
roleId = adminRole.value;
roleLabel = adminRole.label || "超级管理员";
roleTag = adminRole.tag;
} else {
roleLabel = "超级管理员";
roleTag = "tag-violet";
}
}
} catch {
/* 无角色仍展示账号 */
@@ -142,11 +163,16 @@ function openCreate() {
phone: "",
status: "active",
password: "",
dashboardSelfOnly: false,
});
modalOpen.value = true;
}
function openEdit(row: AccountRow) {
if (row.isSuperAdmin) {
message.warning("超级管理员不可在后台编辑");
return;
}
editing.value = row;
Object.assign(form, {
username: row.username,
@@ -156,11 +182,16 @@ function openEdit(row: AccountRow) {
phone: row.phoneMasked === "未填写" ? "" : row.phoneMasked,
status: row.status,
password: "",
dashboardSelfOnly: Boolean(row.dashboardSelfOnly),
});
modalOpen.value = true;
}
async function save() {
if (editing.value?.isSuperAdmin) {
message.warning("超级管理员不可在后台编辑");
return;
}
if (!form.username.trim() || !form.name.trim()) {
message.warning("请填写账号与姓名");
return;
@@ -185,6 +216,7 @@ async function save() {
phone: form.phone.trim() || undefined,
status: form.status === "active" ? 1 : 2,
selectedroles: form.roleId,
dashboardSelfOnly: form.dashboardSelfOnly ? 1 : 0,
};
if (!editing.value) payload.password = form.password;
try {
@@ -321,12 +353,25 @@ function removeAccount(row: AccountRow) {
</td>
<td>
<div class="account-row-actions">
<button class="link-action" type="button" @click="openEdit(row)">编辑</button>
<button class="link-action link-muted" type="button" @click="resetPassword(row)">
<button
v-if="!row.isSuperAdmin"
class="link-action"
type="button"
@click="openEdit(row)"
>
编辑
</button>
<span v-else class="account-locked" title="超级管理员不可在后台编辑">不可编辑</span>
<button
v-if="!row.isSuperAdmin"
class="link-action link-muted"
type="button"
@click="resetPassword(row)"
>
重置密码
</button>
<button
v-if="row.username !== 'admin'"
v-if="!row.isSuperAdmin"
class="link-action link-danger"
type="button"
@click="removeAccount(row)"
@@ -400,6 +445,14 @@ function removeAccount(row: AccountRow) {
<option v-for="r in roleOptions" :key="r.value" :value="r.value">{{ r.label }}</option>
</select>
</div>
<div class="form-row">
<label class="lbl">数据范围</label>
<label class="account-self-only">
<input v-model="form.dashboardSelfOnly" type="checkbox" />
<span>只看自己数据</span>
<em>勾选后,分析看板仅统计本账号企微归属的工单</em>
</label>
</div>
<div class="form-row">
<label class="lbl">状态</label>
<div class="status-toggle">
@@ -466,4 +519,26 @@ function removeAccount(row: AccountRow) {
align-items: center;
gap: 10px;
}
.account-locked {
color: #94a3b8;
font-size: 12px;
}
.account-self-only {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
font-size: 13px;
color: #0f172a;
cursor: pointer;
}
.account-self-only em {
flex: 1 1 100%;
font-style: normal;
font-size: 12px;
color: #64748b;
}
</style>
@@ -129,7 +129,7 @@ async function commitHours(row) {
<tr>
<th style="width: 88px">角色</th>
<th style="width: 160px" title="每条唯一;智能体用此码取超时小时">编码</th>
<th>运输类型</th>
<th>运输分类</th>
<th style="width: 140px">超时设置(小时)</th>
</tr>
</thead>
@@ -81,20 +81,15 @@ const parseDetailState = reactive({
mapping: emptyMappingSummary() as MappingParseSummary,
});
/** 未解析清单:优先 mappingGaps,其次 AI 自报项 */
/** 未解析清单:只认系统 mappingGaps;齐全(空数组)时不展示 AI 自报标签 */
const parseDetailUnparsed = computed(() => {
const m = parseDetailState.mapping;
const list: string[] = [];
for (const g of m.gaps || []) {
if (g) list.push(g);
if (Array.isArray(m.gaps)) {
return (m.gaps || []).filter(Boolean).map(String);
}
if (!list.length) {
for (const c of m.unmappedRequired || []) {
list.push(`字段未映射:${FIELD_LABELS[c] || c}`);
}
for (const lab of m.unmappedLabels || []) {
if (lab) list.push(`表标签未落点:${lab}`);
}
const list: string[] = [];
for (const c of m.unmappedRequired || []) {
list.push(`字段未映射:${FIELD_LABELS[c] || c}`);
}
return list;
});
@@ -377,22 +372,22 @@ function openEdit(row: TemplateRow) {
formOpen.value = true;
}
/** 运营可见的未解析项:优先 mappingGaps,其次标签/必填未映射 */
/** 运营可见的未解析项:只认系统对照清单 mappingGaps;齐全时角标为 0 */
function listMappingGaps(row: TemplateRow): string[] {
if (row.mappingGaps?.length) return [...row.mappingGaps];
const list: string[] = [];
for (const lab of row.unmappedLabels || []) {
if (lab) list.push(`表标签未落点:${lab}`);
// 主账已带 mappingGaps(含空数组)时,不再用 AI 自报 unmapped* 抬角标
if (Array.isArray(row.mappingGaps)) {
return row.mappingGaps.filter(Boolean).map(String);
}
const list: string[] = [];
for (const c of row.unmappedRequired || []) {
if (c) list.push(`字段未映射:${c}`);
}
return list;
}
/** 列表角标条数 */
/** 列表角标条数:优先主账 unparsedHintCount(与 mappingGaps 同口径) */
function unparsedCount(row: TemplateRow): number {
if (row.unparsedHintCount && row.unparsedHintCount > 0) return row.unparsedHintCount;
if (typeof row.unparsedHintCount === "number") return Math.max(0, row.unparsedHintCount);
return listMappingGaps(row).length;
}
@@ -439,6 +434,7 @@ async function toggleStatus(row: TemplateRow) {
/**
* 重新提交 AI 映射解析(已解析也可点;解析中禁用)。
* 未换文件:主账带 mergeExisting,回写时保留已有字段只补缺。
* 副作用:主账入队;列表进入解析中并轮询。
*/
async function reparseRow(row: TemplateRow) {
@@ -692,7 +688,11 @@ async function downloadPreviewOrOriginal() {
type="button"
class="link-action"
:disabled="row.parseStatus === '解析中'"
:title="row.parseStatus === '解析中' ? '解析中,请稍候' : '重新提交 AI 映射解析'"
:title="
row.parseStatus === '解析中'
? '解析中,请稍候'
: '重新解析:保留已有映射,只补缺;换文件请用编辑上传'
"
@click.stop="reparseRow(row)"
>
重新解析
@@ -91,11 +91,18 @@ export const listTemplateVersions = (templateId: string) =>
export const saveTemplateMapping = (versionId: string, mappingJson: string) =>
defHttp.put({ url: Api.mapping, data: { versionId, mappingJson } });
/** 非 GET 时 params 会进 JSON 体,@RequestParam 读不到,必须拼到查询串。 */
export const enableTemplate = (templateId: string, versionId: string) =>
defHttp.post({ url: Api.enable, params: { templateId, versionId } });
defHttp.post(
{ url: Api.enable, params: { templateId, versionId } },
{ joinParamsToUrl: true },
);
export const disableTemplate = (templateId: string) =>
defHttp.post({ url: Api.disable, params: { templateId } });
defHttp.post(
{ url: Api.disable, params: { templateId } },
{ joinParamsToUrl: true },
);
/**
* 删除模板逻辑头及全部版本;不可恢复。
@@ -109,7 +116,10 @@ export const deleteTemplate = (templateId: string) =>
);
export const setTemplateDefault = (templateId: string, isDefault = true) =>
defHttp.post({ url: Api.setDefault, params: { templateId, isDefault } });
defHttp.post(
{ url: Api.setDefault, params: { templateId, isDefault } },
{ joinParamsToUrl: true },
);
/** 只改名称/业务/关键词/默认标记,不换文件;走 JSON 体对接主账 @RequestBody */
export const editTemplateMeta = (data) => defHttp.put({ url: Api.editMeta, data });
@@ -119,7 +129,7 @@ export const editTemplateMeta = (data) => defHttp.put({ url: Api.editMeta, data
*/
export const reparseTemplate = (templateId: string) =>
defHttp.post(
{ url: Api.reparse, params: { id: templateId }, timeout: 60 * 1000 },
{ url: Api.reparse, params: { id: templateId, templateId }, timeout: 60 * 1000 },
{ joinParamsToUrl: true },
);