nx-sk 0.8.0 → 0.8.2
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/CHANGELOG.md +13 -0
- package/README.md +1 -1
- package/assets/nx-sk/references/30-export-backup.md +1 -1
- package/assets/nx-sk/references/40-files.md +2 -2
- package/package.json +1 -1
- package/src/core/attachments.js +15 -6
- package/src/modules/backup/view.jsx +6 -11
- package/src/modules/export/view.jsx +10 -4
- package/src/web/frontend/App.jsx +12 -6
- package/src/web/frontend/components/fieldEditor.jsx +24 -5
- package/src/web/frontend/components/ui.jsx +4 -1
- package/src/web/frontend/registry.js +4 -2
- package/src/web/frontend/styles.css +2 -0
- package/src/web/public/assets/{files-2chm08H_.js → files-CHheSoYs.js} +1 -1
- package/src/web/public/assets/index-LbNmGlYj.js +41 -0
- package/src/web/public/assets/{index-DLtjhflt.css → index-lfnBM8pt.css} +1 -1
- package/src/web/public/assets/view-9J6BDv3t.js +5 -0
- package/src/web/public/assets/view-BIgIZKeA.js +7 -0
- package/src/web/public/assets/{view-DmRiyvj2.js → view-BMVMLYNP.js} +1 -1
- package/src/web/public/assets/{view-Cuhbkxa8.js → view-CmID1t8A.js} +1 -1
- package/src/web/public/assets/{view-BJmqIMAn.js → view-DDcFN2V4.js} +1 -1
- package/src/web/public/assets/view-QFcTiWTj.js +7 -0
- package/src/web/public/index.html +2 -2
- package/src/web/public/assets/index-DzD8wu27.js +0 -41
- package/src/web/public/assets/view-B-jmSJ3J.js +0 -7
- package/src/web/public/assets/view-C0WrdqsX.js +0 -7
- package/src/web/public/assets/view-CzDoriKQ.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式参考 [Keep a Changelog](https://keepachangelog.com/),中文正文。
|
|
4
4
|
|
|
5
|
+
## [0.8.2] - 2026-10-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`file update --name` 改名不带扩展名时,物理文件丢扩展名**:磁盘上只剩 `身份证人像面-赵刘学`(无 `.jpg`),而台账 `ext` 字段还记着 `.jpg`——内联预览、系统默认程序打开、`backup --with-files` 还原出来的文件全部哑掉。现在 `relocateFile` 会在新名字没有扩展名时**自动接回原扩展名**;名字自带扩展名则原样保留(不重复追加)。
|
|
10
|
+
- **历史连写的附件值解析不出来**:0.8.0 之前 `--set 头像图片=f_a,f_b` 会把两个 id **连成一个字符串**存进 values,`fileIdsOf` 此前不拆串内分隔符 → 面板显示「未解析」。现在 `fileIdsOf` / 前端 `toIds` 对字符串统一按 `,,、;;` 拆开——单值、数组、连写串三种历史形态都能归一(附件名里带逗号的极端情况请改用 id 关联)。
|
|
11
|
+
|
|
12
|
+
## [0.8.1] - 2026-10-15
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **条目里的附件字段框支持拖拽上传**:把文件(可多个)直接拖进 `file` 字段框即上传并追加到列表,悬停时虚线描边提示(与「附件」中心同一套视觉语言);超过 `maxItems` 上限时拖入会明确提示。之前只有「附件」中心页支持拖拽。
|
|
17
|
+
|
|
5
18
|
## [0.8.0] - 2026-10-13
|
|
6
19
|
|
|
7
20
|
### Added
|
package/README.md
CHANGED
|
@@ -221,7 +221,7 @@ nx-sk backup import --file ~/nx-sk/backup/xxx.nxskbak --password '你的备份
|
|
|
221
221
|
导入完成给出完整报告。`--replace` 才整份替换(灾难恢复 / 跨机器迁移用)。
|
|
222
222
|
两种方式都先自动留安全快照。
|
|
223
223
|
|
|
224
|
-
Web
|
|
224
|
+
Web 面板的「导出与备份」页同样支持:导出在浏览器下载 `.nxskbak`(可勾选连附件一起打包),
|
|
225
225
|
导入选择文件并输入口令、报告以弹窗呈现;口令错误或文件损坏会明确报错、不改动当前数据。
|
|
226
226
|
|
|
227
227
|
## 数据与安全
|
|
@@ -101,4 +101,4 @@ nx-sk backup import --file <备份>.nxskbak --password '口令' --replace # 整
|
|
|
101
101
|
- 两种方式都会**先自动留安全快照**(`…-before-import.json` / `…-before-restore.json`),可回滚。
|
|
102
102
|
- 敏感字段在包内是明文(靠口令保护),导入时自动用**本机 vaultKey** 重新加密落盘——报告里的值一律打码,密钥明文不进报告。
|
|
103
103
|
- 带 `--with-files` 的包导入时自动还原附件原件(sha256 校验;磁盘已有且内容不同则以磁盘为准);v2 旧包可导入,但不含原件,报告会如实列出缺哪些文件。
|
|
104
|
-
- 口令也可用环境变量 `NX_SK_BACKUP_PASSWORD` 提供;Web
|
|
104
|
+
- 口令也可用环境变量 `NX_SK_BACKUP_PASSWORD` 提供;Web 面板「导出与备份」页提供同样能力(浏览器下载 / 选择文件导入;超大包建议走 CLI)。
|
|
@@ -97,8 +97,8 @@ nx-sk file restore f_xxxx --to "D:\资料" # Web 上传件没有原始
|
|
|
97
97
|
- **组织方式由栏目驱动、不预定义分类**:筛选片按「哪些栏目在引用附件」动态生成(外加「未引用」),
|
|
98
98
|
每张卡片显示被哪个栏目 / 条目 / 字段引用;
|
|
99
99
|
- 编辑弹窗可改名称 / 分类 / 标签 / 备注(改分类或文件名时物理原件随之移动);
|
|
100
|
-
- 条目表单里的 `file`
|
|
101
|
-
|
|
100
|
+
- 条目表单里的 `file` 字段是**列表**:文件可直接**拖进字段框**(可多选)、点「上传文件」多选、
|
|
101
|
+
从库里逐个追加、逐行移除;保存即建立链接。
|
|
102
102
|
|
|
103
103
|
## 七、看不到 `file` 上传字段 / 条目里解析不到附件
|
|
104
104
|
|
package/package.json
CHANGED
package/src/core/attachments.js
CHANGED
|
@@ -130,8 +130,9 @@ export function normalizeFileRecord(raw) {
|
|
|
130
130
|
/**
|
|
131
131
|
* 一个 `file` 字段的值 → 附件 id 数组(**全仓库唯一的归一化入口**)。
|
|
132
132
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
133
|
+
* 字段可以挂**多个**附件:新写入统一存数组(哪怕只有一个)。要吃下三种历史形态:
|
|
134
|
+
* 单值字符串、id 数组、以及旧版把多个 id **连写成一个字符串**(`"f_a,f_b"`——
|
|
135
|
+
* 0.8.0 之前 `--set` 不拆逗号),所以字符串统一按分隔符拆开。
|
|
135
136
|
* 去空、去重、保序:同一个附件在一个字段里出现两次没有意义。
|
|
136
137
|
*/
|
|
137
138
|
export function fileIdsOf(value) {
|
|
@@ -140,9 +141,11 @@ export function fileIdsOf(value) {
|
|
|
140
141
|
: (Array.isArray(value) ? value : [value]);
|
|
141
142
|
const out = [];
|
|
142
143
|
for (const x of raw) {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
144
|
+
for (const piece of String(x ?? '').split(/[,,、;;]+/)) {
|
|
145
|
+
const id = piece.trim();
|
|
146
|
+
if (!id || out.includes(id)) continue;
|
|
147
|
+
out.push(id);
|
|
148
|
+
}
|
|
146
149
|
}
|
|
147
150
|
return out;
|
|
148
151
|
}
|
|
@@ -326,7 +329,13 @@ export async function importContent(store, opts = {}) {
|
|
|
326
329
|
*/
|
|
327
330
|
export async function relocateFile(store, rec, { category, name }) {
|
|
328
331
|
const cat = category || rec.category;
|
|
329
|
-
|
|
332
|
+
// 改名时**不带扩展名**就把原扩展名接回去——否则物理文件丢掉 .jpg/.pdf,
|
|
333
|
+
// 一切按扩展名认类型的地方(内联预览 / Content-Type / 系统默认程序打开 /
|
|
334
|
+
// backup --with-files 还原后的文件)都会哑掉,而台账里的 ext 字段还骗人说没丢。
|
|
335
|
+
let wanted = name || rec.name;
|
|
336
|
+
const originalExt = rec.ext || extname(rec.name).toLowerCase();
|
|
337
|
+
if (name && originalExt && !extname(wanted)) wanted += originalExt;
|
|
338
|
+
const fileName = safeFileName(wanted);
|
|
330
339
|
const same = cat === rec.category && fileName === rec.name;
|
|
331
340
|
const rel = same ? rec.rel : uniqueRel(store, cat, fileName, rec.id);
|
|
332
341
|
if (!same) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useState } from 'react';
|
|
2
2
|
import { api } from '../../web/frontend/api/client.js';
|
|
3
3
|
import { useStore } from '../../web/frontend/store.jsx';
|
|
4
|
-
import {
|
|
4
|
+
import { Empty, Modal } from '../../web/frontend/components/ui.jsx';
|
|
5
5
|
|
|
6
6
|
// Web 上传走 JSON 文本体,接口上限 32MB——留出余量在前端先拦,
|
|
7
7
|
// 超限建议走 CLI(命令行没有请求体上限)。
|
|
@@ -93,7 +93,9 @@ function ReplaceReport({ r }) {
|
|
|
93
93
|
);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export
|
|
96
|
+
// 备份半页:由「导出与备份」合并页(export 模块的 view.jsx)组合渲染,
|
|
97
|
+
// 所以不带页面头与 CLI 提示(CliHints 在合并页统一出一条)。tab: false,不单独出现在导航。
|
|
98
|
+
export default function BackupPanel() {
|
|
97
99
|
const { toast, guard, dialog, reload } = useStore();
|
|
98
100
|
const [pw, setPw] = useState('');
|
|
99
101
|
const [pw2, setPw2] = useState('');
|
|
@@ -177,13 +179,8 @@ export default function BackupView() {
|
|
|
177
179
|
|
|
178
180
|
return (
|
|
179
181
|
<>
|
|
180
|
-
<div className="toolbar rowgap pad">
|
|
181
|
-
<strong>加密备份</strong>
|
|
182
|
-
<span className="muted">整份数据用口令对称加密(AES-256-GCM);导出设口令、恢复输口令,口令不落盘</span>
|
|
183
|
-
</div>
|
|
184
|
-
|
|
185
182
|
<div className="card">
|
|
186
|
-
<div className="colhead"
|
|
183
|
+
<div className="colhead">加密备份(口令 .nxskbak)</div>
|
|
187
184
|
<div className="pad stack">
|
|
188
185
|
<div className="grid">
|
|
189
186
|
<div className="field">
|
|
@@ -211,7 +208,7 @@ export default function BackupView() {
|
|
|
211
208
|
</div>
|
|
212
209
|
|
|
213
210
|
<div className="card">
|
|
214
|
-
<div className="colhead"
|
|
211
|
+
<div className="colhead">解密导入(.nxskbak)</div>
|
|
215
212
|
<div className="pad stack">
|
|
216
213
|
<div className="grid">
|
|
217
214
|
<div className="field">
|
|
@@ -241,8 +238,6 @@ export default function BackupView() {
|
|
|
241
238
|
</div>
|
|
242
239
|
</div>
|
|
243
240
|
|
|
244
|
-
<CliHints module="backup" />
|
|
245
|
-
|
|
246
241
|
{result ? (
|
|
247
242
|
<Modal
|
|
248
243
|
title={result.mode === 'replace' ? '导入完成(整份替换)' : '导入完成(合并)'}
|
|
@@ -2,7 +2,11 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { api } from '../../web/frontend/api/client.js';
|
|
3
3
|
import { useStore } from '../../web/frontend/store.jsx';
|
|
4
4
|
import { CliHints, Copyable, Empty, Modal, Tag } from '../../web/frontend/components/ui.jsx';
|
|
5
|
+
import BackupPanel from '../backup/view.jsx';
|
|
5
6
|
|
|
7
|
+
// 「导出与备份」合并页:明文导出(带走可读文件)与口令加密备份(整份可还原)
|
|
8
|
+
// 对用户来说是同一件事——把数据倒出去、再倒回来,所以并成一个 tab。
|
|
9
|
+
// 后端仍是 export / backup 两个模块、两组 CLI 命令;页脚的 CliHints 把两组合成一条提示。
|
|
6
10
|
export default function ExportView() {
|
|
7
11
|
const { boot, toast, guard } = useStore();
|
|
8
12
|
const [sections, setSections] = useState([]);
|
|
@@ -61,12 +65,12 @@ export default function ExportView() {
|
|
|
61
65
|
return (
|
|
62
66
|
<>
|
|
63
67
|
<div className="toolbar rowgap pad">
|
|
64
|
-
<strong
|
|
65
|
-
<span className="muted"
|
|
68
|
+
<strong>导出与备份</strong>
|
|
69
|
+
<span className="muted">明文导出走可读文件(JSON / Markdown),加密备份走口令保护的 .nxskbak(整份可还原);都只在本机落盘</span>
|
|
66
70
|
</div>
|
|
67
71
|
|
|
68
72
|
<div className="card">
|
|
69
|
-
<div className="colhead"
|
|
73
|
+
<div className="colhead">导出为 JSON / Markdown</div>
|
|
70
74
|
<div className="pad">
|
|
71
75
|
<div className="grid">
|
|
72
76
|
<div className="field">
|
|
@@ -111,6 +115,8 @@ export default function ExportView() {
|
|
|
111
115
|
</div>
|
|
112
116
|
</div>
|
|
113
117
|
|
|
118
|
+
<BackupPanel />
|
|
119
|
+
|
|
114
120
|
<div className="card">
|
|
115
121
|
<div className="colhead">历次导出<span className="right muted">{history?.count ?? 0}</span></div>
|
|
116
122
|
<div className="colbody">
|
|
@@ -147,7 +153,7 @@ export default function ExportView() {
|
|
|
147
153
|
</div>
|
|
148
154
|
</div>
|
|
149
155
|
|
|
150
|
-
<CliHints module=
|
|
156
|
+
<CliHints module={['export', 'backup']} />
|
|
151
157
|
|
|
152
158
|
{modal ? <Modal title={modal.title} onClose={() => setModal(null)}>{modal.body}</Modal> : null}
|
|
153
159
|
</>
|
package/src/web/frontend/App.jsx
CHANGED
|
@@ -5,11 +5,17 @@ import { useStore } from './store.jsx';
|
|
|
5
5
|
|
|
6
6
|
function viewFromHash() {
|
|
7
7
|
const m = /^#\/?([^?/]*)/.exec(window.location.hash || '');
|
|
8
|
-
|
|
8
|
+
const id = m ? decodeURIComponent(m[1]) : '';
|
|
9
|
+
return LEGACY_VIEW[id] || id;
|
|
9
10
|
}
|
|
10
11
|
|
|
12
|
+
// 加密备份并入「导出与备份」合并页后,旧书签(#/backup)与 localStorage 里
|
|
13
|
+
// 残留的 ui.view 都要落到合并页——否则会被「找不到 tab」的兜底甩到第一个栏目。
|
|
14
|
+
const LEGACY_VIEW = { backup: 'export' };
|
|
15
|
+
|
|
11
16
|
export default function App() {
|
|
12
17
|
const { boot, bootError, ui, patchUi, reload } = useStore();
|
|
18
|
+
const view = LEGACY_VIEW[ui.view] || ui.view;
|
|
13
19
|
|
|
14
20
|
// 栏目 tab 是**数据驱动**的(来自 bootstrap.sections),静态 tab 来自前端注册表。
|
|
15
21
|
// 两者的 id 可能撞车时静态 tab 不参与第一优先级的排序——栏目 order 是 10/20,静态是 500+。
|
|
@@ -30,13 +36,13 @@ export default function App() {
|
|
|
30
36
|
// 兜底会选中第一个静态视图并由下面的 effect 写进 ui —— 于是「默认视图 = 求职」被顶掉、
|
|
31
37
|
// 头部高亮也错位成「栏目」,连 `#/job` 这类深链都会被首屏覆盖。
|
|
32
38
|
const booted = !!boot;
|
|
33
|
-
const current = tabs.find((t) => t.id ===
|
|
39
|
+
const current = tabs.find((t) => t.id === view) || (booted ? tabs[0] : null) || null;
|
|
34
40
|
|
|
35
41
|
// hash 与 localStorage 双写对齐:只在挂载时对齐一次,之后交给 hashchange
|
|
36
42
|
useEffect(() => {
|
|
37
43
|
const fromHash = viewFromHash();
|
|
38
|
-
if (fromHash && fromHash !==
|
|
39
|
-
else if (!window.location.hash) window.location.hash = `#/${
|
|
44
|
+
if (fromHash && fromHash !== view) patchUi({ view: fromHash });
|
|
45
|
+
else if (!window.location.hash) window.location.hash = `#/${view}`;
|
|
40
46
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
41
47
|
}, []);
|
|
42
48
|
|
|
@@ -47,8 +53,8 @@ export default function App() {
|
|
|
47
53
|
}, [patchUi]);
|
|
48
54
|
|
|
49
55
|
useEffect(() => {
|
|
50
|
-
if (booted && current && current.id !==
|
|
51
|
-
}, [booted, current,
|
|
56
|
+
if (booted && current && current.id !== view) patchUi({ view: current.id });
|
|
57
|
+
}, [booted, current, view, patchUi]);
|
|
52
58
|
|
|
53
59
|
const go = (id) => {
|
|
54
60
|
patchUi({ view: id });
|
|
@@ -37,15 +37,18 @@ function AutoTextarea({ value, onChange, minRows = 2, placeholder, disabled, max
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* 字段值 → 附件 id 数组。
|
|
40
|
-
* 一个 `file`
|
|
41
|
-
*
|
|
40
|
+
* 一个 `file` 字段可挂**多个**附件:新写入统一存数组;历史形态有单值字符串、
|
|
41
|
+
* 甚至旧版连写的 `"f_a,f_b"`(0.8.0 前 `--set` 不拆逗号)——统一拆分隔符归一。
|
|
42
|
+
* 前端不能 import core,所以这里自带一份同口径的归一化。
|
|
42
43
|
*/
|
|
43
44
|
function toIds(value) {
|
|
44
45
|
const raw = value === null || value === undefined ? [] : (Array.isArray(value) ? value : [value]);
|
|
45
46
|
const out = [];
|
|
46
47
|
for (const x of raw) {
|
|
47
|
-
const
|
|
48
|
-
|
|
48
|
+
for (const piece of String(x ?? '').split(/[,,、;;]+/)) {
|
|
49
|
+
const id = piece.trim();
|
|
50
|
+
if (id && !out.includes(id)) out.push(id);
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
53
|
return out;
|
|
51
54
|
}
|
|
@@ -62,7 +65,10 @@ function toIds(value) {
|
|
|
62
65
|
export function FileFieldControl({ def, value, onChange }) {
|
|
63
66
|
const { toast, guard, patchUi } = useStore();
|
|
64
67
|
const [data, setData] = useState(null);
|
|
68
|
+
const [dragOver, setDragOver] = useState(false);
|
|
65
69
|
const uploadRef = useRef(null);
|
|
70
|
+
// dragenter/dragleave 在子元素间穿梭时会成对触发,用深度计数避免「还没离开就熄掉高亮」
|
|
71
|
+
const dragDepth = useRef(0);
|
|
66
72
|
const ids = toIds(value);
|
|
67
73
|
const max = Number(def.maxItems) || 0;
|
|
68
74
|
const full = max > 0 && ids.length >= max;
|
|
@@ -99,8 +105,21 @@ export function FileFieldControl({ def, value, onChange }) {
|
|
|
99
105
|
const add = (id) => { if (id && !ids.includes(id)) onChange([...ids, id]); };
|
|
100
106
|
const removeAt = (id) => onChange(ids.filter((x) => x !== id));
|
|
101
107
|
|
|
108
|
+
// 拖拽文件进字段框 = 直接上传并追加(与「附件」中心同一套手感)
|
|
109
|
+
const onDrop = guard(async (e) => {
|
|
110
|
+
dragDepth.current = 0;
|
|
111
|
+
setDragOver(false);
|
|
112
|
+
await upload(e.dataTransfer?.files);
|
|
113
|
+
});
|
|
114
|
+
|
|
102
115
|
return (
|
|
103
|
-
<div
|
|
116
|
+
<div
|
|
117
|
+
className={`file-picker ${dragOver ? 'drag' : ''}`}
|
|
118
|
+
onDragEnter={(e) => { e.preventDefault(); dragDepth.current += 1; setDragOver(true); }}
|
|
119
|
+
onDragOver={(e) => e.preventDefault()}
|
|
120
|
+
onDragLeave={() => { dragDepth.current -= 1; if (dragDepth.current <= 0) { dragDepth.current = 0; setDragOver(false); } }}
|
|
121
|
+
onDrop={onDrop}
|
|
122
|
+
>
|
|
104
123
|
{ids.map((id) => {
|
|
105
124
|
const rec = byId(id);
|
|
106
125
|
if (!rec) {
|
|
@@ -120,9 +120,12 @@ export function DialogHost() {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/** 「这个页面每个按钮都有同构 CLI 命令」——提示**由数据派生**,不手写(手写必然与实际命令漂移)。 */
|
|
123
|
+
// module 可以是单个 id,也可以是数组——合并页(如「导出与备份」)一个页面承载
|
|
124
|
+
// 多个模块的命令,提示合成一条而不是每半页各来一句。
|
|
123
125
|
export function CliHints({ module }) {
|
|
124
126
|
const { boot } = useStore();
|
|
125
|
-
const
|
|
127
|
+
const mods = [].concat(module);
|
|
128
|
+
const cmds = (boot?.commands || []).filter((c) => mods.includes(c.module));
|
|
126
129
|
if (!cmds.length) return null;
|
|
127
130
|
return (
|
|
128
131
|
<div className="cli-hint">
|
|
@@ -11,8 +11,10 @@ export const VIEWS = [
|
|
|
11
11
|
{ id: 'sections', title: '栏目', order: 500, component: lazy(() => import('../../modules/sections/view.jsx')) },
|
|
12
12
|
{ id: 'entries', title: '条目', order: 600, tab: false, component: lazy(() => import('../../modules/entries/view.jsx')) },
|
|
13
13
|
{ id: 'files', title: '附件', order: 650, component: lazy(() => import('../../modules/files/view.jsx')) },
|
|
14
|
-
{ id: 'export', title: '
|
|
15
|
-
|
|
14
|
+
{ id: 'export', title: '导出与备份', order: 700, component: lazy(() => import('../../modules/export/view.jsx')) },
|
|
15
|
+
// 加密备份已并入「导出与备份」合并页(export 的 view 组合渲染本视图),
|
|
16
|
+
// 所以 tab: false——与 entries 同款:不出导航,但登记与懒加载必须保留。
|
|
17
|
+
{ id: 'backup', title: '加密备份', order: 750, tab: false, component: lazy(() => import('../../modules/backup/view.jsx')) },
|
|
16
18
|
{ id: 'settings', title: '设置', order: 900, component: lazy(() => import('../../modules/settings/view.jsx')) },
|
|
17
19
|
];
|
|
18
20
|
|
|
@@ -384,6 +384,8 @@ a.file-name:hover { text-decoration: underline; }
|
|
|
384
384
|
|
|
385
385
|
/* ── 字段里的附件选择器 ─────────────────────────────── */
|
|
386
386
|
.file-picker { display: flex; flex-direction: column; gap: 4px; }
|
|
387
|
+
/* 拖文件悬停在字段框上:与「附件」中心同一套视觉语言(虚线描边 + 浅底) */
|
|
388
|
+
.file-picker.drag { outline: 2px dashed var(--ink); outline-offset: -4px; background: var(--soft); }
|
|
387
389
|
.file-picker .file-pick-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
|
388
390
|
.file-picker select { flex: 1; min-width: 120px; }
|
|
389
391
|
.file-preview {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as l}from"./index-
|
|
1
|
+
import{a as l}from"./index-LbNmGlYj.js";let r=null;function o(n=!1){return!n&&r||(r=l("/api/files").then(e=>e).catch(e=>{throw r=null,e})),r}function i(){return r=null,o(!0)}function u(n){return new Promise((e,a)=>{const t=new FileReader;t.onload=()=>e(t.result),t.onerror=()=>a(new Error("读取文件失败")),t.readAsDataURL(n)})}export{u as f,o as l,i as r};
|