mirror of
https://gitee.com/yudaocode/yudao-ui-admin-vben.git
synced 2025-12-30 10:32:25 +00:00
!311 (〃'▽'〃) v2025.12 发布:极大极大完善 vben5 的 antd、vben 版本的功能,新增 admin uniapp vue3 版本
Merge pull request !311 from 芋道源码/dev
This commit is contained in:
@@ -30,6 +30,7 @@ export namespace BpmModelApi {
|
||||
deploymentTime: number;
|
||||
suspensionState: number;
|
||||
formType?: number;
|
||||
formCustomCreatePath?: string;
|
||||
formCustomViewPath?: string;
|
||||
formFields?: string[];
|
||||
}
|
||||
|
||||
@@ -51,12 +51,12 @@ const { getStringAccept } = useUploadType({
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
// 计算当前绑定的值,优先使用 modelValue
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
// 判断是否使用 modelValue
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
@@ -82,19 +82,21 @@ watch(
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as UploadProps['fileList'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
@@ -107,6 +109,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 处理文件删除 */
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
@@ -120,17 +123,17 @@ async function handleRemove(file: UploadFile) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件预览
|
||||
/** 处理文件预览 */
|
||||
function handlePreview(file: UploadFile) {
|
||||
emit('preview', file);
|
||||
}
|
||||
|
||||
// 处理文件数量超限
|
||||
/** 处理文件数量超限 */
|
||||
function handleExceed() {
|
||||
message.error($t('ui.upload.maxNumber', [maxNumber.value]));
|
||||
}
|
||||
|
||||
// 处理上传错误
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error($t('ui.upload.uploadError'));
|
||||
@@ -138,6 +141,11 @@ function handleUploadError(error: any) {
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
* @param file 待上传的文件
|
||||
* @returns 是否允许上传
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
const fileContent = await file.text();
|
||||
emit('returnText', fileContent);
|
||||
@@ -171,7 +179,8 @@ async function beforeUpload(file: File) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
/** 自定义上传请求 */
|
||||
async function customRequest(info: UploadRequestOption) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
@@ -196,7 +205,11 @@ async function customRequest(info: UploadRequestOption<any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
/**
|
||||
* 处理上传成功
|
||||
* @param res 上传响应结果
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
@@ -228,6 +241,10 @@ function handleUploadSuccess(res: any, file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件列表的值
|
||||
* @returns 文件 URL 列表或字符串
|
||||
*/
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
|
||||
@@ -55,12 +55,12 @@ const { getStringAccept } = useUploadType({
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
// 计算当前绑定的值,优先使用 modelValue
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
// 判断是否使用 modelValue
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
@@ -89,19 +89,21 @@ watch(
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
}) as UploadProps['fileList'];
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: `${-i}`,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.DONE,
|
||||
url: item,
|
||||
};
|
||||
} else if (item && isObject(item)) {
|
||||
return item;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as UploadProps['fileList'];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
@@ -114,6 +116,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 将文件转换为 Base64 格式 */
|
||||
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -125,6 +128,7 @@ function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理图片预览 */
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64<string>(file.originFileObj!);
|
||||
@@ -138,6 +142,7 @@ async function handlePreview(file: UploadFile) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 处理文件删除 */
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
@@ -151,11 +156,17 @@ async function handleRemove(file: UploadFile) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭预览弹窗 */
|
||||
function handleCancel() {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
* @param file 待上传的文件
|
||||
* @returns 是否允许上传
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
// 检查文件数量限制
|
||||
if (fileList.value!.length >= props.maxNumber) {
|
||||
@@ -186,7 +197,8 @@ async function beforeUpload(file: File) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
/** 自定义上传请求 */
|
||||
async function customRequest(info: UploadRequestOption) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
@@ -211,7 +223,11 @@ async function customRequest(info: UploadRequestOption<any>) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传成功
|
||||
/**
|
||||
* 处理上传成功
|
||||
* @param res 上传响应结果
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
@@ -243,14 +259,18 @@ function handleUploadSuccess(res: any, file: File) {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传错误
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
message.error('上传错误!!!');
|
||||
message.error($t('ui.upload.uploadError'));
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件列表的值
|
||||
* @returns 文件 URL 列表或字符串
|
||||
*/
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.DONE)
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { Col, Input, Row, Textarea } from 'ant-design-vue';
|
||||
import { Input, Textarea } from 'ant-design-vue';
|
||||
|
||||
import FileUpload from './file-upload.vue';
|
||||
|
||||
@@ -30,6 +30,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
/** 处理文件内容返回 */
|
||||
function handleReturnText(text: string) {
|
||||
modelValue.value = text;
|
||||
emits('change', modelValue.value);
|
||||
@@ -37,6 +38,7 @@ function handleReturnText(text: string) {
|
||||
emits('update:modelValue', modelValue.value);
|
||||
}
|
||||
|
||||
/** 计算输入框属性 */
|
||||
const inputProps = computed(() => {
|
||||
return {
|
||||
...props.inputProps,
|
||||
@@ -44,6 +46,7 @@ const inputProps = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
/** 计算文本域属性 */
|
||||
const textareaProps = computed(() => {
|
||||
return {
|
||||
...props.textareaProps,
|
||||
@@ -51,6 +54,7 @@ const textareaProps = computed(() => {
|
||||
};
|
||||
});
|
||||
|
||||
/** 计算文件上传属性 */
|
||||
const fileUploadProps = computed(() => {
|
||||
return {
|
||||
...props.fileUploadProps,
|
||||
@@ -58,17 +62,17 @@ const fileUploadProps = computed(() => {
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Row>
|
||||
<Col :span="18">
|
||||
<Input readonly v-if="inputType === 'input'" v-bind="inputProps" />
|
||||
<Textarea readonly v-else :row="4" v-bind="textareaProps" />
|
||||
</Col>
|
||||
<Col :span="6">
|
||||
<FileUpload
|
||||
class="ml-4"
|
||||
v-bind="fileUploadProps"
|
||||
@return-text="handleReturnText"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<div class="w-full">
|
||||
<Input v-if="inputType === 'input'" readonly v-bind="inputProps">
|
||||
<template #suffix>
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</template>
|
||||
</Input>
|
||||
<div v-else class="relative w-full">
|
||||
<Textarea readonly :rows="4" v-bind="textareaProps" />
|
||||
<div class="absolute bottom-2 right-2">
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -12,28 +12,21 @@ export enum UploadResultStatus {
|
||||
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
||||
|
||||
export interface FileUploadProps {
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
accept?: string[]; // 根据后缀,或者其他
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
) => Promise<AxiosResponse>;
|
||||
directory?: string; // 上传的目录
|
||||
disabled?: boolean;
|
||||
drag?: boolean; // 是否支持拖拽上传
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
maxNumber?: number; // 最大数量的文件,Infinity不限制
|
||||
modelValue?: string | string[]; // v-model 支持
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
maxSize?: number; // 文件最大多少MB
|
||||
multiple?: boolean; // 是否支持多选
|
||||
resultField?: string; // support xxx.xxx.xx
|
||||
showDescription?: boolean; // 是否显示下面的描述
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@ enum UPLOAD_TYPE {
|
||||
SERVER = 'server',
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传类型钩子函数
|
||||
* @param acceptRef 接受的文件类型
|
||||
* @param helpTextRef 帮助文本
|
||||
* @param maxNumberRef 最大文件数量
|
||||
* @param maxSizeRef 最大文件大小
|
||||
* @returns 文件类型限制和帮助文本的计算属性
|
||||
*/
|
||||
export function useUploadType({
|
||||
acceptRef,
|
||||
helpTextRef,
|
||||
@@ -78,7 +86,11 @@ export function useUploadType({
|
||||
return { getAccept, getStringAccept, getHelpText };
|
||||
}
|
||||
|
||||
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||
/**
|
||||
* 上传钩子函数
|
||||
* @param directory 上传目录
|
||||
* @returns 上传 URL 和自定义上传方法
|
||||
*/
|
||||
export function useUpload(directory?: string) {
|
||||
// 后端上传地址
|
||||
const uploadUrl = getUploadUrl();
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { BpmOALeaveApi } from '#/api/bpm/oa/leave';
|
||||
import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm, Page, useVbenForm } from '@vben/common-ui';
|
||||
import { BpmCandidateStrategyEnum, BpmNodeIdEnum } from '@vben/constants';
|
||||
@@ -13,7 +14,7 @@ import { Button, Card, message, Space } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getProcessDefinition } from '#/api/bpm/definition';
|
||||
import { createLeave, updateLeave } from '#/api/bpm/oa/leave';
|
||||
import { createLeave, getLeave, updateLeave } from '#/api/bpm/oa/leave';
|
||||
import { getApprovalDetail as getApprovalDetailApi } from '#/api/bpm/processInstance';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
@@ -22,6 +23,7 @@ import ProcessInstanceTimeline from '#/views/bpm/processInstance/detail/modules/
|
||||
import { useFormSchema } from './data';
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
const { query } = useRoute();
|
||||
|
||||
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
||||
@@ -35,7 +37,7 @@ const processDefinitionId = ref('');
|
||||
const formData = ref<BpmOALeaveApi.Leave>();
|
||||
const getTitle = computed(() => {
|
||||
return formData.value?.id
|
||||
? $t('ui.actionTitle.edit', ['请假'])
|
||||
? '重新发起请假'
|
||||
: $t('ui.actionTitle.create', ['请假']);
|
||||
});
|
||||
|
||||
@@ -157,6 +159,34 @@ function selectUserConfirm(id: string, userList: any[]) {
|
||||
startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id);
|
||||
}
|
||||
|
||||
/** 获取请假数据,用于重新发起时自动填充 */
|
||||
async function getDetail(id: number) {
|
||||
try {
|
||||
formLoading.value = true;
|
||||
const data = await getLeave(id);
|
||||
if (!data) {
|
||||
message.error('重新发起请假失败,原因:请假数据不存在');
|
||||
return;
|
||||
}
|
||||
formData.value = {
|
||||
...formData.value,
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
reason: data.reason,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
} as BpmOALeaveApi.Leave;
|
||||
await formApi.setValues({
|
||||
type: data.type,
|
||||
reason: data.reason,
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
});
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
|
||||
watch(
|
||||
formData.value as object,
|
||||
@@ -190,6 +220,11 @@ onMounted(async () => {
|
||||
processDefinitionId.value = processDefinitionDetail.id;
|
||||
startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks;
|
||||
|
||||
// 如果是重新发起,则加载请假数据
|
||||
if (query.id) {
|
||||
await getDetail(Number(query.id));
|
||||
}
|
||||
|
||||
await getApprovalDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -168,7 +168,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
width: 240,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
|
||||
@@ -33,6 +33,16 @@ function handleCreate() {
|
||||
});
|
||||
}
|
||||
|
||||
/** 重新发起请假 */
|
||||
function handleReCreate(row: BpmOALeaveApi.Leave) {
|
||||
router.push({
|
||||
name: 'OALeaveCreate',
|
||||
query: {
|
||||
id: row.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 取消请假 */
|
||||
function handleCancel(row: BpmOALeaveApi.Leave) {
|
||||
prompt({
|
||||
@@ -161,9 +171,16 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
type: 'link',
|
||||
danger: true,
|
||||
icon: ACTION_ICON.DELETE,
|
||||
ifShow: row.result === BpmProcessInstanceStatus.RUNNING,
|
||||
ifShow: row.status === BpmProcessInstanceStatus.RUNNING,
|
||||
onClick: handleCancel.bind(null, row),
|
||||
},
|
||||
{
|
||||
label: '重新发起',
|
||||
type: 'link',
|
||||
icon: ACTION_ICON.ADD,
|
||||
ifShow: row.status !== BpmProcessInstanceStatus.RUNNING,
|
||||
onClick: handleReCreate.bind(null, row),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -5,7 +5,11 @@ import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DocAlert, Page, prompt } from '@vben/common-ui';
|
||||
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
|
||||
import {
|
||||
BpmModelFormType,
|
||||
BpmProcessInstanceStatus,
|
||||
DICT_TYPE,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { Button, message, Textarea } from 'ant-design-vue';
|
||||
|
||||
@@ -37,23 +41,34 @@ function handleDetail(row: BpmProcessInstanceApi.ProcessInstance) {
|
||||
}
|
||||
|
||||
/** 重新发起流程 */
|
||||
async function handleCreate(row: BpmProcessInstanceApi.ProcessInstance) {
|
||||
// 如果是【业务表单】,不支持重新发起
|
||||
async function handleCreate(row?: BpmProcessInstanceApi.ProcessInstance) {
|
||||
if (row?.id) {
|
||||
const processDefinitionDetail = await getProcessDefinition(
|
||||
row.processDefinitionId,
|
||||
);
|
||||
if (processDefinitionDetail.formType === 20) {
|
||||
message.error(
|
||||
'重新发起流程失败,原因:该流程使用业务表单,不支持重新发起',
|
||||
);
|
||||
if (processDefinitionDetail?.formType === BpmModelFormType.CUSTOM) {
|
||||
if (!processDefinitionDetail.formCustomCreatePath) {
|
||||
message.error('未配置业务表单的提交路由,无法重新发起');
|
||||
return;
|
||||
}
|
||||
await router.push({
|
||||
path: processDefinitionDetail.formCustomCreatePath,
|
||||
query: {
|
||||
id: row.businessKey,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else if (processDefinitionDetail?.formType === BpmModelFormType.NORMAL) {
|
||||
await router.push({
|
||||
name: 'BpmProcessInstanceCreate',
|
||||
query: { processInstanceId: row.id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 跳转发起流程界面
|
||||
await router.push({
|
||||
name: 'BpmProcessInstanceCreate',
|
||||
query: { processInstanceId: row?.id },
|
||||
query: row?.id ? { processInstanceId: row.id } : {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
// 客户转化率分析
|
||||
case 'conversionStat': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '客户转化率',
|
||||
@@ -40,12 +57,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户转化率分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '转化率(%)',
|
||||
@@ -59,14 +71,13 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'customerSummary': {
|
||||
return {
|
||||
grid: {
|
||||
bottom: '5%',
|
||||
containLabel: true,
|
||||
grid: getGrid({
|
||||
bottom: '8%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '5 %',
|
||||
},
|
||||
legend: {},
|
||||
top: 80,
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数',
|
||||
@@ -92,12 +103,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -134,13 +140,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -166,12 +167,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -208,13 +204,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -240,12 +231,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -277,13 +263,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
const customerDealCycleByDate = res.customerDealCycleByDate;
|
||||
const customerDealCycleByUser = res.customerDealCycleByUser;
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -309,12 +290,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -342,15 +318,13 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
},
|
||||
};
|
||||
}
|
||||
// 客户跟进次数分析
|
||||
case 'followUpSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
grid: getGrid({
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '跟进客户数',
|
||||
@@ -376,12 +350,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户跟进次数分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -412,20 +381,21 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
},
|
||||
};
|
||||
}
|
||||
// 客户跟进方式分析
|
||||
case 'followUpType': {
|
||||
return {
|
||||
title: {
|
||||
text: '客户跟进方式分析',
|
||||
left: 'center',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
legend: getLegend({
|
||||
left: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
}),
|
||||
tooltip: getTooltip({
|
||||
trigger: 'item',
|
||||
axisPointer: undefined,
|
||||
formatter: '{b} : {c}% ',
|
||||
},
|
||||
}),
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '客户跟进方式分析' }, // 保存为图片
|
||||
@@ -458,13 +428,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'poolSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '进入公海客户数',
|
||||
@@ -490,12 +455,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '公海客户分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { erpCalculatePercentage } from '@vben/utils';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(
|
||||
activeTabName: any,
|
||||
active: boolean,
|
||||
@@ -9,26 +30,19 @@ export function getChartOptions(
|
||||
case 'businessInversionRateSummary': {
|
||||
return {
|
||||
color: ['#6ca2ff', '#6ac9d7', '#ff7474'],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
// 坐标轴指示器,坐标轴触发有效
|
||||
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
tooltip: getTooltip(),
|
||||
legend: getLegend({
|
||||
data: ['赢单转化率', '商机总数', '赢单商机数'],
|
||||
bottom: '0px',
|
||||
itemWidth: 14,
|
||||
},
|
||||
grid: {
|
||||
}),
|
||||
grid: getGrid({
|
||||
top: '40px',
|
||||
left: '40px',
|
||||
right: '40px',
|
||||
bottom: '40px',
|
||||
containLabel: true,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
}),
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
@@ -117,13 +131,11 @@ export function getChartOptions(
|
||||
}
|
||||
case 'businessSummary': {
|
||||
return {
|
||||
grid: {
|
||||
grid: getGrid({
|
||||
left: 30,
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增商机数量',
|
||||
@@ -149,12 +161,7 @@ export function getChartOptions(
|
||||
saveAsImage: { show: true, name: '新增商机分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -211,10 +218,11 @@ export function getChartOptions(
|
||||
title: {
|
||||
text: '销售漏斗',
|
||||
},
|
||||
tooltip: {
|
||||
tooltip: getTooltip({
|
||||
trigger: 'item',
|
||||
axisPointer: undefined,
|
||||
formatter: '{a} <br/>{b}',
|
||||
},
|
||||
}),
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataView: { readOnly: false },
|
||||
@@ -222,9 +230,9 @@ export function getChartOptions(
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
legend: getLegend({
|
||||
data: ['客户', '商机', '赢单'],
|
||||
},
|
||||
}),
|
||||
series: [
|
||||
{
|
||||
name: '销售漏斗',
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'ContractCountPerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月合同数量(个)',
|
||||
@@ -65,12 +81,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -131,13 +142,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'ContractPricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月合同金额(元)',
|
||||
@@ -260,13 +266,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'ReceivablePricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月回款金额(元)',
|
||||
|
||||
@@ -13,6 +13,71 @@ function areaReplace(areaName: string) {
|
||||
.replace('省', '');
|
||||
}
|
||||
|
||||
const getPieTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'item',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getPieLegend = (extra: Record<string, any> = {}) => ({
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getPieSeries = (name: string, data: any[]) => ({
|
||||
name,
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
const getPiePanel = ({
|
||||
data,
|
||||
legendExtra,
|
||||
seriesName,
|
||||
title,
|
||||
tooltipExtra,
|
||||
}: {
|
||||
data: any[];
|
||||
legendExtra?: Record<string, any>;
|
||||
seriesName: string;
|
||||
title: string;
|
||||
tooltipExtra?: Record<string, any>;
|
||||
}) => ({
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: getPieTooltip(tooltipExtra),
|
||||
legend: getPieLegend(legendExtra),
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: title },
|
||||
},
|
||||
},
|
||||
series: [getPieSeries(seriesName, data)],
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'area': {
|
||||
@@ -111,326 +176,62 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'industry': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'level': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'source': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = () => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'contactCountRank': {
|
||||
@@ -8,15 +28,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增联系人数排行',
|
||||
@@ -34,12 +47,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '新增联系人数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增联系人数(个)',
|
||||
@@ -56,15 +64,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '签约合同排行',
|
||||
@@ -82,12 +83,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '签约合同排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '签约合同数(个)',
|
||||
@@ -104,15 +100,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '合同金额排行',
|
||||
@@ -130,12 +119,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '合同金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '合同金额(元)',
|
||||
@@ -152,15 +136,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数排行',
|
||||
@@ -178,12 +155,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '新增客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增客户数(个)',
|
||||
@@ -226,12 +198,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '跟进次数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进次数(次)',
|
||||
@@ -274,12 +241,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '跟进客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进客户数(个)',
|
||||
@@ -322,12 +284,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '产品销量排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '产品销量',
|
||||
@@ -370,12 +327,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '回款金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '回款金额(元)',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { provide, ref } from 'vue';
|
||||
import { useAccess } from '@vben/access';
|
||||
import { confirm, DocAlert, Page } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button, message, Tabs } from 'ant-design-vue';
|
||||
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
import { UploadType } from './modules/upload';
|
||||
import UploadFile from './modules/UploadFile.vue';
|
||||
import UploadVideo from './modules/UploadVideo.vue';
|
||||
import {$t} from '@vben/locales';
|
||||
|
||||
defineOptions({ name: 'MpMaterial' });
|
||||
|
||||
@@ -106,7 +106,7 @@ async function onTabChange() {
|
||||
},
|
||||
cellConfig: {
|
||||
height: type.value === UploadType.Image ? 220 : undefined,
|
||||
}
|
||||
},
|
||||
});
|
||||
await gridApi.reload();
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as PayChannelApi.Channel;
|
||||
data.config = JSON.stringify(data.config);
|
||||
data.config = JSON.stringify(data.config || {});
|
||||
try {
|
||||
await (data.id ? updateChannel(data) : createChannel(data));
|
||||
// 关闭并提示
|
||||
|
||||
@@ -30,6 +30,7 @@ export namespace BpmModelApi {
|
||||
deploymentTime: number;
|
||||
suspensionState: number;
|
||||
formType?: number;
|
||||
formCustomCreatePath?: string;
|
||||
formCustomViewPath?: string;
|
||||
formFields?: string[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { CronData, CronValue, ShortcutsType } from './types';
|
||||
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
import {
|
||||
@@ -19,6 +21,8 @@ import {
|
||||
ElTabs,
|
||||
} from 'element-plus';
|
||||
|
||||
import { CronDataDefault, CronValueDefault } from './types';
|
||||
|
||||
defineOptions({ name: 'Crontab' });
|
||||
|
||||
const props = defineProps({
|
||||
@@ -26,237 +30,20 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: '* * * * * ?',
|
||||
},
|
||||
shortcuts: { type: Array as PropType<shortcutsType[]>, default: () => [] },
|
||||
shortcuts: {
|
||||
type: Array as PropType<ShortcutsType[]>,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
// TODO @puhui999:可以参考 apps/web-antd/src/components/cron-tab/cron-tab.vue 简化到 types;ps:可以用 idea 对比两个 ts 或者 vue 文件,看看差异的地方。差异的地方越少越好(容易维护)
|
||||
interface shortcutsType {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const defaultValue = ref('');
|
||||
const dialogVisible = ref(false);
|
||||
const getYear = () => {
|
||||
const v: number[] = [];
|
||||
const y = new Date().getFullYear();
|
||||
for (let i = 0; i < 11; i++) {
|
||||
v.push(y + i);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
// TODO @puhui999:可以参考 apps/web-antd/src/components/cron-tab/cron-tab.vue 简化到 types
|
||||
const cronValue = reactive({
|
||||
second: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [] as string[],
|
||||
},
|
||||
minute: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [] as string[],
|
||||
},
|
||||
hour: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [] as string[],
|
||||
},
|
||||
day: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 1,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [] as string[],
|
||||
},
|
||||
month: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 1,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [] as string[],
|
||||
},
|
||||
week: {
|
||||
type: '5',
|
||||
range: {
|
||||
start: '2',
|
||||
end: '3',
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: '2',
|
||||
},
|
||||
last: '2',
|
||||
appoint: [] as string[],
|
||||
},
|
||||
year: {
|
||||
type: '-1',
|
||||
range: {
|
||||
start: getYear()[0],
|
||||
end: getYear()[1],
|
||||
},
|
||||
loop: {
|
||||
start: getYear()[0],
|
||||
end: 1,
|
||||
},
|
||||
appoint: [] as string[],
|
||||
},
|
||||
});
|
||||
const data = reactive({
|
||||
second: [
|
||||
'0',
|
||||
'5',
|
||||
'15',
|
||||
'20',
|
||||
'25',
|
||||
'30',
|
||||
'35',
|
||||
'40',
|
||||
'45',
|
||||
'50',
|
||||
'55',
|
||||
'59',
|
||||
],
|
||||
minute: [
|
||||
'0',
|
||||
'5',
|
||||
'15',
|
||||
'20',
|
||||
'25',
|
||||
'30',
|
||||
'35',
|
||||
'40',
|
||||
'45',
|
||||
'50',
|
||||
'55',
|
||||
'59',
|
||||
],
|
||||
hour: [
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
],
|
||||
day: [
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
'24',
|
||||
'25',
|
||||
'26',
|
||||
'27',
|
||||
'28',
|
||||
'29',
|
||||
'30',
|
||||
'31',
|
||||
],
|
||||
month: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
|
||||
week: [
|
||||
{
|
||||
value: '1',
|
||||
label: '周日',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '周一',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '周二',
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '周三',
|
||||
},
|
||||
{
|
||||
value: '5',
|
||||
label: '周四',
|
||||
},
|
||||
{
|
||||
value: '6',
|
||||
label: '周五',
|
||||
},
|
||||
{
|
||||
value: '7',
|
||||
label: '周六',
|
||||
},
|
||||
],
|
||||
year: getYear(),
|
||||
});
|
||||
|
||||
const cronValue = reactive<CronValue>(CronValueDefault);
|
||||
|
||||
const data = reactive<CronData>(CronDataDefault);
|
||||
const value_second = computed(() => {
|
||||
const v = cronValue.second;
|
||||
switch (v.type) {
|
||||
|
||||
266
apps/web-ele/src/components/cron-tab/types.ts
Normal file
266
apps/web-ele/src/components/cron-tab/types.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
export interface ShortcutsType {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CronRange {
|
||||
start: number | string | undefined;
|
||||
end: number | string | undefined;
|
||||
}
|
||||
|
||||
export interface CronLoop {
|
||||
start: number | string | undefined;
|
||||
end: number | string | undefined;
|
||||
}
|
||||
|
||||
export interface CronItem {
|
||||
type: string;
|
||||
range: CronRange;
|
||||
loop: CronLoop;
|
||||
appoint: string[];
|
||||
last?: string;
|
||||
}
|
||||
|
||||
export interface CronValue {
|
||||
second: CronItem;
|
||||
minute: CronItem;
|
||||
hour: CronItem;
|
||||
day: CronItem;
|
||||
month: CronItem;
|
||||
week: CronItem & { last: string };
|
||||
year: CronItem;
|
||||
}
|
||||
|
||||
export interface WeekOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface CronData {
|
||||
second: string[];
|
||||
minute: string[];
|
||||
hour: string[];
|
||||
day: string[];
|
||||
month: string[];
|
||||
week: WeekOption[];
|
||||
year: number[];
|
||||
}
|
||||
|
||||
const getYear = (): number[] => {
|
||||
const v: number[] = [];
|
||||
const y = new Date().getFullYear();
|
||||
for (let i = 0; i < 11; i++) {
|
||||
v.push(y + i);
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
export const CronValueDefault: CronValue = {
|
||||
second: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
minute: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
hour: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
day: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 1,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
month: {
|
||||
type: '0',
|
||||
range: {
|
||||
start: 1,
|
||||
end: 2,
|
||||
},
|
||||
loop: {
|
||||
start: 1,
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
week: {
|
||||
type: '5',
|
||||
range: {
|
||||
start: '2',
|
||||
end: '3',
|
||||
},
|
||||
loop: {
|
||||
start: 0,
|
||||
end: '2',
|
||||
},
|
||||
last: '2',
|
||||
appoint: [],
|
||||
},
|
||||
year: {
|
||||
type: '-1',
|
||||
range: {
|
||||
start: getYear()[0],
|
||||
end: getYear()[1],
|
||||
},
|
||||
loop: {
|
||||
start: getYear()[0],
|
||||
end: 1,
|
||||
},
|
||||
appoint: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const CronDataDefault: CronData = {
|
||||
second: [
|
||||
'0',
|
||||
'5',
|
||||
'15',
|
||||
'20',
|
||||
'25',
|
||||
'30',
|
||||
'35',
|
||||
'40',
|
||||
'45',
|
||||
'50',
|
||||
'55',
|
||||
'59',
|
||||
],
|
||||
minute: [
|
||||
'0',
|
||||
'5',
|
||||
'15',
|
||||
'20',
|
||||
'25',
|
||||
'30',
|
||||
'35',
|
||||
'40',
|
||||
'45',
|
||||
'50',
|
||||
'55',
|
||||
'59',
|
||||
],
|
||||
hour: [
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
],
|
||||
day: [
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10',
|
||||
'11',
|
||||
'12',
|
||||
'13',
|
||||
'14',
|
||||
'15',
|
||||
'16',
|
||||
'17',
|
||||
'18',
|
||||
'19',
|
||||
'20',
|
||||
'21',
|
||||
'22',
|
||||
'23',
|
||||
'24',
|
||||
'25',
|
||||
'26',
|
||||
'27',
|
||||
'28',
|
||||
'29',
|
||||
'30',
|
||||
'31',
|
||||
],
|
||||
month: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
|
||||
week: [
|
||||
{
|
||||
value: '1',
|
||||
label: '周日',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '周一',
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '周二',
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '周三',
|
||||
},
|
||||
{
|
||||
value: '5',
|
||||
label: '周四',
|
||||
},
|
||||
{
|
||||
value: '6',
|
||||
label: '周五',
|
||||
},
|
||||
{
|
||||
value: '7',
|
||||
label: '周六',
|
||||
},
|
||||
],
|
||||
year: getYear(),
|
||||
};
|
||||
@@ -1,284 +1,383 @@
|
||||
<script lang="ts" setup>
|
||||
// TODO @puhui999:这个看看怎么和对应的 antd 【代码风格】,保持一致一些;
|
||||
import type {
|
||||
UploadFile,
|
||||
UploadInstance,
|
||||
UploadProps,
|
||||
UploadRawFile,
|
||||
UploadProgressEvent,
|
||||
UploadRequestOptions,
|
||||
UploadUserFile,
|
||||
} from 'element-plus';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { isString } from '@vben/utils';
|
||||
import { $t } from '@vben/locales';
|
||||
import { checkFileType, isFunction, isObject, isString } from '@vben/utils';
|
||||
|
||||
import { ElButton, ElLink, ElMessage, ElUpload } from 'element-plus';
|
||||
import { ElButton, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { useUpload } from './use-upload';
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
autoUpload?: boolean;
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
drag?: boolean;
|
||||
fileSize?: number;
|
||||
fileType?: string[];
|
||||
isShowTip?: boolean;
|
||||
limit?: number;
|
||||
modelValue: string | string[];
|
||||
}>(),
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
value: () => [],
|
||||
modelValue: undefined,
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
drag: false,
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => [],
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: false,
|
||||
});
|
||||
const emit = defineEmits([
|
||||
'change',
|
||||
'update:value',
|
||||
'update:modelValue',
|
||||
'delete',
|
||||
'returnText',
|
||||
'preview',
|
||||
]);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
helpTextRef: helpText,
|
||||
maxNumberRef: maxNumber,
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
|
||||
const isActMsg = ref<boolean>(true); // 文件类型错误提示
|
||||
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
|
||||
const uploadNumber = ref<number>(0); // 上传文件计数器
|
||||
const uploadList = ref<any[]>([]); // 临时上传列表
|
||||
|
||||
watch(
|
||||
currentValue,
|
||||
(v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
return;
|
||||
}
|
||||
let value: string[] = [];
|
||||
if (v) {
|
||||
if (Array.isArray(v)) {
|
||||
value = v;
|
||||
} else {
|
||||
value.push(v);
|
||||
}
|
||||
fileList.value = value
|
||||
.map((item, i) => {
|
||||
if (item && isString(item)) {
|
||||
return {
|
||||
uid: -i,
|
||||
name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)),
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
url: item,
|
||||
} as UploadFile;
|
||||
} else if (item && isObject(item)) {
|
||||
const file = item as unknown as Record<string, any>;
|
||||
return {
|
||||
uid: file.uid ?? -i,
|
||||
name: file.name ?? '',
|
||||
status: file.status ?? UploadResultStatus.SUCCESS,
|
||||
url: file.url,
|
||||
} as UploadFile;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as UploadFile[];
|
||||
} else {
|
||||
// 值为空时清空文件列表
|
||||
fileList.value = [];
|
||||
}
|
||||
if (!isFirstRender.value) {
|
||||
emit('change', value);
|
||||
isFirstRender.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
fileType: () => ['doc', 'xls', 'ppt', 'txt', 'pdf'], // 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
fileSize: 5, // 大小限制(MB)
|
||||
limit: 5, // 数量限制
|
||||
autoUpload: true, // 自动上传
|
||||
drag: false, // 拖拽上传
|
||||
isShowTip: true, // 是否显示提示
|
||||
disabled: false, // 是否禁用上传组件 ==> 非必传(默认为 false)
|
||||
directory: undefined, // 上传目录 ==> 非必传(默认为 undefined)
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
/** 处理文件删除 */
|
||||
function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 上传相关 ==========
|
||||
const uploadRef = ref<UploadInstance>();
|
||||
const uploadList = ref<UploadUserFile[]>([]);
|
||||
const fileList = ref<UploadUserFile[]>([]);
|
||||
const uploadNumber = ref<number>(0);
|
||||
/** 处理文件预览 */
|
||||
function handlePreview(file: UploadFile) {
|
||||
emit('preview', file);
|
||||
if (file.url) {
|
||||
window.open(file.url);
|
||||
}
|
||||
}
|
||||
|
||||
const { uploadUrl, httpRequest }: any = useUpload(props.directory);
|
||||
/** 处理文件数量超限 */
|
||||
function handleExceed() {
|
||||
ElMessage.error($t('ui.upload.maxNumber', [maxNumber.value]));
|
||||
}
|
||||
|
||||
/** httpRequest 适配 ele */
|
||||
const httpRequest0 = (options: UploadRequestOptions) => {
|
||||
return httpRequest(options.file);
|
||||
};
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
ElMessage.error($t('ui.upload.uploadError'));
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
// 文件上传之前判断
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
|
||||
if (fileList.value.length >= props.limit) {
|
||||
ElMessage.error(`上传文件数量不能超过${props.limit}个!`);
|
||||
/**
|
||||
* 上传前校验
|
||||
* @param file 待上传的文件
|
||||
* @returns 是否允许上传
|
||||
*/
|
||||
/* eslint-disable unicorn/no-nested-ternary */
|
||||
async function beforeUpload(file: File) {
|
||||
const fileContent = await file.text();
|
||||
emit('returnText', fileContent);
|
||||
|
||||
// 检查文件数量限制(使用 getValue 获取实际已上传的文件数量)
|
||||
const currentFiles = getValue();
|
||||
const currentCount = Array.isArray(currentFiles)
|
||||
? currentFiles.length
|
||||
: currentFiles
|
||||
? 1
|
||||
: 0;
|
||||
if (currentCount >= props.maxNumber) {
|
||||
ElMessage.error($t('ui.upload.maxNumber', [props.maxNumber]));
|
||||
return false;
|
||||
}
|
||||
let fileExtension = '';
|
||||
// eslint-disable-next-line unicorn/prefer-includes
|
||||
if (file.name.lastIndexOf('.') > -1) {
|
||||
fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
|
||||
}
|
||||
const isImg = props.fileType.some((type: string) => {
|
||||
// eslint-disable-next-line unicorn/prefer-includes
|
||||
if (file.type.indexOf(type) > -1) return true;
|
||||
// eslint-disable-next-line unicorn/prefer-includes
|
||||
return !!(fileExtension && fileExtension.indexOf(type) > -1);
|
||||
});
|
||||
const isLimit = file.size < props.fileSize * 1024 * 1024;
|
||||
if (!isImg) {
|
||||
ElMessage.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式!`);
|
||||
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkFileType(file, accept);
|
||||
if (!isAct) {
|
||||
ElMessage.error($t('ui.upload.acceptUpload', [accept]));
|
||||
isActMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isActMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
if (!isLimit) {
|
||||
ElMessage.error(`上传文件大小不能超过${props.fileSize}MB!`);
|
||||
const isLt = file.size / 1024 / 1024 > maxSize;
|
||||
if (isLt) {
|
||||
ElMessage.error($t('ui.upload.maxSizeMultiple', [maxSize]));
|
||||
isLtMsg.value = false;
|
||||
// 防止弹出多个错误提示
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
ElMessage.success('正在上传文件,请稍候...');
|
||||
|
||||
// 只有在验证通过后才增加计数器
|
||||
uploadNumber.value++;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
// 文件上传成功
|
||||
const handleFileSuccess: UploadProps['onSuccess'] = (url: any): void => {
|
||||
ElMessage.success('上传成功');
|
||||
// 删除自身
|
||||
const index = fileList.value.findIndex((item: any) => item.response === url);
|
||||
fileList.value.splice(index, 1);
|
||||
uploadList.value.push({ name: url, url });
|
||||
if (uploadList.value.length === uploadNumber.value) {
|
||||
fileList.value.push(...uploadList.value);
|
||||
/** 自定义上传请求 */
|
||||
async function customRequest(options: UploadRequestOptions) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
api = useUpload(props.directory).httpRequest;
|
||||
}
|
||||
try {
|
||||
// 上传文件
|
||||
const progressEvent: AxiosProgressEvent = (e) => {
|
||||
const percent = Math.trunc((e.loaded / e.total!) * 100);
|
||||
options.onProgress!({
|
||||
percent,
|
||||
total: e.total || 0,
|
||||
loaded: e.loaded || 0,
|
||||
lengthComputable: true,
|
||||
} as unknown as UploadProgressEvent);
|
||||
};
|
||||
const res = await api?.(options.file, progressEvent);
|
||||
|
||||
// 处理上传成功后的逻辑
|
||||
handleUploadSuccess(res, options.file as File);
|
||||
|
||||
options.onSuccess!(res);
|
||||
ElMessage.success($t('ui.upload.uploadSuccess'));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
options.onError!(error);
|
||||
handleUploadError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传成功
|
||||
* @param res 上传响应结果
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
if (index !== -1) {
|
||||
fileList.value?.splice(index!, 1);
|
||||
}
|
||||
|
||||
// 添加到临时上传列表
|
||||
const fileUrl = res?.url || res?.data || res;
|
||||
uploadList.value.push({
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
uid: file.name + Date.now(),
|
||||
});
|
||||
|
||||
// 检查是否所有文件都上传完成
|
||||
if (uploadList.value.length >= uploadNumber.value) {
|
||||
fileList.value?.push(...uploadList.value);
|
||||
uploadList.value = [];
|
||||
uploadNumber.value = 0;
|
||||
emitUpdateModelValue();
|
||||
}
|
||||
};
|
||||
// 文件数超出提示
|
||||
const handleExceed: UploadProps['onExceed'] = (): void => {
|
||||
ElMessage.error(`上传文件数量不能超过${props.limit}个!`);
|
||||
};
|
||||
// 上传错误提示
|
||||
const excelUploadError: UploadProps['onError'] = (): void => {
|
||||
ElMessage.error('导入数据失败,请您重新上传!');
|
||||
// 上传失败时减少计数器,避免后续上传被阻塞
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
};
|
||||
// 删除上传文件
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
const index = fileList.value.map((f) => f.name).indexOf(file.name);
|
||||
if (index !== -1) {
|
||||
fileList.value.splice(index, 1);
|
||||
emitUpdateModelValue();
|
||||
}
|
||||
};
|
||||
const handlePreview: UploadProps['onPreview'] = (_) => {
|
||||
// console.log(uploadFile);
|
||||
};
|
||||
|
||||
// 监听模型绑定值变动
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val: string | string[]) => {
|
||||
if (!val) {
|
||||
fileList.value = []; // fix:处理掉缓存,表单重置后上传组件的内容并没有重置
|
||||
return;
|
||||
}
|
||||
|
||||
fileList.value = []; // 保障数据为空
|
||||
// 情况1:字符串
|
||||
if (isString(val)) {
|
||||
fileList.value.push(
|
||||
...val.split(',').map((url) => ({
|
||||
// eslint-disable-next-line unicorn/prefer-string-slice
|
||||
name: url.substring(url.lastIndexOf('/') + 1),
|
||||
url,
|
||||
})),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 情况2:数组
|
||||
fileList.value.push(
|
||||
...(val as string[]).map((url) => ({
|
||||
// eslint-disable-next-line unicorn/prefer-string-slice
|
||||
name: url.substring(url.lastIndexOf('/') + 1),
|
||||
url,
|
||||
})),
|
||||
);
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
);
|
||||
// 发送文件链接列表更新
|
||||
const emitUpdateModelValue = () => {
|
||||
// 情况1:数组结果
|
||||
let result: string | string[] = fileList.value.map((file) => file.url!);
|
||||
// 情况2:逗号分隔的字符串
|
||||
if (props.limit === 1 || isString(props.modelValue)) {
|
||||
result = result.join(',');
|
||||
// 更新值
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
}
|
||||
emit('update:modelValue', result);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件列表的值
|
||||
* @returns 文件 URL 列表或字符串
|
||||
*/
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.SUCCESS)
|
||||
.map((item: any) => {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response;
|
||||
}
|
||||
return item?.url || item?.response?.url || item?.response;
|
||||
});
|
||||
|
||||
// 单个文件的情况,根据输入参数类型决定返回格式
|
||||
if (props.maxNumber === 1) {
|
||||
const singleValue = list.length > 0 ? list[0] : '';
|
||||
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
|
||||
if (
|
||||
isString(props.value) ||
|
||||
(isUsingModelValue.value && isString(props.modelValue))
|
||||
) {
|
||||
return singleValue;
|
||||
}
|
||||
return singleValue;
|
||||
}
|
||||
|
||||
// 多文件情况,根据输入参数类型决定返回格式
|
||||
if (isUsingModelValue.value) {
|
||||
return Array.isArray(props.modelValue) ? list : list.join(',');
|
||||
}
|
||||
|
||||
return Array.isArray(props.value) ? list : list.join(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!disabled" class="upload-file">
|
||||
<div>
|
||||
<ElUpload
|
||||
ref="uploadRef"
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:action="uploadUrl"
|
||||
:auto-upload="autoUpload"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:limit="maxNumber"
|
||||
:multiple="multiple"
|
||||
:drag="drag"
|
||||
:http-request="httpRequest0"
|
||||
:limit="props.limit"
|
||||
:multiple="props.limit > 1"
|
||||
:on-error="excelUploadError"
|
||||
:on-exceed="handleExceed"
|
||||
:on-preview="handlePreview"
|
||||
list-type="text"
|
||||
:on-remove="handleRemove"
|
||||
:on-success="handleFileSuccess"
|
||||
:show-file-list="true"
|
||||
class="upload-file-uploader"
|
||||
name="file"
|
||||
:on-preview="handlePreview"
|
||||
:on-exceed="handleExceed"
|
||||
>
|
||||
<ElButton type="primary">
|
||||
<IconifyIcon icon="ep:upload-filled" />
|
||||
选取文件
|
||||
</ElButton>
|
||||
<template v-if="isShowTip" #tip>
|
||||
<div style="font-size: 8px">
|
||||
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
|
||||
</div>
|
||||
<div style="font-size: 8px">
|
||||
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
|
||||
<template v-if="drag">
|
||||
<div class="upload-drag-area">
|
||||
<p class="upload-drag-icon">
|
||||
<IconifyIcon icon="lucide:cloud-upload" :size="48" />
|
||||
</p>
|
||||
<p class="upload-drag-text">点击或拖拽文件到此区域上传</p>
|
||||
<p class="upload-drag-hint">
|
||||
支持{{ accept.join('/') }}格式文件,不超过{{ maxSize }}MB
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #file="row">
|
||||
<div class="flex items-center">
|
||||
<span>{{ row.file.name }}</span>
|
||||
<div class="ml-10px">
|
||||
<ElLink
|
||||
:href="row.file.url"
|
||||
:underline="false"
|
||||
download
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
下载
|
||||
</ElLink>
|
||||
</div>
|
||||
<div class="ml-10px">
|
||||
<ElButton link type="danger" @click="handleRemove(row.file)">
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<ElButton v-if="fileList && fileList.length < maxNumber" type="primary">
|
||||
<IconifyIcon icon="lucide:cloud-upload" class="mr-1" />
|
||||
{{ $t('ui.upload.upload') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElUpload>
|
||||
</div>
|
||||
|
||||
<!-- 上传操作禁用时 -->
|
||||
<div v-if="disabled" class="upload-file">
|
||||
<div
|
||||
v-for="(file, index) in fileList"
|
||||
:key="index"
|
||||
class="file-list-item flex items-center"
|
||||
v-if="showDescription && !drag"
|
||||
class="mt-2 flex flex-wrap items-center text-sm"
|
||||
>
|
||||
<span>{{ file.name }}</span>
|
||||
<div class="ml-10px">
|
||||
<ElLink
|
||||
:href="file.url"
|
||||
:underline="false"
|
||||
download
|
||||
target="_blank"
|
||||
type="primary"
|
||||
>
|
||||
下载
|
||||
</ElLink>
|
||||
</div>
|
||||
请上传不超过
|
||||
<span class="mx-1 font-bold text-primary">{{ maxSize }}MB</span>
|
||||
的
|
||||
<span class="mx-1 font-bold text-primary">{{ accept.join('/') }}</span>
|
||||
格式文件
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.upload-file-uploader {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
:deep(.upload-file-list .el-upload-list__item) {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
line-height: 2;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
:deep(.el-upload-list__item-file-name) {
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
:deep(.upload-file-list .ele-upload-list__item-content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:deep(.ele-upload-list__item-content-action .el-link) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
<style scoped>
|
||||
.upload-drag-area {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
background-color: #fafafa;
|
||||
border-radius: 8px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.upload-drag-area:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-drag-icon {
|
||||
margin-bottom: 16px;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
.upload-drag-text {
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-drag-hint {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,13 +5,11 @@ import type {
|
||||
UploadRequestOptions,
|
||||
} from 'element-plus';
|
||||
|
||||
import type { AxiosResponse } from '@vben/request';
|
||||
|
||||
import type { UploadListType } from './typing';
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
import { computed, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
@@ -23,67 +21,35 @@ import {
|
||||
isString,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { ElMessage, ElUpload } from 'element-plus';
|
||||
import { ElDialog, ElMessage, ElUpload } from 'element-plus';
|
||||
|
||||
import { UploadResultStatus } from './typing';
|
||||
import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||
|
||||
// TODO @xingyu:这个要不要抽时间看看,upload 组件,和 antd 要不要进一步对齐下;(主要是代码风格。微信沟通~~~)
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 组件边框圆角
|
||||
borderradius?: string;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
// 上传框高度
|
||||
height?: number | string;
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
modelValue?: string | string[];
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
// 上传框宽度
|
||||
width?: number | string;
|
||||
}>(),
|
||||
{
|
||||
modelValue: () => [],
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => defaultImageAccepts,
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: true,
|
||||
width: '',
|
||||
height: '',
|
||||
borderradius: '8px',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits(['change', 'update:modelValue', 'delete']);
|
||||
const { accept, helpText, maxNumber, maxSize, width, height, borderradius } =
|
||||
toRefs(props);
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
value: () => [],
|
||||
modelValue: undefined,
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => defaultImageAccepts,
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: true,
|
||||
});
|
||||
const emit = defineEmits([
|
||||
'change',
|
||||
'update:value',
|
||||
'update:modelValue',
|
||||
'delete',
|
||||
]);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
acceptRef: accept,
|
||||
@@ -92,13 +58,28 @@ const { getStringAccept } = useUploadType({
|
||||
maxSizeRef: maxSize,
|
||||
});
|
||||
|
||||
/** 计算当前绑定的值,优先使用 modelValue */
|
||||
const currentValue = computed(() => {
|
||||
return props.modelValue === undefined ? props.value : props.modelValue;
|
||||
});
|
||||
|
||||
/** 判断是否使用 modelValue */
|
||||
const isUsingModelValue = computed(() => {
|
||||
return props.modelValue !== undefined;
|
||||
});
|
||||
const previewOpen = ref<boolean>(false); // 是否展示预览
|
||||
const previewImage = ref<string>(''); // 预览图片
|
||||
const previewTitle = ref<string>(''); // 预览标题
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
const isLtMsg = ref<boolean>(true); // 文件大小错误提示
|
||||
const isActMsg = ref<boolean>(true); // 文件类型错误提示
|
||||
const isFirstRender = ref<boolean>(true); // 是否第一次渲染
|
||||
const uploadNumber = ref<number>(0); // 上传文件计数器
|
||||
const uploadList = ref<any[]>([]); // 临时上传列表
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
currentValue,
|
||||
async (v) => {
|
||||
if (isInnerOperate.value) {
|
||||
isInnerOperate.value = false;
|
||||
@@ -121,11 +102,11 @@ watch(
|
||||
url: item,
|
||||
} as UploadFile;
|
||||
} else if (item && isObject(item)) {
|
||||
const file = item as Record<string, any>;
|
||||
const file = item as unknown as Record<string, any>;
|
||||
return {
|
||||
uid: file.uid || -i,
|
||||
name: file.name || '',
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
uid: file.uid ?? -i,
|
||||
name: file.name ?? '',
|
||||
status: file.status ?? UploadResultStatus.SUCCESS,
|
||||
url: file.url,
|
||||
} as UploadFile;
|
||||
}
|
||||
@@ -144,6 +125,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
/** 将文件转换为 Base64 格式 */
|
||||
function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -155,28 +137,52 @@ function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
/** 处理图片预览 */
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file.url) {
|
||||
const preview = await getBase64<string>(file.raw!);
|
||||
window.open(preview || '');
|
||||
return;
|
||||
file.url = await getBase64<string>(file.raw!);
|
||||
}
|
||||
window.open(file.url);
|
||||
};
|
||||
previewImage.value = file.url || '';
|
||||
previewOpen.value = true;
|
||||
previewTitle.value =
|
||||
file.name ||
|
||||
previewImage.value.slice(
|
||||
Math.max(0, previewImage.value.lastIndexOf('/') + 1),
|
||||
);
|
||||
}
|
||||
|
||||
const handleRemove = async (file: UploadFile) => {
|
||||
/** 处理文件删除 */
|
||||
async function handleRemove(file: UploadFile) {
|
||||
if (fileList.value) {
|
||||
const index = fileList.value.findIndex((item) => item.uid === file.uid);
|
||||
index !== -1 && fileList.value.splice(index, 1);
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 关闭预览弹窗 */
|
||||
function handleCancel() {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前校验
|
||||
* @param file 待上传的文件
|
||||
* @returns 是否允许上传
|
||||
*/
|
||||
async function beforeUpload(file: File) {
|
||||
// 检查文件数量限制
|
||||
if (fileList.value!.length >= props.maxNumber) {
|
||||
ElMessage.error($t('ui.upload.maxNumber', [props.maxNumber]));
|
||||
return false;
|
||||
}
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = isImage(file.name, accept);
|
||||
if (!isAct) {
|
||||
@@ -194,9 +200,13 @@ const beforeUpload = async (file: File) => {
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 只有在验证通过后才增加计数器
|
||||
uploadNumber.value++;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 自定义上传请求 */
|
||||
async function customRequest(options: UploadRequestOptions) {
|
||||
let { api } = props;
|
||||
if (!api || !isFunction(api)) {
|
||||
@@ -215,33 +225,66 @@ async function customRequest(options: UploadRequestOptions) {
|
||||
};
|
||||
const res = await api?.(options.file, progressEvent);
|
||||
|
||||
// TODO @xingyu:看看有没更好的实现代码。
|
||||
// 更新 fileList 中对应文件的 URL 为服务器返回的真实 URL
|
||||
const uploadedFile = fileList.value.find(
|
||||
(file) => file.uid === (options.file as any).uid,
|
||||
);
|
||||
if (uploadedFile) {
|
||||
const responseData = res?.data || res;
|
||||
uploadedFile.url =
|
||||
props.resultField && responseData[props.resultField]
|
||||
? responseData[props.resultField]
|
||||
: responseData.url || responseData;
|
||||
}
|
||||
// 处理上传成功后的逻辑
|
||||
handleUploadSuccess(res, options.file as File);
|
||||
|
||||
options.onSuccess!(res);
|
||||
ElMessage.success($t('ui.upload.uploadSuccess'));
|
||||
|
||||
// 更新文件
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
options.onError!(error);
|
||||
handleUploadError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上传成功
|
||||
* @param res 上传响应结果
|
||||
* @param file 上传的文件
|
||||
*/
|
||||
function handleUploadSuccess(res: any, file: File) {
|
||||
// 删除临时文件
|
||||
const index = fileList.value?.findIndex((item) => item.name === file.name);
|
||||
if (index !== -1) {
|
||||
fileList.value?.splice(index!, 1);
|
||||
}
|
||||
|
||||
// 添加到临时上传列表
|
||||
const fileUrl = res?.url || res?.data || res;
|
||||
uploadList.value.push({
|
||||
name: file.name,
|
||||
url: fileUrl,
|
||||
status: UploadResultStatus.SUCCESS,
|
||||
uid: file.name + Date.now(),
|
||||
});
|
||||
|
||||
// 检查是否所有文件都上传完成
|
||||
if (uploadList.value.length >= uploadNumber.value) {
|
||||
fileList.value?.push(...uploadList.value);
|
||||
uploadList.value = [];
|
||||
uploadNumber.value = 0;
|
||||
|
||||
// 更新值
|
||||
const value = getValue();
|
||||
isInnerOperate.value = true;
|
||||
emit('update:value', value);
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理上传错误 */
|
||||
function handleUploadError(error: any) {
|
||||
console.error('上传错误:', error);
|
||||
ElMessage.error($t('ui.upload.uploadError'));
|
||||
// 上传失败时减少计数器
|
||||
uploadNumber.value = Math.max(0, uploadNumber.value - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前文件列表的值
|
||||
* @returns 文件 URL 列表或字符串
|
||||
*/
|
||||
function getValue() {
|
||||
const list = (fileList.value || [])
|
||||
.filter((item) => item?.status === UploadResultStatus.SUCCESS)
|
||||
@@ -249,171 +292,77 @@ function getValue() {
|
||||
if (item?.response && props?.resultField) {
|
||||
return item?.response;
|
||||
}
|
||||
return item?.response?.url || item?.response;
|
||||
return item?.url || item?.response?.url || item?.response;
|
||||
});
|
||||
// add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型
|
||||
|
||||
// 单个文件的情况,根据输入参数类型决定返回格式
|
||||
if (props.maxNumber === 1) {
|
||||
return list.length > 0 ? list[0] : '';
|
||||
const singleValue = list.length > 0 ? list[0] : '';
|
||||
// 如果原始值是字符串或 modelValue 是字符串,返回字符串
|
||||
if (
|
||||
isString(props.value) ||
|
||||
(isUsingModelValue.value && isString(props.modelValue))
|
||||
) {
|
||||
return singleValue;
|
||||
}
|
||||
return singleValue;
|
||||
}
|
||||
return list;
|
||||
|
||||
// 多文件情况,根据输入参数类型决定返回格式
|
||||
if (isUsingModelValue.value) {
|
||||
return Array.isArray(props.modelValue) ? list : list.join(',');
|
||||
}
|
||||
|
||||
return Array.isArray(props.value) ? list : list.join(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="upload-box"
|
||||
:style="{
|
||||
width: width || '150px',
|
||||
height: height || '150px',
|
||||
borderRadius: borderradius,
|
||||
}"
|
||||
>
|
||||
<template
|
||||
v-if="
|
||||
fileList.length > 0 &&
|
||||
fileList[0] &&
|
||||
fileList[0].status === UploadResultStatus.SUCCESS
|
||||
"
|
||||
<div>
|
||||
<ElUpload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:limit="maxNumber"
|
||||
:multiple="multiple"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:class="{ 'upload-limit-reached': fileList.length >= maxNumber }"
|
||||
>
|
||||
<div class="upload-image-wrapper">
|
||||
<img :src="fileList[0].url" class="upload-image" />
|
||||
<div class="upload-handle">
|
||||
<div class="handle-icon" @click="handlePreview(fileList[0]!)">
|
||||
<IconifyIcon icon="lucide:circle-plus" />
|
||||
<span>详情</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!disabled"
|
||||
class="handle-icon"
|
||||
@click="handleRemove(fileList[0]!)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
<span>删除</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<IconifyIcon icon="lucide:cloud-upload" :size="24" />
|
||||
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElUpload
|
||||
v-bind="$attrs"
|
||||
v-model:file-list="fileList"
|
||||
:accept="getStringAccept"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="customRequest"
|
||||
:disabled="disabled"
|
||||
:list-type="listType"
|
||||
:limit="maxNumber"
|
||||
:multiple="multiple"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
class="upload"
|
||||
:style="{
|
||||
width: width || '150px',
|
||||
height: height || '150px',
|
||||
borderRadius: borderradius,
|
||||
}"
|
||||
>
|
||||
<div class="upload-content flex flex-col items-center justify-center">
|
||||
<IconifyIcon icon="lucide:plus" />
|
||||
</div>
|
||||
</ElUpload>
|
||||
</template>
|
||||
<!-- TODO @xingyu:相比 antd 来说,EL 有点丑;貌似是这里展示的位置不太对; -->
|
||||
<div v-if="showDescription" class="mt-2 text-xs text-gray-500">
|
||||
{{ getStringAccept }}
|
||||
</ElUpload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-sm"
|
||||
>
|
||||
请上传不超过
|
||||
<span class="mx-1 font-bold text-primary">{{ maxSize }}MB</span>
|
||||
的
|
||||
<span class="mx-1 font-bold text-primary">{{ accept.join('/') }}</span>
|
||||
格式文件
|
||||
</div>
|
||||
<ElDialog v-model="previewOpen" :title="previewTitle" @close="handleCancel">
|
||||
<img :src="previewImage" alt="" class="w-full" />
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.upload-box {
|
||||
position: relative;
|
||||
<style scoped>
|
||||
.el-upload--picture-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: #fafafa;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
background: transparent;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-image-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.upload-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.upload-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
background: rgb(0 0 0 / 50%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.handle-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 8px;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
|
||||
span {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-image-wrapper:hover .upload-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
/* 达到上传限制时隐藏上传按钮 */
|
||||
.upload-limit-reached :deep(.el-upload--picture-card) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { FileUploadProps } from './typing';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { ElCol, ElInput, ElRow } from 'element-plus';
|
||||
import { ElInput } from 'element-plus';
|
||||
|
||||
import FileUpload from './file-upload.vue';
|
||||
|
||||
@@ -30,6 +30,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
/** 处理文件内容返回 */
|
||||
function handleReturnText(text: string) {
|
||||
modelValue.value = text;
|
||||
emits('change', modelValue.value);
|
||||
@@ -37,38 +38,42 @@ function handleReturnText(text: string) {
|
||||
emits('update:modelValue', modelValue.value);
|
||||
}
|
||||
|
||||
/** 计算输入框属性 */
|
||||
const inputProps = computed(() => {
|
||||
return {
|
||||
...props.inputProps,
|
||||
value: modelValue.value,
|
||||
modelValue: modelValue.value,
|
||||
};
|
||||
});
|
||||
|
||||
/** 计算文本域属性 */
|
||||
const textareaProps = computed(() => {
|
||||
return {
|
||||
...props.textareaProps,
|
||||
value: modelValue.value,
|
||||
modelValue: modelValue.value,
|
||||
};
|
||||
});
|
||||
|
||||
/** 计算文件上传属性 */
|
||||
const fileUploadProps = computed(() => {
|
||||
return {
|
||||
...props.fileUploadProps,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElRow>
|
||||
<ElCol :span="18">
|
||||
<ElInput v-if="inputType === 'input'" v-bind="inputProps" />
|
||||
<ElInput v-else :row="4" type="textarea" v-bind="textareaProps" />
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<FileUpload
|
||||
class="ml-4"
|
||||
v-bind="fileUploadProps"
|
||||
@return-text="handleReturnText"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<div class="w-full">
|
||||
<ElInput v-if="inputType === 'input'" readonly v-bind="inputProps">
|
||||
<template #suffix>
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</template>
|
||||
</ElInput>
|
||||
<div v-else class="relative w-full">
|
||||
<ElInput readonly :rows="4" type="textarea" v-bind="textareaProps" />
|
||||
<div class="absolute bottom-0 right-2">
|
||||
<FileUpload v-bind="fileUploadProps" @return-text="handleReturnText" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,76 +2,31 @@ import type { AxiosResponse } from '@vben/request';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
||||
|
||||
export type UploadStatus =
|
||||
| 'error'
|
||||
| 'fail'
|
||||
| 'removed'
|
||||
| 'success'
|
||||
| 'uploading';
|
||||
|
||||
export enum UploadResultStatus {
|
||||
DONE = 'done',
|
||||
ERROR = 'error',
|
||||
REMOVED = 'removed',
|
||||
SUCCESS = 'success',
|
||||
UPLOADING = 'uploading',
|
||||
}
|
||||
|
||||
export interface CustomUploadFile {
|
||||
uid: number;
|
||||
name: string;
|
||||
status: UploadStatus;
|
||||
url?: string;
|
||||
response?: any;
|
||||
percentage?: number;
|
||||
size?: number;
|
||||
raw?: File;
|
||||
}
|
||||
|
||||
export function convertToUploadStatus(
|
||||
status: UploadResultStatus,
|
||||
): UploadStatus {
|
||||
switch (status) {
|
||||
case UploadResultStatus.ERROR: {
|
||||
return 'fail';
|
||||
}
|
||||
case UploadResultStatus.REMOVED: {
|
||||
return 'removed';
|
||||
}
|
||||
case UploadResultStatus.SUCCESS: {
|
||||
return 'success';
|
||||
}
|
||||
case UploadResultStatus.UPLOADING: {
|
||||
return 'uploading';
|
||||
}
|
||||
default: {
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
}
|
||||
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
||||
|
||||
export interface FileUploadProps {
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
accept?: string[]; // 根据后缀,或者其他
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
) => Promise<AxiosResponse>;
|
||||
directory?: string; // 上传的目录
|
||||
disabled?: boolean;
|
||||
drag?: boolean; // 是否支持拖拽上传
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
maxNumber?: number; // 最大数量的文件,Infinity 不限制
|
||||
modelValue?: string | string[]; // v-model 支持
|
||||
maxSize?: number; // 文件最大多少 MB
|
||||
multiple?: boolean; // 是否支持多选
|
||||
resultField?: string; // support xxx.xxx.xx
|
||||
showDescription?: boolean; // 是否显示下面的描述
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,14 @@ enum UPLOAD_TYPE {
|
||||
SERVER = 'server',
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传类型钩子函数
|
||||
* @param acceptRef 接受的文件类型
|
||||
* @param helpTextRef 帮助文本
|
||||
* @param maxNumberRef 最大文件数量
|
||||
* @param maxSizeRef 最大文件大小
|
||||
* @returns 文件类型限制和帮助文本的计算属性
|
||||
*/
|
||||
export function useUploadType({
|
||||
acceptRef,
|
||||
helpTextRef,
|
||||
@@ -78,7 +86,11 @@ export function useUploadType({
|
||||
return { getAccept, getStringAccept, getHelpText };
|
||||
}
|
||||
|
||||
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||
/**
|
||||
* 上传钩子函数
|
||||
* @param directory 上传目录
|
||||
* @returns 上传 URL 和自定义上传方法
|
||||
*/
|
||||
export function useUpload(directory?: string) {
|
||||
// 后端上传地址
|
||||
const uploadUrl = getUploadUrl();
|
||||
|
||||
@@ -5,7 +5,11 @@ import type { BpmProcessInstanceApi } from '#/api/bpm/processInstance';
|
||||
import { h } from 'vue';
|
||||
|
||||
import { DocAlert, Page, prompt } from '@vben/common-ui';
|
||||
import { BpmProcessInstanceStatus, DICT_TYPE } from '@vben/constants';
|
||||
import {
|
||||
BpmModelFormType,
|
||||
BpmProcessInstanceStatus,
|
||||
DICT_TYPE,
|
||||
} from '@vben/constants';
|
||||
|
||||
import { ElButton, ElInput, ElMessage } from 'element-plus';
|
||||
|
||||
@@ -37,23 +41,34 @@ function handleDetail(row: BpmProcessInstanceApi.ProcessInstance) {
|
||||
}
|
||||
|
||||
/** 重新发起流程 */
|
||||
async function handleCreate(row: BpmProcessInstanceApi.ProcessInstance) {
|
||||
// 如果是【业务表单】,不支持重新发起
|
||||
async function handleCreate(row?: BpmProcessInstanceApi.ProcessInstance) {
|
||||
if (row?.id) {
|
||||
const processDefinitionDetail = await getProcessDefinition(
|
||||
row.processDefinitionId,
|
||||
);
|
||||
if (processDefinitionDetail.formType === 20) {
|
||||
ElMessage.error(
|
||||
'重新发起流程失败,原因:该流程使用业务表单,不支持重新发起',
|
||||
);
|
||||
if (processDefinitionDetail?.formType === BpmModelFormType.CUSTOM) {
|
||||
if (!processDefinitionDetail.formCustomCreatePath) {
|
||||
ElMessage.error('未配置业务表单的提交路由,无法重新发起');
|
||||
return;
|
||||
}
|
||||
await router.push({
|
||||
path: processDefinitionDetail.formCustomCreatePath,
|
||||
query: {
|
||||
id: row.businessKey,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else if (processDefinitionDetail?.formType === BpmModelFormType.NORMAL) {
|
||||
await router.push({
|
||||
name: 'BpmProcessInstanceCreate',
|
||||
query: { processInstanceId: row.id },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 跳转发起流程界面
|
||||
await router.push({
|
||||
name: 'BpmProcessInstanceCreate',
|
||||
query: { processInstanceId: row?.id },
|
||||
query: row?.id ? { processInstanceId: row.id } : {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,34 @@
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
// 客户转化率分析
|
||||
case 'conversionStat': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '客户转化率',
|
||||
@@ -40,12 +57,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户转化率分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '转化率(%)',
|
||||
@@ -59,14 +71,13 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'customerSummary': {
|
||||
return {
|
||||
grid: {
|
||||
bottom: '5%',
|
||||
containLabel: true,
|
||||
grid: getGrid({
|
||||
bottom: '8%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
top: '5 %',
|
||||
},
|
||||
legend: {},
|
||||
top: 80,
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数',
|
||||
@@ -92,12 +103,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -134,13 +140,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -166,12 +167,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -208,13 +204,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
};
|
||||
});
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -240,12 +231,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -277,13 +263,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
const customerDealCycleByDate = res.customerDealCycleByDate;
|
||||
const customerDealCycleByUser = res.customerDealCycleByUser;
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '成交周期(天)',
|
||||
@@ -309,12 +290,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '成交周期分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -342,15 +318,13 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
},
|
||||
};
|
||||
}
|
||||
// 客户跟进次数分析
|
||||
case 'followUpSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
grid: getGrid({
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '跟进客户数',
|
||||
@@ -376,12 +350,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户跟进次数分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -412,20 +381,21 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
},
|
||||
};
|
||||
}
|
||||
// 客户跟进方式分析
|
||||
case 'followUpType': {
|
||||
return {
|
||||
title: {
|
||||
text: '客户跟进方式分析',
|
||||
left: 'center',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
legend: getLegend({
|
||||
left: 'left',
|
||||
},
|
||||
tooltip: {
|
||||
}),
|
||||
tooltip: getTooltip({
|
||||
trigger: 'item',
|
||||
axisPointer: undefined,
|
||||
formatter: '{b} : {c}% ',
|
||||
},
|
||||
}),
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '客户跟进方式分析' }, // 保存为图片
|
||||
@@ -458,13 +428,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'poolSummary': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 40, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '进入公海客户数',
|
||||
@@ -490,12 +455,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '公海客户分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { erpCalculatePercentage } from '@vben/utils';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(
|
||||
activeTabName: any,
|
||||
active: boolean,
|
||||
@@ -9,26 +30,19 @@ export function getChartOptions(
|
||||
case 'businessInversionRateSummary': {
|
||||
return {
|
||||
color: ['#6ca2ff', '#6ac9d7', '#ff7474'],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
// 坐标轴指示器,坐标轴触发有效
|
||||
type: 'shadow', // 默认为直线,可选为:'line' | 'shadow'
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
tooltip: getTooltip(),
|
||||
legend: getLegend({
|
||||
data: ['赢单转化率', '商机总数', '赢单商机数'],
|
||||
bottom: '0px',
|
||||
itemWidth: 14,
|
||||
},
|
||||
grid: {
|
||||
}),
|
||||
grid: getGrid({
|
||||
top: '40px',
|
||||
left: '40px',
|
||||
right: '40px',
|
||||
bottom: '40px',
|
||||
containLabel: true,
|
||||
borderColor: '#fff',
|
||||
},
|
||||
}),
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
@@ -117,13 +131,11 @@ export function getChartOptions(
|
||||
}
|
||||
case 'businessSummary': {
|
||||
return {
|
||||
grid: {
|
||||
grid: getGrid({
|
||||
left: 30,
|
||||
right: 30, // 让 X 轴右侧显示完整
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
}),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增商机数量',
|
||||
@@ -149,12 +161,7 @@ export function getChartOptions(
|
||||
saveAsImage: { show: true, name: '新增商机分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -211,10 +218,11 @@ export function getChartOptions(
|
||||
title: {
|
||||
text: '销售漏斗',
|
||||
},
|
||||
tooltip: {
|
||||
tooltip: getTooltip({
|
||||
trigger: 'item',
|
||||
axisPointer: undefined,
|
||||
formatter: '{a} <br/>{b}',
|
||||
},
|
||||
}),
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataView: { readOnly: false },
|
||||
@@ -222,9 +230,9 @@ export function getChartOptions(
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
legend: getLegend({
|
||||
data: ['客户', '商机', '赢单'],
|
||||
},
|
||||
}),
|
||||
series: [
|
||||
{
|
||||
name: '销售漏斗',
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
...extra,
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'ContractCountPerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月合同数量(个)',
|
||||
@@ -65,12 +81,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '客户总量分析' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
@@ -131,13 +142,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'ContractPricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月合同金额(元)',
|
||||
@@ -260,13 +266,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'ReceivablePricePerformance': {
|
||||
return {
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '当月回款金额(元)',
|
||||
|
||||
@@ -13,6 +13,71 @@ function areaReplace(areaName: string) {
|
||||
.replace('省', '');
|
||||
}
|
||||
|
||||
const getPieTooltip = (extra: Record<string, any> = {}) => ({
|
||||
trigger: 'item',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getPieLegend = (extra: Record<string, any> = {}) => ({
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getPieSeries = (name: string, data: any[]) => ({
|
||||
name,
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data,
|
||||
});
|
||||
|
||||
const getPiePanel = ({
|
||||
data,
|
||||
legendExtra,
|
||||
seriesName,
|
||||
title,
|
||||
tooltipExtra,
|
||||
}: {
|
||||
data: any[];
|
||||
legendExtra?: Record<string, any>;
|
||||
seriesName: string;
|
||||
title: string;
|
||||
tooltipExtra?: Record<string, any>;
|
||||
}) => ({
|
||||
title: {
|
||||
text: title,
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: getPieTooltip(tooltipExtra),
|
||||
legend: getPieLegend(legendExtra),
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: title },
|
||||
},
|
||||
},
|
||||
series: [getPieSeries(seriesName, data)],
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'area': {
|
||||
@@ -111,326 +176,62 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
}
|
||||
case 'industry': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(
|
||||
DICT_TYPE.CRM_CUSTOMER_INDUSTRY,
|
||||
r.industryId,
|
||||
),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_INDUSTRY, r.industryId),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'level': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_LEVEL, r.level),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
case 'source': {
|
||||
return {
|
||||
left: {
|
||||
title: {
|
||||
text: '全部客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '全部客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '全部客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
right: {
|
||||
title: {
|
||||
text: '成交客户',
|
||||
left: 'center',
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
saveAsImage: { show: true, name: '成交客户' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '成交客户',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: res.map((r: any) => {
|
||||
return {
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
left: getPiePanel({
|
||||
title: '全部客户',
|
||||
seriesName: '全部客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.customerCount,
|
||||
})),
|
||||
}),
|
||||
right: getPiePanel({
|
||||
title: '成交客户',
|
||||
seriesName: '成交客户',
|
||||
data: res.map((r: any) => ({
|
||||
name: getDictLabel(DICT_TYPE.CRM_CUSTOMER_SOURCE, r.source),
|
||||
value: r.dealCount,
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
const getLegend = (extra: Record<string, any> = {}) => ({
|
||||
top: 10,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getGrid = (extra: Record<string, any> = {}) => ({
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const getTooltip = () => ({
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
});
|
||||
|
||||
export function getChartOptions(activeTabName: any, res: any): any {
|
||||
switch (activeTabName) {
|
||||
case 'contactCountRank': {
|
||||
@@ -8,15 +28,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增联系人数排行',
|
||||
@@ -34,12 +47,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '新增联系人数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增联系人数(个)',
|
||||
@@ -56,15 +64,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '签约合同排行',
|
||||
@@ -82,12 +83,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '签约合同排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '签约合同数(个)',
|
||||
@@ -104,15 +100,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '合同金额排行',
|
||||
@@ -130,12 +119,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '合同金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '合同金额(元)',
|
||||
@@ -152,15 +136,8 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
dimensions: ['nickname', 'count'],
|
||||
source: cloneDeep(res).toReversed(),
|
||||
},
|
||||
grid: {
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
top: 50,
|
||||
},
|
||||
grid: getGrid(),
|
||||
legend: getLegend(),
|
||||
series: [
|
||||
{
|
||||
name: '新增客户数排行',
|
||||
@@ -178,12 +155,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '新增客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '新增客户数(个)',
|
||||
@@ -226,12 +198,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '跟进次数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进次数(次)',
|
||||
@@ -274,12 +241,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '跟进客户数排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '跟进客户数(个)',
|
||||
@@ -322,12 +284,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '产品销量排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '产品销量',
|
||||
@@ -370,12 +327,7 @@ export function getChartOptions(activeTabName: any, res: any): any {
|
||||
saveAsImage: { show: true, name: '回款金额排行' }, // 保存为图片
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow',
|
||||
},
|
||||
},
|
||||
tooltip: getTooltip(),
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '回款金额(元)',
|
||||
|
||||
@@ -43,7 +43,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data = (await formApi.getValues()) as PayChannelApi.Channel;
|
||||
data.config = JSON.stringify(data.config);
|
||||
data.config = JSON.stringify(data.config || {});
|
||||
try {
|
||||
await (data.id ? updateChannel(data) : createChannel(data));
|
||||
// 关闭并提示
|
||||
|
||||
64
pnpm-lock.yaml
generated
64
pnpm-lock.yaml
generated
@@ -593,7 +593,7 @@ catalogs:
|
||||
version: 4.1.0
|
||||
vxe-pc-ui:
|
||||
specifier: ^4.10.22
|
||||
version: 4.11.20
|
||||
version: 4.11.21
|
||||
vxe-table:
|
||||
specifier: ^4.17.14
|
||||
version: 4.17.35
|
||||
@@ -2010,7 +2010,7 @@ importers:
|
||||
version: 3.5.26(typescript@5.9.3)
|
||||
vxe-pc-ui:
|
||||
specifier: 'catalog:'
|
||||
version: 4.11.20(vue@3.5.26(typescript@5.9.3))
|
||||
version: 4.11.21(vue@3.5.26(typescript@5.9.3))
|
||||
vxe-table:
|
||||
specifier: 'catalog:'
|
||||
version: 4.17.35(vue@3.5.26(typescript@5.9.3))
|
||||
@@ -2898,11 +2898,11 @@ packages:
|
||||
'@bpmn-io/properties-panel@3.35.1':
|
||||
resolution: {integrity: sha512-bxZkrFzlHxTPC1CzMZ6rwpjdkUKKT/7A6rgNWNtmExyyipZjvUq0GMg395xPdVO9Yir+7O7A/XFpQo6PA1dyFA==}
|
||||
|
||||
'@cacheable/memory@2.0.6':
|
||||
resolution: {integrity: sha512-7e8SScMocHxcAb8YhtkbMhGG+EKLRIficb1F5sjvhSYsWTZGxvg4KIDp8kgxnV2PUJ3ddPe6J9QESjKvBWRDkg==}
|
||||
'@cacheable/memory@2.0.7':
|
||||
resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==}
|
||||
|
||||
'@cacheable/utils@2.3.2':
|
||||
resolution: {integrity: sha512-8kGE2P+HjfY8FglaOiW+y8qxcaQAfAhVML+i66XJR3YX5FtyDqn6Txctr3K2FrbxLKixRRYYBWMbuGciOhYNDg==}
|
||||
'@cacheable/utils@2.3.3':
|
||||
resolution: {integrity: sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==}
|
||||
|
||||
'@camunda/feel-builtins@0.2.0':
|
||||
resolution: {integrity: sha512-Jusm8x3Onqze9E5Y0lGGdPj66bnFKLYNwDz+uG4otsEXgSL0FpF+koGHK48LkF9Jqo67KaP1y3zr2y/HIWRePw==}
|
||||
@@ -5494,8 +5494,8 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.24
|
||||
|
||||
'@vxe-ui/core@4.2.17':
|
||||
resolution: {integrity: sha512-0jsaTPeG3Zbu0Czytl7eI0jU+S/NGhBGmhuEdwlMaqHLH69x8YrXH4UZBfRtCzgleTPS76j3dZVPtVsCSgMn0g==}
|
||||
'@vxe-ui/core@4.2.18':
|
||||
resolution: {integrity: sha512-StRs0zbCdshNwONYmZnFaJCencIo4rbVHbOcqLAddu6PTxPAsfUE9R5upMGj8EJMIpJggPcEUFvbjSp+8VJ8Uw==}
|
||||
peerDependencies:
|
||||
vue: ^3.5.24
|
||||
|
||||
@@ -6400,8 +6400,8 @@ packages:
|
||||
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
cssdb@8.5.2:
|
||||
resolution: {integrity: sha512-Pmoj9RmD8RIoIzA2EQWO4D4RMeDts0tgAH0VXdlNdxjuBGI3a9wMOIcUwaPNmD4r2qtIa06gqkIf7sECl+cBCg==}
|
||||
cssdb@8.6.0:
|
||||
resolution: {integrity: sha512-7ZrRi/Z3cRL1d5I8RuXEWAkRFP3J4GeQRiyVknI4KC70RAU8hT4LysUZDe0y+fYNOktCbxE8sOPUOhyR12UqGQ==}
|
||||
|
||||
cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
@@ -8306,8 +8306,8 @@ packages:
|
||||
kolorist@1.8.0:
|
||||
resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==}
|
||||
|
||||
ky@1.14.1:
|
||||
resolution: {integrity: sha512-hYje4L9JCmpEQBtudo+v52X5X8tgWXUYyPcxKSuxQNboqufecl9VMWjGiucAFH060AwPXHZuH+WB2rrqfkmafw==}
|
||||
ky@1.14.2:
|
||||
resolution: {integrity: sha512-q3RBbsO5A5zrPhB6CaCS8ZUv+NWCXv6JJT4Em0i264G9W0fdPB8YRfnnEi7Dm7X7omAkBIPojzYJ2D1oHTHqug==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
latest-version@9.0.0:
|
||||
@@ -11456,8 +11456,8 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.5.24
|
||||
|
||||
vxe-pc-ui@4.11.20:
|
||||
resolution: {integrity: sha512-6qTbCJOo7hQ7IEElGCWh/E3UCJsb3IF4OIHy+hsy9P68i4GtZgz+qMrRxgyGH608qIbOR8z5i7wetuiENeqyWQ==}
|
||||
vxe-pc-ui@4.11.21:
|
||||
resolution: {integrity: sha512-nt++HK+JUi0y43DxU86s5AQiT0847Zi+zA45wlPeA/m8PYZeKhVQ+WcQXDKPhkMfkv3zzm+hj03MvYIdsqFW/Q==}
|
||||
|
||||
vxe-table@4.17.35:
|
||||
resolution: {integrity: sha512-64biGEeTW9f+2HjiAOGSQqDEYqZpbUOauQvZozKkFk5OdtC+F1bagOHmU7EXl+vueFAof92pUNalDh1uIZt6Wg==}
|
||||
@@ -11630,8 +11630,8 @@ packages:
|
||||
resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
xe-utils@3.8.1:
|
||||
resolution: {integrity: sha512-pI9QNQ1dhUqbrnMDcVo+nOzH4D5/Xrv0+0P8vGJPXezs6bQvdk7HX/+i6pa0jNLZtfzx/YshwMi/zSN9s8D43g==}
|
||||
xe-utils@3.8.2:
|
||||
resolution: {integrity: sha512-u7GMyaDeVHODMd6tUdSqfLl6sYX5qmRjuTbb+QMhYHmp2qlaqsUTD5FwEiM+p/82j4kmeen6TLvxfs7uXdQ5ow==}
|
||||
|
||||
xml-name-validator@4.0.0:
|
||||
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
|
||||
@@ -12689,14 +12689,14 @@ snapshots:
|
||||
min-dash: 4.2.3
|
||||
min-dom: 4.2.1
|
||||
|
||||
'@cacheable/memory@2.0.6':
|
||||
'@cacheable/memory@2.0.7':
|
||||
dependencies:
|
||||
'@cacheable/utils': 2.3.2
|
||||
'@cacheable/utils': 2.3.3
|
||||
'@keyv/bigmap': 1.3.0(keyv@5.5.5)
|
||||
hookified: 1.14.0
|
||||
keyv: 5.5.5
|
||||
|
||||
'@cacheable/utils@2.3.2':
|
||||
'@cacheable/utils@2.3.3':
|
||||
dependencies:
|
||||
hashery: 1.3.0
|
||||
keyv: 5.5.5
|
||||
@@ -15655,11 +15655,11 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
'@vxe-ui/core@4.2.17(vue@3.5.26(typescript@5.9.3))':
|
||||
'@vxe-ui/core@4.2.18(vue@3.5.26(typescript@5.9.3))':
|
||||
dependencies:
|
||||
dom-zindex: 1.0.6
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
xe-utils: 3.8.1
|
||||
xe-utils: 3.8.2
|
||||
|
||||
'@xmldom/xmldom@0.8.11': {}
|
||||
|
||||
@@ -16125,8 +16125,8 @@ snapshots:
|
||||
|
||||
cacheable@2.3.1:
|
||||
dependencies:
|
||||
'@cacheable/memory': 2.0.6
|
||||
'@cacheable/utils': 2.3.2
|
||||
'@cacheable/memory': 2.0.7
|
||||
'@cacheable/utils': 2.3.3
|
||||
hookified: 1.14.0
|
||||
keyv: 5.5.5
|
||||
qified: 0.5.3
|
||||
@@ -16691,7 +16691,7 @@ snapshots:
|
||||
|
||||
css-what@6.2.2: {}
|
||||
|
||||
cssdb@8.5.2: {}
|
||||
cssdb@8.6.0: {}
|
||||
|
||||
cssesc@3.0.0: {}
|
||||
|
||||
@@ -18745,7 +18745,7 @@ snapshots:
|
||||
|
||||
kolorist@1.8.0: {}
|
||||
|
||||
ky@1.14.1: {}
|
||||
ky@1.14.2: {}
|
||||
|
||||
latest-version@9.0.0:
|
||||
dependencies:
|
||||
@@ -19627,7 +19627,7 @@ snapshots:
|
||||
|
||||
package-json@10.0.1:
|
||||
dependencies:
|
||||
ky: 1.14.1
|
||||
ky: 1.14.2
|
||||
registry-auth-token: 5.1.0
|
||||
registry-url: 6.0.1
|
||||
semver: 7.7.3
|
||||
@@ -20159,7 +20159,7 @@ snapshots:
|
||||
css-blank-pseudo: 7.0.1(postcss@8.5.6)
|
||||
css-has-pseudo: 7.0.3(postcss@8.5.6)
|
||||
css-prefers-color-scheme: 10.0.0(postcss@8.5.6)
|
||||
cssdb: 8.5.2
|
||||
cssdb: 8.6.0
|
||||
postcss: 8.5.6
|
||||
postcss-attribute-case-insensitive: 7.0.1(postcss@8.5.6)
|
||||
postcss-clamp: 4.1.0(postcss@8.5.6)
|
||||
@@ -21960,7 +21960,7 @@ snapshots:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.54.0)
|
||||
es-module-lexer: 1.7.0
|
||||
rollup: 4.54.0
|
||||
xe-utils: 3.8.1
|
||||
xe-utils: 3.8.2
|
||||
|
||||
vite-plugin-pwa@1.2.0(vite@5.4.21(@types/node@25.0.3)(less@4.5.1)(sass@1.97.1)(terser@5.44.1))(workbox-build@7.4.0)(workbox-window@7.4.0):
|
||||
dependencies:
|
||||
@@ -22304,15 +22304,15 @@ snapshots:
|
||||
vooks: 0.2.12(vue@3.5.26(typescript@5.9.3))
|
||||
vue: 3.5.26(typescript@5.9.3)
|
||||
|
||||
vxe-pc-ui@4.11.20(vue@3.5.26(typescript@5.9.3)):
|
||||
vxe-pc-ui@4.11.21(vue@3.5.26(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vxe-ui/core': 4.2.17(vue@3.5.26(typescript@5.9.3))
|
||||
'@vxe-ui/core': 4.2.18(vue@3.5.26(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
vxe-table@4.17.35(vue@3.5.26(typescript@5.9.3)):
|
||||
dependencies:
|
||||
vxe-pc-ui: 4.11.20(vue@3.5.26(typescript@5.9.3))
|
||||
vxe-pc-ui: 4.11.21(vue@3.5.26(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
@@ -22571,7 +22571,7 @@ snapshots:
|
||||
|
||||
xdg-basedir@5.1.0: {}
|
||||
|
||||
xe-utils@3.8.1: {}
|
||||
xe-utils@3.8.2: {}
|
||||
|
||||
xml-name-validator@4.0.0: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user