vitepress-plugin-filelist 0.1.2 → 0.2.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/README.md +97 -66
- package/dist/client/browser/index.d.ts +17 -1
- package/dist/client/browser/index.js +109 -44
- package/dist/client/ssr/index.d.ts +17 -1
- package/dist/client/ssr/index.js +84 -6
- package/dist/client/style.css +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
# vitepress-plugin-filelist
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
面向 VitePress 的 OpenList 文件浏览组件:树形展开、勾选批量打包、目录密码、token,直接调用 OpenList 接口。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 功能
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- 📁 树形目录,文件夹按需加载
|
|
8
|
+
- ✅ 勾选文件;选中文件夹会连带其已加载的子项
|
|
9
|
+
- 📦 批量打包 ZIP(基于 `fflate`),带进度
|
|
10
|
+
- ⬇️ 单文件下载与预览(`/d/`、`/p/`、`fs/get` 的 `raw_url`)
|
|
11
|
+
- 🔐 目录密码输入,可选只读 token
|
|
12
|
+
- 🔎 当前树过滤,按名称 / 大小 / 日期排序
|
|
13
|
+
- 🌐 使用 `/api/fs/list`、`/api/fs/get`、`/api/public/settings`
|
|
14
|
+
- 🧪 `base` 为空时使用内置演示数据
|
|
15
|
+
- SSR 安全(网络请求只在客户端执行)
|
|
8
16
|
|
|
9
|
-
|
|
10
|
-
- ✅ Checkbox selection, folder selects its loaded children
|
|
11
|
-
- 📦 Batch download as ZIP (via `fflate`), with progress
|
|
12
|
-
- ⬇️ Single file download and preview (`/d/`, `/p/`, `fs/get` raw URL)
|
|
13
|
-
- 🔐 Folder password prompt, optional read-only token
|
|
14
|
-
- 🔎 Filter current tree, sortable name / size / date
|
|
15
|
-
- 🌐 Uses `/api/fs/list`, `/api/fs/get`, `/api/public/settings`
|
|
16
|
-
- 🧪 Demo mode with built-in fake data when `base` is empty
|
|
17
|
-
- SSR safe (network requests only run on the client)
|
|
18
|
-
|
|
19
|
-
## Install
|
|
17
|
+
## 安装
|
|
20
18
|
|
|
21
19
|
```bash
|
|
22
20
|
pnpm add -D vitepress-plugin-filelist
|
|
23
|
-
#
|
|
21
|
+
# 或:npm i -D vitepress-plugin-filelist
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
##
|
|
24
|
+
## 在 vitepress-tuck 中使用
|
|
27
25
|
|
|
28
26
|
```ts
|
|
29
27
|
// .vitepress/config.ts
|
|
@@ -53,7 +51,7 @@ export default {
|
|
|
53
51
|
<FileList base="https://pan.example.com" path="/课程资料" />
|
|
54
52
|
```
|
|
55
53
|
|
|
56
|
-
##
|
|
54
|
+
## 在普通 VitePress 中使用
|
|
57
55
|
|
|
58
56
|
```ts
|
|
59
57
|
// .vitepress/theme/index.ts
|
|
@@ -69,27 +67,58 @@ export default {
|
|
|
69
67
|
}
|
|
70
68
|
```
|
|
71
69
|
|
|
72
|
-
##
|
|
70
|
+
## 组件属性
|
|
73
71
|
|
|
74
|
-
|
|
|
72
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
75
73
|
| --- | --- | --- | --- |
|
|
76
|
-
| `base` | `string` | `''` | OpenList
|
|
77
|
-
| `path` | `string` | `'/'` |
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
74
|
+
| `base` | `string` | `''` | OpenList 站点地址;为空时进入演示模式 |
|
|
75
|
+
| `path` | `string` | `'/'` | 初始目录 |
|
|
76
|
+
| `sources` | `FileListSource[]` | `[]` | 多根挂载;设置后忽略 `path` |
|
|
77
|
+
| `expanded` | `boolean` | `false` | 目录根是否默认展开 |
|
|
78
|
+
| `token` | `string` | `''` | 可选只读 token;不传则以游客身份请求 |
|
|
79
|
+
| `password` | `string` | `''` | 目录密码,也可以在运行时输入 |
|
|
80
|
+
| `title` | `string` | `''` | 工具栏标题;为空时回退到 OpenList 的 `site_title` |
|
|
81
|
+
| `locale` | `'zh-CN' \| 'en-US'` | `'zh-CN'` | 内置文案预设 |
|
|
82
|
+
| `labels` | `Partial<FileListLabels>` | `undefined` | 按 key 覆盖单条文案 |
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
同时注册了 `<FileList>` 和 `<VPFileList>` 两个组件名。
|
|
85
85
|
|
|
86
|
-
##
|
|
86
|
+
## 多根挂载
|
|
87
|
+
|
|
88
|
+
```md
|
|
89
|
+
<FileList
|
|
90
|
+
base="https://pan.example.com"
|
|
91
|
+
title="课程资料"
|
|
92
|
+
:sources="[
|
|
93
|
+
{ path: '/课程资料/语文' },
|
|
94
|
+
{ path: '/课程资料/数学', expanded: true },
|
|
95
|
+
{ path: '/课程资料/说明.pdf' }
|
|
96
|
+
]"
|
|
97
|
+
/>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- 目录路径挂载为根文件夹,默认折叠;`expanded` 可为全局或单个 source 打开
|
|
101
|
+
- 文件路径挂载为一条文件记录,直接预览/下载
|
|
102
|
+
- 根目录复选框与勾选、全选效果一致:选中根目录会选中其已加载的子项
|
|
103
|
+
- 每个 source 可单独设置 `title`、`password`
|
|
104
|
+
- 根节点顺序按 `sources` 顺序,子项仍按名称/大小/日期排序
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
interface FileListSource {
|
|
108
|
+
path: string
|
|
109
|
+
title?: string
|
|
110
|
+
password?: string
|
|
111
|
+
expanded?: boolean
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 国际化
|
|
87
116
|
|
|
88
117
|
```md
|
|
89
118
|
<FileList locale="en-US" base="https://pan.example.com" />
|
|
90
119
|
```
|
|
91
120
|
|
|
92
|
-
|
|
121
|
+
在预设基础上覆盖单条文案:
|
|
93
122
|
|
|
94
123
|
```md
|
|
95
124
|
<FileList
|
|
@@ -98,74 +127,76 @@ Override individual labels on top of the preset:
|
|
|
98
127
|
/>
|
|
99
128
|
```
|
|
100
129
|
|
|
101
|
-
|
|
130
|
+
内置预设:`zh-CN`、`en-US`。占位符支持 `{count}`、`{percent}`、`{message}`、`{name}`,由 `formatLabel()` 替换。完整 key 列表通过 `FileListLabels` 导出:
|
|
102
131
|
|
|
103
132
|
```ts
|
|
104
133
|
import type { FileListLabels } from 'vitepress-plugin-filelist/client'
|
|
105
134
|
import { fileListLocales, formatLabel, resolveLabels } from 'vitepress-plugin-filelist/client'
|
|
106
135
|
```
|
|
107
136
|
|
|
108
|
-
## OpenList
|
|
137
|
+
## OpenList 要求
|
|
109
138
|
|
|
110
|
-
1.
|
|
111
|
-
2. **CORS
|
|
112
|
-
3.
|
|
113
|
-
4. **
|
|
139
|
+
1. **游客只读权限**:开启游客用户,或传入只读 `token`
|
|
140
|
+
2. **CORS**:OpenList 的 `cors.allow_origins` 必须包含站点来源(默认 `*`)
|
|
141
|
+
3. **同协议**:HTTPS 页面不能请求 HTTP 的 OpenList(混合内容)
|
|
142
|
+
4. **token 是公开的**:只放可撤销的只读 token,不要放管理 token
|
|
114
143
|
|
|
115
|
-
|
|
144
|
+
组件会调用:
|
|
116
145
|
|
|
117
|
-
|
|
|
146
|
+
| 接口 | 用途 |
|
|
118
147
|
| --- | --- |
|
|
119
|
-
| `POST /api/fs/list` |
|
|
120
|
-
| `POST /api/fs/get` |
|
|
121
|
-
| `GET /api/public/settings` | `site_title`
|
|
122
|
-
| `GET /d
|
|
148
|
+
| `POST /api/fs/list` | 目录列表(`per_page: 0`) |
|
|
149
|
+
| `POST /api/fs/get` | 获取下载 / 预览 / 打包用的 `raw_url`;失败时回退到 `/d/...?sign=` |
|
|
150
|
+
| `GET /api/public/settings` | 获取 `site_title` |
|
|
151
|
+
| `GET /d/...`、`GET /p/...` | OpenList 生成的下载 / 预览链接 |
|
|
123
152
|
|
|
124
|
-
##
|
|
153
|
+
## 导出
|
|
125
154
|
|
|
126
|
-
|
|
|
155
|
+
| 入口 | 内容 |
|
|
127
156
|
| --- | --- |
|
|
128
|
-
| `vitepress-plugin-filelist` | `fileList`
|
|
129
|
-
| `vitepress-plugin-filelist/client` | `FileList
|
|
130
|
-
| `vitepress-plugin-filelist/style.css` |
|
|
157
|
+
| `vitepress-plugin-filelist` | `fileList` 插件(默认导出) |
|
|
158
|
+
| `vitepress-plugin-filelist/client` | `FileList`、`VPFileList`、`enhanceAppWithFileList`、类型 |
|
|
159
|
+
| `vitepress-plugin-filelist/style.css` | 组件样式 |
|
|
131
160
|
|
|
132
|
-
##
|
|
161
|
+
## 兼容性
|
|
133
162
|
|
|
134
|
-
|
|
163
|
+
peerDependencies 同时支持 VitePress 1.x 与 2.0 alpha:`^1.0.0 || ^2.0.0-alpha.0`。
|
|
135
164
|
|
|
136
|
-
|
|
165
|
+
在 Node 22 下针对 VitePress `2.0.0-alpha.21` 本地实测:
|
|
137
166
|
|
|
138
|
-
|
|
|
167
|
+
| 包管理器 | 安装 | 构建 |
|
|
139
168
|
| --- | --- | --- |
|
|
140
169
|
| npm 10 | ✅ | ✅ |
|
|
141
170
|
| pnpm 11.25 | ✅ | ✅ |
|
|
142
|
-
| yarn 1.22
|
|
171
|
+
| yarn 1.22(classic) | ✅ | ✅ |
|
|
143
172
|
| bun 1.4 | ✅ | ✅ |
|
|
144
|
-
| deno 2.9 | ✅ `deno install` | ❌
|
|
173
|
+
| deno 2.9 | ✅ `deno install` | ❌ Vite/rolldown 加载配置时失败 |
|
|
145
174
|
|
|
146
|
-
Deno
|
|
175
|
+
Deno 可以安装 npm 依赖,但目前用 Deno 构建 VitePress 会在 rolldown 里报 `stream did not contain valid UTF-8`;构建请使用 Node。
|
|
147
176
|
|
|
148
|
-
##
|
|
177
|
+
## 发布
|
|
149
178
|
|
|
150
|
-
|
|
179
|
+
通过 GitHub Actions 的 Trusted Publishing(OIDC)发布到 npm,带 provenance。
|
|
151
180
|
|
|
152
|
-
##
|
|
181
|
+
## 说明
|
|
153
182
|
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
183
|
+
- 批量 ZIP 通过 `raw_url`(OpenList `/p/` 代理)下载,CORS 保持在 OpenList 源上
|
|
184
|
+
- 全选只统计已加载的文件;页脚会显示还有多少文件夹未展开
|
|
185
|
+
- 目录以 `per_page: 0` 请求全部条目;超大目录可能较慢,暂未实现分页
|
|
157
186
|
|
|
158
|
-
##
|
|
187
|
+
## 开发
|
|
159
188
|
|
|
160
189
|
```bash
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
pnpm install
|
|
191
|
+
pnpm run build # 生成 dist/node、dist/client/ssr、dist/client/browser、style.css
|
|
192
|
+
pnpm run typecheck
|
|
193
|
+
pnpm test # Vitest:utils + locale helpers
|
|
194
|
+
pnpm run pack:check
|
|
166
195
|
```
|
|
167
196
|
|
|
168
|
-
|
|
197
|
+
仓库统一使用 pnpm(`pnpm-lock.yaml`),CI 与发布流程执行 `pnpm install --frozen-lockfile`。
|
|
198
|
+
|
|
199
|
+
`demo/` 下的演示站通过 `file:..` 安装本地包:
|
|
169
200
|
|
|
170
201
|
```bash
|
|
171
202
|
cd demo
|
|
@@ -34,11 +34,25 @@ interface FileListLabels {
|
|
|
34
34
|
batchFailed: string;
|
|
35
35
|
defaultZip: string;
|
|
36
36
|
}
|
|
37
|
+
interface FileListSource {
|
|
38
|
+
/** OpenList path, directory or single file. */
|
|
39
|
+
path: string;
|
|
40
|
+
/** Root label. Defaults to the last path segment. */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** Per-source folder password. */
|
|
43
|
+
password?: string;
|
|
44
|
+
/** Initial expanded state for a directory source. Overrides the global `expanded` prop. */
|
|
45
|
+
expanded?: boolean;
|
|
46
|
+
}
|
|
37
47
|
interface FileListProps {
|
|
38
48
|
/** OpenList base URL, e.g. `https://pan.example.com`. Empty string enables demo mode. */
|
|
39
49
|
base?: string;
|
|
40
50
|
/** Initial directory, defaults to `/`. */
|
|
41
51
|
path?: string;
|
|
52
|
+
/** Mount multiple paths as root nodes. When set, `path` is ignored. */
|
|
53
|
+
sources?: FileListSource[];
|
|
54
|
+
/** Expand directory roots initially. Defaults to `false`. */
|
|
55
|
+
expanded?: boolean;
|
|
42
56
|
/** Optional read-only token. Guest access is used when omitted. */
|
|
43
57
|
token?: string;
|
|
44
58
|
/** Folder password. The component also asks at runtime when OpenList reports one. */
|
|
@@ -64,6 +78,8 @@ interface OpenListEntry {
|
|
|
64
78
|
declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FileListProps> & Readonly<{}>, {
|
|
65
79
|
base: string;
|
|
66
80
|
path: string;
|
|
81
|
+
sources: FileListSource[];
|
|
82
|
+
expanded: boolean;
|
|
67
83
|
token: string;
|
|
68
84
|
password: string;
|
|
69
85
|
title: string;
|
|
@@ -176,4 +192,4 @@ export declare function compareNodes(a: SortableNode, b: SortableNode, key: Sort
|
|
|
176
192
|
* ```
|
|
177
193
|
*/
|
|
178
194
|
export declare function enhanceAppWithFileList({ app }: EnhanceAppContext): void;
|
|
179
|
-
export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type OpenListEntry };
|
|
195
|
+
export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource, type OpenListEntry };
|
|
@@ -173,32 +173,33 @@ const _hoisted_23 = [
|
|
|
173
173
|
".indeterminate",
|
|
174
174
|
"onChange"
|
|
175
175
|
];
|
|
176
|
-
const _hoisted_24 = ["
|
|
177
|
-
const _hoisted_25 =
|
|
176
|
+
const _hoisted_24 = ["title"];
|
|
177
|
+
const _hoisted_25 = ["onClick"];
|
|
178
|
+
const _hoisted_26 = {
|
|
178
179
|
key: 1,
|
|
179
180
|
class: "fl-arrow-placeholder"
|
|
180
181
|
};
|
|
181
|
-
const
|
|
182
|
-
const
|
|
182
|
+
const _hoisted_27 = ["onClick"];
|
|
183
|
+
const _hoisted_28 = {
|
|
183
184
|
key: 0,
|
|
184
185
|
class: "fl-folder-icon"
|
|
185
186
|
};
|
|
186
|
-
const
|
|
187
|
+
const _hoisted_29 = {
|
|
187
188
|
key: 1,
|
|
188
189
|
class: "fl-ext-badge"
|
|
189
190
|
};
|
|
190
|
-
const
|
|
191
|
+
const _hoisted_30 = {
|
|
191
192
|
key: 2,
|
|
192
193
|
class: "fl-loading"
|
|
193
194
|
};
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
const _hoisted_33 = ["title", "onClick"];
|
|
195
|
+
const _hoisted_31 = { class: "fl-cell fl-cell-size" };
|
|
196
|
+
const _hoisted_32 = { class: "fl-cell fl-cell-date" };
|
|
197
|
+
const _hoisted_33 = { class: "fl-cell fl-cell-op" };
|
|
198
198
|
const _hoisted_34 = ["title", "onClick"];
|
|
199
|
-
const _hoisted_35 =
|
|
200
|
-
const _hoisted_36 = {
|
|
201
|
-
const _hoisted_37 = {
|
|
199
|
+
const _hoisted_35 = ["title", "onClick"];
|
|
200
|
+
const _hoisted_36 = { class: "fl-foot" };
|
|
201
|
+
const _hoisted_37 = { key: 0 };
|
|
202
|
+
const _hoisted_38 = {
|
|
202
203
|
key: 4,
|
|
203
204
|
class: "fl-toast"
|
|
204
205
|
};
|
|
@@ -207,6 +208,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
207
208
|
props: {
|
|
208
209
|
base: { default: "" },
|
|
209
210
|
path: { default: "/" },
|
|
211
|
+
sources: { default: () => [] },
|
|
212
|
+
expanded: {
|
|
213
|
+
type: Boolean,
|
|
214
|
+
default: false
|
|
215
|
+
},
|
|
210
216
|
token: { default: "" },
|
|
211
217
|
password: { default: "" },
|
|
212
218
|
title: { default: "" },
|
|
@@ -409,14 +415,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
409
415
|
if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
|
|
410
416
|
return payload.data;
|
|
411
417
|
}
|
|
412
|
-
async function listDir(path) {
|
|
418
|
+
async function listDir(path, password = activePassword.value) {
|
|
413
419
|
if (demo.value) {
|
|
414
420
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
415
421
|
return DEMO_TREE[path] ?? [];
|
|
416
422
|
}
|
|
417
423
|
return (await request("/api/fs/list", {
|
|
418
424
|
path,
|
|
419
|
-
password
|
|
425
|
+
password,
|
|
420
426
|
page: 1,
|
|
421
427
|
per_page: 0,
|
|
422
428
|
refresh: false
|
|
@@ -424,13 +430,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
424
430
|
}
|
|
425
431
|
async function resolveUrl(node) {
|
|
426
432
|
if (demo.value) return "";
|
|
433
|
+
if (node.raw_url) return node.raw_url;
|
|
427
434
|
const cached = downloadCache.get(node.path);
|
|
428
435
|
if (cached) return cached;
|
|
429
436
|
let url = "";
|
|
430
437
|
try {
|
|
431
438
|
url = (await request("/api/fs/get", {
|
|
432
439
|
path: node.path,
|
|
433
|
-
password: activePassword.value
|
|
440
|
+
password: node.password || activePassword.value
|
|
434
441
|
}))?.raw_url ?? "";
|
|
435
442
|
} catch {
|
|
436
443
|
url = "";
|
|
@@ -455,7 +462,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
455
462
|
nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
|
|
456
463
|
for (const node of nodes) if (node.children.length) sortTree(node.children);
|
|
457
464
|
}
|
|
458
|
-
function toNodes(items, parent, depth) {
|
|
465
|
+
function toNodes(items, parent, depth, password = "") {
|
|
459
466
|
return items.map((item) => ({
|
|
460
467
|
...item,
|
|
461
468
|
path: joinPath(parent, item.name),
|
|
@@ -463,7 +470,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
463
470
|
expanded: false,
|
|
464
471
|
loaded: false,
|
|
465
472
|
loading: false,
|
|
466
|
-
children: []
|
|
473
|
+
children: [],
|
|
474
|
+
password
|
|
467
475
|
}));
|
|
468
476
|
}
|
|
469
477
|
function setSort(key) {
|
|
@@ -472,15 +480,82 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
472
480
|
sortKey.value = key;
|
|
473
481
|
sortAsc.value = true;
|
|
474
482
|
}
|
|
475
|
-
|
|
483
|
+
if (props.sources.length) {
|
|
484
|
+
for (const root of roots.value) if (root.children.length) sortTree(root.children);
|
|
485
|
+
} else sortTree(roots.value);
|
|
486
|
+
}
|
|
487
|
+
function sourceName(path) {
|
|
488
|
+
const parts = path.split("/").filter(Boolean);
|
|
489
|
+
return parts[parts.length - 1] || path;
|
|
490
|
+
}
|
|
491
|
+
async function loadChildren(node) {
|
|
492
|
+
if (node.loaded) return true;
|
|
493
|
+
node.loading = true;
|
|
494
|
+
try {
|
|
495
|
+
node.children = toNodes(await listDir(node.path, node.password), node.path, node.depth + 1, node.password);
|
|
496
|
+
sortTree(node.children);
|
|
497
|
+
node.loaded = true;
|
|
498
|
+
return true;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
showToast(t("folderFailed", { message: errorText(error) }));
|
|
501
|
+
return false;
|
|
502
|
+
} finally {
|
|
503
|
+
node.loading = false;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
async function loadSourceRoot(source) {
|
|
507
|
+
const path = source.path;
|
|
508
|
+
const password = source.password ?? "";
|
|
509
|
+
let item = {
|
|
510
|
+
name: sourceName(path),
|
|
511
|
+
size: 0,
|
|
512
|
+
is_dir: true,
|
|
513
|
+
modified: ""
|
|
514
|
+
};
|
|
515
|
+
if (demo.value) {
|
|
516
|
+
if (/\/[^/]+\.[^/]+$/.test(path)) item = {
|
|
517
|
+
...item,
|
|
518
|
+
is_dir: false,
|
|
519
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
520
|
+
};
|
|
521
|
+
} else try {
|
|
522
|
+
const data = await request("/api/fs/get", {
|
|
523
|
+
path,
|
|
524
|
+
password
|
|
525
|
+
});
|
|
526
|
+
item = {
|
|
527
|
+
...item,
|
|
528
|
+
...data
|
|
529
|
+
};
|
|
530
|
+
} catch {}
|
|
531
|
+
return {
|
|
532
|
+
...item,
|
|
533
|
+
name: source.title || item.name || sourceName(path),
|
|
534
|
+
path,
|
|
535
|
+
depth: 0,
|
|
536
|
+
expanded: source.expanded ?? props.expanded,
|
|
537
|
+
loaded: false,
|
|
538
|
+
loading: false,
|
|
539
|
+
children: [],
|
|
540
|
+
password,
|
|
541
|
+
isRoot: true
|
|
542
|
+
};
|
|
476
543
|
}
|
|
477
544
|
async function loadRoot() {
|
|
478
545
|
loadingRoot.value = true;
|
|
479
546
|
errorMsg.value = "";
|
|
480
547
|
needsPassword.value = false;
|
|
481
548
|
try {
|
|
549
|
+
if (props.sources.length) {
|
|
550
|
+
const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
|
|
551
|
+
roots.value = nodes;
|
|
552
|
+
for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
|
|
553
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
554
|
+
downloadCache.clear();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
482
557
|
const items = await listDir(props.path);
|
|
483
|
-
roots.value = toNodes(items, props.path, 0);
|
|
558
|
+
roots.value = toNodes(items, props.path, 0, activePassword.value);
|
|
484
559
|
sortTree(roots.value);
|
|
485
560
|
selected.value = /* @__PURE__ */ new Set();
|
|
486
561
|
downloadCache.clear();
|
|
@@ -498,19 +573,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
498
573
|
node.expanded = false;
|
|
499
574
|
return;
|
|
500
575
|
}
|
|
501
|
-
if (!node
|
|
502
|
-
node.loading = true;
|
|
503
|
-
try {
|
|
504
|
-
node.children = toNodes(await listDir(node.path), node.path, node.depth + 1);
|
|
505
|
-
sortTree(node.children);
|
|
506
|
-
node.loaded = true;
|
|
507
|
-
} catch (error) {
|
|
508
|
-
showToast(t("folderFailed", { message: errorText(error) }));
|
|
509
|
-
return;
|
|
510
|
-
} finally {
|
|
511
|
-
node.loading = false;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
576
|
+
if (!await loadChildren(node)) return;
|
|
514
577
|
node.expanded = true;
|
|
515
578
|
}
|
|
516
579
|
const visible = computed(() => {
|
|
@@ -735,6 +798,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
735
798
|
key: node.path,
|
|
736
799
|
class: normalizeClass(["fl-row", {
|
|
737
800
|
"fl-row-dir": node.is_dir,
|
|
801
|
+
"fl-row-root": node.isRoot,
|
|
738
802
|
"fl-row-selected": selected.value.has(node.path)
|
|
739
803
|
}])
|
|
740
804
|
}, [
|
|
@@ -746,23 +810,24 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
746
810
|
}, null, 40, _hoisted_23)]),
|
|
747
811
|
createElementVNode("span", {
|
|
748
812
|
class: "fl-cell fl-cell-name",
|
|
813
|
+
title: node.isRoot ? node.path : void 0,
|
|
749
814
|
style: normalizeStyle({ paddingLeft: `${node.depth * 22}px` })
|
|
750
815
|
}, [node.is_dir ? (openBlock(), createElementBlock("button", {
|
|
751
816
|
key: 0,
|
|
752
817
|
class: normalizeClass(["fl-arrow", { "fl-arrow-open": node.expanded }]),
|
|
753
818
|
type: "button",
|
|
754
819
|
onClick: ($event) => toggleExpand(node)
|
|
755
|
-
}, "▸", 10,
|
|
820
|
+
}, "▸", 10, _hoisted_25)) : (openBlock(), createElementBlock("span", _hoisted_26)), createElementVNode("span", {
|
|
756
821
|
class: normalizeClass(["fl-name", { "fl-name-link": node.is_dir }]),
|
|
757
822
|
onClick: ($event) => node.is_dir ? toggleExpand(node) : download(node)
|
|
758
823
|
}, [
|
|
759
|
-
node.is_dir ? (openBlock(), createElementBlock("span",
|
|
824
|
+
node.is_dir ? (openBlock(), createElementBlock("span", _hoisted_28, "📁")) : (openBlock(), createElementBlock("span", _hoisted_29, toDisplayString(unref(extOf)(node.name) || "FILE"), 1)),
|
|
760
825
|
createTextVNode(" " + toDisplayString(node.name) + " ", 1),
|
|
761
|
-
node.loading ? (openBlock(), createElementBlock("span",
|
|
762
|
-
], 10,
|
|
763
|
-
createElementVNode("span",
|
|
764
|
-
createElementVNode("span",
|
|
765
|
-
createElementVNode("span",
|
|
826
|
+
node.loading ? (openBlock(), createElementBlock("span", _hoisted_30, "…")) : createCommentVNode("v-if", true)
|
|
827
|
+
], 10, _hoisted_27)], 12, _hoisted_24),
|
|
828
|
+
createElementVNode("span", _hoisted_31, toDisplayString(node.is_dir ? "-" : unref(formatSize)(node.size)), 1),
|
|
829
|
+
createElementVNode("span", _hoisted_32, toDisplayString(unref(formatDate)(node.modified)), 1),
|
|
830
|
+
createElementVNode("span", _hoisted_33, [!node.is_dir ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [canPreview(node) ? (openBlock(), createElementBlock("button", {
|
|
766
831
|
key: 0,
|
|
767
832
|
class: "fl-download",
|
|
768
833
|
type: "button",
|
|
@@ -782,7 +847,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
782
847
|
cx: "12",
|
|
783
848
|
cy: "12",
|
|
784
849
|
r: "3"
|
|
785
|
-
})], -1)])], 8,
|
|
850
|
+
})], -1)])], 8, _hoisted_34)) : createCommentVNode("v-if", true), createElementVNode("button", {
|
|
786
851
|
class: "fl-download",
|
|
787
852
|
type: "button",
|
|
788
853
|
title: t("download"),
|
|
@@ -801,11 +866,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
801
866
|
createElementVNode("path", { d: "M12 3v12" }),
|
|
802
867
|
createElementVNode("path", { d: "m7 10 5 5 5-5" }),
|
|
803
868
|
createElementVNode("path", { d: "M5 21h14" })
|
|
804
|
-
], -1)])], 8,
|
|
869
|
+
], -1)])], 8, _hoisted_35)], 64)) : createCommentVNode("v-if", true)])
|
|
805
870
|
], 2);
|
|
806
871
|
}), 128)),
|
|
807
|
-
createElementVNode("div",
|
|
808
|
-
toast.value ? (openBlock(), createElementBlock("div",
|
|
872
|
+
createElementVNode("div", _hoisted_36, [createElementVNode("span", null, toDisplayString(t("loaded", { count: allFiles.value.length })) + " · " + toDisplayString(t("selected", { count: selected.value.size })), 1), unloadedFolders.value ? (openBlock(), createElementBlock("span", _hoisted_37, " · " + toDisplayString(t("unloaded", { count: unloadedFolders.value })), 1)) : createCommentVNode("v-if", true)]),
|
|
873
|
+
toast.value ? (openBlock(), createElementBlock("div", _hoisted_38, toDisplayString(toast.value), 1)) : createCommentVNode("v-if", true)
|
|
809
874
|
]);
|
|
810
875
|
};
|
|
811
876
|
}
|
|
@@ -34,11 +34,25 @@ interface FileListLabels {
|
|
|
34
34
|
batchFailed: string;
|
|
35
35
|
defaultZip: string;
|
|
36
36
|
}
|
|
37
|
+
interface FileListSource {
|
|
38
|
+
/** OpenList path, directory or single file. */
|
|
39
|
+
path: string;
|
|
40
|
+
/** Root label. Defaults to the last path segment. */
|
|
41
|
+
title?: string;
|
|
42
|
+
/** Per-source folder password. */
|
|
43
|
+
password?: string;
|
|
44
|
+
/** Initial expanded state for a directory source. Overrides the global `expanded` prop. */
|
|
45
|
+
expanded?: boolean;
|
|
46
|
+
}
|
|
37
47
|
interface FileListProps {
|
|
38
48
|
/** OpenList base URL, e.g. `https://pan.example.com`. Empty string enables demo mode. */
|
|
39
49
|
base?: string;
|
|
40
50
|
/** Initial directory, defaults to `/`. */
|
|
41
51
|
path?: string;
|
|
52
|
+
/** Mount multiple paths as root nodes. When set, `path` is ignored. */
|
|
53
|
+
sources?: FileListSource[];
|
|
54
|
+
/** Expand directory roots initially. Defaults to `false`. */
|
|
55
|
+
expanded?: boolean;
|
|
42
56
|
/** Optional read-only token. Guest access is used when omitted. */
|
|
43
57
|
token?: string;
|
|
44
58
|
/** Folder password. The component also asks at runtime when OpenList reports one. */
|
|
@@ -64,6 +78,8 @@ interface OpenListEntry {
|
|
|
64
78
|
declare const _default: import("vue").DefineComponent<FileListProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<FileListProps> & Readonly<{}>, {
|
|
65
79
|
base: string;
|
|
66
80
|
path: string;
|
|
81
|
+
sources: FileListSource[];
|
|
82
|
+
expanded: boolean;
|
|
67
83
|
token: string;
|
|
68
84
|
password: string;
|
|
69
85
|
title: string;
|
|
@@ -176,4 +192,4 @@ export declare function compareNodes(a: SortableNode, b: SortableNode, key: Sort
|
|
|
176
192
|
* ```
|
|
177
193
|
*/
|
|
178
194
|
export declare function enhanceAppWithFileList({ app }: EnhanceAppContext): void;
|
|
179
|
-
export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type OpenListEntry };
|
|
195
|
+
export { _default as FileList, _default as VPFileList, type FileListLabels, type FileListLocale, type FileListProps, type FileListSource, type OpenListEntry };
|
package/dist/client/ssr/index.js
CHANGED
|
@@ -131,6 +131,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
131
131
|
props: {
|
|
132
132
|
base: { default: "" },
|
|
133
133
|
path: { default: "/" },
|
|
134
|
+
sources: { default: () => [] },
|
|
135
|
+
expanded: {
|
|
136
|
+
type: Boolean,
|
|
137
|
+
default: false
|
|
138
|
+
},
|
|
134
139
|
token: { default: "" },
|
|
135
140
|
password: { default: "" },
|
|
136
141
|
title: { default: "" },
|
|
@@ -167,6 +172,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
167
172
|
const batchBusy = ref(false);
|
|
168
173
|
const batchProgress = ref(0);
|
|
169
174
|
const downloadCache = /* @__PURE__ */ new Map();
|
|
175
|
+
let toastTimer;
|
|
176
|
+
function showToast(msg) {
|
|
177
|
+
toast.value = msg;
|
|
178
|
+
clearTimeout(toastTimer);
|
|
179
|
+
toastTimer = setTimeout(() => toast.value = "", 3200);
|
|
180
|
+
}
|
|
170
181
|
function errorText(error) {
|
|
171
182
|
return error instanceof Error ? error.message : String(error);
|
|
172
183
|
}
|
|
@@ -327,14 +338,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
327
338
|
if (payload.code !== 200) throw new ApiError(payload.message || `HTTP ${response.status}`, payload.code ?? response.status);
|
|
328
339
|
return payload.data;
|
|
329
340
|
}
|
|
330
|
-
async function listDir(path) {
|
|
341
|
+
async function listDir(path, password = activePassword.value) {
|
|
331
342
|
if (demo.value) {
|
|
332
343
|
await new Promise((resolve) => setTimeout(resolve, 220));
|
|
333
344
|
return DEMO_TREE[path] ?? [];
|
|
334
345
|
}
|
|
335
346
|
return (await request("/api/fs/list", {
|
|
336
347
|
path,
|
|
337
|
-
password
|
|
348
|
+
password,
|
|
338
349
|
page: 1,
|
|
339
350
|
per_page: 0,
|
|
340
351
|
refresh: false
|
|
@@ -353,7 +364,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
353
364
|
nodes.sort((a, b) => compareNodes(a, b, sortKey.value, sortAsc.value));
|
|
354
365
|
for (const node of nodes) if (node.children.length) sortTree(node.children);
|
|
355
366
|
}
|
|
356
|
-
function toNodes(items, parent, depth) {
|
|
367
|
+
function toNodes(items, parent, depth, password = "") {
|
|
357
368
|
return items.map((item) => ({
|
|
358
369
|
...item,
|
|
359
370
|
path: joinPath(parent, item.name),
|
|
@@ -361,16 +372,82 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
361
372
|
expanded: false,
|
|
362
373
|
loaded: false,
|
|
363
374
|
loading: false,
|
|
364
|
-
children: []
|
|
375
|
+
children: [],
|
|
376
|
+
password
|
|
365
377
|
}));
|
|
366
378
|
}
|
|
379
|
+
function sourceName(path) {
|
|
380
|
+
const parts = path.split("/").filter(Boolean);
|
|
381
|
+
return parts[parts.length - 1] || path;
|
|
382
|
+
}
|
|
383
|
+
async function loadChildren(node) {
|
|
384
|
+
if (node.loaded) return true;
|
|
385
|
+
node.loading = true;
|
|
386
|
+
try {
|
|
387
|
+
node.children = toNodes(await listDir(node.path, node.password), node.path, node.depth + 1, node.password);
|
|
388
|
+
sortTree(node.children);
|
|
389
|
+
node.loaded = true;
|
|
390
|
+
return true;
|
|
391
|
+
} catch (error) {
|
|
392
|
+
showToast(t("folderFailed", { message: errorText(error) }));
|
|
393
|
+
return false;
|
|
394
|
+
} finally {
|
|
395
|
+
node.loading = false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function loadSourceRoot(source) {
|
|
399
|
+
const path = source.path;
|
|
400
|
+
const password = source.password ?? "";
|
|
401
|
+
let item = {
|
|
402
|
+
name: sourceName(path),
|
|
403
|
+
size: 0,
|
|
404
|
+
is_dir: true,
|
|
405
|
+
modified: ""
|
|
406
|
+
};
|
|
407
|
+
if (demo.value) {
|
|
408
|
+
if (/\/[^/]+\.[^/]+$/.test(path)) item = {
|
|
409
|
+
...item,
|
|
410
|
+
is_dir: false,
|
|
411
|
+
modified: "2026-09-01T08:00:00+08:00"
|
|
412
|
+
};
|
|
413
|
+
} else try {
|
|
414
|
+
const data = await request("/api/fs/get", {
|
|
415
|
+
path,
|
|
416
|
+
password
|
|
417
|
+
});
|
|
418
|
+
item = {
|
|
419
|
+
...item,
|
|
420
|
+
...data
|
|
421
|
+
};
|
|
422
|
+
} catch {}
|
|
423
|
+
return {
|
|
424
|
+
...item,
|
|
425
|
+
name: source.title || item.name || sourceName(path),
|
|
426
|
+
path,
|
|
427
|
+
depth: 0,
|
|
428
|
+
expanded: source.expanded ?? props.expanded,
|
|
429
|
+
loaded: false,
|
|
430
|
+
loading: false,
|
|
431
|
+
children: [],
|
|
432
|
+
password,
|
|
433
|
+
isRoot: true
|
|
434
|
+
};
|
|
435
|
+
}
|
|
367
436
|
async function loadRoot() {
|
|
368
437
|
loadingRoot.value = true;
|
|
369
438
|
errorMsg.value = "";
|
|
370
439
|
needsPassword.value = false;
|
|
371
440
|
try {
|
|
441
|
+
if (props.sources.length) {
|
|
442
|
+
const nodes = await Promise.all(props.sources.map((source) => loadSourceRoot(source)));
|
|
443
|
+
roots.value = nodes;
|
|
444
|
+
for (const node of roots.value) if (node.is_dir && node.expanded) node.expanded = await loadChildren(node);
|
|
445
|
+
selected.value = /* @__PURE__ */ new Set();
|
|
446
|
+
downloadCache.clear();
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
372
449
|
const items = await listDir(props.path);
|
|
373
|
-
roots.value = toNodes(items, props.path, 0);
|
|
450
|
+
roots.value = toNodes(items, props.path, 0, activePassword.value);
|
|
374
451
|
sortTree(roots.value);
|
|
375
452
|
selected.value = /* @__PURE__ */ new Set();
|
|
376
453
|
downloadCache.clear();
|
|
@@ -454,8 +531,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
454
531
|
ssrRenderList(visible.value, (node) => {
|
|
455
532
|
_push(`<div class="${ssrRenderClass([{
|
|
456
533
|
"fl-row-dir": node.is_dir,
|
|
534
|
+
"fl-row-root": node.isRoot,
|
|
457
535
|
"fl-row-selected": selected.value.has(node.path)
|
|
458
|
-
}, "fl-row"])}"><span class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></span><span class="fl-cell fl-cell-name" style="${ssrRenderStyle({ paddingLeft: `${node.depth * 22}px` })}">`);
|
|
536
|
+
}, "fl-row"])}"><span class="fl-cell fl-cell-check"><input type="checkbox"${ssrIncludeBooleanAttr(isChecked(node)) ? " checked" : ""}${ssrRenderAttr("indeterminate", isIndeterminate(node))}></span><span class="fl-cell fl-cell-name"${ssrRenderAttr("title", node.isRoot ? node.path : void 0)} style="${ssrRenderStyle({ paddingLeft: `${node.depth * 22}px` })}">`);
|
|
459
537
|
if (node.is_dir) _push(`<button type="button" class="${ssrRenderClass([{ "fl-arrow-open": node.expanded }, "fl-arrow"])}">▸</button>`);
|
|
460
538
|
else _push(`<span class="fl-arrow-placeholder"></span>`);
|
|
461
539
|
_push(`<span class="${ssrRenderClass([{ "fl-name-link": node.is_dir }, "fl-name"])}">`);
|
package/dist/client/style.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vitepress-plugin-filelist",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"packageManager": "pnpm@11.25.0",
|
|
4
5
|
"type": "module",
|
|
5
6
|
"description": "OpenList file browser for VitePress: tree view, batch ZIP download, i18n, password and token support.",
|
|
6
7
|
"author": "nrjycyd",
|