Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
18 changes: 12 additions & 6 deletions src/upload/__test__/__snapshots__/demo.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,8 @@ exports[`Upload > Upload mobileVue demo works fine 1`] = `
>

<div
class="t-upload__item"
class="t-upload__item t-upload__item-drag"
draggable="true"
>
<div
class="t-image t-image--round t-upload__image"
Expand Down Expand Up @@ -404,7 +405,8 @@ exports[`Upload > Upload mobileVue demo works fine 1`] = `
<!---->
</div>
<div
class="t-upload__item"
class="t-upload__item t-upload__item-drag"
draggable="true"
>
<div
class="t-image t-image--round t-upload__image"
Expand Down Expand Up @@ -472,7 +474,8 @@ exports[`Upload > Upload mobileVue demo works fine 1`] = `
</svg>
</div>
<div
class="t-upload__item"
class="t-upload__item t-upload__item-drag"
draggable="true"
>
<div
class="t-image t-image--round t-upload__image"
Expand Down Expand Up @@ -1230,7 +1233,8 @@ exports[`Upload > Upload multipleVue demo works fine 1`] = `
>

<div
class="t-upload__item"
class="t-upload__item t-upload__item-drag"
draggable="true"
>
<div
class="t-image t-image--round t-upload__image"
Expand Down Expand Up @@ -1279,7 +1283,8 @@ exports[`Upload > Upload multipleVue demo works fine 1`] = `
<!---->
</div>
<div
class="t-upload__item"
class="t-upload__item t-upload__item-drag"
draggable="true"
>
<div
class="t-image t-image--round t-upload__image"
Expand Down Expand Up @@ -1347,7 +1352,8 @@ exports[`Upload > Upload multipleVue demo works fine 1`] = `
</svg>
</div>
<div
class="t-upload__item"
class="t-upload__item t-upload__item-drag"
draggable="true"
>
<div
class="t-image t-image--round t-upload__image"
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