账号支持只看本账号看板;模板重新解析改为保留原映射并多轮补缺。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 },
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user