后台按查看权限收紧菜单与「只看自己」数据范围;跨境整车报价填表更稳,协商中顺延不再误报销售超时。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -24,10 +24,12 @@ export const getMenuList = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换成vue3菜单
|
||||
* 切换成 vue3 菜单 —— 询价库永久禁用。
|
||||
* <p>
|
||||
* Jeecg 原逻辑会把 sys_permission rename 成 v2 再切 v3 表;测服 inquiry_robot
|
||||
* 菜单已是 Vue3 且常无 sys_permission_v3,执行会弄挂侧栏。前端不再发切表请求。
|
||||
*/
|
||||
export const switchVue3Menu = () => {
|
||||
return new Promise((resolve) => {
|
||||
defHttp.get({ url: Api.SwitchVue3Menu });
|
||||
});
|
||||
console.warn('[ytd] switchVue3Menu disabled: inquiry_robot menus are already Vue3');
|
||||
return Promise.resolve({ success: false, message: 'switchVue3Menu disabled' });
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
import { useRoute, useRouter, RouterView } from 'vue-router';
|
||||
import { message, ConfigProvider } from 'ant-design-vue';
|
||||
import { useUserStore } from '/@/store/modules/user';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import ProtoIcon from '/@/views/inquiry/components/ProtoIcon.vue';
|
||||
import AdminModal from '/@/views/inquiry/components/AdminModal.vue';
|
||||
import { canSeeInquiryPage } from '/@/views/inquiry/shared/inquiryPagePerms';
|
||||
import '/@/views/inquiry/styles/admin-prototype.css';
|
||||
import '/@/views/inquiry/styles/admin-fidelity.css';
|
||||
import '/@/views/inquiry/styles/dashboard-prototype.css';
|
||||
@@ -74,12 +76,26 @@
|
||||
components: { RouterView, ProtoIcon, AdminModal, ConfigProvider },
|
||||
setup() {
|
||||
const userStore = useUserStore();
|
||||
const { hasPermission } = usePermission();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userMenuOpen = ref(false);
|
||||
const profileOpen = ref(false);
|
||||
const passwordOpen = ref(false);
|
||||
|
||||
/**
|
||||
* 侧栏按查看权限码过滤;无权限的分组整组隐藏。
|
||||
* 不再渲染写死全量 navGroups,避免「没权限仍看见菜单」。
|
||||
*/
|
||||
const visibleNavGroups = computed(() =>
|
||||
navGroups
|
||||
.map((g) => ({
|
||||
...g,
|
||||
items: g.items.filter((item) => canSeeInquiryPage(item.key, hasPermission)),
|
||||
}))
|
||||
.filter((g) => g.items.length > 0),
|
||||
);
|
||||
|
||||
const pageTitle = computed(() => String(route.meta?.title || '友通达智能体运营后台'));
|
||||
const crumb = computed(() => (route.meta?.crumb ? String(route.meta.crumb) : ''));
|
||||
|
||||
@@ -167,11 +183,15 @@
|
||||
if (!t.closest('.admin-user-menu')) closeUserMenu();
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('click', onDocClick));
|
||||
onUnmounted(() => document.removeEventListener('click', onDocClick));
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onDocClick);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onDocClick);
|
||||
});
|
||||
|
||||
return {
|
||||
navGroups,
|
||||
navGroups: visibleNavGroups,
|
||||
themeToken,
|
||||
route,
|
||||
pageTitle,
|
||||
|
||||
@@ -21,11 +21,12 @@ import { getMenuList } from '/@/api/sys/menu';
|
||||
import { getPermCode } from '/@/api/sys/user';
|
||||
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
import { INQUIRY_PAGE_VIEW_PERMS } from '/@/views/inquiry/shared/inquiryPagePerms';
|
||||
|
||||
/**
|
||||
* 询价原型侧栏路径是写死的;Jeecg BACK 模式只注册后台菜单里的路由。
|
||||
* 菜单种子缺失/组件路径不对时,点侧栏会进 404(本机与线上相同)。
|
||||
* 这里按侧栏清单补挂手写页,不依赖种子是否已导入。
|
||||
* 询价手写页:纠正菜单种子里的组件路径/标题。
|
||||
* 禁止把未授权页强行补进路由——否则 backMenuList 会膨胀成全量侧栏,无权限也看得见。
|
||||
* 仅当「后台菜单已有该路径」或「用户已有对应查看权限码」时才补挂缺失叶子。
|
||||
*/
|
||||
type InquiryLeaf = {
|
||||
path: string;
|
||||
@@ -78,16 +79,65 @@ function findParent(list: AppRouteRecordRaw[] = [], parentPath: string): AppRout
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureInquiryHandwrittenRoutes(routeList: AppRouteRecordRaw[]) {
|
||||
const groups: Record<string, { title: string; redirect: string; leaves: InquiryLeaf[] }> = {
|
||||
/**
|
||||
* 永久禁止 Jeecg「Vue2 菜单表」切表弹窗。
|
||||
* <p>
|
||||
* 检测条件:顶层既没有 component===layouts/default/index,图标也不含冒号。
|
||||
* 非管理员若只分到缺标记的询价菜单,会被误判;点确认会 rename sys_permission,测服无 v3 表会弄挂侧栏。
|
||||
* 本函数恒为 false,禁止再恢复弹窗逻辑。
|
||||
*/
|
||||
function shouldPromptVue2MenuSwitch(_routeList: AppRouteRecordRaw[]): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 给菜单补齐 Vue3 标记,避免未来误恢复 Jeecg 检测时再次弹窗。
|
||||
* 副作用:就地改 component / meta.icon;不改业务权限。
|
||||
*/
|
||||
function normalizeVue3MenuMarkers(routeList: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
|
||||
for (const item of routeList || []) {
|
||||
const comp = String((item as any).component || '');
|
||||
// Jeecg 检测认 layouts/default/index;LAYOUT 也会被 transform 成询价壳
|
||||
if (!comp || comp.toUpperCase() === 'LAYOUT') {
|
||||
(item as any).component = 'layouts/default/index';
|
||||
}
|
||||
item.meta = item.meta || ({} as any);
|
||||
const icon = String((item.meta as any).icon || '');
|
||||
if (!icon) {
|
||||
(item.meta as any).icon = 'ant-design:menu-outlined';
|
||||
} else if (!icon.includes(':')) {
|
||||
(item.meta as any).icon = `ant-design:${icon}`;
|
||||
}
|
||||
}
|
||||
return routeList;
|
||||
}
|
||||
|
||||
function ensureInquiryHandwrittenRoutes(routeList: AppRouteRecordRaw[], permCodes: Array<string | number> = []) {
|
||||
const codeSet = new Set((permCodes || []).map(String));
|
||||
const canAddLeaf = (leaf: InquiryLeaf) => {
|
||||
// 后台菜单已下发该路径:只纠正组件,不算越权补挂
|
||||
if (findRouteByPath(routeList, leaf.path)) {
|
||||
return true;
|
||||
}
|
||||
const need = INQUIRY_PAGE_VIEW_PERMS[leaf.path];
|
||||
// 无权限码表时不补挂(宁可少菜单,也不把未授权页塞进侧栏)
|
||||
if (!need) {
|
||||
return false;
|
||||
}
|
||||
return codeSet.has(need);
|
||||
};
|
||||
|
||||
const groups: Record<string, { title: string; redirect: string; icon: string; leaves: InquiryLeaf[] }> = {
|
||||
'/inquiry': {
|
||||
title: '询价',
|
||||
redirect: '/inquiry/dashboard',
|
||||
icon: 'ant-design:robot-outlined',
|
||||
leaves: INQUIRY_LEAVES.filter((l) => l.path.startsWith('/inquiry/') && !l.path.startsWith('/inquiry-sys')),
|
||||
},
|
||||
'/inquiry-sys': {
|
||||
title: '询价系统配置',
|
||||
redirect: '/inquiry-sys/system-setting',
|
||||
icon: 'ant-design:setting-outlined',
|
||||
leaves: INQUIRY_LEAVES.filter((l) => l.path.startsWith('/inquiry-sys/')),
|
||||
},
|
||||
};
|
||||
@@ -105,6 +155,10 @@ function ensureInquiryHandwrittenRoutes(routeList: AppRouteRecordRaw[]) {
|
||||
existing.meta = { ...(existing.meta || {}), title: leaf.title, ignoreKeepAlive: true };
|
||||
continue;
|
||||
}
|
||||
// 角色未授权且无对应查看权限:禁止补挂(根因:曾把 8 个询价页全塞进 backMenuList)
|
||||
if (!canAddLeaf(leaf)) {
|
||||
continue;
|
||||
}
|
||||
const node: AppRouteRecordRaw = {
|
||||
path: leaf.path,
|
||||
name: leaf.name,
|
||||
@@ -119,12 +173,23 @@ function ensureInquiryHandwrittenRoutes(routeList: AppRouteRecordRaw[]) {
|
||||
parent = {
|
||||
path: parentPath,
|
||||
name: parentPath.replace(/\//g, '-').replace(/^-/, '') || 'inquiry-root',
|
||||
component: 'LAYOUT' as any,
|
||||
// 必须用 layouts/default/index + 带冒号图标,满足 Jeecg Vue3 菜单检测(实际仍映射到询价壳)
|
||||
component: 'layouts/default/index' as any,
|
||||
redirect: group.redirect,
|
||||
meta: { title: group.title },
|
||||
meta: { title: group.title, icon: group.icon },
|
||||
children: [],
|
||||
};
|
||||
routeList.push(parent);
|
||||
} else {
|
||||
// 已有父级也补标记,防止非管理员菜单缺图标被误判 Vue2
|
||||
const pComp = String((parent as any).component || '');
|
||||
if (!pComp || pComp.toUpperCase() === 'LAYOUT') {
|
||||
(parent as any).component = 'layouts/default/index';
|
||||
}
|
||||
parent.meta = {
|
||||
...(parent.meta || {}),
|
||||
icon: (parent.meta as any)?.icon || group.icon,
|
||||
};
|
||||
}
|
||||
parent.children = parent.children || [];
|
||||
if (leaf.path === group.redirect) {
|
||||
@@ -137,6 +202,49 @@ function ensureInquiryHandwrittenRoutes(routeList: AppRouteRecordRaw[]) {
|
||||
return routeList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 去掉无查看权限的询价叶子(角色树勾了菜单但没勾按钮、或历史脏数据)。
|
||||
* 父节点若子级删空则一并去掉,避免侧栏空分组。
|
||||
*/
|
||||
function pruneInquiryLeavesWithoutViewPerm(
|
||||
routeList: AppRouteRecordRaw[],
|
||||
permCodes: Array<string | number> = [],
|
||||
): AppRouteRecordRaw[] {
|
||||
const codeSet = new Set((permCodes || []).map(String));
|
||||
// 权限码尚未就绪时禁止裁剪:否则会把 getMenuList 结果清空 → 重登 404
|
||||
if (codeSet.size === 0) {
|
||||
return routeList;
|
||||
}
|
||||
const norm = (p: string) => {
|
||||
const s = String(p || '');
|
||||
return s.startsWith('/') ? s : `/${s}`;
|
||||
};
|
||||
const walk = (list: AppRouteRecordRaw[] = []): AppRouteRecordRaw[] => {
|
||||
const out: AppRouteRecordRaw[] = [];
|
||||
for (const item of list) {
|
||||
const path = norm(item.path || '');
|
||||
const need = INQUIRY_PAGE_VIEW_PERMS[path];
|
||||
if (need && !codeSet.has(need)) {
|
||||
continue;
|
||||
}
|
||||
const next = { ...item };
|
||||
if (next.children?.length) {
|
||||
next.children = walk(next.children as AppRouteRecordRaw[]);
|
||||
// 询价分组下无子页则不保留空壳
|
||||
if (
|
||||
(path === '/inquiry' || path === '/inquiry-sys') &&
|
||||
(!next.children || next.children.length === 0)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(next);
|
||||
}
|
||||
return out;
|
||||
};
|
||||
return walk(routeList);
|
||||
}
|
||||
|
||||
|
||||
// 系统权限
|
||||
interface AuthItem {
|
||||
@@ -323,21 +431,27 @@ export const usePermissionStore = defineStore({
|
||||
// 这个函数可能只需要执行一次,并且实际的项目可以在正确的时间被放置
|
||||
let routeList: AppRouteRecordRaw[] = [];
|
||||
try {
|
||||
this.changePermissionCode();
|
||||
// 必须 await:logout 会清空 permCodeList;若此处不候等,后面 prune 会在空权限码下
|
||||
// 把询价叶子全删掉 → 侧栏仍能靠稍后到的权限码显示,点进去却是 404。
|
||||
await this.changePermissionCode();
|
||||
routeList = (await getMenuList()) as AppRouteRecordRaw[];
|
||||
// Jeecg 原逻辑:顶层没有 layouts/default/index 且图标无冒号 → 弹窗并调用 /sys/switchVue3Menu
|
||||
// (把 sys_permission rename 成 v2,再把 sys_permission_v3 rename 成正式表)。
|
||||
// 询价库 inquiry_robot 菜单种子已是 Vue3,且测服没有 sys_permission_v3;点确认会弄挂侧栏。
|
||||
// 因此禁用自动切表弹窗;后面仍会 ensureInquiryHandwrittenRoutes 补齐询价页。
|
||||
// update-begin----author:ytd---date:20261007------for: 禁用危险的 Vue2→Vue3 菜单切表提示 ---
|
||||
// update-end----author:ytd---date:20261007------for: 禁用危险的 Vue2→Vue3 菜单切表提示 ---
|
||||
// 永久禁止 Vue2→Vue3 切表弹窗(shouldPromptVue2MenuSwitch 恒 false)
|
||||
if (shouldPromptVue2MenuSwitch(routeList)) {
|
||||
// 不可达;保留分支防止以后误合并 Jeecg 原弹窗代码
|
||||
}
|
||||
// 非管理员菜单常缺 Vue3 标记 → 误弹「菜单表是 Vue2」;先规范化再往下走
|
||||
routeList = normalizeVue3MenuMarkers(routeList || []);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// 菜单接口失败时仍补挂询价手写页,避免侧栏点进去全是 404
|
||||
routeList = Array.isArray(routeList) ? routeList : [];
|
||||
}
|
||||
// 无论菜单接口成功与否,都按侧栏清单补齐手写路由
|
||||
routeList = ensureInquiryHandwrittenRoutes(routeList || []);
|
||||
// 只纠正/补挂「已有菜单或已有查看权限」的手写页,禁止灌入未授权路由
|
||||
const permCodes = this.getPermCodeList || [];
|
||||
routeList = ensureInquiryHandwrittenRoutes(routeList || [], permCodes);
|
||||
routeList = pruneInquiryLeavesWithoutViewPerm(routeList, permCodes);
|
||||
// 补挂后再标一次(ensure 新建的父级也要带 Vue3 标记)
|
||||
routeList = normalizeVue3MenuMarkers(routeList);
|
||||
// 组件地址前加斜杠处理 author: lsq date:2021-09-08
|
||||
routeList = addSlashToRouteComponent(routeList);
|
||||
// 动态引入组件
|
||||
|
||||
@@ -19,6 +19,12 @@ import {
|
||||
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 { usePermission } from "/@/hooks/web/usePermission";
|
||||
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
/** 账号新建/编辑/删除/重置密码均属 edit 权限 */
|
||||
const canEditAccount = () => hasPermission(InquiryBtnPerm.accountEdit);
|
||||
|
||||
const rows = ref<AccountRow[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -299,7 +305,7 @@ function removeAccount(row: AccountRow) {
|
||||
<button type="button" class="btn btn-secondary" disabled title="导出待后续开放">
|
||||
<ProtoIcon name="download" :size="14" />导出
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="openCreate">
|
||||
<button v-if="canEditAccount()" type="button" class="btn btn-primary" @click="openCreate">
|
||||
<ProtoIcon name="plus" :size="14" />新建账号
|
||||
</button>
|
||||
</div>
|
||||
@@ -369,7 +375,7 @@ function removeAccount(row: AccountRow) {
|
||||
<td>
|
||||
<div class="account-row-actions">
|
||||
<button
|
||||
v-if="!row.isSuperAdmin"
|
||||
v-if="canEditAccount() && !row.isSuperAdmin"
|
||||
class="link-action"
|
||||
type="button"
|
||||
@click="openEdit(row)"
|
||||
@@ -378,6 +384,7 @@ function removeAccount(row: AccountRow) {
|
||||
</button>
|
||||
<!-- 超管只隐藏编辑/删除;重置密码仍可用。不展示「不可编辑」占位文案 -->
|
||||
<button
|
||||
v-if="canEditAccount()"
|
||||
class="link-action link-muted"
|
||||
type="button"
|
||||
@click="resetPassword(row)"
|
||||
@@ -385,7 +392,7 @@ function removeAccount(row: AccountRow) {
|
||||
重置密码
|
||||
</button>
|
||||
<button
|
||||
v-if="!row.isSuperAdmin"
|
||||
v-if="canEditAccount() && !row.isSuperAdmin"
|
||||
class="link-action link-danger"
|
||||
type="button"
|
||||
@click="removeAccount(row)"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 机器人回复语:对接主账 /inquiry/botReply。
|
||||
* 分类壳用本地元数据;条目内容全部来自库。
|
||||
@@ -19,6 +19,13 @@ import {
|
||||
} from "/@/views/inquiry/botReply/replyCategories";
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
import { usePermission } from "/@/hooks/web/usePermission";
|
||||
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canAdd = () => hasPermission(InquiryBtnPerm.replyAdd);
|
||||
const canEdit = () => hasPermission(InquiryBtnPerm.replyEdit);
|
||||
const canDisable = () => hasPermission(InquiryBtnPerm.replyDisable);
|
||||
|
||||
const categories = ref<ReplyCategory[]>(emptyCategories());
|
||||
const loading = ref(false);
|
||||
@@ -152,7 +159,7 @@ async function save() {
|
||||
|
||||
<template>
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<button type="button" class="btn btn-primary" @click="openCreate()">
|
||||
<button v-if="canAdd()" type="button" class="btn btn-primary" @click="openCreate()">
|
||||
<ProtoIcon name="plus" :size="16" />添加话术
|
||||
</button>
|
||||
</Teleport>
|
||||
@@ -177,13 +184,21 @@ async function save() {
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0">
|
||||
<button
|
||||
v-if="canDisable()"
|
||||
type="button"
|
||||
class="switch"
|
||||
:class="{ 'is-on': item.enabled }"
|
||||
:aria-label="item.enabled ? '停用' : '启用'"
|
||||
@click="toggle(item)"
|
||||
/>
|
||||
<button type="button" class="link-action" @click="openEdit(cat.id, item)">编辑</button>
|
||||
<button
|
||||
v-if="canEdit()"
|
||||
type="button"
|
||||
class="link-action"
|
||||
@click="openEdit(cat.id, item)"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -26,6 +26,11 @@ import {
|
||||
import ProtoIcon from "/@/views/inquiry/components/ProtoIcon.vue";
|
||||
import AdminModal from "/@/views/inquiry/components/AdminModal.vue";
|
||||
import { joinPermissionIds } from "/@/views/inquiry/role/rolePermission";
|
||||
import { usePermission } from "/@/hooks/web/usePermission";
|
||||
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canEditRole = () => hasPermission(InquiryBtnPerm.roleEdit);
|
||||
|
||||
const rows = ref<RoleRow[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -250,7 +255,7 @@ function onDataPermCheck(checked: string[] | { checked: string[] }) {
|
||||
|
||||
<template>
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<button type="button" class="btn btn-primary" @click="openCreate">
|
||||
<button v-if="canEditRole()" type="button" class="btn btn-primary" @click="openCreate">
|
||||
<ProtoIcon name="plus" :size="14" />新增角色
|
||||
</button>
|
||||
</Teleport>
|
||||
@@ -295,7 +300,7 @@ function onDataPermCheck(checked: string[] | { checked: string[] }) {
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="row.editable">
|
||||
<template v-if="canEditRole() && row.editable">
|
||||
<div class="role-row-actions">
|
||||
<button class="link-action" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button class="link-action link-danger" type="button" @click="removeRole(row)">
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 询价后台页面级权限码:侧栏显隐与「查看」按钮同源。
|
||||
* <p>
|
||||
* 为何:侧栏曾写死全量菜单,且 ensureInquiryHandwrittenRoutes 会把未授权页补进路由,
|
||||
* 导致无 reply:list 等权限的人仍能看到「机器人回复语」。以 Jeecg 权限码为准过滤。
|
||||
*/
|
||||
|
||||
/** 侧栏路径 → 进入该页至少需要的查看类权限码 */
|
||||
export const INQUIRY_PAGE_VIEW_PERMS: Record<string, string> = {
|
||||
'/inquiry/dashboard': 'inquiry:dashboard:view',
|
||||
'/inquiry/work-order': 'inquiry:workorder:list',
|
||||
'/inquiry/staff-role': 'inquiry:staff:list',
|
||||
'/inquiry/template': 'inquiry:template:view',
|
||||
'/inquiry/bot-reply': 'inquiry:reply:list',
|
||||
'/inquiry-sys/system-setting': 'inquiry:timeout:list',
|
||||
'/inquiry-sys/user': 'inquiry:account:view',
|
||||
'/inquiry-sys/role': 'inquiry:role:view',
|
||||
};
|
||||
|
||||
/** 按钮细粒度权限码(与 sys_permission / @RequiresPermissions 对齐) */
|
||||
export const InquiryBtnPerm = {
|
||||
workorderList: 'inquiry:workorder:list',
|
||||
workorderDetail: 'inquiry:workorder:detail',
|
||||
workorderTransition: 'inquiry:workorder:transition',
|
||||
workorderExport: 'inquiry:workorder:export',
|
||||
replyList: 'inquiry:reply:list',
|
||||
replyAdd: 'inquiry:reply:add',
|
||||
replyEdit: 'inquiry:reply:edit',
|
||||
replyDisable: 'inquiry:reply:disable',
|
||||
staffList: 'inquiry:staff:list',
|
||||
staffAdd: 'inquiry:staff:add',
|
||||
staffEdit: 'inquiry:staff:edit',
|
||||
staffDisable: 'inquiry:staff:disable',
|
||||
templateView: 'inquiry:template:view',
|
||||
templateUpload: 'inquiry:template:upload',
|
||||
templateEdit: 'inquiry:template:edit',
|
||||
templateToggle: 'inquiry:template:toggle',
|
||||
accountView: 'inquiry:account:view',
|
||||
accountEdit: 'inquiry:account:edit',
|
||||
roleView: 'inquiry:role:view',
|
||||
roleEdit: 'inquiry:role:edit',
|
||||
timeoutList: 'inquiry:timeout:list',
|
||||
timeoutEdit: 'inquiry:timeout:edit',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* 当前权限码是否允许进入某侧栏路径。
|
||||
* @param path 侧栏 key,如 /inquiry/bot-reply
|
||||
* @param hasPerm hasPermission 回调
|
||||
*/
|
||||
export function canSeeInquiryPage(
|
||||
path: string,
|
||||
hasPerm: (code: string) => boolean,
|
||||
): boolean {
|
||||
const code = INQUIRY_PAGE_VIEW_PERMS[path];
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
return hasPerm(code);
|
||||
}
|
||||
@@ -25,6 +25,13 @@ import {
|
||||
dataPermTitle,
|
||||
normalizeStaffRouteKeys,
|
||||
} from "/@/views/inquiry/shared/dataPermCatalog";
|
||||
import { usePermission } from "/@/hooks/web/usePermission";
|
||||
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canAddStaff = () => hasPermission(InquiryBtnPerm.staffAdd);
|
||||
const canEditStaff = () => hasPermission(InquiryBtnPerm.staffEdit);
|
||||
const canDisableStaff = () => hasPermission(InquiryBtnPerm.staffDisable);
|
||||
|
||||
type StaffRole = "sales" | "air" | "sea" | "land";
|
||||
|
||||
@@ -485,10 +492,16 @@ function filterRouteOption(input: string, option: any) {
|
||||
<template>
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<div class="staff-topbar-actions">
|
||||
<button type="button" class="btn btn-secondary" :disabled="syncing" @click="syncWecom">
|
||||
<button
|
||||
v-if="canEditStaff()"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:disabled="syncing"
|
||||
@click="syncWecom"
|
||||
>
|
||||
<ProtoIcon name="refresh" :size="16" />{{ syncing ? "同步中…" : "同步企业微信" }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="openCreate">
|
||||
<button v-if="canAddStaff()" type="button" class="btn btn-primary" @click="openCreate">
|
||||
<ProtoIcon name="userPlus" :size="16" />新增员工
|
||||
</button>
|
||||
</div>
|
||||
@@ -586,9 +599,16 @@ function filterRouteOption(input: string, option: any) {
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<button class="link-action" type="button" @click="openEdit(row)">编辑</button>
|
||||
<button
|
||||
v-if="row.status === 'active'"
|
||||
v-if="canEditStaff()"
|
||||
class="link-action"
|
||||
type="button"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
v-if="canDisableStaff() && row.status === 'active'"
|
||||
class="link-action link-danger"
|
||||
type="button"
|
||||
@click="removeStaff(row)"
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
getTimeoutListAll,
|
||||
saveTimeoutHours,
|
||||
} from '/@/views/inquiry/systemSetting/systemSetting.api';
|
||||
import { usePermission } from '/@/hooks/web/usePermission';
|
||||
import { InquiryBtnPerm } from '/@/views/inquiry/shared/inquiryPagePerms';
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canEditTimeout = () => hasPermission(InquiryBtnPerm.timeoutEdit);
|
||||
|
||||
/** 分块顺序:海运 → 空运 → 陆运(产品确认同页分块) */
|
||||
const MODE_BLOCKS = [
|
||||
@@ -153,7 +158,7 @@ async function commitHours(row) {
|
||||
min="0"
|
||||
step="0.01"
|
||||
:value="draftHours[row.id]"
|
||||
:disabled="savingId === row.id"
|
||||
:disabled="!canEditTimeout() || savingId === row.id"
|
||||
@input="onHoursInput(row.id, $event.target.value)"
|
||||
@blur="commitHours(row)"
|
||||
@keydown.enter.prevent="commitHours(row)"
|
||||
|
||||
@@ -27,6 +27,13 @@ import {
|
||||
summarizeMappingParse,
|
||||
type MappingParseSummary,
|
||||
} from "/@/views/inquiry/template/previewSample";
|
||||
import { usePermission } from "/@/hooks/web/usePermission";
|
||||
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canUploadTemplate = () => hasPermission(InquiryBtnPerm.templateUpload);
|
||||
const canEditTemplate = () => hasPermission(InquiryBtnPerm.templateEdit);
|
||||
const canToggleTemplate = () => hasPermission(InquiryBtnPerm.templateToggle);
|
||||
|
||||
// buildTemplateFileUrl 已由 downloadTemplateFile 替代(鉴权 blob 下载)
|
||||
|
||||
@@ -683,7 +690,12 @@ async function downloadPreviewOrOriginal() {
|
||||
<template>
|
||||
<!-- 唯一上传入口:顶栏 -->
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<button type="button" class="btn btn-primary" @click.stop.prevent="openCreate">
|
||||
<button
|
||||
v-if="canUploadTemplate()"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click.stop.prevent="openCreate"
|
||||
>
|
||||
<ProtoIcon name="upload" :size="16" />上传模板
|
||||
</button>
|
||||
</Teleport>
|
||||
@@ -773,8 +785,16 @@ async function downloadPreviewOrOriginal() {
|
||||
<button type="button" class="link-action link-muted" @click.stop="openPreview(row)">
|
||||
预览
|
||||
</button>
|
||||
<button type="button" class="link-action" @click.stop="openEdit(row)">编辑</button>
|
||||
<button
|
||||
v-if="canEditTemplate()"
|
||||
type="button"
|
||||
class="link-action"
|
||||
@click.stop="openEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
v-if="canEditTemplate()"
|
||||
type="button"
|
||||
class="link-action"
|
||||
:disabled="row.parseStatus === '解析中'"
|
||||
@@ -788,6 +808,7 @@ async function downloadPreviewOrOriginal() {
|
||||
重新解析
|
||||
</button>
|
||||
<button
|
||||
v-if="canToggleTemplate()"
|
||||
type="button"
|
||||
class="link-action"
|
||||
:class="row.status === 'enabled' ? 'link-danger' : ''"
|
||||
@@ -795,7 +816,12 @@ async function downloadPreviewOrOriginal() {
|
||||
>
|
||||
{{ row.status === "enabled" ? "禁用" : "启用" }}
|
||||
</button>
|
||||
<button type="button" class="link-action link-danger" @click.stop="removeTemplate(row)">
|
||||
<button
|
||||
v-if="canEditTemplate()"
|
||||
type="button"
|
||||
class="link-action link-danger"
|
||||
@click.stop="removeTemplate(row)"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,23 @@ import {
|
||||
quantitySummary,
|
||||
type DemandField,
|
||||
} from "/@/views/inquiry/workOrder/demandFields";
|
||||
import { usePermission } from "/@/hooks/web/usePermission";
|
||||
import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
const canCloseTicket = () => hasPermission(InquiryBtnPerm.workorderTransition);
|
||||
const canRetryTicket = () => hasPermission(InquiryBtnPerm.workorderTransition);
|
||||
const canViewDetail = () =>
|
||||
hasPermission(InquiryBtnPerm.workorderDetail) || hasPermission(InquiryBtnPerm.workorderList);
|
||||
|
||||
/**
|
||||
* 与主账 TicketStatuses 一致:仅询价中/已报价/协商中/转人工可关到「已关闭」。
|
||||
* 已成交/未成交/已关闭为终态,禁止再显示「关闭工单」(否则点确认会报不允许流转)。
|
||||
*/
|
||||
const CLOSEABLE_STATUSES = new Set(["询价中", "已报价", "协商中", "转人工"]);
|
||||
function canShowCloseForStatus(status: string) {
|
||||
return canCloseTicket() && CLOSEABLE_STATUSES.has(String(status || "").trim());
|
||||
}
|
||||
|
||||
/** 单条费用明细:名称 + 带币种金额,对齐企微价格明细。 */
|
||||
type TicketQuoteFee = { name: string; amount: string };
|
||||
@@ -615,6 +632,10 @@ async function openDetail(row: TicketRow) {
|
||||
}
|
||||
|
||||
function openClose(row: TicketRow) {
|
||||
if (!canShowCloseForStatus(row.status)) {
|
||||
message.warning(`「${row.status}」为终态,不能关闭工单`);
|
||||
return;
|
||||
}
|
||||
current.value = row;
|
||||
closeReason.value = "";
|
||||
closeOpen.value = true;
|
||||
@@ -903,7 +924,7 @@ function refresh() {
|
||||
<td class="align-top col-actions col-sticky-right">
|
||||
<div class="ticket-row-actions">
|
||||
<button
|
||||
v-if="row.systemException !== '-' && row.status !== '已关闭'"
|
||||
v-if="canRetryTicket() && row.systemException !== '-' && row.status !== '已关闭'"
|
||||
class="link-action"
|
||||
type="button"
|
||||
@click="retryTicket(row)"
|
||||
@@ -911,14 +932,21 @@ function refresh() {
|
||||
重试
|
||||
</button>
|
||||
<button
|
||||
v-if="row.status !== '已关闭'"
|
||||
v-if="canShowCloseForStatus(row.status)"
|
||||
class="link-action"
|
||||
type="button"
|
||||
@click="openClose(row)"
|
||||
>
|
||||
关闭工单
|
||||
</button>
|
||||
<button class="link-action" type="button" @click="openDetail(row)">查看详情</button>
|
||||
<button
|
||||
v-if="canViewDetail()"
|
||||
class="link-action"
|
||||
type="button"
|
||||
@click="openDetail(row)"
|
||||
>
|
||||
查看详情
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -946,7 +974,12 @@ function refresh() {
|
||||
<template #titleExtra>
|
||||
<span v-if="current" class="ticket-detail-no">{{ current.id }}</span>
|
||||
<button
|
||||
v-if="current && current.systemException !== '-' && current.status !== '已关闭'"
|
||||
v-if="
|
||||
canRetryTicket() &&
|
||||
current &&
|
||||
current.systemException !== '-' &&
|
||||
current.status !== '已关闭'
|
||||
"
|
||||
class="link-action"
|
||||
type="button"
|
||||
@click="retryTicket(current)"
|
||||
|
||||
Reference in New Issue
Block a user