空运人工价锁舱现拉补全舱位并拦清缺参;成交/报价填表与工单陆运费率展示同步收紧。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-07 23:35:24 +08:00
co-authored by Cursor
parent 419b4a1bf7
commit e197ecea10
31 changed files with 1607 additions and 150 deletions
@@ -2,7 +2,6 @@ import type { AppRouteRecordRaw, Menu } from '/@/router/types';
import { defineStore } from 'pinia';
import { store } from '/@/store';
import { useI18n } from '/@/hooks/web/useI18n';
import { useUserStore } from './user';
import { useAppStoreWithOut } from './app';
import { toRaw } from 'vue';
@@ -18,10 +17,9 @@ import { ERROR_LOG_ROUTE, PAGE_NOT_FOUND_ROUTE } from '/@/router/routes/basic';
import { filter } from '/@/utils/helper/treeHelper';
import { getMenuList,switchVue3Menu } from '/@/api/sys/menu';
import { getMenuList } from '/@/api/sys/menu';
import { getPermCode } from '/@/api/sys/user';
import { useMessage } from '/@/hooks/web/useMessage';
import { PageEnum } from '/@/enums/pageEnum';
/**
@@ -246,7 +244,6 @@ export const usePermissionStore = defineStore({
this.setAuthData(systemPermission);
},
async buildRoutesAction(): Promise<AppRouteRecordRaw[]> {
const { t } = useI18n();
const userStore = useUserStore();
const appStore = useAppStoreWithOut();
@@ -322,54 +319,18 @@ export const usePermissionStore = defineStore({
// 后台菜单构建
case PermissionModeEnum.BACK:
const { createMessage, createWarningModal } = useMessage();
// 菜单加载提示
// createMessage.loading({
// content: t('sys.app.menuLoading'),
// duration: 1,
// });
// 从后台获取权限码,
// 这个函数可能只需要执行一次,并且实际的项目可以在正确的时间被放置
let routeList: AppRouteRecordRaw[] = [];
try {
this.changePermissionCode();
routeList = (await getMenuList()) as AppRouteRecordRaw[];
// update-begin----author:sunjianlei---date:20220315------for: 判断是否是 vue3 版本的菜单 ---
let hasIndex: boolean = false;
let hasIcon: boolean = false;
for (let menuItem of routeList) {
// 条件1:判断组件是否是 layouts/default/index
if (!hasIndex) {
hasIndex = menuItem.component === 'layouts/default/index';
}
// 条件2:判断图标是否带有 冒号
if (!hasIcon) {
hasIcon = !!menuItem.meta?.icon?.includes(':');
}
// 满足任何一个条件都直接跳出循环
if (hasIcon || hasIndex) {
break;
}
}
// 两个条件都不满足,就弹出提示框
if (!hasIcon && !hasIndex) {
// 延迟1.5秒之后再出现提示,否则提示框出不来
setTimeout(
() =>
createWarningModal({
title: '检测提示',
content:
'当前菜单表是 <b>Vue2版本</b>,导致菜单加载异常!<br>点击确认,切换到Vue3版菜单!',
onOk:function () {
switchVue3Menu();
location.reload();
}
}),
100
);
}
// update-end----author:sunjianlei---date:20220315------for: 判断是否是 vue3 版本的菜单 ---
// 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 菜单切表提示 ---
} catch (error) {
console.error(error);
// 菜单接口失败时仍补挂询价手写页,避免侧栏点进去全是 404
@@ -325,6 +325,7 @@ function removeAccount(row: AccountRow) {
<th>账号</th>
<th>姓名</th>
<th>绑定角色</th>
<th>数据范围</th>
<th>手机号</th>
<th>最后登录</th>
<th>状态</th>
@@ -333,10 +334,10 @@ function removeAccount(row: AccountRow) {
</thead>
<tbody>
<tr v-if="loading">
<td colspan="7" style="text-align: center; color: #94a3b8">加载中…</td>
<td colspan="8" style="text-align: center; color: #94a3b8">加载中…</td>
</tr>
<tr v-else-if="!rows.length">
<td colspan="7" style="text-align: center; color: #94a3b8">暂无账号</td>
<td colspan="8" style="text-align: center; color: #94a3b8">暂无账号</td>
</tr>
<tr v-for="row in rows" :key="row.id">
<td class="account-username">{{ row.username }}</td>
@@ -344,6 +345,20 @@ function removeAccount(row: AccountRow) {
<td>
<span class="tag" :class="row.roleTag">{{ row.roleLabel }}</span>
</td>
<td>
<!-- 与编辑弹窗「数据范围」同源:dashboardSelfOnly -->
<span
class="tag"
:class="row.dashboardSelfOnly ? 'tag-amber' : 'tag-teal'"
:title="
row.dashboardSelfOnly
? '分析看板仅统计本账号企微归属的工单'
: '分析看板可看权限内全部数据'
"
>
{{ row.dashboardSelfOnly ? "只看自己数据" : "全部数据" }}
</span>
</td>
<td>{{ row.phoneMasked }}</td>
<td>{{ row.lastLogin }}</td>
<td>
@@ -2,6 +2,9 @@
/**
* 询价后台登录页:视觉 1:1 对齐 admin/LoginView(居中白卡片 + 品牌区,无验证码)。
* 鉴权走 Jeecg loginApi;主账 inquiry 配置关闭图形验证码(jeecg.loginCaptchaEnabled=false)。
*
* 禁止写死 admin/123456:退出后再进登录页会误显示测试账号。
* 仅记住上次成功登录的账号(不存密码),方便换人登录时改账号。
*/
import { reactive, ref, toRaw } from 'vue';
import { ConfigProvider, message } from 'ant-design-vue';
@@ -10,11 +13,29 @@
import '/@/views/inquiry/styles/admin-prototype.css';
import '/@/views/inquiry/styles/admin-fidelity.css';
const LAST_USERNAME_KEY = 'inquiry_admin_last_username';
function readLastUsername(): string {
try {
return String(localStorage.getItem(LAST_USERNAME_KEY) || '').trim();
} catch {
return '';
}
}
function rememberUsername(username: string) {
try {
localStorage.setItem(LAST_USERNAME_KEY, username);
} catch {
/* 隐私模式等写失败时忽略 */
}
}
const userStore = useUserStore();
const loading = ref(false);
const form = reactive({
username: 'admin',
password: '123456',
username: readLastUsername(),
password: '',
});
const themeToken = {
@@ -36,9 +57,10 @@
}
loading.value = true;
try {
const username = form.username.trim();
const { userInfo } = await userStore.login(
toRaw({
username: form.username.trim(),
username,
password: form.password,
// 主账已关验证码;仍传空字段以兼容 LoginParams 类型
captcha: '',
@@ -48,6 +70,9 @@
})
);
if (userInfo) {
rememberUsername(username);
// 成功后清空密码,避免浏览器后退/再进登录页时残留明文
form.password = '';
message.success(`登录成功,欢迎 ${userInfo.realname || userInfo.username}`);
}
} catch (e: any) {
@@ -342,7 +342,7 @@ export const mockReplyCategories: ReplyCategory[] = [
id: "brv10000000000000000000000000041",
replyCode: "lost.reason_ack",
trigger: "销售回复未成交原因",
text: "已记下未成交原因:{{lost_reason}}。",
text: "已将未成交原因“{{lost_reason}}”记录在后台,工单 {{ticket_no}}已关闭,智能体不再跟进该工单,请悉知。",
enabled: true,
}
],
@@ -23,13 +23,16 @@ body {
grid-template-columns: 220px minmax(0, 1fr) !important;
width: 100%;
max-width: 100%;
overflow-x: hidden;
/* 锁视口:避免 html overflow:hidden 裁切后整页既无文档滚、内容区又不内滚 */
height: 100vh !important;
max-height: 100vh !important;
min-height: 0 !important;
overflow: hidden !important;
}
.main-stage {
background: #f7f8fb !important;
min-width: 0 !important;
overflow-x: hidden;
/* 主区锁视口高度:列表页表格区内滚;看板等非列表页在 .admin-content 内滚 */
height: 100vh !important;
max-height: 100vh !important;
@@ -43,10 +46,16 @@ body {
padding: 16px 16px 40px !important;
gap: 12px !important;
min-width: 0 !important;
overflow-x: hidden;
flex: 1 1 auto !important;
/*
* flex-basis:0:先按剩余视口占高,再在区内滚动。
* 若用 1 1 auto,内容区会先被看板撑满,再被 .main-stage 裁掉 → 右侧无滚动条、滚轮无效。
*/
flex: 1 1 0 !important;
min-height: 0 !important;
overflow: auto !important;
height: 0 !important;
overflow-x: hidden !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
}
/*
@@ -1070,6 +1079,9 @@ h6 {
.admin-content:has(.ref-surface--metrics) {
padding: 14px 14px 36px !important;
gap: 10px !important;
/* 分析看板:明确在内容区竖滚,避免被列表页 overflow:hidden 规则误伤 */
overflow-x: hidden !important;
overflow-y: auto !important;
}
/* ---- 话术 ---- */
@@ -253,9 +253,88 @@ function parseQuoteFees(q: any): TicketQuoteFee[] {
pushRows(asFeeArray(payload[key] || payload[bucket]));
}
}
// 陆运 TMS:燃油附加费率 / 税率挂在线路段上,不是金额费目行。
for (const extra of parseLandRateFees(payload)) {
if (!out.some((x) => x.name === extra.name && x.amount === extra.amount)) {
out.push(extra);
}
}
return out;
}
/**
* 陆运线路:fuelSurchargeRate=燃油附加费率;rate=税率;rateFlag=是否含税(1是0否)。
* 只读 payload,不写库。海运/空运没有这些键则不出行。
*/
function parseLandRateFees(payload: Record<string, any>): TicketQuoteFee[] {
const rows = asFeeArray(payload.route_rows || payload.landSegments || payload.land_segments);
const out: TicketQuoteFee[] = [];
const seen = new Set<string>();
for (const row of rows) {
if (!row || typeof row !== "object") continue;
const fuel = formatLandPercent(row.fuel_surcharge_rate ?? row.fuelSurchargeRate);
const tax = formatLandTaxRate(row.tax_rate ?? row.taxRate ?? row.rate);
const included = formatLandTaxIncluded(
row.tax_included ?? row.rateFlag ?? row.rate_flag
);
if (fuel) {
const key = `fuel:${fuel}`;
if (!seen.has(key)) {
seen.add(key);
out.push({ name: "燃油附加费率", amount: fuel });
}
}
if (tax) {
const key = `tax:${tax}`;
if (!seen.has(key)) {
seen.add(key);
out.push({ name: "税率", amount: tax });
}
}
if (included) {
const key = `inc:${included}`;
if (!seen.has(key)) {
seen.add(key);
out.push({ name: "是否含税", amount: included });
}
}
}
return out;
}
/** 燃油附加费率:数字带 %。 */
function formatLandPercent(raw: unknown): string {
const s = String(raw ?? "").trim();
if (!s || s === "-" || s.toLowerCase() === "null") return "";
if (s.includes("%")) return s;
if (/^-?\d+(\.\d+)?$/.test(s)) {
return `${s.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "")}%`;
}
return s;
}
/** 税率 rate:0.09→9%;大于 1 按百分点。 */
function formatLandTaxRate(raw: unknown): string {
const s = String(raw ?? "").trim();
if (!s || s === "-" || s.toLowerCase() === "null") return "";
if (s.includes("%")) return s;
if (!/^-?\d+(\.\d+)?$/.test(s)) return s;
const n = Number(s);
if (!Number.isFinite(n)) return s;
const abs = Math.abs(n);
const pct = abs > 0 && abs <= 1 ? n * 100 : n;
return `${String(pct).replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "")}%`;
}
/** rateFlag:1=含税,0=不含税。 */
function formatLandTaxIncluded(raw: unknown): string {
const s = String(raw ?? "").trim();
if (!s || s === "-" || s.toLowerCase() === "null") return "";
if (s === "1" || s.toLowerCase() === "true" || s === "是" || s === "含税") return "含税";
if (s === "0" || s.toLowerCase() === "false" || s === "否" || s === "不含税") return "不含税";
return s;
}
/**
* 主账 inq_ticket_status_log.action 是内部码,禁止当备注展示。
* transition 与「已报价」等 toStatus 重复,无用户备注时不再跟一段英文。