补齐超时响应唯一 timeoutCode 与智能体按码查询,并增强报价模板预览与解析落库。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+33
@@ -38,6 +38,9 @@ public class InquiryAgentController {
|
||||
@Autowired
|
||||
private org.jeecg.modules.inquiry.service.IInquiryStaffRoleService staffRoleService;
|
||||
|
||||
@Autowired
|
||||
private org.jeecg.modules.inquiry.service.IInquiryResponseTimeoutService responseTimeoutService;
|
||||
|
||||
@Autowired
|
||||
private org.jeecg.modules.inquiry.tms.InquiryTmsCabinService cabinService;
|
||||
|
||||
@@ -234,6 +237,36 @@ public class InquiryAgentController {
|
||||
return Result.OK(staffRoleService.findDisplayByName(firstStr(body, "name")));
|
||||
}
|
||||
|
||||
@ApiOperation("按唯一超时编码取小时数(timeoutCode,如 sea_sales)")
|
||||
@PostMapping("/responseTimeout/byCode")
|
||||
public Result<Map<String, Object>> responseTimeoutByCode(@RequestBody Map<String, Object> body,
|
||||
HttpServletRequest http) {
|
||||
if (!authed(http)) {
|
||||
return Result.error(401, "unauthorized");
|
||||
}
|
||||
String code = firstStr(body, "timeoutCode", "timeout_code", "code");
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return Result.error("timeoutCode 必填");
|
||||
}
|
||||
responseTimeoutService.ensureTimeoutCodes();
|
||||
org.jeecg.modules.inquiry.entity.InquiryResponseTimeout row =
|
||||
responseTimeoutService.getByTimeoutCode(code);
|
||||
Map<String, Object> out = new java.util.LinkedHashMap<>();
|
||||
if (row == null) {
|
||||
out.put("found", false);
|
||||
out.put("timeoutCode", code);
|
||||
return Result.OK(out);
|
||||
}
|
||||
out.put("found", true);
|
||||
out.put("timeoutCode", row.getTimeoutCode());
|
||||
out.put("timeoutHours", row.getTimeoutHours());
|
||||
out.put("transportMode", row.getTransportMode());
|
||||
out.put("roleCode", row.getRoleCode());
|
||||
out.put("routeCategory", row.getRouteCategory());
|
||||
out.put("transportType", row.getTransportType());
|
||||
return Result.OK(out);
|
||||
}
|
||||
|
||||
@ApiOperation("记下海运协同群(不改六态)")
|
||||
@PostMapping("/ticket/bindCollabGroup")
|
||||
public Result<Map<String, Object>> bindCollabGroup(@RequestBody Map<String, Object> body,
|
||||
|
||||
+4
-2
@@ -20,9 +20,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 响应超时配置 API(系统设置页)。
|
||||
* 响应超时配置 API(后台「超时响应」页)。
|
||||
* <p>
|
||||
* 只提供全量列表与改小时数;无 add/delete,维度由 SQL 种子固定。
|
||||
* 每条有唯一 timeoutCode(如 sea_sales),智能体单码调用;roleCode 仅为角色维度可重复。
|
||||
* 本接口供管理后台使用;工单超时判定尚未接入。
|
||||
* </p>
|
||||
*/
|
||||
@@ -59,8 +60,9 @@ public class InquiryResponseTimeoutController
|
||||
@ApiOperation("全量列表(按 sort_no,供系统设置页一次加载)")
|
||||
@GetMapping("/listAll")
|
||||
public Result<List<InquiryResponseTimeout>> listAll() {
|
||||
// 打开系统设置时顺带纠偏旧运输类型文案(幂等,已是新文案则无写)
|
||||
// 打开超时响应页时顺带纠偏旧运输类型文案、回填唯一编码(幂等)
|
||||
responseTimeoutService.ensureCanonicalTransportTypeLabels();
|
||||
responseTimeoutService.ensureTimeoutCodes();
|
||||
QueryWrapper<InquiryResponseTimeout> qw = new QueryWrapper<>();
|
||||
qw.orderByAsc("sort_no");
|
||||
return Result.OK(responseTimeoutService.list(qw));
|
||||
|
||||
+5
-1
@@ -18,6 +18,7 @@ import java.util.Date;
|
||||
* 响应超时配置,对应 inq_response_timeout。
|
||||
* <p>
|
||||
* 维度行由种子固定(运输方式/角色/线路类别/运输类型),后台只允许改 timeoutHours。
|
||||
* timeoutCode 每条唯一,供智能体单码查询超时小时;roleCode 仅为角色维度,可重复。
|
||||
* 本表是配置真相;工单超时判定后续再读本表,本期仅 CRUD 小时数。
|
||||
* </p>
|
||||
*/
|
||||
@@ -33,10 +34,13 @@ public class InquiryResponseTimeout implements Serializable {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private String id;
|
||||
|
||||
@ApiModelProperty("唯一业务编码(智能体调用),如 sea_sales / land_hk_express")
|
||||
private String timeoutCode;
|
||||
|
||||
@ApiModelProperty("运输方式 AIR/SEA/LAND")
|
||||
private String transportMode;
|
||||
|
||||
@ApiModelProperty("角色 sales/product/air_route")
|
||||
@ApiModelProperty("角色维度 sales/product/air_route(可重复,非调用主键)")
|
||||
private String roleCode;
|
||||
|
||||
@ApiModelProperty("线路类别(仅陆运产品有值,其余空串)")
|
||||
|
||||
+11
@@ -28,4 +28,15 @@ public interface IInquiryResponseTimeoutService extends IService<InquiryResponse
|
||||
* </p>
|
||||
*/
|
||||
void ensureCanonicalTransportTypeLabels();
|
||||
|
||||
/**
|
||||
* 回填每条规则的唯一 timeoutCode(已有则跳过)。
|
||||
* 智能体只凭 timeoutCode 取超时小时。
|
||||
*/
|
||||
void ensureTimeoutCodes();
|
||||
|
||||
/**
|
||||
* 按唯一业务编码查超时配置;无则 null。
|
||||
*/
|
||||
InquiryResponseTimeout getByTimeoutCode(String timeoutCode);
|
||||
}
|
||||
|
||||
+58
-3
@@ -132,7 +132,10 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
m.put("objectKey", ver.getObjectKey());
|
||||
m.put("parseMessage", ver.getParseMessage());
|
||||
m.put("unmappedRequired", extractUnmapped(ver.getMappingJson()));
|
||||
m.put("unmappedLabels", extractUnmappedLabels(ver.getMappingJson()));
|
||||
m.put("mappingGaps", extractMappingGaps(ver.getMappingJson()));
|
||||
// 列表提示用:合并后的未解析条数(有则运营要点开看)
|
||||
m.put("unparsedHintCount", countUnparsedHints(ver.getMappingJson()));
|
||||
} else {
|
||||
m.put("versionId", null);
|
||||
m.put("fileName", "");
|
||||
@@ -1355,14 +1358,14 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 mappingJson 取出智能体对照清单算出的未映射说明(中文)。
|
||||
* AI 自报「表上标签未落点」原文。
|
||||
*/
|
||||
private static List<String> extractMappingGaps(String mappingJson) {
|
||||
private static List<String> extractUnmappedLabels(String mappingJson) {
|
||||
if (StringUtils.isBlank(mappingJson)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
JSONArray arr = JSON.parseObject(mappingJson).getJSONArray("mappingGaps");
|
||||
JSONArray arr = JSON.parseObject(mappingJson).getJSONArray("unmappedLabels");
|
||||
if (arr == null || arr.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -1379,6 +1382,58 @@ public class InquiryQuoteTemplateServiceImpl extends ServiceImpl<InquiryQuoteTem
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 mappingJson 取出智能体对照清单算出的未映射说明(中文)。
|
||||
* 若 mappingGaps 为空,用 unmappedLabels / unmappedRequired 兜底,保证列表能提示。
|
||||
*/
|
||||
private static List<String> extractMappingGaps(String mappingJson) {
|
||||
if (StringUtils.isBlank(mappingJson)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
JSONObject obj = JSON.parseObject(mappingJson);
|
||||
JSONArray arr = obj.getJSONArray("mappingGaps");
|
||||
List<String> out = new ArrayList<>();
|
||||
if (arr != null) {
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
String s = arr.getString(i);
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
out.add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!out.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
JSONArray labels = obj.getJSONArray("unmappedLabels");
|
||||
if (labels != null) {
|
||||
for (int i = 0; i < labels.size(); i++) {
|
||||
String s = labels.getString(i);
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
out.add("表标签未落点:" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
JSONArray required = obj.getJSONArray("unmappedRequired");
|
||||
if (required != null) {
|
||||
for (int i = 0; i < required.size(); i++) {
|
||||
String s = required.getString(i);
|
||||
if (StringUtils.isNotBlank(s)) {
|
||||
out.add("字段未映射:" + s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
} catch (Exception e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表角标用的未解析条数(与 extractMappingGaps 口径一致)。 */
|
||||
private static int countUnparsedHints(String mappingJson) {
|
||||
return extractMappingGaps(mappingJson).size();
|
||||
}
|
||||
|
||||
private static String mapParseStatusLabel(String status) {
|
||||
if ("parsed".equals(status)) {
|
||||
return "已解析";
|
||||
|
||||
+80
-1
@@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.inquiry.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.inquiry.entity.InquiryResponseTimeout;
|
||||
import org.jeecg.modules.inquiry.mapper.InquiryResponseTimeoutMapper;
|
||||
@@ -9,13 +10,15 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 响应超时配置:只改 timeout_hours,不动维度列(文案纠偏除外)。
|
||||
* 响应超时配置:只改 timeout_hours,不动维度列(文案纠偏与编码回填除外)。
|
||||
* <p>
|
||||
* 为何单独方法而不用 updateById 整行:避免前端误传改写运输方式/角色等固定种子。
|
||||
* 运输类型纠偏是产品确认的一次性旧→新映射,不开放通用改维度接口。
|
||||
* timeoutCode 每条唯一,供智能体单码调用。
|
||||
* </p>
|
||||
*/
|
||||
@Service
|
||||
@@ -23,12 +26,20 @@ public class InquiryResponseTimeoutServiceImpl
|
||||
extends ServiceImpl<InquiryResponseTimeoutMapper, InquiryResponseTimeout>
|
||||
implements IInquiryResponseTimeoutService {
|
||||
|
||||
private static final org.slf4j.Logger log =
|
||||
org.slf4j.LoggerFactory.getLogger(InquiryResponseTimeoutServiceImpl.class);
|
||||
|
||||
/**
|
||||
* 陆运产品运输类型:旧逗号写法 → 产品确认的括号写法。
|
||||
* 急件文案不变,不在此映射。
|
||||
*/
|
||||
private static final Map<String, String> LAND_PRODUCT_TYPE_RENAMES = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 种子 id → 唯一业务编码。智能体只认 timeoutCode,不认 roleCode。
|
||||
*/
|
||||
private static final Map<String, String> ID_TO_TIMEOUT_CODE = new LinkedHashMap<>();
|
||||
|
||||
static {
|
||||
LAND_PRODUCT_TYPE_RENAMES.put(
|
||||
"普件,中港、东南亚常用线路/常规车型",
|
||||
@@ -45,10 +56,31 @@ public class InquiryResponseTimeoutServiceImpl
|
||||
LAND_PRODUCT_TYPE_RENAMES.put(
|
||||
"普件,非常用线路/临时线路/特种车型",
|
||||
"普件(非常用线路/临时线路/特种车型)");
|
||||
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000001", "air_sales");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000002", "air_route");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000003", "sea_sales");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000004", "land_sales");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000005", "sea_product_ptp");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000006", "sea_product_other");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000007", "land_hk_common");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000008", "land_hk_cross");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000009", "land_hk_special");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000010", "land_hk_express");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000011", "land_ltl_common");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000012", "land_ltl_special");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000013", "land_ltl_express");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000014", "land_city_common");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000015", "land_city_special");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000016", "land_city_express");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000017", "land_sea_common");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000018", "land_sea_special");
|
||||
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000019", "land_sea_express");
|
||||
}
|
||||
|
||||
/** 本进程内只纠偏一次,避免每次打开系统设置都打 UPDATE */
|
||||
private volatile boolean labelsEnsured;
|
||||
private volatile boolean codesEnsured;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -94,4 +126,51 @@ public class InquiryResponseTimeoutServiceImpl
|
||||
labelsEnsured = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void ensureTimeoutCodes() {
|
||||
if (codesEnsured) {
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
if (codesEnsured) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Date now = new Date();
|
||||
List<InquiryResponseTimeout> all = list();
|
||||
for (InquiryResponseTimeout row : all) {
|
||||
if (row == null || StringUtils.isNotBlank(row.getTimeoutCode())) {
|
||||
continue;
|
||||
}
|
||||
String code = ID_TO_TIMEOUT_CODE.get(row.getId());
|
||||
if (StringUtils.isBlank(code)) {
|
||||
// 非种子行:用 id 兜底,保证唯一
|
||||
code = "rt_" + row.getId();
|
||||
}
|
||||
lambdaUpdate()
|
||||
.eq(InquiryResponseTimeout::getId, row.getId())
|
||||
.set(InquiryResponseTimeout::getTimeoutCode, code)
|
||||
.set(InquiryResponseTimeout::getUpdateBy, "system")
|
||||
.set(InquiryResponseTimeout::getUpdateTime, now)
|
||||
.update();
|
||||
}
|
||||
codesEnsured = true;
|
||||
} catch (Exception e) {
|
||||
// 列未迁移时不挡列表;运维执行 migration_inq_response_timeout_code_v1.sql 后即可
|
||||
log.warn("ensureTimeoutCodes 跳过(请确认已执行 timeout_code 迁移): {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InquiryResponseTimeout getByTimeoutCode(String timeoutCode) {
|
||||
if (StringUtils.isBlank(timeoutCode)) {
|
||||
return null;
|
||||
}
|
||||
return lambdaQuery()
|
||||
.eq(InquiryResponseTimeout::getTimeoutCode, timeoutCode.trim())
|
||||
.one();
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
-- 超时响应:每条规则增加唯一业务编码 timeout_code,供智能体单码调用。
|
||||
-- 仅改 inquiry_robot;幂等可重复执行。
|
||||
-- 库:inquiry_robot
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- 1) 加列(已存在则跳过:手工执行前请先 DESCRIBE 确认)
|
||||
ALTER TABLE `inq_response_timeout`
|
||||
ADD COLUMN `timeout_code` varchar(64) NULL COMMENT '唯一业务编码,智能体调用' AFTER `id`;
|
||||
|
||||
-- 2) 按主键回填(与种子 id 对齐)
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='air_sales' WHERE `id`='rt100000000000000000000000000001' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='air_route' WHERE `id`='rt100000000000000000000000000002' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='sea_sales' WHERE `id`='rt100000000000000000000000000003' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_sales' WHERE `id`='rt100000000000000000000000000004' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='sea_product_ptp' WHERE `id`='rt100000000000000000000000000005' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='sea_product_other' WHERE `id`='rt100000000000000000000000000006' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_common' WHERE `id`='rt100000000000000000000000000007' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_cross' WHERE `id`='rt100000000000000000000000000008' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_special' WHERE `id`='rt100000000000000000000000000009' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_express' WHERE `id`='rt100000000000000000000000000010' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_common' WHERE `id`='rt100000000000000000000000000011' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_special' WHERE `id`='rt100000000000000000000000000012' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_express' WHERE `id`='rt100000000000000000000000000013' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_common' WHERE `id`='rt100000000000000000000000000014' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_special' WHERE `id`='rt100000000000000000000000000015' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_express' WHERE `id`='rt100000000000000000000000000016' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_common' WHERE `id`='rt100000000000000000000000000017' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_special' WHERE `id`='rt100000000000000000000000000018' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_express' WHERE `id`='rt100000000000000000000000000019' AND (`timeout_code` IS NULL OR `timeout_code`='');
|
||||
|
||||
-- 3) 唯一索引(列已全部非空后再建;若仍有 NULL 会失败)
|
||||
UPDATE `inq_response_timeout` SET `timeout_code`=CONCAT('rt_', `id`) WHERE `timeout_code` IS NULL OR `timeout_code`='';
|
||||
ALTER TABLE `inq_response_timeout`
|
||||
MODIFY COLUMN `timeout_code` varchar(64) NOT NULL COMMENT '唯一业务编码,智能体调用';
|
||||
ALTER TABLE `inq_response_timeout`
|
||||
ADD UNIQUE KEY `uk_inq_resp_timeout_code` (`timeout_code`);
|
||||
@@ -25,7 +25,7 @@ pnpm run dev
|
||||
| 机器人回复语 | `inquiry/botReply/index` |
|
||||
| 账号管理 | `inquiry/account/index` |
|
||||
| 角色管理 | `inquiry/role/index` |
|
||||
| 系统设置 | `inquiry/systemSetting/index` |
|
||||
| 超时响应 | `inquiry/systemSetting/index` |
|
||||
|
||||
菜单由后端 `sys_permission` 种子下发(见 `deploy/sql/inquiry_admin_menu_seed_v1.sql`)。登录后刷新权限即可看到询价菜单。
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
title: '系统配置',
|
||||
items: [
|
||||
{ key: '/inquiry-sys/system-setting', label: '系统设置', icon: 'clock' },
|
||||
{ key: '/inquiry-sys/system-setting', label: '超时响应', icon: 'clock' },
|
||||
{ key: '/inquiry-sys/user', label: '账号管理', icon: 'account' },
|
||||
{ key: '/inquiry-sys/role', label: '角色管理', icon: 'role' },
|
||||
],
|
||||
|
||||
@@ -46,7 +46,7 @@ const INQUIRY_LEAVES: InquiryLeaf[] = [
|
||||
path: '/inquiry-sys/system-setting',
|
||||
name: 'inquiry-system-setting',
|
||||
component: 'inquiry/systemSetting/index',
|
||||
title: '系统设置',
|
||||
title: '超时响应',
|
||||
},
|
||||
{ path: '/inquiry-sys/user', name: 'inquiry-sys-user', component: 'inquiry/account/index', title: '账号管理' },
|
||||
{ path: '/inquiry-sys/role', name: 'inquiry-sys-role', component: 'inquiry/role/index', title: '角色管理' },
|
||||
@@ -102,8 +102,9 @@ function ensureInquiryHandwrittenRoutes(routeList: AppRouteRecordRaw[]) {
|
||||
const comp = String((existing as any).component || '');
|
||||
if (!comp.includes(leaf.component) && !comp.includes(leaf.component.replace(/\/index$/, ''))) {
|
||||
(existing as any).component = leaf.component;
|
||||
existing.meta = { ...(existing.meta || {}), title: leaf.title, ignoreKeepAlive: true };
|
||||
}
|
||||
// 侧栏/页题以手写清单为准(例:系统设置 → 超时响应)
|
||||
existing.meta = { ...(existing.meta || {}), title: leaf.title, ignoreKeepAlive: true };
|
||||
continue;
|
||||
}
|
||||
const node: AppRouteRecordRaw = {
|
||||
|
||||
@@ -28,8 +28,11 @@ export type TemplateRow = {
|
||||
currentVersion?: number;
|
||||
parseMessage?: string;
|
||||
unmappedRequired?: string[];
|
||||
unmappedLabels?: string[];
|
||||
/** 表上有、对照清单未映射上的中文说明(智能体 mappingGaps) */
|
||||
mappingGaps?: string[];
|
||||
/** 列表提示:未解析条数(主账合并口径) */
|
||||
unparsedHintCount?: number;
|
||||
objectKey?: string;
|
||||
};
|
||||
|
||||
@@ -61,7 +64,9 @@ export function mapApiRow(raw: any): TemplateRow {
|
||||
currentVersion: raw.currentVersion,
|
||||
parseMessage: raw.parseMessage,
|
||||
unmappedRequired: Array.isArray(raw.unmappedRequired) ? raw.unmappedRequired.map(String) : [],
|
||||
unmappedLabels: Array.isArray(raw.unmappedLabels) ? raw.unmappedLabels.map(String) : [],
|
||||
mappingGaps: Array.isArray(raw.mappingGaps) ? raw.mappingGaps.map(String) : [],
|
||||
unparsedHintCount: Number(raw.unparsedHintCount || 0),
|
||||
objectKey: raw.objectKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup>
|
||||
/**
|
||||
* 系统设置:响应超时小时配置。
|
||||
* 海运 / 空运 / 陆运同页分三块展示,只允许改超时小时;无增删、无 Tab。
|
||||
* 超时响应:响应超时小时配置。
|
||||
* 海运 / 空运 / 陆运同页自上而下分块展示,只允许改超时小时;无增删、无 Tab。
|
||||
* 「编码」= timeoutCode,每条唯一(如 sea_sales / land_hk_express),供智能体单码调用;只读。
|
||||
* roleCode(销售/产品)仅为角色维度,可重复,不是调用主键。
|
||||
* 不展示线路类别(维度仍在主账,仅后台列表隐藏)。
|
||||
* 数据来自主账 inq_response_timeout;本页不做工单超时判定。
|
||||
*/
|
||||
@@ -50,6 +52,12 @@ function displayOrDash(v) {
|
||||
return s || '—';
|
||||
}
|
||||
|
||||
/** 唯一业务编码;缺省时提示需迁移 */
|
||||
function timeoutCodeOf(row) {
|
||||
const c = String(row.timeoutCode || '').trim();
|
||||
return c || '—';
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -105,7 +113,7 @@ async function commitHours(row) {
|
||||
<template>
|
||||
<div class="sys-setting-page">
|
||||
<div class="toolbar-bar">
|
||||
<span class="hint">固定规则,仅可改超时小时;失焦或回车保存</span>
|
||||
<span class="hint">固定规则,仅可改超时小时;编码唯一只读,供智能体调用;失焦或回车保存</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" style="padding: 24px; color: #94a3b8">加载中…</div>
|
||||
@@ -120,16 +128,22 @@ async function commitHours(row) {
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 88px">角色</th>
|
||||
<th style="width: 160px" title="每条唯一;智能体用此码取超时小时">编码</th>
|
||||
<th>运输类型</th>
|
||||
<th style="width: 140px">超时设置(小时)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-if="!block.rows.length">
|
||||
<td colspan="3" style="color: #94a3b8">暂无配置</td>
|
||||
<td colspan="4" style="color: #94a3b8">暂无配置</td>
|
||||
</tr>
|
||||
<tr v-for="row in block.rows" :key="row.id">
|
||||
<td>{{ roleLabel(row.roleCode) }}</td>
|
||||
<td>
|
||||
<code class="role-code" :title="'智能体调用编码:' + timeoutCodeOf(row)">{{
|
||||
timeoutCodeOf(row)
|
||||
}}</code>
|
||||
</td>
|
||||
<td>{{ displayOrDash(row.transportType) }}</td>
|
||||
<td>
|
||||
<div class="hours-cell">
|
||||
@@ -202,4 +216,16 @@ async function commitHours(row) {
|
||||
color: #667085;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.role-code {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #344054;
|
||||
background: #f2f4f7;
|
||||
border: 1px solid #e4e7ec;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 系统设置 · 响应超时配置 API。
|
||||
* 超时响应配置 API。
|
||||
* 对接主账 /inquiry/responseTimeout;仅 listAll + 改小时数。
|
||||
* timeoutCode 每条唯一(如 sea_sales),智能体 POST /inquiry/agent/responseTimeout/byCode 单码查询。
|
||||
*/
|
||||
import { defHttp } from '/@/utils/http/axios';
|
||||
|
||||
@@ -11,6 +12,8 @@ enum Api {
|
||||
|
||||
export type ResponseTimeoutRow = {
|
||||
id: string;
|
||||
/** 唯一业务编码,智能体调用主键 */
|
||||
timeoutCode?: string;
|
||||
transportMode: string;
|
||||
roleCode: string;
|
||||
routeCategory?: string;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* 报价模板页:对接主账 /inquiry/quoteTemplate。
|
||||
* - 上传按钮只在顶栏一处;弹窗用原生 overlay
|
||||
* - 版式由上传 Excel 决定;列表展示「映射点数」而非假占位符
|
||||
* - 预览:原表截图(LibreOffice 转图),不是 AI 画格子
|
||||
* - 预览:原表截图(LibreOffice 转图)
|
||||
* - 解析状态列可点:弹窗看已解析 / 未解析清单
|
||||
*/
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { Modal, message } from "ant-design-vue";
|
||||
@@ -17,9 +18,15 @@ import {
|
||||
enableTemplate,
|
||||
fetchTemplateSnapshotBlob,
|
||||
getTemplateList,
|
||||
getTemplateVersion,
|
||||
reparseTemplate,
|
||||
uploadTemplate,
|
||||
} from "/@/views/inquiry/template/template.api";
|
||||
import {
|
||||
FIELD_LABELS,
|
||||
summarizeMappingParse,
|
||||
type MappingParseSummary,
|
||||
} from "/@/views/inquiry/template/previewSample";
|
||||
|
||||
// buildTemplateFileUrl 已由 downloadTemplateFile 替代(鉴权 blob 下载)
|
||||
|
||||
@@ -45,7 +52,17 @@ const formOpen = ref(false);
|
||||
const editing = ref<TemplateRow | null>(null);
|
||||
const previewOpen = ref(false);
|
||||
const previewRow = ref<TemplateRow | null>(null);
|
||||
/** 原表截图预览状态 */
|
||||
/** 空映射摘要 */
|
||||
const emptyMappingSummary = (): MappingParseSummary => ({
|
||||
fields: [],
|
||||
regions: [],
|
||||
gaps: [],
|
||||
unmappedRequired: [],
|
||||
unmappedLabels: [],
|
||||
pointCount: 0,
|
||||
});
|
||||
|
||||
/** 原表截图预览状态(不含映射清单) */
|
||||
const previewState = reactive({
|
||||
loading: false,
|
||||
error: "",
|
||||
@@ -54,6 +71,34 @@ const previewState = reactive({
|
||||
mappedCount: 0,
|
||||
fileName: "",
|
||||
});
|
||||
|
||||
/** 列表「解析状态」点击弹窗:已解析 / 未解析 */
|
||||
const parseDetailOpen = ref(false);
|
||||
const parseDetailRow = ref<TemplateRow | null>(null);
|
||||
const parseDetailState = reactive({
|
||||
loading: false,
|
||||
mappedCount: 0,
|
||||
mapping: emptyMappingSummary() as MappingParseSummary,
|
||||
});
|
||||
|
||||
/** 未解析清单:优先 mappingGaps,其次 AI 自报项 */
|
||||
const parseDetailUnparsed = computed(() => {
|
||||
const m = parseDetailState.mapping;
|
||||
const list: string[] = [];
|
||||
for (const g of m.gaps || []) {
|
||||
if (g) list.push(g);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
});
|
||||
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const pendingFile = ref<File | null>(null);
|
||||
|
||||
@@ -98,7 +143,7 @@ function revokePreviewUrl() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开预览:读取上传后已缓存的截图(未就绪则现转)。
|
||||
* 打开预览:仅原表截图。
|
||||
* 副作用:HTTP 读 previewSnapshot;本地 blob URL;不写库。
|
||||
*/
|
||||
async function openPreview(row: TemplateRow) {
|
||||
@@ -132,6 +177,57 @@ async function openPreview(row: TemplateRow) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表点「解析状态」:弹窗展示已解析 / 未解析清单。
|
||||
* 副作用:HTTP 读 version.mappingJson;不写库。
|
||||
*/
|
||||
async function openParseDetail(row: TemplateRow) {
|
||||
if (row.parseStatus === "解析中" || row.parseStatus === "未上传") {
|
||||
message.info(row.parseStatus === "解析中" ? "解析中,请稍候再看" : "请先上传模板文件");
|
||||
return;
|
||||
}
|
||||
if (!row.versionId) {
|
||||
message.warning("无可用版本");
|
||||
return;
|
||||
}
|
||||
parseDetailRow.value = row;
|
||||
parseDetailOpen.value = true;
|
||||
parseDetailState.loading = true;
|
||||
parseDetailState.mappedCount = row.mappingPointCount || 0;
|
||||
parseDetailState.mapping = emptyMappingSummary();
|
||||
try {
|
||||
const ver: any = await getTemplateVersion(row.versionId);
|
||||
const mappingJson = ver?.mappingJson ?? ver?.result?.mappingJson ?? null;
|
||||
let summary = summarizeMappingParse(mappingJson);
|
||||
if (!summary.gaps.length && row.mappingGaps?.length) {
|
||||
summary = { ...summary, gaps: [...row.mappingGaps] };
|
||||
}
|
||||
if (!summary.unmappedRequired.length && row.unmappedRequired?.length) {
|
||||
summary = { ...summary, unmappedRequired: [...row.unmappedRequired] };
|
||||
}
|
||||
parseDetailState.mapping = summary;
|
||||
if (summary.pointCount > 0) {
|
||||
parseDetailState.mappedCount = summary.pointCount;
|
||||
}
|
||||
} catch {
|
||||
parseDetailState.mapping = {
|
||||
...emptyMappingSummary(),
|
||||
gaps: listMappingGaps(row),
|
||||
unmappedRequired: row.unmappedRequired || [],
|
||||
pointCount: row.mappingPointCount || 0,
|
||||
};
|
||||
} finally {
|
||||
parseDetailState.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeParseDetail() {
|
||||
parseDetailOpen.value = false;
|
||||
parseDetailRow.value = null;
|
||||
parseDetailState.loading = false;
|
||||
parseDetailState.mapping = emptyMappingSummary();
|
||||
}
|
||||
|
||||
function closeForm() {
|
||||
formOpen.value = false;
|
||||
}
|
||||
@@ -281,13 +377,23 @@ function openEdit(row: TemplateRow) {
|
||||
formOpen.value = true;
|
||||
}
|
||||
|
||||
/** 运营可见的未解析项:优先 mappingGaps,其次 AI 自报 unmappedRequired */
|
||||
/** 运营可见的未解析项:优先 mappingGaps,其次标签/必填未映射 */
|
||||
function listMappingGaps(row: TemplateRow): string[] {
|
||||
if (row.mappingGaps?.length) return [...row.mappingGaps];
|
||||
if (row.unmappedRequired?.length) {
|
||||
return row.unmappedRequired.map((c) => `字段未映射:${c}`);
|
||||
const list: string[] = [];
|
||||
for (const lab of row.unmappedLabels || []) {
|
||||
if (lab) list.push(`表标签未落点:${lab}`);
|
||||
}
|
||||
return [];
|
||||
for (const c of row.unmappedRequired || []) {
|
||||
if (c) list.push(`字段未映射:${c}`);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 列表角标条数 */
|
||||
function unparsedCount(row: TemplateRow): number {
|
||||
if (row.unparsedHintCount && row.unparsedHintCount > 0) return row.unparsedHintCount;
|
||||
return listMappingGaps(row).length;
|
||||
}
|
||||
|
||||
function normalizeKeywordItems(items: string[]): string[] {
|
||||
@@ -543,24 +649,29 @@ async function downloadPreviewOrOriginal() {
|
||||
{{ formatKeywords(row.keywords) || "—" }}
|
||||
</td>
|
||||
<td
|
||||
class="qt-col-muted"
|
||||
class="qt-col-muted qt-parse-cell-click"
|
||||
:class="{
|
||||
'qt-parse-fail': row.parseStatus === '失败' || row.parseStatus === '映射未完成',
|
||||
'qt-parse-gaps': listMappingGaps(row).length > 0 && row.parseStatus === '已解析',
|
||||
'qt-parse-gaps': unparsedCount(row) > 0 && row.parseStatus === '已解析',
|
||||
}"
|
||||
:title="row.parseMessage || row.parseStatus"
|
||||
:title="
|
||||
unparsedCount(row) > 0
|
||||
? `有 ${unparsedCount(row)} 项未解析,点击查看`
|
||||
: '点击查看已解析 / 未解析内容'
|
||||
"
|
||||
@click.stop="openParseDetail(row)"
|
||||
>
|
||||
<span>{{ row.parseStatus }}</span>
|
||||
<span v-if="listMappingGaps(row).length" class="qt-gap-badge">
|
||||
未解析 {{ listMappingGaps(row).length }}
|
||||
</span>
|
||||
<div class="qt-parse-status-row">
|
||||
<span class="qt-parse-status-link">{{ row.parseStatus }}</span>
|
||||
<span v-if="unparsedCount(row) > 0" class="qt-gap-badge">未解析 {{ unparsedCount(row) }}</span>
|
||||
</div>
|
||||
<div v-if="unparsedCount(row) > 0" class="qt-parse-gap-hint">有未解析内容,点击查看</div>
|
||||
<div
|
||||
v-if="
|
||||
v-else-if="
|
||||
row.parseMessage &&
|
||||
(row.parseStatus === '失败' ||
|
||||
row.parseStatus === '映射未完成' ||
|
||||
row.parseStatus === '解析中' ||
|
||||
listMappingGaps(row).length > 0)
|
||||
row.parseStatus === '解析中')
|
||||
"
|
||||
class="qt-parse-msg"
|
||||
>
|
||||
@@ -830,6 +941,86 @@ async function downloadPreviewOrOriginal() {
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 解析状态弹窗:已解析 / 未解析清单 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="parseDetailOpen && parseDetailRow"
|
||||
class="modal-overlay"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@click.self="closeParseDetail"
|
||||
>
|
||||
<div class="modal-dialog qt-parse-detail-dialog" @click.stop>
|
||||
<div class="qt-modal-head">
|
||||
<div class="qt-modal-title-row">
|
||||
<div class="qt-modal-icon qt-modal-icon--green">
|
||||
<ProtoIcon name="fileSpreadsheet" :size="16" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="qt-modal-sub">解析结果</div>
|
||||
<div class="qt-modal-title">{{ parseDetailRow.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-ghost" aria-label="关闭" @click="closeParseDetail">
|
||||
<ProtoIcon name="x" :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="qt-preview-modal-body">
|
||||
<div class="qt-preview-tags">
|
||||
<span class="tag" :class="bizTag(parseDetailRow.bizType)">{{ parseDetailRow.bizType }}</span>
|
||||
<span class="tag tag-green">已解析 {{ parseDetailState.mappedCount }} 项</span>
|
||||
<span v-if="parseDetailUnparsed.length" class="tag tag-amber"
|
||||
>未解析 {{ parseDetailUnparsed.length }} 项</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="qt-parse-panels">
|
||||
<div class="qt-parse-panel qt-parse-panel--ok">
|
||||
<div class="qt-parse-panel-title">已解析内容</div>
|
||||
<div v-if="parseDetailState.loading" class="qt-parse-panel-empty">加载映射中…</div>
|
||||
<template
|
||||
v-else-if="parseDetailState.mapping.fields.length || parseDetailState.mapping.regions.length"
|
||||
>
|
||||
<ul class="qt-parse-panel-list">
|
||||
<li v-for="f in parseDetailState.mapping.fields" :key="'f-' + f.code">
|
||||
<span class="qt-parse-name">{{ f.label }}</span>
|
||||
<code class="qt-parse-code">{{ f.code }}</code>
|
||||
<span class="qt-parse-cell">{{ f.cell }}</span>
|
||||
</li>
|
||||
<li v-for="r in parseDetailState.mapping.regions" :key="'r-' + r.kind + r.id">
|
||||
<span class="qt-parse-name"
|
||||
>{{ r.kind === "fee" ? "费用区" : "线路区" }} · {{ r.title }}</span
|
||||
>
|
||||
<code v-if="r.id" class="qt-parse-code">{{ r.id }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div v-else class="qt-parse-panel-empty">暂无已映射字段(解析中或未完成)</div>
|
||||
</div>
|
||||
<div
|
||||
class="qt-parse-panel"
|
||||
:class="parseDetailUnparsed.length ? 'qt-parse-panel--gap' : 'qt-parse-panel--ok'"
|
||||
>
|
||||
<div class="qt-parse-panel-title">未解析内容</div>
|
||||
<div v-if="parseDetailState.loading" class="qt-parse-panel-empty">加载映射中…</div>
|
||||
<ul v-else-if="parseDetailUnparsed.length" class="qt-parse-panel-list">
|
||||
<li v-for="(g, i) in parseDetailUnparsed" :key="'g-' + i">{{ g }}</li>
|
||||
</ul>
|
||||
<div v-else class="qt-parse-panel-empty">
|
||||
暂无未解析项。若刚升级功能,可点「重新解析」刷新缺口清单。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qt-preview-modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @click="closeParseDetail">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 截图放大灯箱:1:1 比例,仅关灯箱不关预览弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
@@ -855,6 +1046,95 @@ async function downloadPreviewOrOriginal() {
|
||||
max-width: min(96vw, 980px) !important;
|
||||
width: min(96vw, 980px);
|
||||
}
|
||||
.qt-parse-detail-dialog {
|
||||
max-width: min(96vw, 720px) !important;
|
||||
width: min(96vw, 720px);
|
||||
}
|
||||
.qt-parse-cell-click {
|
||||
cursor: pointer;
|
||||
}
|
||||
.qt-parse-cell-click:hover .qt-parse-status-link {
|
||||
color: #155eef;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.qt-parse-status-link {
|
||||
font-weight: 700;
|
||||
}
|
||||
.qt-parse-panels {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.qt-parse-panels {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.qt-parse-panel {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e4e7ec;
|
||||
background: #f9fafb;
|
||||
padding: 10px 12px;
|
||||
min-height: 88px;
|
||||
max-height: min(50vh, 360px);
|
||||
overflow: auto;
|
||||
}
|
||||
.qt-parse-panel--ok {
|
||||
border-color: #abefc6;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
.qt-parse-panel--gap {
|
||||
border-color: #fec84b;
|
||||
background: #fffaeb;
|
||||
}
|
||||
.qt-parse-panel-title {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #344054;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.qt-parse-panel--gap .qt-parse-panel-title {
|
||||
color: #b54708;
|
||||
}
|
||||
.qt-parse-panel-empty {
|
||||
font-size: 11px;
|
||||
color: #98a2b3;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.qt-parse-panel-list {
|
||||
margin: 0;
|
||||
padding-left: 16px;
|
||||
font-size: 11px;
|
||||
color: #344054;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.qt-parse-panel-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.qt-parse-name {
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.qt-parse-code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 10px;
|
||||
color: #475467;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ec;
|
||||
border-radius: 3px;
|
||||
padding: 0 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
.qt-parse-cell {
|
||||
color: #667085;
|
||||
font-size: 10px;
|
||||
}
|
||||
.tag-amber {
|
||||
background: #fffaeb;
|
||||
color: #b54708;
|
||||
border: 1px solid #fec84b;
|
||||
}
|
||||
.qt-col-name {
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
@@ -875,9 +1155,21 @@ async function downloadPreviewOrOriginal() {
|
||||
.qt-parse-gaps {
|
||||
color: #b45309;
|
||||
}
|
||||
.qt-parse-status-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.qt-parse-gap-hint {
|
||||
margin-top: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #b54708;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.qt-gap-badge {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 报价模板预览:Excel 样式网格(上传原文),辅以映射摘要。
|
||||
* 主数据来自 /previewSheet;mappingJson 仅用于未映射必填提示。
|
||||
* 主数据来自 /previewSheet;mappingJson 用于已映射/未映射清单。
|
||||
*/
|
||||
|
||||
/** 系统字段 code → 中文标签 */
|
||||
@@ -27,8 +27,131 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
weight_kg: "重量",
|
||||
volume_cbm: "体积",
|
||||
total_amount: "报价合计",
|
||||
chargeable_weight_kg: "计费重",
|
||||
delivery_address: "送货地址",
|
||||
pickup_address: "提货地址",
|
||||
transit_time: "运输时效",
|
||||
cargo_value: "货值",
|
||||
hs_code: "HS编码",
|
||||
oil_elec_mag: "油电磁",
|
||||
has_battery: "是否带电",
|
||||
on_board_date: "开船日",
|
||||
special_remarks: "特殊备注",
|
||||
eta_request: "要求到港",
|
||||
sailing_schedule: "船期",
|
||||
shipping_line: "船公司",
|
||||
flight_no: "航班号",
|
||||
transfer_airport: "中转机场",
|
||||
effective_date: "生效日期",
|
||||
fuel_surcharge_rate: "燃油附加费率",
|
||||
tax_rate: "税率",
|
||||
has_oil: "是否含油",
|
||||
has_magnet: "是否含磁",
|
||||
vehicle_qty: "车型/数量",
|
||||
};
|
||||
|
||||
/** 解析结果摘要:供预览弹窗展示「已解析 / 未解析」 */
|
||||
export type MappingFieldItem = {
|
||||
code: string;
|
||||
label: string;
|
||||
cell: string;
|
||||
};
|
||||
|
||||
export type MappingRegionItem = {
|
||||
kind: "fee" | "route";
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type MappingParseSummary = {
|
||||
fields: MappingFieldItem[];
|
||||
regions: MappingRegionItem[];
|
||||
gaps: string[];
|
||||
unmappedRequired: string[];
|
||||
unmappedLabels: string[];
|
||||
pointCount: number;
|
||||
};
|
||||
|
||||
function asMappingObject(mappingJson: unknown): Record<string, any> | null {
|
||||
if (!mappingJson) return null;
|
||||
if (typeof mappingJson === "object") return mappingJson as Record<string, any>;
|
||||
if (typeof mappingJson === "string") {
|
||||
try {
|
||||
const obj = JSON.parse(mappingJson);
|
||||
return obj && typeof obj === "object" ? obj : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 mappingJson 拆出运营可读的已映射 / 未映射清单。
|
||||
* 不打接口;mappingGaps 为空时(旧版解析)仍可看 fields 与 unmapped*。
|
||||
*/
|
||||
export function summarizeMappingParse(mappingJson: unknown): MappingParseSummary {
|
||||
const obj = asMappingObject(mappingJson);
|
||||
const empty: MappingParseSummary = {
|
||||
fields: [],
|
||||
regions: [],
|
||||
gaps: [],
|
||||
unmappedRequired: [],
|
||||
unmappedLabels: [],
|
||||
pointCount: 0,
|
||||
};
|
||||
if (!obj) return empty;
|
||||
|
||||
const fieldsObj = obj.fields && typeof obj.fields === "object" ? obj.fields : {};
|
||||
const fields: MappingFieldItem[] = Object.keys(fieldsObj)
|
||||
.filter((k) => fieldsObj[k])
|
||||
.map((code) => ({
|
||||
code,
|
||||
label: FIELD_LABELS[code] || code,
|
||||
cell: String(fieldsObj[code] || ""),
|
||||
}))
|
||||
.sort((a, b) => a.label.localeCompare(b.label, "zh"));
|
||||
|
||||
const regions: MappingRegionItem[] = [];
|
||||
const feeRegions = Array.isArray(obj.feeRegions) ? obj.feeRegions : [];
|
||||
for (const r of feeRegions) {
|
||||
if (!r || typeof r !== "object") continue;
|
||||
regions.push({
|
||||
kind: "fee",
|
||||
id: String(r.id || ""),
|
||||
title: String(r.title || r.id || "费用区"),
|
||||
});
|
||||
}
|
||||
const routeRegions = Array.isArray(obj.routeRegions) ? obj.routeRegions : [];
|
||||
for (const r of routeRegions) {
|
||||
if (!r || typeof r !== "object") continue;
|
||||
regions.push({
|
||||
kind: "route",
|
||||
id: String(r.id || ""),
|
||||
title: String(r.title || r.id || "线路区"),
|
||||
});
|
||||
}
|
||||
|
||||
const gaps = Array.isArray(obj.mappingGaps)
|
||||
? obj.mappingGaps.map((x: any) => String(x)).filter(Boolean)
|
||||
: [];
|
||||
const unmappedRequired = Array.isArray(obj.unmappedRequired)
|
||||
? obj.unmappedRequired.map((x: any) => String(x)).filter(Boolean)
|
||||
: [];
|
||||
const unmappedLabels = Array.isArray(obj.unmappedLabels)
|
||||
? obj.unmappedLabels.map((x: any) => String(x)).filter(Boolean)
|
||||
: [];
|
||||
|
||||
return {
|
||||
fields,
|
||||
regions,
|
||||
gaps,
|
||||
unmappedRequired,
|
||||
unmappedLabels,
|
||||
pointCount: fields.length + regions.length,
|
||||
};
|
||||
}
|
||||
|
||||
export type SheetPreviewCell = {
|
||||
addr: string;
|
||||
text: string;
|
||||
|
||||
Reference in New Issue
Block a user