iter(g82): 文件选择补类型体积数量校验
This commit is contained in:
@@ -458,6 +458,8 @@ export interface UploadProps {
|
||||
accept?: string; // 接受的类型(同原生 accept),如 'image/*,.pdf'
|
||||
multiple?: boolean; // 允许多选;缺省 false(单选覆盖)
|
||||
hint?: string; // 提示文案;缺省「点击或拖拽文件到此处」
|
||||
maxSize?: number; // 单文件体积上限(字节);超限丢弃并在提示行说明,缺省不限
|
||||
maxCount?: number; // multiple 时的文件数上限;超出部分丢弃并提示,缺省不限
|
||||
disabled?: boolean; // 禁用:不可点选、不接受拖拽
|
||||
onChange?: (files: File[]) => void; // 文件变化回调(选择/拖拽/移除后触发,传副本)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { h } from '@ui-arch/frame';
|
||||
import type { UploadProps } from './types.js';
|
||||
|
||||
/*** 文件选择:点击或拖拽入列,已选清单可逐个移除;只负责选文件并回传,不内置上传请求(网络层交给调用方) ***/
|
||||
/*** 文件选择:点击或拖拽入列,已选清单可逐个移除;accept / maxSize / maxCount 对拖拽同样生效,被拒文件在提示行说明原因;不内置上传请求(网络层交给调用方) ***/
|
||||
export function upload(props: UploadProps): HTMLElement {
|
||||
// —— 状态集中声明 ——
|
||||
const files: File[] = []; // 已选文件(顺序即选择顺序)
|
||||
@@ -9,11 +9,27 @@ export function upload(props: UploadProps): HTMLElement {
|
||||
if (props.accept) input.accept = props.accept;
|
||||
if (props.multiple) input.multiple = true;
|
||||
const listEl = h('div', { class: 'ua-upload-list' }) as HTMLElement; // 已选文件清单
|
||||
const tipEl = h('p', { class: 'ua-upload-tip', role: 'status' }) as HTMLElement; // 拒收原因播报行(类型/体积/数量超限)
|
||||
tipEl.hidden = true;
|
||||
|
||||
/*** 按字节数换算可读体积 ***/
|
||||
const size = (bytes: number) =>
|
||||
bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1048576).toFixed(1)} MB`;
|
||||
|
||||
/*** 是否命中 accept 规则:支持 .ext / type/subtype / type/* 三种写法,缺省放行 ***/
|
||||
const accepts = (file: File): boolean => {
|
||||
const rule = props.accept;
|
||||
if (!rule) return true; // 卫语句:未限制类型
|
||||
const name = file.name.toLowerCase();
|
||||
return rule.split(',').some((raw) => {
|
||||
const t = raw.trim().toLowerCase();
|
||||
if (!t) return false; // 卫语句:空规则跳过
|
||||
if (t.startsWith('.')) return name.endsWith(t); // 扩展名形式
|
||||
if (t.endsWith('/*')) return file.type.startsWith(t.slice(0, -1)); // 大类型通配
|
||||
return file.type === t; // 完整 MIME
|
||||
});
|
||||
};
|
||||
|
||||
/*** 移除指定下标的文件 ***/
|
||||
const remove = (i: number) => {
|
||||
files.splice(i, 1);
|
||||
@@ -21,7 +37,7 @@ export function upload(props: UploadProps): HTMLElement {
|
||||
};
|
||||
|
||||
/*** 重绘清单并回传(选择/拖拽/移除都走这里,保证单一出口) ***/
|
||||
const sync = () => {
|
||||
const sync = (tip = '') => {
|
||||
listEl.replaceChildren(
|
||||
...files.map((file, i) =>
|
||||
h(
|
||||
@@ -33,18 +49,24 @@ export function upload(props: UploadProps): HTMLElement {
|
||||
),
|
||||
),
|
||||
);
|
||||
tipEl.textContent = tip; // 本轮拒收原因(空串即无)
|
||||
tipEl.hidden = !tip;
|
||||
props.onChange?.(files.slice()); // 回传副本,避免调用方改动内部数组
|
||||
};
|
||||
|
||||
/*** 追加文件:单选模式直接替换,多选模式追加并去重(同名同大小视为同一个) ***/
|
||||
const add = (picked: FileList | File[]) => {
|
||||
let tip = ''; // 本轮第一条拒收原因,覆盖式提示(避免刷屏)
|
||||
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); // 单选:只留最新一个
|
||||
if (!accepts(file)) { tip = `「${file.name}」类型不符:${props.accept}`; continue; }
|
||||
if (props.maxSize && file.size > props.maxSize) { tip = `「${file.name}」超过 ${size(props.maxSize)} 上限`; continue; }
|
||||
if (files.some((f) => f.name === file.name && f.size === file.size)) continue; // 卫语句:重复文件跳过
|
||||
if (props.multiple) {
|
||||
if (props.maxCount && files.length >= props.maxCount) { tip = `最多选 ${props.maxCount} 个文件`; continue; }
|
||||
files.push(file);
|
||||
} else files.splice(0, files.length, file); // 单选:只留最新一个
|
||||
}
|
||||
sync();
|
||||
sync(tip);
|
||||
};
|
||||
|
||||
const zone = h(
|
||||
@@ -83,5 +105,5 @@ export function upload(props: UploadProps): HTMLElement {
|
||||
input.value = ''; // 清空以便重复选择同一文件也能触发 change
|
||||
});
|
||||
|
||||
return h('div', { class: 'ua-upload' }, zone, listEl);
|
||||
return h('div', { class: 'ua-upload' }, zone, listEl, tipEl);
|
||||
}
|
||||
|
||||
@@ -1989,6 +1989,16 @@
|
||||
"t": "string",
|
||||
"d": "提示文案;缺省「点击或拖拽文件到此处」"
|
||||
},
|
||||
{
|
||||
"n": "maxSize",
|
||||
"t": "number",
|
||||
"d": "单文件体积上限(字节);超限丢弃并在提示行说明,缺省不限"
|
||||
},
|
||||
{
|
||||
"n": "maxCount",
|
||||
"t": "number",
|
||||
"d": "multiple 时的文件数上限;超出部分丢弃并提示,缺省不限"
|
||||
},
|
||||
{
|
||||
"n": "disabled",
|
||||
"t": "boolean",
|
||||
@@ -2003,11 +2013,10 @@
|
||||
],
|
||||
"example": "upload({ multiple: true, accept: 'image/*', hint: '拖拽图片到此处', onChange: (files) => console.log(files.map((f) => f.name)) })",
|
||||
"steps": [
|
||||
"组件只做「选文件 + 回传」,onChange 里拿 File[] 自行决定上传接口(fetch/FormData 都行)",
|
||||
"multiple: true 允许多选,重复的文件(同名同大小)自动跳过;单选模式下新选择覆盖旧文件",
|
||||
"accept 透传原生 accept 限制可选类型,但仍需在 onChange 里二次校验扩展名",
|
||||
"键盘:区域可 Tab 聚焦,Enter/空格 打开选择器;拖拽时区域高亮,拖出恢复",
|
||||
"disabled 时不可点选、不接受拖拽;已选清单的 × 会触发 onChange 回传剩余文件"
|
||||
"把 upload 放进表单,accept 限定类型、multiple 允许多选",
|
||||
"需要拦体积就给 maxSize(字节),限制数量给 maxCount",
|
||||
"被拒的文件不会进清单,原因显示在下方提示行(拖拽与点选走同一套校验)",
|
||||
"onChange 拿到的 File[] 是副本,自行发请求上传(组件不内置网络层)"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2317,6 +2317,11 @@ button.ua-step-dot:hover {
|
||||
}
|
||||
|
||||
/* —— 文件选择 —— */
|
||||
.ua-upload-tip {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--ua-color-danger);
|
||||
}
|
||||
.ua-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user