协商中再问按后台超时小时顺延并可强制清跟进时刻;工单与账号支持按筛选导出 Excel。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,10 +6,12 @@ import {
|
||||
changePassword,
|
||||
deleteUser,
|
||||
getAllRolesListNoByTenant,
|
||||
getExportUrl,
|
||||
getUserRoles,
|
||||
list as listUsers,
|
||||
saveOrUpdateUser,
|
||||
} from '/@/views/system/user/user.api';
|
||||
import { downloadAuthGet } from '/@/views/inquiry/shared/inquiryAuthDownload';
|
||||
|
||||
export {
|
||||
changePassword,
|
||||
@@ -20,6 +22,12 @@ export {
|
||||
saveOrUpdateUser,
|
||||
};
|
||||
|
||||
/**
|
||||
* 按当前筛选导出账号 Excel(Jeecg /sys/user/exportXls,与列表 QueryGenerator 同源)。
|
||||
*/
|
||||
export const exportAccounts = (params: Record<string, any>) =>
|
||||
downloadAuthGet(getExportUrl, params, '账号列表.xlsx');
|
||||
|
||||
export type AccountRow = {
|
||||
id: string;
|
||||
username: string;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { message, Modal } from "ant-design-vue";
|
||||
import {
|
||||
changePassword,
|
||||
deleteUser,
|
||||
exportAccounts,
|
||||
getAllRolesListNoByTenant,
|
||||
getUserRoles,
|
||||
listUsers,
|
||||
@@ -25,6 +26,10 @@ import { InquiryBtnPerm } from "/@/views/inquiry/shared/inquiryPagePerms";
|
||||
const { hasPermission } = usePermission();
|
||||
/** 账号新建/编辑/删除/重置密码均属 edit 权限 */
|
||||
const canEditAccount = () => hasPermission(InquiryBtnPerm.accountEdit);
|
||||
/** 能看账号页即可导出(与列表同源筛选) */
|
||||
const canExportAccount = () =>
|
||||
hasPermission(InquiryBtnPerm.accountView) || hasPermission(InquiryBtnPerm.accountEdit);
|
||||
const exporting = ref(false);
|
||||
|
||||
const rows = ref<AccountRow[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -68,27 +73,34 @@ async function loadRoles() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 与列表同源的 Jeecg 查询参数(不含分页)。 */
|
||||
function buildAccountFilterParams(): Record<string, any> {
|
||||
const kw = filters.keyword.trim();
|
||||
const params: Record<string, any> = {};
|
||||
// Jeecg QueryGenerator:字段值包 * 走 LIKE。多字段 AND 过严,按关键字形态选一个字段。
|
||||
if (kw) {
|
||||
if (/[\u4e00-\u9fff]/.test(kw)) {
|
||||
params.realname = `*${kw}*`;
|
||||
} else if (/^\d{5,}$/.test(kw)) {
|
||||
params.phone = `*${kw}*`;
|
||||
} else {
|
||||
params.username = `*${kw}*`;
|
||||
}
|
||||
}
|
||||
if (filters.status === "active") params.status = 1;
|
||||
if (filters.status === "disabled") params.status = 2;
|
||||
return params;
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
if (!roleOptions.value.length) await loadRoles();
|
||||
const kw = filters.keyword.trim();
|
||||
// Jeecg QueryGenerator:字段值包 * 走 LIKE。多字段 AND 过严,按关键字形态选一个字段。
|
||||
const params: Record<string, any> = {
|
||||
...buildAccountFilterParams(),
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
if (kw) {
|
||||
if (/[\u4e00-\u9fff]/.test(kw)) {
|
||||
params.realname = `*${kw}*`;
|
||||
} else if (/^\d{5,}$/.test(kw)) {
|
||||
params.phone = `*${kw}*`;
|
||||
} else {
|
||||
params.username = `*${kw}*`;
|
||||
}
|
||||
}
|
||||
if (filters.status === "active") params.status = 1;
|
||||
if (filters.status === "disabled") params.status = 2;
|
||||
|
||||
const res: any = await listUsers(params);
|
||||
const page = res?.records ? res : res?.result || {};
|
||||
@@ -297,13 +309,37 @@ function removeAccount(row: AccountRow) {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 按当前筛选导出账号 Excel(Jeecg 用户导出)。 */
|
||||
async function exportAccountList() {
|
||||
if (!canExportAccount()) {
|
||||
message.warning("无导出权限");
|
||||
return;
|
||||
}
|
||||
if (exporting.value) return;
|
||||
exporting.value = true;
|
||||
try {
|
||||
await exportAccounts(buildAccountFilterParams());
|
||||
message.success("已开始下载 Excel");
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "导出失败");
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<div class="account-topbar-actions">
|
||||
<button type="button" class="btn btn-secondary" disabled title="导出待后续开放">
|
||||
<ProtoIcon name="download" :size="14" />导出
|
||||
<button
|
||||
v-if="canExportAccount()"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:disabled="exporting"
|
||||
@click="exportAccountList"
|
||||
>
|
||||
<ProtoIcon name="download" :size="14" />{{ exporting ? "导出中…" : "导出" }}
|
||||
</button>
|
||||
<button v-if="canEditAccount()" type="button" class="btn btn-primary" @click="openCreate">
|
||||
<ProtoIcon name="plus" :size="14" />新建账号
|
||||
|
||||
@@ -320,7 +320,7 @@ export const mockReplyCategories: ReplyCategory[] = [
|
||||
id: "brv10000000000000000000000000039",
|
||||
replyCode: "negotiating.recorded",
|
||||
trigger: "成交跟进中选择「协商中」",
|
||||
text: "已记录为「协商中」。已顺延三个工作日,{{followup_when}}的9:00再次发起成交状态询问。",
|
||||
text: "已记录为「协商中」。已顺延 {{followup_hours}} 个工作小时,将于 {{followup_when}} 再次发起成交状态询问。",
|
||||
enabled: true,
|
||||
}
|
||||
],
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 带鉴权下载二进制文件(Excel 等)。
|
||||
* 为何:window.open 带 token 遇 JSON 错误页难提示;统一走 fetch + blob。
|
||||
*/
|
||||
import { useGlobSetting } from '/@/hooks/setting';
|
||||
import { getToken } from '/@/utils/auth';
|
||||
|
||||
export type AuthDownloadParams = Record<string, string | number | undefined | null>;
|
||||
|
||||
/**
|
||||
* GET 下载附件并触发浏览器保存。
|
||||
* @param path 相对 API 路径,如 /inquiry/workOrder/view/export
|
||||
* @param params query 参数(空值跳过)
|
||||
* @param fallbackName 无 Content-Disposition 时的文件名
|
||||
*/
|
||||
export async function downloadAuthGet(
|
||||
path: string,
|
||||
params: AuthDownloadParams = {},
|
||||
fallbackName = 'download.bin',
|
||||
): Promise<void> {
|
||||
const glob = useGlobSetting();
|
||||
const base = (glob.apiUrl || '').replace(/\/$/, '');
|
||||
const qs = new URLSearchParams();
|
||||
Object.keys(params).forEach((k) => {
|
||||
const v = params[k];
|
||||
if (v === undefined || v === null || String(v).trim() === '') return;
|
||||
qs.set(k, String(v));
|
||||
});
|
||||
const url = `${base}${path}${qs.toString() ? `?${qs.toString()}` : ''}`;
|
||||
const token = getToken() || '';
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Access-Token': String(token),
|
||||
},
|
||||
});
|
||||
const ctype = (res.headers.get('content-type') || '').toLowerCase();
|
||||
if (!res.ok || ctype.includes('application/json')) {
|
||||
let msg = `下载失败 HTTP ${res.status}`;
|
||||
try {
|
||||
const j = await res.json();
|
||||
if (j?.message) msg = String(j.message);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
let name = fallbackName;
|
||||
const cd = res.headers.get('content-disposition') || '';
|
||||
const m = /filename\*=UTF-8''([^;]+)|filename=\"?([^\";]+)\"?/i.exec(cd);
|
||||
if (m) {
|
||||
name = decodeURIComponent((m[1] || m[2] || name).trim());
|
||||
}
|
||||
const href = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(href);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 超时响应:响应超时小时配置。
|
||||
* 海运 / 空运 / 陆运同页自上而下分块展示,只允许改超时小时;无增删、无 Tab。
|
||||
* 「编码」= timeoutCode,每条唯一(如 sea_sales / land_hk_express),供智能体单码调用;只读。
|
||||
* 成交跟进 / 海运 / 空运 / 陆运同页自上而下分块展示,只允许改超时小时;无增删、无 Tab。
|
||||
* 「编码」= timeoutCode,每条唯一(如 deal_negotiate / sea_sales),供智能体单码调用;只读。
|
||||
* roleCode(销售/产品)仅为角色维度,可重复,不是调用主键。
|
||||
* 陆运产品运输分类 5 项(与线路解耦);不展示线路类别。
|
||||
* 协商中再问读 deal_negotiate(工作小时,非工作日不算);0=不排再问。
|
||||
* 数据来自主账 inq_response_timeout;本页不做工单超时判定。
|
||||
*/
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
@@ -19,8 +20,9 @@ import { InquiryBtnPerm } from '/@/views/inquiry/shared/inquiryPagePerms';
|
||||
const { hasPermission } = usePermission();
|
||||
const canEditTimeout = () => hasPermission(InquiryBtnPerm.timeoutEdit);
|
||||
|
||||
/** 分块顺序:海运 → 空运 → 陆运(产品确认同页分块) */
|
||||
/** 分块顺序:成交跟进 → 海运 → 空运 → 陆运(产品确认同页分块) */
|
||||
const MODE_BLOCKS = [
|
||||
{ code: 'DEAL', title: '成交跟进' },
|
||||
{ code: 'SEA', title: '海运' },
|
||||
{ code: 'AIR', title: '空运' },
|
||||
{ code: 'LAND', title: '陆运' },
|
||||
@@ -118,7 +120,7 @@ async function commitHours(row) {
|
||||
<template>
|
||||
<div class="sys-setting-page">
|
||||
<div class="toolbar-bar">
|
||||
<span class="hint">固定规则,仅可改超时小时(支持小数,0=不监测);编码唯一只读,供智能体调用;失焦或回车保存</span>
|
||||
<span class="hint">固定规则,仅可改超时小时(支持小数;响应超时 0=不监测,协商再问 0=不排再问);编码唯一只读;失焦或回车保存</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 超时响应配置 API。
|
||||
* 对接主账 /inquiry/responseTimeout;仅 listAll + 改小时数。
|
||||
* timeoutCode 每条唯一(如 sea_sales),智能体 POST /inquiry/agent/responseTimeout/byCode 单码查询。
|
||||
* timeoutCode 每条唯一(如 deal_negotiate / sea_sales),智能体 POST /inquiry/agent/responseTimeout/byCode 单码查询。
|
||||
* deal_negotiate=协商中再问工作小时(成交跟进分块)。
|
||||
*/
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import {
|
||||
exportWorkOrders,
|
||||
getWorkOrderDetail,
|
||||
getWorkOrderList,
|
||||
retryAbnormal,
|
||||
@@ -30,6 +31,10 @@ const canCloseTicket = () => hasPermission(InquiryBtnPerm.workorderTransition);
|
||||
const canRetryTicket = () => hasPermission(InquiryBtnPerm.workorderTransition);
|
||||
const canViewDetail = () =>
|
||||
hasPermission(InquiryBtnPerm.workorderDetail) || hasPermission(InquiryBtnPerm.workorderList);
|
||||
/** 导出:有 export 或 list 均可(与主账 Logical.OR 对齐) */
|
||||
const canExportTicket = () =>
|
||||
hasPermission(InquiryBtnPerm.workorderExport) || hasPermission(InquiryBtnPerm.workorderList);
|
||||
const exporting = ref(false);
|
||||
|
||||
/**
|
||||
* 与主账 TicketStatuses 一致:仅询价中/已报价/协商中/转人工可关到「已关闭」。
|
||||
@@ -676,13 +681,47 @@ function retryTicket(row: TicketRow) {
|
||||
function refresh() {
|
||||
loadList().then(() => message.success("已刷新"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前筛选导出 Excel(与列表同源,不分页;主账上限 2 万行)。
|
||||
*/
|
||||
async function exportTickets() {
|
||||
if (!canExportTicket()) {
|
||||
message.warning("无导出权限");
|
||||
return;
|
||||
}
|
||||
if (exporting.value) return;
|
||||
exporting.value = true;
|
||||
try {
|
||||
await exportWorkOrders({
|
||||
keyword: filters.keyword.trim() || undefined,
|
||||
status: filters.status || undefined,
|
||||
eventException: filters.eventException || undefined,
|
||||
systemException: filters.systemException || undefined,
|
||||
bizType: filters.bizType || undefined,
|
||||
dateFrom: filters.dateFrom || undefined,
|
||||
dateTo: filters.dateTo || undefined,
|
||||
});
|
||||
message.success("已开始下载 Excel");
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || "导出失败");
|
||||
} finally {
|
||||
exporting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport defer to="#page-topbar-actions">
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<button type="button" class="btn btn-secondary" disabled title="导出待后续开放">
|
||||
<ProtoIcon name="download" :size="16" />导出工单
|
||||
<button
|
||||
v-if="canExportTicket()"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:disabled="exporting"
|
||||
@click="exportTickets"
|
||||
>
|
||||
<ProtoIcon name="download" :size="16" />{{ exporting ? "导出中…" : "导出工单" }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" @click="refresh">
|
||||
<ProtoIcon name="refresh" :size="16" />刷新
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* 询价工单 API:列表/详情/六态/异常。
|
||||
* 询价工单 API:列表/详情/六态/异常/导出。
|
||||
*/
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
import { downloadAuthGet } from '/@/views/inquiry/shared/inquiryAuthDownload';
|
||||
|
||||
enum Api {
|
||||
list = '/inquiry/workOrder/view/list',
|
||||
detail = '/inquiry/workOrder/view/detail',
|
||||
exportXls = '/inquiry/workOrder/view/export',
|
||||
transition = '/inquiry/workOrder/transition',
|
||||
abnormalHandle = '/inquiry/workOrder/abnormal/handle',
|
||||
abnormalRetry = '/inquiry/workOrder/abnormal/retry',
|
||||
@@ -18,3 +20,9 @@ export const getWorkOrderDetail = (workOrderNo: string) =>
|
||||
export const transitionWorkOrder = (data) => defHttp.post({ url: Api.transition, data });
|
||||
export const handleAbnormal = (data) => defHttp.post({ url: Api.abnormalHandle, data });
|
||||
export const retryAbnormal = (data) => defHttp.post({ url: Api.abnormalRetry, data });
|
||||
|
||||
/**
|
||||
* 按当前筛选导出 Excel(与列表同源参数,不分页)。
|
||||
*/
|
||||
export const exportWorkOrders = (params: Record<string, any>) =>
|
||||
downloadAuthGet(Api.exportXls, params, '询价工单.xlsx');
|
||||
|
||||
Reference in New Issue
Block a user