后台按查看权限收紧菜单与「只看自己」数据范围;跨境整车报价填表更稳,协商中顺延不再误报销售超时。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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