mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2026-05-13 02:42:41 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c164904a14 | ||
|
|
a0ceb45df9 | ||
|
|
c641542c71 | ||
|
|
a3d8e4bfc1 | ||
|
|
e385823d46 | ||
|
|
897220e19a | ||
|
|
b293e112c6 | ||
|
|
627e31f1b0 | ||
|
|
8020b4b743 | ||
|
|
228c5463da | ||
|
|
50ee691191 | ||
|
|
eda6ffaf1e | ||
|
|
e136679934 |
@@ -128,6 +128,49 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
},
|
||||
});
|
||||
|
||||
// add by 芋艿:对应 https://t.zsxq.com/SHqWw 反馈
|
||||
// 处理 Blob 响应中的业务错误(如 401):后端把「账号未登录」包成 HTTP 200 + body {code: 401, msg: ...},
|
||||
// download 强制 responseType: 'blob' 后被 axios 包成 application/json 的 Blob,defaultResponseInterceptor 走
|
||||
// responseReturn === 'body' 分支直接返回,绕过了 authenticateResponseInterceptor 的 401 token 刷新;
|
||||
// 这里把这种 Blob 解析回 JSON,再以 axios 风格抛出,让后续拦截器接管
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: async (response) => {
|
||||
const blob = response.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
return response;
|
||||
}
|
||||
// Blob.type 在部分环境可能为空或大小写不一,叠加 response header 一起判断更稳
|
||||
const blobType = (blob.type || '').toLowerCase();
|
||||
const headerType = String(
|
||||
response.headers?.['content-type'] ??
|
||||
response.headers?.['Content-Type'] ??
|
||||
'',
|
||||
).toLowerCase();
|
||||
if (
|
||||
!blobType.includes('application/json') &&
|
||||
!headerType.includes('application/json')
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(await blob.text());
|
||||
} catch {
|
||||
return response;
|
||||
}
|
||||
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
||||
response.data = parsed;
|
||||
throw Object.assign(new Error(parsed.msg ?? 'Request failed'), {
|
||||
config: response.config,
|
||||
response,
|
||||
data: parsed,
|
||||
isAxiosError: true,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
|
||||
@@ -6,18 +6,36 @@ export function useImagesUpload() {
|
||||
return defineComponent({
|
||||
name: 'ImagesUpload',
|
||||
props: {
|
||||
multiple: {
|
||||
accept: {
|
||||
type: Array,
|
||||
default: () => ['image/jpeg', 'image/png', 'image/gif'],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
|
||||
<ImageUpload
|
||||
accept={props.accept as string[]}
|
||||
disabled={props.disabled}
|
||||
maxNumber={props.maxNumber}
|
||||
maxSize={props.maxSize}
|
||||
multiple={props.multiple}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useUploadFileRule() {
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '文件类型',
|
||||
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
|
||||
options: [
|
||||
@@ -40,12 +40,6 @@ export function useUploadFileRule() {
|
||||
mode: 'multiple',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'autoUpload',
|
||||
title: '是否在选取文件后立即进行上传',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
@@ -54,23 +48,23 @@ export function useUploadFileRule() {
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'isShowTip',
|
||||
field: 'showDescription',
|
||||
title: '是否显示提示',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
field: 'maxNumber',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
props: { min: 1 },
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
|
||||
@@ -24,15 +24,9 @@ export function useUploadImageRule() {
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
@@ -52,40 +46,16 @@ export function useUploadImageRule() {
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否显示删除按钮',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'showBtnText',
|
||||
title: '是否显示按钮文字',
|
||||
value: true,
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -24,15 +24,9 @@ export function useUploadImagesRule() {
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
@@ -48,40 +42,27 @@ export function useUploadImagesRule() {
|
||||
],
|
||||
props: {
|
||||
mode: 'multiple',
|
||||
maxNumber: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
field: 'maxNumber',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
props: { min: 1 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -175,7 +175,7 @@ const resetCustomConfigList = () => {
|
||||
approveType.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:ApproveType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
|
||||
value: ApproveType.USER,
|
||||
});
|
||||
@@ -184,7 +184,7 @@ const resetCustomConfigList = () => {
|
||||
assignStartUserHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
|
||||
value: 1,
|
||||
});
|
||||
@@ -194,13 +194,13 @@ const resetCustomConfigList = () => {
|
||||
rejectHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
|
||||
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
|
||||
returnNodeIdEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
|
||||
value: '',
|
||||
});
|
||||
@@ -210,7 +210,7 @@ const resetCustomConfigList = () => {
|
||||
assignEmptyHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
|
||||
value: 1,
|
||||
});
|
||||
@@ -218,7 +218,7 @@ const resetCustomConfigList = () => {
|
||||
assignEmptyUserIdsEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
|
||||
value: '',
|
||||
});
|
||||
|
||||
@@ -767,6 +767,14 @@ export const COMPARISON_OPERATORS: DictDataType[] = [
|
||||
value: '<=',
|
||||
label: '小于等于',
|
||||
},
|
||||
{
|
||||
value: 'contain',
|
||||
label: '包含',
|
||||
},
|
||||
{
|
||||
value: '!contain',
|
||||
label: '不包含',
|
||||
},
|
||||
];
|
||||
// 审批操作按钮名称
|
||||
export const OPERATION_BUTTON_NAME = new Map<number, string>();
|
||||
|
||||
@@ -24,6 +24,7 @@ import { useUserStore } from '@vben/stores';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import FormCreate from '@form-create/ant-design-vue';
|
||||
import { until, useDebounceFn } from '@vueuse/core';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -113,6 +114,8 @@ const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
|
||||
[],
|
||||
); // 下一个审批节点信息
|
||||
const nextAssigneesTimelineRef = ref(); // 下一个节点审批人时间线组件的引用
|
||||
let nextApprovalRequestId = 0; // 请求序号;onChange 高频触发时,丢弃过期请求结果
|
||||
let pendingNextNodesTask: null | Promise<unknown> = null; // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成
|
||||
const approveReasonForm: any = reactive({
|
||||
reason: '',
|
||||
signPicUrl: '',
|
||||
@@ -256,7 +259,6 @@ async function openPopover(type: string) {
|
||||
message.warning('表单校验不通过,请先完善表单!!');
|
||||
return;
|
||||
}
|
||||
await initNextAssigneesFormField();
|
||||
}
|
||||
if (type === 'return') {
|
||||
// 获取退回节点
|
||||
@@ -269,6 +271,20 @@ async function openPopover(type: string) {
|
||||
Object.keys(popOverVisible.value).forEach((item) => {
|
||||
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
|
||||
});
|
||||
if (type === 'approve') {
|
||||
// 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点;
|
||||
// 没有节点表单时,approveFormFApi 永远不会被赋值,跳过等待
|
||||
if (runningTask.value?.formId > 0) {
|
||||
// 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行
|
||||
await until(
|
||||
() => typeof approveFormFApi.value?.validate === 'function',
|
||||
)
|
||||
.toBeTruthy({ timeout: 1000 })
|
||||
.catch(() => {});
|
||||
}
|
||||
// 初始化下一个审批人表单字段
|
||||
await initNextAssigneesFormField();
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭气泡卡 */
|
||||
@@ -286,6 +302,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
|
||||
|
||||
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
|
||||
async function initNextAssigneesFormField() {
|
||||
// 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废
|
||||
const requestId = ++nextApprovalRequestId;
|
||||
// 获取修改的流程变量, 暂时只支持流程表单
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
const data = await getNextApprovalNodes({
|
||||
@@ -293,6 +311,12 @@ async function initNextAssigneesFormField() {
|
||||
taskId: runningTask.value.id,
|
||||
processVariablesStr: JSON.stringify(variables),
|
||||
});
|
||||
// 已有更新的请求发出,丢弃本次过期结果,避免把旧分支节点回写到当前列表
|
||||
if (requestId !== nextApprovalRequestId) {
|
||||
return;
|
||||
}
|
||||
// 在最新结果到达时再清空,避免请求期间出现节点信息抖动
|
||||
nextAssigneesActivityNode.value = [];
|
||||
if (data && data.length > 0) {
|
||||
const customApproveUsersData: Record<string, any[]> = {}; // 用于收集需要设置到 Timeline 组件的自定义审批人数据
|
||||
data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
||||
@@ -327,6 +351,12 @@ async function initNextAssigneesFormField() {
|
||||
}
|
||||
}
|
||||
|
||||
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
|
||||
const debouncedInitNextAssigneesFormField = useDebounceFn(
|
||||
initNextAssigneesFormField,
|
||||
300,
|
||||
);
|
||||
|
||||
/** 选择下一个节点的审批人 */
|
||||
function selectNextAssigneesConfirm(id: string, userList: any[]) {
|
||||
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
|
||||
@@ -362,6 +392,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
}
|
||||
|
||||
if (pass) {
|
||||
// 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交
|
||||
if (pendingNextNodesTask) {
|
||||
await pendingNextNodesTask;
|
||||
}
|
||||
const nextAssigneesValid = validateNextAssignees();
|
||||
if (!nextAssigneesValid) return;
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
@@ -376,12 +410,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
if (runningTask.value.signEnable) {
|
||||
data.signPicUrl = approveReasonForm.signPicUrl;
|
||||
}
|
||||
// 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
|
||||
// TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
|
||||
// 多表单处理:节点表单需要校验;变量已经在 getUpdatedProcessInstanceVariables 中合并到 data.variables,无需再覆盖
|
||||
const formCreateApi = approveFormFApi.value;
|
||||
if (Object.keys(formCreateApi)?.length > 0) {
|
||||
await formCreateApi.validate();
|
||||
data.variables = approveForm.value.value;
|
||||
}
|
||||
await approveTask(data);
|
||||
popOverVisible.value.approve = false;
|
||||
@@ -648,18 +680,32 @@ function loadTodoTask(task: any) {
|
||||
approveForm.value = {};
|
||||
runningTask.value = task;
|
||||
approveFormFApi.value = {};
|
||||
// 切换任务时重置请求序号与 pending 重算,避免旧任务飞行中的请求/Promise 串到新任务
|
||||
nextApprovalRequestId += 1;
|
||||
pendingNextNodesTask = null;
|
||||
reasonRequire.value = task?.reasonRequire ?? false;
|
||||
nodeTypeName.value =
|
||||
task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批';
|
||||
// 处理 approve 表单
|
||||
if (task && task.formId && task.formConf) {
|
||||
const tempApproveForm = {};
|
||||
const tempApproveForm: { option?: any; rule?: any; value?: any } = {};
|
||||
setConfAndFields2(
|
||||
tempApproveForm,
|
||||
task.formConf,
|
||||
task.formFields,
|
||||
task.formVariables,
|
||||
);
|
||||
// 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段
|
||||
tempApproveForm.option.onChange = () => {
|
||||
// 弹窗打开时,才重新计算下一个节点的信息
|
||||
if (!popOverVisible.value.approve) {
|
||||
return;
|
||||
}
|
||||
// useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled'
|
||||
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(
|
||||
() => {},
|
||||
);
|
||||
};
|
||||
approveForm.value = tempApproveForm;
|
||||
} else {
|
||||
approveForm.value = {}; // 占位,避免为空
|
||||
@@ -684,9 +730,17 @@ async function validateNormalForm() {
|
||||
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
|
||||
function getUpdatedProcessInstanceVariables() {
|
||||
const variables: any = {};
|
||||
props.writableFields.forEach((field: string) => {
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
// 从流程表单(流程定义级别)中获取变量
|
||||
if (props.writableFields?.length && props.normalFormApi) {
|
||||
props.writableFields.forEach((field: string) => {
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
}
|
||||
// 从节点表单(节点级别)中获取变量;通过 form-create 官方的 formData() 拿当前值
|
||||
const nodeFormData = approveFormFApi.value?.formData?.();
|
||||
if (nodeFormData) {
|
||||
Object.assign(variables, nodeFormData);
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { schema } from './data';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelClass: 'w-100',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { schema } from './data';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelClass: 'w-100',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema,
|
||||
|
||||
@@ -29,7 +29,10 @@ function handleChange(val: any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form.Item label="元素类型" name="property.dataSpecs.childDataType">
|
||||
<Form.Item
|
||||
:name="['property', 'dataSpecs', 'childDataType']"
|
||||
label="元素类型"
|
||||
>
|
||||
<Radio.Group v-model:value="dataSpecs.childDataType" @change="handleChange">
|
||||
<template v-for="item in getDataTypeOptions()" :key="item.value">
|
||||
<Radio
|
||||
@@ -50,7 +53,7 @@ function handleChange(val: any) {
|
||||
</template>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="元素个数" name="property.dataSpecs.size">
|
||||
<Form.Item :name="['property', 'dataSpecs', 'size']" label="元素个数">
|
||||
<Input
|
||||
v-model:value="dataSpecs.size"
|
||||
placeholder="请输入数组中的元素个数"
|
||||
|
||||
@@ -35,9 +35,9 @@ watch(
|
||||
|
||||
<template>
|
||||
<Form.Item
|
||||
:name="['event', 'type']"
|
||||
:rules="[{ required: true, message: '请选择事件类型', trigger: 'change' }]"
|
||||
label="事件类型"
|
||||
name="event.type"
|
||||
>
|
||||
<Radio.Group v-model:value="thingModelEvent.type">
|
||||
<Radio
|
||||
|
||||
@@ -154,8 +154,8 @@ watch(
|
||||
<!-- 文本型配置 -->
|
||||
<Form.Item
|
||||
v-if="property.dataType === IoTDataSpecsDataTypeEnum.TEXT"
|
||||
:name="['property', 'dataSpecs', 'length']"
|
||||
label="数据长度"
|
||||
name="property.dataSpecs.length"
|
||||
>
|
||||
<Input
|
||||
v-model:value="property.dataSpecs.length"
|
||||
@@ -189,8 +189,8 @@ watch(
|
||||
/>
|
||||
<Form.Item
|
||||
v-if="!isStructDataSpecs && !isParams"
|
||||
:name="['property', 'accessMode']"
|
||||
label="读写类型"
|
||||
name="property.accessMode"
|
||||
>
|
||||
<Radio.Group v-model:value="property.accessMode">
|
||||
<Radio
|
||||
|
||||
@@ -35,9 +35,9 @@ watch(
|
||||
|
||||
<template>
|
||||
<Form.Item
|
||||
:name="['service', 'callType']"
|
||||
:rules="[{ required: true, message: '请选择调用方式', trigger: 'change' }]"
|
||||
label="调用方式"
|
||||
name="service.callType"
|
||||
>
|
||||
<Radio.Group v-model:value="service.callType">
|
||||
<Radio
|
||||
|
||||
@@ -89,10 +89,10 @@ export function useGridColumns(
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '商品售价',
|
||||
title: '商品售价(元)',
|
||||
minWidth: 90,
|
||||
align: 'center',
|
||||
formatter: 'formatAmount2',
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'salesCount',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { fenToYuan, handleTree, treeToString } from '@vben/utils';
|
||||
import { handleTree, treeToString } from '@vben/utils';
|
||||
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
@@ -101,25 +101,21 @@ export function useGridColumns(
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '价格',
|
||||
title: '价格(元)',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '市场价',
|
||||
title: '市场价(元)',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return `${fenToYuan(row.marketPrice)} 元`;
|
||||
},
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'costPrice',
|
||||
title: '成本价',
|
||||
title: '成本价(元)',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return `${fenToYuan(row.costPrice)} 元`;
|
||||
},
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'salesCount',
|
||||
|
||||
@@ -181,31 +181,28 @@ async function handleSubmit() {
|
||||
.merge(descriptionFormApi)
|
||||
.merge(otherFormApi)
|
||||
.submitAllForm(true);
|
||||
values.skus = formData.value.skus;
|
||||
// 校验商品名称不能为空(用于 SKU name)
|
||||
if (!values.name || values.name.trim() === '') {
|
||||
message.error('商品名称不能为空');
|
||||
return;
|
||||
}
|
||||
if (values.skus) {
|
||||
try {
|
||||
// 校验 sku
|
||||
skuListRef.value.validateSku();
|
||||
} catch {
|
||||
message.error('【库存价格】不完善,请填写相关信息');
|
||||
return;
|
||||
}
|
||||
values.skus.forEach((item) => {
|
||||
// 给 sku name 赋值(使用商品名称作为 SKU 名称)
|
||||
item.name = values.name;
|
||||
// 金额转换:元转分
|
||||
item.price = convertToInteger(item.price);
|
||||
item.marketPrice = convertToInteger(item.marketPrice);
|
||||
item.costPrice = convertToInteger(item.costPrice);
|
||||
item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice);
|
||||
});
|
||||
try {
|
||||
// 校验 sku
|
||||
skuListRef.value.validateSku();
|
||||
} catch {
|
||||
message.error('【库存价格】不完善,请填写相关信息');
|
||||
return;
|
||||
}
|
||||
// 金额转换:元转分
|
||||
values.skus = formData.value.skus!.map((item) => ({
|
||||
...item,
|
||||
name: values.name,
|
||||
price: convertToInteger(item.price),
|
||||
marketPrice: convertToInteger(item.marketPrice),
|
||||
costPrice: convertToInteger(item.costPrice),
|
||||
firstBrokeragePrice: convertToInteger(item.firstBrokeragePrice),
|
||||
secondBrokeragePrice: convertToInteger(item.secondBrokeragePrice),
|
||||
}));
|
||||
// 处理轮播图列表:上传组件可能返回对象或字符串,统一处理成字符串数组
|
||||
const newSliderPicUrls: any[] = [];
|
||||
values.sliderPicUrls!.forEach((item: any) => {
|
||||
@@ -235,14 +232,15 @@ async function getDetail() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
const res = await getSpu(spuId.value!);
|
||||
// 金额转换:元转分
|
||||
res.skus?.forEach((item) => {
|
||||
item.price = formatToFraction(item.price);
|
||||
item.marketPrice = formatToFraction(item.marketPrice);
|
||||
item.costPrice = formatToFraction(item.costPrice);
|
||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice);
|
||||
});
|
||||
// 金额转换:分转元
|
||||
res.skus = res.skus?.map((item) => ({
|
||||
...item,
|
||||
price: formatToFraction(item.price),
|
||||
marketPrice: formatToFraction(item.marketPrice),
|
||||
costPrice: formatToFraction(item.costPrice),
|
||||
firstBrokeragePrice: formatToFraction(item.firstBrokeragePrice),
|
||||
secondBrokeragePrice: formatToFraction(item.secondBrokeragePrice),
|
||||
}));
|
||||
formData.value = res;
|
||||
// 初始化各表单值
|
||||
infoFormApi.setValues(res).then();
|
||||
|
||||
@@ -128,6 +128,49 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
},
|
||||
});
|
||||
|
||||
// add by 芋艿:对应 https://t.zsxq.com/SHqWw 反馈
|
||||
// 处理 Blob 响应中的业务错误(如 401):后端把「账号未登录」包成 HTTP 200 + body {code: 401, msg: ...},
|
||||
// download 强制 responseType: 'blob' 后被 axios 包成 application/json 的 Blob,defaultResponseInterceptor 走
|
||||
// responseReturn === 'body' 分支直接返回,绕过了 authenticateResponseInterceptor 的 401 token 刷新;
|
||||
// 这里把这种 Blob 解析回 JSON,再以 axios 风格抛出,让后续拦截器接管
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: async (response) => {
|
||||
const blob = response.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
return response;
|
||||
}
|
||||
// Blob.type 在部分环境可能为空或大小写不一,叠加 response header 一起判断更稳
|
||||
const blobType = (blob.type || '').toLowerCase();
|
||||
const headerType = String(
|
||||
response.headers?.['content-type'] ??
|
||||
response.headers?.['Content-Type'] ??
|
||||
'',
|
||||
).toLowerCase();
|
||||
if (
|
||||
!blobType.includes('application/json') &&
|
||||
!headerType.includes('application/json')
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(await blob.text());
|
||||
} catch {
|
||||
return response;
|
||||
}
|
||||
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
||||
response.data = parsed;
|
||||
throw Object.assign(new Error(parsed.msg ?? 'Request failed'), {
|
||||
config: response.config,
|
||||
response,
|
||||
data: parsed,
|
||||
isAxiosError: true,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
|
||||
@@ -6,18 +6,36 @@ export function useImagesUpload() {
|
||||
return defineComponent({
|
||||
name: 'ImagesUpload',
|
||||
props: {
|
||||
multiple: {
|
||||
accept: {
|
||||
type: Array,
|
||||
default: () => ['image/jpeg', 'image/png', 'image/gif'],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
default: false,
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
return () => (
|
||||
<ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} />
|
||||
<ImageUpload
|
||||
accept={props.accept as string[]}
|
||||
disabled={props.disabled}
|
||||
maxNumber={props.maxNumber}
|
||||
maxSize={props.maxSize}
|
||||
multiple={props.multiple}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useUploadFileRule() {
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '文件类型',
|
||||
value: ['doc', 'xls', 'ppt', 'txt', 'pdf'],
|
||||
options: [
|
||||
@@ -40,12 +40,6 @@ export function useUploadFileRule() {
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'autoUpload',
|
||||
title: '是否在选取文件后立即进行上传',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
@@ -54,23 +48,23 @@ export function useUploadFileRule() {
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'isShowTip',
|
||||
field: 'showDescription',
|
||||
title: '是否显示提示',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
field: 'maxNumber',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
props: { min: 1 },
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
|
||||
@@ -24,15 +24,9 @@ export function useUploadImageRule() {
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
@@ -52,40 +46,16 @@ export function useUploadImageRule() {
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否显示删除按钮',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'showBtnText',
|
||||
title: '是否显示按钮文字',
|
||||
value: true,
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -24,15 +24,9 @@ export function useUploadImagesRule() {
|
||||
props(_: any, { t }: any) {
|
||||
return localeProps(t, `${name}.props`, [
|
||||
makeRequiredRule(),
|
||||
{
|
||||
type: 'switch',
|
||||
field: 'drag',
|
||||
title: '拖拽上传',
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
field: 'fileType',
|
||||
field: 'accept',
|
||||
title: '图片类型限制',
|
||||
value: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
options: [
|
||||
@@ -48,40 +42,27 @@ export function useUploadImagesRule() {
|
||||
],
|
||||
props: {
|
||||
multiple: true,
|
||||
maxNumber: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'fileSize',
|
||||
field: 'maxSize',
|
||||
title: '大小限制(MB)',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
},
|
||||
{
|
||||
type: 'inputNumber',
|
||||
field: 'limit',
|
||||
field: 'maxNumber',
|
||||
title: '数量限制',
|
||||
value: 5,
|
||||
props: { min: 0 },
|
||||
props: { min: 1 },
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'height',
|
||||
title: '组件高度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'width',
|
||||
title: '组件宽度',
|
||||
value: '150px',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
field: 'borderradius',
|
||||
title: '组件边框圆角',
|
||||
value: '8px',
|
||||
type: 'switch',
|
||||
field: 'disabled',
|
||||
title: '是否禁用',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -164,7 +164,7 @@ const resetCustomConfigList = () => {
|
||||
approveType.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:ApproveType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:ApproveType`, {
|
||||
value: ApproveType.USER,
|
||||
});
|
||||
@@ -173,7 +173,7 @@ const resetCustomConfigList = () => {
|
||||
assignStartUserHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignStartUserHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, {
|
||||
value: 1,
|
||||
});
|
||||
@@ -183,13 +183,13 @@ const resetCustomConfigList = () => {
|
||||
rejectHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:RejectHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 });
|
||||
rejectHandlerType.value = rejectHandlerTypeEl.value.value;
|
||||
returnNodeIdEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:RejectReturnTaskId`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, {
|
||||
value: '',
|
||||
});
|
||||
@@ -199,7 +199,7 @@ const resetCustomConfigList = () => {
|
||||
assignEmptyHandlerTypeEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyHandlerType`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, {
|
||||
value: 1,
|
||||
});
|
||||
@@ -207,7 +207,7 @@ const resetCustomConfigList = () => {
|
||||
assignEmptyUserIdsEl.value =
|
||||
elExtensionElements.value.values?.find(
|
||||
(ex: any) => ex.$type === `${prefix}:AssignEmptyUserIds`,
|
||||
)?.[0] ||
|
||||
) ||
|
||||
bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, {
|
||||
value: '',
|
||||
});
|
||||
|
||||
@@ -767,6 +767,14 @@ export const COMPARISON_OPERATORS: DictDataType[] = [
|
||||
value: '<=',
|
||||
label: '小于等于',
|
||||
},
|
||||
{
|
||||
value: 'contain',
|
||||
label: '包含',
|
||||
},
|
||||
{
|
||||
value: '!contain',
|
||||
label: '不包含',
|
||||
},
|
||||
];
|
||||
// 审批操作按钮名称
|
||||
export const OPERATION_BUTTON_NAME = new Map<number, string>();
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useUserStore } from '@vben/stores';
|
||||
import { isEmpty } from '@vben/utils';
|
||||
|
||||
import FormCreate from '@form-create/element-ui';
|
||||
import { until, useDebounceFn } from '@vueuse/core';
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
@@ -111,6 +112,8 @@ const nextAssigneesActivityNode = ref<BpmProcessInstanceApi.ApprovalNodeInfo[]>(
|
||||
[],
|
||||
); // 下一个审批节点信息
|
||||
const nextAssigneesTimelineRef = ref(); // 下一个节点审批人时间线组件的引用
|
||||
let nextApprovalRequestId = 0; // 请求序号;onChange 高频触发时,丢弃过期请求结果
|
||||
let pendingNextNodesTask: null | Promise<unknown> = null; // 跟踪 onChange 触发的最新一轮重算,提交前需 await 等其完成
|
||||
const approveReasonForm: any = reactive({
|
||||
reason: '',
|
||||
signPicUrl: '',
|
||||
@@ -264,7 +267,6 @@ async function openPopover(type: string) {
|
||||
ElMessage.warning('表单校验不通过,请先完善表单!!');
|
||||
return;
|
||||
}
|
||||
await initNextAssigneesFormField();
|
||||
}
|
||||
if (type === 'return') {
|
||||
// 获取退回节点
|
||||
@@ -277,6 +279,20 @@ async function openPopover(type: string) {
|
||||
Object.keys(popOverVisible.value).forEach((item) => {
|
||||
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
|
||||
});
|
||||
if (type === 'approve') {
|
||||
// 当前任务有节点表单时,等 form-create 的 fApi 就绪后再计算下一个节点;
|
||||
// 没有节点表单时,approveFormFApi 永远不会被赋值,跳过等待
|
||||
if (runningTask.value?.formId > 0) {
|
||||
// 1s 兜底超时;超时 until 会抛错,这里静默吞掉,让首次计算照常进行
|
||||
await until(
|
||||
() => typeof approveFormFApi.value?.validate === 'function',
|
||||
)
|
||||
.toBeTruthy({ timeout: 1000 })
|
||||
.catch(() => {});
|
||||
}
|
||||
// 初始化下一个审批人表单字段
|
||||
await initNextAssigneesFormField();
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭气泡卡 */
|
||||
@@ -294,6 +310,8 @@ function closePopover(type: string, formRef: any | FormInstance) {
|
||||
|
||||
/** 流程通过时,根据表单变量查询新的流程节点,判断下一个节点类型是否为自选审批人 */
|
||||
async function initNextAssigneesFormField() {
|
||||
// 记录当前请求序号;如果在等待响应期间又有新请求发出,本次结果作废
|
||||
const requestId = ++nextApprovalRequestId;
|
||||
// 获取修改的流程变量, 暂时只支持流程表单
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
const data = await getNextApprovalNodes({
|
||||
@@ -301,6 +319,12 @@ async function initNextAssigneesFormField() {
|
||||
taskId: runningTask.value.id,
|
||||
processVariablesStr: JSON.stringify(variables),
|
||||
});
|
||||
// 已有更新的请求发出,丢弃本次过期结果,避免把旧分支节点回写到当前列表
|
||||
if (requestId !== nextApprovalRequestId) {
|
||||
return;
|
||||
}
|
||||
// 在最新结果到达时再清空,避免请求期间出现节点信息抖动
|
||||
nextAssigneesActivityNode.value = [];
|
||||
if (data && data.length > 0) {
|
||||
const customApproveUsersData: Record<string, any[]> = {}; // 用于收集需要设置到 Timeline 组件的自定义审批人数据
|
||||
data.forEach((node: BpmProcessInstanceApi.ApprovalNodeInfo) => {
|
||||
@@ -335,6 +359,12 @@ async function initNextAssigneesFormField() {
|
||||
}
|
||||
}
|
||||
|
||||
/** onChange 高频触发时合并 300ms 内的连续按键,减少网关查询请求 */
|
||||
const debouncedInitNextAssigneesFormField = useDebounceFn(
|
||||
initNextAssigneesFormField,
|
||||
300,
|
||||
);
|
||||
|
||||
/** 选择下一个节点的审批人 */
|
||||
function selectNextAssigneesConfirm(id: string, userList: any[]) {
|
||||
approveReasonForm.nextAssignees[id] = userList?.map((item: any) => item.id);
|
||||
@@ -370,6 +400,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
}
|
||||
|
||||
if (pass) {
|
||||
// 等待 onChange 触发的最新一轮重算落地,避免拿旧分支节点 + 旧审批人选择 + 新表单变量的错配组合提交
|
||||
if (pendingNextNodesTask) {
|
||||
await pendingNextNodesTask;
|
||||
}
|
||||
const nextAssigneesValid = validateNextAssignees();
|
||||
if (!nextAssigneesValid) return;
|
||||
const variables = getUpdatedProcessInstanceVariables();
|
||||
@@ -384,12 +418,10 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||
if (runningTask.value.signEnable) {
|
||||
data.signPicUrl = approveReasonForm.signPicUrl;
|
||||
}
|
||||
// 多表单处理,并且有额外的 approveForm 表单,需要校验 + 拼接到 data 表单里提交
|
||||
// TODO 芋艿 任务有多表单这里要如何处理,会和可编辑的字段冲突
|
||||
// 多表单处理:节点表单需要校验;变量已经在 getUpdatedProcessInstanceVariables 中合并到 data.variables,无需再覆盖
|
||||
const formCreateApi = approveFormFApi.value;
|
||||
if (Object.keys(formCreateApi)?.length > 0) {
|
||||
await formCreateApi.validate();
|
||||
data.variables = approveForm.value.value;
|
||||
}
|
||||
await approveTask(data);
|
||||
popOverVisible.value.approve = false;
|
||||
@@ -656,18 +688,32 @@ function loadTodoTask(task: any) {
|
||||
approveForm.value = {};
|
||||
runningTask.value = task;
|
||||
approveFormFApi.value = {};
|
||||
// 切换任务时重置请求序号与 pending 重算,避免旧任务飞行中的请求/Promise 串到新任务
|
||||
nextApprovalRequestId += 1;
|
||||
pendingNextNodesTask = null;
|
||||
reasonRequire.value = task?.reasonRequire ?? false;
|
||||
nodeTypeName.value =
|
||||
task?.nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理' : '审批';
|
||||
// 处理 approve 表单
|
||||
if (task && task.formId && task.formConf) {
|
||||
const tempApproveForm = {};
|
||||
const tempApproveForm: { option?: any; rule?: any; value?: any } = {};
|
||||
setConfAndFields2(
|
||||
tempApproveForm,
|
||||
task.formConf,
|
||||
task.formFields,
|
||||
task.formVariables,
|
||||
);
|
||||
// 为表单添加 onChange 事件,当表单值变化时,重新计算下一个节点的信息;网关分支可能依赖表单字段
|
||||
tempApproveForm.option.onChange = () => {
|
||||
// 弹窗打开时,才重新计算下一个节点的信息
|
||||
if (!popOverVisible.value.approve) {
|
||||
return;
|
||||
}
|
||||
// useDebounceFn 会把前一次返回的 Promise reject 掉,需 catch 吞掉 'cancelled'
|
||||
pendingNextNodesTask = debouncedInitNextAssigneesFormField().catch(
|
||||
() => {},
|
||||
);
|
||||
};
|
||||
approveForm.value = tempApproveForm;
|
||||
} else {
|
||||
approveForm.value = {}; // 占位,避免为空
|
||||
@@ -692,9 +738,17 @@ async function validateNormalForm() {
|
||||
/** 从可以编辑的流程表单字段,获取需要修改的流程实例的变量 */
|
||||
function getUpdatedProcessInstanceVariables() {
|
||||
const variables: any = {};
|
||||
props.writableFields.forEach((field: string) => {
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
// 从流程表单(流程定义级别)中获取变量
|
||||
if (props.writableFields?.length && props.normalFormApi) {
|
||||
props.writableFields.forEach((field: string) => {
|
||||
variables[field] = props.normalFormApi.getValue(field);
|
||||
});
|
||||
}
|
||||
// 从节点表单(节点级别)中获取变量;通过 form-create 官方的 formData() 拿当前值
|
||||
const nodeFormData = approveFormFApi.value?.formData?.();
|
||||
if (nodeFormData) {
|
||||
Object.assign(variables, nodeFormData);
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
return handleTree(data);
|
||||
},
|
||||
multiple: true,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择应用部门',
|
||||
defaultExpandAll: true,
|
||||
},
|
||||
|
||||
@@ -122,7 +122,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getAreaTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择地址',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -144,7 +144,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getAreaTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择地址',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { schema } from './data';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelClass: 'w-100',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema,
|
||||
|
||||
@@ -131,7 +131,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
component: 'ApiTreeSelect',
|
||||
componentProps: {
|
||||
api: getAreaTree,
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择地址',
|
||||
allowClear: true,
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ import { schema } from './data';
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
labelClass: 'w-100',
|
||||
labelWidth: 120,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema,
|
||||
|
||||
@@ -70,7 +70,9 @@ export function useFormSchema(): VbenFormSchema[] {
|
||||
const data = await getProductCategoryList();
|
||||
return handleTree(data);
|
||||
},
|
||||
fieldNames: { label: 'name', value: 'id', children: 'children' },
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
childrenField: 'children',
|
||||
placeholder: '请选择产品类型',
|
||||
clearable: true,
|
||||
},
|
||||
|
||||
@@ -67,10 +67,8 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getCustomerSimpleList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
@@ -83,10 +81,8 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getSimpleUserList,
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -201,10 +197,8 @@ export function useFormSchema(formType: string): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getAccountSimpleList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -353,10 +347,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getProductSimpleList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -377,10 +369,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getCustomerSimpleList,
|
||||
fieldNames: {
|
||||
label: 'name',
|
||||
value: 'id',
|
||||
},
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -405,10 +395,8 @@ export function useGridFormSchema(): VbenFormSchema[] {
|
||||
allowClear: true,
|
||||
showSearch: true,
|
||||
api: getSimpleUserList,
|
||||
fieldNames: {
|
||||
label: 'nickname',
|
||||
value: 'id',
|
||||
},
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -88,10 +88,10 @@ export function useGridColumns(
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '商品售价',
|
||||
title: '商品售价(元)',
|
||||
minWidth: 90,
|
||||
align: 'center',
|
||||
formatter: 'formatAmount2',
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'salesCount',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||
|
||||
import { fenToYuan, handleTree, treeToString } from '@vben/utils';
|
||||
import { handleTree, treeToString } from '@vben/utils';
|
||||
|
||||
import { getCategoryList } from '#/api/mall/product/category';
|
||||
import { getRangePickerDefaultProps } from '#/utils';
|
||||
@@ -105,25 +105,21 @@ export function useGridColumns(
|
||||
},
|
||||
{
|
||||
field: 'price',
|
||||
title: '价格',
|
||||
title: '价格(元)',
|
||||
minWidth: 100,
|
||||
formatter: 'formatAmount2',
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'marketPrice',
|
||||
title: '市场价',
|
||||
title: '市场价(元)',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return `${fenToYuan(row.marketPrice)} 元`;
|
||||
},
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'costPrice',
|
||||
title: '成本价',
|
||||
title: '成本价(元)',
|
||||
minWidth: 100,
|
||||
formatter: ({ row }) => {
|
||||
return `${fenToYuan(row.costPrice)} 元`;
|
||||
},
|
||||
formatter: 'formatFenToYuanAmount',
|
||||
},
|
||||
{
|
||||
field: 'salesCount',
|
||||
|
||||
@@ -181,31 +181,28 @@ async function handleSubmit() {
|
||||
.merge(descriptionFormApi)
|
||||
.merge(otherFormApi)
|
||||
.submitAllForm(true);
|
||||
values.skus = formData.value.skus;
|
||||
// 校验商品名称不能为空(用于 SKU name)
|
||||
if (!values.name || values.name.trim() === '') {
|
||||
ElMessage.error('商品名称不能为空');
|
||||
return;
|
||||
}
|
||||
if (values.skus) {
|
||||
try {
|
||||
// 校验 sku
|
||||
skuListRef.value.validateSku();
|
||||
} catch {
|
||||
ElMessage.error('【库存价格】不完善,请填写相关信息');
|
||||
return;
|
||||
}
|
||||
values.skus.forEach((item) => {
|
||||
// 给 sku name 赋值(使用商品名称作为 SKU 名称)
|
||||
item.name = values.name;
|
||||
// 金额转换:元转分
|
||||
item.price = convertToInteger(item.price);
|
||||
item.marketPrice = convertToInteger(item.marketPrice);
|
||||
item.costPrice = convertToInteger(item.costPrice);
|
||||
item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice);
|
||||
});
|
||||
try {
|
||||
// 校验 sku
|
||||
skuListRef.value.validateSku();
|
||||
} catch {
|
||||
ElMessage.error('【库存价格】不完善,请填写相关信息');
|
||||
return;
|
||||
}
|
||||
// 金额转换:元转分
|
||||
values.skus = formData.value.skus!.map((item) => ({
|
||||
...item,
|
||||
name: values.name,
|
||||
price: convertToInteger(item.price),
|
||||
marketPrice: convertToInteger(item.marketPrice),
|
||||
costPrice: convertToInteger(item.costPrice),
|
||||
firstBrokeragePrice: convertToInteger(item.firstBrokeragePrice),
|
||||
secondBrokeragePrice: convertToInteger(item.secondBrokeragePrice),
|
||||
}));
|
||||
// 处理轮播图列表:上传组件可能返回对象或字符串,统一处理成字符串数组
|
||||
const newSliderPicUrls: any[] = [];
|
||||
values.sliderPicUrls!.forEach((item: any) => {
|
||||
@@ -235,14 +232,15 @@ async function getDetail() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
const res = await getSpu(spuId.value!);
|
||||
// 金额转换:元转分
|
||||
res.skus?.forEach((item) => {
|
||||
item.price = formatToFraction(item.price);
|
||||
item.marketPrice = formatToFraction(item.marketPrice);
|
||||
item.costPrice = formatToFraction(item.costPrice);
|
||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||
item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice);
|
||||
});
|
||||
// 金额转换:分转元
|
||||
res.skus = res.skus?.map((item) => ({
|
||||
...item,
|
||||
price: formatToFraction(item.price),
|
||||
marketPrice: formatToFraction(item.marketPrice),
|
||||
costPrice: formatToFraction(item.costPrice),
|
||||
firstBrokeragePrice: formatToFraction(item.firstBrokeragePrice),
|
||||
secondBrokeragePrice: formatToFraction(item.secondBrokeragePrice),
|
||||
}));
|
||||
formData.value = res;
|
||||
// 初始化各表单值
|
||||
infoFormApi.setValues(res).then();
|
||||
|
||||
@@ -127,6 +127,49 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
},
|
||||
});
|
||||
|
||||
// add by 芋艿:对应 https://t.zsxq.com/SHqWw 反馈
|
||||
// 处理 Blob 响应中的业务错误(如 401):后端把「账号未登录」包成 HTTP 200 + body {code: 401, msg: ...},
|
||||
// download 强制 responseType: 'blob' 后被 axios 包成 application/json 的 Blob,defaultResponseInterceptor 走
|
||||
// responseReturn === 'body' 分支直接返回,绕过了 authenticateResponseInterceptor 的 401 token 刷新;
|
||||
// 这里把这种 Blob 解析回 JSON,再以 axios 风格抛出,让后续拦截器接管
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: async (response) => {
|
||||
const blob = response.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
return response;
|
||||
}
|
||||
// Blob.type 在部分环境可能为空或大小写不一,叠加 response header 一起判断更稳
|
||||
const blobType = (blob.type || '').toLowerCase();
|
||||
const headerType = String(
|
||||
response.headers?.['content-type'] ??
|
||||
response.headers?.['Content-Type'] ??
|
||||
'',
|
||||
).toLowerCase();
|
||||
if (
|
||||
!blobType.includes('application/json') &&
|
||||
!headerType.includes('application/json')
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(await blob.text());
|
||||
} catch {
|
||||
return response;
|
||||
}
|
||||
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
||||
response.data = parsed;
|
||||
throw Object.assign(new Error(parsed.msg ?? 'Request failed'), {
|
||||
config: response.config,
|
||||
response,
|
||||
data: parsed,
|
||||
isAxiosError: true,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
|
||||
@@ -127,6 +127,49 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
|
||||
},
|
||||
});
|
||||
|
||||
// add by 芋艿:对应 https://t.zsxq.com/SHqWw 反馈
|
||||
// 处理 Blob 响应中的业务错误(如 401):后端把「账号未登录」包成 HTTP 200 + body {code: 401, msg: ...},
|
||||
// download 强制 responseType: 'blob' 后被 axios 包成 application/json 的 Blob,defaultResponseInterceptor 走
|
||||
// responseReturn === 'body' 分支直接返回,绕过了 authenticateResponseInterceptor 的 401 token 刷新;
|
||||
// 这里把这种 Blob 解析回 JSON,再以 axios 风格抛出,让后续拦截器接管
|
||||
client.addResponseInterceptor({
|
||||
fulfilled: async (response) => {
|
||||
const blob = response.data;
|
||||
if (!(blob instanceof Blob)) {
|
||||
return response;
|
||||
}
|
||||
// Blob.type 在部分环境可能为空或大小写不一,叠加 response header 一起判断更稳
|
||||
const blobType = (blob.type || '').toLowerCase();
|
||||
const headerType = String(
|
||||
response.headers?.['content-type'] ??
|
||||
response.headers?.['Content-Type'] ??
|
||||
'',
|
||||
).toLowerCase();
|
||||
if (
|
||||
!blobType.includes('application/json') &&
|
||||
!headerType.includes('application/json')
|
||||
) {
|
||||
return response;
|
||||
}
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(await blob.text());
|
||||
} catch {
|
||||
return response;
|
||||
}
|
||||
if (parsed && parsed.code !== undefined && parsed.code !== 0) {
|
||||
response.data = parsed;
|
||||
throw Object.assign(new Error(parsed.msg ?? 'Request failed'), {
|
||||
config: response.config,
|
||||
response,
|
||||
data: parsed,
|
||||
isAxiosError: true,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
},
|
||||
});
|
||||
|
||||
// 处理返回的响应数据格式
|
||||
client.addResponseInterceptor(
|
||||
defaultResponseInterceptor({
|
||||
|
||||
@@ -103,11 +103,37 @@ export const defaultImageAccepts = [
|
||||
'webp',
|
||||
];
|
||||
|
||||
/**
|
||||
* 图片类 MIME 子类型到扩展名的别名映射;未列出的子类型按字面量与扩展名比较
|
||||
*/
|
||||
const IMAGE_MIME_SUBTYPE_ALIASES: Record<string, string[]> = {
|
||||
apng: ['apng', 'png'],
|
||||
jpeg: ['jpeg', 'jpg'],
|
||||
pjpeg: ['jpeg', 'jpg'],
|
||||
'svg+xml': ['svg'],
|
||||
tiff: ['tif', 'tiff'],
|
||||
'x-icon': ['ico'],
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断 MIME 子类型是否与文件扩展名匹配;image/* 限定为已知图片扩展名集合
|
||||
*/
|
||||
function matchMimeSubtype(subtype: string, ext: string): boolean {
|
||||
if (subtype === '*') {
|
||||
return defaultImageAccepts.includes(ext);
|
||||
}
|
||||
const aliases = IMAGE_MIME_SUBTYPE_ALIASES[subtype];
|
||||
if (aliases) {
|
||||
return aliases.includes(ext);
|
||||
}
|
||||
return subtype === ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断文件是否为图片
|
||||
*
|
||||
* @param filename 文件名
|
||||
* @param accepts 支持的文件类型
|
||||
* @param accepts 支持的文件类型,兼容 MIME(如 image/png)、.ext(如 .png)与纯后缀(如 png)
|
||||
* @returns 是否为图片
|
||||
*/
|
||||
export function isImage(
|
||||
@@ -118,7 +144,23 @@ export function isImage(
|
||||
return false;
|
||||
}
|
||||
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
||||
return accepts.includes(ext);
|
||||
if (!ext) {
|
||||
return false;
|
||||
}
|
||||
return accepts.some((accept) => {
|
||||
const lower = accept.toLowerCase();
|
||||
// MIME 类型,例如 image/png ;image/* 仅放行已知图片扩展
|
||||
if (lower.includes('/')) {
|
||||
const subtype = lower.split('/').pop() || '';
|
||||
return matchMimeSubtype(subtype, ext);
|
||||
}
|
||||
// 以点号开头的扩展名,例如 .png
|
||||
if (lower.startsWith('.')) {
|
||||
return lower.slice(1) === ext;
|
||||
}
|
||||
// 纯后缀
|
||||
return lower === ext;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user