新增前端vue

This commit is contained in:
2025-11-26 13:55:01 +08:00
parent ae391f1b94
commit ffd5a6ad66
781 changed files with 83348 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
import { withInstall } from '@jeesite/core/utils';
import basicUpload from './src/BasicUpload.vue';
export const BasicUpload = withInstall(basicUpload);

View File

@@ -0,0 +1,155 @@
<template>
<div class="jeesite-basic-upload">
<Space>
<a-button
v-if="!(readonly || disabled)"
type="primary"
@click="openUploadModal"
preIcon="i-carbon:cloud-upload"
:size="props.size"
>
{{ uploadText || t('component.upload.upload') }}
</a-button>
<Tooltip placement="bottom" v-if="getShowPreview">
<template #title>
{{ t('component.upload.uploaded') }}
<template v-if="fileList.length">
{{ fileList.length }}
</template>
</template>
<a-button @click="props.showPreviewList ? null : openPreviewModal()" :size="props.size">
<Icon icon="i-bi:eye" />
<template v-if="fileList.length && props.showPreviewNumber">
{{ fileList.length }}
</template>
</a-button>
</Tooltip>
</Space>
<UploadModal
v-bind="bindValue"
:previewFileList="fileList"
:apiUploadUrl="apiUploadUrl"
:apiDownloadUrl="apiDownloadUrl"
@register="registerUploadModal"
@change="handleChange"
@delete="handleDelete"
/>
<UploadPreviewModal
:value="fileList"
:readonly="readonly || disabled"
:imageThumbName="imageThumbName"
:apiDownloadUrl="apiDownloadUrl"
@register="registerPreviewModal"
@change="handlePreviewChange"
@delete="handleDelete"
/>
</div>
</template>
<script lang="ts" setup name="BasicUpload">
import { defineComponent, ref, watch, unref, computed, useAttrs } from 'vue';
import UploadModal from './UploadModal.vue';
import UploadPreviewModal from './UploadPreviewModal.vue';
import { Icon } from '@jeesite/core/components/Icon';
import { Tooltip, Space } from 'ant-design-vue';
import { useModal } from '@jeesite/core/components/Modal';
import { uploadContainerProps } from './props';
import { omit } from 'lodash-es';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { isArray } from '@jeesite/core/utils/is';
import { FileUpload, uploadFileList } from '@jeesite/core/api/sys/upload';
const props = defineProps(uploadContainerProps);
const emit = defineEmits(['change', 'delete', 'update:value', 'click']);
const attrs = useAttrs();
const { t } = useI18n();
const [registerUploadModal, { openModal }] = useModal();
const [registerPreviewModal, { openModal: openPreviewModal }] = useModal();
function openUploadModal() {
openModal();
emit('click');
}
const dataMap = ref<object>({});
const fileList = ref<FileUpload[]>([]);
const fileListDel = ref<FileUpload[]>([]);
const getShowPreview = computed(() => {
const { showPreview, emptyHidePreview, showPreviewList, showPreviewNumber } = props;
if (showPreviewList && !showPreviewNumber) return false;
if (!showPreview) return false;
if (!emptyHidePreview) return true;
return emptyHidePreview ? fileList.value.length > 0 : true;
});
const bindValue = computed(() => {
const value = { ...attrs, ...props };
return omit(value, 'onChange', 'class');
});
watch(
() => props.value,
(value) => {
dataMap.value = value;
emit('update:value', dataMap.value);
},
{ immediate: true },
);
watch(
() => [props.bizKey, props.loadTime],
() => {
loadFileList();
},
{ immediate: true },
);
function loadFileList() {
fileList.value = [];
fileListDel.value = [];
if (props.bizKey != '') {
uploadFileList(
{
bizKey: props.bizKey,
bizType: props.bizType,
},
props.apiFileListUrl,
).then((res) => {
if (isArray(res)) {
fileList.value = res;
dataMap.value[props.bizType + '__len'] = fileList.value.length;
emit('update:value', dataMap.value);
emit('change', dataMap.value);
}
});
}
}
// 上传modal保存操作
function handleChange(records: FileUpload[]) {
fileList.value = [...unref(fileList), ...(records || [])];
dataMap.value[props.bizType] = fileList.value.map((item) => item.id).join(',');
dataMap.value[props.bizType + '__len'] = fileList.value.length;
emit('update:value', dataMap.value);
emit('change', dataMap.value, fileList.value);
}
// 预览modal保存操作
function handlePreviewChange(records: FileUpload[]) {
fileList.value = [...(records || [])];
dataMap.value[props.bizType] = fileList.value.map((item) => item.id).join(',');
dataMap.value[props.bizType + '__len'] = fileList.value.length;
emit('update:value', dataMap.value);
emit('change', dataMap.value, fileList.value);
}
function handleDelete(record: FileUpload) {
fileListDel.value.push(record);
dataMap.value[props.bizType + '__del'] = fileListDel.value.map((item) => item.id).join(',');
dataMap.value[props.bizType + '__len'] = fileList.value.length;
emit('delete', record);
emit('update:value', dataMap.value);
emit('change', dataMap.value, fileList.value);
}
</script>

View File

@@ -0,0 +1,126 @@
<script lang="tsx">
import { defineComponent, CSSProperties, nextTick, ref } from 'vue';
import { useResizeObserver } from '@vueuse/core';
import { fileListProps } from './props';
import { isFunction } from '@jeesite/core/utils/is';
import { useModalContext } from '@jeesite/core/components/Modal/src/hooks/useModalContext';
import { get } from 'lodash-es';
export default defineComponent({
name: 'FileList',
props: fileListProps,
setup(props) {
const modalFn = useModalContext();
const tableRef = ref<HTMLTableElement>();
useResizeObserver(tableRef, () => {
nextTick(() => {
modalFn?.redoModalHeight?.();
});
});
return () => {
const { columns, actionColumn, dataSource } = props;
const columnList = [...columns, actionColumn];
return (
<table class="file-table" ref={tableRef}>
<colgroup>
{columnList.map((item) => {
const { width = 0, dataIndex } = item;
const style: CSSProperties = {
width: `${width}px`,
minWidth: `${width}px`,
};
return <col style={width ? style : {}} key={dataIndex} />;
})}
</colgroup>
<thead>
<tr class="file-table-tr">
{columnList.map((item) => {
const { title = '', align = 'center', dataIndex } = item;
return (
dataIndex && (
<th class={['file-table-th', align]} key={dataIndex}>
{title}
</th>
)
);
})}
</tr>
</thead>
<tbody>
{dataSource.map((record = {}, index) => {
return (
<tr class="file-table-tr" key={`${index + record.name || ''}`}>
{columnList.map((item) => {
const { dataIndex = '', customRender, align = 'center' } = item;
const render = customRender && isFunction(customRender);
return (
dataIndex && (
<td class={['file-table-td', align]} key={dataIndex}>
{render
? customRender?.({ text: get(record, dataIndex), record, index })
: get(record, dataIndex)}
</td>
)
);
})}
</tr>
);
})}
{dataSource.length == 0 && (
<tr class="file-table-tr">
<td class="file-table-td center" colspan={columnList.length}>
{props.emptyText}
</td>
</tr>
)}
</tbody>
</table>
);
};
},
});
</script>
<style lang="less">
.file-table {
width: 100%;
border-top: 1px solid @border-color-base;
border-right: 1px solid @border-color-base;
border-collapse: separate;
border-spacing: 0;
&-th,
&-td {
border-left: 1px solid @border-color-base;
border-bottom: 1px solid @border-color-base;
padding: 12px 8px;
overflow-wrap: break-word;
word-break: break-all;
white-space: normal;
min-width: 100px;
}
thead {
background-color: @background-color-light;
}
.center {
text-align: center;
}
.left {
text-align: left;
}
.right {
text-align: right;
}
}
html[data-theme='dark'] {
.file-table {
thead {
background-color: #1a1a1a;
}
}
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<span class="thumb">
<Image
v-if="fileUrl"
:src="fileUrl"
:preview="{
src: previewUrl || fileUrl,
}"
:width="104"
/>
</span>
</template>
<script lang="ts" setup>
import { propTypes } from '@jeesite/core/utils/propTypes';
import { Image } from 'ant-design-vue';
defineProps({
fileUrl: propTypes.string.def(''),
fileName: propTypes.string.def(''),
previewUrl: propTypes.string.def(''),
});
</script>
<style lang="less">
.thumb {
img {
position: static;
display: block;
cursor: zoom-in;
border-radius: 4px;
object-fit: cover;
}
}
</style>

View File

@@ -0,0 +1,319 @@
<template>
<BasicModal
width="80%"
:title="t('component.upload.upload')"
:okText="t('component.upload.save')"
v-bind="$attrs"
@register="register"
@ok="handleOk"
:closeFunc="handleCloseFunc"
:maskClosable="false"
:keyboard="false"
wrapClassName="upload-modal"
:okButtonProps="getOkButtonProps"
:cancelButtonProps="{ disabled: uploading }"
>
<template #centerFooter>
<a-button
@click="handleStartUpload"
color="success"
:disabled="!getIsSelectFile"
:loading="uploading"
v-show="isLazy"
>
{{ getUploadBtnText }}
</a-button>
</template>
<div class="upload-modal-toolbar">
<Alert :message="getHelpText" type="info" banner class="upload-modal-toolbar__text" />
<Upload
:accept="getStringAccept"
:multiple="multiple"
:before-upload="beforeUpload"
:directory="directory"
:show-upload-list="false"
class="upload-modal-toolbar__btn"
>
<a-button type="primary">
{{ t('component.upload.choose') }}
</a-button>
</Upload>
</div>
<div class="overflow-x-auto">
<FileList
:dataSource="fileItemList"
:columns="columns"
:actionColumn="actionColumn"
:emptyText="t('component.upload.fileListEmpty')"
/>
</div>
</BasicModal>
</template>
<script lang="ts" setup>
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author ThinkGem
*/
import { ref, unref, computed } from 'vue';
import { Upload, Alert } from 'ant-design-vue';
import { BasicModal, useModalInner } from '@jeesite/core/components/Modal';
// import { BasicTable, useTable } from '@jeesite/core/components/Table';
import FileList from './FileList.vue';
import { uploadProps } from './props';
import { useUpload } from './useUpload';
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
import { FileItem, UploadResultStatus } from './typing';
import { createTableColumns, createActionColumn } from './data';
import { checkImgType, getBase64WithFile } from './helper';
import { buildUUID } from '@jeesite/core/utils/uuid';
import { isFunction } from '@jeesite/core/utils/is';
import { warn } from '@jeesite/core/utils/log';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { FileUpload } from '@jeesite/core/api/sys/upload';
import { openWindowLayer } from '@jeesite/core/utils';
const props = defineProps(uploadProps);
const emit = defineEmits(['change', 'register', 'delete']);
const { t } = useI18n();
const [register, { closeModal }] = useModalInner();
const fileItemList = ref<FileItem[]>([]);
const uploading = ref(false);
const { getStringAccept, getHelpText, getMaxFileSize, getUploadParams } = useUpload(props);
const { createMessage } = useMessage();
const getIsSelectFile = computed(() => {
return (
fileItemList.value.length > 0 && !fileItemList.value.every((item) => item.status === UploadResultStatus.SUCCESS)
);
});
const getOkButtonProps = computed(() => {
const someSuccess = fileItemList.value.some((item) => item.status === UploadResultStatus.SUCCESS);
return {
disabled: uploading.value || fileItemList.value.length === 0 || !someSuccess,
};
});
const getUploadBtnText = computed(() => {
const someError = fileItemList.value.some((item) => item.status === UploadResultStatus.ERROR);
return uploading.value
? t('component.upload.uploading')
: someError
? t('component.upload.reUploadFailed')
: t('component.upload.startUpload');
});
// 上传前校验
function beforeUpload(file: File) {
const { size, name } = file;
const { bizKey, bizType, uploadType } = props;
// 设置最大值,则判断
if (file.size >= unref(getMaxFileSize)) {
createMessage.error(t('component.upload.maxSizeMultiple', [unref(getMaxFileSize) / 1024 / 1024]));
return false;
}
const id = buildUUID();
const commonItem = {
id,
file,
size,
name,
percent: 0,
type: name.split('.').pop(),
fileMd5: id,
fileName: name,
fileUploadId: '',
fileEntityId: '',
bizKey,
bizType,
uploadType,
} as FileItem;
function addFileItem() {
// 生成图片缩略图
if (checkImgType(file)) {
getBase64WithFile(file).then(({ result: fileUrl }) => {
addFileItemList({
fileUrl,
...commonItem,
});
});
} else {
addFileItemList(commonItem);
}
}
addFileItem();
return false;
}
function addFileItemList(record: FileItem) {
const { maxNumber } = props;
if (fileItemList.value.length + (props.previewFileList?.length || 0) >= maxNumber) {
createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
return;
}
fileItemList.value = [...unref(fileItemList), record];
if (!props.isLazy) {
uploadApiByItem(fileItemList.value[fileItemList.value.length - 1]);
}
}
// 删除
function handleRemove(record: FileItem) {
const index = fileItemList.value.findIndex((item) => item.id === record.id);
if (index !== -1) {
const removed = fileItemList.value.splice(index, 1);
const item = removed[0] as FileItem;
if (item && item.responseData?.fileUpload) {
emit('delete', item.responseData?.fileUpload);
}
}
}
// 开始上传文件
async function uploadApiByItem(item: FileItem) {
const { api } = props;
if (!api || !isFunction(api)) {
return warn('upload api must exist and be a function');
}
try {
item.status = UploadResultStatus.UPLOADING;
if (item.percent != 100) {
await uploadFile(item);
}
item.status = UploadResultStatus.SUCCESS;
return {
success: true,
error: null,
};
} catch (e) {
console.log(e);
item.status = UploadResultStatus.ERROR;
return {
success: false,
error: e,
};
}
}
// 上传文件
async function uploadFile(item: FileItem) {
const { api } = props;
try {
const params = unref(getUploadParams);
const { data } = await api(
{
bizKey: item.bizKey,
bizType: item.bizType,
uploadType: item.uploadType,
fileMd5: item.fileMd5,
fileName: item.fileName,
fileUploadId: item.fileUploadId,
fileEntityId: item.fileEntityId,
imageMaxWidth: params.imageMaxWidth || '',
imageMaxHeight: params.imageMaxHeight || '',
...(props.uploadParams || {}),
file: item.file,
},
(progressEvent: ProgressEvent) => {
item.percent = ((progressEvent.loaded / progressEvent.total) * 100) | 0;
},
props.apiUploadUrl,
);
item.responseData = data;
} catch (e) {
throw e;
}
}
// 点击开始上传
async function handleStartUpload() {
if (uploading.value) return;
uploading.value = true;
try {
// 只上传不是成功状态的
const uploadFileList = fileItemList.value.filter((item) => item.status !== UploadResultStatus.SUCCESS) || [];
const data = await Promise.all(
uploadFileList.map((item) => {
return uploadApiByItem(item);
}),
);
// 生产环境:抛出错误
const errorList = data.filter((item: any) => item.result === 'false');
if (errorList.length > 0) throw errorList;
} finally {
uploading.value = false;
}
}
// 点击保存
function handleOk() {
const { maxNumber } = props;
if (fileItemList.value.length > maxNumber) {
return createMessage.warning(t('component.upload.maxNumber', [maxNumber]));
}
if (uploading.value) {
return createMessage.warning(t('component.upload.saveWarn'));
}
const fileList: FileUpload[] = [];
for (const item of fileItemList.value) {
const { status, responseData } = item;
if (status === UploadResultStatus.SUCCESS && responseData) {
fileList.push(responseData.fileUpload);
}
}
// 存在一个上传成功的即可保存
if (fileList.length <= 0) {
return createMessage.warning(t('component.upload.saveError'));
}
fileItemList.value = [];
closeModal();
emit('change', fileList);
}
// 点击关闭:则所有操作不保存,包括上传的
async function handleCloseFunc() {
if (!uploading.value) {
fileItemList.value = [];
return true;
} else {
createMessage.warning(t('component.upload.uploadWait'));
return false;
}
}
const columns = createTableColumns() as any[];
const actionColumn = createActionColumn(handleRemove) as any;
</script>
<style lang="less">
.upload-modal {
.ant-upload-list {
display: none;
}
.ant-table-wrapper .ant-spin-nested-loading {
padding: 0;
}
&-toolbar {
display: flex;
align-items: center;
margin-bottom: 8px;
&__btn {
margin-left: 8px;
text-align: right;
flex: 1;
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<BasicModal
v-if="!props.showPreviewList"
width="80%"
:title="t('component.upload.view')"
:cancelText="t('component.modal.okText')"
wrapClassName="upload-preview-modal"
v-bind="$attrs"
@register="register"
:showOkBtn="false"
>
<div class="overflow-x-auto">
<FileList
:dataSource="fileList"
:columns="columns"
:actionColumn="actionColumn"
:readonly="readonly"
:emptyText="t('component.upload.fileListEmpty')"
/>
</div>
</BasicModal>
<div v-else-if="props.emptyHidePreview ? fileList.length > 0 : true" class="overflow-x-auto mt-3">
<FileList
:dataSource="fileList"
:columns="columns"
:actionColumn="actionColumn"
:readonly="readonly"
:emptyText="t('component.upload.fileListEmpty')"
/>
</div>
</template>
<script lang="ts" setup>
import { watch, ref, useAttrs } from 'vue';
import { BasicModal, useModalInner } from '@jeesite/core/components/Modal';
// import { BasicTable, useTable } from '@jeesite/core/components/Table';
import FileList from './FileList.vue';
import { previewProps } from './props';
import { downloadByUrl } from '@jeesite/core/utils/file/download';
import { createPreviewColumns, createPreviewActionColumn } from './data';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import { isArray } from '@jeesite/core/utils/is';
import { FileUpload } from '@jeesite/core/api/sys/upload';
const props = defineProps(previewProps);
const emit = defineEmits(['change', 'register', 'delete']);
const [register] = useModalInner();
const { t } = useI18n();
const fileList = ref<FileUpload[]>([]);
watch(
() => props.value,
(value) => {
if (!isArray(value)) value = [];
fileList.value = value;
},
{ immediate: true },
);
// 删除
function handleRemove(record: FileUpload) {
const index = fileList.value.findIndex((item) => item.id === record.id);
if (index !== -1) {
const removed = fileList.value.splice(index, 1);
emit('delete', removed[0]);
emit('change', fileList.value);
}
}
// 下载
function handleDownload(record: FileUpload) {
downloadByUrl({ url: props.apiDownloadUrl + '/' + record.id });
}
const columns = createPreviewColumns(props) as any[];
const actionColumn = createPreviewActionColumn(
{
handleRemove,
handleDownload,
},
props.readonly,
) as any;
</script>
<style lang="less">
.upload-preview-modal {
.ant-upload-list {
display: none;
}
.ant-table-wrapper .ant-spin-nested-loading {
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,198 @@
import type { BasicColumn, ActionItem } from '@jeesite/core/components/Table';
import { FileItem, UploadResultStatus } from './typing';
import { formatSize, isImgTypeByName } from './helper';
import { Avatar, Progress, Tag } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import TableAction from '@jeesite/core/components/Table/src/components/TableAction.vue';
import ThumbUrl from './ThumbUrl.vue';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import { FileUpload } from '@jeesite/core/api/sys/upload';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
const { t } = useI18n();
const { ctxPath } = useGlobSetting();
// 文件上传列表
export function createTableColumns(): BasicColumn[] {
return [
{
dataIndex: 'fileUrl',
title: t('component.upload.legend'),
width: 100,
customRender: ({ record, index }) => {
const { fileUrl, type, fileEntity } = (record as FileUpload) || {};
let url = fileUrl || '';
if (!url.startsWith('data:image/') && url.indexOf('://') == -1) {
url = ctxPath + url;
}
if (isImgTypeByName(url)) {
return <ThumbUrl fileUrl={url} />;
}
const ext = type || fileEntity?.fileExtension || <Icon icon="i-ant-design:file-outlined" />;
const color = ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae'][index % 4];
return <Avatar style={{ backgroundColor: color, verticalAlign: 'middle' }}>{ext}</Avatar>;
},
},
{
dataIndex: 'fileName',
title: t('component.upload.fileName'),
align: 'left',
customRender: ({ text, record }) => {
const { percent, status: uploadStatus } = (record as FileItem) || {};
let status: 'normal' | 'exception' | 'active' | 'success' = 'normal';
if (uploadStatus === UploadResultStatus.ERROR) {
status = 'exception';
} else if (uploadStatus === UploadResultStatus.UPLOADING) {
status = 'active';
} else if (uploadStatus === UploadResultStatus.SUCCESS) {
status = 'success';
}
return (
<span>
<p class="mb-1" title={text}>
{text}
</p>
<Progress percent={percent} size="small" status={status} />
</span>
);
},
},
{
dataIndex: 'size',
title: t('component.upload.fileSize'),
width: 100,
customRender: ({ text = 0 }) => {
return text && formatSize(text);
},
},
{
dataIndex: 'status',
title: t('component.upload.fileStatue'),
width: 100,
customRender: ({ text, record }) => {
const { responseData } = (record as FileItem) || {};
if (text === UploadResultStatus.SUCCESS) {
return <Tag color="green">{() => responseData?.message || t('component.upload.uploadSuccess')}</Tag>;
} else if (text === UploadResultStatus.ERROR) {
return <Tag color="red">{() => responseData?.message || t('component.upload.uploadError')}</Tag>;
} else if (text === UploadResultStatus.UPLOADING) {
return <Tag color="blue">{() => responseData?.message || t('component.upload.uploading')}</Tag>;
}
return text;
},
},
];
}
export function createActionColumn(handleRemove: Function): BasicColumn {
return {
width: 120,
title: t('component.upload.operating'),
dataIndex: 'actions',
align: 'center',
fixed: false,
customRender: ({ record }) => {
const actions: ActionItem[] = [
{
label: t('component.upload.del'),
color: 'error',
popConfirm: {
title: t('component.upload.delConfirm'),
confirm: handleRemove.bind(null, record),
},
},
];
return <TableAction actions={actions} outside={true} />;
},
};
}
// 文件预览列表
export function createPreviewColumns(props: any): BasicColumn[] {
return [
{
dataIndex: 'fileUrl',
title: t('component.upload.legend'),
width: 100,
customRender: ({ record, index }) => {
const { fileUrl, type, fileEntity } = (record as FileUpload) || {};
let url = fileUrl || '';
let previewUrl;
if (!url.startsWith('data:image/') && url.indexOf('://') == -1) {
url = ctxPath + url;
if (props.imageThumbName) {
previewUrl = url;
if (url.indexOf('?') == -1) {
url += '.' + props.imageThumbName;
} else {
url = url.replace('?', '.' + props.imageThumbName + '?');
}
}
}
if (isImgTypeByName(url)) {
return <ThumbUrl fileUrl={url} previewUrl={previewUrl} />;
}
const ext = type || fileEntity?.fileExtension || <Icon icon="i-ant-design:file-outlined" />;
const color = ['#f56a00', '#7265e6', '#ffbf00', '#00a2ae'][index % 4];
return <Avatar style={{ backgroundColor: color, verticalAlign: 'middle' }}>{ext}</Avatar>;
},
},
{
dataIndex: 'fileName',
title: t('component.upload.fileName'),
align: 'left',
},
{
dataIndex: 'fileEntity.fileSize',
title: t('component.upload.fileSize'),
width: 100,
customRender: ({ text = 0 }) => {
return text && formatSize(text);
},
},
{
title: t('上传时间'),
dataIndex: 'createDate',
width: 130,
align: 'center',
customRender: ({ text = 0 }) => {
return <span class="truncate">{text}</span>;
},
},
];
}
export function createPreviewActionColumn(
{
handleRemove,
handleDownload,
}: {
handleRemove: Fn;
handleDownload: Fn;
},
readonly = false,
): BasicColumn {
return {
width: 160,
title: t('component.upload.operating'),
dataIndex: 'actions',
align: 'center',
fixed: false,
customRender: ({ record }) => {
const actions: ActionItem[] = [];
if (!readonly) {
actions.push({
label: t('component.upload.del'),
color: 'error',
popConfirm: {
title: t('component.upload.delConfirm'),
confirm: handleRemove.bind(null, record),
},
});
}
actions.push({
label: t('component.upload.download'),
onClick: handleDownload.bind(null, record),
});
return <TableAction actions={actions} outside={true} />;
},
};
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author ThinkGem
*/
export function checkFileType(file: File, accepts: string[]) {
const newTypes = accepts.join('|');
// const reg = /\.(jpg|jpeg|png|gif|txt|doc|docx|xls|xlsx|xml)$/i;
const reg = new RegExp('\\.(' + newTypes + ')$', 'i');
return reg.test(file.name);
}
export function checkImgType(file: File) {
return isImgTypeByName(file.name);
}
export function isImgTypeByName(name: string) {
return name.startsWith('data:image/') || /\.(jpg|jpeg|png|gif)$/i.test(name);
}
export function getBase64WithFile(file: File) {
return new Promise<{
result: string;
file: File;
}>((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve({ result: reader.result as string, file });
reader.onerror = (error) => reject(error);
});
}
// 格式化文件大小, 输出成带单位的字符串 think gem
export function formatSize(size: number, pointLength = 2, units = ['B', 'K', 'M', 'G', 'TB']) {
if (!size) return '';
let unit;
while ((unit = units.shift()) && size > 1024) {
size = size / 1024;
}
return (unit === 'B' ? size : size.toFixed(pointLength)) + unit;
}

View File

@@ -0,0 +1,206 @@
import type { PropType } from 'vue';
import { FileBasicColumn } from './typing';
import { FileUpload, uploadFile } from '@jeesite/core/api/sys/upload';
import { useGlobSetting } from '@jeesite/core/hooks/setting';
import type { SizeType } from '@jeesite/core/components/Table';
import { DEFAULT_SIZE } from '@jeesite/core/components/Table/src/const';
type UploadType = 'image' | 'media' | 'file' | 'all';
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author ThinkGem
*/
const { ctxAdminPath } = useGlobSetting();
export const basicProps = {
uploadText: {
type: String as PropType<string>,
default: '',
},
helpText: {
type: String as PropType<string>,
default: '',
},
// 文件最大多少MB
maxSize: {
type: Number as PropType<number>,
default: null, // 默认从后台获取
},
// 最大数量的文件Infinity不限制
maxNumber: {
type: Number as PropType<number>,
default: Infinity,
},
// 根据后缀,或者其他
accept: {
type: Array as PropType<string[]>,
default: () => [],
},
multiple: {
type: Boolean as PropType<boolean>,
default: true,
},
uploadParams: {
type: Object as PropType<any>,
default: {},
},
readonly: {
type: Boolean as PropType<boolean>,
default: false,
},
disabled: {
type: Boolean as PropType<boolean>,
default: false,
},
api: {
type: Function as PropType<PromiseFn>,
default: uploadFile,
},
apiUploadUrl: {
type: String as PropType<string>,
default: ctxAdminPath + '/file/upload',
},
apiDownloadUrl: {
type: String as PropType<string>,
default: ctxAdminPath + '/file/download',
},
apiFileListUrl: {
type: String as PropType<string>,
default: ctxAdminPath + '/file/fileList',
},
// 选择文件后,是否需要点击上传按钮再上传文件
isLazy: {
type: Boolean as PropType<boolean>,
default: false,
},
// 业务主键
bizKey: {
type: [String, Number] as PropType<string | number>,
default: '',
},
// 业务类型
bizType: {
type: String as PropType<string>,
default: '',
},
// 上传类型
uploadType: {
type: String as PropType<UploadType>,
default: 'all',
},
// 图片压缩最大宽度
imageMaxWidth: {
type: Number as PropType<number>,
default: null, // 默认从后台获取
},
// 图片压缩最大高度
imageMaxHeight: {
type: Number as PropType<number>,
default: null, // 默认从后台获取
},
// 如果开启了图片缩略图这里可以指定缩略图名称例如150x150.jpg v5.4.2
imageThumbName: {
type: String as PropType<string>,
default: '',
},
// 是否启用秒传(标准版/专业版)
checkmd5: {
type: Boolean as PropType<boolean>,
default: null, // 默认从后台获取
},
// 是否开启分片上传(标准版/专业版)
chunked: {
type: Boolean as PropType<boolean>,
default: null, // 默认从后台获取
},
// 分片大小(字节)(标准版/专业版)
chunkSize: {
type: Number as PropType<number>,
default: null, // 默认从后台获取
},
// 最大上传线程数(标准版/专业版)
threads: {
type: Number as PropType<number>,
default: null, // 默认从后台获取
},
// 是否文件夹上传caniuse
directory: {
type: Boolean as PropType<boolean>,
default: false,
},
// 预览按钮大小
size: {
type: String as PropType<SizeType>,
default: DEFAULT_SIZE,
},
};
export const uploadContainerProps = {
...basicProps,
value: {
type: Object as PropType<any>,
default: {},
},
// 是否显示预览按钮
showPreview: {
type: Boolean as PropType<boolean>,
default: true,
},
// 是否显示已上传的文件个数
showPreviewNumber: {
type: Boolean as PropType<boolean>,
default: true,
},
// 直接在表单里显示预览文件列表
showPreviewList: {
type: Boolean as PropType<boolean>,
default: false,
},
// 预览表格为空的时候,是否不显示预览框
emptyHidePreview: {
type: Boolean as PropType<boolean>,
default: true,
},
// 加载时间戳,此为监听属性,方便刷新文件列表数据
loadTime: {
type: Number as PropType<number>,
default: 0,
},
};
export const uploadProps = {
...basicProps,
previewFileList: {
type: Array as PropType<FileUpload[]>,
default: () => [],
},
};
export const previewProps = {
...uploadContainerProps,
value: {
type: Array as PropType<FileUpload[]>,
default: () => [],
},
};
export const fileListProps = {
columns: {
type: [Array] as PropType<FileBasicColumn[]>,
default: null,
},
actionColumn: {
type: Object as PropType<FileBasicColumn>,
default: null,
},
dataSource: {
type: Array as PropType<any[]>,
default: null,
},
emptyText: {
type: String,
default: '',
},
};

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
* No deletion without permission, or be held responsible to law.
* @author ThinkGem
*/
import { UploadApiResult } from '@jeesite/core/api/sys/upload';
export enum UploadResultStatus {
SUCCESS = 'success',
ERROR = 'error',
UPLOADING = 'uploading',
}
export interface FileItem {
file: File;
size: string | number;
type?: string;
percent: number;
status?: UploadResultStatus;
responseData?: UploadApiResult;
// jeesite
id: string;
fileMd5: string;
fileName: string;
fileUploadId: string;
fileEntityId: string;
bizKey: string;
bizType: string;
uploadType: string;
imageMaxWidth?: string | number;
imageMaxHeight?: string | number;
fileUrl?: string;
uploadInfo?: string;
}
// export interface PreviewFileItem extends FileItem {
// url: string;
// name: string;
// type: string;
// }
export interface FileBasicColumn {
/**
* Renderer of the table cell. The return value should be a VNode, or an object for colSpan/rowSpan config
* @type Function | ScopedSlot
*/
customRender?: Function;
/**
* Title of this column
* @type any (string | slot)
*/
title: string;
/**
* Width of this column
* @type string | number
*/
width?: number;
/**
* Display field of the data record, could be set like a.b.c
* @type string
*/
dataIndex: string;
/**
* specify how content is aligned
* @default 'left'
* @type string
*/
align?: 'left' | 'right' | 'center';
}

View File

@@ -0,0 +1,100 @@
import { computed, ComputedRef, onMounted, Ref, ref, ToRefs, unref } from 'vue';
import { UploadParams, uploadParams } from '@jeesite/core/api/sys/upload';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
export function useUpload(props: any) {
const { t } = useI18n();
const uploadParamsRef = ref<UploadParams>({} as any);
onMounted(async () => {
uploadParamsRef.value = await uploadParams();
});
const getUploadParams = computed(() => {
const params = unref(uploadParamsRef);
if (props.checkmd5 != null) {
params.checkmd5 = props.checkmd5;
}
if (props.imageMaxWidth) {
params.imageMaxWidth = props.imageMaxWidth;
}
if (props.imageMaxHeight) {
params.imageMaxHeight = props.imageMaxHeight;
}
return params;
});
// 上传最大文件大小(字节)
const getMaxFileSize = computed(() => {
const maxSize = props.maxSize;
if (maxSize) {
return maxSize * 1024 * 1024;
} else {
const { maxFileSize } = unref(uploadParamsRef);
if (maxFileSize) {
return maxFileSize;
}
}
return 500 * 1024 * 1024;
});
// 文件类型限制
const getAccept = computed(() => {
const accept = props.accept;
if (accept && accept.length > 0) {
return accept;
}
const { imageAllowSuffixes, mediaAllowSuffixes, fileAllowSuffixes } = unref(uploadParamsRef);
const uploadType = props.uploadType;
if (uploadType == 'image') {
return imageAllowSuffixes.split(',');
} else if (uploadType == 'media') {
return mediaAllowSuffixes.split(',');
} else if (uploadType == 'file') {
return fileAllowSuffixes.split(',');
} else {
return [...imageAllowSuffixes.split(','), ...mediaAllowSuffixes.split(','), ...fileAllowSuffixes.split(',')];
}
});
// 文件类型限制(文件选择框类型)
const getStringAccept = computed(() => {
return unref(getAccept)
.map((item: any) => {
if (item.indexOf('/') > 0 || item.startsWith('.')) {
return item;
} else {
return `.${item}`;
}
})
.join(',');
});
// 支持jpg、jpeg、png格式不超过2M最多可选择10张图片
const getHelpText = computed(() => {
const helpText = props.helpText;
if (helpText) {
return helpText;
}
const helpTexts: string[] = [];
const accept = props.accept;
if (accept.length > 0) {
helpTexts.push(t('component.upload.accept', [accept.join(',')]));
}
const maxSize = unref(getMaxFileSize);
if (maxSize) {
helpTexts.push(t('component.upload.maxSize', [maxSize / 1024 / 1024]));
}
const maxNumber = props.maxNumber;
if (maxNumber && maxNumber !== Infinity) {
helpTexts.push(t('component.upload.maxNumber', [maxNumber]));
}
return helpTexts.join('');
});
return { getUploadParams, getMaxFileSize, getAccept, getStringAccept, getHelpText };
}