Compare commits
29
Commits
85b8a8c2a3
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5208b7ccec | ||
|
|
9d36c5d654 | ||
|
|
e836becda6 | ||
|
|
c115b6529d | ||
|
|
24b9b244b7 | ||
|
|
5defb62bc2 | ||
|
|
d0eb2f05be | ||
|
|
b0f832c37e | ||
|
|
287dc094df | ||
|
|
bd7fba5184 | ||
|
|
1542369770 | ||
|
|
826b080da0 | ||
|
|
c47cd3fb0d | ||
|
|
e25afb7445 | ||
|
|
e19ae3d802 | ||
|
|
95c6bb4d82 | ||
|
|
bb13b28ba2 | ||
|
|
a81126d4da | ||
|
|
f89f82704e | ||
|
|
b0730f5114 | ||
|
|
bb6e7c2874 | ||
|
|
5e4b700e0c | ||
|
|
b02e96a021 | ||
|
|
df24dcb3e2 | ||
|
|
5cf7549b35 | ||
|
|
e0ec08c874 | ||
|
|
8897e73aa8 | ||
|
|
e50c6e6805 | ||
|
|
83c093c4ee |
+29
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
run.out
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/asserts/favicon.ico" />
|
||||
<link rel="stylesheet" href="/src/reset.css" type="text/css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>LionWebsite</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
# LionWebsite 项目分析
|
||||
|
||||
## 项目概述
|
||||
|
||||
LionWebsite 是一个基于 Vue 3 + Vite 构建的漫画/Gallery 下载与管理前端应用。用户通过授权码连接后端服务,可以搜索、下载、在线预览来自 exhentai/e-hentai 的漫画 gallery,并管理下载任务。
|
||||
|
||||
后端基址: `https://downloader.lionwebsite.xyz/`
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 用途 |
|
||||
|------|------|
|
||||
| **Vue 3** (Composition API, `<script setup>`) | 前端框架 |
|
||||
| **Vite 8** | 构建工具 |
|
||||
| **Element Plus 2** | UI 组件库 |
|
||||
| **Vuex 4** | 状态管理 |
|
||||
| **Axios** | HTTP 请求 |
|
||||
| **qs** | URL 序列化 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
D:\WorkSpace\code\Vue\LionWebsite\
|
||||
├── index.html # 入口 HTML
|
||||
├── package.json # 依赖声明
|
||||
├── vite.config.js # Vite 配置(仅 vue 插件 + 0.0.0.0 host)
|
||||
├── public/ # 静态资源(空)
|
||||
├── dist/ # 构建产物
|
||||
└── src/
|
||||
├── main.js # 应用入口: mount Element Plus + 全局 CSS
|
||||
├── reset.css # CSS reset
|
||||
├── App.vue # 根组件: el-container 布局
|
||||
├── store/
|
||||
│ └── index.js # Vuex store (所有状态与业务逻辑)
|
||||
└── components/
|
||||
├── DashBoard.vue # 仪表盘: 授权、查询、配置、搜索结果弹窗
|
||||
├── Side.vue # 主列表: Gallery 表格、翻页、分类/排序
|
||||
├── HentaiSearch.vue# 在线搜索弹窗: 远程搜索 exhentai gallery
|
||||
└── OnlineReader.vue# 在线预览弹窗: 阅读下载完成的 gallery
|
||||
```
|
||||
|
||||
## 组件详解
|
||||
|
||||
### App.vue (根组件)
|
||||
- 使用 `el-container` 布局
|
||||
- 左侧 `el-aside` (750px) 放置 `Side` 组件
|
||||
- 右侧 `el-main` 放置 `DashBoard` 组件
|
||||
|
||||
### DashBoard.vue — 仪表盘
|
||||
**功能**: 授权登录、查询任务、修改授权码、在线搜索入口、节点重连、配置管理
|
||||
- **授权模块**: 输入授权码 → 调用 `store.dispatch("validate")` → 记住密码(localStorage)
|
||||
- **查询模块**: 支持按链接远程查询或按链接/关键字本地查询
|
||||
- **操作按钮**: 修改授权码、删除本地授权码、夜间模式切换、在线搜索入口、重连节点
|
||||
- **配置弹窗**: 夜间模式(跟随系统)、在线预览分页数、默认分类/排序/显示类型 → 持久化到 localStorage
|
||||
- **查询结果弹窗**: 显示 gallery 详细信息,可选择分辨率提交下载任务
|
||||
- **缩略图预览**: 鼠标悬停 gallery 时在仪表盘显示封面缩略图
|
||||
- **初始化**: `onMounted` 时从 localStorage 恢复授权码与配置
|
||||
|
||||
### Side.vue — 主列表
|
||||
**功能**: 展示 gallery 表格、分页、分类/排序切换
|
||||
- **表格**: 使用 Element Plus `el-table`,列含展开行(详细信息)、名字、操作(下载/收藏/在线看)、进度
|
||||
- **操作列**:
|
||||
- 下载: 仅当 status==="下载完成" 时可用,直接 `window.open` 下载链接
|
||||
- 收藏/取消收藏: 调用 `collectGallery` / `disCollectGallery`
|
||||
- 在线看: 调用 `readOnlineGallery`
|
||||
- **分类切换**: 我的下载 / 我的收藏 / 全部
|
||||
- **排序**: 名字 / 简洁名字 / 任务创建时间
|
||||
- **显示类型**: 完整名字 / 简洁名字
|
||||
- **翻页**: 首页/上一页/下一页/尾页 + 直接输入页码
|
||||
- **缩略图悬停**: 500ms 防抖后触发缩略图更新
|
||||
- **空状态提示**: 根据分类显示"您未下载过"或"您未收藏过"
|
||||
|
||||
### HentaiSearch.vue — 在线搜索
|
||||
**功能**: 远程搜索 exhentai 上的 gallery
|
||||
- 输入关键字 → 调用 `https://downloader.lionwebsite.xyz/query?keyword=xxx`
|
||||
- 支持分页导航(首页/上一页/下一页/尾页),后端返回分页链接
|
||||
- 每个搜索结果展示: 封面缩略图、名字、上传时间、页数、类型、链接
|
||||
- 操作: "在线看"直接阅读、"查看详情"查询到本地并打开详情弹窗
|
||||
- 加载状态: CSS 旋转动画 loading 指示器
|
||||
|
||||
### OnlineReader.vue — 在线预览
|
||||
**功能**: 在弹窗中阅读 gallery 图片
|
||||
- 分页阅读: 每页显示 `lengthPerPage` 张图片(默认30),支持翻页
|
||||
- 图片懒加载: 每加载完成一张自动触发加载下一张(`loadImage`)
|
||||
- 支持大图预览: 点击图片使用 Element Plus `el-image` 的 `preview-src-list`
|
||||
- 连续阅读: 翻页滚动体验优化(监听 `switch` 事件)
|
||||
- 页数 < 9 时显示全部页码按钮,>= 9 时只显示输入跳转
|
||||
|
||||
## Vuex Store 分析 (store/index.js)
|
||||
|
||||
Store 是应用的核心,管理所有状态与后端通信。
|
||||
|
||||
### State 关键字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `totalGalleryTask` | Array | 所有 gallery 任务 |
|
||||
| `chosenGallery` | Object/false | 当前选中的 gallery(待下载) |
|
||||
| `collectGallery` | Array | 收藏的 gallery |
|
||||
| `downloadGallery` | Array | 分配给当前用户的 gallery |
|
||||
| `searchTask` | Array | 本地搜索结果 |
|
||||
| `currentTasks` | Array | 当前显示的 tasks(根据分类筛选) |
|
||||
| `readingGallery` | Object | 正在阅读的 gallery |
|
||||
| `isReading` | Boolean | 是否正在阅读 |
|
||||
| `isAuth` | Boolean | 是否已验证 |
|
||||
| `AuthCode` | String | 授权码 |
|
||||
| `userId` | Number | 用户 ID |
|
||||
| `username` | String | 用户名 |
|
||||
| `category` | String | 分类: myDownload / myCollect / total |
|
||||
| `sortType` | String | 排序: shortName / name / createTime |
|
||||
| `galleryNameType` | String | 显示名: shortName / name |
|
||||
| `page` | Number | 当前页码 |
|
||||
| `length` | Number | 每页条目数 |
|
||||
| `websocket` | WebSocket | WebSocket 连接 |
|
||||
|
||||
### Actions (后端交互)
|
||||
|
||||
| Action | 方法 | 端点 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `validate` | POST | `/validate` | 验证授权码,初始化数据 |
|
||||
| `updateGalleryTasks` | GET | `/GalleryManage` | 获取所有 gallery 任务 |
|
||||
| `postGalleryTask` | POST | `/GalleryManage` | 提交下载任务 |
|
||||
| `queryGalleryTask` | GET | `/GalleryManage` | 按链接查询 gallery |
|
||||
| `loadWeekUsedAmount` | GET | `/GalleryManage/weekUsedAmount` | 查询本周用量 |
|
||||
| `collectGallery` | POST | `/GalleryManage/collect` | 收藏 gallery |
|
||||
| `disCollectGallery` | POST | `/GalleryManage/disCollect` | 取消收藏 |
|
||||
| `deleteGallery` | DELETE | `/GalleryManage` | 删除 gallery |
|
||||
| `readOnlineGallery` | POST | `/GalleryManage/cache` | 缓存并开始在线预览 |
|
||||
| `alterAuthCode` | PUT | `/AuthCode` | 修改授权码 |
|
||||
| `reconnect` | POST | `/GalleryManage/reconnect` | 重连下载节点 |
|
||||
| `resetUndone` | POST | `/GalleryManage/reset` | 重置未完成任务 |
|
||||
|
||||
### WebSocket 通信
|
||||
|
||||
- 端点: `wss://downloader.lionwebsite.xyz/ws/`
|
||||
- 连接后发送: `"DownloaderWebsocket"`
|
||||
- 消息类型:
|
||||
- `updateTasks`: 增量更新任务进度
|
||||
- `fullUpdate`: 触发全量刷新
|
||||
|
||||
### Getters
|
||||
|
||||
| Getter | 说明 |
|
||||
|--------|------|
|
||||
| `currentTasks` | 返回当前页的 tasks(支持搜索态) |
|
||||
| `min` | 最小页码(1) |
|
||||
| `max` | 最大页码 |
|
||||
|
||||
### 辅助函数
|
||||
|
||||
- `getShortname(name)`: 从 gallery 名中去除中括号/括号内的标签信息
|
||||
- `confirmCurrentTask(state)`: 根据 category 切换 currentTasks
|
||||
- `sortTasks(state)`: 根据 sortType 排序
|
||||
- `deleteTask(tasks, key, value)`: 从多个数组中删除匹配的任务
|
||||
|
||||
## API 端点汇总
|
||||
|
||||
所有请求均需携带 `AuthCode` 参数:
|
||||
|
||||
| 端点 | 用途 |
|
||||
|------|------|
|
||||
| `POST /validate` | 授权码验证 |
|
||||
| `GET/POST/DELETE /GalleryManage` | 任务 CRUD |
|
||||
| `GET /GalleryManage/weekUsedAmount` | 每周用量查询 |
|
||||
| `POST /GalleryManage/collect` | 收藏 |
|
||||
| `POST /GalleryManage/disCollect` | 取消收藏 |
|
||||
| `POST /GalleryManage/cache` | 缓存 gallery 用于在线预览 |
|
||||
| `POST /GalleryManage/reconnect` | 重连下载节点 |
|
||||
| `POST /GalleryManage/reset` | 重置未完成任务 |
|
||||
| `GET /GalleryManage/ehThumbnail` | 获取缩略图 |
|
||||
| `GET /GalleryManage/file/{name}.zip` | 下载文件 |
|
||||
| `GET /GalleryManage/onlineImage/{i}` | 在线预览图片 |
|
||||
| `PUT /AuthCode` | 修改授权码 |
|
||||
| `GET /query` | 远程搜索 exhentai |
|
||||
| `WS /ws/` | WebSocket 实时推送 |
|
||||
|
||||
## 特色与关键实现
|
||||
|
||||
1. **简洁名字处理**: `getShortname()` 使用启发式逻辑从 gallery 完整名中剥离标签
|
||||
2. **防抖缩略图**: Side 组件 mouseenter 时 500ms 防抖才更新缩略图
|
||||
3. **夜间模式**: 通过 Element Plus 的 `dark` CSS class 切换,支持跟随系统偏好
|
||||
4. **权限分级**: userId===3 的用户(Lion)有额外操作权限(重置任务、查看 downloader)
|
||||
5. **本地持久化**: 授权码、分页配置、分类/排序/显示偏好均存 localStorage
|
||||
6. **WebSocket 实时更新**: 任务进度变化时服务端通过 WebSocket 推送增量更新
|
||||
7. **图片懒加载**: 按页加载图片,每加载完成一张自动触发下一张加载
|
||||
|
||||
## 构建与运行
|
||||
|
||||
```bash
|
||||
npm run dev # 开发服务器 (0.0.0.0)
|
||||
npm run build # 生产构建
|
||||
npm run preview # 预览构建产物
|
||||
```
|
||||
|
||||
## 潜在问题/改进点
|
||||
|
||||
1. `store/index.js` 中 `updateGalleryTasks` 的 action 引用了未在函数签名中声明的 `state` — 使用了外部模块级变量而非 context.state,这在严格模式下可能引起问题
|
||||
2. `state.websocket` 在 Vuex 中初始化为 `{}` 而非 `null`,检查方式不够严谨
|
||||
3. `getShortname` 函数逻辑复杂且有多处副作用,可读性较低
|
||||
4. 部分错误处理较简略(如 `queryGalleries` 中无网络错误处理)
|
||||
5. `store/index.js` 中 action 使用全局 `state` 变量(而非 `context.state`),可能导致作用域混淆
|
||||
Generated
+1703
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "lionwebsite",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "node --test tests/*.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.0.0",
|
||||
"element-plus": "^2.2.0",
|
||||
"qs": "^6.11.0",
|
||||
"vue": "^3.0.4",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "6.0.9",
|
||||
"vite": "8.3.0"
|
||||
}
|
||||
}
|
||||
+75
-11
@@ -1,17 +1,81 @@
|
||||
<script setup>
|
||||
import Side from "./components/Side.vue";
|
||||
import DashBoard from "./components/DashBoard.vue";
|
||||
import {computed, onMounted, onBeforeUnmount} from "vue";
|
||||
import store from "./store/index.js";
|
||||
import AuthScreen from "./components/AuthScreen.vue";
|
||||
import NavRail from "./components/NavRail.vue";
|
||||
import TaskView from "./components/TaskView.vue";
|
||||
import SearchView from "./components/SearchView.vue";
|
||||
import SettingsView from "./components/SettingsView.vue";
|
||||
import DetailPanel from "./components/DetailPanel.vue";
|
||||
import OnlineReader from "./components/OnlineReader.vue";
|
||||
|
||||
const resumeProgress = () => {
|
||||
if (store.state.isAuth && document.visibilityState === 'visible') store.dispatch("initWebsocket");
|
||||
};
|
||||
const pauseProgress = () => store.dispatch("disconnectWebsocket");
|
||||
onMounted(() => {
|
||||
restoreSettings();
|
||||
window.addEventListener('online', resumeProgress);
|
||||
window.addEventListener('pageshow', resumeProgress);
|
||||
window.addEventListener('pagehide', pauseProgress);
|
||||
document.addEventListener('visibilitychange', resumeProgress);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('online', resumeProgress);
|
||||
window.removeEventListener('pageshow', resumeProgress);
|
||||
window.removeEventListener('pagehide', pauseProgress);
|
||||
document.removeEventListener('visibilitychange', resumeProgress);
|
||||
pauseProgress();
|
||||
});
|
||||
|
||||
// Preferences are persisted in SettingsView; this restores them on load.
|
||||
function restoreSettings() {
|
||||
const auth = localStorage.getItem("auth");
|
||||
store.state.lengthPerPage = localStorage.getItem("lengthPerPage") === null
|
||||
? 30
|
||||
: Number(localStorage.getItem("lengthPerPage"));
|
||||
store.state.category = localStorage.getItem("category") || "myDownload";
|
||||
store.state.sortType = localStorage.getItem("sortType") || "createTime";
|
||||
store.state.galleryNameType = localStorage.getItem("galleryNameType") || "shortName";
|
||||
|
||||
const darkConfigStr = localStorage.getItem("darkConfig");
|
||||
let followSystem = false;
|
||||
if (darkConfigStr !== null) {
|
||||
try {
|
||||
followSystem = Boolean(JSON.parse(darkConfigStr).followSystem);
|
||||
} catch {
|
||||
followSystem = false;
|
||||
}
|
||||
}
|
||||
if (followSystem && window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
document.documentElement.classList.add("dark");
|
||||
else
|
||||
document.documentElement.classList.remove("dark");
|
||||
|
||||
if (auth !== null) store.dispatch("validate", auth);
|
||||
}
|
||||
|
||||
const loadComplete = computed(() => store.state.loadComplete);
|
||||
const activeView = computed(() => store.state.activeView);
|
||||
const selectedGid = computed(() => store.state.selectedGallery?.gid ?? null);
|
||||
|
||||
function openTask(gallery) {
|
||||
store.commit("_selectGallery", gallery);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app">
|
||||
<el-container>
|
||||
<el-aside width="750px">
|
||||
<Side/>
|
||||
</el-aside>
|
||||
<el-main>
|
||||
<DashBoard/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
<AuthScreen :load-complete="loadComplete" />
|
||||
|
||||
<div class="app" v-show="loadComplete">
|
||||
<NavRail />
|
||||
<main class="workspace">
|
||||
<TaskView v-show="activeView === 'tasks'" :selected-gid="selectedGid" @open="openTask" />
|
||||
<SearchView v-show="activeView === 'search'" />
|
||||
<SettingsView v-show="activeView === 'settings'" />
|
||||
</main>
|
||||
<DetailPanel />
|
||||
</div>
|
||||
|
||||
<OnlineReader />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
// Small stroke-based icon set (24x24 grid) so the UI needs no extra dependency.
|
||||
// Brand-ish icons are stroked; star/sun/moon render filled for legibility.
|
||||
const FILLED = new Set(['star', 'sun', 'moon']);
|
||||
const PATHS = {
|
||||
download: ["M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", "M7 10l5 5 5-5", "M12 15V3"],
|
||||
eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z", "M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"],
|
||||
star: ["M12 2.5l2.9 5.9 6.5.95-4.7 4.6 1.1 6.45L12 17.35 6.2 20.4l1.1-6.45-4.7-4.6 6.5-.95L12 2.5z"],
|
||||
refresh: ["M21 12a9 9 0 0 1-15.5 6.2L3 16", "M3 21v-5h5", "M3 12a9 9 0 0 1 15.5-6.2L21 8", "M21 3v5h-5"],
|
||||
trash: ["M3 6h18", "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6", "M10 11v6", "M14 11v6", "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"],
|
||||
search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M20 20l-4-4"],
|
||||
external: ["M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6", "M15 3h6v6", "M10 14L21 3"],
|
||||
"chevron-left": ["M15 18l-6-6 6-6"],
|
||||
"chevron-right": ["M9 6l6 6-6 6"],
|
||||
"chevrons-left": ["M11 17l-5-5 5-5", "M18 17l-5-5 5-5"],
|
||||
"chevrons-right": ["M13 17l5-5-5-5", "M6 17l5-5-5-5"],
|
||||
sun: ["M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z", "M12 2v2", "M12 20v2", "M4.5 4.5l1.4 1.4", "M18.1 18.1l1.4 1.4", "M2 12h2", "M20 12h2", "M4.5 19.5l1.4-1.4", "M18.1 5.9l1.4-1.4"],
|
||||
moon: ["M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"],
|
||||
list: ["M8 6h13", "M8 12h13", "M8 18h13", "M3 6h.01", "M3 12h.01", "M3 18h.01"],
|
||||
settings: ["M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z", "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 1 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 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 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 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 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-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"],
|
||||
sort: ["M11 5h10", "M11 9h7", "M11 13h4", "M3 17l3 3 3-3", "M6 18V4"],
|
||||
clock: ["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z", "M12 7v5l3 2"],
|
||||
close: ["M18 6L6 18", "M6 6l12 12"],
|
||||
image: ["M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", "M8.5 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z", "M21 15l-5-5L5 21"],
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
name: { type: String, required: true },
|
||||
size: { type: [Number, String], default: 16 },
|
||||
filled: { type: Boolean, default: false },
|
||||
});
|
||||
|
||||
const paths = computed(() => PATHS[props.name] || []);
|
||||
const isFilled = computed(() => props.filled || FILLED.has(props.name));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg class="app-icon"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
:fill="isFilled ? 'currentColor' : 'none'"
|
||||
stroke="currentColor"
|
||||
:stroke-width="isFilled ? 0 : 1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
focusable="false">
|
||||
<path v-for="(d, i) in paths" :key="i" :d="d" />
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
import {ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
|
||||
const AuthCode = ref("");
|
||||
const isRemember = ref(false);
|
||||
|
||||
defineProps({
|
||||
loadComplete: {type: Boolean, default: false},
|
||||
});
|
||||
|
||||
function validate() {
|
||||
if (AuthCode.value.trim() === "") {
|
||||
ElMessage("请输入授权码后再验证");
|
||||
return;
|
||||
}
|
||||
store.dispatch("validate", AuthCode.value);
|
||||
if (isRemember.value) localStorage.setItem("auth", AuthCode.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="!loadComplete" class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-brand">
|
||||
<span class="auth-mark" aria-hidden="true">L</span>
|
||||
<span class="auth-name">LionWebsite</span>
|
||||
</div>
|
||||
<h1>下载管理器</h1>
|
||||
<p class="auth-subtitle">输入授权码以继续</p>
|
||||
<div class="auth-form">
|
||||
<el-input v-model="AuthCode"
|
||||
placeholder="请输入授权码"
|
||||
size="large"
|
||||
show-password
|
||||
@keydown.enter="validate" />
|
||||
<el-checkbox v-model="isRemember">记住授权码</el-checkbox>
|
||||
<el-button @click="validate" type="primary" size="large" class="auth-btn">验证</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,580 +0,0 @@
|
||||
<template>
|
||||
<div class="DashBoard" v-show="loadComplete">
|
||||
<span>E站额度本周已用:{{weekUsed.weekUsedAmount}} 上次重置时间:{{weekUsed.lastResetAmountTime}}</span><br>
|
||||
<el-button @click="queryWeekUsedAmount">查询用量</el-button>
|
||||
<hr>
|
||||
<el-row>
|
||||
<el-col>
|
||||
查询参数类型: <el-select style="width: 125px" v-model="type" @change="resetLocalQuery()">
|
||||
<el-option value="link" label="链接"/>
|
||||
<el-option value="keyword" label="关键字"/>
|
||||
<el-option value="tag" label="标签"/>
|
||||
</el-select>
|
||||
<el-input style="width: 200px;" v-model="param" v-if="type !== 'tag'">
|
||||
<template #prepend>
|
||||
参数:
|
||||
</template>
|
||||
</el-input>
|
||||
<div v-if="type === 'tag'">
|
||||
<el-tag v-for="tid in paramForTags" closable @close="removeQueryTag(tid)">
|
||||
{{store.state.tags.get(tid).tag}}
|
||||
</el-tag>
|
||||
<el-autocomplete v-model="param" :fetch-suggestions="completeQueryTag" @select="handleTagSelect" ref="tagInput" placeholder="检索标签"/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<el-button @click="queryRemoteTask" v-show="type === 'link'">远程查询</el-button>
|
||||
<el-button @click="queryLocalTask">当前页查询</el-button>
|
||||
<el-button @click="removeAllQueryTag" v-show="type === 'tag'">清空查询标签</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br>
|
||||
<hr>
|
||||
<el-button @click="isAlterAuthCode = true">修改授权码</el-button>
|
||||
<el-button @click="deleteAuthCode">删除本地授权码</el-button>
|
||||
<el-button @click="isConfig = true">配置</el-button>
|
||||
<span style="display: inline">夜间模式</span>
|
||||
<el-switch @click="toggleStyle" v-model="isDark">夜间模式</el-switch>
|
||||
<hr>
|
||||
<el-button @click="isQuerying = true">里站搜索</el-button>
|
||||
<el-button @click="isViewingTag = true">查看标签</el-button>
|
||||
<hr>
|
||||
<el-button v-if="isLion" @click="resetUndone">重置任务</el-button>
|
||||
<div v-show="thumbnailGallery.url !== undefined">
|
||||
<span>
|
||||
{{thumbnailGallery.shortName}}<br>
|
||||
<el-space v-show="thumbnailGallery.tag !== ''">标签:{{thumbnailGallery.tag}}</el-space>
|
||||
</span><br>
|
||||
<picture>
|
||||
<el-image :src="thumbnailGallery.url" :preview-src-list="[thumbnailGallery.images[0], ]" :initial-index="0" class="preview"
|
||||
style="width: 300px; height: 250px" fit="contain"/>
|
||||
</picture>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog title="查询本子" v-model="chosenGallery">
|
||||
<el-image v-show='chosenGallery.thumb_link !== undefined'
|
||||
style="float: right; width: 250px; height: 250px" fit="contain"
|
||||
:src="'https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?path=' + chosenGallery.thumb_link"/>
|
||||
<table>
|
||||
<tr>本子名字:{{chosenGallery.name}}</tr>
|
||||
<tr>本子页数:{{chosenGallery.pages}}</tr>
|
||||
<tr>本子语言:{{chosenGallery.language}}</tr>
|
||||
<tr>本子大小:{{chosenGallery.fileSize}}</tr>
|
||||
<tr>本子状态:{{chosenGallery.status}}</tr>
|
||||
<tr v-if="chosenGallery.availableResolution">
|
||||
目标分辨率:<el-select v-model="targetResolution" style="width: 200px">
|
||||
<el-option v-for="(fileSize, resolution) in chosenGallery.availableResolution" :value="resolution"
|
||||
:label="resolution + ' ' + fileSize">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</tr>
|
||||
<tr v-if="chosenGallery.availableResolution">
|
||||
下载模式:<el-select v-model="targetDownloadMode" style="width: 200px" default-first-option>
|
||||
<el-option :value="1" label="仅下载"/>
|
||||
<el-option :value="2" label="仅在线看"/>
|
||||
<el-option :value="3" label="在线看并下载"/>
|
||||
</el-select>
|
||||
</tr>
|
||||
<tr>
|
||||
标签:<el-tag v-for="tid in paramForTags" closable @close="removeQueryTag(tid)">
|
||||
{{store.state.tags.get(tid).tag}}
|
||||
</el-tag>
|
||||
<el-autocomplete v-model="param" :fetch-suggestions="completeQueryTag" @select="handleTagSelect" ref="tagInputForSubmit"/>
|
||||
</tr>
|
||||
</table>
|
||||
<el-button @click="postTask" v-if="chosenGallery.availableResolution">下载</el-button>
|
||||
<el-button @click="removeAllQueryTag" v-if="chosenGallery.availableResolution">清空标签</el-button>
|
||||
<tr v-if="chosenGallery.status === '下载完成'">
|
||||
<el-button @click="onlineGalleryReader(chosenGallery.gid)">在线预览</el-button>
|
||||
<el-button @click="showThumbnailGallery(chosenGallery)">查看封面图</el-button>
|
||||
<el-button @click="deleteGallery">删除</el-button>
|
||||
</tr>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="修改授权码" v-model="isAlterAuthCode">
|
||||
<el-form>
|
||||
<el-form-item>
|
||||
<template #label>当前授权码</template>
|
||||
<template #default>{{realAuthCode}}</template>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>新的授权码</template>
|
||||
<template #default>
|
||||
<el-input v-model="newAuthCode"></el-input>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>再次输入授权码</template>
|
||||
<template #default>
|
||||
<el-input v-model="tempAuthCode"></el-input>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-footer>
|
||||
<el-button @click="alterAuthCode">提交</el-button>
|
||||
</el-footer>
|
||||
</el-dialog>
|
||||
|
||||
<HentaiSearch v-model:is-querying="isQuerying" @close="isQuerying = false"></HentaiSearch>
|
||||
|
||||
<el-dialog title="配置" v-model="isConfig">
|
||||
<div>
|
||||
夜间模式<hr>
|
||||
<span style="display: inline-block">夜间模式跟随系统</span>
|
||||
<el-switch v-model="darkConfig.followSystem"></el-switch><br>
|
||||
<span style="display: inline-block">自定义起始时间(精确到分)</span>
|
||||
<el-switch v-model="darkConfig.customTime"></el-switch><br>
|
||||
<el-form :disabled="!darkConfig.customTime">
|
||||
<el-time-picker v-model="darkConfig.startTime" /> ~
|
||||
<el-time-picker v-model="darkConfig.endTime"/>
|
||||
</el-form>
|
||||
</div>
|
||||
<div>
|
||||
在线预览<hr>
|
||||
<span style="display: inline-block">在线预览分页页数:</span>
|
||||
<input v-model="lengthPerPage">
|
||||
(建议为3的倍数,因为固定了一行三张)
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="saveConfig">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog title="查看标签" v-model="isViewingTag" style="margin-top: 0; width: 550px">
|
||||
<div style="text-align: center">
|
||||
输入关键字:<el-input style="width: 150px" v-model="tagKeyWord"></el-input>
|
||||
<el-button @click="tagKeyWord = ''">清空</el-button>
|
||||
<el-button @click="pullNewTag">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tags" height="450px" stripe>
|
||||
<el-table-column prop="id" label="id" width="50px" sortable/>
|
||||
<el-table-column prop="tag" label="标签" width="300px" sortable/>
|
||||
<el-table-column prop="usage" label="用量" width="75px" sortable/>
|
||||
|
||||
<el-table-column width="75px">
|
||||
<template #default="scoped">
|
||||
<el-button v-if="scoped.row.usage === 0" @click="deleteTag(scoped.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span style="color: red; font: bold 15px sans-serif">
|
||||
创建标签前先看看有没有符合的,尽量用统一一点的标签,比如已经有个图包就不要创建图集之类的了。毕竟标签多起来数据挺多的。
|
||||
</span>
|
||||
<template #footer>
|
||||
输入新标签:<el-input v-model="tag"></el-input>
|
||||
<el-button @click="postTag">创建标签</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="DashBoard" v-show="!loadComplete">
|
||||
<el-input v-model="AuthCode" placeholder="请输入授权码" style="padding-top: 200px"/> <br>
|
||||
<el-checkbox v-model="isRemember">是否记住授权码</el-checkbox>
|
||||
<el-button @click="validate" type="primary" @keydown.enter="validate" style="margin-left: 50px">验证</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import store from "../store";
|
||||
import {computed, ref, onMounted} from "vue";
|
||||
import {ElMessage} from "element-plus"
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import HentaiSearch from "./HentaiSearch.vue";
|
||||
|
||||
//授权码相关
|
||||
let AuthCode = ref("")
|
||||
let isRemember = ref(false)
|
||||
let isAlterAuthCode = ref(false)
|
||||
let newAuthCode = ref("")
|
||||
let tempAuthCode = ref("")
|
||||
|
||||
let isQuerying = ref(false)
|
||||
let isViewingTag = ref(false)
|
||||
let isConfig = ref(false)
|
||||
let isDark = ref(false)
|
||||
let keyword = ref("")
|
||||
let darkConfig = ref({})
|
||||
let lengthPerPage = ref(0)
|
||||
|
||||
//查询相关
|
||||
let type = ref("link")
|
||||
let param = ref("")
|
||||
let paramForTags = ref([]) //tidS
|
||||
|
||||
let targetResolution = ref("")
|
||||
let targetDownloadMode = ref("")
|
||||
let tag = ref("")
|
||||
let tagKeyWord = ref("") //查询tag的关键字
|
||||
|
||||
let realAuthCode = computed(() => {
|
||||
return store.state.AuthCode
|
||||
})
|
||||
|
||||
let chosenGallery = computed(() => {
|
||||
paramForTags.value.splice(0)
|
||||
param.value = ''
|
||||
return store.state.chosenGallery
|
||||
})
|
||||
|
||||
let loadComplete = computed(() => {
|
||||
return store.state.loadComplete
|
||||
})
|
||||
let weekUsed = computed(() => {
|
||||
return store.state.weekUsed
|
||||
})
|
||||
let tags = computed(() => {
|
||||
let tags = store.state.tags
|
||||
let result = []
|
||||
tags.forEach((tag) => {
|
||||
result.push(tag)
|
||||
})
|
||||
|
||||
if(isViewingTag.value) { //正在查看标签
|
||||
if (tagKeyWord.value.trim() === '')
|
||||
return result
|
||||
else
|
||||
return result.filter((tag) => {
|
||||
return tag.tag.includes(tagKeyWord.value)
|
||||
})
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
let thumbnailGallery = computed(() => {
|
||||
if(store.state.thumbnailGallery.images === undefined)
|
||||
store.state.thumbnailGallery.images = []
|
||||
return store.state.thumbnailGallery
|
||||
})
|
||||
|
||||
let isLion = computed(() => {
|
||||
return store.state.userId === 3
|
||||
})
|
||||
|
||||
function pullNewTag(){
|
||||
store.dispatch("loadTags")
|
||||
}
|
||||
|
||||
function postTag(){
|
||||
store.dispatch("postTag", tag.value)
|
||||
}
|
||||
|
||||
function deleteTag(tid){
|
||||
store.dispatch("deleteTag", tid)
|
||||
}
|
||||
|
||||
//修改授权码
|
||||
function alterAuthCode(){
|
||||
if(newAuthCode.value.trim() === "" || tempAuthCode.value.trim() === "" || newAuthCode.value !== tempAuthCode.value)
|
||||
ElMessage("请检查授权码输入是否错误")
|
||||
else {
|
||||
store.dispatch("alterAuthCode", newAuthCode.value)
|
||||
isAlterAuthCode.value = false
|
||||
newAuthCode.value = ""
|
||||
tempAuthCode.value = ""
|
||||
}
|
||||
}
|
||||
//查询用量
|
||||
function queryWeekUsedAmount(){
|
||||
store.dispatch("loadWeekUsedAmount")
|
||||
}
|
||||
//提交任务
|
||||
function postTask(){
|
||||
if(!validateLink(chosenGallery.value.link)){
|
||||
ElMessage("链接错误")
|
||||
return
|
||||
}
|
||||
if(targetResolution.value === ''){
|
||||
ElMessage("请选择分辨率再提交")
|
||||
return
|
||||
}
|
||||
if(targetDownloadMode.value === ''){
|
||||
ElMessage("请选择下载模式再提交")
|
||||
return
|
||||
}
|
||||
store.dispatch("postGalleryTask",
|
||||
{link: chosenGallery.value.link,
|
||||
targetResolution: targetResolution.value,
|
||||
mode: targetDownloadMode.value,
|
||||
tags:paramForTags.value})
|
||||
targetResolution.value = ""
|
||||
}
|
||||
|
||||
//查询任务
|
||||
function queryRemoteTask(){
|
||||
if(!validateLink(param.value)){
|
||||
ElMessage("链接错误")
|
||||
return
|
||||
}
|
||||
if(param.value.includes("e-hentai"))
|
||||
param.value = param.value.replace("e-hentai", "exhentai")
|
||||
store.dispatch("queryGalleryTask", param.value)
|
||||
}
|
||||
function queryLocalTask(){
|
||||
switch (type.value){
|
||||
case "link":
|
||||
store.commit("_searchLocalByLink", param.value)
|
||||
break
|
||||
case "keyword":
|
||||
store.commit("_searchLocalByKeyword", param.value)
|
||||
break
|
||||
case "tag":
|
||||
store.commit("_searchLocalByTag", paramForTags.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
let tagInput = ref({}) //用于查询
|
||||
let tagInputForSubmit = ref({}) //用于提交
|
||||
function completeQueryTag(keyWord, cb) {
|
||||
if(keyWord.includes(' ')) { //查询多个标签的时候
|
||||
let temp = keyWord.split(' ')
|
||||
keyWord = temp[temp.length - 1]
|
||||
}else{ //只有一个标签的时候
|
||||
keyWord = param.value
|
||||
}
|
||||
let result = []
|
||||
let skip
|
||||
let hit = false //用于检测是否有重复标签
|
||||
tags.value.forEach((tag) => {
|
||||
if(tag.tag.includes(keyWord)) {
|
||||
skip = false
|
||||
for (let id of paramForTags.value) { //跳过已选中的标签
|
||||
if(tag.id === id){
|
||||
if(!hit && tag.tag === keyWord){ //是否命中标签
|
||||
hit = true
|
||||
}
|
||||
skip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if(!skip)
|
||||
result.push({value: tag.tag, tid: tag.id})
|
||||
}
|
||||
})
|
||||
if(result.length === 0 && !keyWord.includes("#") && chosenGallery.value.gid !== undefined && !hit){ //未命中结果并且准备与下载任务一并提交
|
||||
result.push({value: '新建 #' + keyWord + ' 标签?', tag:keyWord})
|
||||
}
|
||||
cb(result)
|
||||
}
|
||||
|
||||
function removeQueryTag(tid){
|
||||
for (let i=0; i<paramForTags.value.length; i++){
|
||||
if(paramForTags.value[i] === tid){
|
||||
paramForTags.value.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
if(chosenGallery.value.gid === undefined) //查询本地标签
|
||||
queryLocalTask()
|
||||
}
|
||||
|
||||
function removeAllQueryTag(){
|
||||
paramForTags.value.splice(0)
|
||||
if(chosenGallery.value.gid === undefined) //查询本地标签
|
||||
queryLocalTask()
|
||||
}
|
||||
|
||||
function handleTagSelect(data){
|
||||
if('tag' in data){ //需要新建,不知道怎么处理回调,直接把axios搬到vue里面
|
||||
axios.post("https://downloader.lionwebsite.xyz/GalleryManage/tag?" + qs.stringify({
|
||||
tag:data.tag,
|
||||
AuthCode: store.state.AuthCode
|
||||
})
|
||||
).then((res) => {
|
||||
if (res.data.result === 'success') {
|
||||
ElMessage('创建标签成功')
|
||||
paramForTags.value.push(parseInt(res.data.tid))
|
||||
tagInputForSubmit.value.blur()
|
||||
store.dispatch("loadTags", false).then()
|
||||
}
|
||||
else
|
||||
ElMessage(res.data.data)
|
||||
})}else{
|
||||
paramForTags.value.push(data.tid)
|
||||
console.log(chosenGallery)
|
||||
if(chosenGallery.value.gid === undefined){
|
||||
queryLocalTask()
|
||||
tagInput.value.blur()
|
||||
}else{
|
||||
tagInputForSubmit.value.blur()
|
||||
}
|
||||
}
|
||||
param.value = ''
|
||||
}
|
||||
|
||||
function resetLocalQuery(){
|
||||
store.commit("_searchLocalByKeyword", "")
|
||||
param.value = ""
|
||||
}
|
||||
|
||||
function deleteGallery(){
|
||||
store.dispatch("deleteGallery", chosenGallery.value.gid)
|
||||
}
|
||||
|
||||
//验证授权码
|
||||
function validate(){
|
||||
if(AuthCode.value.trim() === ""){
|
||||
ElMessage("请输入授权码后再验证")
|
||||
}
|
||||
else{
|
||||
store.dispatch("validate", AuthCode.value)
|
||||
if(isRemember.value)
|
||||
localStorage.setItem("auth", AuthCode.value)
|
||||
}
|
||||
}
|
||||
|
||||
//验证链接以及伪装链接
|
||||
function validateLink(rawLink){
|
||||
if(rawLink.trim() === "")
|
||||
return false
|
||||
if(rawLink.includes("hentai"))
|
||||
return rawLink.includes("/g/")
|
||||
else
|
||||
return false
|
||||
}
|
||||
|
||||
//在线阅读以及展示缩略图
|
||||
function onlineGalleryReader(gid){
|
||||
store.dispatch("queryOnlineLinks", gid)
|
||||
}
|
||||
function showThumbnailGallery(gallery){
|
||||
store.commit("_changeThumbnailGallery", gallery)
|
||||
document.querySelector(".preview > img").click()
|
||||
}
|
||||
|
||||
//重新给节点发送未完成任务
|
||||
function resetUndone(){
|
||||
store.dispatch("resetUndone").then()
|
||||
}
|
||||
function deleteAuthCode(){
|
||||
localStorage.removeItem('auth')
|
||||
ElMessage("删除授权码完成")
|
||||
}
|
||||
function toggleStyle(){
|
||||
if(isDark.value)
|
||||
dark()
|
||||
else
|
||||
light()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const auth = localStorage.getItem("auth")
|
||||
adjustForStyle()
|
||||
store.state.lengthPerPage = localStorage.getItem("lengthPerPage")
|
||||
store.state.lengthPerPage = store.state.lengthPerPage === null ? 30: Number(store.state.lengthPerPage)
|
||||
lengthPerPage.value = store.state.lengthPerPage
|
||||
|
||||
if(auth !== null){
|
||||
store.dispatch("validate", auth)
|
||||
AuthCode.value = localStorage.getItem("auth")
|
||||
}
|
||||
})
|
||||
|
||||
function adjustForStyle(){
|
||||
let darkConfigStr = localStorage.getItem("darkConfig")
|
||||
if(darkConfigStr !== null) {
|
||||
darkConfig.value = JSON.parse(darkConfigStr)
|
||||
if (darkConfig.value.followSystem)
|
||||
if (isSystemDark()) {
|
||||
dark()
|
||||
isDark.value = true
|
||||
}
|
||||
else {
|
||||
light()
|
||||
isDark.value = false
|
||||
}
|
||||
|
||||
if (darkConfig.value.customTime)
|
||||
if (isDarkTime(darkConfig.value)) {
|
||||
dark()
|
||||
isDark.value = true
|
||||
}
|
||||
else {
|
||||
light()
|
||||
isDark.value = false
|
||||
}
|
||||
|
||||
if(isDark.value)
|
||||
dark()
|
||||
else
|
||||
light()
|
||||
}else {
|
||||
light()
|
||||
darkConfig.value = {'followSystem': false, 'customTime': false}
|
||||
}
|
||||
}
|
||||
function isSystemDark(){
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
function isDarkTime(darkConfig){
|
||||
let date = new Date()
|
||||
let startTime = darkConfig.startTime
|
||||
let endTime = darkConfig.endTime
|
||||
if(startTime.hour > endTime.hour){ //隔夜 22:00 ~ 8:00
|
||||
if(date.getHours() > endTime.getHours() && date.getHours() < startTime.getHours()){ // 大于结束时间且小于起始时间 16:00
|
||||
return false
|
||||
}else if(date.getHours() === endTime.getHours()){ //22:00 ~ 8:30 8:26
|
||||
return date.getMinutes() <= endTime.getMinutes();
|
||||
}else if(date.getHours() === startTime.getHours()){ //22:30 ~ 8:00 22:46
|
||||
return date.getMinutes() > startTime.getMinutes();
|
||||
}else
|
||||
return true
|
||||
}else{ //不隔夜 00:00 ~ 6:00 22:00 ~ 23:00
|
||||
if(date.getHours() > endTime.getHours() || date.getHours() < startTime.getHours()){
|
||||
return false
|
||||
}else if(date.getHours() === startTime.getHours()){ // 01:30 ~ 06:00 01:32
|
||||
return date.getMinutes() >= startTime.getMinutes();
|
||||
}else if(date.getHours() === endTime.getHours()){ // 01:30 ~ 06:30 06:35
|
||||
return date.getMinutes() <= endTime.getMinutes();
|
||||
}else
|
||||
return true
|
||||
}
|
||||
}
|
||||
function dark(){
|
||||
let html = document.querySelector("html")
|
||||
if(!html.classList.contains("dark"))
|
||||
html.classList.add('dark')
|
||||
document.querySelector(".DashBoard").style.setProperty("background-color", null)
|
||||
document.querySelector(".app").style.setProperty("background-color", null)
|
||||
}
|
||||
function light(){
|
||||
let html = document.querySelector("html")
|
||||
if(html.classList.contains("dark"))
|
||||
html.classList.remove('dark')
|
||||
|
||||
document.querySelector(".DashBoard").style.setProperty("background-color", "ghostwhite")
|
||||
document.querySelector(".app").style.setProperty("background-color", "#c6e2ff")
|
||||
}
|
||||
function saveConfig(){
|
||||
if(darkConfig.value.customTime) {
|
||||
if(darkConfig.value.startTime === undefined || darkConfig.value.endTime === undefined){
|
||||
ElMessage("请正确选择起始时间")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if(lengthPerPage.value < 0 || lengthPerPage.value > 30) {
|
||||
ElMessage("分页页数设置错误,范围1~30")
|
||||
lengthPerPage.value = 30
|
||||
}
|
||||
else {
|
||||
store.state.lengthPerPage = Number(lengthPerPage.value)
|
||||
localStorage.setItem("lengthPerPage", lengthPerPage.value)
|
||||
}
|
||||
localStorage.setItem("darkConfig", JSON.stringify(darkConfig.value))
|
||||
isConfig.value = false
|
||||
adjustForStyle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.DashBoard{
|
||||
width: auto;
|
||||
height: 92vh;
|
||||
text-align: center;
|
||||
}
|
||||
.el-input{
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,254 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
import {ehThumbnailUrl, pickDefaultResolution} from "../utils/thumbnail.js";
|
||||
|
||||
const targetResolution = ref("");
|
||||
const isRetrying = ref(false);
|
||||
|
||||
// Two flows share this panel: a task already in the list, and a remote gallery
|
||||
// that was resolved from a link or from the online search.
|
||||
const task = computed(() => store.state.selectedGallery);
|
||||
const remote = computed(() => store.state.chosenGallery || null);
|
||||
|
||||
// A freshly resolved gallery is a pending decision, so it takes the panel over
|
||||
// even if a task was selected earlier.
|
||||
const showRemote = computed(() => Boolean(remote.value));
|
||||
|
||||
watch(remote, gallery => {
|
||||
targetResolution.value = gallery
|
||||
? pickDefaultResolution(gallery.availableResolution)
|
||||
: "";
|
||||
});
|
||||
|
||||
// Admin-only fields come from the backend's isAdmin flag, not a hardcoded id.
|
||||
const isAdmin = computed(() => store.state.isAdmin);
|
||||
|
||||
const isDone = computed(() => Boolean(task.value && task.value.status === "下载完成"));
|
||||
|
||||
const statusClass = computed(() => {
|
||||
const status = task.value?.status;
|
||||
if (status === "下载完成") return "is-done";
|
||||
if (status === "下载中" || status === "压缩中") return "is-running";
|
||||
return "is-waiting";
|
||||
});
|
||||
|
||||
const percent = computed(() => {
|
||||
const item = task.value;
|
||||
if (!item || item.status !== "下载中" || !item.pages) return null;
|
||||
const done = Number(item.proceeding) || 0;
|
||||
return Math.min(100, Math.max(0, Math.round((done / item.pages) * 100)));
|
||||
});
|
||||
|
||||
const resolutions = computed(() => {
|
||||
const gallery = remote.value;
|
||||
if (!gallery || !gallery.availableResolution) return [];
|
||||
return Object.entries(gallery.availableResolution).map(([resolution, fileSize]) => ({
|
||||
resolution,
|
||||
fileSize,
|
||||
}));
|
||||
});
|
||||
|
||||
// Remote galleries carry a raw path; the proxy URL needs the active auth code.
|
||||
const remoteThumb = computed(() => {
|
||||
const gallery = remote.value;
|
||||
if (!gallery) return "";
|
||||
return ehThumbnailUrl(gallery.thumb_link || gallery.thumbnailUrl, store.state.AuthCode);
|
||||
});
|
||||
|
||||
function closeInspector() {
|
||||
if (remote.value) store.commit("_setChosenGallery", {gallery: false});
|
||||
else store.commit("_selectGallery", null);
|
||||
}
|
||||
|
||||
function downloadTask() {
|
||||
if (!task.value) return;
|
||||
if (task.value.download) window.open(task.value.download);
|
||||
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||
}
|
||||
|
||||
function downloadRemote() {
|
||||
const gallery = remote.value;
|
||||
if (!gallery) return;
|
||||
if (gallery.download) window.open(gallery.download);
|
||||
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||
}
|
||||
|
||||
function readOnline(gallery) {
|
||||
if (!gallery) return;
|
||||
store.dispatch("readOnlineGallery", gallery);
|
||||
}
|
||||
|
||||
function toggleCollect() {
|
||||
if (!task.value) return;
|
||||
const gallery = task.value;
|
||||
if (gallery.isCollect) store.dispatch("disCollectGallery", gallery.gid);
|
||||
else store.dispatch("collectGallery", gallery.gid);
|
||||
}
|
||||
|
||||
function deleteTask() {
|
||||
if (!task.value) return;
|
||||
store.dispatch("deleteGallery", task.value.gid).then(() => store.commit("_selectGallery", null));
|
||||
}
|
||||
|
||||
async function retryTask() {
|
||||
if (!task.value || isRetrying.value) return;
|
||||
isRetrying.value = true;
|
||||
try {
|
||||
await store.dispatch("retryGallery", task.value.gid);
|
||||
} finally {
|
||||
isRetrying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function submitTask() {
|
||||
const gallery = remote.value;
|
||||
if (!gallery) return;
|
||||
if (targetResolution.value === "") {
|
||||
ElMessage("请选择分辨率再提交");
|
||||
return;
|
||||
}
|
||||
store.dispatch("postGalleryTask", {
|
||||
link: gallery.link,
|
||||
targetResolution: targetResolution.value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="inspector" aria-label="详情">
|
||||
<template v-if="showRemote">
|
||||
<header class="inspector-bar">
|
||||
<h2 class="section-title">远端任务</h2>
|
||||
<span class="view-bar-spacer"></span>
|
||||
<button type="button" class="icon-button" title="取消" aria-label="取消" @click="closeInspector">
|
||||
<AppIcon name="close" :size="15" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="inspector-scroll">
|
||||
<div class="preview-frame">
|
||||
<img v-if="remoteThumb" :src="remoteThumb" alt="" loading="lazy">
|
||||
<span v-else class="preview-empty">
|
||||
<AppIcon name="image" :size="22" />无缩略图
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="inspector-title">{{ remote.name }}</h3>
|
||||
|
||||
<div class="inspector-badges">
|
||||
<span v-if="remote.status" class="status-pill is-waiting">{{ remote.status }}</span>
|
||||
<span v-if="remote.pages" class="inspector-badge">{{ remote.pages }} 页</span>
|
||||
<span v-if="remote.fileSize" class="inspector-badge">{{ remote.fileSize }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="resolutions.length">
|
||||
<p class="section-title inspector-label">目标分辨率</p>
|
||||
<div class="resolution-list">
|
||||
<button v-for="item in resolutions"
|
||||
:key="item.resolution"
|
||||
type="button"
|
||||
:aria-pressed="targetResolution === item.resolution"
|
||||
@click="targetResolution = item.resolution">
|
||||
<span>{{ item.resolution }}</span>
|
||||
<span class="resolution-size">{{ item.fileSize }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="inspector-actions is-remote">
|
||||
<template v-if="resolutions.length">
|
||||
<el-button type="primary" @click="submitTask">提交下载</el-button>
|
||||
<el-button @click="readOnline(remote)">
|
||||
<AppIcon name="eye" :size="15" />在线看
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" @click="downloadRemote">
|
||||
<AppIcon name="download" :size="15" />下载文件
|
||||
</el-button>
|
||||
<el-button @click="readOnline(remote)">
|
||||
<AppIcon name="eye" :size="15" />在线看
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="task">
|
||||
<header class="inspector-bar">
|
||||
<h2 class="section-title">任务详情</h2>
|
||||
<span class="view-bar-spacer"></span>
|
||||
<button type="button" class="icon-button" title="收起详情" aria-label="收起详情" @click="closeInspector">
|
||||
<AppIcon name="close" :size="15" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="inspector-scroll">
|
||||
<div class="preview-frame">
|
||||
<img v-if="task.thumb_link" :src="task.thumb_link" alt="" loading="lazy">
|
||||
<span v-else class="preview-empty">
|
||||
<AppIcon name="image" :size="22" />无缩略图
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 class="inspector-title">{{ task.name }}</h3>
|
||||
|
||||
<div class="inspector-badges">
|
||||
<span class="status-pill" :class="statusClass">{{ task.status }}</span>
|
||||
<span class="inspector-badge">{{ task.pages }} 页</span>
|
||||
<span class="inspector-badge">{{ task.language }}</span>
|
||||
<span class="inspector-badge">{{ task.fileSize }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="percent !== null" class="progress-line">
|
||||
<el-progress :percentage="percent" :stroke-width="5" :show-text="false" />
|
||||
<span class="progress-value">{{ percent }}% · {{ task.proceeding }}/{{ task.pages }} 页</span>
|
||||
</div>
|
||||
|
||||
<dl class="inspector-facts">
|
||||
<dt>分辨率</dt>
|
||||
<dd>{{ task.resolution || "—" }}</dd>
|
||||
<dt>创建时间</dt>
|
||||
<dd>{{ task.createTimeDisplay || "—" }}</dd>
|
||||
<dt>文件大小</dt>
|
||||
<dd>{{ task.fileSize || "—" }}</dd>
|
||||
<dt>原始页面</dt>
|
||||
<dd>
|
||||
<a v-if="task.link" :href="task.link" target="_blank" rel="noopener">打开链接</a>
|
||||
<span v-else>—</span>
|
||||
</dd>
|
||||
<template v-if="isAdmin">
|
||||
<dt>下载人</dt>
|
||||
<dd>{{ task.downloaderName || `#${task.downloader}` }}</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="inspector-actions">
|
||||
<el-button type="primary" :disabled="!isDone" @click="downloadTask">
|
||||
<AppIcon name="download" :size="15" />下载
|
||||
</el-button>
|
||||
<el-button @click="readOnline(task)">
|
||||
<AppIcon name="eye" :size="15" />在线看
|
||||
</el-button>
|
||||
<el-button :class="{'is-collected': task.isCollect}" @click="toggleCollect">
|
||||
<AppIcon name="star" :size="15" />{{ task.isCollect ? "已收藏" : "收藏" }}
|
||||
</el-button>
|
||||
<el-button v-if="!isDone" :loading="isRetrying" @click="retryTask">
|
||||
<AppIcon name="refresh" :size="15" />重试
|
||||
</el-button>
|
||||
<el-button type="danger" plain :disabled="!isDone" @click="deleteTask">
|
||||
<AppIcon name="trash" :size="15" />删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="inspector-empty">
|
||||
<AppIcon name="image" :size="26" />
|
||||
<p>选择一条任务查看详情</p>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -1,126 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
import {ref, watch} from "vue";
|
||||
import axios from "axios";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
|
||||
let props = defineProps(['isQuerying'])
|
||||
let emit = defineEmits(['close'])
|
||||
let scrollBar = ref()
|
||||
let keyword = ref("")
|
||||
let queryPage = ref({})
|
||||
let galleries = ref([])
|
||||
let param = ref()
|
||||
let isShowUp = ref()
|
||||
|
||||
watch(props, () => {
|
||||
isShowUp.value = props.isQuerying
|
||||
})
|
||||
|
||||
function queryGalleries(link) {
|
||||
let tempParam
|
||||
if (link !== null) {
|
||||
let url = new URL(link)
|
||||
tempParam = url.search.replace("?f_search=", "")
|
||||
} else {
|
||||
tempParam = keyword.value
|
||||
}
|
||||
tempParam = tempParam.replace(" ", "+")
|
||||
document.getElementById("loading").style.display = "inline-block";
|
||||
|
||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + tempParam)
|
||||
.then((res) => {
|
||||
document.getElementById("loading").style.display = "none";
|
||||
if (res.data.result === "success") {
|
||||
let tempGalleries = JSON.parse(res.data.data)
|
||||
queryPage.value.first = 'first' in res.data ? res.data.first : undefined
|
||||
queryPage.value.previous = 'previous' in res.data ? res.data.previous : undefined
|
||||
queryPage.value.next = 'next' in res.data ? res.data.next : undefined
|
||||
queryPage.value.last = 'last' in res.data ? res.data.last : undefined
|
||||
|
||||
galleries.value.splice(0)
|
||||
tempGalleries.forEach((gallery) => {
|
||||
galleries.value.push(gallery)
|
||||
})
|
||||
|
||||
scrollBar.value.setScrollTop(0)
|
||||
} else {
|
||||
ElMessage({message: res.data.data, type: "error"})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function queryRemoteTask(){
|
||||
if(!validateLink(param.value)){
|
||||
ElMessage("链接错误")
|
||||
return
|
||||
}
|
||||
store.dispatch("queryGalleryTask", param.value)
|
||||
}
|
||||
|
||||
function validateLink(rawLink){
|
||||
if(rawLink.trim() === "")
|
||||
return false
|
||||
if(rawLink.includes("hentai"))
|
||||
return rawLink.includes("/g/")
|
||||
else
|
||||
return false
|
||||
}
|
||||
|
||||
function close(){
|
||||
emit("close")
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog title="里站搜索" v-model="isShowUp" top="0" style="margin-bottom: 0" @close="close">
|
||||
<div style="text-align: center">
|
||||
<el-input v-model="keyword"></el-input> <el-button @click="queryGalleries(null)">查询</el-button> <div id="loading"/>
|
||||
</div>
|
||||
<el-scrollbar height="75vh" ref="scrollBar">
|
||||
<div style="height: 251px; width: 100%; " v-for="gallery in galleries">
|
||||
<el-image alt="picture" :preview-src-list="['https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?path=' + gallery.thumbnailUrl,]"
|
||||
:src="'https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?path=' + gallery.thumbnailUrl"
|
||||
style="height:250px;width:250px;float: left;"
|
||||
fit="contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div style="font: bold 16px semi-condensed; margin-top: 10px; padding-top: 15px; padding-left: 275px">
|
||||
<span>{{gallery.name}}</span><br><br>
|
||||
<span>上传时间:{{gallery.uploadTime}}</span><br>
|
||||
<span>页数:{{gallery.page}}</span><br>
|
||||
<span class="ct6">类型:{{gallery.type}}</span><br>
|
||||
<a :href="gallery.link">链接</a><br>
|
||||
<el-button style=" margin-left: 80%; margin-top: -5vh" @click="param=gallery.link; queryRemoteTask()">查看</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div style="padding-top: 1vh; text-align: center">
|
||||
<el-button @click="queryGalleries(queryPage.first)" :disabled="queryPage.first === undefined">首页</el-button>
|
||||
<el-button @click="queryGalleries(queryPage.previous)" :disabled="queryPage.previous === undefined">上一页</el-button>
|
||||
<el-button @click="queryGalleries(queryPage.next)" :disabled="queryPage.next === undefined">下一页</el-button>
|
||||
<el-button @click="queryGalleries(queryPage.last)" :disabled="queryPage.last === undefined">尾页</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.el-input{
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#loading {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border: 2px solid #ccc;
|
||||
border-top-color: #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import store from "../store/index.js";
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
|
||||
const activeView = computed({
|
||||
get: () => store.state.activeView,
|
||||
set: value => store.commit("_setActiveView", value),
|
||||
});
|
||||
|
||||
const taskCount = computed(() => store.state.totalGalleryTask.length);
|
||||
const searchCount = computed(() => store.state.searchTask.length);
|
||||
|
||||
const isConnected = computed(() => store.state.connectionStatus === "connected");
|
||||
const connectionLabel = computed(() => ({
|
||||
connected: "进度已连接",
|
||||
connecting: "正在连接进度",
|
||||
reconnecting: "连接中断,正在重连",
|
||||
disconnected: "进度连接已断开",
|
||||
})[store.state.connectionStatus]);
|
||||
|
||||
const username = computed(() => store.state.username || "未登录");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="nav-rail" aria-label="主导航">
|
||||
<div class="nav-brand" aria-hidden="true">L</div>
|
||||
|
||||
<div class="nav-items">
|
||||
<button type="button"
|
||||
class="nav-item"
|
||||
:class="{'is-active': activeView === 'tasks'}"
|
||||
:aria-current="activeView === 'tasks' ? 'page' : undefined"
|
||||
title="任务"
|
||||
@click="activeView = 'tasks'">
|
||||
<AppIcon name="list" :size="20" />
|
||||
<span v-if="taskCount" class="nav-badge">{{ taskCount }}</span>
|
||||
<span class="nav-label">任务</span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="nav-item"
|
||||
:class="{'is-active': activeView === 'search'}"
|
||||
:aria-current="activeView === 'search' ? 'page' : undefined"
|
||||
title="搜索"
|
||||
@click="activeView = 'search'">
|
||||
<AppIcon name="search" :size="20" />
|
||||
<span v-if="searchCount" class="nav-badge">{{ searchCount }}</span>
|
||||
<span class="nav-label">搜索</span>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="nav-item"
|
||||
:class="{'is-active': activeView === 'settings'}"
|
||||
:aria-current="activeView === 'settings' ? 'page' : undefined"
|
||||
title="设置"
|
||||
@click="activeView = 'settings'">
|
||||
<AppIcon name="settings" :size="20" />
|
||||
<span class="nav-label">设置</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-foot">
|
||||
<span class="nav-connection"
|
||||
:class="{'is-connected': isConnected}"
|
||||
role="status"
|
||||
:title="connectionLabel">
|
||||
<i class="connection-dot"></i>
|
||||
</span>
|
||||
<span class="nav-user" :title="username">{{ username }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
+405
-91
@@ -1,120 +1,434 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {computed, nextTick, onBeforeUnmount, ref, watch} from "vue";
|
||||
import {startImages, settleImage, retryImage} from "../utils/progressiveImages.js";
|
||||
import store from "../store/index.js";
|
||||
let props = defineProps(['currentGallery', 'isOnlineReading'])
|
||||
let emit = defineEmits(['close'])
|
||||
let isShowUp = ref()
|
||||
let onlineReadingScrollbar = ref()
|
||||
let links = ref()
|
||||
let index = ref(0)
|
||||
let temp_index = ref(0) //用于跳转
|
||||
let max = ref(0)
|
||||
let current_page = 0
|
||||
let lengthPerPage = computed(() => {
|
||||
return store.state.lengthPerPage
|
||||
})
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
import {ehThumbnailUrl} from "../utils/thumbnail.js";
|
||||
|
||||
watch(props, (props)=>{
|
||||
isShowUp.value = props.isOnlineReading
|
||||
alterPage()
|
||||
})
|
||||
const viewer = ref(null);
|
||||
const readingGallery = computed(() => store.state.readingGallery);
|
||||
|
||||
//切换本子
|
||||
function alterPage(){
|
||||
if(props.currentGallery.images.length > lengthPerPage.value){
|
||||
links.value = props.currentGallery.images.slice(0, lengthPerPage.value)
|
||||
max.value = Math.ceil(props.currentGallery.images.length / lengthPerPage.value)
|
||||
}else{
|
||||
links.value = props.currentGallery.images
|
||||
max.value = 0
|
||||
// Images arrive a page at a time; "page" is a window into the full gallery.
|
||||
const links = ref([]);
|
||||
const pageIndex = ref(0);
|
||||
const pageInput = ref(1);
|
||||
const isReading = ref(false);
|
||||
const imagesForLoading = ref([]);
|
||||
const currentImage = ref(0);
|
||||
const fitMode = ref("height");
|
||||
|
||||
// Remote galleries keep a raw thumbnail path; tasks store an already-proxied URL.
|
||||
const thumbUrl = computed(() => {
|
||||
const path = readingGallery.value.thumb_link;
|
||||
if (!path) return "";
|
||||
return /^https?:/i.test(path) ? path : ehThumbnailUrl(path, store.state.AuthCode);
|
||||
});
|
||||
|
||||
const lengthPerPage = computed(() => Math.max(1, Number(store.state.lengthPerPage) || 10));
|
||||
const totalImages = computed(() => readingGallery.value.images.length);
|
||||
const pageCount = computed(() =>
|
||||
Math.max(1, Math.ceil(totalImages.value / lengthPerPage.value)));
|
||||
|
||||
const rangeLabel = computed(() => {
|
||||
if (totalImages.value <= lengthPerPage.value) return `共 ${totalImages.value} 页`;
|
||||
const start = pageIndex.value * lengthPerPage.value + 1;
|
||||
const end = Math.min(totalImages.value, start + links.value.length - 1);
|
||||
return `${start} - ${end} / ${totalImages.value}`;
|
||||
});
|
||||
|
||||
watch(() => store.state.isReading, open => {
|
||||
if (open && !isReading.value) {
|
||||
showPage(0);
|
||||
isReading.value = true;
|
||||
} else if (!open && isReading.value) {
|
||||
isReading.value = false;
|
||||
}
|
||||
index.value = 0
|
||||
temp_index.value = 1
|
||||
});
|
||||
|
||||
function showPage(target) {
|
||||
if (target < 0 || target >= pageCount.value) return;
|
||||
pageIndex.value = target;
|
||||
pageInput.value = target + 1;
|
||||
currentImage.value = 0;
|
||||
links.value = readingGallery.value.images.slice(
|
||||
target * lengthPerPage.value,
|
||||
(target + 1) * lengthPerPage.value
|
||||
);
|
||||
imagesForLoading.value = startImages(links.value);
|
||||
nextTick(() => viewer.value?.setScrollTop(0));
|
||||
}
|
||||
|
||||
//跳转到对应页数
|
||||
function jump(targetIndex){
|
||||
links.value = props.currentGallery.images.slice(targetIndex * lengthPerPage.value, (targetIndex + 1) * lengthPerPage.value)
|
||||
index.value = targetIndex
|
||||
temp_index.value = targetIndex + 1
|
||||
onlineReadingScrollbar.value.setScrollTop(0)
|
||||
function imageResult(item, failed = false) {
|
||||
settleImage(imagesForLoading.value, links.value, item, failed);
|
||||
}
|
||||
|
||||
function closeDialog(){
|
||||
onlineReadingScrollbar.value.setScrollTop(0)
|
||||
emit("close")
|
||||
function close() {
|
||||
imagesForLoading.value = [];
|
||||
links.value = [];
|
||||
store.commit("_closeReader");
|
||||
}
|
||||
|
||||
function switch_page(target_page){
|
||||
console.log(current_page, target_page)
|
||||
//上一页
|
||||
if(target_page > (current_page + 1) && current_page === 0 && index.value > 0) {
|
||||
console.log("上一页")
|
||||
document.querySelector("span.el-image-viewer__btn.el-image-viewer__close").click()
|
||||
jump(index.value - 1)
|
||||
onlineReadingScrollbar.value.setScrollTop(onlineReadingScrollbar.value.wrapRef.scrollHeight)
|
||||
let top = 0
|
||||
let timer = setInterval(() => {
|
||||
if(onlineReadingScrollbar.value.scrollTop === top){
|
||||
clearInterval(timer)
|
||||
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:last-child > img").click()
|
||||
// Scroll the active image into view when the user steps between images.
|
||||
function revealCurrent() {
|
||||
nextTick(() => {
|
||||
const node = viewer.value?.wrapRef?.querySelectorAll(".reader-image")[currentImage.value];
|
||||
node?.scrollIntoView({block: "nearest", behavior: "smooth"});
|
||||
});
|
||||
}
|
||||
top = onlineReadingScrollbar.value.scrollTop
|
||||
onlineReadingScrollbar.value.setScrollTop(onlineReadingScrollbar.value.wrapRef.scrollHeight)
|
||||
}, 100)
|
||||
//下一页
|
||||
}else if(target_page === 0 && current_page === lengthPerPage.value - 1 && index.value < max.value){
|
||||
console.log("下一页")
|
||||
jump(index.value + 1)
|
||||
current_page = 0
|
||||
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:nth-child(1) > img").click()
|
||||
|
||||
function step(delta) {
|
||||
if (currentImage.value + delta < 0) {
|
||||
if (pageIndex.value > 0) showPage(pageIndex.value - 1);
|
||||
return;
|
||||
}
|
||||
else{
|
||||
current_page = target_page
|
||||
if (currentImage.value + delta >= links.value.length) {
|
||||
if (pageIndex.value < pageCount.value - 1) showPage(pageIndex.value + 1);
|
||||
return;
|
||||
}
|
||||
currentImage.value += delta;
|
||||
revealCurrent();
|
||||
}
|
||||
|
||||
// Reader shortcuts: arrows step, space/esc close or advance, +/- fit.
|
||||
function onKeydown(event) {
|
||||
if (!isReading.value) return;
|
||||
// The built-in image viewer owns the keyboard while it is open.
|
||||
if (document.querySelector(".el-image-viewer__wrapper")) return;
|
||||
const key = event.key;
|
||||
if (key === "ArrowRight" || key === "PageDown" || key === " ") {
|
||||
event.preventDefault();
|
||||
step(1);
|
||||
} else if (key === "ArrowLeft" || key === "PageUp") {
|
||||
event.preventDefault();
|
||||
step(-1);
|
||||
} else if (key === "Escape") {
|
||||
close();
|
||||
} else if (key === "+" || key === "=") {
|
||||
fitMode.value = "height";
|
||||
} else if (key === "-") {
|
||||
fitMode.value = "width";
|
||||
}
|
||||
}
|
||||
|
||||
function set_current_page(page){
|
||||
current_page = page
|
||||
}
|
||||
window.addEventListener("keydown", onKeydown);
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", onKeydown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog v-model="isShowUp" width="90%" style="margin-top: 0; margin-bottom: 0; padding: 0" @close="closeDialog">
|
||||
<template #header style="padding-bottom: 0">
|
||||
在线预览: {{currentGallery.name}} 页数:{{currentGallery.pages}}<br>
|
||||
<div style="font-size: 3vh; display: inline" v-if="max > 0">
|
||||
{{ index + 1 }} - {{ (index) * lengthPerPage + 1 }} ~
|
||||
{{ (index + 1) * lengthPerPage + 1 > currentGallery.images.length ? currentGallery.images.length : (index + 1) * lengthPerPage }}
|
||||
<Teleport to="body">
|
||||
<div v-if="isReading" class="reader-layer" role="dialog" aria-modal="true" aria-label="在线预览">
|
||||
<header class="reader-bar">
|
||||
<span class="reader-thumb">
|
||||
<img v-if="thumbUrl" :src="thumbUrl" alt="">
|
||||
<AppIcon v-else name="image" :size="16" />
|
||||
</span>
|
||||
<div class="reader-heading">
|
||||
<p class="reader-title" :title="readingGallery.name">{{ readingGallery.name }}</p>
|
||||
<p class="reader-sub">
|
||||
<span>{{ rangeLabel }}</span>
|
||||
<span v-if="readingGallery.language">{{ readingGallery.language }}</span>
|
||||
<span v-if="readingGallery.fileSize">{{ readingGallery.fileSize }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="reader-tools">
|
||||
<div class="segmented is-dark" role="group" aria-label="缩放模式">
|
||||
<button type="button" :aria-pressed="fitMode === 'height'" @click="fitMode = 'height'">适应高度</button>
|
||||
<button type="button" :aria-pressed="fitMode === 'width'" @click="fitMode = 'width'">适应宽度</button>
|
||||
</div>
|
||||
<a v-if="readingGallery.link" class="icon-button reader-external" title="打开原始页面"
|
||||
aria-label="打开原始页面" :href="readingGallery.link" target="_blank" rel="noopener">
|
||||
<AppIcon name="external" :size="15" />
|
||||
</a>
|
||||
<button type="button" class="icon-button" title="关闭" aria-label="关闭" @click="close">
|
||||
<AppIcon name="close" :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-scrollbar class="reader-scroll" ref="viewer">
|
||||
<div class="reader-stack" :class="`is-${fitMode}`">
|
||||
<figure class="reader-image" v-for="(item, i) in imagesForLoading" :key="item.url + '-' + item.attempt">
|
||||
<el-image :src="item.src"
|
||||
:preview-src-list="links"
|
||||
:initial-index="i"
|
||||
fit="contain"
|
||||
loading="lazy"
|
||||
@show="currentImage = i"
|
||||
@load="imageResult(item)"
|
||||
@error="imageResult(item, true)" />
|
||||
<div v-if="item.failed" class="reader-error" role="alert">
|
||||
图片加载失败
|
||||
<el-button size="small" @click="retryImage(item)">重试</el-button>
|
||||
</div>
|
||||
<figcaption>{{ pageIndex * lengthPerPage + i + 1 }}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar height="75vh" ref="onlineReadingScrollbar">
|
||||
<el-image v-for="(link, i) in links" :src="link" :style="{'width': store.state.imageWidth, 'padding-right': store.state.imagePadding, 'background-color': 'gary'}"
|
||||
:preview-src-list="links" :initial-index="i" @switch="switch_page" @show="set_current_page(i)">
|
||||
</el-image>
|
||||
</el-scrollbar>
|
||||
|
||||
<!-- 十页以下-->
|
||||
<span v-if="max > 1 && max < 9">
|
||||
<el-button @click="jump(index - 1)" :disabled="index === 0">上一页</el-button>
|
||||
<el-button v-for="i in max" @click="jump(i - 1)">
|
||||
{{i}}
|
||||
</el-button>
|
||||
<el-button @click="jump(index + 1)" :disabled="index === max - 1">下一页</el-button>
|
||||
<footer class="reader-foot">
|
||||
<button type="button" class="text-button" :disabled="pageIndex === 0" @click="showPage(pageIndex - 1)">
|
||||
<AppIcon name="chevrons-left" :size="15" />上一组
|
||||
</button>
|
||||
<button type="button" class="text-button" :disabled="currentImage === 0" @click="step(-1)">
|
||||
<AppIcon name="chevron-left" :size="15" />上一张
|
||||
</button>
|
||||
|
||||
<span class="reader-pager">
|
||||
<el-input-number v-model="pageInput"
|
||||
:min="1"
|
||||
:max="pageCount"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="showPage(Number(pageInput) - 1)" />
|
||||
<span class="reader-page-total">/ {{ pageCount }} 组</span>
|
||||
</span>
|
||||
|
||||
<!-- 十页及以上-->
|
||||
<span v-if="max >= 9">
|
||||
<el-button @click="jump(index - 1)" :disabled="index === 0">上一页</el-button>
|
||||
1 <
|
||||
<el-input-number v-model="temp_index" :min="1" :max="max"/>
|
||||
< {{max}}
|
||||
<el-button @click="jump(index + 1)" :disabled="index === max - 1">下一页</el-button>
|
||||
<el-button @click="jump(temp_index - 1)" size="large">跳转</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
<button type="button" class="text-button"
|
||||
:disabled="currentImage >= links.length - 1"
|
||||
@click="step(1)">
|
||||
下一张<AppIcon name="chevron-right" :size="15" />
|
||||
</button>
|
||||
<button type="button" class="text-button"
|
||||
:disabled="pageIndex >= pageCount - 1"
|
||||
@click="showPage(pageIndex + 1)">
|
||||
下一组<AppIcon name="chevrons-right" :size="15" />
|
||||
</button>
|
||||
|
||||
<span class="reader-foot-spacer"></span>
|
||||
<span class="reader-hint">{{ currentImage + 1 }} / {{ links.length }}</span>
|
||||
</footer>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.reader-layer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0f1116;
|
||||
}
|
||||
|
||||
.reader-bar {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: #171a21;
|
||||
border-bottom: 1px solid #262a33;
|
||||
color: #e8eaee;
|
||||
}
|
||||
|
||||
.reader-thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 46px;
|
||||
border-radius: 4px;
|
||||
background: #22262f;
|
||||
color: #6b7280;
|
||||
overflow: hidden;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.reader-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.reader-heading {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reader-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #e8eaee;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.reader-sub {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 14px;
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: #9aa3b2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.reader-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.reader-tools .icon-button {
|
||||
background: #1e222a;
|
||||
border-color: #2f3542;
|
||||
color: #c6ccd6;
|
||||
}
|
||||
|
||||
.reader-tools .icon-button:hover {
|
||||
border-color: #4b5563;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* The segmented control needs a dark variant inside the reader chrome. */
|
||||
.reader-tools .segmented.is-dark {
|
||||
background: #1e222a;
|
||||
border-color: #2f3542;
|
||||
}
|
||||
|
||||
.reader-tools .segmented.is-dark button {
|
||||
color: #9aa3b2;
|
||||
}
|
||||
|
||||
.reader-tools .segmented.is-dark button[aria-pressed="true"] {
|
||||
background: #2f3542;
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.reader-external {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.reader-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.reader-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.reader-image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.reader-image :deep(.el-image) {
|
||||
background: #171a21;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.reader-stack.is-height .reader-image :deep(.el-image) {
|
||||
height: calc(100vh - 168px);
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.reader-stack.is-width .reader-image :deep(.el-image) {
|
||||
width: min(1100px, 100%);
|
||||
}
|
||||
|
||||
.reader-stack.is-width .reader-image :deep(.el-image img) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.reader-error {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.reader-image figcaption {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.reader-foot {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: #171a21;
|
||||
border-top: 1px solid #262a33;
|
||||
}
|
||||
|
||||
.reader-foot .text-button {
|
||||
background: #1e222a;
|
||||
border-color: #2f3542;
|
||||
color: #c6ccd6;
|
||||
}
|
||||
|
||||
.reader-foot .text-button:hover:not(:disabled) {
|
||||
border-color: #4b5563;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reader-foot .text-button:disabled {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.reader-pager {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.reader-pager :deep(.el-input-number) {
|
||||
width: 104px;
|
||||
}
|
||||
|
||||
/* Keep the group picker in the dark palette used by the reader chrome. */
|
||||
.reader-pager :deep(.el-input__wrapper) {
|
||||
background: #1e222a;
|
||||
box-shadow: 0 0 0 1px #2f3542 inset;
|
||||
}
|
||||
|
||||
.reader-pager :deep(.el-input__inner) {
|
||||
color: #e8eaee;
|
||||
}
|
||||
|
||||
.reader-pager :deep(.el-input-number__increase),
|
||||
.reader-pager :deep(.el-input-number__decrease) {
|
||||
background: #262a33;
|
||||
border-color: #2f3542;
|
||||
color: #c6ccd6;
|
||||
}
|
||||
|
||||
.reader-pager :deep(.el-input-number__increase:hover),
|
||||
.reader-pager :deep(.el-input-number__decrease:hover) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.reader-page-total {
|
||||
font-size: 13px;
|
||||
color: #9aa3b2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.reader-foot-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.reader-hint {
|
||||
font-size: 12px;
|
||||
color: #9aa3b2;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,209 @@
|
||||
<script setup>
|
||||
import {computed, ref} from "vue";
|
||||
import axios from "axios";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
import TaskRow from "./TaskRow.vue";
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
import {validateLink} from "../utils/validate.js";
|
||||
import {ehThumbnailUrl} from "../utils/thumbnail.js";
|
||||
|
||||
const mode = ref("ehentai");
|
||||
const localMode = ref("keyword");
|
||||
const localKeyword = ref("");
|
||||
const linkParam = ref("");
|
||||
|
||||
const remoteKeyword = ref("");
|
||||
const remoteGalleries = ref([]);
|
||||
const remotePage = ref({});
|
||||
const remoteLoading = ref(false);
|
||||
const retryingGids = ref(new Set());
|
||||
|
||||
const localResults = computed(() => store.getters.currentTasks || []);
|
||||
const localTotal = computed(() => store.state.searchTask.length);
|
||||
const isSearching = computed(() => store.state.isSearch);
|
||||
|
||||
function runLocalSearch() {
|
||||
if (localKeyword.value.trim() === "") {
|
||||
ElMessage("请输入查询内容");
|
||||
return;
|
||||
}
|
||||
if (localMode.value === "link") store.commit("_searchLocalByLink", localKeyword.value);
|
||||
else store.commit("_searchLocalByKeyword", localKeyword.value);
|
||||
}
|
||||
|
||||
function clearLocalSearch() {
|
||||
store.commit("_searchLocalByKeyword", "");
|
||||
localKeyword.value = "";
|
||||
}
|
||||
|
||||
function queryGalleries(link) {
|
||||
let keyword;
|
||||
if (link != null) {
|
||||
keyword = new URL(link).search.replace("?f_search=", "");
|
||||
} else {
|
||||
keyword = remoteKeyword.value;
|
||||
if (keyword.trim() === "") {
|
||||
ElMessage("请输入搜索关键词");
|
||||
return;
|
||||
}
|
||||
}
|
||||
remoteLoading.value = true;
|
||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + keyword.replace(" ", "+"))
|
||||
.then(res => {
|
||||
if (res.data.result === "success") {
|
||||
remotePage.value = {
|
||||
first: res.data.first,
|
||||
previous: res.data.previous,
|
||||
next: res.data.next,
|
||||
last: res.data.last,
|
||||
};
|
||||
remoteGalleries.value.splice(0);
|
||||
JSON.parse(res.data.data).forEach(g => remoteGalleries.value.push(g));
|
||||
} else {
|
||||
ElMessage({message: res.data.data, type: "error"});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
remoteLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Hand a remote gallery to the shared resolver so its resolution can be picked.
|
||||
function submitRemote(gallery) {
|
||||
store.dispatch("queryGalleryTask", gallery.link);
|
||||
}
|
||||
|
||||
// Preview a remote gallery without submitting it first.
|
||||
function previewRemote(gallery) {
|
||||
store.dispatch("readOnlineGallery", gallery);
|
||||
}
|
||||
|
||||
function parseLink() {
|
||||
let link = linkParam.value;
|
||||
if (!validateLink(link)) {
|
||||
ElMessage("链接错误");
|
||||
return;
|
||||
}
|
||||
if (link.includes("e-hentai")) link = link.replace("e-hentai", "exhentai");
|
||||
store.dispatch("queryGalleryTask", link);
|
||||
}
|
||||
|
||||
function thumbFor(path) {
|
||||
return ehThumbnailUrl(path, store.state.AuthCode);
|
||||
}
|
||||
|
||||
function downloadTask(gallery) {
|
||||
if (gallery.download) window.open(gallery.download);
|
||||
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||
}
|
||||
|
||||
async function retryGallery(gallery) {
|
||||
if (retryingGids.value.has(gallery.gid)) return;
|
||||
retryingGids.value.add(gallery.gid);
|
||||
try {
|
||||
await store.dispatch("retryGallery", gallery.gid);
|
||||
} finally {
|
||||
retryingGids.value.delete(gallery.gid);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="view">
|
||||
<header class="view-bar">
|
||||
<h1 class="view-title">搜索</h1>
|
||||
<span class="view-count" v-if="mode === 'ehentai' && remoteGalleries.length">本页 {{ remoteGalleries.length }} 个结果</span>
|
||||
<span class="view-bar-spacer"></span>
|
||||
</header>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="segmented is-quiet" role="group" aria-label="搜索来源">
|
||||
<button type="button" :aria-pressed="mode === 'ehentai'" @click="mode = 'ehentai'">E站搜索</button>
|
||||
<button type="button" :aria-pressed="mode === 'local'" @click="mode = 'local'">本地任务</button>
|
||||
<button type="button" :aria-pressed="mode === 'link'" @click="mode = 'link'">解析链接</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-scroll">
|
||||
<template v-if="mode === 'ehentai'">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="remoteKeyword"
|
||||
placeholder="在 E站搜索画廊"
|
||||
clearable
|
||||
@keydown.enter="queryGalleries(null)" />
|
||||
<el-button type="primary" :loading="remoteLoading" @click="queryGalleries(null)">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<div class="remote-row" v-for="gallery in remoteGalleries" :key="gallery.link">
|
||||
<span class="task-thumb">
|
||||
<img :src="thumbFor(gallery.thumbnailUrl)" alt="" loading="lazy">
|
||||
</span>
|
||||
<div class="task-body">
|
||||
<p class="task-title" :title="gallery.name">{{ gallery.name }}</p>
|
||||
<p class="task-meta">
|
||||
<span>{{ gallery.page }} 页</span>
|
||||
<span>{{ gallery.type }}</span>
|
||||
<span>{{ (gallery.uploadTime || "").split(" ")[0] }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="task-acts">
|
||||
<button type="button" class="text-button" @click="previewRemote(gallery)">
|
||||
<AppIcon name="eye" :size="15" />在线看
|
||||
</button>
|
||||
<el-button size="small" type="primary" @click="submitRemote(gallery)">提交</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="!remoteGalleries.length && !remoteLoading" class="empty-state">输入关键词开始搜索</p>
|
||||
|
||||
<div class="pager-row" v-if="remoteGalleries.length">
|
||||
<el-button size="small" :disabled="remotePage.first === undefined" @click="queryGalleries(remotePage.first)">首页</el-button>
|
||||
<el-button size="small" :disabled="remotePage.previous === undefined" @click="queryGalleries(remotePage.previous)">上一页</el-button>
|
||||
<el-button size="small" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.next)">下一页</el-button>
|
||||
<el-button size="small" :disabled="remotePage.last === undefined" @click="queryGalleries(remotePage.last)">尾页</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="mode === 'local'">
|
||||
<div class="search-bar">
|
||||
<div class="segmented is-quiet" role="group" aria-label="本地查询方式">
|
||||
<button type="button" :aria-pressed="localMode === 'keyword'" @click="localMode = 'keyword'">关键字</button>
|
||||
<button type="button" :aria-pressed="localMode === 'link'" @click="localMode = 'link'">链接</button>
|
||||
</div>
|
||||
<el-input v-model="localKeyword"
|
||||
:placeholder="localMode === 'keyword' ? '在我的下载里搜索' : '粘贴已下载任务的链接'"
|
||||
clearable
|
||||
@keydown.enter="runLocalSearch" />
|
||||
<el-button type="primary" @click="runLocalSearch">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<template v-if="isSearching">
|
||||
<p class="hint">
|
||||
匹配到 {{ localTotal }} 项
|
||||
<button type="button" class="hint-action" @click="clearLocalSearch">清除</button>
|
||||
</p>
|
||||
<TaskRow v-for="gallery in localResults"
|
||||
:key="gallery.gid"
|
||||
:gallery="gallery"
|
||||
:retrying="retryingGids.has(gallery.gid)"
|
||||
@open="store.commit('_selectGallery', $event)"
|
||||
@download="downloadTask"
|
||||
@retry="retryGallery" />
|
||||
</template>
|
||||
<p v-else class="empty-state">输入关键字或链接在已有任务里查找</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="search-bar">
|
||||
<el-input v-model="linkParam"
|
||||
placeholder="粘贴 e-hentai 画廊链接"
|
||||
clearable
|
||||
@keydown.enter="parseLink" />
|
||||
<el-button type="primary" @click="parseLink">解析</el-button>
|
||||
</div>
|
||||
<p class="hint">解析成功后可直接选分辨率提交下载</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,229 @@
|
||||
<script setup>
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
|
||||
const weekUsed = computed(() => store.state.weekUsed);
|
||||
const isAdmin = computed(() => store.state.isAdmin);
|
||||
const username = computed(() => store.state.username || "—");
|
||||
|
||||
const isDark = ref(document.documentElement.classList.contains("dark"));
|
||||
const followSystem = ref(false);
|
||||
const lengthPerPage = ref(30);
|
||||
|
||||
const category = computed({
|
||||
get: () => store.state.category,
|
||||
set: value => {
|
||||
store.commit("_setCategory", value);
|
||||
localStorage.setItem("category", value);
|
||||
},
|
||||
});
|
||||
const sortType = computed({
|
||||
get: () => store.state.sortType,
|
||||
set: value => {
|
||||
store.commit("_setSortType", value);
|
||||
localStorage.setItem("sortType", value);
|
||||
},
|
||||
});
|
||||
const galleryNameType = computed({
|
||||
get: () => store.state.galleryNameType,
|
||||
set: value => {
|
||||
store.commit("_setGalleryNameType", value);
|
||||
localStorage.setItem("galleryNameType", value);
|
||||
},
|
||||
});
|
||||
|
||||
// Auth code editing stays in a dialog: it is a destructive, infrequent action.
|
||||
const isAlterAuthCode = ref(false);
|
||||
const newAuthCode = ref("");
|
||||
const tempAuthCode = ref("");
|
||||
const realAuthCode = computed(() => store.state.AuthCode);
|
||||
|
||||
function applyTheme() {
|
||||
localStorage.setItem("darkConfig", JSON.stringify({followSystem: followSystem.value}));
|
||||
const dark = followSystem.value
|
||||
? window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
: isDark.value;
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
isDark.value = dark;
|
||||
}
|
||||
|
||||
function toggleDark(value) {
|
||||
followSystem.value = false;
|
||||
isDark.value = value;
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function toggleFollowSystem(value) {
|
||||
followSystem.value = value;
|
||||
applyTheme();
|
||||
}
|
||||
|
||||
function saveLengthPerPage(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 30) {
|
||||
ElMessage("分页页数设置错误,范围 1~30");
|
||||
lengthPerPage.value = store.state.lengthPerPage || 30;
|
||||
return;
|
||||
}
|
||||
store.state.lengthPerPage = parsed;
|
||||
localStorage.setItem("lengthPerPage", String(parsed));
|
||||
}
|
||||
|
||||
function alterAuthCode() {
|
||||
if (newAuthCode.value.trim() === "" || newAuthCode.value !== tempAuthCode.value) {
|
||||
ElMessage("请检查授权码输入是否错误");
|
||||
return;
|
||||
}
|
||||
store.dispatch("alterAuthCode", newAuthCode.value);
|
||||
isAlterAuthCode.value = false;
|
||||
newAuthCode.value = "";
|
||||
tempAuthCode.value = "";
|
||||
}
|
||||
|
||||
function deleteAuthCode() {
|
||||
localStorage.removeItem("auth");
|
||||
ElMessage("删除授权码完成");
|
||||
}
|
||||
|
||||
function reconnect() {
|
||||
store.dispatch("reconnect");
|
||||
}
|
||||
|
||||
function resetUndone() {
|
||||
store.dispatch("resetUndone").then();
|
||||
}
|
||||
|
||||
function refreshUsage() {
|
||||
store.dispatch("loadWeekUsedAmount");
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const darkConfigStr = localStorage.getItem("darkConfig");
|
||||
if (darkConfigStr !== null) {
|
||||
try {
|
||||
followSystem.value = Boolean(JSON.parse(darkConfigStr).followSystem);
|
||||
} catch {
|
||||
followSystem.value = false;
|
||||
}
|
||||
}
|
||||
lengthPerPage.value = Number(store.state.lengthPerPage) || 30;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="view">
|
||||
<header class="view-bar">
|
||||
<h1 class="view-title">设置</h1>
|
||||
<span class="view-count">{{ username }}</span>
|
||||
<span class="view-bar-spacer"></span>
|
||||
</header>
|
||||
|
||||
<div class="settings-scroll">
|
||||
<section class="settings-group">
|
||||
<h2 class="section-title">本周用量</h2>
|
||||
<div class="usage-line">
|
||||
<span class="usage-value">{{ weekUsed.weekUsedAmount ?? "—" }}</span>
|
||||
<span class="usage-note">上次重置 {{ weekUsed.lastResetAmountTime ?? "—" }}</span>
|
||||
<span class="view-bar-spacer"></span>
|
||||
<button type="button" class="text-button" @click="refreshUsage">
|
||||
<AppIcon name="refresh" :size="15" />刷新
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2 class="section-title">外观</h2>
|
||||
<div class="setting-row">
|
||||
<span>夜间模式</span>
|
||||
<el-switch :model-value="isDark" @update:model-value="toggleDark" />
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>跟随系统</span>
|
||||
<el-switch :model-value="followSystem" @update:model-value="toggleFollowSystem" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2 class="section-title">默认视图</h2>
|
||||
<div class="setting-row">
|
||||
<span>分类</span>
|
||||
<el-select v-model="category" style="width: 180px">
|
||||
<el-option label="我的下载" value="myDownload" />
|
||||
<el-option label="我的收藏" value="myCollect" />
|
||||
<el-option label="全部" value="total" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>排序方式</span>
|
||||
<el-select v-model="sortType" style="width: 180px">
|
||||
<el-option label="简洁名字" value="shortName" />
|
||||
<el-option label="名字" value="name" />
|
||||
<el-option label="任务创建时间" value="createTime" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>显示类型</span>
|
||||
<el-select v-model="galleryNameType" style="width: 180px">
|
||||
<el-option label="简洁名字" value="shortName" />
|
||||
<el-option label="名字" value="name" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>在线预览每页图片数量</span>
|
||||
<el-input-number :model-value="lengthPerPage"
|
||||
:min="1"
|
||||
:max="30"
|
||||
:step="1"
|
||||
style="width: 160px"
|
||||
@change="saveLengthPerPage" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2 class="section-title">授权</h2>
|
||||
<div class="setting-row">
|
||||
<span>当前授权码</span>
|
||||
<span class="mono-value">{{ realAuthCode }}</span>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<span>管理授权码</span>
|
||||
<span class="setting-actions">
|
||||
<el-button size="small" @click="isAlterAuthCode = true">修改授权码</el-button>
|
||||
<el-button size="small" @click="deleteAuthCode">删除本地授权码</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group">
|
||||
<h2 class="section-title">系统</h2>
|
||||
<div class="setting-row">
|
||||
<span>下载节点</span>
|
||||
<span class="setting-actions">
|
||||
<el-button size="small" @click="reconnect">重连节点</el-button>
|
||||
<el-button v-if="isAdmin" size="small" type="danger" plain @click="resetUndone">重置任务</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog title="修改授权码" v-model="isAlterAuthCode" width="420px">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="当前授权码">
|
||||
<span class="mono-value">{{ realAuthCode }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="新的授权码">
|
||||
<el-input v-model="newAuthCode" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="再次输入授权码">
|
||||
<el-input v-model="tempAuthCode" show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="isAlterAuthCode = false">取消</el-button>
|
||||
<el-button type="primary" @click="alterAuthCode">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,362 +0,0 @@
|
||||
<template>
|
||||
<div class="side" style="width: 100%">
|
||||
<span v-show="loadComplete">本子历史</span>
|
||||
<div v-show="loadComplete" class="load_complete">
|
||||
<el-table :data="currentTasks"
|
||||
:empty-text="emptyText"
|
||||
:row-key="gallery=>gallery.gid"
|
||||
class="table"
|
||||
@cellMouseEnter="showThumbnail"
|
||||
>
|
||||
|
||||
<el-table-column type="expand">
|
||||
<template #default="props">
|
||||
名字:{{ props.row.name}} <br>
|
||||
链接:<el-link :href="props.row.link">链接</el-link> <br>
|
||||
语言:{{props.row.language}} <br>
|
||||
页数:{{props.row.pages}} <br>
|
||||
文件大小:{{props.row.fileSize}}<br>
|
||||
分辨率:{{props.row.resolution}}<br>
|
||||
任务创建时间:{{props.row.createTimeDisplay}}<br>
|
||||
标签:{{props.row.tag === '' ? '无': props.row.tag}} <br>
|
||||
<span v-show="isLion">
|
||||
downloader:{{props.row.downloader}}
|
||||
</span>
|
||||
|
||||
<el-button @click="deleteGallery(props.row.gid)" :disabled="props.row.status !== '下载完成'">删除</el-button>
|
||||
<el-button @click="editGalleryTag(props.row)">编辑标签</el-button>
|
||||
<el-button @click="updateGallery(props.row.link)" :disabled="props.row.status !== '下载完成'">更新</el-button>
|
||||
<el-button @click="shareGallery({gid:props.row.gid, shortName:props.row.shortName + '.zip'})" v-show="isLion" :disabled="props.row.status !== '下载完成'">分享</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="名字" width="300vw">
|
||||
<template #default="scoped">
|
||||
{{galleryNameType === 'shortName' ? scoped.row.shortName: scoped.row.name}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="300vw">
|
||||
<template #default="scoped">
|
||||
<span>
|
||||
<el-button @click="downloadTask(scoped.row.download)" :disabled="scoped.row.status !== '下载完成' || scoped.row.mode === 2">下载</el-button>
|
||||
<el-button @click="changeGalleryCollect(scoped.row.gid, scoped.row.isCollect)">{{scoped.row.isCollect ? '取消收藏' : '收藏'}}</el-button>
|
||||
<el-button @click="onlineGalleryReader(scoped.row)" :disabled="scoped.row.status !== '下载完成' || scoped.row.mode === 1">在线看</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="进度" width="80">
|
||||
<template #default="scoped">
|
||||
{{ scoped.row.progress }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-row class="pageChanger">
|
||||
<el-col>
|
||||
<el-select v-model="category" @change="changeCategory">
|
||||
<template #prefix>
|
||||
分类
|
||||
</template>
|
||||
<el-option value="myCollect" label="我的收藏"/>
|
||||
<el-option value="myDownload" label="我的下载"/>
|
||||
<el-option value="total" label="全部"/>
|
||||
</el-select>
|
||||
<el-select v-model="sortType" @change="changeSortType" style="width: 170px">
|
||||
<template #prefix>
|
||||
排序
|
||||
</template>
|
||||
<el-option value="name" label="名字"/>
|
||||
<el-option value="shortName" label="简洁名字"/>
|
||||
<el-option value="createTime" label="任务创建时间"/>
|
||||
</el-select>
|
||||
<el-select v-model="galleryNameType" @change="changeGalleryNameType">
|
||||
<template #prefix>
|
||||
显示
|
||||
</template>
|
||||
<el-option value="name" label="名字"/>
|
||||
<el-option value="shortName" label="简洁名字"/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<el-button @click="toMin">{{min}}</el-button>
|
||||
<el-button @click="previous">-</el-button>
|
||||
<el-input v-model="targetPage"
|
||||
@change="changePage"
|
||||
v-show="isEditingPage"
|
||||
@blur="reverseEditMode"
|
||||
class="page"
|
||||
ref="inputNode"></el-input>
|
||||
<span @click="reverseEditMode" v-show="!isEditingPage" class="page">{{page}}</span>
|
||||
<el-button @click="next">+</el-button>
|
||||
<el-button @click="toMax">{{max}}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="isEditingTag" title="编辑本子标签">
|
||||
<el-form>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
标签:
|
||||
</template>
|
||||
<template #default>
|
||||
<el-tag v-for="tid in galleryForTag.tags" closable @close="disMark(galleryForTag.gid, tid)">
|
||||
{{tags.get(tid).tag}}
|
||||
</el-tag>
|
||||
<el-autocomplete v-model="newTag" :fetch-suggestions="querySimilarTag" @select="handleTagSelect" ref="tagInput"/>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
<OnlineReader :current-gallery="currentGallery" :isOnlineReading="isOnlineReading" @close="isOnlineReading = false"/>
|
||||
|
||||
<span v-show="!loadComplete" class="side">请输入授权码后再查看</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import store from "../store";
|
||||
import {computed, ref} from "vue";
|
||||
import axios from "axios";
|
||||
import {ElMessage} from "element-plus";
|
||||
import OnlineReader from "./OnlineReader.vue";
|
||||
|
||||
let link = "https://downloader.lionwebsite.xyz/GalleryManage/"
|
||||
|
||||
//输入
|
||||
let inputNode = ref(null)
|
||||
//是否正在编辑页数
|
||||
let isEditingPage = ref(false)
|
||||
//是否正在编辑标签
|
||||
let isEditingTag = ref(false)
|
||||
//是否预览本子
|
||||
let isOnlineReading = ref(false)
|
||||
|
||||
//临时变量
|
||||
let galleryForTag = ref({})
|
||||
|
||||
let category = ref("myDownload") //myDownload myCollect total
|
||||
let galleryNameType = ref("shortName") // shortName name
|
||||
let sortType = ref("shortName") // shortName name createTime
|
||||
let targetPage = ref(1) // 当前页数
|
||||
|
||||
let onlineReadingScrollbar = ref(null)
|
||||
|
||||
//防抖计时器
|
||||
let debounceTimer = 0
|
||||
//是否加载完成
|
||||
let loadComplete = computed(() => {
|
||||
return store.state.loadComplete
|
||||
})
|
||||
|
||||
let currentTasks = computed(() => {
|
||||
return store.getters.currentTasks ? store.getters.currentTasks: null
|
||||
})
|
||||
|
||||
let min = computed(() => {
|
||||
return store.getters.min
|
||||
})
|
||||
let max = computed(() => {
|
||||
if(targetPage.value > store.getters.max)
|
||||
store.commit("_changePage", store.getters.max)
|
||||
return store.getters.max
|
||||
})
|
||||
let page = computed(() => {
|
||||
targetPage.value = store.state.page
|
||||
return store.state.page
|
||||
})
|
||||
let isLion = computed(() => {
|
||||
return store.state.userId === 3
|
||||
})
|
||||
|
||||
let emptyText = computed(() => {
|
||||
let action = category.value === 'myDownload' ? '下载': '收藏'
|
||||
return '您未' + action + "本子"
|
||||
})
|
||||
|
||||
//在线预览相关
|
||||
let currentGallery = ref({name:"name"})
|
||||
let imageWidth = computed(() => {
|
||||
return store.state.imageWidth
|
||||
})
|
||||
let imagePadding = computed(() => {
|
||||
return store.state.imagePadding
|
||||
})
|
||||
|
||||
//标签
|
||||
let tags = computed(() => {
|
||||
return store.state.tags
|
||||
})
|
||||
let newTag = ref("")
|
||||
|
||||
//翻页
|
||||
function next() {
|
||||
if(targetPage.value < max.value) {
|
||||
targetPage.value++
|
||||
store.commit("_changePage", targetPage.value)
|
||||
}
|
||||
}
|
||||
function previous() {
|
||||
if(targetPage.value > min.value) {
|
||||
targetPage.value--
|
||||
store.commit("_changePage", targetPage.value)
|
||||
}
|
||||
}
|
||||
function toMax() {
|
||||
store.commit("_changePage", max.value)
|
||||
targetPage.value = max.value
|
||||
}
|
||||
function toMin(){
|
||||
store.commit("_changePage", min.value)
|
||||
targetPage.value = min.value
|
||||
}
|
||||
function changePage(){
|
||||
if(targetPage.value >= min.value && targetPage.value <= max.value)
|
||||
store.commit("_changePage", targetPage.value)
|
||||
}
|
||||
function reverseEditMode(){
|
||||
isEditingPage.value = !isEditingPage.value
|
||||
if(isEditingPage.value){
|
||||
inputNode.value.focus()
|
||||
}
|
||||
targetPage.value = page.value
|
||||
}
|
||||
|
||||
//改变展示类型
|
||||
function changeCategory(){
|
||||
store.commit("_setCategory", category.value)
|
||||
}
|
||||
function changeGalleryNameType(){
|
||||
store.commit("_setShowNameType", galleryNameType.value)
|
||||
}
|
||||
function changeSortType(){
|
||||
store.commit("_setSortType", sortType.value)
|
||||
}
|
||||
|
||||
//收藏,编辑标签,提交编辑
|
||||
function changeGalleryCollect(gid, isCollect){
|
||||
if(isCollect)
|
||||
store.dispatch("disCollectGallery", gid)
|
||||
else
|
||||
store.dispatch("collectGallery", gid)
|
||||
}
|
||||
function editGalleryTag(gallery){
|
||||
galleryForTag.value = gallery
|
||||
isEditingTag.value = true
|
||||
}
|
||||
let tagInput = ref()
|
||||
|
||||
function querySimilarTag(keyWord, cb){
|
||||
let result = []
|
||||
let hit = false //用于检测是否有重复标签
|
||||
let skip //用于过滤已经有了的标签
|
||||
for(const [key, tag] of store.state.tags){
|
||||
skip = false
|
||||
for(let id of galleryForTag.value.tags)
|
||||
if(id === key) {
|
||||
if(!hit && tag.tag === keyWord) //是否命中标签
|
||||
hit = true
|
||||
skip = true
|
||||
break
|
||||
}else{
|
||||
console.log("id:", id, key)
|
||||
}
|
||||
if(skip)
|
||||
continue
|
||||
if(tag.tag.includes(keyWord))
|
||||
result.push({value: tag.tag, tag:tag})
|
||||
}
|
||||
|
||||
if(keyWord.trim() !== '' && !keyWord.includes("#") && !hit && !keyWord.includes("?") && result.length === 0)
|
||||
result.push({value: '新建 #' + keyWord + ' 标签?', tag:{tag:keyWord}})
|
||||
cb(result)
|
||||
}
|
||||
|
||||
function handleTagSelect(data){
|
||||
if(data.value.includes('#')) {
|
||||
if(data.tag.tag.includes("?")) {
|
||||
ElMessage("非法字符?")
|
||||
return
|
||||
}
|
||||
else
|
||||
store.dispatch("createTagAndMark", {tag: data.tag.tag.replace('#', ''), gid: galleryForTag.value.gid})
|
||||
}
|
||||
else
|
||||
store.dispatch("mark", {gid:galleryForTag.value.gid, tid:data.tag.id})
|
||||
newTag.value = ""
|
||||
tagInput.value.blur()
|
||||
}
|
||||
|
||||
function mark(gid, tid){
|
||||
store.dispatch("mark", {gid, tid})
|
||||
}
|
||||
|
||||
function disMark(gid, tid){
|
||||
store.dispatch("disMark", {gid, tid})
|
||||
}
|
||||
|
||||
//下载,删除,在线看
|
||||
function downloadTask(link){
|
||||
window.open(link)
|
||||
}
|
||||
function updateGallery(link){
|
||||
store.dispatch("updateGallery", link)
|
||||
}
|
||||
function deleteGallery(gid){
|
||||
store.dispatch("deleteGallery", gid)
|
||||
}
|
||||
function onlineGalleryReader(gallery){
|
||||
currentGallery.value = gallery
|
||||
isOnlineReading.value = true;
|
||||
}
|
||||
function shareGallery(data){
|
||||
const {gid, shortName} = data
|
||||
let link
|
||||
axios.post("https://downloader.lionwebsite.xyz/GalleryManage/share?userId=3&expireHour=3&gid=" + gid).then((res) => {
|
||||
if(res.data.result === "success"){
|
||||
let data = JSON.parse(res.data.data)
|
||||
link = 'https://lionwebsite.xyz/GetFile/{0}?ShareCode={1}'.replace('{0}', encodeURIComponent(shortName)).replace('{1}', data.shareCode)
|
||||
ElMessage({dangerouslyUseHTMLString: true,
|
||||
message: "<span>分享成功, 过期时间:" + data.expireTime + "</span><br><a href=" + link + ">链接</a>",
|
||||
duration: 0,
|
||||
'show-close': true
|
||||
})
|
||||
navigator.clipboard.writeText(link)
|
||||
}
|
||||
else
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
}
|
||||
|
||||
//显示缩略图
|
||||
function showThumbnail(gallery){
|
||||
if(gallery.status === "下载完成") {
|
||||
clearTimeout(debounceTimer)
|
||||
debounceTimer = setTimeout(() => {
|
||||
store.commit("_changeThumbnailGallery", gallery)
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.side{
|
||||
text-align: center;
|
||||
height: 300px;
|
||||
}
|
||||
span{
|
||||
display: block;
|
||||
}
|
||||
.pageChanger{
|
||||
margin-top: 5vh;
|
||||
}
|
||||
.el-select{
|
||||
width: 135px;
|
||||
}
|
||||
.page{
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import {computed} from "vue";
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
gallery: {type: Object, required: true},
|
||||
nameType: {type: String, default: "shortName"},
|
||||
selected: {type: Boolean, default: false},
|
||||
retrying: {type: Boolean, default: false},
|
||||
});
|
||||
const emit = defineEmits(["open", "download", "retry", "collect"]);
|
||||
|
||||
const title = computed(() =>
|
||||
props.nameType === "shortName" ? props.gallery.shortName : props.gallery.name);
|
||||
|
||||
const statusClass = computed(() => {
|
||||
const status = props.gallery.status;
|
||||
if (status === "下载完成") return "is-done";
|
||||
if (status === "下载中" || status === "压缩中") return "is-running";
|
||||
return "is-waiting";
|
||||
});
|
||||
|
||||
const percent = computed(() => {
|
||||
const task = props.gallery;
|
||||
if (task.status !== "下载中" || !task.pages) return null;
|
||||
const done = Number(task.proceeding) || 0;
|
||||
return Math.min(100, Math.max(0, Math.round((done / task.pages) * 100)));
|
||||
});
|
||||
|
||||
function open() {
|
||||
emit("open", props.gallery);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article class="task-row" :class="{'is-selected': selected}" @click="open">
|
||||
<span class="task-thumb">
|
||||
<img v-if="gallery.thumb_link" :src="gallery.thumb_link" alt="" loading="lazy">
|
||||
<AppIcon v-else name="image" :size="16" />
|
||||
</span>
|
||||
|
||||
<div class="task-body">
|
||||
<p class="task-title">{{ title }}</p>
|
||||
<p class="task-meta">
|
||||
<span>{{ gallery.pages }} 页</span>
|
||||
<span>{{ gallery.language }}</span>
|
||||
<span>{{ gallery.fileSize }}</span>
|
||||
</p>
|
||||
<div v-if="percent !== null" class="task-progress">
|
||||
<el-progress :percentage="percent" :stroke-width="4" :show-text="false" />
|
||||
<span class="progress-value">{{ percent }}% · {{ gallery.proceeding }}/{{ gallery.pages }} 页</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-side">
|
||||
<span class="status-pill" :class="statusClass">{{ gallery.status }}</span>
|
||||
<div class="task-acts" @click.stop>
|
||||
<button type="button"
|
||||
class="icon-button"
|
||||
title="下载文件"
|
||||
aria-label="下载文件"
|
||||
:disabled="gallery.status !== '下载完成'"
|
||||
@click="emit('download', gallery)">
|
||||
<AppIcon name="download" :size="15" />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="icon-button"
|
||||
:class="{'is-on': gallery.isCollect}"
|
||||
:title="gallery.isCollect ? '取消收藏' : '收藏'"
|
||||
:aria-label="gallery.isCollect ? '取消收藏' : '收藏'"
|
||||
@click="emit('collect', gallery)">
|
||||
<AppIcon name="star" :size="15" />
|
||||
</button>
|
||||
<button v-if="gallery.status !== '下载完成'"
|
||||
type="button"
|
||||
class="icon-button"
|
||||
title="重试任务"
|
||||
aria-label="重试任务"
|
||||
:disabled="retrying"
|
||||
@click="emit('retry', gallery)">
|
||||
<AppIcon name="refresh" :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
@@ -0,0 +1,224 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
import TaskRow from "./TaskRow.vue";
|
||||
import AppIcon from "./AppIcon.vue";
|
||||
|
||||
const props = defineProps({
|
||||
selectedGid: {type: [Number, String], default: null},
|
||||
});
|
||||
const emit = defineEmits(["open"]);
|
||||
|
||||
const retryingGids = ref(new Set());
|
||||
const refreshing = ref(false);
|
||||
const isEditingPage = ref(false);
|
||||
const inputNode = ref(null);
|
||||
|
||||
const currentTasks = computed(() => store.getters.currentTasks || []);
|
||||
const total = computed(() => (store.getters.filteredTasks || []).length);
|
||||
const isSearching = computed(() => store.state.isSearch);
|
||||
|
||||
const category = computed({
|
||||
get: () => store.state.category,
|
||||
set: value => store.commit("_setCategory", value),
|
||||
});
|
||||
const sortType = computed({
|
||||
get: () => store.state.sortType,
|
||||
set: value => store.commit("_setSortType", value),
|
||||
});
|
||||
const galleryNameType = computed({
|
||||
get: () => store.state.galleryNameType,
|
||||
set: value => store.commit("_setGalleryNameType", value),
|
||||
});
|
||||
|
||||
const isAdmin = computed(() => store.state.isAdmin);
|
||||
const downloaderFilter = computed({
|
||||
get: () => store.state.downloaderFilter,
|
||||
set: value => store.commit("_setDownloaderFilter", value),
|
||||
});
|
||||
|
||||
// Options come from the current category, so the dropdown can never offer a
|
||||
// downloader that would filter the visible list down to nothing.
|
||||
const downloaderOptions = computed(() => {
|
||||
const seen = new Map();
|
||||
;(store.state.currentTasks || []).forEach(task => {
|
||||
if (task.downloaderName && !seen.has(task.downloader))
|
||||
seen.set(task.downloader, task.downloaderName);
|
||||
});
|
||||
return [...seen].map(([id, name]) => ({id, name}));
|
||||
});
|
||||
|
||||
const isConnected = computed(() => store.state.connectionStatus === "connected");
|
||||
const connectionLabel = computed(() => ({
|
||||
connected: "进度已连接",
|
||||
connecting: "正在连接进度",
|
||||
reconnecting: "连接中断,正在重连",
|
||||
disconnected: "进度连接已断开",
|
||||
})[store.state.connectionStatus]);
|
||||
|
||||
const emptyText = computed(() => {
|
||||
if (isSearching.value) return "没有匹配的任务";
|
||||
if (category.value === "myDownload") return "您未下载过";
|
||||
if (category.value === "myCollect") return "您未收藏过";
|
||||
return "暂无任务";
|
||||
});
|
||||
|
||||
const min = computed(() => store.getters.min);
|
||||
const max = computed(() => store.getters.max);
|
||||
const page = computed(() => store.state.page);
|
||||
const targetPage = ref(1);
|
||||
|
||||
watch(page, value => {
|
||||
targetPage.value = value;
|
||||
}, {immediate: true});
|
||||
|
||||
function goto(value) {
|
||||
if (value < min.value || value > max.value) return;
|
||||
store.commit("_changePage", value);
|
||||
}
|
||||
|
||||
function reverseEditMode() {
|
||||
isEditingPage.value = !isEditingPage.value;
|
||||
if (isEditingPage.value) inputNode.value?.focus();
|
||||
targetPage.value = page.value;
|
||||
}
|
||||
|
||||
function downloadTask(gallery) {
|
||||
if (gallery.download) window.open(gallery.download);
|
||||
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||
}
|
||||
|
||||
function toggleCollect(gallery) {
|
||||
if (gallery.isCollect) store.dispatch("disCollectGallery", gallery.gid);
|
||||
else store.dispatch("collectGallery", gallery.gid);
|
||||
}
|
||||
|
||||
async function retryGallery(gallery) {
|
||||
if (retryingGids.value.has(gallery.gid)) return;
|
||||
retryingGids.value.add(gallery.gid);
|
||||
try {
|
||||
await store.dispatch("retryGallery", gallery.gid);
|
||||
} finally {
|
||||
retryingGids.value.delete(gallery.gid);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
if (refreshing.value) return;
|
||||
refreshing.value = true;
|
||||
try {
|
||||
await store.dispatch("updateGalleryTasks");
|
||||
} finally {
|
||||
refreshing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearSearch() {
|
||||
store.commit("_searchLocalByKeyword", "");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="view">
|
||||
<header class="view-bar">
|
||||
<h1 class="view-title">任务</h1>
|
||||
<span class="view-count">{{ total }} 项</span>
|
||||
<span class="connection" :class="{'is-connected': isConnected}" role="status" :title="connectionLabel">
|
||||
<i class="connection-dot"></i>{{ connectionLabel }}
|
||||
</span>
|
||||
<span class="view-bar-spacer"></span>
|
||||
<button type="button"
|
||||
class="text-button"
|
||||
:disabled="refreshing"
|
||||
@click="refreshTasks">
|
||||
<AppIcon name="refresh" :size="15" />刷新
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="filter-bar">
|
||||
<div class="filter">
|
||||
<span class="filter-label">分类</span>
|
||||
<el-select v-model="category" class="filter-select filter-select-category" size="small">
|
||||
<el-option value="myDownload" label="我的下载" />
|
||||
<el-option value="myCollect" label="我的收藏" />
|
||||
<el-option value="total" label="全部" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<span class="filter-label">排序</span>
|
||||
<el-select v-model="sortType" class="filter-select filter-select-sort" size="small">
|
||||
<el-option value="shortName" label="简洁名字" />
|
||||
<el-option value="name" label="名字" />
|
||||
<el-option value="createTime" label="任务创建时间" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<span class="filter-label">显示</span>
|
||||
<el-select v-model="galleryNameType" class="filter-select filter-select-name" size="small">
|
||||
<el-option value="shortName" label="简洁名字" />
|
||||
<el-option value="name" label="名字" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div v-if="isAdmin" class="filter">
|
||||
<span class="filter-label">下载人</span>
|
||||
<el-select v-model="downloaderFilter"
|
||||
class="filter-select filter-select-downloader"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="全部">
|
||||
<el-option v-for="item in downloaderOptions"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<button v-if="isSearching" type="button" class="chip-button" @click="clearSearch">
|
||||
清除搜索
|
||||
</button>
|
||||
|
||||
<span class="view-bar-spacer"></span>
|
||||
|
||||
<div class="pager">
|
||||
<button type="button" class="icon-button" title="第一页" :disabled="page <= min" @click="goto(min)">
|
||||
<AppIcon name="chevrons-left" :size="15" />
|
||||
</button>
|
||||
<button type="button" class="icon-button" title="上一页" :disabled="page <= min" @click="goto(page - 1)">
|
||||
<AppIcon name="chevron-left" :size="15" />
|
||||
</button>
|
||||
<span class="pager-readout">
|
||||
<el-input v-if="isEditingPage"
|
||||
v-model="targetPage"
|
||||
class="page-input"
|
||||
size="small"
|
||||
ref="inputNode"
|
||||
@change="goto(Number(targetPage))"
|
||||
@blur="reverseEditMode" />
|
||||
<button v-else type="button" class="page-current" @click="reverseEditMode">{{ page }}</button>
|
||||
<span class="page-total">/ {{ max }}</span>
|
||||
</span>
|
||||
<button type="button" class="icon-button" title="下一页" :disabled="page >= max" @click="goto(page + 1)">
|
||||
<AppIcon name="chevron-right" :size="15" />
|
||||
</button>
|
||||
<button type="button" class="icon-button" title="最后一页" :disabled="page >= max" @click="goto(max)">
|
||||
<AppIcon name="chevrons-right" :size="15" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-scroll">
|
||||
<p v-if="currentTasks.length === 0" class="empty-state">{{ emptyText }}</p>
|
||||
<TaskRow v-for="gallery in currentTasks"
|
||||
:key="gallery.gid"
|
||||
:gallery="gallery"
|
||||
:name-type="galleryNameType"
|
||||
:selected="String(gallery.gid) === String(selectedGid)"
|
||||
:retrying="retryingGids.has(gallery.gid)"
|
||||
@open="emit('open', $event)"
|
||||
@download="downloadTask"
|
||||
@collect="toggleCollect"
|
||||
@retry="retryGallery" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,6 +2,8 @@ import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import 'element-plus/dist/index.css'
|
||||
import element from "element-plus"
|
||||
import "./reset.css"
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import "./styles/theme.css"
|
||||
|
||||
createApp(App).use(element).mount('#app')
|
||||
|
||||
+246
-388
@@ -2,59 +2,63 @@ import vuex from "vuex"
|
||||
import axios from "axios"
|
||||
import {ElMessage} from "element-plus"
|
||||
import qs from "qs"
|
||||
import {createTaskSocket} from "../utils/taskSocket.js"
|
||||
let taskSocket
|
||||
let taskRefreshVersion = 0
|
||||
const BaseUrl = "https://downloader.lionwebsite.xyz/"
|
||||
const GalleryManageUrl = BaseUrl + "GalleryManage"
|
||||
|
||||
const actions = {
|
||||
updateGalleryTasks(context, type){
|
||||
axios.get(GalleryManageUrl, {
|
||||
params:{
|
||||
type,
|
||||
AuthCode: state.AuthCode
|
||||
}
|
||||
}).then((res) => {
|
||||
if(res.data.result === "success") {
|
||||
let tasks = JSON.parse(res.data.data)
|
||||
if (type === "all" && state.galleryRefreshTimer === 0) { //判断是否有未下载完成的本子以及定时更新是否开启
|
||||
for (let i = tasks.length - 1; i > tasks.length - 11; i--) //从后往前遍历十个本子,查看是否有未下载完成的本子
|
||||
if (tasks[i].status !== "下载完成") {
|
||||
state.galleryRefreshTimer = setInterval(() => {
|
||||
context.dispatch("updateGalleryTasks", "undone").then()
|
||||
}, 20000)
|
||||
break
|
||||
}
|
||||
}
|
||||
context.commit("_updateGalleryTasks", {tasks, type})
|
||||
// Axios 全局错误处理
|
||||
axios.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
ElMessage({message: "网络请求失败: " + (error.message || ""), type: "error"})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
else if(type === 'undone') {
|
||||
context.dispatch("updateGalleryTasks", "all").then()
|
||||
clearInterval(state.galleryRefreshTimer)
|
||||
state.galleryRefreshTimer = 0
|
||||
const actions = {
|
||||
reconnect(context) {
|
||||
axios.post(GalleryManageUrl + "/reconnect", null, {
|
||||
params: {
|
||||
AuthCode: context.state.AuthCode
|
||||
}
|
||||
}).then(res => {
|
||||
if (res.data.result === "success") {
|
||||
ElMessage("重连成功")
|
||||
} else {
|
||||
ElMessage("重连失败")
|
||||
}
|
||||
})
|
||||
},
|
||||
updateGalleryTasks(context){
|
||||
const version = ++taskRefreshVersion
|
||||
return axios.get(GalleryManageUrl, {
|
||||
params:{
|
||||
AuthCode: context.state.AuthCode,
|
||||
type: 'all'
|
||||
}
|
||||
}).then((res) => {
|
||||
if(res.data.result === "success" && version === taskRefreshVersion)
|
||||
context.commit("_updateGalleryTasks", JSON.parse(res.data.data))
|
||||
})
|
||||
},
|
||||
postGalleryTask(context, data){
|
||||
axios.post(GalleryManageUrl + '?' + qs.stringify({
|
||||
AuthCode: state.AuthCode,
|
||||
return axios.post(GalleryManageUrl + '?' + qs.stringify({
|
||||
AuthCode: context.state.AuthCode,
|
||||
...data
|
||||
}, {indices:false})).then((res) => {
|
||||
if(res.data.result === "success") {
|
||||
ElMessage("提交成功")
|
||||
context.commit("_setChosenGallery", {gallery: false,
|
||||
resolution:data.targetResolution,
|
||||
tags: data.tags})
|
||||
if(state.galleryRefreshTimer === 0)
|
||||
state.galleryRefreshTimer = setInterval(() => {
|
||||
context.dispatch("updateGalleryTasks", "undone").then()
|
||||
}, 20000)
|
||||
context.commit("_setChosenGallery", {gallery: false})
|
||||
}
|
||||
else{
|
||||
else
|
||||
if(res.data.data)
|
||||
ElMessage(res.data.data)
|
||||
else
|
||||
ElMessage("提交失败")
|
||||
}
|
||||
// A node timeout can still leave a persisted task; always reload its actual state.
|
||||
return Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")])
|
||||
})
|
||||
},
|
||||
queryGalleryTask(context, link){
|
||||
@@ -62,143 +66,78 @@ const actions = {
|
||||
params:{
|
||||
param: link,
|
||||
type: 'link',
|
||||
AuthCode: state.AuthCode
|
||||
AuthCode: context.state.AuthCode
|
||||
}
|
||||
}).then((res) => {
|
||||
if(res.data.result === 'success')
|
||||
context.commit("_setChosenGallery", {gallery: JSON.parse(res.data.data)})
|
||||
|
||||
if(res.data.result === 'success'){
|
||||
let gallery = JSON.parse(res.data.data)
|
||||
if(gallery.status === "下载完成")
|
||||
gallery.download = buildGalleryDownloadUrl(gallery.gid, context.state.AuthCode)
|
||||
context.commit("_setChosenGallery", {gallery})
|
||||
}
|
||||
else
|
||||
ElMessage("查询失败")
|
||||
})
|
||||
},
|
||||
updateGallery(context, link){
|
||||
axios.post(GalleryManageUrl + "/update", qs.stringify({AuthCode: state.AuthCode, link}))
|
||||
.then((res) => {
|
||||
if(res.data.result === 'success' && state.galleryRefreshTimer === 0){
|
||||
setTimeout(() => {
|
||||
context.dispatch("updateGalleryTasks", "all").then()
|
||||
}, 5000)
|
||||
state.galleryRefreshTimer = setInterval(() => {
|
||||
context.dispatch("updateGalleryTasks", "undone").then()
|
||||
}, 20000)
|
||||
}
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
},
|
||||
validate(context, AuthCode){
|
||||
axios.post(BaseUrl + "validate?AuthCode=" + AuthCode).then((res)=>{
|
||||
if(res.data.result === 'success'){
|
||||
let data = JSON.parse(res.data.data);
|
||||
if(!data.isAvailable){
|
||||
if(!data.isAvailable)
|
||||
ElMessage({duration:0, message:"节点挂了,不能下也不能看,找狮子处理", type: "error"})
|
||||
}
|
||||
context.commit("_authed", {AuthCode, ...data})
|
||||
//初始化
|
||||
context.dispatch("loadTags", true).then()
|
||||
context.dispatch("loadWeekUsedAmount").then()
|
||||
context.dispatch("updateGalleryTasks", "all").then(() => confirmCurrentTask(context.state))
|
||||
|
||||
//写死图片合适宽度
|
||||
state.imageWidth = 27 + 'vw'
|
||||
state.imagePadding = 1 + 'vw'
|
||||
context.dispatch("updateGalleryTasks").then(() => confirmCurrentTask(context.state))
|
||||
context.dispatch("initWebsocket").then()
|
||||
}
|
||||
else
|
||||
else {
|
||||
context.dispatch("disconnectWebsocket")
|
||||
context.commit("_unAuthed")
|
||||
}
|
||||
})
|
||||
},
|
||||
initWebsocket(context){
|
||||
taskSocket?.stop()
|
||||
taskSocket = createTaskSocket({
|
||||
url: "wss://downloader.lionwebsite.xyz/ws/",
|
||||
onState: status => context.commit("_setConnectionStatus", status),
|
||||
onOpen: () => {
|
||||
// Catch up on every connection, including events missed during an outage.
|
||||
Promise.all([context.dispatch("updateGalleryTasks"), context.dispatch("loadWeekUsedAmount")]).catch(() => {})
|
||||
},
|
||||
onMessage: event => {
|
||||
let message
|
||||
try { message = JSON.parse(event.data) }
|
||||
catch { return }
|
||||
if(message.type === "updateTasks" && Array.isArray(message.data))
|
||||
context.commit("_updateGalleryTaskProceeding", message.data)
|
||||
else if(message.type === "fullUpdate")
|
||||
context.dispatch("updateGalleryTasks").catch(() => {})
|
||||
}
|
||||
})
|
||||
taskSocket.start()
|
||||
},
|
||||
disconnectWebsocket(context){
|
||||
taskSocket?.stop()
|
||||
taskSocket = undefined
|
||||
context.commit("_setConnectionStatus", "disconnected")
|
||||
},
|
||||
loadWeekUsedAmount(context){
|
||||
axios.get(GalleryManageUrl + "/weekUsedAmount", {
|
||||
return axios.get(GalleryManageUrl + "/weekUsedAmount", {
|
||||
params: {
|
||||
AuthCode: state.AuthCode
|
||||
AuthCode: context.state.AuthCode
|
||||
}
|
||||
}).then((res) => {
|
||||
if(res.data.result === "success"){
|
||||
context.state.weekUsed = JSON.parse(res.data.data)
|
||||
ElMessage("查询用量成功")
|
||||
}else
|
||||
ElMessage("查询用量失败")
|
||||
})
|
||||
},
|
||||
loadTags(context, isShowTip){
|
||||
axios.get(GalleryManageUrl + "/allTag", {
|
||||
params: {
|
||||
AuthCode: state.AuthCode
|
||||
}
|
||||
}).then((res) => {
|
||||
if(res.data.result === "success"){
|
||||
context.commit("_loadTags", JSON.parse(res.data.data))
|
||||
if(isShowTip)
|
||||
ElMessage("加载标签成功")
|
||||
}else
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
},
|
||||
postTag(context, tag){
|
||||
axios.post(GalleryManageUrl + "/tag?" + qs.stringify({
|
||||
tag,
|
||||
AuthCode: state.AuthCode
|
||||
})
|
||||
).then((res) => {
|
||||
if (res.data.result === 'success') {
|
||||
ElMessage('创建标签成功')
|
||||
context.commit("_postTag", {tag:tag, id:parseInt(res.data.tid), usage: 0})
|
||||
}
|
||||
else
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
},
|
||||
deleteTag(context, tid){
|
||||
axios.delete(GalleryManageUrl + "/tag?" + qs.stringify({
|
||||
tid,
|
||||
AuthCode: state.AuthCode
|
||||
})).then((res) => {
|
||||
if (res.data.result === 'success') {
|
||||
ElMessage('删除标签成功')
|
||||
context.commit("_deleteTag", tid)
|
||||
}
|
||||
else
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
},
|
||||
mark(context, data){
|
||||
data.AuthCode = context.state.AuthCode
|
||||
axios.post(GalleryManageUrl + "/mark?" + qs.stringify(data)).then((res) => {
|
||||
if(res.data.result === 'success'){
|
||||
ElMessage("标记成功")
|
||||
context.commit("_mark", data)
|
||||
}else{
|
||||
ElMessage(res.data.data)
|
||||
}
|
||||
})
|
||||
},
|
||||
disMark(context, data){
|
||||
data.AuthCode = context.state.AuthCode
|
||||
axios.post(GalleryManageUrl + "/disMark?" + qs.stringify(data)).then((res) => {
|
||||
if(res.data.result === 'success'){
|
||||
ElMessage("取消标记成功")
|
||||
context.commit("_disMark", data)
|
||||
}else {
|
||||
ElMessage(res.data.data)
|
||||
}
|
||||
})
|
||||
},
|
||||
createTagAndMark(context, data){
|
||||
data.AuthCode = context.state.AuthCode
|
||||
axios.post(GalleryManageUrl + '/tagAndMark?' + qs.stringify(data)).then((res) => {
|
||||
if(res.data.result === 'success') {
|
||||
ElMessage("创建标签并标记成功")
|
||||
context.commit("_mark", {tid:parseInt(res.data.tid), gid: data.gid, usage: 1, tag:data.tag})
|
||||
|
||||
}else {
|
||||
ElMessage(res.data.data)
|
||||
}
|
||||
})
|
||||
},
|
||||
collectGallery(context, gid){
|
||||
axios.post(GalleryManageUrl + "/collect?" +qs.stringify( {
|
||||
gid,
|
||||
AuthCode:state.AuthCode
|
||||
gid, AuthCode:context.state.AuthCode
|
||||
})).then((res) => {
|
||||
ElMessage(res.data.data)
|
||||
if(res.data.result === 'success')
|
||||
@@ -207,8 +146,7 @@ const actions = {
|
||||
},
|
||||
disCollectGallery(context, gid){
|
||||
axios.post(GalleryManageUrl + "/disCollect?" + qs.stringify({
|
||||
gid,
|
||||
AuthCode:state.AuthCode
|
||||
gid, AuthCode:context.state.AuthCode
|
||||
})).then((res) => {
|
||||
ElMessage(res.data.data)
|
||||
if(res.data.result === 'success')
|
||||
@@ -216,10 +154,10 @@ const actions = {
|
||||
})
|
||||
},
|
||||
deleteGallery(context, gid){
|
||||
axios.delete(GalleryManageUrl, {
|
||||
// Returned so callers can clear a selection once the task is really gone.
|
||||
return axios.delete(GalleryManageUrl, {
|
||||
params:{
|
||||
AuthCode:state.AuthCode,
|
||||
gid
|
||||
AuthCode:context.state.AuthCode, gid
|
||||
}}).then((res) => {
|
||||
if(res.data.result === "success"){
|
||||
ElMessage("删除成功")
|
||||
@@ -229,12 +167,39 @@ const actions = {
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
},
|
||||
retryGallery(context, gid){
|
||||
return axios.post(GalleryManageUrl + "/retry?" + qs.stringify({
|
||||
gid, AuthCode: context.state.AuthCode
|
||||
})).then((res) => {
|
||||
if(res.data.result === "success"){
|
||||
ElMessage({message: "重试结果:" + res.data.data, type: "success"})
|
||||
return context.dispatch("updateGalleryTasks")
|
||||
}
|
||||
ElMessage({message: res.data.data || "重试失败", type: "error"})
|
||||
})
|
||||
},
|
||||
readOnlineGallery(context, gallery){
|
||||
if(gallery.images !== undefined && gallery.images.length !== 0)
|
||||
context.commit("_setReadingGallery", gallery)
|
||||
else
|
||||
return axios.post(GalleryManageUrl + "/cache", null, {
|
||||
params: {url: gallery.link, AuthCode: context.state.AuthCode}
|
||||
}).then((res) => {
|
||||
if (res.data.result === 'success') {
|
||||
gallery.pages = res.data.data.pages
|
||||
setTimeout(() => {
|
||||
context.commit("_setReadingGallery", gallery)
|
||||
}, 100)
|
||||
} else
|
||||
ElMessage.error(res.data.data)
|
||||
})
|
||||
},
|
||||
alterAuthCode(context, AuthCode){
|
||||
axios.put(BaseUrl + "AuthCode?" + qs.stringify({'AuthCode': state.AuthCode, 'newAuthCode': AuthCode}))
|
||||
axios.put(BaseUrl + "AuthCode?" + qs.stringify({'AuthCode': context.state.AuthCode, 'newAuthCode': AuthCode}))
|
||||
.then((res) => {
|
||||
if(res.data.result === 'success') {
|
||||
ElMessage("修改成功")
|
||||
if(localStorage.getItem("auth") === state.AuthCode)
|
||||
if(localStorage.getItem("auth") === context.state.AuthCode)
|
||||
localStorage.setItem("auth", AuthCode)
|
||||
context.state.AuthCode = AuthCode
|
||||
}
|
||||
@@ -242,14 +207,17 @@ const actions = {
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
},
|
||||
resetUndone(){
|
||||
axios.post(GalleryManageUrl + "/reset?AuthCode=big+lion").then((res) => {
|
||||
resetUndone(context){
|
||||
axios.post(GalleryManageUrl + "/reset?AuthCode=" + context.state.AuthCode).then((res) => {
|
||||
ElMessage(res.data.data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
_setConnectionStatus(state, status){
|
||||
state.connectionStatus = status
|
||||
},
|
||||
_collectGallery(state, gid){
|
||||
let tasks = state.totalGalleryTask
|
||||
for(let i=0; i< tasks.length; i++){
|
||||
@@ -261,67 +229,49 @@ const mutations = {
|
||||
},
|
||||
_disCollectGallery(state, gid){
|
||||
let index
|
||||
for(let i=0; i < state.collectGallery.length; i++){
|
||||
for(let i=0; i < state.collectGallery.length; i++)
|
||||
if(state.collectGallery[i].gid === gid){
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
state.collectGallery[index].isCollect = false
|
||||
state.collectGallery.splice(index, 1)
|
||||
},
|
||||
_updateGalleryTasks(state, data){
|
||||
let {tasks, type} = data
|
||||
if(type === 'all') {
|
||||
_updateGalleryTasks(state, tasks){
|
||||
state.totalGalleryTask.splice(0)
|
||||
state.collectGallery.slice(0)
|
||||
state.collectGallery.splice(0)
|
||||
state.downloadGallery.splice(0)
|
||||
|
||||
tasks.forEach((task) => {
|
||||
//处理名字
|
||||
task.shortName = getShortname(task.name)
|
||||
if(task.thumb_link.trim() !== '')
|
||||
task.thumb_link = buildGalleryManageUrl("/ehThumbnail", {
|
||||
path: task.thumb_link,
|
||||
AuthCode: state.AuthCode
|
||||
})
|
||||
else
|
||||
delete task.thumb_link
|
||||
|
||||
//处理进度相关
|
||||
switch (task.status) {
|
||||
case "已提交":
|
||||
task.progress = "已提交"
|
||||
case "等待压缩":
|
||||
case "压缩中":
|
||||
task.progress = task.status
|
||||
break;
|
||||
case "下载中":
|
||||
task.progress = (Math.round((task.proceeding / task.pages) * 100)).toString() + "%"
|
||||
break;
|
||||
case "下载完成":
|
||||
task.progress = "下载完成"
|
||||
if(task.mode === 1 || task.mode === 3)
|
||||
task.download = GalleryManageUrl + "/file/" + encodeURI(task.name) + ".zip?AuthCode=" + state.AuthCode + "&gid=" + task.gid
|
||||
|
||||
if(task.mode === 2 || task.mode === 3) {
|
||||
let links = []
|
||||
for (let i = 1; i <= task.pages; i++)
|
||||
links.push(GalleryManageUrl + "/onlineImage/" + i + "?gid=" + task.gid);
|
||||
task.images = links
|
||||
}
|
||||
task.progress = task.status
|
||||
task.download = buildGalleryDownloadUrl(task.gid, state.AuthCode)
|
||||
break;
|
||||
case "压缩中":
|
||||
task.progress = "压缩中"
|
||||
break
|
||||
}
|
||||
|
||||
//处理时间戳
|
||||
task.createTimeDisplay = new Date(task.createTime * 1000).toLocaleString("zh")
|
||||
|
||||
//处理标签
|
||||
if ('tags' in task) {
|
||||
task.tag = ''
|
||||
task.tags.forEach((tid) => {
|
||||
task.tag += ' ' + state.tags.get(tid).tag
|
||||
})
|
||||
task.tag = task.tag.trim()
|
||||
|
||||
}else{
|
||||
task.tag = ""
|
||||
task.tags = []
|
||||
}
|
||||
|
||||
//处理是否收藏
|
||||
if('isCollect' in task)
|
||||
state.collectGallery.push(task)
|
||||
@@ -334,52 +284,31 @@ const mutations = {
|
||||
|
||||
state.totalGalleryTask.push(task)
|
||||
})
|
||||
}
|
||||
else {
|
||||
let tempArray = Array.from(state.totalGalleryTask)
|
||||
state.totalGalleryTask.splice(0)
|
||||
let preDeleteIndex
|
||||
tempArray.forEach((task) => {
|
||||
preDeleteIndex = -1
|
||||
if(task.status !== "下载完成")
|
||||
for(let i=0; i < tasks.length; i++)
|
||||
if (tasks[i] !== undefined && tasks[i].name === task.name) {
|
||||
preDeleteIndex = i
|
||||
task.status = tasks[i].status
|
||||
task.proceeding = tasks[i].proceeding
|
||||
if (task.proceeding === 0)
|
||||
task.progress = task.status
|
||||
else
|
||||
task.progress = (Math.round((task.proceeding / task.pages) * 100)).toString() + "%"
|
||||
}
|
||||
|
||||
if(preDeleteIndex !== -1)
|
||||
delete tasks[preDeleteIndex]
|
||||
state.totalGalleryTask.push(task)
|
||||
})
|
||||
}
|
||||
|
||||
switch (state.sortType) {
|
||||
case "name":
|
||||
state.totalGalleryTask = state.totalGalleryTask.sort((before, after) => {
|
||||
return before.name > after.name ? 1: -1
|
||||
})
|
||||
break
|
||||
case "shortName":
|
||||
state.totalGalleryTask = state.totalGalleryTask.sort((before, after) => {
|
||||
return before.shortName > after.shortName ? 1: -1
|
||||
})
|
||||
break
|
||||
case "createTime":
|
||||
state.totalGalleryTask = state.totalGalleryTask.sort((before, after) => {
|
||||
return before.createTime - after.createTime
|
||||
})
|
||||
}
|
||||
sortTasks(state)
|
||||
if(state.isAuth && !state.loadComplete){
|
||||
state.loadComplete = true
|
||||
ElMessage("加载完成")
|
||||
}
|
||||
},
|
||||
_updateGalleryTaskProceeding(state, tasks){
|
||||
let galleries = Array.from(state.totalGalleryTask)
|
||||
state.totalGalleryTask.splice(0)
|
||||
galleries.forEach((gallery) => {
|
||||
if(gallery.status !== '下载完成')
|
||||
tasks.forEach((task) => {
|
||||
if(task.gid === gallery.gid){
|
||||
gallery.status = status[task.status]
|
||||
gallery.proceeding = task.proceeding
|
||||
if(gallery.status === '下载中')
|
||||
gallery.progress = (Math.round((gallery.proceeding / gallery.pages) * 100)).toString() + "%"
|
||||
else
|
||||
gallery.progress = gallery.status
|
||||
}
|
||||
})
|
||||
state.totalGalleryTask.push(gallery)
|
||||
})
|
||||
},
|
||||
_changePage(state, targetPage){
|
||||
state.page = targetPage
|
||||
},
|
||||
@@ -387,6 +316,8 @@ const mutations = {
|
||||
state.AuthCode = data.AuthCode
|
||||
state.userId = data.userId
|
||||
state.username = data.username
|
||||
state.isAdmin = Boolean(data.isAdmin)
|
||||
state.downloaderFilter = null
|
||||
state.isAuth = true
|
||||
ElMessage("验证成功,加载中")
|
||||
},
|
||||
@@ -398,62 +329,21 @@ const mutations = {
|
||||
ElMessage("授权码错误")
|
||||
localStorage.removeItem("auth")
|
||||
},
|
||||
_loadTags(state, tags){
|
||||
state.tags.clear()
|
||||
for(let i in tags)
|
||||
state.tags.set(parseInt(i), tags[i])
|
||||
},
|
||||
_postTag(state, tag){
|
||||
state.tags.set(tag.id, tag)
|
||||
},
|
||||
_deleteTag(state, tid){
|
||||
state.tags.delete(tid)
|
||||
},
|
||||
_mark(state, data){
|
||||
state.totalGalleryTask.forEach((gallery) => {
|
||||
if(gallery.gid === data.gid){
|
||||
gallery.tags.push(data.tid)
|
||||
if('tag' in data) //新建的tag
|
||||
state.tags.set(data.tid, {id:data.tid, tag: data.tag, usage:data.usage})
|
||||
else
|
||||
state.tags.get(data.tid).usage++
|
||||
generateNewTag(state, gallery)
|
||||
}
|
||||
})
|
||||
},
|
||||
_disMark(state, data){
|
||||
state.totalGalleryTask.forEach((gallery) => {
|
||||
if(gallery.gid === data.gid){
|
||||
let index
|
||||
for(index=0; index<gallery.tags.length; index++){
|
||||
if(gallery.tags[index] === data.tid){
|
||||
gallery.tags.splice(index, 1) //删除tid
|
||||
state.tags.get(data.tid).usage--
|
||||
break;
|
||||
}
|
||||
}
|
||||
generateNewTag(state, gallery)
|
||||
}
|
||||
})
|
||||
},
|
||||
_searchLocalByLink(state, link){
|
||||
let tasks = state.currentTasks
|
||||
let i = 0
|
||||
let gid = null
|
||||
let gid = link.split("/")[4]
|
||||
let name = null
|
||||
|
||||
gid = link.split("/")[4]
|
||||
|
||||
if(gid === null)
|
||||
for (i = 0; i < tasks.length; i++) {
|
||||
if(gid === undefined)
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
if (tasks[i].link === link) {
|
||||
state.page = Math.floor(i / state.length) + 1
|
||||
name = tasks[i].name
|
||||
break
|
||||
}
|
||||
}
|
||||
else for (i = 0; i < tasks.length; i++)
|
||||
if (tasks[i].gid === gid) {
|
||||
else for (let i = 0; i < tasks.length; i++)
|
||||
if (String(tasks[i].gid) === gid) {
|
||||
state.page = Math.floor(i / state.length) + 1
|
||||
name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name
|
||||
break
|
||||
@@ -485,67 +375,17 @@ const mutations = {
|
||||
confirmCurrentTask(state)
|
||||
}
|
||||
},
|
||||
_searchLocalByTag(state, tidS) {
|
||||
state.searchTask.splice(0)
|
||||
state.page = 1
|
||||
|
||||
let hitAmount
|
||||
let tasks = state.currentTasks
|
||||
|
||||
if (tidS.length > 0) {
|
||||
tasks.forEach((task) => {
|
||||
if(tidS.length <= task.tags.length) {
|
||||
|
||||
hitAmount = 0
|
||||
for(let i=0; i<task.tag.length; i++){
|
||||
for(let j=0; j<tidS.length; j++){
|
||||
if(task.tags[i] === tidS[j])
|
||||
hitAmount ++
|
||||
}
|
||||
}
|
||||
if (hitAmount === tidS.length)
|
||||
state.searchTask.push(task)
|
||||
}
|
||||
})
|
||||
|
||||
if (state.searchTask.length === 0) {
|
||||
ElMessage("未找到符合这些tag的任务")
|
||||
state.isSearch = false
|
||||
}
|
||||
else {
|
||||
state.isSearch = true
|
||||
}
|
||||
}else {
|
||||
state.isSearch = false
|
||||
confirmCurrentTask(state)
|
||||
}
|
||||
},
|
||||
_deleteGallery(state, gid){
|
||||
let tasks = [state.totalGalleryTask, state.downloadGallery, state.collectGallery]
|
||||
deleteTask(tasks, 'gid', gid)
|
||||
},
|
||||
_setChosenGallery(state,data){
|
||||
if(data.gallery === false) {
|
||||
state.chosenGallery.shortName = getShortname(state.chosenGallery.name)
|
||||
state.chosenGallery.resolution = data.resolution
|
||||
state.chosenGallery.fileSize = "等待下载完成后再查看"
|
||||
state.chosenGallery.createTimeDisplay = "等待下载完成后再查看"
|
||||
state.chosenGallery.progress = "已提交"
|
||||
state.chosenGallery.tags = []
|
||||
state.chosenGallery.downloader = state.userId
|
||||
if(data.tags.length > 0){
|
||||
for (let tag of data.tags) {
|
||||
state.chosenGallery.tags.push(parseInt(tag))
|
||||
}
|
||||
}
|
||||
generateNewTag(state, state.chosenGallery)
|
||||
state.totalGalleryTask.push(state.chosenGallery)
|
||||
state.downloadGallery.push(state.chosenGallery)
|
||||
}
|
||||
state.chosenGallery = data.gallery
|
||||
},
|
||||
_setCategory(state, category){
|
||||
state.category = category
|
||||
// 换分类后旧的下载人筛选可能在新分类里根本不存在,先清掉。
|
||||
state.downloaderFilter = null
|
||||
confirmCurrentTask(state)
|
||||
sortTasks(state)
|
||||
},
|
||||
@@ -553,36 +393,58 @@ const mutations = {
|
||||
state.sortType = sortType
|
||||
sortTasks(state)
|
||||
},
|
||||
_setGalleryNameType(state, galleryNameType){
|
||||
state.galleryNameType = galleryNameType
|
||||
},
|
||||
_setShowNameType(state, type){
|
||||
if(type === "shortName")
|
||||
state.length = state.shortLength
|
||||
else
|
||||
state.length = state.defaultLength
|
||||
},
|
||||
_changeThumbnailGallery(state, gallery){
|
||||
if(gallery.mode === 2 || gallery.mode === 3) {
|
||||
state.thumbnailGallery = gallery
|
||||
state.thumbnailGallery.url = GalleryManageUrl + "/onlineImage/1?gid=" + gallery.gid
|
||||
} else if(gallery.thumb_link !== undefined){
|
||||
state.thumbnailGallery = gallery
|
||||
state.thumbnailGallery.url = GalleryManageUrl + "/ehThumbnail?path=" + gallery.thumb_link
|
||||
state.thumbnailGallery.images = [GalleryManageUrl + "/ehThumbnail?path=" + gallery.thumb_link,]
|
||||
_setReadingGallery(state, gallery){
|
||||
if(gallery.images === undefined) {
|
||||
gallery.images = []
|
||||
for(let i=1; i<=gallery.pages; i++)
|
||||
gallery.images.push(buildGalleryManageUrl("/onlineImage/" + i, {
|
||||
gid: gallery.gid,
|
||||
AuthCode: state.AuthCode
|
||||
}));
|
||||
}
|
||||
state.readingGallery = gallery
|
||||
state.isReading = true;
|
||||
},
|
||||
_closeReader(state){
|
||||
state.isReading = false;
|
||||
},
|
||||
_setActiveView(state, view){
|
||||
state.activeView = view
|
||||
},
|
||||
_selectGallery(state, gallery){
|
||||
state.selectedGallery = gallery || null
|
||||
},
|
||||
_setDownloaderFilter(state, downloader){
|
||||
state.downloaderFilter = downloader === null || downloader === undefined
|
||||
? null
|
||||
: Number(downloader)
|
||||
// 筛完可能不足一页,回到第一页避免停在空页上。
|
||||
state.page = 1
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
totalGalleryTask: [], //存放本子数据的数组
|
||||
chosenGallery: false, //准备下载的本子
|
||||
thumbnailGallery: {}, //缩略图链接
|
||||
collectGallery: [], //收藏的本子
|
||||
downloadGallery: [], //下载的本子
|
||||
tags: new Map(), //可用tag
|
||||
connectionStatus: "disconnected", // live task updates
|
||||
|
||||
totalGalleryTask: [], //存放数据的数组
|
||||
chosenGallery: false, //准备下载
|
||||
collectGallery: [], //收藏
|
||||
downloadGallery: [], //下载
|
||||
isSearch: false, //用于决定是否显示搜索结果
|
||||
|
||||
readingGallery: {'name': '', 'images': []}, //在线看
|
||||
isReading: false, //是否正在看
|
||||
|
||||
currentGid: "", //当前GID
|
||||
imageWidth: "", //图片宽度
|
||||
imagePadding: "", //图片padding
|
||||
lengthPerPage: 0, //在线预览每页图片数量
|
||||
|
||||
page: 1, //当前页数
|
||||
@@ -595,33 +457,37 @@ const state = {
|
||||
isAuth: false, //是否授权
|
||||
AuthCode: '', //授权码
|
||||
loadComplete: false, //是否加载完成
|
||||
galleryRefreshTimer: 0, //本子更新计时器id
|
||||
galleryRefreshTimer: 0, //更新计时器id
|
||||
|
||||
isInclude: false, //是否搜索到任务
|
||||
searchTask: [], //搜索到的任务
|
||||
category: 'myDownload', //分类
|
||||
sortType:'shortName', //排序类型
|
||||
category: 'myDownload', //分类 myDownload myCollect total
|
||||
sortType:'shortName', //排序类型 shortName name createTime
|
||||
galleryNameType: 'shortName', //名字类型 shortName name
|
||||
currentTasks: [], //当前任务
|
||||
weekUsed: {}, //每周用量
|
||||
|
||||
activeView: 'tasks', //当前视图 tasks search settings
|
||||
selectedGallery: null, //右侧详情面板选中的任务
|
||||
isAdmin: false, //管理员(后端 validate 下发)
|
||||
downloaderFilter: null, //管理员按下载人筛选,null 表示全部
|
||||
}
|
||||
|
||||
const getters = {
|
||||
/** 当前筛选条件命中的完整任务列表(未分页),用于计数。 */
|
||||
filteredTasks(state){
|
||||
return activeTaskList(state)
|
||||
},
|
||||
currentTasks(state){
|
||||
if(state.isSearch)
|
||||
return state.searchTask.slice((state.page - 1) * state.length, state.page * state.length)
|
||||
else
|
||||
return state.currentTasks.slice((state.page - 1) * state.length, state.page * state.length)
|
||||
const tasks = activeTaskList(state)
|
||||
return tasks.slice((state.page - 1) * state.length, state.page * state.length)
|
||||
},
|
||||
min(){
|
||||
return 1
|
||||
},
|
||||
max(state){
|
||||
let max = 0
|
||||
let tasks
|
||||
if(state.isSearch)
|
||||
tasks = state.searchTask
|
||||
else
|
||||
tasks = state.currentTasks
|
||||
const tasks = activeTaskList(state)
|
||||
|
||||
if(!tasks)
|
||||
return 1
|
||||
@@ -641,42 +507,41 @@ export default new vuex.Store({
|
||||
getters
|
||||
})
|
||||
|
||||
/**
|
||||
* 当前视图实际要展示的任务。
|
||||
* 下载人筛选只在管理员生效,作用于「我的下载/收藏/全部」三种分类的结果。
|
||||
*/
|
||||
function activeTaskList(state){
|
||||
if(state.isSearch)
|
||||
return state.searchTask
|
||||
if(state.isAdmin && state.downloaderFilter !== null)
|
||||
return state.currentTasks.filter(task => task.downloader === state.downloaderFilter)
|
||||
return state.currentTasks
|
||||
}
|
||||
|
||||
function getShortname(name){
|
||||
if(name === null){
|
||||
console.log(name)
|
||||
return null
|
||||
}
|
||||
if (name.includes("[")) {
|
||||
let lastIndex = name.lastIndexOf("[")
|
||||
name = name.substring(0, lastIndex)
|
||||
while (name.includes("[") && name.includes("]")) {
|
||||
let start = name.indexOf("[")
|
||||
let end = name.indexOf("]") + 1
|
||||
let temp = name.substring(start, end)
|
||||
temp = name.replace(temp, "")
|
||||
if(temp.trim() === ""){
|
||||
name = name.replace("[", "").replace("]", "")
|
||||
break
|
||||
if (!name.includes("[")) return name
|
||||
|
||||
// 截取最后一个 [ 之前的部分,然后移除所有 [...] 和 (...) 标签
|
||||
const trimmed = name.substring(0, name.lastIndexOf("["))
|
||||
.replace(/\s*\[[^\]]*\]\s*/g, '')
|
||||
.replace(/\s*\([^\)]*\)\s*/g, '')
|
||||
.trim()
|
||||
// 名字以 [...] 开头时(例如 "[作者] 标题"),截取结果为空,退回原名避免整列空白
|
||||
return trimmed === '' ? name.trim() : trimmed
|
||||
}
|
||||
else
|
||||
name = temp
|
||||
}
|
||||
while (name.includes("(") && name.includes(")")) {
|
||||
let start = name.indexOf("(")
|
||||
let end = name.indexOf(")") + 1
|
||||
let temp = name.substring(start, end)
|
||||
temp = name.replace(temp, "")
|
||||
if(temp.trim() === ""){
|
||||
name = name.replace("(", "").replace(")", "")
|
||||
break
|
||||
}
|
||||
else
|
||||
name = temp
|
||||
}
|
||||
return name.trim()
|
||||
} else {
|
||||
return name
|
||||
|
||||
function buildGalleryDownloadUrl(gid, AuthCode){
|
||||
let params = new URLSearchParams({AuthCode, gid: gid.toString()})
|
||||
return GalleryManageUrl + "/file/" + gid + ".zip?" + params.toString()
|
||||
}
|
||||
|
||||
function buildGalleryManageUrl(path, params){
|
||||
return GalleryManageUrl + path + "?" + new URLSearchParams(params).toString()
|
||||
}
|
||||
|
||||
function confirmCurrentTask(state){
|
||||
@@ -717,16 +582,9 @@ function deleteTask(tasks, key, value){
|
||||
for(let i=0; i<tasks[j].length; i++)
|
||||
if(tasks[j][i][key] === value){
|
||||
tasks[j].splice(i, 1)
|
||||
i--
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function generateNewTag(state, gallery){
|
||||
let tag = ''
|
||||
console.log(state.tags)
|
||||
console.log(gallery.tags)
|
||||
gallery.tags.forEach((tid) => {
|
||||
tag += state.tags.get(tid).tag + ' '
|
||||
})
|
||||
gallery.tag = tag.trim()
|
||||
}
|
||||
let status = ['已提交', '下载中', '等待压缩', '压缩中', '下载完成']
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
function imageItem(url, index) {
|
||||
return {url, src: url, index, attempt: 0, failed: false, settled: false}
|
||||
}
|
||||
|
||||
export function startImages(urls) {
|
||||
return urls.length ? [imageItem(urls[0], 0)] : []
|
||||
}
|
||||
|
||||
export function settleImage(items, urls, item, failed = false) {
|
||||
// Ignore events from images removed by a page change or dialog close.
|
||||
if (!items.includes(item)) return
|
||||
item.failed = failed
|
||||
if (item.settled) return
|
||||
item.settled = true
|
||||
if (items.length < urls.length)
|
||||
items.push(imageItem(urls[items.length], items.length))
|
||||
}
|
||||
|
||||
export function retryImage(item) {
|
||||
item.failed = false
|
||||
item.attempt++
|
||||
item.src = item.url + (item.url.includes('?') ? '&' : '?') + 'retry=' + item.attempt
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
export function createTaskSocket({url, onOpen, onMessage, onState,
|
||||
createSocket = address => new WebSocket(address),
|
||||
setTimer = setTimeout, clearTimer = clearTimeout}) {
|
||||
let socket = null
|
||||
let retryTimer
|
||||
let connectTimer
|
||||
let attempts = 0
|
||||
let stopped = true
|
||||
|
||||
function detach() {
|
||||
clearTimer(connectTimer)
|
||||
connectTimer = undefined
|
||||
if (socket) {
|
||||
const previous = socket
|
||||
socket = null
|
||||
previous.onopen = previous.onclose = previous.onerror = previous.onmessage = null
|
||||
try { previous.close() } catch { /* already closed */ }
|
||||
}
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
if (stopped) return
|
||||
onState('reconnecting')
|
||||
const delay = Math.min(1000 * 2 ** attempts, 30000)
|
||||
attempts = Math.min(attempts + 1, 5)
|
||||
clearTimer(retryTimer)
|
||||
retryTimer = setTimer(connect, delay)
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (stopped) return
|
||||
clearTimer(retryTimer)
|
||||
retryTimer = undefined
|
||||
onState(attempts ? 'reconnecting' : 'connecting')
|
||||
let current
|
||||
try { current = socket = createSocket(url) }
|
||||
catch { schedule(); return }
|
||||
const retry = () => {
|
||||
if (stopped || socket !== current) return
|
||||
detach()
|
||||
schedule()
|
||||
}
|
||||
current.onopen = () => {
|
||||
if (stopped || socket !== current) return
|
||||
clearTimer(connectTimer)
|
||||
connectTimer = undefined
|
||||
attempts = 0
|
||||
try { current.send('DownloaderWebsocket') }
|
||||
catch { retry(); return }
|
||||
onState('connected')
|
||||
onOpen()
|
||||
}
|
||||
current.onmessage = event => {
|
||||
if (!stopped && socket === current) onMessage(event)
|
||||
}
|
||||
current.onclose = retry
|
||||
current.onerror = retry
|
||||
connectTimer = setTimer(retry, 10000)
|
||||
}
|
||||
|
||||
return {
|
||||
start() {
|
||||
if (!stopped) return
|
||||
stopped = false
|
||||
attempts = 0
|
||||
connect()
|
||||
},
|
||||
stop() {
|
||||
stopped = true
|
||||
clearTimer(retryTimer)
|
||||
retryTimer = undefined
|
||||
detach()
|
||||
onState('disconnected')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
const BaseUrl = "https://downloader.lionwebsite.xyz/";
|
||||
|
||||
// Remote galleries carry a raw thumbnail path; it only resolves through the
|
||||
// backend proxy once the active auth code is attached.
|
||||
export function ehThumbnailUrl(path, AuthCode) {
|
||||
if (!path) return "";
|
||||
return BaseUrl + "GalleryManage/ehThumbnail?" + new URLSearchParams({path, AuthCode}).toString();
|
||||
}
|
||||
|
||||
const UNIT_SCALE = {b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4};
|
||||
|
||||
function sizeInBytes(text) {
|
||||
const match = String(text).match(/([\d.]+)\s*([kmgt]?)(?:i?b)/i);
|
||||
if (!match) return 0;
|
||||
const scale = UNIT_SCALE[match[2].toLowerCase()] ?? 1;
|
||||
return Number(match[1]) * scale;
|
||||
}
|
||||
|
||||
// Prefer the original file; otherwise take whichever entry reports the most bytes.
|
||||
export function pickDefaultResolution(availableResolution) {
|
||||
const entries = Object.entries(availableResolution || {});
|
||||
if (entries.length === 0) return "";
|
||||
const original = entries.find(([resolution]) => /original/i.test(resolution));
|
||||
if (original) return original[0];
|
||||
return entries.reduce((best, current) =>
|
||||
sizeInBytes(current[1]) > sizeInBytes(best[1]) ? current : best)[0];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function validateLink(rawLink){
|
||||
if(rawLink.trim() === "")
|
||||
return false
|
||||
if(rawLink.includes("hentai"))
|
||||
return rawLink.includes("/g/")
|
||||
else
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {startImages, settleImage, retryImage} from '../src/utils/progressiveImages.js'
|
||||
|
||||
test('a failed image does not prevent later pages from loading', () => {
|
||||
const urls = ['first', 'second', 'third']
|
||||
const items = startImages(urls)
|
||||
settleImage(items, urls, items[0], true)
|
||||
assert.equal(items[0].failed, true)
|
||||
assert.equal(items[1].url, 'second')
|
||||
settleImage(items, urls, items[1])
|
||||
assert.equal(items[2].url, 'third')
|
||||
})
|
||||
|
||||
test('retry does not append a duplicate page and bypasses a failed URL cache', () => {
|
||||
const urls = ['first?gid=1', 'second', 'third']
|
||||
const items = startImages(urls)
|
||||
settleImage(items, urls, items[0], true)
|
||||
retryImage(items[0])
|
||||
assert.equal(items[0].src, 'first?gid=1&retry=1')
|
||||
settleImage(items, urls, items[0])
|
||||
assert.equal(items.length, 2)
|
||||
assert.equal(items[0].failed, false)
|
||||
})
|
||||
|
||||
test('late image events cannot append content after changing pages', () => {
|
||||
const old = startImages(['old'])[0]
|
||||
const urls = ['new', 'next']
|
||||
const items = startImages(urls)
|
||||
settleImage(items, urls, old)
|
||||
assert.equal(items.length, 1)
|
||||
assert.deepEqual(startImages([]), [])
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import vm from 'node:vm'
|
||||
|
||||
function fixture() {
|
||||
let code = fs.readFileSync(new URL('../src/store/index.js', import.meta.url), 'utf8')
|
||||
.replace(/^import .*$/gm, '')
|
||||
.replace('export default new vuex.Store(', 'globalThis.store = new vuex.Store(')
|
||||
const messages = []
|
||||
const showMessage = message => messages.push(message)
|
||||
showMessage.error = message => messages.push(message)
|
||||
const axios = {interceptors: {response: {use() {}}}}
|
||||
const context = vm.createContext({axios, vuex: {Store: function (config) {Object.assign(this, config)}},
|
||||
ElMessage: showMessage, URLSearchParams, console,
|
||||
qs: {stringify: () => ''}})
|
||||
vm.runInContext(code, context)
|
||||
const store = context.store
|
||||
const api = {state: store.state,
|
||||
commit: (name, data) => store.mutations[name](store.state, data),
|
||||
dispatch: (name, data) => Promise.resolve(store.actions[name](api, data))}
|
||||
return {store, messages, axios, api}
|
||||
}
|
||||
|
||||
test('link search matches a numeric gid and jumps to the right page', () => {
|
||||
const {store, messages} = fixture()
|
||||
store.state.currentTasks = [{gid: 1}, {gid: 2}, {gid: 12345, name: 'sample', shortName: 'sample'}]
|
||||
store.state.length = 2
|
||||
store.mutations._searchLocalByLink(store.state, 'https://example.org/g/12345/key/')
|
||||
assert.equal(store.state.page, 2)
|
||||
assert.match(messages[0], /已跳转/)
|
||||
})
|
||||
|
||||
test('missing gid still reports no match', () => {
|
||||
const {store, messages} = fixture()
|
||||
store.state.currentTasks = [{gid: 12345}]
|
||||
store.mutations._searchLocalByLink(store.state, 'https://example.org/g/999/key/')
|
||||
assert.equal(messages[0], '未找到此任务')
|
||||
})
|
||||
|
||||
test('submission reloads persisted status without duplicating a completed task', async () => {
|
||||
const {store, axios, api} = fixture()
|
||||
const gallery = {gid: 123, name: 'sample', status: '下载完成', thumb_link: '', createTime: 1, downloader: 7}
|
||||
store.state.chosenGallery = {...gallery, status: '已提交'}
|
||||
store.state.totalGalleryTask.push(gallery)
|
||||
axios.post = async () => ({data: {result: 'success'}})
|
||||
axios.get = async url => ({data: {result: 'success', data: JSON.stringify(url.endsWith('weekUsedAmount') ? {} : [gallery])}})
|
||||
await store.actions.postGalleryTask(api, {targetResolution: 'original'})
|
||||
assert.equal(store.state.totalGalleryTask.length, 1)
|
||||
assert.equal(store.state.totalGalleryTask[0].status, '下载完成')
|
||||
assert.equal(store.state.chosenGallery, false)
|
||||
})
|
||||
|
||||
test('gallery management subrequests carry the active auth code', async () => {
|
||||
const {store, axios, api} = fixture()
|
||||
store.state.AuthCode = 'test-auth'
|
||||
let call
|
||||
axios.post = async (...args) => {
|
||||
call = args
|
||||
return {data: {result: args[0].endsWith('/cache') ? 'failure' : 'success', data: 'expected test response'}}
|
||||
}
|
||||
await store.actions.reconnect(api)
|
||||
assert.equal(call[0], 'https://downloader.lionwebsite.xyz/GalleryManage/reconnect')
|
||||
assert.equal(call[1], null)
|
||||
assert.equal(call[2].params.AuthCode, 'test-auth')
|
||||
|
||||
await store.actions.readOnlineGallery(api, {link: 'https://example.org/g/1/key/', images: []})
|
||||
assert.equal(call[0], 'https://downloader.lionwebsite.xyz/GalleryManage/cache')
|
||||
assert.equal(call[1], null)
|
||||
assert.deepEqual({...call[2].params}, {
|
||||
url: 'https://example.org/g/1/key/',
|
||||
AuthCode: 'test-auth'
|
||||
})
|
||||
})
|
||||
|
||||
test('thumbnail and online image URLs carry the active auth code', () => {
|
||||
const {store} = fixture()
|
||||
store.state.AuthCode = 'code with spaces'
|
||||
store.mutations._updateGalleryTasks(store.state, [{
|
||||
gid: 123,
|
||||
name: 'sample',
|
||||
status: '已提交',
|
||||
thumb_link: 'https://example.org/thumb?a=1&b=2',
|
||||
createTime: 1,
|
||||
downloader: 7
|
||||
}])
|
||||
const thumbnail = new URL(store.state.totalGalleryTask[0].thumb_link)
|
||||
assert.equal(thumbnail.searchParams.get('AuthCode'), 'code with spaces')
|
||||
assert.equal(thumbnail.searchParams.get('path'), 'https://example.org/thumb?a=1&b=2')
|
||||
|
||||
store.mutations._setReadingGallery(store.state, {gid: 123, pages: 1})
|
||||
const image = new URL(store.state.readingGallery.images[0])
|
||||
assert.equal(image.searchParams.get('gid'), '123')
|
||||
assert.equal(image.searchParams.get('AuthCode'), 'code with spaces')
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {createTaskSocket} from '../src/utils/taskSocket.js'
|
||||
|
||||
function fixture() {
|
||||
const sockets = [], states = [], messages = [], timers = new Map()
|
||||
let id = 0, opens = 0
|
||||
const manager = createTaskSocket({url: 'wss://example.invalid',
|
||||
onOpen: () => opens++, onState: value => states.push(value), onMessage: event => messages.push(event.data),
|
||||
setTimer: (fn, delay) => { timers.set(++id, {fn, delay}); return id },
|
||||
clearTimer: key => timers.delete(key),
|
||||
createSocket: () => {
|
||||
const socket = {sent: [], send(value) { this.sent.push(value) }, close() { this.closed = true }}
|
||||
sockets.push(socket)
|
||||
return socket
|
||||
}
|
||||
})
|
||||
function tick(delay) {
|
||||
const entry = [...timers].find(([, timer]) => timer.delay === delay)
|
||||
assert.ok(entry, `expected timer ${delay}`)
|
||||
timers.delete(entry[0]); entry[1].fn()
|
||||
}
|
||||
return {manager, sockets, states, messages, timers, tick, opens: () => opens}
|
||||
}
|
||||
|
||||
test('reconnects and refreshes after each successful connection', () => {
|
||||
const f = fixture(); f.manager.start()
|
||||
f.sockets[0].onopen()
|
||||
assert.deepEqual(f.sockets[0].sent, ['DownloaderWebsocket'])
|
||||
f.sockets[0].onclose()
|
||||
f.tick(1000)
|
||||
f.sockets[1].onopen()
|
||||
assert.equal(f.opens(), 2)
|
||||
assert.equal(f.states.at(-1), 'connected')
|
||||
assert.equal(f.timers.size, 0)
|
||||
f.manager.stop()
|
||||
})
|
||||
|
||||
test('backs off and times out connection attempts', () => {
|
||||
const f = fixture(); f.manager.start()
|
||||
f.tick(10000)
|
||||
assert.equal(f.sockets[0].closed, true)
|
||||
f.tick(1000); f.sockets[1].onerror(); f.tick(2000)
|
||||
f.sockets[2].onerror(); f.tick(4000)
|
||||
assert.equal(f.sockets.length, 4)
|
||||
f.manager.stop()
|
||||
})
|
||||
|
||||
test('stop cancels timers and ignores callbacks from replaced sockets', () => {
|
||||
const f = fixture(); f.manager.start()
|
||||
const oldOpen = f.sockets[0].onopen, oldMessage = f.sockets[0].onmessage
|
||||
f.sockets[0].onerror()
|
||||
f.manager.stop()
|
||||
assert.equal(f.timers.size, 0)
|
||||
f.manager.start()
|
||||
oldOpen(); oldMessage({data: 'stale'})
|
||||
assert.equal(f.opens(), 0)
|
||||
assert.deepEqual(f.messages, [])
|
||||
f.sockets[1].onopen()
|
||||
f.sockets[1].onmessage({data: 'current'})
|
||||
assert.deepEqual(f.messages, ['current'])
|
||||
f.manager.stop()
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {pickDefaultResolution, ehThumbnailUrl} from '../src/utils/thumbnail.js'
|
||||
|
||||
test('original resolution wins whenever it is offered', () => {
|
||||
assert.equal(pickDefaultResolution({
|
||||
'800x': '37.53MiB',
|
||||
'1280x': '66.57MiB',
|
||||
'2560x': '84.01MiB',
|
||||
'Original': '498.2MiB'
|
||||
}), 'Original')
|
||||
})
|
||||
|
||||
test('without an original entry the largest file is chosen', () => {
|
||||
// 1920x is bigger than 2560x here, so byte size decides, not the label.
|
||||
assert.equal(pickDefaultResolution({
|
||||
'800x': '37.53MiB',
|
||||
'1920x': '811.1MiB',
|
||||
'2560x': '84.01MiB'
|
||||
}), '1920x')
|
||||
})
|
||||
|
||||
test('an empty or missing resolution map yields an empty choice', () => {
|
||||
assert.equal(pickDefaultResolution({}), '')
|
||||
assert.equal(pickDefaultResolution(undefined), '')
|
||||
})
|
||||
|
||||
test('thumbnail urls carry the path and the active auth code', () => {
|
||||
const url = new URL(ehThumbnailUrl('https://example.org/t?a=1&b=2', 'code with spaces'))
|
||||
assert.equal(url.searchParams.get('path'), 'https://example.org/t?a=1&b=2')
|
||||
assert.equal(url.searchParams.get('AuthCode'), 'code with spaces')
|
||||
assert.equal(ehThumbnailUrl('', 'x'), '')
|
||||
})
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
// Nginx 从 /asserts/index/ 提供桌面端静态文件;部署文件名保持固定,
|
||||
// 以便根页面和现有静态路由无需随每次构建改名。
|
||||
base: '/asserts/index/',
|
||||
assetsDir: '',
|
||||
plugins: [vue()],
|
||||
server:{
|
||||
host: '0.0.0.0'
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'index.js',
|
||||
chunkFileNames: '[name].js',
|
||||
assetFileNames: '[name][extname]'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user