agentcss 0.16.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/02-primitives/choice/choice.css +31 -2
- package/02-primitives/input/input.css +14 -0
- package/02-primitives/progress/progress.css +56 -0
- package/02-primitives/skeleton/skeleton.css +18 -0
- package/02-primitives/switch/switch.css +61 -2
- package/02-primitives/timeline/README.md +70 -0
- package/02-primitives/timeline/demo.html +161 -0
- package/02-primitives/timeline/timeline.css +152 -0
- package/02-primitives/upload/README.md +142 -0
- package/02-primitives/upload/demo.html +318 -0
- package/02-primitives/upload/upload.css +490 -0
- package/02-primitives/upload/upload.js +676 -0
- package/03-patterns/drawer/drawer.js +30 -5
- package/04-recipes/app-shell/README.md +272 -0
- package/04-recipes/app-shell/app-shell.css +588 -0
- package/04-recipes/app-shell/demo.html +322 -0
- package/04-recipes/app-shell/shell-nav.js +288 -0
- package/04-recipes/table/table.css +22 -0
- package/AGENTS.md +8 -0
- package/API.md +4 -0
- package/CHANGELOG.md +736 -0
- package/CONTRIBUTING.md +22 -1
- package/README.md +17 -5
- package/README.zh-CN.md +16 -6
- package/START-HERE.md +12 -7
- package/ai/START-HERE.md +37 -3
- package/ai/behaviors.json +32 -0
- package/ai/cli.js +183 -25
- package/ai/components.json +185 -7
- package/ai/components.meta.json +13 -0
- package/ai/pitfalls-writing.json +119 -0
- package/ai/pitfalls.json +193 -0
- package/dist/02-primitives/choice/choice.css +1 -1
- package/dist/02-primitives/input/input.css +1 -1
- package/dist/02-primitives/progress/progress.css +1 -1
- package/dist/02-primitives/skeleton/skeleton.css +1 -1
- package/dist/02-primitives/switch/switch.css +1 -1
- package/dist/02-primitives/timeline/timeline.css +1 -0
- package/dist/02-primitives/upload/upload.css +1 -0
- package/dist/02-primitives/upload/upload.js +515 -0
- package/dist/03-patterns/drawer/drawer.js +14 -4
- package/dist/04-recipes/app-shell/app-shell.css +1 -0
- package/dist/04-recipes/app-shell/shell-nav.js +173 -0
- package/dist/04-recipes/table/table.css +1 -1
- package/dist/COSTS.md +12 -10
- package/docs/ACCESSIBILITY.md +20 -7
- package/docs/AI-CONTRACT.md +2 -0
- package/docs/BUDGET.md +179 -0
- package/docs/CONVENTIONS.md +3 -0
- package/docs/STABILITY.md +126 -0
- package/esm/index.mjs +2 -0
- package/esm/upload.mjs +7 -0
- package/index.html +5 -2
- package/llms-full.txt +153 -0
- package/llms.txt +55 -0
- package/package.json +4 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# upload · 文件上传
|
|
2
|
+
|
|
3
|
+
> 本库表单控件一直是"缺一块"的状态:input / select / choice / switch 都有,
|
|
4
|
+
> 唯独没有"选文件"。于是每个项目自己糊一个 `<input type=file>`,
|
|
5
|
+
> 它长得和全站不一样,而且拖放区、多文件、失败原因全缺。
|
|
6
|
+
|
|
7
|
+
## 两层形态
|
|
8
|
+
|
|
9
|
+
| 形态 | 需要 JS | 得到什么 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `.upload` | **不需要** | 原生 `<input type=file>` + 美化过的 `::file-selector-button`;原生文件名回显、表单提交、`accept` 过滤全走浏览器 |
|
|
12
|
+
| `.upload--drop` | 需要 | 在上面加一层拖放区 + 文件列表 + 大小/类型/数量校验 + 移除 |
|
|
13
|
+
|
|
14
|
+
`.upload--drop` 是**加**在基础形态上的一层,不是替代品。原生 `<input>` 始终在 DOM 里、
|
|
15
|
+
始终可聚焦、始终在表单里。
|
|
16
|
+
|
|
17
|
+
## 用法
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<!-- ① 基础:零 JS -->
|
|
21
|
+
<link rel="stylesheet" href="upload.css">
|
|
22
|
+
<input class="upload__input" type="file" id="f1" name="statement" accept=".csv,.xlsx">
|
|
23
|
+
<label class="field__label" for="f1">导入对账单</label>
|
|
24
|
+
|
|
25
|
+
<!-- ② 增强:拖放区 + 列表 -->
|
|
26
|
+
<link rel="stylesheet" href="upload.css">
|
|
27
|
+
<script src="upload.js"></script>
|
|
28
|
+
|
|
29
|
+
<div class="upload upload--drop" data-upload data-max-size="20971520" data-max-files="5">
|
|
30
|
+
<input class="upload__input" type="file" id="f2" name="files" multiple
|
|
31
|
+
accept=".pdf,.xlsx" aria-describedby="f2-hint">
|
|
32
|
+
<!-- 拖放区是 label for:点它 = 原生打开选择器,这一步不依赖 JS。
|
|
33
|
+
⚠️ 不要给它加 aria-hidden:加了它就从无障碍树里消失,
|
|
34
|
+
input 只能靠外面的 .field__label 拿名字 —— 少写一个就是 axe critical。
|
|
35
|
+
(0.18.0 的 axe 实测:对比块里漏了 .field__label ⇒ label 违规。) -->
|
|
36
|
+
<label class="upload__dropzone" for="f2">
|
|
37
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
38
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
39
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
40
|
+
</svg>
|
|
41
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
42
|
+
<span class="upload__hint">PDF / XLSX,单个不超过 20 MB</span>
|
|
43
|
+
</label>
|
|
44
|
+
<ul class="upload__list"></ul>
|
|
45
|
+
<span class="upload__status" role="status" aria-live="polite"></span>
|
|
46
|
+
</div>
|
|
47
|
+
<p class="field__hint" id="f2-hint">可以一次选多个</p>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 声明式属性(写在 `.upload` 根上)
|
|
51
|
+
|
|
52
|
+
| 属性 | 含义 | 默认 |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `data-upload` | 自动初始化标记 | — |
|
|
55
|
+
| `data-max-size` | 单文件上限(**字节**) | 0 = 不限 |
|
|
56
|
+
| `data-max-files` | 文件数上限 | 0 = 不限(`multiple` 时)/1(单选时) |
|
|
57
|
+
| `data-state` | **输出**:`dragover` 时存在 | — |
|
|
58
|
+
| `data-count` | **输出**:当前文件数(供"没选文件就不让提交"用) | `"0"` |
|
|
59
|
+
|
|
60
|
+
## JS API
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
var u = document.getElementById('f2').closest('.upload').uploadInstance;
|
|
64
|
+
// 或 new Upload(root, { maxSize: 20 * 1024 * 1024 })
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| 成员 | 说明 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `add(files[, dirCount])` | 收下一批文件(= 拖放入口)。**走同一条校验**,返回是否至少收下一个 |
|
|
70
|
+
| `removeAt(i)` | 移除第 i 个;返回 false 表示浏览器不支持写 `input.files` |
|
|
71
|
+
| `setProgress(i, pct)` | `0–99` 上传中;`-1` 或 `>=100` 标记完成并去掉进度条 |
|
|
72
|
+
| `render()` | 从 `input.files` 重画列表(你手动改了 `files` 之后调它) |
|
|
73
|
+
| `destroy()` | 解绑全部监听 |
|
|
74
|
+
|
|
75
|
+
### 事件
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
document.addEventListener('fl-upload', function (e) {
|
|
79
|
+
e.target // .upload 根元素(认它,别认事件名 —— 嵌套组件会冒泡到一起)
|
|
80
|
+
e.detail // { files: File[], added: number, removed?: File, rejected: {file, reason, text}[] }
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`reason` 取值:`size` / `type` / `count` / `dir` / `unsupported`。
|
|
85
|
+
|
|
86
|
+
> ⚠️ **程序性改 `input.files` 不会触发原生 `change`**(这是规范,不是本库的选择)。
|
|
87
|
+
> 本组件不补发假 `change`(那会形成"渲染两遍"的循环),
|
|
88
|
+
> 所以 **HTML 表单校验请监听 `fl-upload`,不要监听 `change`**。
|
|
89
|
+
|
|
90
|
+
## 三条硬约定
|
|
91
|
+
|
|
92
|
+
### 1 · `input.files` 是唯一真值,列表是镜像
|
|
93
|
+
|
|
94
|
+
拖放不是"第二条通道":它把文件塞进 `input.files`,然后重画。
|
|
95
|
+
代价是**再次选择/拖入会替换整个列表**(与原生 `<input multiple>` 一致)。
|
|
96
|
+
不做"下一次追加"—— 否则同一个控件里存在两种累加规则,迟早打架。
|
|
97
|
+
|
|
98
|
+
### 2 · 校验只有一份实现
|
|
99
|
+
|
|
100
|
+
拖放与原生选择都调 `_validate`。写成两遍的后果不是"难看",是它们会**漂移**:
|
|
101
|
+
某天给拖放加了大小限制,忘了给原生选择加 ⇒ 同一页面上,
|
|
102
|
+
拖进去的被拒、点选进去的通过。这种 bug 只有用户会碰到。
|
|
103
|
+
|
|
104
|
+
`accept` 必须二次校验:它只约束**文件选择器**,拖放完全绕过它。
|
|
105
|
+
只写 `accept=".pdf"` 而拖入 `.exe` 时,浏览器不会拦 —— 这是规范行为。
|
|
106
|
+
|
|
107
|
+
### 3 · `DataTransfer` 不存在时,拖放明确拒绝
|
|
108
|
+
|
|
109
|
+
老 Safari(< 14.1)没有 `new DataTransfer()` ⇒ `input.files` 不可写。
|
|
110
|
+
此时照样渲染列表、照样派发事件,用户会看到"文件进去了",点提交却什么都没有,
|
|
111
|
+
**且不报错**。所以本组件直接提示"当前浏览器不支持拖放,请点选择文件",不接收。
|
|
112
|
+
|
|
113
|
+
## 无障碍
|
|
114
|
+
|
|
115
|
+
- **状态不只靠颜色**:左侧图形(空心圆 / 勾 / 叹号)+ 右侧文本("上传中 42%" / "2.4 MB" / "超过 20 MB")。
|
|
116
|
+
灰度打印、色盲、强制色三种条件下信息都不丢。
|
|
117
|
+
- **播报走单独的 `role="status"`**,不给 `<ul>` 加 `aria-live` ——
|
|
118
|
+
后者会把"进度 42% → 43%"每 200ms 打断一次。
|
|
119
|
+
同一句话连续播两次时读屏不再读,所以 `announce()` 先清空再延迟写入。
|
|
120
|
+
- **移除按钮的 `aria-label` 带文件名**("移除 2025年度审计报告.pdf"),
|
|
121
|
+
否则读屏用户听到一排"移除、移除、移除"。
|
|
122
|
+
- 拖放区是 `<label for>`,本身**不进 Tab 序列**(焦点属于 input);
|
|
123
|
+
聚焦时焦点环画在 dropzone 上,视觉上看到的是"这一块被选中了"。
|
|
124
|
+
- **可访问名字来自两处**:`.field__label`("上传审计底稿")与 dropzone 里的文字。
|
|
125
|
+
读屏会连着念,略啰嗦,但这是**安全的冗余** —— 只用 dropzone、不写 `.field__label`
|
|
126
|
+
的页面也有名字。反过来(dropzone 加 `aria-hidden`、靠 `.field__label` 给名)
|
|
127
|
+
看起来更干净,代价是少写一个 label 就变成 axe critical,而且没有任何门禁拦得住。
|
|
128
|
+
- 强制色模式下**不需要额外 media query**:边界全是 `border`(会自动变 CanvasText 实线),
|
|
129
|
+
图标全是内联 SVG + `currentColor`。这是 0.17.0 逐页实拍定下的两条规律。
|
|
130
|
+
|
|
131
|
+
## 何时**不该**用
|
|
132
|
+
|
|
133
|
+
- **需要断点续传 / 分片上传** ⇒ 这不是纯前端控件能解决的,本组件只负责"选与校验",
|
|
134
|
+
传输交给你的上传库。它不发送任何请求。
|
|
135
|
+
- **单个文件且没有校验** ⇒ 用基础形态(`.upload__input` 一行)就够了,不要套拖放区。
|
|
136
|
+
- **图片裁剪/预览** ⇒ 需要 canvas 预览与交互,超出本组件范围。
|
|
137
|
+
|
|
138
|
+
## 相关
|
|
139
|
+
|
|
140
|
+
- `progress` —— 列表里的进度条复用它;没引入时退化成空槽,不报错
|
|
141
|
+
- `field`(在 `input` 里)—— `field__label` / `field__hint` / `field__error` 的外层结构
|
|
142
|
+
- `form-validation` —— 提交前校验;错误提示用 `field__error` + `aria-describedby`
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<link rel="icon" href="data:,">
|
|
7
|
+
<title>文件上传 · upload</title>
|
|
8
|
+
<link rel="stylesheet" href="../../01-tokens/tokens.css">
|
|
9
|
+
<link rel="stylesheet" href="../../01-tokens/typography.css">
|
|
10
|
+
<link rel="stylesheet" href="../../01-tokens/page.css">
|
|
11
|
+
<link rel="stylesheet" href="../input/input.css">
|
|
12
|
+
<link rel="stylesheet" href="../progress/progress.css">
|
|
13
|
+
<link rel="stylesheet" href="upload.css">
|
|
14
|
+
<script src="upload.js"></script>
|
|
15
|
+
<style>
|
|
16
|
+
.demo {
|
|
17
|
+
background: var(--surface);
|
|
18
|
+
border: 1px solid var(--border-decor);
|
|
19
|
+
border-radius: var(--r-md);
|
|
20
|
+
padding: var(--sp-4);
|
|
21
|
+
margin-bottom: var(--sp-3);
|
|
22
|
+
}
|
|
23
|
+
.demo + .note { margin-top: 0; }
|
|
24
|
+
/* 演示用:窄槽 */
|
|
25
|
+
.cq-narrow {
|
|
26
|
+
max-width: 240px;
|
|
27
|
+
padding: var(--sp-3);
|
|
28
|
+
border: 1px dashed var(--border-decor);
|
|
29
|
+
border-radius: var(--r-md);
|
|
30
|
+
}
|
|
31
|
+
/* 演示用:把"上传中"的进度定住,视觉基线才不会拍到动画中间帧 */
|
|
32
|
+
.frozen { margin-top: var(--sp-2); }
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body class="page">
|
|
36
|
+
<main class="wrap">
|
|
37
|
+
|
|
38
|
+
<h1>文件上传 · upload</h1>
|
|
39
|
+
<p class="lede">上传是「导入 CSV / 附一份财报 / 传凭证」的必经入口。这个组件的核心不在拖放区好看,
|
|
40
|
+
而在于:<b>JS 没加载时它仍然是能用的</b>,以及文件有没有真的进到表单里。</p>
|
|
41
|
+
|
|
42
|
+
<h2>1 · 基础形态:零 JS 也能用</h2>
|
|
43
|
+
<div class="demo">
|
|
44
|
+
<div class="field">
|
|
45
|
+
<label class="field__label" for="u-basic">导入对账单</label>
|
|
46
|
+
<input class="upload__input" type="file" id="u-basic" name="statement"
|
|
47
|
+
accept=".csv,.xlsx" aria-describedby="u-basic-hint">
|
|
48
|
+
<p class="field__hint" id="u-basic-hint">支持 CSV / XLSX,单个不超过 20 MB</p>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
<p class="note">这一块<b>没有任何 JS</b>:浏览器原生的"选择文件"按钮经
|
|
52
|
+
<code>::file-selector-button</code> 美化,选完立刻在按钮右侧回显文件名 ——
|
|
53
|
+
不需要脚本读 <code>files</code> 再写进 DOM。表单提交、<code>FormData</code>、
|
|
54
|
+
<code>accept</code> 过滤全走原生路径。</p>
|
|
55
|
+
|
|
56
|
+
<h2>2 · 增强形态:拖放区 + 文件列表</h2>
|
|
57
|
+
<div class="demo">
|
|
58
|
+
<div class="field">
|
|
59
|
+
<label class="field__label" for="u-drop">上传审计底稿</label>
|
|
60
|
+
<div class="upload upload--drop" id="u-drop-wrap" data-upload data-max-size="20971520">
|
|
61
|
+
<input class="upload__input" type="file" id="u-drop" name="workpapers" multiple
|
|
62
|
+
accept=".pdf,.xlsx,.csv" aria-describedby="u-drop-hint">
|
|
63
|
+
<label class="upload__dropzone" for="u-drop">
|
|
64
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
65
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
66
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
67
|
+
</svg>
|
|
68
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
69
|
+
<span class="upload__hint">PDF / XLSX / CSV,单个不超过 20 MB</span>
|
|
70
|
+
</label>
|
|
71
|
+
<ul class="upload__list"></ul>
|
|
72
|
+
<span class="upload__status" role="status" aria-live="polite"></span>
|
|
73
|
+
</div>
|
|
74
|
+
<p class="field__hint" id="u-drop-hint">可以一次选多个;再次拖入会<b>替换</b>整个列表</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<p class="note">拖放区是一个 <code><label for></code>:点它 = 原生打开选择器,
|
|
78
|
+
这一步不依赖 JS。脚本只负责三件事 —— 把拖入的 <code>File</code> 塞进
|
|
79
|
+
<code>input.files</code>、重画列表、派发 <code>fl-upload</code>。
|
|
80
|
+
所以 <code><form></code> 拿到的永远是真实文件,不是"组件自己记的一份清单"。</p>
|
|
81
|
+
|
|
82
|
+
<h2>3 · 上传中:进度既是图形也是文字</h2>
|
|
83
|
+
<div class="demo">
|
|
84
|
+
<ul class="upload__list">
|
|
85
|
+
<li class="upload__item" data-state="pending">
|
|
86
|
+
<span class="upload__mark">
|
|
87
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M8 1.7a6.3 6.3 0 1 0 0 12.6A6.3 6.3 0 0 0 8 1.7zM8 4.6V8l2.3 1.5"/></svg>
|
|
88
|
+
<span class="upload__status">待上传</span>
|
|
89
|
+
</span>
|
|
90
|
+
<span class="upload__name">2025年度审计报告.pdf</span>
|
|
91
|
+
<span class="upload__meta num">上传中 42%</span>
|
|
92
|
+
<span class="upload__progress">
|
|
93
|
+
<span class="progress"><span class="progress__bar" style="--progress:42%"></span></span>
|
|
94
|
+
</span>
|
|
95
|
+
<button class="upload__remove" type="button" aria-label="移除 2025年度审计报告.pdf">
|
|
96
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6"/></svg>
|
|
97
|
+
</button>
|
|
98
|
+
</li>
|
|
99
|
+
<li class="upload__item" data-state="done">
|
|
100
|
+
<span class="upload__mark">
|
|
101
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3.3 8.4l3.1 3.1 6.3-6.9"/></svg>
|
|
102
|
+
<span class="upload__status">已完成</span>
|
|
103
|
+
</span>
|
|
104
|
+
<span class="upload__name">应付账款明细.xlsx</span>
|
|
105
|
+
<span class="upload__meta num">2.4 MB</span>
|
|
106
|
+
<button class="upload__remove" type="button" aria-label="移除 应付账款明细.xlsx">
|
|
107
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6"/></svg>
|
|
108
|
+
</button>
|
|
109
|
+
</li>
|
|
110
|
+
<li class="upload__item" data-state="error">
|
|
111
|
+
<span class="upload__mark">
|
|
112
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M8 1.7a6.3 6.3 0 1 0 0 12.6A6.3 6.3 0 0 0 8 1.7zM8 4.9v3.4M8 11.1v.5"/></svg>
|
|
113
|
+
<span class="upload__status">失败</span>
|
|
114
|
+
</span>
|
|
115
|
+
<span class="upload__name">往来余额扫描件.pdf</span>
|
|
116
|
+
<span class="upload__meta num">超过 20 MB</span>
|
|
117
|
+
</li>
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
<p class="note">三个状态的差别<b>不只是颜色</b>:左侧图形(空心圆 / 勾 / 叹号)、
|
|
121
|
+
中间文件名的灰度、以及右侧那行文字("上传中 42%" / "2.4 MB" / "超过 20 MB")。
|
|
122
|
+
把这一块截图转成灰度打印,信息一条都不丢。</p>
|
|
123
|
+
|
|
124
|
+
<h2>4 · 校验拒绝:一个都没收进来</h2>
|
|
125
|
+
<div class="demo">
|
|
126
|
+
<div class="field">
|
|
127
|
+
<label class="field__label" for="u-reject">上传凭证</label>
|
|
128
|
+
<div class="upload upload--drop" id="u-reject-wrap" data-upload
|
|
129
|
+
data-max-size="20971520" data-max-files="3">
|
|
130
|
+
<input class="upload__input" type="file" id="u-reject" name="proofs" multiple
|
|
131
|
+
accept=".pdf,.jpg,.png" aria-describedby="u-reject-hint">
|
|
132
|
+
<label class="upload__dropzone" for="u-reject">
|
|
133
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
134
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
135
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
136
|
+
</svg>
|
|
137
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
138
|
+
<span class="upload__hint">PDF / JPG / PNG,单个不超过 20 MB,最多 3 个</span>
|
|
139
|
+
</label>
|
|
140
|
+
<ul class="upload__list"></ul>
|
|
141
|
+
<span class="upload__status" role="status" aria-live="polite"></span>
|
|
142
|
+
</div>
|
|
143
|
+
<p class="field__hint" id="u-reject-hint">下面这一组是脚本真实拖入 3 个不合规文件的结果</p>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
<p class="note">被拒的文件<b>不会进入 <code>input.files</code></b> ——
|
|
147
|
+
它们根本没被接收,所以提交时不会悄悄带上一个 30 MB 的扫描件。
|
|
148
|
+
拒绝原因写在文件名的位置,不是只把边框变红。</p>
|
|
149
|
+
|
|
150
|
+
<h2>5 · 紧凑档(侧栏 / 抽屉)</h2>
|
|
151
|
+
<div class="demo">
|
|
152
|
+
<div class="upload upload--drop upload--compact" data-upload>
|
|
153
|
+
<input class="upload__input" type="file" id="u-compact" name="att" multiple>
|
|
154
|
+
<label class="upload__dropzone" for="u-compact" aria-label="添加附件">
|
|
155
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
156
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
157
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
158
|
+
</svg>
|
|
159
|
+
<span class="upload__title">添加附件</span>
|
|
160
|
+
</label>
|
|
161
|
+
<ul class="upload__list"></ul>
|
|
162
|
+
<span class="upload__status" role="status" aria-live="polite"></span>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<h2>6 · 槽宽适配(看槽宽,不看视口)</h2>
|
|
167
|
+
<div class="demo">
|
|
168
|
+
<div class="cq-narrow" data-cq>
|
|
169
|
+
<div class="upload upload--drop" data-upload>
|
|
170
|
+
<input class="upload__input" type="file" id="u-cq" name="cq" multiple>
|
|
171
|
+
<label class="upload__dropzone" for="u-cq" aria-label="窄槽里的上传">
|
|
172
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
173
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
174
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
175
|
+
</svg>
|
|
176
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
177
|
+
<span class="upload__hint">PDF / XLSX</span>
|
|
178
|
+
</label>
|
|
179
|
+
<ul class="upload__list"></ul>
|
|
180
|
+
<span class="upload__status" role="status" aria-live="polite"></span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
<p class="note">左侧这块是 240px 宽的槽(外层加了 <code>data-cq</code>)。
|
|
185
|
+
它比右边窄,但没有"响应式跳跃":内边距与图标各降一档,限制说明<b>没有</b>被藏起来 ——
|
|
186
|
+
藏掉它,用户就不知道该选什么文件了。</p>
|
|
187
|
+
|
|
188
|
+
<h2>7 · 禁用</h2>
|
|
189
|
+
<div class="demo">
|
|
190
|
+
<div class="field">
|
|
191
|
+
<label class="field__label" for="u-dis">上传补充材料</label>
|
|
192
|
+
<div class="upload upload--drop" data-upload>
|
|
193
|
+
<input class="upload__input" type="file" id="u-dis" name="extra" disabled>
|
|
194
|
+
<label class="upload__dropzone" for="u-dis">
|
|
195
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
196
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
197
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
198
|
+
</svg>
|
|
199
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
200
|
+
</label>
|
|
201
|
+
<ul class="upload__list"></ul>
|
|
202
|
+
</div>
|
|
203
|
+
<p class="field__hint">先在"基础信息"里选好主体类型,这里才能上传</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<p class="note">禁用不用 <code>opacity</code>:半透明会让文字对比度随底色漂移,
|
|
207
|
+
暗色下直接不达标。这里换的是<b>明确色值</b>,边框同时从虚线变实线 ——
|
|
208
|
+
不用颜色也能看出"这一档不一样"。</p>
|
|
209
|
+
|
|
210
|
+
<h2 class="rule-bottom">对比:两种"拖放区"</h2>
|
|
211
|
+
<div class="cmp">
|
|
212
|
+
<div class="cmp__pane cmp__bad" data-bad=".upload__input|focus|=0">
|
|
213
|
+
<h3>❌ 常见写法:拖放区是唯一入口</h3>
|
|
214
|
+
<div class="upload upload--drop">
|
|
215
|
+
<input class="upload__input" type="file" id="u-bad" aria-label="上传文件(反例)" style="display:none">
|
|
216
|
+
<label class="upload__dropzone" for="u-bad">
|
|
217
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
218
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
219
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
220
|
+
</svg>
|
|
221
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
222
|
+
</label>
|
|
223
|
+
</div>
|
|
224
|
+
<p class="cmp__why">把原生 <code><input></code> 设成 <code>display:none</code> 图省事 —— 两栏<b>看起来一模一样</b>,差别在键盘:
|
|
225
|
+
后果是键盘 <kbd>Tab</kbd> 永远聚焦不到它(不可聚焦元素进不了 Tab 序列),
|
|
226
|
+
读屏也拿不到可访问名字 —— 鼠标用户能用,键盘用户<b>完全无法上传</b>。</p>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="cmp__pane cmp__good" data-good=".upload__input|focus|=1">
|
|
229
|
+
<h3>✅ 本库:隐藏的是像素,不是控件</h3>
|
|
230
|
+
<div class="upload upload--drop">
|
|
231
|
+
<input class="upload__input" type="file" id="u-good" aria-label="上传文件">
|
|
232
|
+
<label class="upload__dropzone" for="u-good">
|
|
233
|
+
<svg class="upload__icon" viewBox="0 0 16 16" fill="none" stroke="currentColor"
|
|
234
|
+
stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
|
235
|
+
<path d="M8 11V2.6M4.4 6.2L8 2.6l3.6 3.6M2.6 11.4v1.2a.8.8 0 0 0 .8.8h9.2a.8.8 0 0 0 .8-.8v-1.2"/>
|
|
236
|
+
</svg>
|
|
237
|
+
<span class="upload__title">拖入文件,或 <span class="upload__browse">选择文件</span></span>
|
|
238
|
+
</label>
|
|
239
|
+
</div>
|
|
240
|
+
<p class="cmp__why">同样看不见原生控件,但它只是 1px + <code>opacity:0</code>:
|
|
241
|
+
仍在 Tab 序列里、仍在表单里、仍能被 <code>label</code> 触发。
|
|
242
|
+
聚焦时焦点环画在拖放区上,用户看到的是"这块被选中了"。</p>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
|
|
246
|
+
<script>
|
|
247
|
+
/* ============================================================================
|
|
248
|
+
demo 脚本:只做两件事 —— 造出"真的文件",以及冻结进度
|
|
249
|
+
============================================================================
|
|
250
|
+
⚠️ 不写动画。视觉基线是逐像素比对的,一个 setInterval 就能让每次截图都不一样,
|
|
251
|
+
然后"基线过期"会被误读成"组件改坏了"。
|
|
252
|
+
========================================================================== */
|
|
253
|
+
(function () {
|
|
254
|
+
'use strict';
|
|
255
|
+
|
|
256
|
+
/* 1 字节的真实 File + 覆盖 size。
|
|
257
|
+
⚠️ 不要为了"看起来真实"去 new Uint8Array(30MB):那是真分配内存,
|
|
258
|
+
demo 页会多花几十毫秒,也会让无头浏览器在低配 CI 上偶发慢加载。
|
|
259
|
+
而 size 只是 `_validate` 读的一个数字 —— 覆盖它不影响任何被验证的逻辑。 */
|
|
260
|
+
function fakeFile(name, size, type) {
|
|
261
|
+
var f = new File([new Uint8Array(1)], name, { type: type || '' });
|
|
262
|
+
try { Object.defineProperty(f, 'size', { value: size }); } catch (e) { /* 老引擎不支持就算了 */ }
|
|
263
|
+
return f;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function seed(rootId, list) {
|
|
267
|
+
var root = document.getElementById(rootId);
|
|
268
|
+
var inst = root && root.uploadInstance;
|
|
269
|
+
if (!inst) return;
|
|
270
|
+
var files = [];
|
|
271
|
+
for (var i = 0; i < list.length; i++) {
|
|
272
|
+
files.push(fakeFile(list[i][0], list[i][1], list[i][2]));
|
|
273
|
+
}
|
|
274
|
+
inst.add(files, 0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function run() {
|
|
278
|
+
/* 2 · 增强形态:预置两个已经收下的文件 */
|
|
279
|
+
seed('u-drop-wrap', [
|
|
280
|
+
['应付账款明细.xlsx', 2457600, 'application/vnd.ms-excel'],
|
|
281
|
+
['银行存款余额调节表.pdf', 962560, 'application/pdf']
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
/* 4 · 校验拒绝:五个各自触发一条规则(类型 / 大小 / 数量) */
|
|
285
|
+
var rejRoot = document.getElementById('u-reject-wrap');
|
|
286
|
+
if (rejRoot && rejRoot.uploadInstance) {
|
|
287
|
+
rejRoot.uploadInstance.add([
|
|
288
|
+
fakeFile('会议纪要.docx', 120000, 'application/msword'), /* 类型不符 */
|
|
289
|
+
fakeFile('往来余额扫描件.pdf', 30 * 1024 * 1024, 'application/pdf'), /* 超过 20 MB */
|
|
290
|
+
fakeFile('a.pdf', 1024, 'application/pdf'),
|
|
291
|
+
fakeFile('b.pdf', 2048, 'application/pdf'),
|
|
292
|
+
fakeFile('c.pdf', 4096, 'application/pdf'),
|
|
293
|
+
fakeFile('d.pdf', 8192, 'application/pdf') /* 超过 3 个 */
|
|
294
|
+
], 1);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* 5 · 紧凑档:一个文件,让"有文件时"的样子也在 */
|
|
298
|
+
var compact = document.querySelector('.upload--compact');
|
|
299
|
+
if (compact && compact.uploadInstance) {
|
|
300
|
+
compact.uploadInstance.add([fakeFile('回函.pdf', 358400, 'application/pdf')], 0);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* upload.js 在 <head> 里加载,它的 init 挂在 DOMContentLoaded 上;
|
|
305
|
+
本脚本在 body 末尾,此时 readyState 还是 'loading' ⇒ 同样挂事件,
|
|
306
|
+
且**注册在后** ⇒ 一定在 init 之后跑。若脚本被动态注入(readyState 已就绪)
|
|
307
|
+
则直接跑 —— upload.js 那边也走"立即 init"分支,顺序依然成立。 */
|
|
308
|
+
if (document.readyState === 'loading') {
|
|
309
|
+
document.addEventListener('DOMContentLoaded', run);
|
|
310
|
+
} else {
|
|
311
|
+
run();
|
|
312
|
+
}
|
|
313
|
+
})();
|
|
314
|
+
</script>
|
|
315
|
+
|
|
316
|
+
</main>
|
|
317
|
+
</body>
|
|
318
|
+
</html>
|