Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/image/image.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ref, computed, defineComponent, watchEffect } from 'vue';
import type { PropType } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';
import { CloseIcon } from 'tdesign-icons-vue-next';

Expand All @@ -13,7 +14,11 @@ const { prefix } = config;

export default defineComponent({
name: `${prefix}-image`,
props,
props: {
...props,
// 非对外暴露参数
onClick: Function as PropType<(e: MouseEvent) => void>,
},
setup(props, context) {
const imageClass = usePrefixClass('image');
const renderTNodeJSX = useTNodeJSX();
Expand Down Expand Up @@ -101,7 +106,7 @@ export default defineComponent({

return () => {
return (
<div class={imageClasses.value}>
<div class={imageClasses.value} onClick={props.onClick}>
{maskContent.value}
<picture>
{props.srcset &&
Expand Down
1 change: 1 addition & 0 deletions src/upload/demos/multiple.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<t-upload
:default-files="files"
multiple
draggable
:max="10"
:grid-config="gridConfig"
:action="action"
Expand Down
148 changes: 148 additions & 0 deletions src/upload/hooks/useDrag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { ref, Ref } from 'vue';
import { TdUploadProps, UploadFile } from '../type';

export default function useDrag(
props: TdUploadProps,
setUploadValue: any,
uploadClass: any,
listRef: Ref<HTMLElement | undefined>,
) {
const dragIndex = ref<number>(-1);
let pendingTargetIndex = -1;

// 缓存网格布局信息
let baseData = {
itemWidth: 0,
itemHeight: 0,
columns: 0,
wrapLeft: 0,
wrapTop: 0,
};

// 生成文件唯一ID,用于拖拽排序识别
const getFileId = (file: UploadFile) => {
if ((file as any).__uid) return (file as any).__uid;
const uid = `u_${Math.random().toString(36).slice(2, 9)}`;
(file as any).__uid = uid;
return uid;
};

const updateDragBaseData = () => {
const listEl = listRef.value;
const itemEl = listEl?.querySelector(`.${uploadClass.value}__item`);
if (listEl && itemEl) {
const rect = listEl.getBoundingClientRect();
const itemWidth = (itemEl as HTMLElement).offsetWidth;
const itemHeight = (itemEl as HTMLElement).offsetHeight;
const style = window.getComputedStyle(listEl);
const gapX = parseInt(style.columnGap || style.gap, 10) || 8;
const gapY = parseInt(style.rowGap || style.gap, 10) || 16;
const paddingLeft = parseInt(style.paddingLeft, 10) || 0;
const paddingTop = parseInt(style.paddingTop, 10) || 0;

baseData = {
itemWidth: itemWidth + gapX,
itemHeight: itemHeight + gapY,
columns:
Math.round(
(rect.width - paddingLeft - (parseInt(style.paddingRight, 10) || 0) + gapX) / (itemWidth + gapX),
) || 4,
wrapLeft: rect.left + paddingLeft,
wrapTop: rect.top + paddingTop,
};
}
};

const performSort = (index: number, displayFiles: UploadFile[], e: any) => {
if (index === -1 || index === dragIndex.value) {
pendingTargetIndex = -1;
return;
}
if (index === pendingTargetIndex) return;

pendingTargetIndex = index;
const sourceIdx = dragIndex.value;
const newFiles = [...displayFiles];

if (sourceIdx === -1 || sourceIdx === index) return;

const [item] = newFiles.splice(sourceIdx, 1);
newFiles.splice(index, 0, item);

setUploadValue(newFiles, { e, trigger: 'sort', index, file: item, files: newFiles });
if (dragIndex.value !== -1) {
dragIndex.value = index;
}
};

// 根据网格坐标计算目标索引
const onTouchmove = (e: TouchEvent, displayFiles: UploadFile[]) => {
if (!props.draggable || dragIndex.value === -1) return;
if (e.cancelable) e.preventDefault();

const touch = e.touches[0];
const { itemWidth, itemHeight, columns, wrapLeft, wrapTop } = baseData;

const x = touch.clientX - wrapLeft;
const y = touch.clientY - wrapTop;

let curX = Math.floor(x / itemWidth);
const curY = Math.floor(y / itemHeight);

// 将 X 限制在列范围内,确保在行边界内滑动
curX = Math.max(0, Math.min(columns - 1, curX));

let targetIndex = curX + columns * curY;
targetIndex = Math.max(0, Math.min(displayFiles.length - 1, targetIndex));

if (targetIndex !== dragIndex.value) {
const targetX = targetIndex % columns;
const targetY = Math.floor(targetIndex / columns);
const sourceX = dragIndex.value % columns;
const sourceY = Math.floor(dragIndex.value / columns);

const xProgress = x / itemWidth - targetX;
const yProgress = y / itemHeight - targetY;

let shouldSort = false;
if (targetY !== sourceY) {
shouldSort = targetY > sourceY ? yProgress > 0.5 : yProgress < 0.5;
} else {
shouldSort = targetX > sourceX ? xProgress > 0.5 : xProgress < 0.5;
}

if (shouldSort) {
performSort(targetIndex, displayFiles, e);
} else if (pendingTargetIndex !== -1) {
pendingTargetIndex = -1;
}
}
};

const resetDrag = () => {
dragIndex.value = -1;
pendingTargetIndex = -1;
};

return {
onDragstart: (e: DragEvent, index: number) => {
if (!props.draggable) return;
dragIndex.value = index;
updateDragBaseData();
},
onDragover: (e: DragEvent, index: number, files: any) => {
if (!props.draggable || dragIndex.value === -1) return;
e.preventDefault();
performSort(index, files, e);
},
onDragend: resetDrag,
onTouchstart: (e: TouchEvent, index: number) => {
if (!props.draggable) return;
dragIndex.value = index;
updateDragBaseData();
},
onTouchmove,
dragIndex,
getFileId,
};
}
25 changes: 22 additions & 3 deletions src/upload/hooks/useUpload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { computed, h, ref, toRefs, ComputedRef } from 'vue';
import { computed, h, ref, toRefs, ComputedRef, Ref } from 'vue';
import { isFunction, isString } from 'lodash-es';
import { SizeLimitObj, TdUploadProps, UploadChangeContext, UploadFile, UploadRemoveContext } from '../type';
import useVModel from '../../hooks/useVModel';
Expand All @@ -20,12 +20,30 @@ import { getFileList, getFileUrlByFileRaw } from '../../_common/js/upload/utils'
// @ts-ignore
export type ValidateParams = Parameters<TdUploadProps['onValidate']>[0];

export default function useUpload(props: TdUploadProps) {
export interface UseUpload {
toUploadFiles: Ref<UploadFile[]>;
uploadValue: Ref<UploadFile[]>;
displayFiles: ComputedRef<UploadFile[]>;
sizeOverLimitMessage: Ref<string>;
uploading: Ref<boolean>;
inputRef: Ref<HTMLInputElement | undefined>;
disabled: Ref<boolean | undefined>;
xhrReq: Ref<{ files: UploadFile[]; xhrReq: XMLHttpRequest }[]>;
uploadFilePercent: (params: { file: UploadFile; percent: number }) => void;
uploadFiles: (toFiles?: UploadFile[]) => void;
onFileChange: (files: File[]) => void;
onNormalFileChange: (e: Event) => void;
onInnerRemove: (p: UploadRemoveContext) => void;
cancelUpload: (context?: { file?: UploadFile; e?: MouseEvent }) => void;
setUploadValue: (value: UploadFile[], context: UploadChangeContext) => void;
}

export default function useUpload(props: TdUploadProps): UseUpload {
const inputRef = ref<HTMLInputElement>();
const { disabled, autoUpload, isBatchUpload, multiple, files, modelValue, defaultFiles } = toRefs(props);
// @ts-ignore
const [uploadValue, setUploadValue] = useVModel(files, modelValue, defaultFiles.value, props.onChange, 'files');
const xhrReq = ref<{ files: UploadFile[]; xhrReq: XMLHttpRequest }[]>([]);
const xhrReq: UseUpload['xhrReq'] = ref([]);
const toUploadFiles = ref<UploadFile[]>([]);
const sizeOverLimitMessage = ref('');

Expand Down Expand Up @@ -385,5 +403,6 @@ export default function useUpload(props: TdUploadProps) {
onNormalFileChange,
onInnerRemove,
cancelUpload,
setUploadValue,
};
}
13 changes: 11 additions & 2 deletions src/upload/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ export default {
type: Boolean,
default: undefined,
},
/** 是否启用拖拽上传 */
draggable: {
type: Boolean,
default: undefined,
},
/** 用于完全自定义文件列表界面内容(UI),单文件和多文件均有效 */
fileListDisplay: {
type: Function as PropType<TdUploadProps['fileListDisplay']>,
Expand Down Expand Up @@ -134,13 +139,13 @@ export default {
sizeLimit: {
type: [Number, Object] as PropType<TdUploadProps['sizeLimit']>,
},
/** 是否在同一个请求中上传全部文件,默认一个请求上传一个文件。多文件上传时有效 */
uploadAllFilesInOneRequest: Boolean,
/** 是否在请求时间超过 300ms 后显示模拟进度。上传进度有模拟进度和真实进度两种。一般大小的文件上传,真实的上传进度只有 0 和 100,不利于交互呈现,因此组件内置模拟上传进度。真实上传进度一般用于大文件上传 */
useMockProgress: {
type: Boolean,
default: true,
},
/** 是否在同一个请求中上传全部文件,默认一个请求上传一个文件。多文件上传时有效 */
uploadAllFilesInOneRequest: Boolean,
/** 已上传文件列表,同 `files`。TS 类型:`UploadFile` */
value: {
type: Array as PropType<TdUploadProps['value']>,
Expand All @@ -163,6 +168,10 @@ export default {
onChange: Function as PropType<TdUploadProps['onChange']>,
/** 点击上传区域时触发 */
onClickUpload: Function as PropType<TdUploadProps['onClickUpload']>,
/** 拖拽开始时触发 */
onDrag: Function as PropType<TdUploadProps['onDrag']>,
/** 拖拽结束后触发,返回上传的文件列表(拖拽后的文件顺序) */
onDrop: Function as PropType<TdUploadProps['onDrop']>,
/** 上传失败后触发。`response` 指接口响应结果,`response.error` 会作为错误文本提醒。如果希望判定为上传失败,但接口响应数据不包含 `error` 字段,可以使用 `formatResponse` 格式化 `response` 数据结构。如果是多文件多请求上传场景,请到事件 `onOneFileFail` 中查看 `response` */
onFail: Function as PropType<TdUploadProps['onFail']>,
/** 多文件/图片场景下,单个文件上传失败后触发,如果一个请求上传一个文件,则会触发多次。单文件/图片不会触发 */
Expand Down
Loading
Loading