refactor: 升级框架
This commit is contained in:
@@ -29,14 +29,14 @@ withDefaults(
|
||||
<Card :body-style="bodyStyle" :title="title" class="mb-4">
|
||||
<template v-if="title" #title>
|
||||
<div class="flex items-center">
|
||||
<span class="text-4 font-[700]">{{ title }}</span>
|
||||
<span class="text-base font-bold">{{ title }}</span>
|
||||
<Tooltip placement="right">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">{{ message }}</div>
|
||||
</template>
|
||||
<ShieldQuestion :size="14" class="ml-5px" />
|
||||
<ShieldQuestion :size="14" class="ml-1" />
|
||||
</Tooltip>
|
||||
<div class="pl-20px flex flex-grow">
|
||||
<div class="flex flex-grow pl-5">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { CropperAvatarProps } from './typing';
|
||||
import { computed, ref, unref, watch, watchEffect } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
@@ -27,13 +28,10 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
|
||||
const emit = defineEmits(['update:value', 'change']);
|
||||
|
||||
const sourceValue = ref(props.value || '');
|
||||
const prefixCls = 'cropper-avatar';
|
||||
const [CropperModal, modalApi] = useVbenModal({
|
||||
connectedComponent: cropperModal,
|
||||
});
|
||||
|
||||
const getClass = computed(() => [prefixCls]);
|
||||
|
||||
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
|
||||
|
||||
const getIconWidth = computed(
|
||||
@@ -73,28 +71,42 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="getClass" :style="getStyle">
|
||||
<!-- 头像容器 -->
|
||||
<div class="inline-block text-center" :style="getStyle">
|
||||
<!-- 图片包装器 -->
|
||||
<div
|
||||
:class="`${prefixCls}-image-wrapper`"
|
||||
class="bg-card group relative cursor-pointer overflow-hidden rounded-full border border-gray-200"
|
||||
:style="getImageWrapperStyle"
|
||||
@click="openModal"
|
||||
>
|
||||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
|
||||
<span
|
||||
<!-- 遮罩层 -->
|
||||
<div
|
||||
class="duration-400 absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
:style="getImageWrapperStyle"
|
||||
>
|
||||
<IconifyIcon
|
||||
icon="lucide:cloud-upload"
|
||||
class="m-auto text-gray-400"
|
||||
:style="{
|
||||
...getImageWrapperStyle,
|
||||
width: `${getIconWidth}`,
|
||||
height: `${getIconWidth}`,
|
||||
lineHeight: `${getIconWidth}`,
|
||||
width: getIconWidth,
|
||||
height: getIconWidth,
|
||||
lineHeight: getIconWidth,
|
||||
}"
|
||||
class="icon-[ant-design--cloud-upload-outlined] text-[#d6d6d6]"
|
||||
></span>
|
||||
/>
|
||||
</div>
|
||||
<img v-if="sourceValue" :src="sourceValue" alt="avatar" />
|
||||
<!-- 头像图片 -->
|
||||
<img
|
||||
v-if="sourceValue"
|
||||
:src="sourceValue"
|
||||
alt="avatar"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<!-- 上传按钮 -->
|
||||
<Button
|
||||
v-if="showBtn"
|
||||
:class="`${prefixCls}-upload-btn`"
|
||||
class="mx-auto mt-2"
|
||||
@click="openModal"
|
||||
v-bind="btnProps"
|
||||
>
|
||||
@@ -109,49 +121,3 @@ defineExpose({
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cropper-avatar {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
|
||||
&-image-wrapper {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-image-mask {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
cursor: pointer;
|
||||
background: rgb(0 0 0 / 40%);
|
||||
border: inherit;
|
||||
border-radius: inherit;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
|
||||
::v-deep(svg) {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&-image-mask:hover {
|
||||
opacity: 40;
|
||||
}
|
||||
|
||||
&-upload-btn {
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { CropendResult, CropperModalProps, CropperType } from './typing';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { dataURLtoBlob, isFunction } from '@vben/utils';
|
||||
|
||||
@@ -36,13 +37,20 @@ const cropper = ref<CropperType>();
|
||||
let scaleX = 1;
|
||||
let scaleY = 1;
|
||||
|
||||
const prefixCls = 'cropper-am';
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
onConfirm: handleOk,
|
||||
onOpenChange(isOpen) {
|
||||
if (isOpen) {
|
||||
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading(通过 handleReady)
|
||||
modalLoading(true);
|
||||
const img = new Image();
|
||||
img.src = src.value;
|
||||
img.addEventListener('load', () => {
|
||||
modalLoading(false);
|
||||
});
|
||||
img.addEventListener('error', () => {
|
||||
modalLoading(false);
|
||||
});
|
||||
} else {
|
||||
// 关闭时,清空右侧预览
|
||||
previewSource.value = '';
|
||||
@@ -118,11 +126,15 @@ async function handleOk() {
|
||||
:confirm-text="$t('ui.cropper.okText')"
|
||||
:fullscreen-button="false"
|
||||
:title="$t('ui.cropper.modalTitle')"
|
||||
class="w-[800px]"
|
||||
class="w-2/3"
|
||||
>
|
||||
<div :class="prefixCls">
|
||||
<div :class="`${prefixCls}-left`" class="w-full">
|
||||
<div :class="`${prefixCls}-cropper`">
|
||||
<div class="flex h-96">
|
||||
<!-- 左侧区域 -->
|
||||
<div class="h-full w-3/5">
|
||||
<!-- 裁剪器容器 -->
|
||||
<div
|
||||
class="relative h-[300px] bg-gradient-to-b from-neutral-50 to-neutral-200"
|
||||
>
|
||||
<CropperImage
|
||||
v-if="src"
|
||||
:circled="circled"
|
||||
@@ -133,7 +145,8 @@ async function handleOk() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="`${prefixCls}-toolbar`">
|
||||
<!-- 工具栏 -->
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<Upload
|
||||
:before-upload="handleBeforeUpload"
|
||||
:file-list="[]"
|
||||
@@ -143,7 +156,7 @@ async function handleOk() {
|
||||
<Button size="small" type="primary">
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--upload-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:upload" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -159,7 +172,7 @@ async function handleOk() {
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--reload-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:rotate-ccw" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -176,9 +189,7 @@ async function handleOk() {
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
class="icon-[ant-design--rotate-left-outlined]"
|
||||
></span>
|
||||
<IconifyIcon icon="ant-design:rotate-left-outlined" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -189,16 +200,13 @@ async function handleOk() {
|
||||
>
|
||||
<Button
|
||||
:disabled="!src"
|
||||
pre-icon="ant-design:rotate-right-outlined"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handlerToolbar('rotate', 45)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
class="icon-[ant-design--rotate-right-outlined]"
|
||||
></span>
|
||||
<IconifyIcon icon="ant-design:rotate-right-outlined" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -212,7 +220,7 @@ async function handleOk() {
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[vaadin--arrows-long-h]"></span>
|
||||
<IconifyIcon icon="vaadin:arrows-long-h" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -226,7 +234,7 @@ async function handleOk() {
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[vaadin--arrows-long-v]"></span>
|
||||
<IconifyIcon icon="vaadin:arrows-long-v" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -240,7 +248,7 @@ async function handleOk() {
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--zoom-in-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:zoom-in" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -254,7 +262,7 @@ async function handleOk() {
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--zoom-out-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:zoom-out" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
@@ -262,16 +270,26 @@ async function handleOk() {
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="`${prefixCls}-right`">
|
||||
<div :class="`${prefixCls}-preview`">
|
||||
|
||||
<!-- 右侧区域 -->
|
||||
<div class="h-full w-2/5">
|
||||
<!-- 预览区域 -->
|
||||
<div
|
||||
class="mx-auto h-56 w-56 overflow-hidden rounded-full border border-gray-200"
|
||||
>
|
||||
<img
|
||||
v-if="previewSource"
|
||||
:alt="$t('ui.cropper.preview')"
|
||||
:src="previewSource"
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 头像组合预览 -->
|
||||
<template v-if="previewSource">
|
||||
<div :class="`${prefixCls}-group`">
|
||||
<div
|
||||
class="mt-2 flex items-center justify-around border-t border-gray-200 pt-2"
|
||||
>
|
||||
<Avatar :src="previewSource" size="large" />
|
||||
<Avatar :size="48" :src="previewSource" />
|
||||
<Avatar :size="64" :src="previewSource" />
|
||||
@@ -282,76 +300,3 @@ async function handleOk() {
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.cropper-am {
|
||||
display: flex;
|
||||
|
||||
&-left,
|
||||
&-right {
|
||||
height: 340px;
|
||||
}
|
||||
|
||||
&-left {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&-cropper {
|
||||
height: 300px;
|
||||
background: #eee;
|
||||
background-image:
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgb(0 0 0 / 25%) 25%,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
rgb(0 0 0 / 25%) 0
|
||||
),
|
||||
linear-gradient(
|
||||
45deg,
|
||||
rgb(0 0 0 / 25%) 25%,
|
||||
transparent 0,
|
||||
transparent 75%,
|
||||
rgb(0 0 0 / 25%) 0
|
||||
);
|
||||
background-position:
|
||||
0 0,
|
||||
12px 12px;
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 50%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding-top: 8px;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,7 +33,6 @@ const imgElRef = ref<ElRef<HTMLImageElement>>();
|
||||
const cropper = ref<Cropper | null>();
|
||||
const isReady = ref(false);
|
||||
|
||||
const prefixCls = 'cropper-image';
|
||||
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
|
||||
|
||||
const getImageStyle = computed((): CSSProperties => {
|
||||
@@ -46,10 +45,9 @@ const getImageStyle = computed((): CSSProperties => {
|
||||
|
||||
const getClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
attrs.class,
|
||||
{
|
||||
[`${prefixCls}--circled`]: props.circled,
|
||||
'cropper-image--circled': props.circled,
|
||||
},
|
||||
];
|
||||
});
|
||||
@@ -115,10 +113,9 @@ function cropped() {
|
||||
imgInfo,
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line unicorn/prefer-add-event-listener
|
||||
fileReader.onerror = () => {
|
||||
fileReader.addEventListener('error', () => {
|
||||
emit('cropendError');
|
||||
};
|
||||
});
|
||||
}, 'image/png');
|
||||
}
|
||||
|
||||
@@ -157,6 +154,7 @@ function getRoundedCanvas() {
|
||||
:crossorigin="crossorigin"
|
||||
:src="src"
|
||||
:style="getImageStyle"
|
||||
class="h-auto max-w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { isValidColor, TinyColor } from '@vben/utils';
|
||||
|
||||
import { Tag } from 'ant-design-vue';
|
||||
|
||||
// import { isHexColor } from '@/utils/color' // TODO @芋艿:【可优化】增加 cssClass 的处理 https://gitee.com/yudaocode/yudao-ui-admin-vben/blob/v2.4.1/src/components/DictTag/src/DictTag.vue#L60
|
||||
import { getDictObj } from '#/utils';
|
||||
|
||||
interface DictTagProps {
|
||||
@@ -58,15 +59,23 @@ const dictTag = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
if (isValidColor(dict.cssClass)) {
|
||||
colorType = new TinyColor(dict.cssClass).toHexString();
|
||||
}
|
||||
|
||||
return {
|
||||
label: dict.label || '',
|
||||
colorType,
|
||||
cssClass: dict.cssClass,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Tag v-if="dictTag" :color="dictTag.colorType">
|
||||
<Tag
|
||||
v-if="dictTag"
|
||||
:color="dictTag.colorType ? dictTag.colorType : dictTag.cssClass"
|
||||
>
|
||||
{{ dictTag.label }}
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SelectOption,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { getDictObj, getIntDictOptions, getStrDictOptions } from '#/utils';
|
||||
import { getDictOptions } from '#/utils';
|
||||
|
||||
defineOptions({ name: 'DictSelect' });
|
||||
|
||||
@@ -25,17 +25,16 @@ const props = withDefaults(defineProps<DictSelectProps>(), {
|
||||
const attrs = useAttrs();
|
||||
|
||||
// 获得字典配置
|
||||
// TODO @dhb:可以使用 getDictOptions 替代么?
|
||||
const getDictOptions = computed(() => {
|
||||
const getDictOption = computed(() => {
|
||||
switch (props.valueType) {
|
||||
case 'bool': {
|
||||
return getDictObj(props.dictType, 'bool');
|
||||
return getDictOptions(props.dictType, 'boolean');
|
||||
}
|
||||
case 'int': {
|
||||
return getIntDictOptions(props.dictType);
|
||||
return getDictOptions(props.dictType, 'number');
|
||||
}
|
||||
case 'str': {
|
||||
return getStrDictOptions(props.dictType);
|
||||
return getDictOptions(props.dictType, 'string');
|
||||
}
|
||||
default: {
|
||||
return [];
|
||||
@@ -45,27 +44,27 @@ const getDictOptions = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
|
||||
<Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
|
||||
<SelectOption
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
v-for="(dict, index) in getDictOption"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<RadioGroup v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
|
||||
<RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
|
||||
<Radio
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
v-for="(dict, index) in getDictOption"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
|
||||
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-full" v-bind="attrs">
|
||||
<Checkbox
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
v-for="(dict, index) in getDictOption"
|
||||
:key="index"
|
||||
:value="dict.value"
|
||||
>
|
||||
|
||||
@@ -190,7 +190,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
// fix:多写此步是为了解决 multiple 属性问题
|
||||
return (
|
||||
<Select
|
||||
class="w-1/1"
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
mode="multiple"
|
||||
{...attrs}
|
||||
@@ -210,7 +210,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
class="w-1/1"
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
// TODO: @dhb52 remote 对等实现, 还是说没作用
|
||||
@@ -235,7 +235,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
];
|
||||
}
|
||||
return (
|
||||
<CheckboxGroup class="w-1/1" {...attrs}>
|
||||
<CheckboxGroup class="w-full" {...attrs}>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Checkbox key={index} value={item.value}>
|
||||
@@ -254,7 +254,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
||||
];
|
||||
}
|
||||
return (
|
||||
<RadioGroup class="w-1/1" {...attrs}>
|
||||
<RadioGroup class="w-full" {...attrs}>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Radio key={index} value={item.value}>
|
||||
|
||||
@@ -121,7 +121,7 @@ const apiSelectRule = [
|
||||
field: 'data',
|
||||
title: '请求参数 JSON 格式',
|
||||
props: {
|
||||
autosize: true,
|
||||
autoSize: true,
|
||||
type: 'textarea',
|
||||
placeholder: '{"type": 1}',
|
||||
},
|
||||
@@ -155,7 +155,7 @@ const apiSelectRule = [
|
||||
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
|
||||
(data: any)=>{ label: string; value: any }[]`,
|
||||
props: {
|
||||
autosize: true,
|
||||
autoSize: true,
|
||||
rows: { minRows: 2, maxRows: 6 },
|
||||
type: 'textarea',
|
||||
placeholder: `
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from './icons';
|
||||
|
||||
export { default as TableAction } from './table-action.vue';
|
||||
export * from './typing';
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<!-- add by 星语:参考 vben2 的方式,增加 TableAction 组件 -->
|
||||
<script setup lang="ts">
|
||||
import type { ButtonType } from 'ant-design-vue/es/button';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { ActionItem, PopConfirm } from './typing';
|
||||
|
||||
import { computed, toRaw } from 'vue';
|
||||
import { computed, unref } from 'vue';
|
||||
|
||||
import { useAccess } from '@vben/access';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
@@ -43,34 +41,31 @@ const props = defineProps({
|
||||
|
||||
const { hasAccessByCodes } = useAccess();
|
||||
|
||||
/** 检查是否显示 */
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
if (isIfShow) {
|
||||
isIfShow =
|
||||
hasAccessByCodes(action.auth || []) || (action.auth || []).length === 0;
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
/** 处理按钮 actions */
|
||||
const getActions = computed(() => {
|
||||
return (toRaw(props.actions) || [])
|
||||
.filter((action) => {
|
||||
return (
|
||||
(hasAccessByCodes(action.auth || []) ||
|
||||
(action.auth || []).length === 0) &&
|
||||
isIfShow(action)
|
||||
);
|
||||
})
|
||||
.map((action) => {
|
||||
return (props.actions || [])
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
// getPopupContainer: document.body,
|
||||
type: 'link' as ButtonType,
|
||||
type: action.type || 'link',
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
@@ -80,19 +75,16 @@ const getActions = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const getDropdownList = computed((): any[] => {
|
||||
return (toRaw(props.dropDownActions) || [])
|
||||
.filter((action) => {
|
||||
return (
|
||||
(hasAccessByCodes(action.auth || []) ||
|
||||
(action.auth || []).length === 0) &&
|
||||
isIfShow(action)
|
||||
);
|
||||
})
|
||||
.map((action, index) => {
|
||||
/** 处理下拉菜单 actions */
|
||||
const getDropdownList = computed(() => {
|
||||
return (props.dropDownActions || [])
|
||||
.filter((action: ActionItem) => isIfShow(action))
|
||||
.map((action: ActionItem, index: number) => {
|
||||
const { label, popConfirm } = action;
|
||||
const processedAction = { ...action };
|
||||
delete processedAction.icon;
|
||||
return {
|
||||
...action,
|
||||
...processedAction,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
@@ -103,8 +95,16 @@ const getDropdownList = computed((): any[] => {
|
||||
});
|
||||
});
|
||||
|
||||
/** Space 组件的 size */
|
||||
const spaceSize = computed(() => {
|
||||
return unref(getActions)?.some((item: ActionItem) => item.type === 'link')
|
||||
? 0
|
||||
: 8;
|
||||
});
|
||||
|
||||
/** 获取 PopConfirm 属性 */
|
||||
function getPopConfirmProps(attrs: PopConfirm) {
|
||||
const originAttrs: any = attrs;
|
||||
const originAttrs: any = { ...attrs };
|
||||
delete originAttrs.icon;
|
||||
if (attrs.confirm && isFunction(attrs.confirm)) {
|
||||
originAttrs.onConfirm = attrs.confirm;
|
||||
@@ -117,31 +117,44 @@ function getPopConfirmProps(attrs: PopConfirm) {
|
||||
return originAttrs;
|
||||
}
|
||||
|
||||
/** 获取 Button 属性 */
|
||||
function getButtonProps(action: ActionItem) {
|
||||
const res = {
|
||||
type: action.type || 'primary',
|
||||
...action,
|
||||
return {
|
||||
type: action.type || 'link',
|
||||
danger: action.danger || false,
|
||||
disabled: action.disabled,
|
||||
loading: action.loading,
|
||||
size: action.size,
|
||||
};
|
||||
delete res.icon;
|
||||
return res;
|
||||
}
|
||||
|
||||
/** 获取 Tooltip 属性 */
|
||||
function getTooltipProps(tooltip: any | string) {
|
||||
if (!tooltip) return {};
|
||||
return typeof tooltip === 'string' ? { title: tooltip } : { ...tooltip };
|
||||
}
|
||||
|
||||
/** 处理菜单点击 */
|
||||
function handleMenuClick(e: any) {
|
||||
const action = getDropdownList.value[e.key];
|
||||
if (action.onClick && isFunction(action.onClick)) {
|
||||
if (action && action.onClick && isFunction(action.onClick)) {
|
||||
action.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/** 生成稳定的 key */
|
||||
function getActionKey(action: ActionItem, index: number) {
|
||||
return `${action.label || ''}-${action.type || ''}-${index}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="m-table-action">
|
||||
<Space
|
||||
:size="
|
||||
getActions?.some((item: ActionItem) => item.type === 'link') ? 0 : 8
|
||||
"
|
||||
>
|
||||
<template v-for="(action, index) in getActions" :key="index">
|
||||
<div class="table-actions">
|
||||
<Space :size="spaceSize">
|
||||
<template
|
||||
v-for="(action, index) in getActions"
|
||||
:key="getActionKey(action, index)"
|
||||
>
|
||||
<Popconfirm
|
||||
v-if="action.popConfirm"
|
||||
v-bind="getPopConfirmProps(action.popConfirm)"
|
||||
@@ -149,13 +162,7 @@ function handleMenuClick(e: any) {
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
<IconifyIcon :icon="action.popConfirm.icon" />
|
||||
</template>
|
||||
<Tooltip
|
||||
v-bind="
|
||||
typeof action.tooltip === 'string'
|
||||
? { title: action.tooltip }
|
||||
: { ...action.tooltip }
|
||||
"
|
||||
>
|
||||
<Tooltip v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)">
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
@@ -164,14 +171,7 @@ function handleMenuClick(e: any) {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
<Tooltip
|
||||
v-else
|
||||
v-bind="
|
||||
typeof action.tooltip === 'string'
|
||||
? { title: action.tooltip }
|
||||
: { ...action.tooltip }
|
||||
"
|
||||
>
|
||||
<Tooltip v-else v-bind="getTooltipProps(action.tooltip)">
|
||||
<Button v-bind="getButtonProps(action)" @click="action.onClick">
|
||||
<template v-if="action.icon" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
@@ -184,16 +184,21 @@ function handleMenuClick(e: any) {
|
||||
|
||||
<Dropdown v-if="getDropdownList.length > 0" :trigger="['hover']">
|
||||
<slot name="more">
|
||||
<Button size="small" type="link">
|
||||
<Button :type="getDropdownList[0]?.type">
|
||||
<template #icon>
|
||||
{{ $t('page.action.more') }}
|
||||
<IconifyIcon class="icon-more" icon="ant-design:more-outlined" />
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
</template>
|
||||
</Button>
|
||||
</slot>
|
||||
<template #overlay>
|
||||
<Menu @click="handleMenuClick">
|
||||
<Menu.Item v-for="(action, index) in getDropdownList" :key="index">
|
||||
<Menu>
|
||||
<Menu.Item
|
||||
v-for="(action, index) in getDropdownList"
|
||||
:key="index"
|
||||
:disabled="action.disabled"
|
||||
@click="!action.popConfirm && handleMenuClick({ key: index })"
|
||||
>
|
||||
<template v-if="action.popConfirm">
|
||||
<Popconfirm v-bind="getPopConfirmProps(action.popConfirm)">
|
||||
<template v-if="action.popConfirm.icon" #icon>
|
||||
@@ -207,7 +212,9 @@ function handleMenuClick(e: any) {
|
||||
"
|
||||
>
|
||||
<IconifyIcon v-if="action.icon" :icon="action.icon" />
|
||||
<span class="ml-1">{{ action.text }}</span>
|
||||
<span :class="action.icon ? 'ml-1' : ''">
|
||||
{{ action.text }}
|
||||
</span>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
</template>
|
||||
@@ -229,9 +236,10 @@ function handleMenuClick(e: any) {
|
||||
</Dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.m-table-action {
|
||||
.ant-btn {
|
||||
.table-actions {
|
||||
.ant-btn-link {
|
||||
padding: 4px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ButtonProps } from 'ant-design-vue/es/button/buttonTypes';
|
||||
import type {
|
||||
ButtonProps,
|
||||
ButtonType,
|
||||
} from 'ant-design-vue/es/button/buttonTypes';
|
||||
import type { TooltipProps } from 'ant-design-vue/es/tooltip/Tooltip';
|
||||
|
||||
export interface PopConfirm {
|
||||
@@ -13,6 +16,7 @@ export interface PopConfirm {
|
||||
|
||||
export interface ActionItem extends ButtonProps {
|
||||
onClick?: () => void;
|
||||
type?: ButtonType;
|
||||
label?: string;
|
||||
color?: 'error' | 'success' | 'warning';
|
||||
icon?: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { VxeToolbarInstance } from '#/adapter/vxe-table';
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useContentMaximize, useRefresh } from '@vben/hooks';
|
||||
import { Expand, MsRefresh, Search, TMinimize } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Tooltip } from 'ant-design-vue';
|
||||
|
||||
@@ -41,37 +41,39 @@ defineExpose({
|
||||
<slot></slot>
|
||||
<Tooltip placement="bottom">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">搜索</div>
|
||||
<div class="max-w-52">搜索</div>
|
||||
</template>
|
||||
<Button
|
||||
class="ml-2 font-[8px]"
|
||||
class="ml-2 font-normal"
|
||||
shape="circle"
|
||||
@click="onHiddenSearchBar"
|
||||
>
|
||||
<Search :size="15" />
|
||||
<IconifyIcon icon="lucide:search" :size="15" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">刷新</div>
|
||||
<div class="max-w-52">刷新</div>
|
||||
</template>
|
||||
<Button class="ml-2 font-[8px]" shape="circle" @click="refresh">
|
||||
<MsRefresh :size="15" />
|
||||
<Button class="ml-2 font-medium" shape="circle" @click="refresh">
|
||||
<IconifyIcon icon="lucide:refresh-cw" :size="15" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">
|
||||
<div class="max-w-52">
|
||||
{{ contentIsMaximize ? '还原' : '全屏' }}
|
||||
</div>
|
||||
</template>
|
||||
<Button
|
||||
class="ml-2 font-[8px]"
|
||||
class="ml-2 font-medium"
|
||||
shape="circle"
|
||||
@click="toggleMaximizeAndTabbarHidden"
|
||||
>
|
||||
<Expand v-if="!contentIsMaximize" :size="15" />
|
||||
<TMinimize v-else :size="15" />
|
||||
<IconifyIcon
|
||||
:icon="contentIsMaximize ? 'lucide:minimize' : 'lucide:maximize'"
|
||||
:size="15"
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
|
||||
import type { AxiosResponse } from '@vben/request';
|
||||
import type { FileUploadProps } from './typing';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
@@ -20,45 +20,20 @@ import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'FileUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
value?: string | string[];
|
||||
}>(),
|
||||
{
|
||||
value: () => [],
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => [],
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: false,
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(['change', 'update:value', 'delete']);
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
value: () => [],
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => [],
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: false,
|
||||
});
|
||||
const emit = defineEmits(['change', 'update:value', 'delete', 'returnText']);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
const { getStringAccept } = useUploadType({
|
||||
@@ -112,7 +87,7 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
@@ -122,9 +97,12 @@ const handleRemove = async (file: UploadFile) => {
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function beforeUpload(file: File) {
|
||||
const fileContent = await file.text();
|
||||
emit('returnText', fileContent);
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkFileType(file, accept);
|
||||
if (!isAct) {
|
||||
@@ -141,7 +119,7 @@ const beforeUpload = async (file: File) => {
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
}
|
||||
return (isAct && !isLt) || Upload.LIST_IGNORE;
|
||||
};
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
let { api } = props;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* 默认图片类型
|
||||
*/
|
||||
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
export function checkFileType(file: File, accepts: string[]) {
|
||||
if (!accepts || accepts.length === 0) {
|
||||
return true;
|
||||
@@ -7,11 +12,6 @@ export function checkFileType(file: File, accepts: string[]) {
|
||||
return reg.test(file.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认图片类型
|
||||
*/
|
||||
export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
||||
|
||||
export function checkImgType(
|
||||
file: File,
|
||||
accepts: string[] = defaultImageAccepts,
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
|
||||
|
||||
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 { CloudUpload } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isFunction, isObject, isString } from '@vben/utils';
|
||||
|
||||
@@ -22,46 +20,20 @@ import { useUpload, useUploadType } from './use-upload';
|
||||
|
||||
defineOptions({ name: 'ImageUpload', inheritAttrs: false });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
value?: string | string[];
|
||||
}>(),
|
||||
{
|
||||
value: () => [],
|
||||
directory: undefined,
|
||||
disabled: false,
|
||||
listType: 'picture-card',
|
||||
helpText: '',
|
||||
maxSize: 2,
|
||||
maxNumber: 1,
|
||||
accept: () => defaultImageAccepts,
|
||||
multiple: false,
|
||||
api: undefined,
|
||||
resultField: '',
|
||||
showDescription: true,
|
||||
},
|
||||
);
|
||||
const props = withDefaults(defineProps<FileUploadProps>(), {
|
||||
value: () => [],
|
||||
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', 'delete']);
|
||||
const { accept, helpText, maxNumber, maxSize } = toRefs(props);
|
||||
const isInnerOperate = ref<boolean>(false);
|
||||
@@ -130,7 +102,7 @@ function getBase64<T extends ArrayBuffer | null | string>(file: File) {
|
||||
});
|
||||
}
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
async function handlePreview(file: UploadFile) {
|
||||
if (!file.url && !file.preview) {
|
||||
file.preview = await getBase64<string>(file.originFileObj!);
|
||||
}
|
||||
@@ -141,9 +113,9 @@ const handlePreview = async (file: UploadFile) => {
|
||||
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);
|
||||
@@ -153,14 +125,14 @@ const handleRemove = async (file: UploadFile) => {
|
||||
emit('change', value);
|
||||
emit('delete', file);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
function handleCancel() {
|
||||
previewOpen.value = false;
|
||||
previewTitle.value = '';
|
||||
};
|
||||
}
|
||||
|
||||
const beforeUpload = async (file: File) => {
|
||||
async function beforeUpload(file: File) {
|
||||
const { maxSize, accept } = props;
|
||||
const isAct = checkImgType(file, accept);
|
||||
if (!isAct) {
|
||||
@@ -177,7 +149,7 @@ const beforeUpload = async (file: File) => {
|
||||
setTimeout(() => (isLtMsg.value = true), 1000);
|
||||
}
|
||||
return (isAct && !isLt) || Upload.LIST_IGNORE;
|
||||
};
|
||||
}
|
||||
|
||||
async function customRequest(info: UploadRequestOption<any>) {
|
||||
let { api } = props;
|
||||
@@ -242,13 +214,13 @@ function getValue() {
|
||||
v-if="fileList && fileList.length < maxNumber"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<CloudUpload />
|
||||
<IconifyIcon icon="lucide:cloud-upload" />
|
||||
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
|
||||
</div>
|
||||
</Upload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-[14px]"
|
||||
class="mt-2 flex flex-wrap items-center text-sm"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as FileUpload } from './file-upload.vue';
|
||||
export { default as ImageUpload } from './image-upload.vue';
|
||||
export { default as InputUpload } from './input-upload.vue';
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type { AxiosResponse } from '@vben/request';
|
||||
|
||||
import type { AxiosProgressEvent } from '#/api/infra/file';
|
||||
|
||||
export enum UploadResultStatus {
|
||||
DONE = 'done',
|
||||
ERROR = 'error',
|
||||
@@ -6,3 +10,28 @@ export enum UploadResultStatus {
|
||||
}
|
||||
|
||||
export type UploadListType = 'picture' | 'picture-card' | 'text';
|
||||
|
||||
export interface FileUploadProps {
|
||||
// 根据后缀,或者其他
|
||||
accept?: string[];
|
||||
api?: (
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => Promise<AxiosResponse<any>>;
|
||||
// 上传的目录
|
||||
directory?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
listType?: UploadListType;
|
||||
// 最大数量的文件,Infinity不限制
|
||||
maxNumber?: number;
|
||||
// 文件最大多少MB
|
||||
maxSize?: number;
|
||||
// 是否支持多选
|
||||
multiple?: boolean;
|
||||
// support xxx.xxx.xx
|
||||
resultField?: string;
|
||||
// 是否显示下面的描述
|
||||
showDescription?: boolean;
|
||||
value?: string | string[];
|
||||
}
|
||||
|
||||
@@ -80,17 +80,17 @@ export function useUploadType({
|
||||
}
|
||||
|
||||
// TODO @芋艿:目前保持和 admin-vue3 一致,后续可能重构
|
||||
export const useUpload = (directory?: string) => {
|
||||
export function useUpload(directory?: string) {
|
||||
// 后端上传地址
|
||||
const uploadUrl = getUploadUrl();
|
||||
// 是否使用前端直连上传
|
||||
const isClientUpload =
|
||||
UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE;
|
||||
// 重写ElUpload上传方法
|
||||
const httpRequest = async (
|
||||
async function httpRequest(
|
||||
file: File,
|
||||
onUploadProgress?: AxiosProgressEvent,
|
||||
) => {
|
||||
) {
|
||||
// 模式一:前端上传
|
||||
if (isClientUpload) {
|
||||
// 1.1 生成文件名称
|
||||
@@ -114,20 +114,20 @@ export const useUpload = (directory?: string) => {
|
||||
// 模式二:后端上传
|
||||
return uploadFile({ file, directory }, onUploadProgress);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
uploadUrl,
|
||||
httpRequest,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得上传 URL
|
||||
*/
|
||||
export const getUploadUrl = (): string => {
|
||||
export function getUploadUrl(): string {
|
||||
return `${apiURL}/infra/file/upload`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件信息
|
||||
@@ -135,7 +135,10 @@ export const getUploadUrl = (): string => {
|
||||
* @param vo 文件预签名信息
|
||||
* @param file 文件
|
||||
*/
|
||||
function createFile0(vo: InfraFileApi.FilePresignedUrlRespVO, file: File) {
|
||||
function createFile0(
|
||||
vo: InfraFileApi.FilePresignedUrlResp,
|
||||
file: File,
|
||||
): InfraFileApi.File {
|
||||
const fileVO = {
|
||||
configId: vo.configId,
|
||||
url: vo.url,
|
||||
|
||||
Reference in New Issue
Block a user