Files
UI/packages/skin/ui/src/upload.ts
T
2026-10-26 11:44:34 +08:00

88 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { h } from '@ui-arch/frame';
import type { UploadProps } from './types.js';
/*** 文件选择:点击或拖拽入列,已选清单可逐个移除;只负责选文件并回传,不内置上传请求(网络层交给调用方) ***/
export function upload(props: UploadProps): HTMLElement {
// —— 状态集中声明 ——
const files: File[] = []; // 已选文件(顺序即选择顺序)
const input = h('input', { type: 'file', class: 'ua-upload-input' }) as HTMLInputElement; // 隐藏的原生选择器
if (props.accept) input.accept = props.accept;
if (props.multiple) input.multiple = true;
const listEl = h('div', { class: 'ua-upload-list' }) as HTMLElement; // 已选文件清单
/*** 按字节数换算可读体积 ***/
const size = (bytes: number) =>
bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
/*** 移除指定下标的文件 ***/
const remove = (i: number) => {
files.splice(i, 1);
sync();
};
/*** 重绘清单并回传(选择/拖拽/移除都走这里,保证单一出口) ***/
const sync = () => {
listEl.replaceChildren(
...files.map((file, i) =>
h(
'div',
{ class: 'ua-upload-item' },
h('span', { class: 'ua-upload-name' }, file.name),
h('span', { class: 'ua-upload-size' }, size(file.size)),
h('button', { type: 'button', class: 'ua-upload-remove', 'aria-label': `移除 ${file.name}`, onclick: () => remove(i) }, '×'),
),
),
);
props.onChange?.(files.slice()); // 回传副本,避免调用方改动内部数组
};
/*** 追加文件:单选模式直接替换,多选模式追加并去重(同名同大小视为同一个) ***/
const add = (picked: FileList | File[]) => {
for (const file of Array.from(picked)) {
const dup = files.some((f) => f.name === file.name && f.size === file.size);
if (dup) continue; // 卫语句:重复文件跳过
if (props.multiple) files.push(file);
else files.splice(0, files.length, file); // 单选:只留最新一个
}
sync();
};
const zone = h(
'div',
{
class: props.disabled ? 'ua-upload-zone ua-upload-disabled' : 'ua-upload-zone',
tabindex: props.disabled ? '-1' : '0',
role: 'button',
'aria-label': '选择文件',
onclick: () => { if (!props.disabled) input.click(); },
onkeydown: (e: KeyboardEvent) => {
if (props.disabled) return; // 卫语句:禁用不响应
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); input.click(); } // 键盘等效点击
},
ondragover: (e: Event) => {
if (props.disabled) return; // 卫语句:禁用不接受拖拽
e.preventDefault();
(e.currentTarget as HTMLElement).classList.add('ua-upload-over');
},
ondragleave: (e: Event) => (e.currentTarget as HTMLElement).classList.remove('ua-upload-over'),
ondrop: (e: Event) => {
const ev = e as DragEvent;
ev.preventDefault();
(ev.currentTarget as HTMLElement).classList.remove('ua-upload-over');
if (props.disabled || !ev.dataTransfer?.files?.length) return; // 卫语句:无文件或已禁用
add(ev.dataTransfer.files);
},
},
h('span', { class: 'ua-upload-icon', 'aria-hidden': 'true' }, '⤒'),
h('span', { class: 'ua-upload-hint' }, props.hint ?? '点击或拖拽文件到此处'),
input,
);
input.addEventListener('change', () => {
if (input.files?.length) add(input.files);
input.value = ''; // 清空以便重复选择同一文件也能触发 change
});
return h('div', { class: 'ua-upload' }, zone, listEl);
}