Merge remote-tracking branch 'refs/remotes/origin/master' into feature/bfi-api-rate
# Conflicts: # pnpm-lock.yaml # src/axios.js # src/views/settlement/formal-settlement.vue # src/views/vehicle/customer-archive.vue
This commit is contained in:
+24
-1
@@ -6,8 +6,31 @@ const RefreshTokenKey = website.refreshTokenKey;
|
||||
const SessionId = 'JSESSIONID';
|
||||
const UserId = 'b-user-id';
|
||||
|
||||
/** 超级管理员角色别名 */
|
||||
export const ADMINISTRATOR_ROLE_ALIAS = 'administrator';
|
||||
|
||||
/**
|
||||
* 是否超级管理员:角色别名中包含 administrator 即为超级管理员
|
||||
*
|
||||
* @param {object|string|string[]} [userInfoOrAuthority] 用户信息,或 authority / role_name 字符串/数组
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAdministrator(userInfoOrAuthority) {
|
||||
let authority = userInfoOrAuthority;
|
||||
if (authority && typeof authority === 'object' && !Array.isArray(authority)) {
|
||||
authority = authority.authority ?? authority.role_name ?? authority.roleName ?? '';
|
||||
}
|
||||
const roles = Array.isArray(authority)
|
||||
? authority
|
||||
: String(authority || '')
|
||||
.split(/[,,]/)
|
||||
.map(role => role.trim())
|
||||
.filter(Boolean);
|
||||
return roles.some(role => String(role).trim() === ADMINISTRATOR_ROLE_ALIAS);
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(TokenKey);
|
||||
return Cookies.get(TokenKey);
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
|
||||
@@ -28,6 +28,34 @@ const parseBlobJson = async blob => {
|
||||
return text ? JSON.parse(text) : {};
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析导入失败时的错误提示文案。
|
||||
*
|
||||
* 背景:导入接口统一用 `responseType: 'blob'` 请求,因此**非 2xx 响应体也是 Blob**。
|
||||
* axios 的错误分支不做解析,直接 reject,`error.message` 只有
|
||||
* 「Request failed with status code 400」这类通用文案(或全局兜底的「系统错误」),
|
||||
* 用户会误以为系统崩了——而后端其实已返回了明确的中文业务提示(如「导入数据不能为空」)。
|
||||
*
|
||||
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
|
||||
*/
|
||||
const resolveErrorMessage = async error => {
|
||||
const blob = error?.response?.data ?? error?.data;
|
||||
if (blob instanceof Blob) {
|
||||
try {
|
||||
const result = await parseBlobJson(blob);
|
||||
if (result?.msg) return result.msg;
|
||||
} catch (ignored) {
|
||||
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
|
||||
}
|
||||
}
|
||||
const fallback = error?.response?.data?.msg || error?.message;
|
||||
// axios 的英文通用文案对用户没有意义,统一换成中文提示
|
||||
if (!fallback || /Request failed with status code/i.test(fallback)) {
|
||||
return '导入失败,请检查导入文件后重试';
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加工后端返回的导入失败明细 Excel。
|
||||
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
|
||||
@@ -125,7 +153,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
|
||||
option.onSuccess(res);
|
||||
}
|
||||
} catch (error) {
|
||||
vm.$message.error(error.message || '导入失败');
|
||||
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
|
||||
// 只保留一次清晰的中文提示。
|
||||
if (!error?._notified) {
|
||||
vm.$message.error(await resolveErrorMessage(error));
|
||||
}
|
||||
if (typeof option.onError === 'function') {
|
||||
option.onError(error);
|
||||
}
|
||||
|
||||
+138
-15
@@ -1,6 +1,59 @@
|
||||
/** 地点搜索 v5/place/text 使用的「Web 服务」类型 Key */
|
||||
export const AMAP_WEB_SERVICE_KEY = 'b43016a0d1f5b0c32211560392059708';
|
||||
|
||||
/**
|
||||
* 将高德 Geocoder 结果规范化为地图选址浮层列表数据。
|
||||
* @param {Array|Object} geocodesOrResult geocodes 数组,或含 geocodes/location 的结果对象
|
||||
* 高德地点关键词搜索(Web 服务 v5)。
|
||||
* @see https://restapi.amap.com/v5/place/text
|
||||
* @param {string} keyword 地点关键字
|
||||
* @param {{ key?: string, pageSize?: number, pageNum?: number, region?: string, cityLimit?: boolean }} options
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
export function searchAmapPlaceText(keyword, options = {}) {
|
||||
const key = options.key || AMAP_WEB_SERVICE_KEY;
|
||||
const pageSize = options.pageSize || 20;
|
||||
const pageNum = options.pageNum || 1;
|
||||
if (!key) {
|
||||
return Promise.reject(new Error('高德地图 Key 未配置'));
|
||||
}
|
||||
const trimmed = String(keyword || '').trim();
|
||||
if (!trimmed) {
|
||||
return Promise.reject(new Error('请输入地址关键词'));
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
key,
|
||||
keywords: trimmed,
|
||||
page_size: String(pageSize),
|
||||
page_num: String(pageNum),
|
||||
show_fields: 'photos',
|
||||
});
|
||||
if (options.region) {
|
||||
params.set('region', options.region);
|
||||
}
|
||||
if (options.cityLimit != null) {
|
||||
params.set('city_limit', options.cityLimit ? 'true' : 'false');
|
||||
}
|
||||
|
||||
return fetch(`https://restapi.amap.com/v5/place/text?${params.toString()}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('高德地点搜索请求失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (String(data?.status) === '1') {
|
||||
return Array.isArray(data.pois) ? data.pois : [];
|
||||
}
|
||||
const error = new Error(data?.info || '高德地点搜索失败');
|
||||
error.code = data?.infocode;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将高德 Geocoder / place/text 结果规范化为地图选址浮层列表数据。
|
||||
* @param {Array|Object} geocodesOrResult geocodes/pois 数组,或含 geocodes/pois/location 的结果对象
|
||||
* @param {string} keyword 搜索关键词兜底文案
|
||||
* @returns {Array<{id: string|number, name: string, address: string, location: any, photo: string}>}
|
||||
*/
|
||||
@@ -10,25 +63,95 @@ export function normalizeMapSearchResults(geocodesOrResult, keyword = '') {
|
||||
if (Array.isArray(geocodesOrResult)) {
|
||||
list = geocodesOrResult;
|
||||
} else if (geocodesOrResult && typeof geocodesOrResult === 'object') {
|
||||
if (Array.isArray(geocodesOrResult.geocodes)) {
|
||||
if (Array.isArray(geocodesOrResult.pois)) {
|
||||
list = geocodesOrResult.pois;
|
||||
} else if (Array.isArray(geocodesOrResult.geocodes)) {
|
||||
list = geocodesOrResult.geocodes;
|
||||
} else if (geocodesOrResult.location) {
|
||||
list = [geocodesOrResult];
|
||||
}
|
||||
}
|
||||
|
||||
return list.map((item, index) => {
|
||||
const buildingName = pickNamedField(item?.building);
|
||||
const neighborhoodName = pickNamedField(item?.neighborhood);
|
||||
const address = item?.formattedAddress || item?.address || fallback || '-';
|
||||
return {
|
||||
id: item?.id || index,
|
||||
name: buildingName || neighborhoodName || address,
|
||||
address,
|
||||
location: item?.location,
|
||||
photo: item?.photo || item?.image || '',
|
||||
};
|
||||
});
|
||||
return list
|
||||
.map((item, index) => {
|
||||
if (!item) return null;
|
||||
if (isPlacePoi(item)) {
|
||||
const address = buildPoiAddress(item) || fallback || '-';
|
||||
return {
|
||||
id: item.id || index,
|
||||
name: String(item.name || '').trim() || address,
|
||||
address,
|
||||
location: parseMapLocation(item.location),
|
||||
photo: pickPoiPhoto(item),
|
||||
};
|
||||
}
|
||||
|
||||
const buildingName = pickNamedField(item?.building);
|
||||
const neighborhoodName = pickNamedField(item?.neighborhood);
|
||||
const address = item?.formattedAddress || item?.address || fallback || '-';
|
||||
return {
|
||||
id: item?.id || index,
|
||||
name: buildingName || neighborhoodName || address,
|
||||
address,
|
||||
location: parseMapLocation(item?.location),
|
||||
photo: item?.photo || item?.image || '',
|
||||
};
|
||||
})
|
||||
.filter(item => item && item.location);
|
||||
}
|
||||
|
||||
export function parseMapLocation(location) {
|
||||
if (!location) return null;
|
||||
if (typeof location.getLng === 'function' && typeof location.getLat === 'function') {
|
||||
return location;
|
||||
}
|
||||
if (typeof location === 'string') {
|
||||
const parts = location.split(',');
|
||||
const lng = Number(parts[0]);
|
||||
const lat = Number(parts[1]);
|
||||
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
|
||||
return { lng, lat };
|
||||
}
|
||||
if (Number.isFinite(Number(location.lng)) && Number.isFinite(Number(location.lat))) {
|
||||
return { lng: Number(location.lng), lat: Number(location.lat) };
|
||||
}
|
||||
if (
|
||||
Number.isFinite(Number(location.longitude)) &&
|
||||
Number.isFinite(Number(location.latitude))
|
||||
) {
|
||||
return { lng: Number(location.longitude), lat: Number(location.latitude) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPlacePoi(item) {
|
||||
return Boolean(item && item.name && item.location && (item.id || item.typecode || item.adname));
|
||||
}
|
||||
|
||||
function buildPoiAddress(item) {
|
||||
const detail = String(item.address || '').trim();
|
||||
const region = [item.pname, item.cityname, item.adname]
|
||||
.map(value => String(value || '').trim())
|
||||
.filter(Boolean);
|
||||
// 直辖市等场景省市名可能重复
|
||||
const uniqueRegion = region.filter((value, index) => region.indexOf(value) === index);
|
||||
const prefix = uniqueRegion.join('');
|
||||
if (prefix && detail) {
|
||||
if (detail.startsWith(prefix)) return detail;
|
||||
return `${prefix}${detail}`;
|
||||
}
|
||||
return detail || prefix || String(item.name || '').trim();
|
||||
}
|
||||
|
||||
function pickPoiPhoto(item) {
|
||||
const photos = item?.photos;
|
||||
if (Array.isArray(photos) && photos.length) {
|
||||
return photos[0]?.url || '';
|
||||
}
|
||||
if (photos && typeof photos === 'object') {
|
||||
return photos.url || '';
|
||||
}
|
||||
return item?.photo || item?.image || '';
|
||||
}
|
||||
|
||||
function pickNamedField(value) {
|
||||
|
||||
+65
-10
@@ -1,5 +1,5 @@
|
||||
import { getDictionary } from '@/api/system/dictbiz';
|
||||
import { processDelete, processSubmit } from '@/api/system/business-process';
|
||||
import { hasMkProcessInstance, processDelete, processSubmit } from '@/api/system/business-process';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
export const MK_BIZ = {
|
||||
@@ -7,36 +7,55 @@ export const MK_BIZ = {
|
||||
dictName: '提交客商审核流',
|
||||
subjectPrefix: '客商准入审批',
|
||||
rejected: ['rejected'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'project-apply': {
|
||||
dictName: '提交项目审核流',
|
||||
subjectPrefix: '项目审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'contract-manage': {
|
||||
dictName: '提交合同审核流',
|
||||
subjectPrefix: '合同审批',
|
||||
rejected: ['rejected', 'change_rejected', 'withdrawn'],
|
||||
},
|
||||
'waybill-manage': {
|
||||
dictName: '提交运单审核流',
|
||||
subjectPrefix: '运单审批',
|
||||
rejected: ['rejected', 'returned'],
|
||||
approved: ['approved', 'change_approved'],
|
||||
},
|
||||
'pre-settlement': {
|
||||
dictName: '提交预结算审核流',
|
||||
subjectPrefix: '预结算审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'formal-settlement': {
|
||||
dictName: '提交正式结算审核流',
|
||||
subjectPrefix: '正式结算审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'payment-application': {
|
||||
dictName: '提交付款审核流',
|
||||
subjectPrefix: '付款审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'invoice-application': {
|
||||
dictName: '提交开票审核流',
|
||||
subjectPrefix: '开票审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'invoice-receipt': {
|
||||
dictName: '提交收票审核流',
|
||||
subjectPrefix: '收票审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
'bill-payment': {
|
||||
dictName: '提交汇票付款审核流',
|
||||
subjectPrefix: '汇票付款审批',
|
||||
rejected: ['returned'],
|
||||
approved: ['approved'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,6 +72,12 @@ export function isMkEnabled(list = []) {
|
||||
return String(matched?.dictKey ?? '').trim() === '1';
|
||||
}
|
||||
|
||||
/** 加载业务字典并判断是否开启 MK */
|
||||
export async function loadMkEnabled() {
|
||||
const list = await loadMkTemplateList();
|
||||
return isMkEnabled(list);
|
||||
}
|
||||
|
||||
export async function resolveMkTemplateCode(dictName, list) {
|
||||
const dictList = list || (await loadMkTemplateList());
|
||||
const matched = dictList.find(item => String(item.dictValue || '').trim() === dictName);
|
||||
@@ -64,9 +89,26 @@ export async function resolveMkTemplateCode(dictName, list) {
|
||||
return String(templateCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否属于「审核通过后再提交」需要新建表单实例的状态
|
||||
*/
|
||||
export function isMkApprovedStatus(bizType, approvalStatus) {
|
||||
const conf = MK_BIZ[bizType];
|
||||
if (!conf) return false;
|
||||
return (conf.approved || []).includes(approvalStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交 MK 审核流
|
||||
* @param {Object} options
|
||||
* @param {string} options.bizType 业务类型
|
||||
* @param {string|number} options.bizId 业务主键(后端据此生成/复用 formInstanceId)
|
||||
* @param {string} [options.subjectName] 标题后缀
|
||||
* @param {string} [options.approvalStatus] 提交前的审批状态(须在业务 submit 改状态前取值;通过态强制新建实例)
|
||||
*/
|
||||
export async function submitMkApprovalFlow({
|
||||
bizType,
|
||||
formInstanceId,
|
||||
bizId,
|
||||
subjectName = '',
|
||||
approvalStatus = '',
|
||||
}) {
|
||||
@@ -74,21 +116,34 @@ export async function submitMkApprovalFlow({
|
||||
if (!conf) {
|
||||
return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`));
|
||||
}
|
||||
if (bizId == null || bizId === '') {
|
||||
return Promise.reject(new Error('业务id不能为空'));
|
||||
}
|
||||
const list = await loadMkTemplateList();
|
||||
if (!isMkEnabled(list)) {
|
||||
return;
|
||||
}
|
||||
const bizIdText = String(bizId);
|
||||
const forceNewFormInstance = isMkApprovedStatus(bizType, approvalStatus);
|
||||
// 审核通过后再提交必须新建流程;其余场景已有有效实例则跳过
|
||||
if (!forceNewFormInstance) {
|
||||
const existsRes = await hasMkProcessInstance({ bizId: bizIdText });
|
||||
if (existsRes?.data?.data) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ((conf.rejected || []).includes(approvalStatus)) {
|
||||
await processDelete({ formInstanceId: String(formInstanceId) });
|
||||
await processDelete({ bizId: bizIdText });
|
||||
}
|
||||
const templateCode = await resolveMkTemplateCode(conf.dictName, list);
|
||||
const subject = subjectName
|
||||
? `${conf.subjectPrefix}:${subjectName}`
|
||||
: `${conf.subjectPrefix}:${formInstanceId}`;
|
||||
: `${conf.subjectPrefix}:${bizIdText}`;
|
||||
return processSubmit({
|
||||
templateCode,
|
||||
formInstanceId: String(formInstanceId),
|
||||
bizId: bizIdText,
|
||||
subject,
|
||||
bizType,
|
||||
forceNewFormInstance,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user