Pre Merge pull request !362 from XuZhiqiang/feat-antdv-next
commit
c264735a4a
|
|
@ -0,0 +1,86 @@
|
||||||
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
|
||||||
|
|
||||||
|
const RawDatePicker = defineAsyncComponent(
|
||||||
|
() => import('antdv-next/dist/date-picker/index'),
|
||||||
|
);
|
||||||
|
const RawRangePicker = defineAsyncComponent(() =>
|
||||||
|
import('antdv-next/dist/date-picker/index').then(
|
||||||
|
(res) => res.DateRangePicker,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const TIMESTAMP_VALUE_FORMATS = new Set(['x', 'X']);
|
||||||
|
|
||||||
|
function isTimestampValueFormat(valueFormat: unknown) {
|
||||||
|
return (
|
||||||
|
typeof valueFormat === 'string' && TIMESTAMP_VALUE_FORMATS.has(valueFormat)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTimestampPickerValue(value: any, valueFormat: unknown): any {
|
||||||
|
if (!isTimestampValueFormat(valueFormat)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => normalizeTimestampPickerValue(item, valueFormat));
|
||||||
|
}
|
||||||
|
return typeof value === 'number' ? String(value) : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimestampValueFormat(
|
||||||
|
component: Component,
|
||||||
|
name: 'DatePicker' | 'RangePicker',
|
||||||
|
) {
|
||||||
|
return defineComponent({
|
||||||
|
name,
|
||||||
|
inheritAttrs: false,
|
||||||
|
setup(_, { attrs, expose, slots }) {
|
||||||
|
const innerRef = ref();
|
||||||
|
expose(
|
||||||
|
new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_target, key) => innerRef.value?.[key],
|
||||||
|
has: (_target, key) => key in (innerRef.value || {}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const pickerAttrs: Recordable<any> = { ...attrs };
|
||||||
|
if (
|
||||||
|
'value-format' in pickerAttrs &&
|
||||||
|
!Reflect.has(pickerAttrs, 'valueFormat')
|
||||||
|
) {
|
||||||
|
pickerAttrs.valueFormat = pickerAttrs['value-format'];
|
||||||
|
}
|
||||||
|
const valueFormat = pickerAttrs.valueFormat;
|
||||||
|
|
||||||
|
for (const key of [
|
||||||
|
'value',
|
||||||
|
'defaultValue',
|
||||||
|
'pickerValue',
|
||||||
|
'defaultPickerValue',
|
||||||
|
]) {
|
||||||
|
if (Reflect.has(pickerAttrs, key)) {
|
||||||
|
pickerAttrs[key] = normalizeTimestampPickerValue(
|
||||||
|
pickerAttrs[key],
|
||||||
|
valueFormat,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(component, { ...pickerAttrs, ref: innerRef }, slots);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const DatePicker = withTimestampValueFormat(RawDatePicker, 'DatePicker');
|
||||||
|
const RangePicker = withTimestampValueFormat(RawRangePicker, 'RangePicker');
|
||||||
|
|
||||||
|
export { DatePicker, RangePicker };
|
||||||
|
|
@ -75,6 +75,9 @@ import { message, Modal, notification } from 'antdv-next';
|
||||||
import { uploadFile as uploadFileApi } from '#/api/infra/file';
|
import { uploadFile as uploadFileApi } from '#/api/infra/file';
|
||||||
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
import { Tinymce as RichTextarea } from '#/components/tinymce';
|
||||||
import { FileUpload, ImageUpload } from '#/components/upload';
|
import { FileUpload, ImageUpload } from '#/components/upload';
|
||||||
|
|
||||||
|
import { DatePicker, RangePicker } from './date-picker';
|
||||||
|
|
||||||
type AdapterUploadProps = UploadProps & {
|
type AdapterUploadProps = UploadProps & {
|
||||||
aspectRatio?: string;
|
aspectRatio?: string;
|
||||||
crop?: boolean;
|
crop?: boolean;
|
||||||
|
|
@ -97,9 +100,6 @@ const Checkbox = defineAsyncComponent(
|
||||||
const CheckboxGroup = defineAsyncComponent(() =>
|
const CheckboxGroup = defineAsyncComponent(() =>
|
||||||
import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
|
import('antdv-next/dist/checkbox/index').then((res) => res.CheckboxGroup),
|
||||||
);
|
);
|
||||||
const DatePicker = defineAsyncComponent(
|
|
||||||
() => import('antdv-next/dist/date-picker/index'),
|
|
||||||
);
|
|
||||||
const Divider = defineAsyncComponent(
|
const Divider = defineAsyncComponent(
|
||||||
() => import('antdv-next/dist/divider/index'),
|
() => import('antdv-next/dist/divider/index'),
|
||||||
);
|
);
|
||||||
|
|
@ -117,11 +117,6 @@ const Radio = defineAsyncComponent(() => import('antdv-next/dist/radio/index'));
|
||||||
const RadioGroup = defineAsyncComponent(() =>
|
const RadioGroup = defineAsyncComponent(() =>
|
||||||
import('antdv-next/dist/radio/index').then((res) => res.RadioGroup),
|
import('antdv-next/dist/radio/index').then((res) => res.RadioGroup),
|
||||||
);
|
);
|
||||||
const RangePicker = defineAsyncComponent(() =>
|
|
||||||
import('antdv-next/dist/date-picker/index').then(
|
|
||||||
(res) => res.DateRangePicker,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const Rate = defineAsyncComponent(() => import('antdv-next/dist/rate/index'));
|
const Rate = defineAsyncComponent(() => import('antdv-next/dist/rate/index'));
|
||||||
const Select = defineAsyncComponent(
|
const Select = defineAsyncComponent(
|
||||||
() => import('antdv-next/dist/select/index'),
|
() => import('antdv-next/dist/select/index'),
|
||||||
|
|
@ -794,4 +789,4 @@ async function initComponentAdapter() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { initComponentAdapter };
|
export { DatePicker, initComponentAdapter, RangePicker };
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ export namespace BpmProcessInstanceApi {
|
||||||
reason: string;
|
reason: string;
|
||||||
signPicUrl: string;
|
signPicUrl: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
attachments?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 抄送流程实例 */
|
/** 抄送流程实例 */
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { computed, defineComponent, ref, unref, useAttrs } from 'vue';
|
||||||
|
|
||||||
import { get, getNestedValue, isFunction } from '@vben/utils';
|
import { get, getNestedValue, isFunction } from '@vben/utils';
|
||||||
|
|
||||||
import { Card, Descriptions } from 'antdv-next';
|
import { Card, Descriptions, DescriptionsItem } from 'antdv-next';
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
bordered: { default: true, type: Boolean },
|
bordered: { default: true, type: Boolean },
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ onMounted(() => {
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
v-model:value="times"
|
v-model:value="times"
|
||||||
:format="rangePickerProps.format"
|
:format="rangePickerProps.format"
|
||||||
:value-format="rangePickerProps.valueFormat"
|
|
||||||
:placeholder="rangePickerProps.placeholder"
|
:placeholder="rangePickerProps.placeholder"
|
||||||
:presets="rangePickerProps.presets"
|
:presets="rangePickerProps.presets"
|
||||||
class="!w-full !max-w-96"
|
class="!w-full !max-w-96"
|
||||||
|
|
|
||||||
|
|
@ -188,8 +188,8 @@ watch(
|
||||||
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||||
<slot name="more">
|
<slot name="more">
|
||||||
<Button type="link">
|
<Button type="link">
|
||||||
|
{{ $t('page.action.more') }}
|
||||||
<template #icon>
|
<template #icon>
|
||||||
{{ $t('page.action.more') }}
|
|
||||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { UploadFile, UploadProps } from 'antdv-next';
|
import type { UploadFile, UploadProps } from 'antdv-next';
|
||||||
import type { UploadRequestOption } from 'antdv-next/lib/vc-upload/interface';
|
|
||||||
|
|
||||||
import type { FileUploadProps } from './typing';
|
import type { FileUploadProps } from './typing';
|
||||||
|
|
||||||
|
|
@ -17,6 +16,8 @@ import { Button, message, Upload } from 'antdv-next';
|
||||||
import { UploadResultStatus } from './typing';
|
import { UploadResultStatus } from './typing';
|
||||||
import { useUpload, useUploadType } from './use-upload';
|
import { useUpload, useUploadType } from './use-upload';
|
||||||
|
|
||||||
|
type UploadRequestOption = any;
|
||||||
|
|
||||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||||
|
|
||||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||||
|
|
@ -34,6 +35,7 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||||
resultField: '',
|
resultField: '',
|
||||||
returnText: false,
|
returnText: false,
|
||||||
showDescription: false,
|
showDescription: false,
|
||||||
|
showDownloadIcon: true,
|
||||||
});
|
});
|
||||||
const emit = defineEmits([
|
const emit = defineEmits([
|
||||||
'change',
|
'change',
|
||||||
|
|
@ -295,7 +297,7 @@ function getValue() {
|
||||||
:show-upload-list="{
|
:show-upload-list="{
|
||||||
showPreviewIcon: true,
|
showPreviewIcon: true,
|
||||||
showRemoveIcon: true,
|
showRemoveIcon: true,
|
||||||
showDownloadIcon: true,
|
showDownloadIcon,
|
||||||
}"
|
}"
|
||||||
@remove="handleRemove"
|
@remove="handleRemove"
|
||||||
@preview="handlePreview"
|
@preview="handlePreview"
|
||||||
|
|
|
||||||
|
|
@ -29,5 +29,6 @@ export interface FileUploadProps {
|
||||||
resultField?: string; // support xxx.xxx.xx
|
resultField?: string; // support xxx.xxx.xx
|
||||||
returnText?: boolean; // 是否返回文件文本内容
|
returnText?: boolean; // 是否返回文件文本内容
|
||||||
showDescription?: boolean; // 是否显示下面的描述
|
showDescription?: boolean; // 是否显示下面的描述
|
||||||
|
showDownloadIcon?: boolean; // 是否显示下载按钮
|
||||||
value?: string | string[];
|
value?: string | string[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
|
type DateRangeTuple = [Dayjs, Dayjs];
|
||||||
|
type StringRangeTuple = [string, string];
|
||||||
|
|
||||||
|
function dateRange(start: Dayjs, end: Dayjs): DateRangeTuple {
|
||||||
|
return [start, end];
|
||||||
|
}
|
||||||
|
|
||||||
/** 时间段选择器拓展 */
|
/** 时间段选择器拓展 */
|
||||||
export function getRangePickerDefaultProps() {
|
export function getRangePickerDefaultProps() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -13,55 +22,55 @@ export function getRangePickerDefaultProps() {
|
||||||
placeholder: [
|
placeholder: [
|
||||||
$t('utils.rangePicker.beginTime'),
|
$t('utils.rangePicker.beginTime'),
|
||||||
$t('utils.rangePicker.endTime'),
|
$t('utils.rangePicker.endTime'),
|
||||||
],
|
] as StringRangeTuple,
|
||||||
// 快捷时间范围
|
// 快捷时间范围
|
||||||
presets: [
|
presets: [
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.today'),
|
label: $t('utils.rangePicker.today'),
|
||||||
value: [dayjs().startOf('day'), dayjs().endOf('day')],
|
value: dateRange(dayjs().startOf('day'), dayjs().endOf('day')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.last7Days'),
|
label: $t('utils.rangePicker.last7Days'),
|
||||||
value: [
|
value: dateRange(
|
||||||
dayjs().subtract(7, 'day').startOf('day'),
|
dayjs().subtract(7, 'day').startOf('day'),
|
||||||
dayjs().endOf('day'),
|
dayjs().endOf('day'),
|
||||||
],
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.last30Days'),
|
label: $t('utils.rangePicker.last30Days'),
|
||||||
value: [
|
value: dateRange(
|
||||||
dayjs().subtract(30, 'day').startOf('day'),
|
dayjs().subtract(30, 'day').startOf('day'),
|
||||||
dayjs().endOf('day'),
|
dayjs().endOf('day'),
|
||||||
],
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.yesterday'),
|
label: $t('utils.rangePicker.yesterday'),
|
||||||
value: [
|
value: dateRange(
|
||||||
dayjs().subtract(1, 'day').startOf('day'),
|
dayjs().subtract(1, 'day').startOf('day'),
|
||||||
dayjs().subtract(1, 'day').endOf('day'),
|
dayjs().subtract(1, 'day').endOf('day'),
|
||||||
],
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.thisWeek'),
|
label: $t('utils.rangePicker.thisWeek'),
|
||||||
value: [dayjs().startOf('week'), dayjs().endOf('day')],
|
value: dateRange(dayjs().startOf('week'), dayjs().endOf('day')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.thisMonth'),
|
label: $t('utils.rangePicker.thisMonth'),
|
||||||
value: [dayjs().startOf('month'), dayjs().endOf('day')],
|
value: dateRange(dayjs().startOf('month'), dayjs().endOf('day')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: $t('utils.rangePicker.lastWeek'),
|
label: $t('utils.rangePicker.lastWeek'),
|
||||||
value: [
|
value: dateRange(
|
||||||
dayjs().subtract(1, 'week').startOf('day'),
|
dayjs().subtract(1, 'week').startOf('day'),
|
||||||
dayjs().endOf('day'),
|
dayjs().endOf('day'),
|
||||||
],
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
showTime: {
|
showTime: {
|
||||||
defaultValue: [
|
defaultValue: dateRange(
|
||||||
dayjs('00:00:00', 'HH:mm:ss'),
|
dayjs('00:00:00', 'HH:mm:ss'),
|
||||||
dayjs('23:59:59', 'HH:mm:ss'),
|
dayjs('23:59:59', 'HH:mm:ss'),
|
||||||
],
|
),
|
||||||
format: 'HH:mm:ss',
|
format: 'HH:mm:ss',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import {
|
||||||
transferTask,
|
transferTask,
|
||||||
} from '#/api/bpm/task';
|
} from '#/api/bpm/task';
|
||||||
import { setConfAndFields2 } from '#/components/form-create';
|
import { setConfAndFields2 } from '#/components/form-create';
|
||||||
|
import { FileUpload } from '#/components/upload';
|
||||||
import { $t } from '#/locales';
|
import { $t } from '#/locales';
|
||||||
|
|
||||||
import Signature from './signature.vue';
|
import Signature from './signature.vue';
|
||||||
|
|
@ -120,6 +121,7 @@ const approveReasonForm: any = reactive({
|
||||||
reason: '',
|
reason: '',
|
||||||
signPicUrl: '',
|
signPicUrl: '',
|
||||||
nextAssignees: {},
|
nextAssignees: {},
|
||||||
|
attachments: [],
|
||||||
});
|
});
|
||||||
const approveReasonRule: Record<string, any> = computed(() => {
|
const approveReasonRule: Record<string, any> = computed(() => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -140,7 +142,8 @@ const approveReasonRule: Record<string, any> = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const rejectFormRef = ref<FormInstance>();
|
const rejectFormRef = ref<FormInstance>();
|
||||||
const rejectReasonForm = reactive({
|
const rejectReasonForm = reactive<{ attachments: string[]; reason: string }>({
|
||||||
|
attachments: [],
|
||||||
reason: '',
|
reason: '',
|
||||||
}); // 拒绝表单
|
}); // 拒绝表单
|
||||||
const rejectReasonRule: any = computed(() => {
|
const rejectReasonRule: any = computed(() => {
|
||||||
|
|
@ -290,6 +293,14 @@ function closePopover(type: string, formRef: any | FormInstance) {
|
||||||
if (formRef) {
|
if (formRef) {
|
||||||
formRef.resetFields();
|
formRef.resetFields();
|
||||||
}
|
}
|
||||||
|
if (type === 'approve') {
|
||||||
|
approveReasonForm.reason = '';
|
||||||
|
approveReasonForm.attachments = [];
|
||||||
|
approveReasonForm.signPicUrl = '';
|
||||||
|
} else if (type === 'reject') {
|
||||||
|
rejectReasonForm.reason = '';
|
||||||
|
rejectReasonForm.attachments = [];
|
||||||
|
}
|
||||||
if (popOverVisible.value[type]) popOverVisible.value[type] = false;
|
if (popOverVisible.value[type]) popOverVisible.value[type] = false;
|
||||||
nextAssigneesActivityNode.value = [];
|
nextAssigneesActivityNode.value = [];
|
||||||
// 清理 Timeline 组件中的自定义审批人数据
|
// 清理 Timeline 组件中的自定义审批人数据
|
||||||
|
|
@ -401,6 +412,7 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||||
const data = {
|
const data = {
|
||||||
id: runningTask.value.id,
|
id: runningTask.value.id,
|
||||||
reason: approveReasonForm.reason,
|
reason: approveReasonForm.reason,
|
||||||
|
attachments: approveReasonForm.attachments,
|
||||||
variables, // 审批通过, 把修改的字段值赋于流程实例变量
|
variables, // 审批通过, 把修改的字段值赋于流程实例变量
|
||||||
nextAssignees: approveReasonForm.nextAssignees, // 下个自选节点选择的审批人信息
|
nextAssignees: approveReasonForm.nextAssignees, // 下个自选节点选择的审批人信息
|
||||||
} as any;
|
} as any;
|
||||||
|
|
@ -414,6 +426,9 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||||
await formCreateApi.validate();
|
await formCreateApi.validate();
|
||||||
}
|
}
|
||||||
await approveTask(data);
|
await approveTask(data);
|
||||||
|
approveReasonForm.reason = '';
|
||||||
|
approveReasonForm.attachments = [];
|
||||||
|
approveReasonForm.signPicUrl = '';
|
||||||
popOverVisible.value.approve = false;
|
popOverVisible.value.approve = false;
|
||||||
nextAssigneesActivityNode.value = [];
|
nextAssigneesActivityNode.value = [];
|
||||||
// 清理 Timeline 组件中的自定义审批人数据
|
// 清理 Timeline 组件中的自定义审批人数据
|
||||||
|
|
@ -425,9 +440,12 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
|
||||||
// 审批不通过数据
|
// 审批不通过数据
|
||||||
const data = {
|
const data = {
|
||||||
id: runningTask.value.id,
|
id: runningTask.value.id,
|
||||||
|
attachments: rejectReasonForm.attachments,
|
||||||
reason: rejectReasonForm.reason,
|
reason: rejectReasonForm.reason,
|
||||||
};
|
};
|
||||||
await rejectTask(data);
|
await rejectTask(data);
|
||||||
|
rejectReasonForm.reason = '';
|
||||||
|
rejectReasonForm.attachments = [];
|
||||||
popOverVisible.value.reject = false;
|
popOverVisible.value.reject = false;
|
||||||
message.success('审批不通过成功');
|
message.success('审批不通过成功');
|
||||||
}
|
}
|
||||||
|
|
@ -748,6 +766,38 @@ function handleSignFinish(url: string) {
|
||||||
approveFormRef.value?.validateFields(['signPicUrl']);
|
approveFormRef.value?.validateFields(['signPicUrl']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 附件图片预览 */
|
||||||
|
const imagePreviewOpen = ref(false);
|
||||||
|
const imagePreviewUrl = ref('');
|
||||||
|
|
||||||
|
/** 判断文件是否为图片类型 */
|
||||||
|
function isImageUrl(url: string) {
|
||||||
|
return /\.(bmp|gif|jpe?g|png|svg|webp)$/i.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理文件预览 */
|
||||||
|
function handleFilePreview(file: any) {
|
||||||
|
if (!file?.url && !file?.response) {
|
||||||
|
message.warning('文件地址不存在,无法预览');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = file.url || file?.response?.url || file?.response;
|
||||||
|
if (!url) {
|
||||||
|
message.warning('文件地址不存在,无法预览');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isImageUrl(url)) {
|
||||||
|
imagePreviewUrl.value = url;
|
||||||
|
imagePreviewOpen.value = true;
|
||||||
|
} else {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleImagePreviewOpenChange(open: boolean) {
|
||||||
|
imagePreviewOpen.value = open;
|
||||||
|
}
|
||||||
|
|
||||||
/** 处理弹窗可见性 */
|
/** 处理弹窗可见性 */
|
||||||
function handlePopoverVisible(visible: boolean) {
|
function handlePopoverVisible(visible: boolean) {
|
||||||
if (!visible) {
|
if (!visible) {
|
||||||
|
|
@ -843,6 +893,16 @@ defineExpose({ loadTodoTask });
|
||||||
:rows="4"
|
:rows="4"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem label="上传附件/图片" name="attachments">
|
||||||
|
<FileUpload
|
||||||
|
v-model:value="approveReasonForm.attachments"
|
||||||
|
:max-number="10"
|
||||||
|
:multiple="true"
|
||||||
|
:show-download-icon="false"
|
||||||
|
help-text="支持多文件/图片上传"
|
||||||
|
@preview="handleFilePreview"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -900,6 +960,15 @@ defineExpose({ loadTodoTask });
|
||||||
:rows="4"
|
:rows="4"
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem label="上传附件/图片" name="attachments">
|
||||||
|
<FileUpload
|
||||||
|
v-model:value="rejectReasonForm.attachments"
|
||||||
|
:max-number="10"
|
||||||
|
:multiple="true"
|
||||||
|
help-text="支持多文件/图片上传"
|
||||||
|
@preview="handleFilePreview"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<Button
|
<Button
|
||||||
:disabled="formLoading"
|
:disabled="formLoading"
|
||||||
|
|
@ -1444,4 +1513,14 @@ defineExpose({ loadTodoTask });
|
||||||
|
|
||||||
<!-- 签名弹窗 -->
|
<!-- 签名弹窗 -->
|
||||||
<SignatureModal @success="handleSignFinish" />
|
<SignatureModal @success="handleSignFinish" />
|
||||||
|
|
||||||
|
<!-- 图片预览(隐藏的 Image 组件,仅用于附件预览弹窗) -->
|
||||||
|
<Image
|
||||||
|
:preview="{
|
||||||
|
open: imagePreviewOpen,
|
||||||
|
onOpenChange: handleImagePreviewOpenChange,
|
||||||
|
}"
|
||||||
|
:src="imagePreviewUrl"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -201,16 +201,25 @@ function shouldShowCustomUserSelect(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断是否需要显示审批意见 */
|
/** 判断是否需要显示审批意见和附件 */
|
||||||
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
|
function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
|
||||||
return (
|
return (
|
||||||
task.reason &&
|
(task.reason || task.attachments?.length > 0) &&
|
||||||
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||||
nodeType,
|
nodeType,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAttachmentName(url: string) {
|
||||||
|
return decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageAttachment(url: string) {
|
||||||
|
const ext = url.split('.').pop()?.toLowerCase();
|
||||||
|
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
|
||||||
|
}
|
||||||
|
|
||||||
/** 用户选择弹窗关闭 */
|
/** 用户选择弹窗关闭 */
|
||||||
function handleUserSelectClosed() {
|
function handleUserSelectClosed() {
|
||||||
selectedUsers.value = [];
|
selectedUsers.value = [];
|
||||||
|
|
@ -413,13 +422,60 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 审批意见和签名 -->
|
<!-- 审批意见、附件和签名 -->
|
||||||
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
|
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
|
||||||
<div
|
<div
|
||||||
v-if="shouldShowApprovalReason(task, activity.nodeType)"
|
v-if="shouldShowReasonAndAttachment(task, activity.nodeType)"
|
||||||
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||||
>
|
>
|
||||||
审批意见:{{ task.reason }}
|
<div v-if="task.reason">审批意见:{{ task.reason }}</div>
|
||||||
|
<div
|
||||||
|
v-if="(task.attachments?.length || 0) > 0"
|
||||||
|
:class="{
|
||||||
|
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||||
|
task.reason,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="mb-1 text-xs font-semibold text-gray-400">
|
||||||
|
附件列表:
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-1.5">
|
||||||
|
<template
|
||||||
|
v-for="(
|
||||||
|
attachment, attachmentIndex
|
||||||
|
) in task.attachments"
|
||||||
|
:key="attachmentIndex"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<IconifyIcon
|
||||||
|
:icon="
|
||||||
|
isImageAttachment(attachment)
|
||||||
|
? 'lucide:image'
|
||||||
|
: 'lucide:file-text'
|
||||||
|
"
|
||||||
|
class="text-gray-400"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
v-if="isImageAttachment(attachment)"
|
||||||
|
:width="32"
|
||||||
|
:height="32"
|
||||||
|
class="rounded border border-solid border-gray-200 object-cover"
|
||||||
|
:src="attachment"
|
||||||
|
:preview="{ src: attachment }"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
:href="attachment"
|
||||||
|
target="_blank"
|
||||||
|
class="max-w-[240px] truncate text-blue-500 hover:text-blue-600 hover:underline"
|
||||||
|
:title="getAttachmentName(attachment)"
|
||||||
|
>
|
||||||
|
{{ getAttachmentName(attachment) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const loading = ref(false);
|
||||||
const fileTree = ref<FileNode[]>([]);
|
const fileTree = ref<FileNode[]>([]);
|
||||||
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
|
const previewFiles = ref<InfraCodegenApi.CodegenPreview[]>([]);
|
||||||
const activeKey = ref<string>('');
|
const activeKey = ref<string>('');
|
||||||
|
const selectedKeys = ref<string[]>([]);
|
||||||
|
|
||||||
/** 代码地图 */
|
/** 代码地图 */
|
||||||
const codeMap = ref<Map<string, string>>(new Map<string, string>());
|
const codeMap = ref<Map<string, string>>(new Map<string, string>());
|
||||||
|
|
@ -53,31 +54,35 @@ function removeCodeMapKey(targetKey: any) {
|
||||||
/** 复制代码 */
|
/** 复制代码 */
|
||||||
async function copyCode() {
|
async function copyCode() {
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
const file = previewFiles.value.find(
|
const file = findPreviewFile(activeKey.value);
|
||||||
(item) => item.filePath === activeKey.value,
|
|
||||||
);
|
|
||||||
if (file) {
|
if (file) {
|
||||||
await copy(file.code);
|
await copy(file.code);
|
||||||
message.success('复制成功');
|
message.success('复制成功');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 文件节点点击事件 */
|
function findPreviewFile(fileKey: string) {
|
||||||
function handleNodeClick(_: any[], e: any) {
|
return previewFiles.value.find((item) => {
|
||||||
if (!e.node.isLeaf) {
|
const list = fileKey.split('.');
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeKey.value = e.node.key;
|
|
||||||
const file = previewFiles.value.find((item) => {
|
|
||||||
const list = activeKey.value.split('.');
|
|
||||||
// 特殊处理 - 包合并
|
// 特殊处理 - 包合并
|
||||||
if (list.length > 2) {
|
if (list.length > 2) {
|
||||||
const lang = list.pop();
|
const lang = list.pop();
|
||||||
return item.filePath === `${list.join('/')}.${lang}`;
|
return item.filePath === `${list.join('/')}.${lang}`;
|
||||||
}
|
}
|
||||||
return item.filePath === activeKey.value;
|
return item.filePath === fileKey;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 文件节点点击事件 */
|
||||||
|
function handleNodeClick(_: any[], e: any) {
|
||||||
|
if (!e.node.isLeaf) {
|
||||||
|
selectedKeys.value = activeKey.value ? [activeKey.value] : [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeKey.value = String(e.node.key);
|
||||||
|
selectedKeys.value = [activeKey.value];
|
||||||
|
const file = findPreviewFile(activeKey.value);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -179,6 +184,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
// 关闭时清除代码视图缓存
|
// 关闭时清除代码视图缓存
|
||||||
codeMap.value.clear();
|
codeMap.value.clear();
|
||||||
|
selectedKeys.value = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,6 +203,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
fileTree.value = handleFiles(data);
|
fileTree.value = handleFiles(data);
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
activeKey.value = data[0]?.filePath || '';
|
activeKey.value = data[0]?.filePath || '';
|
||||||
|
selectedKeys.value = activeKey.value ? [activeKey.value] : [];
|
||||||
const code = data[0]?.code || '';
|
const code = data[0]?.code || '';
|
||||||
setCodeMap(activeKey.value, code);
|
setCodeMap(activeKey.value, code);
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +224,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
<DirectoryTree
|
<DirectoryTree
|
||||||
v-if="fileTree.length > 0"
|
v-if="fileTree.length > 0"
|
||||||
default-expand-all
|
default-expand-all
|
||||||
v-model:active-key="activeKey"
|
v-model:selected-keys="selectedKeys"
|
||||||
@select="handleNodeClick"
|
@select="handleNodeClick"
|
||||||
:tree-data="fileTree"
|
:tree-data="fileTree"
|
||||||
/>
|
/>
|
||||||
|
|
@ -240,7 +247,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||||
>
|
>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
class="max-h-200"
|
class="max-h-200"
|
||||||
:value="codeMap.get(activeKey)"
|
:value="codeMap.get(key)"
|
||||||
mode="application/json"
|
mode="application/json"
|
||||||
:readonly="true"
|
:readonly="true"
|
||||||
:bordered="true"
|
:bordered="true"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatePicker,
|
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
message,
|
message,
|
||||||
|
|
@ -16,6 +15,7 @@ import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import { DatePicker } from '#/adapter/component/date-picker';
|
||||||
import {
|
import {
|
||||||
createDemo01Contact,
|
createDemo01Contact,
|
||||||
getDemo01Contact,
|
getDemo01Contact,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatePicker,
|
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
message,
|
message,
|
||||||
|
|
@ -16,6 +15,7 @@ import {
|
||||||
RadioGroup,
|
RadioGroup,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import { DatePicker } from '#/adapter/component/date-picker';
|
||||||
import {
|
import {
|
||||||
createDemo03Student,
|
createDemo03Student,
|
||||||
getDemo03Student,
|
getDemo03Student,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatePicker,
|
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
message,
|
message,
|
||||||
|
|
@ -17,6 +16,7 @@ import {
|
||||||
Tabs,
|
Tabs,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import { DatePicker } from '#/adapter/component/date-picker';
|
||||||
import {
|
import {
|
||||||
createDemo03Student,
|
createDemo03Student,
|
||||||
getDemo03Student,
|
getDemo03Student,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import { DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatePicker,
|
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
message,
|
message,
|
||||||
|
|
@ -17,6 +16,7 @@ import {
|
||||||
Tabs,
|
Tabs,
|
||||||
} from 'antdv-next';
|
} from 'antdv-next';
|
||||||
|
|
||||||
|
import { DatePicker } from '#/adapter/component/date-picker';
|
||||||
import {
|
import {
|
||||||
createDemo03Student,
|
createDemo03Student,
|
||||||
getDemo03Student,
|
getDemo03Student,
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,17 @@ function handleSearch(value: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理节点点击:支持点击同一节点取消选中 */
|
/** 处理节点点击:支持点击同一节点取消选中 */
|
||||||
function handleSelect(_selectedKeys: any[], info: any) {
|
function handleSelect(selectedNodeKeys: any[], info: any) {
|
||||||
const row = info.node.dataRef as MesMdItemTypeApi.ItemType;
|
const selectedKey = selectedNodeKeys[0] ?? info.node?.id ?? info.node?.key;
|
||||||
|
const row = itemTypeList.value.find(
|
||||||
|
(item) => String(item.id) === String(selectedKey),
|
||||||
|
);
|
||||||
|
if (!row) {
|
||||||
|
currentNodeId.value = undefined;
|
||||||
|
selectedKeys.value = [];
|
||||||
|
emit('nodeClick', undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (currentNodeId.value === row.id) {
|
if (currentNodeId.value === row.id) {
|
||||||
currentNodeId.value = undefined;
|
currentNodeId.value = undefined;
|
||||||
selectedKeys.value = [];
|
selectedKeys.value = [];
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,12 @@ function handleSearch(e: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 选中部门:点击已选中的节点时取消选中 */
|
/** 选中部门:点击已选中的节点时取消选中 */
|
||||||
function handleSelect(_selectedKeys: any[], info: any) {
|
function handleSelect(selectedNodeKeys: any[], info: any) {
|
||||||
emit('select', info.selected ? info.node.dataRef : undefined);
|
const selectedKey = selectedNodeKeys[0];
|
||||||
|
const dept = info.selected
|
||||||
|
? deptList.value.find((item) => String(item.id) === String(selectedKey))
|
||||||
|
: undefined;
|
||||||
|
emit('select', dept);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重置选中状态(供外部重置按钮调用) */
|
/** 重置选中状态(供外部重置按钮调用) */
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,12 @@ function handleSearch(e: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 选中部门 */
|
/** 选中部门 */
|
||||||
function handleSelect(_selectedKeys: any[], info: any) {
|
function handleSelect(selectedNodeKeys: any[], info: any) {
|
||||||
emit('select', info.node.dataRef);
|
const selectedKey = selectedNodeKeys[0] ?? info.node?.id ?? info.node?.key;
|
||||||
|
const dept = info.selected
|
||||||
|
? deptList.value.find((item) => String(item.id) === String(selectedKey))
|
||||||
|
: undefined;
|
||||||
|
emit('select', dept);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,17 @@ function handleSearch(value: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理节点点击:支持点击同一节点取消选中 */
|
/** 处理节点点击:支持点击同一节点取消选中 */
|
||||||
function handleSelect(_selectedKeys: any[], info: any) {
|
function handleSelect(selectedNodeKeys: any[], info: any) {
|
||||||
const row = info.node.dataRef as WmsItemCategoryApi.ItemCategory;
|
const selectedKey = selectedNodeKeys[0] ?? info.node?.id ?? info.node?.key;
|
||||||
|
const row = categoryList.value.find(
|
||||||
|
(item) => String(item.id) === String(selectedKey),
|
||||||
|
);
|
||||||
|
if (!row) {
|
||||||
|
currentNodeId.value = undefined;
|
||||||
|
selectedKeys.value = [];
|
||||||
|
emit('nodeClick', undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (currentNodeId.value === row.id) {
|
if (currentNodeId.value === row.id) {
|
||||||
currentNodeId.value = undefined;
|
currentNodeId.value = undefined;
|
||||||
selectedKeys.value = [];
|
selectedKeys.value = [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue