This commit is contained in:
liRQ 2026-08-31 18:30:30 +08:00
parent 42146aedf4
commit 6faaa7984b
51 changed files with 1597 additions and 248 deletions

8
public/icon/add-file.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 添加文件图标:文档+加号 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<!-- 加号(青绿色) -->
<line x1="12" y1="13" x2="12" y2="19" stroke="#2dd4bf" stroke-width="2"/>
<line x1="9" y1="16" x2="15" y2="16" stroke="#2dd4bf" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@ -0,0 +1,7 @@
<!-- 添加文件夹图标:文件夹+加号 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<!-- 加号(青绿色) -->
<line x1="12" y1="11" x2="12" y2="17" stroke="#2dd4bf" stroke-width="2"/>
<line x1="9" y1="14" x2="15" y2="14" stroke="#2dd4bf" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 510 B

View File

@ -0,0 +1,10 @@
<!-- 批量删除图标:垃圾桶+列表 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<!-- 列表线(青绿色) -->
<line x1="10" y1="11" x2="10" y2="17" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="14" y1="11" x2="14" y2="17" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="9" y1="9" x2="15" y2="9" stroke="#2dd4bf" stroke-width="1"/>
</svg>

After

Width:  |  Height:  |  Size: 628 B

View File

@ -0,0 +1,13 @@
<!-- 批量刮削图标:电影+列表 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="6" width="18" height="14" rx="1"/>
<path d="M3 10h18"/>
<path d="M7 6V3"/>
<path d="M12 6V3"/>
<path d="M17 6V3"/>
<!-- 场记板条纹(青绿色) -->
<line x1="5" y1="8" x2="7" y2="8" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="9" y1="8" x2="11" y2="8" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="13" y1="8" x2="15" y2="8" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="17" y1="8" x2="19" y2="8" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

8
public/icon/cast.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 主演图标:面具/戏剧 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12c0-5 4-9 10-9s10 4 10 9"/>
<path d="M2 12c0 3 2 5 4 5s3-2 4-5"/>
<path d="M14 12c1 3 2 5 4 5s4-2 4-5"/>
<!-- 面具高亮(青绿色) -->
<path d="M2 12c0 3 2 5 4 5s3-2 4-5" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.3" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 492 B

5
public/icon/checkbox.svg Normal file
View File

@ -0,0 +1,5 @@
<!-- 多选图标:复选框+对勾 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3"/>
<path d="M7 12l3.5 3.5L17 9" stroke="#2dd4bf" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 346 B

7
public/icon/close.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 关闭图标X -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
<!-- 关闭线高亮(青绿色) -->
<line x1="18" y1="6" x2="6" y2="18" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

10
public/icon/delete.svg Normal file
View File

@ -0,0 +1,10 @@
<!-- 删除图标:垃圾桶 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
<!-- 桶身高亮(青绿色) -->
<path d="M9 6v12a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V6" stroke="#2dd4bf" stroke-width="0.8" opacity="0.3"/>
</svg>

After

Width:  |  Height:  |  Size: 577 B

View File

@ -0,0 +1,7 @@
<!-- 简介图标:打开的书 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
<!-- 书页高亮(青绿色) -->
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.3" opacity="0.15"/>
</svg>

After

Width:  |  Height:  |  Size: 495 B

10
public/icon/detail.svg Normal file
View File

@ -0,0 +1,10 @@
<!-- 详情图标:文档+放大镜 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="8" y1="13" x2="16" y2="13"/>
<line x1="8" y1="17" x2="12" y2="17"/>
<!-- 放大镜(青绿色) -->
<circle cx="17" cy="17" r="3" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="19.5" y1="19.5" x2="22" y2="22" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 608 B

7
public/icon/edit.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 编辑图标:铅笔 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
<!-- 铅笔尖高亮(青绿色) -->
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.3" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 529 B

8
public/icon/export.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 导出图标:上传箭头 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
<!-- 箭头高亮(青绿色) -->
<polyline points="17 8 12 3 7 8" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 469 B

7
public/icon/eye-off.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 隐藏图标:眼睛+斜线 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23"/>
<!-- 斜线高亮(青绿色) -->
<line x1="1" y1="1" x2="23" y2="23" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

7
public/icon/eye.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 显示图标:眼睛 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
<!-- 眼睛高亮(青绿色) -->
<circle cx="12" cy="12" r="1.5" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 436 B

9
public/icon/fail.svg Normal file
View File

@ -0,0 +1,9 @@
<!-- 失败图标:叉号圆圈 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<line x1="9" y1="9" x2="15" y2="15"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<!-- 叉号高亮(青绿色) -->
<line x1="9" y1="9" x2="15" y2="15" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="15" y1="9" x2="9" y2="15" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 528 B

9
public/icon/file.svg Normal file
View File

@ -0,0 +1,9 @@
<!-- 文件图标:文档 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="8" y1="13" x2="16" y2="13"/>
<line x1="8" y1="17" x2="13" y2="17"/>
<!-- 文件折角高亮(青绿色) -->
<polyline points="14 2 14 8 20 8" stroke="#2dd4bf" stroke-width="1" opacity="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -0,0 +1,7 @@
<!-- 打开的文件夹图标 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<path d="M2 10h20"/>
<!-- 文件夹内部高亮(青绿色) -->
<path d="M4 10h16v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-7z" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.3" opacity="0.15"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@ -0,0 +1,7 @@
<!-- 刷新文件夹图标:文件夹+刷新箭头 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<!-- 刷新箭头(青绿色) -->
<path d="M14 13a3 3 0 1 1-1-2.2L14 12" stroke="#2dd4bf" stroke-width="1.5"/>
<polyline points="12 10 14 12 16 10" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 527 B

6
public/icon/folder.svg Normal file
View File

@ -0,0 +1,6 @@
<!-- 文件夹图标:关闭状态 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<!-- 文件夹标签高亮(青绿色) -->
<path d="M4 5h5l2 3h9" stroke="#2dd4bf" stroke-width="1" opacity="0.4"/>
</svg>

After

Width:  |  Height:  |  Size: 439 B

8
public/icon/globe.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 网络图标:地球 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
<!-- 地球高亮(青绿色) -->
<path d="M12 2a15.3 15.3 0 0 1 4 10" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

8
public/icon/info.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 信息图标圆圈i -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
<!-- 信息符号高亮(青绿色) -->
<line x1="12" y1="16" x2="12" y2="12" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 461 B

7
public/icon/link.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 链接图标:链条 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
<!-- 链条连接处高亮(青绿色) -->
<path d="M10 13a5 5 0 0 0 2 1.5" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 487 B

10
public/icon/log.svg Normal file
View File

@ -0,0 +1,10 @@
<!-- 日志图标:剪贴板 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
<line x1="8" y1="10" x2="16" y2="10"/>
<line x1="8" y1="14" x2="16" y2="14"/>
<line x1="8" y1="18" x2="12" y2="18"/>
<!-- 日志条目高亮(青绿色) -->
<line x1="8" y1="10" x2="11" y2="10" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 609 B

8
public/icon/logout.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 退出图标:门+箭头 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
<!-- 箭头高亮(青绿色) -->
<polyline points="16 17 21 12 16 7" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 471 B

11
public/icon/movie.svg Normal file
View File

@ -0,0 +1,11 @@
<!-- 电影图标:场记板 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="6" width="18" height="14" rx="1"/>
<path d="M3 10h18"/>
<path d="M7 6V3"/>
<path d="M12 6V3"/>
<path d="M17 6V3"/>
<!-- 场记板条纹高亮(青绿色) -->
<line x1="5" y1="8" x2="7" y2="8" stroke="#2dd4bf" stroke-width="1.5"/>
<line x1="9" y1="8" x2="11" y2="8" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 557 B

10
public/icon/network.svg Normal file
View File

@ -0,0 +1,10 @@
<!-- 内外网切换图标:双向箭头 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="8" x2="20" y2="8"/>
<polyline points="16 4 20 8 16 12"/>
<line x1="20" y1="16" x2="4" y2="16"/>
<polyline points="8 12 4 16 8 20"/>
<!-- 箭头高亮(青绿色) -->
<polyline points="16 4 20 8 16 12" stroke="#2dd4bf" stroke-width="1.2"/>
<polyline points="8 12 4 16 8 20" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 577 B

154
public/icon/preview.html Normal file
View File

@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>图标预览 - 网盘管理系统</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
background: #f5f7fa;
padding: 32px;
color: #374151;
}
h1 { text-align: center; font-size: 22px; font-weight: 700; margin-bottom: 8px; }
.subtitle { text-align: center; font-size: 13px; color: #94a3b8; margin-bottom: 32px; }
.section-title {
font-size: 15px; font-weight: 600; color: #64748b; margin: 28px 0 12px;
padding-bottom: 8px; border-bottom: 1px solid #e2e8f0;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 14px;
max-width: 960px;
margin: 0 auto;
}
.icon-card {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 16px 8px 12px; background: #fff; border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06); transition: all 0.2s;
}
.icon-card:hover { box-shadow: 0 4px 16px rgba(45,212,191,0.15); transform: translateY(-2px); }
.icon-card img { width: 40px; height: 40px; object-fit: contain; }
.icon-card span { font-size: 11px; color: #64748b; text-align: center; white-space: nowrap; }
.style-info {
max-width: 960px; margin: 32px auto 0; padding: 20px 24px;
background: #fff; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
font-size: 13px; line-height: 1.8; color: #64748b;
}
.style-info strong { color: #374151; }
.color-dot { display: inline-block; width: 12px; height: 12px; border-radius: 3px; vertical-align: middle; margin-right: 4px; }
</style>
</head>
<body>
<h1>网盘管理系统 - 统一风格图标集</h1>
<p class="subtitle">深色描边 + 青绿色点缀 · 简约立体风格 · 根据代码实际使用场景生成</p>
<div class="section-title">顶部导航栏</div>
<div class="icon-grid" id="grid-topbar"></div>
<div class="section-title">用户菜单</div>
<div class="icon-grid" id="grid-user"></div>
<div class="section-title">网盘与文件管理</div>
<div class="icon-grid" id="grid-drive"></div>
<div class="section-title">刮削与日志</div>
<div class="icon-grid" id="grid-scrape"></div>
<div class="section-title">海报详情</div>
<div class="icon-grid" id="grid-poster"></div>
<div class="section-title">设置与绑定</div>
<div class="icon-grid" id="grid-settings"></div>
<div class="section-title">状态与提示</div>
<div class="icon-grid" id="grid-status"></div>
<div class="style-info">
<strong>设计规范:</strong><br/>
• 主色(描边):<span class="color-dot" style="background:#374151"></span>#374151 深灰色<br/>
• 点缀色(高亮):<span class="color-dot" style="background:#2dd4bf"></span>#2dd4bf 青绿色<br/>
• 尺寸viewBox 24×24显示 48×48px<br/>
• 风格:线条描边 + 局部色块填充,简约立体感<br/>
• 格式SVG 矢量图,可无损缩放<br/>
• 用途:替换代码中现有的 emoji 图标,实现视觉统一
</div>
<script>
const iconGroups = {
'grid-topbar': [
{ file: 'search.svg', name: '搜索' },
{ file: 'settings.svg', name: '设置' },
{ file: 'globe.svg', name: '外网' },
{ file: 'network.svg', name: '内外网切换' },
{ file: 'user.svg', name: '用户' },
{ file: 'switch-view.svg', name: '切换视图' },
],
'grid-user': [
{ file: 'movie.svg', name: '海报展示' },
{ file: 'log.svg', name: '控制台' },
{ file: 'settings.svg', name: 'TMDB配置' },
{ file: 'tools.svg', name: '设置' },
{ file: 'export.svg', name: '导出数据' },
{ file: 'refresh.svg', name: '恢复数据' },
{ file: 'logout.svg', name: '退出登录' },
],
'grid-drive': [
{ file: 'folder.svg', name: '文件夹' },
{ file: 'folder-open.svg', name: '打开文件夹' },
{ file: 'file.svg', name: '文件' },
{ file: 'add-folder.svg', name: '添加文件夹' },
{ file: 'add-file.svg', name: '添加文件' },
{ file: 'folder-refresh.svg', name: '刷新文件夹' },
{ file: 'checkbox.svg', name: '多选' },
{ file: 'batch-delete.svg', name: '批量删除' },
{ file: 'delete.svg', name: '删除' },
{ file: 'edit.svg', name: '编辑' },
],
'grid-scrape': [
{ file: 'scrape.svg', name: '刮削' },
{ file: 'batch-scrape.svg', name: '批量刮削' },
{ file: 'sync.svg', name: '同步' },
{ file: 'log.svg', name: '刮削日志' },
],
'grid-poster': [
{ file: 'movie.svg', name: '电影' },
{ file: 'cast.svg', name: '主演' },
{ file: 'tag.svg', name: '标签' },
{ file: 'detail.svg', name: '查看详情' },
{ file: 'description.svg', name: '简介' },
],
'grid-settings': [
{ file: 'link.svg', name: '绑定链接' },
{ file: 'globe.svg', name: 'OpenList地址' },
{ file: 'user.svg', name: '账号' },
{ file: 'eye.svg', name: '显示密码' },
{ file: 'eye-off.svg', name: '隐藏密码' },
],
'grid-status': [
{ file: 'success.svg', name: '成功' },
{ file: 'fail.svg', name: '失败' },
{ file: 'warning.svg', name: '警告' },
{ file: 'info.svg', name: '信息' },
{ file: 'sparkle.svg', name: '提示' },
{ file: 'save.svg', name: '保存' },
{ file: 'close.svg', name: '关闭' },
],
};
Object.entries(iconGroups).forEach(([gridId, icons]) => {
const grid = document.getElementById(gridId);
if (!grid) return;
icons.forEach(icon => {
const card = document.createElement('div');
card.className = 'icon-card';
card.innerHTML = `<img src="${icon.file}" alt="${icon.name}" /><span>${icon.name}</span>`;
grid.appendChild(card);
});
});
</script>
</body>
</html>

9
public/icon/refresh.svg Normal file
View File

@ -0,0 +1,9 @@
<!-- 刷新图标:循环箭头 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2v6h-6"/>
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/>
<path d="M3 22v-6h6"/>
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>
<!-- 箭头高亮(青绿色) -->
<path d="M21 2v6h-6" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

8
public/icon/save.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 保存图标:软盘 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
<!-- 软盘标签高亮(青绿色) -->
<rect x="9" y="15" width="6" height="4" rx="0.5" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 525 B

11
public/icon/scrape.svg Normal file
View File

@ -0,0 +1,11 @@
<!-- 刮削图标:电影+刷新 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="6" width="18" height="14" rx="1"/>
<path d="M3 10h18"/>
<path d="M7 6V3"/>
<path d="M12 6V3"/>
<path d="M17 6V3"/>
<!-- 刷新箭头(青绿色) -->
<path d="M14 13a3 3 0 1 1-1-2.2L14 12" stroke="#2dd4bf" stroke-width="1.5"/>
<polyline points="12 10 14 12 16 10" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 559 B

7
public/icon/search.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 搜索图标:放大镜 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10.5" cy="10.5" r="6.5"/>
<line x1="15.5" y1="15.5" x2="21" y2="21"/>
<!-- 镜片反光(青绿色) -->
<path d="M7 7.5a5 5 0 0 1 3-1" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 416 B

7
public/icon/settings.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 设置图标:齿轮 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
<!-- 齿轮中心高亮(青绿色) -->
<circle cx="12" cy="12" r="1.5" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1012 B

6
public/icon/sparkle.svg Normal file
View File

@ -0,0 +1,6 @@
<!-- 提示图标:星星/闪光 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z"/>
<!-- 星星高亮(青绿色) -->
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22z" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.3" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

5
public/icon/success.svg Normal file
View File

@ -0,0 +1,5 @@
<!-- 成功图标:对勾圆圈 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<path d="M8 12l3 3 5-6" stroke="#2dd4bf" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1,10 @@
<!-- 切换视图图标:⇄ 双向切换 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/>
<path d="M3 11V9a4 4 0 0 1 4-4h14"/>
<polyline points="7 23 3 19 7 15"/>
<path d="M21 13v2a4 4 0 0 1-4 4H3"/>
<!-- 箭头高亮(青绿色) -->
<polyline points="17 1 21 5 17 9" stroke="#2dd4bf" stroke-width="1.2"/>
<polyline points="7 23 3 19 7 15" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

7
public/icon/sync.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 同步图标:云端同步 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
<polyline points="21 3 21 9 15 9"/>
<!-- 同步箭头高亮(青绿色) -->
<polyline points="21 3 21 9 15 9" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 422 B

7
public/icon/tag.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 标签图标 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
<line x1="7" y1="7" x2="7.01" y2="7"/>
<!-- 标签孔高亮(青绿色) -->
<circle cx="7" cy="7" r="1" fill="#2dd4bf" stroke="#2dd4bf" stroke-width="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 467 B

6
public/icon/tools.svg Normal file
View File

@ -0,0 +1,6 @@
<!-- 工具图标:扳手 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
<!-- 扳手头部高亮(青绿色) -->
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 513 B

7
public/icon/user.svg Normal file
View File

@ -0,0 +1,7 @@
<!-- 用户图标:人物轮廓 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
<!-- 头像高亮(青绿色) -->
<path d="M10 5.5a3 3 0 0 1 2-1" stroke="#2dd4bf" stroke-width="1.2"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

8
public/icon/warning.svg Normal file
View File

@ -0,0 +1,8 @@
<!-- 警告图标:三角感叹号 -->
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#374151" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
<!-- 感叹号高亮(青绿色) -->
<line x1="12" y1="9" x2="12" y2="13" stroke="#2dd4bf" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 534 B

View File

@ -41,10 +41,11 @@
</Transition> </Transition>
<section class="search-topbar"> <section class="search-topbar">
<div class="search-topbar__input"> <div class="search-topbar__input">
<span class="search-input-icon"><AppIcon name="search" :size="18" /></span>
<input <input
v-model="searchKeyword" v-model="searchKeyword"
type="search" type="search"
placeholder="🔍 输入关键词搜索..." placeholder="输入关键词搜索..."
@focus="isSearchFocused = true" @focus="isSearchFocused = true"
@blur="isSearchFocused = false" @blur="isSearchFocused = false"
@keydown.enter="handleHeroSearch" @keydown.enter="handleHeroSearch"
@ -59,22 +60,7 @@
@click="toggleDarkMode" @click="toggleDarkMode"
:title="isDarkMode ? '切换到白天模式' : '切换到黑夜模式'" :title="isDarkMode ? '切换到白天模式' : '切换到黑夜模式'"
> >
<svg class="theme-toggle-btn__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <AppIcon :name="isDarkMode ? 'moon' : 'sun'" :size="20" />
<g v-if="!isDarkMode" class="theme-icon--sun">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</g>
<g v-if="isDarkMode" class="theme-icon--moon">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</g>
</svg>
</button> </button>
<!-- 快捷入口按钮和悬浮面板 --> <!-- 快捷入口按钮和悬浮面板 -->
@ -113,7 +99,7 @@
@click="networkMode = 'internal'" @click="networkMode = 'internal'"
>内网</button> >内网</button>
</div> </div>
<button v-if="!hasSearched" type="button" class="quick-icons__manage" @click="showIconManageDialog = true" title="管理图标"></button> <button v-if="!hasSearched" type="button" class="quick-icons__manage" @click="showIconManageDialog = true" title="管理图标"><AppIcon name="settings" :size="18" /></button>
</div> </div>
</div> </div>
<div v-if="iconList.length" class="quick-icons__grid"> <div v-if="iconList.length" class="quick-icons__grid">
@ -138,7 +124,7 @@
</a> </a>
</div> </div>
<div v-else class="quick-icons__empty"> <div v-else class="quick-icons__empty">
<span>暂无快捷图标点击 添加</span> <span>暂无快捷图标点击 <AppIcon name="settings" :size="14" /> 添加</span>
</div> </div>
</div> </div>
</Transition> </Transition>
@ -180,29 +166,29 @@
</div> </div>
<div class="user-menu__divider"></div> <div class="user-menu__divider"></div>
<button type="button" class="user-menu__item user-menu__item--toggle" @click="toggleView" :title="currentView === 'manage' ? '切换到海报展示' : '切换到控制台'"> <button type="button" class="user-menu__item user-menu__item--toggle" @click="toggleView" :title="currentView === 'manage' ? '切换到海报展示' : '切换到控制台'">
<span class="user-menu__icon">{{ currentView === 'manage' ? '🎬' : '📋' }}</span> <span class="user-menu__icon"><AppIcon :name="currentView === 'manage' ? 'movie' : 'log'" :size="18" /></span>
<span>{{ currentView === 'manage' ? '海报展示' : '控制台' }}</span> <span>{{ currentView === 'manage' ? '海报展示' : '控制台' }}</span>
<span class="user-menu__toggle-icon"></span> <span class="user-menu__toggle-icon"></span>
</button> </button>
<button type="button" class="user-menu__item" @click="handleTmdbConfig"> <button type="button" class="user-menu__item" @click="handleTmdbConfig">
<span class="user-menu__icon"></span> <span class="user-menu__icon"><AppIcon name="settings" :size="18" /></span>
<span>TMDB 配置</span> <span>TMDB 配置</span>
</button> </button>
<button type="button" class="user-menu__item" @click="handleSettings"> <button type="button" class="user-menu__item" @click="handleSettings">
<span class="user-menu__icon">🛠</span> <span class="user-menu__icon"><AppIcon name="tools" :size="18" /></span>
<span>设置</span> <span>设置</span>
</button> </button>
<button type="button" class="user-menu__item" @click="handleExport"> <button type="button" class="user-menu__item" @click="handleExport">
<span class="user-menu__icon">📤</span> <span class="user-menu__icon"><AppIcon name="export" :size="18" /></span>
<span>导出数据</span> <span>导出数据</span>
</button> </button>
<button type="button" class="user-menu__item" @click="handleReset"> <button type="button" class="user-menu__item" @click="handleReset">
<span class="user-menu__icon">🔄</span> <span class="user-menu__icon"><AppIcon name="refresh" :size="18" /></span>
<span>恢复初始数据</span> <span>恢复初始数据</span>
</button> </button>
<div class="user-menu__divider"></div> <div class="user-menu__divider"></div>
<button type="button" class="user-menu__item user-menu__item--danger" @click="handleLogout"> <button type="button" class="user-menu__item user-menu__item--danger" @click="handleLogout">
<span class="user-menu__icon">🚪</span> <span class="user-menu__icon"><AppIcon name="logout" :size="18" /></span>
<span>退出登录</span> <span>退出登录</span>
</button> </button>
</div> </div>
@ -222,13 +208,12 @@
<!-- 主海报横幅 --> <!-- 主海报横幅 -->
<Transition name="hero-fade" mode="out-in"> <Transition name="hero-fade" mode="out-in">
<article <article
v-if="featuredPosters.length"
:key="activeFeaturedPoster.id" :key="activeFeaturedPoster.id"
class="hero-main" class="hero-main"
:class="{ :class="{
'hero-loading': !heroImageLoaded, 'hero-loading': !heroImageLoaded,
'hero-loaded': heroImageLoaded, 'hero-loaded': heroImageLoaded,
'hero-wide': isWidePoster(activeFeaturedPoster.posterUrl) 'hero-wide': isWidePoster(activeFeaturedPoster.posterUrl) || isHeroWideDynamic
}" }"
:style="buildHeroStyle(activeFeaturedPoster)" :style="buildHeroStyle(activeFeaturedPoster)"
tabindex="0" tabindex="0"
@ -241,8 +226,7 @@
> >
<!-- 海报图片层 --> <!-- 海报图片层 -->
<img <img
v-if="activeFeaturedPoster.posterUrl" :src="activeFeaturedPoster.posterUrl || '/暂无新片上映海报生成.png'"
:src="activeFeaturedPoster.posterUrl"
:alt="activeFeaturedPoster.title" :alt="activeFeaturedPoster.title"
class="hero-bg-image" class="hero-bg-image"
:class="{ 'hero-bg-image--wide': isHeroWideDynamic }" :class="{ 'hero-bg-image--wide': isHeroWideDynamic }"
@ -255,9 +239,12 @@
<h2 class="hero-main__title">{{ activeFeaturedPoster.title }}</h2> <h2 class="hero-main__title">{{ activeFeaturedPoster.title }}</h2>
<p class="hero-main__desc">{{ activeFeaturedPoster.subtitle || activeFeaturedPoster.description || '根据网盘资源内容自动匹配海报展示。' }}</p> <p class="hero-main__desc">{{ activeFeaturedPoster.subtitle || activeFeaturedPoster.description || '根据网盘资源内容自动匹配海报展示。' }}</p>
<div class="hero-main__meta"> <div class="hero-main__meta">
<span v-if="activeFeaturedPoster.accountName" class="hero-main__meta-item">📁 {{ activeFeaturedPoster.accountName }}</span> <span v-if="activeFeaturedPoster.rating" class="hero-main__meta-item"><AppIcon name="sparkle" :size="14" /> {{ activeFeaturedPoster.rating }}</span>
<span v-if="activeFeaturedPoster.owner" class="hero-main__meta-item">👤 {{ activeFeaturedPoster.owner }}</span> <template v-if="activeFeaturedPoster.tags && activeFeaturedPoster.tags.length">
<span v-if="activeFeaturedPoster.status" class="hero-main__meta-item">{{ activeFeaturedPoster.status === '已删除' ? '🗑' : '' }} {{ activeFeaturedPoster.status }}</span> <span v-for="tag in activeFeaturedPoster.tags" :key="tag" class="hero-main__meta-item hero-main__tag"><AppIcon name="tag" :size="14" /> {{ tag }}</span>
</template>
<span v-if="activeFeaturedPoster.casts" class="hero-main__meta-item"><AppIcon name="cast" :size="14" /> {{ activeFeaturedPoster.casts }}</span>
<span v-if="activeFeaturedPoster.directors" class="hero-main__meta-item"><AppIcon name="movie" :size="14" /> {{ activeFeaturedPoster.directors }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -291,7 +278,7 @@
<Transition name="fade" mode="out-in"> <Transition name="fade" mode="out-in">
<section v-if="isSearchFocused && !hasSearched" class="search-focus-hint" key="search-hint"> <section v-if="isSearchFocused && !hasSearched" class="search-focus-hint" key="search-hint">
<div class="search-focus-content"> <div class="search-focus-content">
<div class="search-focus-icon"></div> <div class="search-focus-icon"><AppIcon name="sparkle" :size="32" /></div>
<h3>输入搜索关键词</h3> <h3>输入搜索关键词</h3>
<p>搜索标题标签资源库或维护人</p> <p>搜索标题标签资源库或维护人</p>
</div> </div>
@ -320,7 +307,7 @@
@click="networkMode = 'internal'" @click="networkMode = 'internal'"
>内网</button> >内网</button>
</div> </div>
<button v-if="!hasSearched" type="button" class="quick-icons__manage" @click="showIconManageDialog = true" title="管理图标"> 管理</button> <button v-if="!hasSearched" type="button" class="quick-icons__manage" @click="showIconManageDialog = true" title="管理图标"><AppIcon name="settings" :size="16" /> 管理</button>
</div> </div>
</div> </div>
<div v-if="iconList.length" class="quick-icons__grid"> <div v-if="iconList.length" class="quick-icons__grid">
@ -345,7 +332,7 @@
</a> </a>
</div> </div>
<div v-else-if="!hasSearched" class="quick-icons__empty"> <div v-else-if="!hasSearched" class="quick-icons__empty">
<span>暂无快捷图标点击 添加</span> <span>暂无快捷图标点击 <AppIcon name="settings" :size="14" /> 添加</span>
</div> </div>
</section> </section>
@ -387,7 +374,7 @@
:alt="sheet.name" :alt="sheet.name"
class="drive-tab__logo" class="drive-tab__logo"
/> />
<div v-else class="drive-tab__logo drive-tab__logo--fallback">📁</div> <div v-else class="drive-tab__logo drive-tab__logo--fallback"><AppIcon name="folder" :size="20" /></div>
<span class="drive-tab__name">{{ sheet.name }}</span> <span class="drive-tab__name">{{ sheet.name }}</span>
</div> </div>
</div> </div>
@ -405,10 +392,10 @@
:class="{ active: multiSelectMode }" :class="{ active: multiSelectMode }"
@click="toggleMultiSelectMode" @click="toggleMultiSelectMode"
> >
多选删除 <AppIcon name="checkbox" :size="16" /> 多选删除
</button> </button>
<button type="button" class="folder-tree-actions__btn" @click="handleOpenScraperLogDialog"> <button type="button" class="folder-tree-actions__btn" @click="handleOpenScraperLogDialog">
📋 刮削日志 <AppIcon name="log" :size="16" /> 刮削日志
</button> </button>
<button <button
type="button" type="button"
@ -416,7 +403,7 @@
@click="handleRefreshFolders" @click="handleRefreshFolders"
title="刷新当前网盘文件夹列表" title="刷新当前网盘文件夹列表"
> >
🗂 刷新文件夹 <AppIcon name="folder-refresh" :size="16" /> 刷新文件夹
</button> </button>
<button <button
type="button" type="button"
@ -424,7 +411,7 @@
@click="handleRescrapeAll" @click="handleRescrapeAll"
title="重新刮削所有电影文件" title="重新刮削所有电影文件"
> >
🔄 重新刮削 <AppIcon name="refresh" :size="16" /> 重新刮削
</button> </button>
<button <button
type="button" type="button"
@ -444,14 +431,14 @@
@click="handleBatchScrape" @click="handleBatchScrape"
title="批量刮削选中的电影文件" title="批量刮削选中的电影文件"
> >
🎬 批量刮削 <AppIcon name="batch-scrape" :size="16" /> 批量刮削
</button> </button>
</div> </div>
<!-- 搜索结果为空时的提示 --> <!-- 搜索结果为空时的提示 -->
<div v-if="hasSearched && filteredRows.length === 0" class="empty-search-result"> <div v-if="hasSearched && filteredRows.length === 0" class="empty-search-result">
<div class="empty-search-content"> <div class="empty-search-content">
<div class="empty-search-icon">🔍</div> <div class="empty-search-icon"><AppIcon name="search" :size="32" /></div>
<h3>空空如也</h3> <h3>空空如也</h3>
<p>没有找到匹配{{ searchKeyword }}的内容</p> <p>没有找到匹配{{ searchKeyword }}的内容</p>
<button type="button" class="secondary" @click="hasSearched = false; searchKeyword = ''"> <button type="button" class="secondary" @click="hasSearched = false; searchKeyword = ''">
@ -602,7 +589,7 @@
</div> </div>
<div v-else class="empty-search-result"> <div v-else class="empty-search-result">
<div class="empty-search-content"> <div class="empty-search-content">
<div class="empty-search-icon">🔍</div> <div class="empty-search-icon"><AppIcon name="search" :size="32" /></div>
<h3>空空如也</h3> <h3>空空如也</h3>
<p>没有找到匹配{{ searchKeyword }}的内容</p> <p>没有找到匹配{{ searchKeyword }}的内容</p>
<button type="button" class="secondary" @click="hasSearched = false; searchKeyword = ''"> <button type="button" class="secondary" @click="hasSearched = false; searchKeyword = ''">
@ -662,7 +649,7 @@
<div v-if="log.showDetail && log.detailList && log.detailList.length > 0" class="log-entry__detail"> <div v-if="log.showDetail && log.detailList && log.detailList.length > 0" class="log-entry__detail">
<template v-for="(item, idx) in log.detailList" :key="idx"> <template v-for="(item, idx) in log.detailList" :key="idx">
<div class="detail-item detail-item--user"> <div class="detail-item detail-item--user">
<span class="detail-item__name">👤 {{ item.username || '用户 ' + (item.userId || idx) }}</span> <span class="detail-item__name"><AppIcon name="user" :size="14" /> {{ item.username || '用户 ' + (item.userId || idx) }}</span>
<span class="detail-item__status detail-item__status--info"> <span class="detail-item__status detail-item__status--info">
处理: {{ item.processed || 0 }} | 成功: {{ item.success || 0 }} | 失败: {{ item.failed || 0 }} | 跳过: {{ item.skipped || 0 }} 处理: {{ item.processed || 0 }} | 成功: {{ item.success || 0 }} | 失败: {{ item.failed || 0 }} | 跳过: {{ item.skipped || 0 }}
</span> </span>
@ -888,15 +875,10 @@
@cancel="showConfirmDialog = false" @cancel="showConfirmDialog = false"
/> />
<!-- 设置页面 --> <!-- 设置页面使用 SimpleDialog 统一弹窗方式 -->
<Transition name="fade"> <SimpleDialog :open="showSettingsView" title="设置" @close="showSettingsView = false">
<div v-if="showSettingsView" class="settings-overlay" @click.self="showSettingsView = false"> <SettingsView :key="settingsKey" class="settings-in-dialog" />
<div class="settings-container"> </SimpleDialog>
<button type="button" class="settings-close" @click="showSettingsView = false" title="关闭设置"></button>
<SettingsView />
</div>
</div>
</Transition>
<!-- 网盘标签页悬浮描述提示框Teleport到body避免被父容器overflow裁剪 --> <!-- 网盘标签页悬浮描述提示框Teleport到body避免被父容器overflow裁剪 -->
<Teleport to="body"> <Teleport to="body">
@ -920,6 +902,7 @@
<script setup> <script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, onUnmounted } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, onUnmounted } from 'vue'
import AppIcon from './components/AppIcon.vue'
import LoginView from './components/LoginView.vue' import LoginView from './components/LoginView.vue'
import PosterCard from './components/PosterCard.vue' import PosterCard from './components/PosterCard.vue'
import RecordDialog from './components/RecordDialog.vue' import RecordDialog from './components/RecordDialog.vue'
@ -1009,7 +992,7 @@ const confirmConfig = reactive({
title: '确认操作', title: '确认操作',
message: '', message: '',
hint: '', hint: '',
icon: '⚠️', icon: 'warning',
confirmText: '确认', confirmText: '确认',
cancelText: '取消', cancelText: '取消',
danger: false, danger: false,
@ -1263,17 +1246,12 @@ function buildHeroStyle(poster) {
function buildSubThumbStyle(poster) { function buildSubThumbStyle(poster) {
// 使 // 使
if (!poster || poster.trim() === '') { const imgUrl = poster && poster.trim() !== '' ? poster : '/暂无新片上映海报生成.png'
return { return {
backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.3), rgba(8, 15, 29, 0.7)), url('/暂无新片上映海报生成.png')`, backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.3), rgba(8, 15, 29, 0.7)), url(${imgUrl})`,
backgroundSize: 'cover', backgroundSize: 'contain',
backgroundPosition: 'center',
}
}
return {
backgroundImage: `linear-gradient(180deg, rgba(8, 15, 29, 0.3), rgba(8, 15, 29, 0.7)), url(${poster})`,
backgroundSize: 'cover',
backgroundPosition: 'center', backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
} }
} }
@ -2078,9 +2056,11 @@ function handleTmdbConfig() {
// //
const showSettingsView = ref(false) const showSettingsView = ref(false)
const settingsKey = ref(0)
function handleSettings() { function handleSettings() {
showUserMenu.value = false showUserMenu.value = false
settingsKey.value++
showSettingsView.value = true showSettingsView.value = true
} }
@ -2309,7 +2289,7 @@ function handleDeleteNode(node) {
title: '确认删除', title: '确认删除',
message: `确定要删除 <strong>“${node.title}”</strong> 吗?`, message: `确定要删除 <strong>“${node.title}”</strong> 吗?`,
hint: isFolder ? '注意:该文件夹下的所有子文件夹和文件都将被永久删除!此操作不可恢复。' : '', hint: isFolder ? '注意:该文件夹下的所有子文件夹和文件都将被永久删除!此操作不可恢复。' : '',
icon: '⚠️', icon: 'warning',
confirmText: '确认删除', confirmText: '确认删除',
cancelText: '取消', cancelText: '取消',
danger: true, danger: true,
@ -2503,7 +2483,7 @@ function handleDeleteSheet(sheet) {
title: '确认删除', title: '确认删除',
message: `确定要删除网盘 <strong>“${sheet.name}”</strong> 吗?`, message: `确定要删除网盘 <strong>“${sheet.name}”</strong> 吗?`,
hint: '注意:该网盘下的所有文件和文件夹都将被永久删除!此操作不可恢复。', hint: '注意:该网盘下的所有文件和文件夹都将被永久删除!此操作不可恢复。',
icon: '⚠️', icon: 'warning',
confirmText: '确认删除', confirmText: '确认删除',
cancelText: '取消', cancelText: '取消',
danger: true, danger: true,
@ -2774,10 +2754,12 @@ async function loadRecommendMovies() {
id: movie.id, id: movie.id,
title: movie.title || '未知标题', title: movie.title || '未知标题',
description: movie.description || '', description: movie.description || '',
posterUrl: movie.posterUrl || '', posterUrl: movie.backdropUrl || '',
rating: movie.rating || 0, rating: movie.rating || 0,
category: movie.category || '', category: movie.category || '',
tags: typeof movie.tags === 'string' ? movie.tags.split(',') : movie.tags || [], tags: typeof movie.tags === 'string' ? movie.tags.split(',') : movie.tags || [],
casts: movie.casts || '',
directors: movie.directors || '',
createTime: movie.createTime || '', createTime: movie.createTime || '',
})) }))
console.log(`成功加载 ${recommendMovies.value.length} 条每日推荐`) console.log(`成功加载 ${recommendMovies.value.length} 条每日推荐`)
@ -2821,9 +2803,9 @@ watch(
// //
heroImageLoaded.value = false heroImageLoaded.value = false
// URL // URL
if (!newPosterUrl || newPosterUrl.trim() === '') { if (!newPosterUrl || newPosterUrl.trim() === '') {
heroImageLoaded.value = true preloadImage('/暂无新片上映海报生成.png')
return return
} }
@ -2998,7 +2980,7 @@ function handleRescrapeAll() {
title: '确认重新刮削', title: '确认重新刮削',
message: '确定要重新刮削当前网盘下的所有电影文件吗?', message: '确定要重新刮削当前网盘下的所有电影文件吗?',
hint: '注意:这将重新发起对所有电影文件的 TMDB 海报刮削请求,可能需要较长时间。', hint: '注意:这将重新发起对所有电影文件的 TMDB 海报刮削请求,可能需要较长时间。',
icon: '🔄', icon: 'refresh',
confirmText: '确认刮削', confirmText: '确认刮削',
cancelText: '取消', cancelText: '取消',
danger: false, danger: false,
@ -3160,7 +3142,7 @@ async function handleBatchDelete(selectedIdSet) {
title: '确认批量删除', title: '确认批量删除',
message: `确定要删除选中的 <strong>${count}</strong> 个项目吗?`, message: `确定要删除选中的 <strong>${count}</strong> 个项目吗?`,
hint: '注意:选中的所有文件夹和文件都将被永久删除!此操作不可恢复。', hint: '注意:选中的所有文件夹和文件都将被永久删除!此操作不可恢复。',
icon: '⚠️', icon: 'warning',
confirmText: '确认删除', confirmText: '确认删除',
cancelText: '取消', cancelText: '取消',
danger: true, danger: true,

363
src/components/AppIcon.vue Normal file
View File

@ -0,0 +1,363 @@
<!--
统一图标组件 - 支持白天/黑夜两种主题
用法: <AppIcon name="search" size="20" />
主题颜色通过 CSS 变量自动适配:
- 白天模式: 描边 #374151, 点缀 #0f766e (品牌青)
- 黑夜模式: 描边 #94a3b8, 点缀 #38bdf8 (品牌蓝)
-->
<template>
<span class="app-icon" :style="{ width: size + 'px', height: size + 'px' }">
<!-- 搜索 -->
<svg v-if="name === 'search'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10.5" cy="10.5" r="6.5"/>
<line x1="15.5" y1="15.5" x2="21" y2="21"/>
<path d="M7 7.5a5 5 0 0 1 3-1" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 设置/齿轮 -->
<svg v-else-if="name === 'settings'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
<circle cx="12" cy="12" r="1.5" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.5"/>
</svg>
<!-- 文件夹(关闭) -->
<svg v-else-if="name === 'folder'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<path d="M4 5h5l2 3h9" stroke="var(--icon-accent)" stroke-width="1" opacity="0.4"/>
</svg>
<!-- 文件夹(打开) -->
<svg v-else-if="name === 'folder-open'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<path d="M2 10h20"/>
<path d="M4 10h16v7a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-7z" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.3" opacity="0.15"/>
</svg>
<!-- 用户 -->
<svg v-else-if="name === 'user'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
<path d="M10 5.5a3 3 0 0 1 2-1" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 成功/对勾 -->
<svg v-else-if="name === 'success'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<path d="M8 12l3 3 5-6" stroke="var(--icon-accent)" stroke-width="2"/>
</svg>
<!-- 删除/垃圾桶 -->
<svg v-else-if="name === 'delete'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
<path d="M9 6v12a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V6" stroke="var(--icon-accent)" stroke-width="0.8" opacity="0.3"/>
</svg>
<!-- 刷新 -->
<svg v-else-if="name === 'refresh'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 2v6h-6"/>
<path d="M3 12a9 9 0 0 1 15-6.7L21 8"/>
<path d="M3 22v-6h6"/>
<path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>
<path d="M21 2v6h-6" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 电影/场记板 -->
<svg v-else-if="name === 'movie'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="6" width="18" height="14" rx="1"/>
<path d="M3 10h18"/>
<path d="M7 6V3"/>
<path d="M12 6V3"/>
<path d="M17 6V3"/>
<line x1="5" y1="8" x2="7" y2="8" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="9" y1="8" x2="11" y2="8" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 日志/剪贴板 -->
<svg v-else-if="name === 'log'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
<line x1="8" y1="10" x2="16" y2="10"/>
<line x1="8" y1="14" x2="16" y2="14"/>
<line x1="8" y1="18" x2="12" y2="18"/>
<line x1="8" y1="10" x2="11" y2="10" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 工具/扳手 -->
<svg v-else-if="name === 'tools'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 导出/上传 -->
<svg v-else-if="name === 'export'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
<polyline points="17 8 12 3 7 8" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 退出登录 -->
<svg v-else-if="name === 'logout'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
<polyline points="16 17 21 12 16 7" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 提示/闪光 -->
<svg v-else-if="name === 'sparkle'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z"/>
<path d="M12 2l2.4 7.2L22 12l-7.6 2.8L12 22z" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.3" opacity="0.2"/>
</svg>
<!-- 保存/软盘 -->
<svg v-else-if="name === 'save'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
<rect x="9" y="15" width="6" height="4" rx="0.5" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.5"/>
</svg>
<!-- 链接 -->
<svg v-else-if="name === 'link'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
<path d="M10 13a5 5 0 0 0 2 1.5" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 地球/网络 -->
<svg v-else-if="name === 'globe'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
<path d="M12 2a15.3 15.3 0 0 1 4 10" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 眼睛(显示) -->
<svg v-else-if="name === 'eye'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
<circle cx="12" cy="12" r="1.5" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.5"/>
</svg>
<!-- 眼睛关闭(隐藏) -->
<svg v-else-if="name === 'eye-off'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
<line x1="1" y1="1" x2="23" y2="23" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 多选/复选框 -->
<svg v-else-if="name === 'checkbox'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3"/>
<path d="M7 12l3.5 3.5L17 9" stroke="var(--icon-accent)" stroke-width="2"/>
</svg>
<!-- 警告 -->
<svg v-else-if="name === 'warning'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<!-- 文件/文档 -->
<svg v-else-if="name === 'file'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="8" y1="13" x2="16" y2="13"/>
<line x1="8" y1="17" x2="13" y2="17"/>
<polyline points="14 2 14 8 20 8" stroke="var(--icon-accent)" stroke-width="1" opacity="0.5"/>
</svg>
<!-- 信息 -->
<svg v-else-if="name === 'info'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<!-- 内外网切换 -->
<svg v-else-if="name === 'network'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="8" x2="20" y2="8"/>
<polyline points="16 4 20 8 16 12" stroke="var(--icon-accent)" stroke-width="1.2"/>
<line x1="20" y1="16" x2="4" y2="16"/>
<polyline points="8 12 4 16 8 20" stroke="var(--icon-accent)" stroke-width="1.2"/>
</svg>
<!-- 标签 -->
<svg v-else-if="name === 'tag'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
<circle cx="7" cy="7" r="1" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.5"/>
</svg>
<!-- 主演/面具 -->
<svg v-else-if="name === 'cast'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12c0-5 4-9 10-9s10 4 10 9"/>
<path d="M2 12c0 3 2 5 4 5s3-2 4-5"/>
<path d="M14 12c1 3 2 5 4 5s4-2 4-5"/>
<path d="M2 12c0 3 2 5 4 5s3-2 4-5" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.3" opacity="0.2"/>
</svg>
<!-- 详情 -->
<svg v-else-if="name === 'detail'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="8" y1="13" x2="16" y2="13"/>
<line x1="8" y1="17" x2="12" y2="17"/>
<circle cx="17" cy="17" r="3" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="19.5" y1="19.5" x2="22" y2="22" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 简介/书本 -->
<svg v-else-if="name === 'description'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.3" opacity="0.15"/>
</svg>
<!-- 编辑/铅笔 -->
<svg v-else-if="name === 'edit'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4z" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.3" opacity="0.2"/>
</svg>
<!-- 同步 -->
<svg v-else-if="name === 'sync'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
<polyline points="21 3 21 9 15 9" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 失败/叉号 -->
<svg v-else-if="name === 'fail'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<line x1="9" y1="9" x2="15" y2="15" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="15" y1="9" x2="9" y2="15" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 关闭/X -->
<svg v-else-if="name === 'close'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" stroke="var(--icon-accent)" stroke-width="1.2"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
<!-- 太阳(白天模式) -->
<svg v-else-if="name === 'sun'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"/>
<line x1="12" y1="1" x2="12" y2="3"/>
<line x1="12" y1="21" x2="12" y2="23"/>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
<line x1="1" y1="12" x2="3" y2="12"/>
<line x1="21" y1="12" x2="23" y2="12"/>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
<circle cx="12" cy="12" r="2.5" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.5" opacity="0.3"/>
</svg>
<!-- 月亮(黑夜模式) -->
<svg v-else-if="name === 'moon'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
<path d="M21 12.79A9 9 0 0 1 12 21" fill="var(--icon-accent)" stroke="var(--icon-accent)" stroke-width="0.3" opacity="0.2"/>
</svg>
<!-- 添加文件夹 -->
<svg v-else-if="name === 'add-folder'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
<line x1="12" y1="11" x2="12" y2="17" stroke="var(--icon-accent)" stroke-width="2"/>
<line x1="9" y1="14" x2="15" y2="14" stroke="var(--icon-accent)" stroke-width="2"/>
</svg>
<!-- 添加文件 -->
<svg v-else-if="name === 'add-file'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="12" y1="13" x2="12" y2="19" stroke="var(--icon-accent)" stroke-width="2"/>
<line x1="9" y1="16" x2="15" y2="16" stroke="var(--icon-accent)" stroke-width="2"/>
</svg>
<!-- 批量删除 -->
<svg v-else-if="name === 'batch-delete'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
<line x1="10" y1="11" x2="10" y2="17" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="14" y1="11" x2="14" y2="17" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="9" y1="9" x2="15" y2="9" stroke="var(--icon-accent)" stroke-width="1"/>
</svg>
<!-- 批量刮削 -->
<svg v-else-if="name === 'batch-scrape'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="6" width="18" height="14" rx="1"/>
<path d="M3 10h18"/>
<path d="M7 6V3"/><path d="M12 6V3"/><path d="M17 6V3"/>
<line x1="5" y1="8" x2="7" y2="8" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="9" y1="8" x2="11" y2="8" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="13" y1="8" x2="15" y2="8" stroke="var(--icon-accent)" stroke-width="1.5"/>
<line x1="17" y1="8" x2="19" y2="8" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 刮削 -->
<svg v-else-if="name === 'scrape'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="6" width="18" height="14" rx="1"/>
<path d="M3 10h18"/>
<path d="M7 6V3"/><path d="M12 6V3"/><path d="M17 6V3"/>
<path d="M14 13a3 3 0 1 1-1-2.2L14 12" stroke="var(--icon-accent)" stroke-width="1.5"/>
<polyline points="12 10 14 12 16 10" stroke="var(--icon-accent)" stroke-width="1.5"/>
</svg>
<!-- 切换视图 -->
<svg v-else-if="name === 'switch-view'" viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9" stroke="var(--icon-accent)" stroke-width="1.2"/>
<path d="M3 11V9a4 4 0 0 1 4-4h14"/>
<polyline points="7 23 3 19 7 15" stroke="var(--icon-accent)" stroke-width="1.2"/>
<path d="M21 13v2a4 4 0 0 1-4 4H3"/>
</svg>
<!-- 默认占位 -->
<svg v-else viewBox="0 0 24 24" fill="none" stroke="var(--icon-stroke)" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
</svg>
</span>
</template>
<script setup>
/**
* AppIcon 组件属性
* @prop {string} name - 图标名称
* @prop {number|string} size - 图标尺寸默认 20
*/
defineProps({
name: { type: String, default: '' },
size: { type: [Number, String], default: 20 },
})
</script>
<style scoped>
/*
* 图标容器 - 使用 CSS 变量实现主题自适应
* 白天模式: --ink=#172033, --brand=#0f766e
* 黑夜模式: --ink=#e2e8f0, --brand=#38bdf8
*/
.app-icon {
display: inline-flex;
align-items: center;
justify-content: center;
/* 主色(描边): 继承当前文本颜色 */
--icon-stroke: var(--ink);
/* 点缀色(高亮): 继承品牌色 */
--icon-accent: var(--brand);
vertical-align: middle;
line-height: 1;
}
.app-icon svg {
width: 100%;
height: 100%;
}
</style>

View File

@ -4,7 +4,7 @@
<Transition name="confirm-scale-in"> <Transition name="confirm-scale-in">
<div v-if="showContent" class="confirm-dialog"> <div v-if="showContent" class="confirm-dialog">
<header> <header>
<span class="confirm-icon">{{ icon }}</span> <span class="confirm-icon"><AppIcon v-if="icon" :name="icon" :size="22" /></span>
{{ title }} {{ title }}
</header> </header>
<div class="confirm-body"> <div class="confirm-body">
@ -28,13 +28,14 @@
<script setup> <script setup>
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import { lockScroll, unlockScroll } from '../utils/scrollLock' import { lockScroll, unlockScroll } from '../utils/scrollLock'
import AppIcon from './AppIcon.vue'
const props = defineProps({ const props = defineProps({
open: Boolean, open: Boolean,
title: { type: String, default: '确认操作' }, title: { type: String, default: '确认操作' },
message: { type: String, default: '' }, message: { type: String, default: '' },
hint: { type: String, default: '' }, hint: { type: String, default: '' },
icon: { type: String, default: '⚠️' }, icon: { type: String, default: 'warning' },
confirmText: { type: String, default: '确认' }, confirmText: { type: String, default: '确认' },
cancelText: { type: String, default: '取消' }, cancelText: { type: String, default: '取消' },
danger: { type: Boolean, default: false }, danger: { type: Boolean, default: false },

View File

@ -40,14 +40,14 @@
:alt="drive.name" :alt="drive.name"
class="drive-logo-img" class="drive-logo-img"
/> />
<div v-else class="drive-icon-small">📁</div> <div v-else class="drive-icon-small"><AppIcon name="folder" :size="18" /></div>
</div> </div>
<!-- 网盘名称 + 悬浮描述 --> <!-- 网盘名称 + 悬浮描述 -->
<div class="drive-name-wrapper" @mouseenter="showTooltip(drive.id)" @mouseleave="hideTooltip"> <div class="drive-name-wrapper" @mouseenter="showTooltip(drive.id)" @mouseleave="hideTooltip">
<h4 class="drive-name">{{ drive.name }}</h4> <h4 class="drive-name">{{ drive.name }}</h4>
<!-- 悬浮提示图标 --> <!-- 悬浮提示图标 -->
<span v-if="drive.description" class="tooltip-trigger" title=""></span> <span v-if="drive.description" class="tooltip-trigger" title=""><AppIcon name="info" :size="16" /></span>
<!-- 悬浮描述提示框 --> <!-- 悬浮描述提示框 -->
<Transition name="tooltip-fade"> <Transition name="tooltip-fade">
@ -186,7 +186,7 @@
<dialog ref="confirmRef" class="confirm-overlay" @click.self="closeConfirmDialog" @close="closeConfirmDialog"> <dialog ref="confirmRef" class="confirm-overlay" @click.self="closeConfirmDialog" @close="closeConfirmDialog">
<Transition name="confirm-scale-in"> <Transition name="confirm-scale-in">
<div v-if="showConfirm" class="confirm-dialog"> <div v-if="showConfirm" class="confirm-dialog">
<header> 确认删除</header> <header><AppIcon name="warning" :size="20" /> 确认删除</header>
<div class="confirm-body"> <div class="confirm-body">
<p>确定要删除网盘 <strong>"{{ confirmTarget?.name }}"</strong> </p> <p>确定要删除网盘 <strong>"{{ confirmTarget?.name }}"</strong> </p>
<p class="confirm-hint">注意该网盘下的所有文件和文件夹都将被永久删除此操作不可恢复</p> <p class="confirm-hint">注意该网盘下的所有文件和文件夹都将被永久删除此操作不可恢复</p>
@ -206,6 +206,7 @@
import { ref, computed, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick } from 'vue'
import { fetchDriveList, createDrive, updateDrive, deleteDrive } from '../api' import { fetchDriveList, createDrive, updateDrive, deleteDrive } from '../api'
import { lockScroll, unlockScroll } from '../utils/scrollLock' import { lockScroll, unlockScroll } from '../utils/scrollLock'
import AppIcon from './AppIcon.vue'
// ==================== Props & Emits ==================== // ==================== Props & Emits ====================
const props = defineProps({ const props = defineProps({

View File

@ -4,10 +4,10 @@
<div v-if="multiSelectMode" class="multi-select-toolbar"> <div v-if="multiSelectMode" class="multi-select-toolbar">
<span class="multi-select-toolbar__count">已选择 {{ selectedIds.size }} </span> <span class="multi-select-toolbar__count">已选择 {{ selectedIds.size }} </span>
<button type="button" class="multi-select-toolbar__btn multi-select-toolbar__btn--danger" @click="handleBatchDelete"> <button type="button" class="multi-select-toolbar__btn multi-select-toolbar__btn--danger" @click="handleBatchDelete">
🗑 批量删除 <AppIcon name="batch-delete" :size="16" /> 批量删除
</button> </button>
<button type="button" class="multi-select-toolbar__btn" @click="exitMultiSelect"> <button type="button" class="multi-select-toolbar__btn" @click="exitMultiSelect">
退出多选 <AppIcon name="close" :size="16" /> 退出多选
</button> </button>
</div> </div>

View File

@ -34,8 +34,8 @@
</span> </span>
<span class="icon-grid-url" :title="icon.url || icon.internalUrl">{{ icon.url || icon.internalUrl }}</span> <span class="icon-grid-url" :title="icon.url || icon.internalUrl">{{ icon.url || icon.internalUrl }}</span>
<div class="icon-grid-actions"> <div class="icon-grid-actions">
<button type="button" class="action-btn" title="编辑" @click="startEdit(icon)"></button> <button type="button" class="action-btn" title="编辑" @click="startEdit(icon)"><AppIcon name="edit" :size="16" /></button>
<button type="button" class="action-btn action-btn--danger" title="删除" @click="confirmDeleteIcon(icon)">🗑</button> <button type="button" class="action-btn action-btn--danger" title="删除" @click="confirmDeleteIcon(icon)"><AppIcon name="delete" :size="16" /></button>
</div> </div>
</div> </div>
</div> </div>
@ -54,13 +54,13 @@
<span class="field-label">链接类型</span> <span class="field-label">链接类型</span>
<div class="link-type-group"> <div class="link-type-group">
<label class="link-type-option" :class="{ 'link-type-option--active': form.linkType === 'external' }"> <label class="link-type-option" :class="{ 'link-type-option--active': form.linkType === 'external' }">
<input type="radio" v-model="form.linkType" value="external" />🌐 外网 <input type="radio" v-model="form.linkType" value="external" /><AppIcon name="globe" :size="16" /> 外网
</label> </label>
<label class="link-type-option" :class="{ 'link-type-option--active': form.linkType === 'internal' }"> <label class="link-type-option" :class="{ 'link-type-option--active': form.linkType === 'internal' }">
<input type="radio" v-model="form.linkType" value="internal" />🔒 内网 <input type="radio" v-model="form.linkType" value="internal" /><AppIcon name="save" :size="16" /> 内网
</label> </label>
<label class="link-type-option" :class="{ 'link-type-option--active': form.linkType === 'both' }"> <label class="link-type-option" :class="{ 'link-type-option--active': form.linkType === 'both' }">
<input type="radio" v-model="form.linkType" value="both" /> 内外网 <input type="radio" v-model="form.linkType" value="both" /><AppIcon name="network" :size="16" /> 内外网
</label> </label>
</div> </div>
</div> </div>
@ -135,7 +135,7 @@
<dialog ref="confirmRef" class="confirm-overlay" @click.self="closeConfirm" @close="closeConfirm"> <dialog ref="confirmRef" class="confirm-overlay" @click.self="closeConfirm" @close="closeConfirm">
<Transition name="confirm-scale-in"> <Transition name="confirm-scale-in">
<div v-if="showConfirm" class="confirm-dialog"> <div v-if="showConfirm" class="confirm-dialog">
<header><span class="confirm-icon"></span> 确认删除</header> <header><span class="confirm-icon"><AppIcon name="warning" :size="20" /></span> 确认删除</header>
<div class="confirm-body"> <div class="confirm-body">
<p>确定要删除图标 <strong>"{{ deleteTarget?.name }}"</strong> </p> <p>确定要删除图标 <strong>"{{ deleteTarget?.name }}"</strong> </p>
</div> </div>
@ -154,6 +154,7 @@
import { ref, watch, computed } from 'vue' import { ref, watch, computed } from 'vue'
import { fetchIconList, createIcon, updateIcon, deleteIcon, uploadIconLogo } from '../api' import { fetchIconList, createIcon, updateIcon, deleteIcon, uploadIconLogo } from '../api'
import { lockScroll, unlockScroll } from '../utils/scrollLock' import { lockScroll, unlockScroll } from '../utils/scrollLock'
import AppIcon from './AppIcon.vue'
const props = defineProps({ open: Boolean }) const props = defineProps({ open: Boolean })
const emit = defineEmits(['close', 'success', 'error', 'refresh']) const emit = defineEmits(['close', 'success', 'error', 'refresh'])

View File

@ -14,10 +14,10 @@
<!-- 导演和主演 --> <!-- 导演和主演 -->
<div v-if="item.directors" class="poster-director"> <div v-if="item.directors" class="poster-director">
🎬 {{ item.directors }} <AppIcon name="movie" :size="14" /> {{ item.directors }}
</div> </div>
<div v-if="item.casts" class="poster-casts"> <div v-if="item.casts" class="poster-casts">
🎭 {{ item.casts }} <AppIcon name="cast" :size="14" /> {{ item.casts }}
</div> </div>
<!-- 简介按钮点击打开弹窗 --> <!-- 简介按钮点击打开弹窗 -->
@ -27,7 +27,7 @@
class="description-toggle" class="description-toggle"
@click.stop="showDescriptionDialog = true" @click.stop="showDescriptionDialog = true"
> >
📝 查看详情 <AppIcon name="detail" :size="14" /> 查看详情
</button> </button>
</div> </div>
@ -48,33 +48,33 @@
<div class="description-dialog__body"> <div class="description-dialog__body">
<!-- 基本信息 --> <!-- 基本信息 -->
<div v-if="item.description" class="detail-section"> <div v-if="item.description" class="detail-section">
<h4>📖 简介</h4> <h4><AppIcon name="description" :size="16" /> 简介</h4>
<p class="detail-text">{{ item.description }}</p> <p class="detail-text">{{ item.description }}</p>
</div> </div>
<div v-if="item.directors" class="detail-section"> <div v-if="item.directors" class="detail-section">
<h4>🎬 导演</h4> <h4><AppIcon name="movie" :size="16" /> 导演</h4>
<p class="detail-text">{{ item.directors }}</p> <p class="detail-text">{{ item.directors }}</p>
</div> </div>
<div v-if="item.casts" class="detail-section"> <div v-if="item.casts" class="detail-section">
<h4>🎭 主演</h4> <h4><AppIcon name="cast" :size="16" /> 主演</h4>
<p class="detail-text">{{ item.casts }}</p> <p class="detail-text">{{ item.casts }}</p>
</div> </div>
<div v-if="item.tags" class="detail-section"> <div v-if="item.tags" class="detail-section">
<h4>🏷 标签</h4> <h4><AppIcon name="tag" :size="16" /> 标签</h4>
<div class="detail-tags"> <div class="detail-tags">
<span v-for="tag in displayTags" :key="tag" class="detail-tag">{{ tag }}</span> <span v-for="tag in displayTags" :key="tag" class="detail-tag">{{ tag }}</span>
</div> </div>
</div> </div>
<div v-if="item.score || item.rating" class="detail-section"> <div v-if="item.score || item.rating" class="detail-section">
<h4> 评分</h4> <h4><AppIcon name="sparkle" :size="16" /> 评分</h4>
<p class="detail-text">{{ displayScore }}</p> <p class="detail-text">{{ displayScore }}</p>
</div> </div>
<div v-if="item.category" class="detail-section"> <div v-if="item.category" class="detail-section">
<h4>📂 分类</h4> <h4><AppIcon name="folder-open" :size="16" /> 分类</h4>
<p class="detail-text">{{ item.category }}</p> <p class="detail-text">{{ item.category }}</p>
</div> </div>
<div v-if="item.status" class="detail-section"> <div v-if="item.status" class="detail-section">
<h4>📌 状态</h4> <h4><AppIcon name="info" :size="16" /> 状态</h4>
<p class="detail-text">{{ displayStatus }}</p> <p class="detail-text">{{ displayStatus }}</p>
</div> </div>
</div> </div>
@ -87,6 +87,7 @@
<script setup> <script setup>
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import AppIcon from './AppIcon.vue'
const props = defineProps({ const props = defineProps({
item: { item: {
@ -113,7 +114,7 @@ const posterImageUrl = computed(() => {
// //
const displayScore = computed(() => { const displayScore = computed(() => {
const score = props.item.score ?? props.item.rating ?? 0 const score = props.item.score ?? props.item.rating ?? 0
return score ? `${score}` : '暂无评分' return score ? `${score}` : '暂无评分'
}) })
// ISO 8601 // ISO 8601

View File

@ -1,114 +1,22 @@
<template> <template>
<div class="settings-view"> <div class="settings-view">
<div class="settings-header"> <div class="settings-header">
<h2>设置</h2>
<p>配置 OpenList 后端接口绑定您的云盘账户</p> <p>配置 OpenList 后端接口绑定您的云盘账户</p>
</div> </div>
<div class="settings-card"> <!-- 加载状态等待后端返回数据 -->
<h3 class="settings-card__title"> <div v-if="loading" class="loading-container">
<span class="settings-card__icon">🔗</span> <div class="loading-spinner-icon"></div>
OpenList 绑定 <p class="loading-text">正在加载绑定信息...</p>
</h3>
<form class="settings-form" @submit.prevent="handleBind">
<!-- OpenList 地址 -->
<div class="form-group">
<label class="form-label" for="openlist-url">
<span class="form-label__icon">🌐</span>
<span class="form-label__text">OpenList 地址</span>
</label>
<input
id="openlist-url"
v-model="formData.url"
type="url"
class="form-input"
placeholder="例如https://olist.example.com 或 http://localhost:5575"
required
autocomplete="off"
/>
<span class="form-help">请填写 OpenList 服务的完整访问地址包含协议头</span>
</div> </div>
<!-- 账号 --> <!-- 数据加载完成后显示内容 -->
<div class="form-group"> <div v-else class="settings-card">
<label class="form-label" for="openlist-username"> <!-- 已绑定信息顶部展示 -->
<span class="form-label__icon">👤</span>
<span class="form-label__text">账号</span>
</label>
<input
id="openlist-username"
v-model="formData.username"
type="text"
class="form-input"
placeholder="请输入 OpenList 用户名"
required
autocomplete="username"
/>
</div>
<!-- 密码 -->
<div class="form-group">
<label class="form-label" for="openlist-password">
<span class="form-label__icon">🔒</span>
<span class="form-label__text">密码</span>
</label>
<div class="password-input-wrapper">
<input
id="openlist-password"
v-model="formData.password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
placeholder="请输入 OpenList 密码"
required
autocomplete="current-password"
/>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
:title="showPassword ? '隐藏密码' : '显示密码'"
>
{{ showPassword ? '🙈' : '👁️' }}
</button>
</div>
</div>
<!-- 绑定按钮 -->
<div class="form-actions">
<button
type="submit"
class="btn-bind"
:disabled="binding"
:class="{ 'btn-bind--loading': binding }"
>
<span v-if="!binding">🔗 绑定 OpenList</span>
<span v-else class="loading-spinner"> 绑定中...</span>
</button>
<button
type="button"
class="btn-reset"
@click="resetForm"
:disabled="binding"
>
重置
</button>
</div>
</form>
<!-- 绑定状态提示 -->
<Transition name="fade">
<div v-if="bindStatus" class="bind-status" :class="`bind-status--${bindStatus.type}`">
<span class="bind-status__icon">{{ bindStatus.icon }}</span>
<span class="bind-status__text">{{ bindStatus.message }}</span>
</div>
</Transition>
<!-- 已绑定信息 -->
<Transition name="fade"> <Transition name="fade">
<div v-if="boundInfo" class="bound-info"> <div v-if="boundInfo" class="bound-info">
<div class="bound-info__header"> <div class="bound-info__header">
<span class="bound-info__icon"></span> <span class="bound-info__icon"><AppIcon name="success" :size="18" /></span>
<span class="bound-info__text">已绑定 OpenList</span> <span class="bound-info__text">已绑定 OpenList</span>
</div> </div>
<div class="bound-info__details"> <div class="bound-info__details">
@ -134,12 +42,15 @@
</div> </div>
<div class="bound-info__item"> <div class="bound-info__item">
<span class="bound-info__label">Token有效</span> <span class="bound-info__label">Token有效</span>
<span class="bound-info__value">{{ boundInfo.tokenValid ? '✅ 是' : '❌ 否' }}</span> <span class="bound-info__value">
<AppIcon :name="boundInfo.tokenValid ? 'success' : 'fail'" :size="14" />
{{ boundInfo.tokenValid ? '是' : '否' }}
</span>
</div> </div>
<div class="bound-info__item" v-if="boundInfo.connectMessage"> <div class="bound-info__item" v-if="boundInfo.connectMessage">
<span class="bound-info__label">连接状态</span> <span class="bound-info__label">连接状态</span>
<span class="bound-info__value connect-message" :class="{ 'connect-ok': boundInfo.connected === true, 'connect-error': boundInfo.connected === false }"> <span class="bound-info__value connect-message" :class="{ 'connect-ok': boundInfo.connected === true, 'connect-error': boundInfo.connected === false }">
{{ boundInfo.connected ? '✅' : '❌' }} {{ boundInfo.connectMessage }} <AppIcon :name="boundInfo.connected ? 'success' : 'fail'" :size="14" /> {{ boundInfo.connectMessage }}
</span> </span>
</div> </div>
<div class="bound-info__item"> <div class="bound-info__item">
@ -156,10 +67,110 @@
class="btn-unbind" class="btn-unbind"
@click="handleUnbind" @click="handleUnbind"
> >
🗑 解除绑定 <AppIcon name="delete" :size="16" /> 解除绑定
</button> </button>
</div> </div>
</Transition> </Transition>
<h3 class="settings-card__title">
<span class="settings-card__icon"><AppIcon name="link" :size="20" /></span>
OpenList 绑定
</h3>
<form class="settings-form" @submit.prevent="handleBind">
<!-- OpenList 地址 -->
<div class="form-group">
<label class="form-label" for="openlist-url">
<span class="form-label__icon"><AppIcon name="globe" :size="16" /></span>
<span class="form-label__text">OpenList 地址</span>
</label>
<input
id="openlist-url"
v-model="formData.url"
type="url"
class="form-input"
placeholder="例如https://olist.example.com 或 http://localhost:5575"
required
autocomplete="off"
readonly
onfocus="this.removeAttribute('readonly')"
/>
<span class="form-help">请填写 OpenList 服务的完整访问地址包含协议头</span>
</div>
<!-- 账号 -->
<div class="form-group">
<label class="form-label" for="openlist-username">
<span class="form-label__icon"><AppIcon name="user" :size="16" /></span>
<span class="form-label__text">账号</span>
</label>
<input
id="openlist-username"
v-model="formData.username"
type="text"
class="form-input"
placeholder="请输入 OpenList 用户名"
required
autocomplete="off"
/>
</div>
<!-- 密码 -->
<div class="form-group">
<label class="form-label" for="openlist-password">
<span class="form-label__icon"><AppIcon name="save" :size="16" /></span>
<span class="form-label__text">密码</span>
</label>
<div class="password-input-wrapper">
<input
id="openlist-password"
v-model="formData.password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
placeholder="请输入 OpenList 密码"
required
autocomplete="off"
/>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
:title="showPassword ? '隐藏密码' : '显示密码'"
>
<AppIcon :name="showPassword ? 'eye-off' : 'eye'" :size="18" />
</button>
</div>
</div>
<!-- 绑定按钮 -->
<div class="form-actions">
<button
type="submit"
class="btn-bind"
:disabled="binding"
:class="{ 'btn-bind--loading': binding }"
>
<span v-if="!binding"><AppIcon name="link" :size="16" /> 绑定 OpenList</span>
<span v-else class="loading-spinner"><AppIcon name="sync" :size="16" /> 绑定中...</span>
</button>
<button
type="button"
class="btn-reset"
@click="resetForm"
:disabled="binding"
>
重置
</button>
</div>
</form>
<!-- 绑定状态提示 -->
<Transition name="fade">
<div v-if="bindStatus" class="bind-status" :class="`bind-status--${bindStatus.type}`">
<span class="bind-status__icon"><AppIcon v-if="bindStatus.iconName" :name="bindStatus.iconName" :size="18" /></span>
<span class="bind-status__text">{{ bindStatus.message }}</span>
</div>
</Transition>
</div> </div>
</div> </div>
</template> </template>
@ -167,6 +178,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { bindOpenList, unbindOpenList, getOpenListInfo } from '../api' import { bindOpenList, unbindOpenList, getOpenListInfo } from '../api'
import AppIcon from './AppIcon.vue'
// //
const formData = reactive({ const formData = reactive({
@ -176,6 +188,7 @@ const formData = reactive({
}) })
// //
const loading = ref(true)
const binding = ref(false) const binding = ref(false)
const showPassword = ref(false) const showPassword = ref(false)
const bindStatus = ref(null) const bindStatus = ref(null)
@ -188,6 +201,7 @@ const boundInfo = ref(null)
* - 已绑定返回 { code: 200, data: { id, openlistUrl, openlistUsername, storageId, mountPath, status, tokenValid, connected, connectMessage, createTime, updateTime } } * - 已绑定返回 { code: 200, data: { id, openlistUrl, openlistUsername, storageId, mountPath, status, tokenValid, connected, connectMessage, createTime, updateTime } }
*/ */
async function loadBoundInfo() { async function loadBoundInfo() {
loading.value = true
try { try {
const result = await getOpenListInfo() const result = await getOpenListInfo()
// result null { code: 200, data: {...} } // result null { code: 200, data: {...} }
@ -217,6 +231,8 @@ async function loadBoundInfo() {
// //
const errorMsg = err?.payload?.message || err?.message || '加载绑定信息失败' const errorMsg = err?.payload?.message || err?.message || '加载绑定信息失败'
setBindStatus('error', errorMsg) setBindStatus('error', errorMsg)
} finally {
loading.value = false
} }
} }
@ -335,17 +351,17 @@ function setBindStatus(type, message, autoClear = true) {
return return
} }
const icons = { const iconNames = {
success: '', success: 'success',
error: '', error: 'fail',
warning: '⚠️', warning: 'warning',
info: '', info: 'info',
} }
bindStatus.value = { bindStatus.value = {
type, type,
message, message,
icon: icons[type] || '', iconName: iconNames[type] || '',
} }
// 3 // 3
@ -364,9 +380,7 @@ onMounted(() => {
<style scoped> <style scoped>
.settings-view { .settings-view {
max-width: 720px; padding: 16px 0;
margin: 0 auto;
padding: 24px 16px;
} }
.settings-header { .settings-header {
@ -666,6 +680,37 @@ onMounted(() => {
gap: 6px; gap: 6px;
} }
/* 加载容器 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 24px;
gap: 16px;
}
.loading-spinner-icon {
width: 36px;
height: 36px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 14px;
color: #666;
margin: 0;
}
/* 过渡动画 */ /* 过渡动画 */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
@ -759,6 +804,15 @@ onMounted(() => {
background: #3a1a1a; background: #3a1a1a;
border-color: #f56c6c; border-color: #f56c6c;
} }
.loading-spinner-icon {
border-color: #374151;
border-top-color: #60a5fa;
}
.loading-text {
color: #9ca3af;
}
} }
/* 响应式 */ /* 响应式 */

View File

@ -19,9 +19,9 @@
<!-- 文件夹/文件图标 --> <!-- 文件夹/文件图标 -->
<span v-if="multiSelectMode" class="tree-icon tree-icon--checkbox"></span> <span v-if="multiSelectMode" class="tree-icon tree-icon--checkbox"></span>
<span v-else-if="node.type === 'folder'" class="tree-icon"> <span v-else-if="node.type === 'folder'" class="tree-icon">
{{ isExpanded ? '📂' : '📁' }} <AppIcon :name="isExpanded ? 'folder-open' : 'folder'" :size="18" />
</span> </span>
<span v-else class="tree-icon">📄</span> <span v-else class="tree-icon"><AppIcon name="file" :size="18" /></span>
<span v-if="node.type === 'folder' && node.isLoading" class="tree-loading">加载中...</span> <span v-if="node.type === 'folder' && node.isLoading" class="tree-loading">加载中...</span>
<!-- 刮削状态徽章仅文件类型 --> <!-- 刮削状态徽章仅文件类型 -->
@ -46,7 +46,7 @@
v-if="node.type === 'folder' && node.description" v-if="node.type === 'folder' && node.description"
class="tree-info-badge" class="tree-info-badge"
title="有描述" title="有描述"
></span> ><AppIcon name="info" :size="16" /></span>
<div class="tree-actions" @click.stop> <div class="tree-actions" @click.stop>
<!-- 刮削进度条刮削中时显示 --> <!-- 刮削进度条刮削中时显示 -->
@ -63,7 +63,7 @@
title="添加子文件夹" title="添加子文件夹"
@click="handleAddFolder" @click="handleAddFolder"
> >
+📁 <AppIcon name="add-folder" :size="16" />
</button> </button>
<button <button
v-if="node.type === 'folder'" v-if="node.type === 'folder'"
@ -72,7 +72,7 @@
title="添加文件" title="添加文件"
@click="handleAddFile" @click="handleAddFile"
> >
+📄 <AppIcon name="add-file" :size="16" />
</button> </button>
<button <button
v-if="node.type === 'file'" v-if="node.type === 'file'"
@ -81,7 +81,7 @@
title="重新刮削" title="重新刮削"
@click="handleRescrape" @click="handleRescrape"
> >
🔄 <AppIcon name="refresh" :size="16" />
</button> </button>
<button <button
v-if="node.type === 'file'" v-if="node.type === 'file'"
@ -90,7 +90,7 @@
title="查询刮削状态" title="查询刮削状态"
@click="handleScrapeStatus" @click="handleScrapeStatus"
> >
🔍 <AppIcon name="search" :size="16" />
</button> </button>
<button <button
type="button" type="button"
@ -98,7 +98,7 @@
title="编辑" title="编辑"
@click="handleEdit" @click="handleEdit"
> >
<AppIcon name="edit" :size="16" />
</button> </button>
<button <button
type="button" type="button"
@ -106,7 +106,7 @@
title="删除" title="删除"
@click="handleDelete" @click="handleDelete"
> >
🗑 <AppIcon name="delete" :size="16" />
</button> </button>
</div> </div>
</div> </div>
@ -120,7 +120,7 @@
:style="tooltipStyle" :style="tooltipStyle"
> >
<div class="app-tooltip__header"> <div class="app-tooltip__header">
<span class="app-tooltip__icon">📁</span> <span class="app-tooltip__icon"><AppIcon name="folder" :size="16" /></span>
<span class="app-tooltip__title">{{ node.title }}</span> <span class="app-tooltip__title">{{ node.title }}</span>
</div> </div>
<div class="app-tooltip__body"> <div class="app-tooltip__body">
@ -162,6 +162,7 @@
<script setup> <script setup>
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import AppIcon from './AppIcon.vue'
const props = defineProps({ const props = defineProps({
node: { node: {

View File

@ -244,7 +244,7 @@
> >
<div class="confirm-box"> <div class="confirm-box">
<header class="confirm-header"> <header class="confirm-header">
<h3> 确认删除</h3> <h3><AppIcon name="warning" :size="20" /> 确认删除</h3>
<button type="button" class="confirm-close" @click="closeConfirmDialog">×</button> <button type="button" class="confirm-close" @click="closeConfirmDialog">×</button>
</header> </header>
@ -269,12 +269,13 @@
<script setup> <script setup>
import { reactive, ref, computed, watch } from 'vue' import { reactive, ref, computed, watch } from 'vue'
import { import {
fetchUserList, fetchUsers,
createUser, createUser,
updateUser, updateUser,
deleteUser, deleteUser,
} from '../api' } from '../api'
import { lockScroll, unlockScroll } from '../utils/scrollLock' import { lockScroll, unlockScroll } from '../utils/scrollLock'
import AppIcon from './AppIcon.vue'
// ==================== ==================== // ==================== ====================
const props = defineProps({ const props = defineProps({
@ -1197,6 +1198,19 @@ watch(
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 14px;
transition: border-color 0.2s; transition: border-color 0.2s;
/* 重置 select 元素的默认样式 */
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
}
/* 为 select 添加自定义下拉箭头 */
.form-group select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23606266' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
} }
.form-group input:focus, .form-group input:focus,

View File

@ -508,12 +508,26 @@ body.dark-mode .network-toggle__btn--active:hover {
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
gap: 12px; gap: 12px;
flex: 1; flex: 1;
position: relative;
}
.search-input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 1;
opacity: 0.5;
} }
.search-topbar__input input { .search-topbar__input input {
min-height: 52px; min-height: 52px;
border-radius: 999px; border-radius: 999px;
padding: 0 20px; padding: 0 20px 0 48px;
transition: all 0.3s ease; transition: all 0.3s ease;
outline: none; outline: none;
} }
@ -960,7 +974,7 @@ body.dark-mode .quick-access-btn__icon {
.hero-main { .hero-main {
position: relative; position: relative;
width: 100%; width: 100%;
min-height: 520px; height: 700px;
border-radius: 24px; border-radius: 24px;
overflow: hidden; overflow: hidden;
outline: none; outline: none;
@ -970,7 +984,7 @@ body.dark-mode .quick-access-btn__icon {
opacity: 1; opacity: 1;
} }
/* 海报图片层 */ /* 海报图片层 - cover 填充,顶部对齐 */
.hero-bg-image { .hero-bg-image {
position: absolute; position: absolute;
top: 0; top: 0;
@ -978,27 +992,15 @@ body.dark-mode .quick-access-btn__icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
object-position: center; object-position: top center;
z-index: 0; z-index: 0;
} }
/* 横向海报适配 - 使用独立容器确保图片完整显示 */ /* 横版海报 - 宽度撑满,高度按比例自适应,顶部对齐,底部由容器裁剪 */
.hero-main.hero-wide {
overflow: visible;
min-height: 200px;
max-height: none;
}
.hero-main.hero-wide .hero-bg-image { .hero-main.hero-wide .hero-bg-image {
position: relative; position: relative !important;
object-fit: contain; width: 100% !important;
object-position: center; height: auto !important;
width: 100%;
height: auto;
max-height: 500px;
display: block;
margin: 0 auto;
background-color: #0f172a;
} }
/* 海报加载状态 - 渐变动画模拟视频加载 */ /* 海报加载状态 - 渐变动画模拟视频加载 */
@ -1050,7 +1052,7 @@ body.dark-mode .quick-access-btn__icon {
flex-direction: column; flex-direction: column;
justify-content: flex-end; justify-content: flex-end;
padding: 48px; padding: 48px;
background: linear-gradient(180deg, rgba(8, 15, 29, 0.05) 0%, rgba(8, 15, 29, 0.75) 100%); background: linear-gradient(180deg, rgba(8, 15, 29, 0) 50%, rgba(8, 15, 29, 0.55) 100%);
color: #fff; color: #fff;
} }
@ -1223,6 +1225,12 @@ body.dark-mode .quick-access-btn__icon {
font-weight: 600; font-weight: 600;
} }
.hero-main__tag {
background: rgba(99, 102, 241, 0.25);
font-size: 12px;
padding: 4px 10px;
}
/* ==================== 上方小海报卡片(悬浮于大海报之上,与每日推荐同级) ==================== */ /* ==================== 上方小海报卡片(悬浮于大海报之上,与每日推荐同级) ==================== */
.hero-sub { .hero-sub {
display: flex; display: flex;
@ -4034,19 +4042,18 @@ body.dark-mode .quick-access-btn__icon {
background: linear-gradient(135deg, #34495e 0%, #5cb8c9 100%); background: linear-gradient(135deg, #34495e 0%, #5cb8c9 100%);
} }
.theme-toggle-btn__icon { .theme-toggle-btn .app-icon {
width: 22px; width: 22px;
height: 22px; height: 22px;
color: #555; transition: transform 0.3s ease;
transition: color 0.3s ease;
} }
.theme-toggle-btn--dark .theme-toggle-btn__icon { .theme-toggle-btn:hover .app-icon {
color: #f0f0f0; transform: rotate(30deg);
} }
/* 太阳和月亮图标的动画 */ /* 太阳图标旋转动画 */
.theme-icon--sun { .theme-toggle-btn:not(.theme-toggle-btn--dark) .app-icon svg {
animation: sun-rotate 20s linear infinite; animation: sun-rotate 20s linear infinite;
} }
@ -5055,7 +5062,8 @@ body.dark-mode .dialog-form header {
border-bottom-color: rgba(51, 65, 85, 0.5); border-bottom-color: rgba(51, 65, 85, 0.5);
} }
body.dark-mode .simple-dialog__header h2 { body.dark-mode .simple-dialog__header h2,
body.dark-mode .dialog-form header h2 {
color: #e2e8f0; color: #e2e8f0;
} }
@ -5118,6 +5126,13 @@ body.dark-mode select {
border-color: rgba(51, 65, 85, 0.5); border-color: rgba(51, 65, 85, 0.5);
} }
/* 覆盖 scoped 组件中硬编码的浅色边框 */
body.dark-mode .form-group input,
body.dark-mode .form-group select,
body.dark-mode .form-group textarea {
border-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode input::placeholder, body.dark-mode input::placeholder,
body.dark-mode textarea::placeholder { body.dark-mode textarea::placeholder {
color: #64748b; color: #64748b;
@ -6331,3 +6346,453 @@ body.dark-mode .dialog-form header button[type="button"]:hover {
background: rgba(71, 85, 105, 0.8); background: rgba(71, 85, 105, 0.8);
color: #e2e8f0; color: #e2e8f0;
} }
/* ===== 黑夜模式:用户管理弹窗搜索和重置按钮 ===== */
body.dark-mode .btn-search {
background: rgba(56, 189, 248, 0.1);
border-color: #38bdf8;
color: #38bdf8;
}
body.dark-mode .btn-search:hover {
background: #38bdf8;
color: #0f172a;
}
body.dark-mode .btn-reset {
background: rgba(239, 68, 68, 0.1);
border-color: #f87171;
color: #f87171;
}
body.dark-mode .btn-reset:hover {
background: #f87171;
color: #0f172a;
}
/* 用户管理弹窗表格样式 */
body.dark-mode .user-table th {
background: rgba(15, 23, 42, 0.6);
color: #94a3b8;
border-bottom-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .user-table td {
border-bottom-color: rgba(51, 65, 85, 0.3);
}
body.dark-mode .user-table tbody tr:hover {
background: rgba(56, 189, 248, 0.08);
}
/* 用户管理弹窗分页样式 */
body.dark-mode .pagination-bar {
border-top-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .pagination-controls button {
background: rgba(15, 23, 42, 0.5);
border-color: rgba(51, 65, 85, 0.5);
color: #94a3b8;
}
body.dark-mode .pagination-controls button:hover:not(:disabled) {
background: rgba(56, 189, 248, 0.1);
border-color: #38bdf8;
color: #38bdf8;
}
/* 用户管理弹窗编辑/删除按钮 */
body.dark-mode .btn-edit {
background: rgba(56, 189, 248, 0.1);
border-color: rgba(56, 189, 248, 0.3);
color: #38bdf8;
}
body.dark-mode .btn-edit:hover {
background: #38bdf8;
color: #0f172a;
border-color: #38bdf8;
}
body.dark-mode .btn-delete {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
body.dark-mode .btn-delete:hover {
background: #f87171;
color: #0f172a;
border-color: #f87171;
}
/* 用户管理弹窗角色和状态徽章 */
body.dark-mode .role-0 {
background: rgba(56, 189, 248, 0.15);
color: #38bdf8;
}
body.dark-mode .role-1 {
background: rgba(251, 191, 36, 0.15);
color: #fbbf24;
}
body.dark-mode .role-2 {
background: rgba(148, 163, 184, 0.15);
color: #94a3b8;
}
body.dark-mode .status-0 {
background: rgba(239, 68, 68, 0.15);
color: #f87171;
}
body.dark-mode .status-1 {
background: rgba(34, 197, 94, 0.15);
color: #4ade80;
}
body.dark-mode .status-2 {
background: rgba(148, 163, 184, 0.15);
color: #94a3b8;
}
/* 用户管理弹窗加载遮罩 */
body.dark-mode .global-loading-overlay {
background: rgba(15, 23, 42, 0.85);
}
body.dark-mode .spinner {
border-color: rgba(51, 65, 85, 0.5);
border-top-color: #38bdf8;
}
body.dark-mode .loading-text {
color: #94a3b8;
}
/* ===== 黑夜模式:网盘管理弹窗完整兼容 ===== */
/* 网盘管理工具栏 */
body.dark-mode .drive-toolbar {
border-bottom-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .drive-toolbar h3 {
color: #e2e8f0;
}
/* 网盘卡片 */
body.dark-mode .drive-card {
background: rgba(15, 23, 42, 0.5);
border-color: rgba(51, 65, 85, 0.4);
}
body.dark-mode .drive-card:hover {
background: rgba(30, 41, 59, 0.6);
border-color: rgba(56, 189, 248, 0.3);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* 网盘Logo */
body.dark-mode .drive-logo-small {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
border-color: rgba(51, 65, 85, 0.5);
}
/* 网盘名称 */
body.dark-mode .drive-name {
color: #e2e8f0;
}
/* 网盘元数据 */
body.dark-mode .drive-meta {
color: #64748b;
}
/* 网盘悬浮提示 */
body.dark-mode .drive-tooltip {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
border-color: rgba(51, 65, 85, 0.5);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
}
body.dark-mode .tooltip-header {
color: #e2e8f0;
border-bottom-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .tooltip-content {
color: #94a3b8;
}
body.dark-mode .tooltip-arrow {
background: #1e293b;
border-color: rgba(51, 65, 85, 0.5);
}
/* 网盘表单视图 */
body.dark-mode .form-header {
border-bottom-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .form-header h3 {
color: #e2e8f0;
}
body.dark-mode .btn-back {
background: rgba(51, 65, 85, 0.3);
border-color: rgba(51, 65, 85, 0.5);
color: #94a3b8;
}
body.dark-mode .btn-back:hover {
background: rgba(51, 65, 85, 0.5);
border-color: rgba(71, 85, 105, 0.7);
color: #e2e8f0;
}
/* 网盘表单输入框 */
body.dark-mode .form-input,
body.dark-mode .form-textarea {
background: rgba(15, 23, 42, 0.6);
border-color: rgba(51, 65, 85, 0.5);
color: #e2e8f0;
}
body.dark-mode .form-input:focus,
body.dark-mode .form-textarea:focus {
background: rgba(15, 23, 42, 0.8);
border-color: #38bdf8;
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.15);
}
body.dark-mode .form-input:disabled,
body.dark-mode .form-textarea:disabled {
background: rgba(15, 23, 42, 0.4);
color: #64748b;
}
body.dark-mode .form-group label {
color: #cbd5e1;
}
body.dark-mode .char-count {
color: #64748b;
}
/* Logo上传区域 */
body.dark-mode .logo-upload-area {
background: rgba(15, 23, 42, 0.4);
border-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .logo-upload-area:hover {
border-color: #38bdf8;
background: rgba(56, 189, 248, 0.05);
}
body.dark-mode .upload-placeholder {
color: #64748b;
}
body.dark-mode .logo-upload-tips {
background: rgba(56, 189, 248, 0.1);
border-color: rgba(56, 189, 248, 0.3);
color: #7dd3fc;
}
body.dark-mode .logo-upload-tips strong {
color: #bae6fd;
}
/* 表单底部 */
body.dark-mode .form-footer {
border-top-color: rgba(51, 65, 85, 0.5);
}
body.dark-mode .btn-cancel {
background: rgba(15, 23, 42, 0.5);
border-color: rgba(51, 65, 85, 0.5);
color: #94a3b8;
}
body.dark-mode .btn-cancel:hover:not(:disabled) {
background: rgba(56, 189, 248, 0.1);
border-color: rgba(56, 189, 248, 0.5);
color: #38bdf8;
}
/* 网盘编辑/删除按钮 */
body.dark-mode .drive-actions .btn-edit {
background: rgba(56, 189, 248, 0.1);
border-color: rgba(56, 189, 248, 0.3);
color: #38bdf8;
}
body.dark-mode .drive-actions .btn-edit:hover {
background: #38bdf8;
color: #0f172a;
border-color: #38bdf8;
}
body.dark-mode .drive-actions .btn-delete {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.3);
color: #f87171;
}
body.dark-mode .drive-actions .btn-delete:hover {
background: #f87171;
color: #0f172a;
border-color: #f87171;
}
/* 网盘加载状态 */
body.dark-mode .spinner-small {
border-color: rgba(51, 65, 85, 0.5);
border-top-color: #38bdf8;
}
/* ===== 黑夜模式:设置弹窗内部内容兼容 ===== */
/* 设置弹窗在 SimpleDialog 中,需要覆盖 SettingsView 组件的 scoped 样式(加 !important 提升优先级) */
body.dark-mode .settings-in-dialog .settings-header p {
color: #94a3b8 !important;
}
body.dark-mode .settings-in-dialog .settings-card {
background: rgba(15, 23, 42, 0.5) !important;
border-color: rgba(51, 65, 85, 0.4) !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important;
}
body.dark-mode .settings-in-dialog .settings-card__title {
color: #e2e8f0 !important;
border-bottom-color: rgba(51, 65, 85, 0.5) !important;
}
body.dark-mode .settings-in-dialog .form-label {
color: #cbd5e1 !important;
}
body.dark-mode .settings-in-dialog .form-input {
background: rgba(15, 23, 42, 0.6) !important;
border-color: rgba(51, 65, 85, 0.5) !important;
color: #e2e8f0 !important;
}
body.dark-mode .settings-in-dialog .form-input:hover {
border-color: #38bdf8 !important;
}
body.dark-mode .settings-in-dialog .form-input:focus {
background: rgba(15, 23, 42, 0.8) !important;
border-color: #38bdf8 !important;
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.15) !important;
}
body.dark-mode .settings-in-dialog .form-input::placeholder {
color: #64748b !important;
}
body.dark-mode .settings-in-dialog .form-help {
color: #64748b !important;
}
body.dark-mode .settings-in-dialog .password-toggle:hover {
background-color: rgba(51, 65, 85, 0.5) !important;
}
body.dark-mode .settings-in-dialog .form-actions {
border-top-color: rgba(51, 65, 85, 0.5) !important;
}
body.dark-mode .settings-in-dialog .btn-bind {
background: linear-gradient(135deg, #38bdf8, #0ea5e9) !important;
}
body.dark-mode .settings-in-dialog .btn-bind:hover:not(:disabled) {
background: linear-gradient(135deg, #7dd3fc, #38bdf8) !important;
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.4) !important;
}
body.dark-mode .settings-in-dialog .btn-bind--loading {
background: linear-gradient(135deg, #475569, #334155) !important;
}
body.dark-mode .settings-in-dialog .btn-reset {
color: #94a3b8 !important;
background: rgba(51, 65, 85, 0.3) !important;
border-color: rgba(51, 65, 85, 0.5) !important;
}
body.dark-mode .settings-in-dialog .btn-reset:hover:not(:disabled) {
color: #38bdf8 !important;
border-color: #38bdf8 !important;
background: rgba(56, 189, 248, 0.1) !important;
}
body.dark-mode .settings-in-dialog .bind-status--success {
background: rgba(34, 197, 94, 0.1) !important;
border-color: rgba(34, 197, 94, 0.3) !important;
color: #4ade80 !important;
}
body.dark-mode .settings-in-dialog .bind-status--error {
background: rgba(239, 68, 68, 0.1) !important;
border-color: rgba(239, 68, 68, 0.3) !important;
color: #f87171 !important;
}
body.dark-mode .settings-in-dialog .bound-info {
background: rgba(56, 189, 248, 0.05) !important;
border-color: rgba(56, 189, 248, 0.2) !important;
}
body.dark-mode .settings-in-dialog .bound-info__header {
color: #4ade80 !important;
}
body.dark-mode .settings-in-dialog .bound-info__item {
color: #94a3b8 !important;
}
body.dark-mode .settings-in-dialog .bound-info__label {
color: #64748b !important;
}
body.dark-mode .settings-in-dialog .bound-info__value {
color: #e2e8f0 !important;
}
body.dark-mode .settings-in-dialog .connect-ok {
color: #4ade80 !important;
}
body.dark-mode .settings-in-dialog .connect-error {
color: #f87171 !important;
}
body.dark-mode .settings-in-dialog .status-active {
color: #4ade80 !important;
}
body.dark-mode .settings-in-dialog .btn-unbind {
color: #f87171 !important;
background: rgba(239, 68, 68, 0.05) !important;
border-color: rgba(239, 68, 68, 0.2) !important;
}
body.dark-mode .settings-in-dialog .btn-unbind:hover {
background: rgba(239, 68, 68, 0.1) !important;
border-color: #f87171 !important;
}
body.dark-mode .settings-in-dialog .loading-spinner-icon {
border-color: #374151 !important;
border-top-color: #60a5fa !important;
}
body.dark-mode .settings-in-dialog .loading-text {
color: #94a3b8 !important;
}