Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ea4bfcdbc | ||
|
|
dfee73b3f9 | ||
|
|
c9af72dc85 | ||
|
|
dc39e468bb | ||
|
|
3a39677bb3 | ||
|
|
abb1cdb952 | ||
|
|
169925c197 | ||
|
|
6ddd383294 | ||
|
|
6874ba442c | ||
|
|
b87703ec00 | ||
|
|
040811ec5b | ||
|
|
47ccefb2d2 | ||
|
|
8ba8a5d125 | ||
|
|
aee27de0c0 | ||
|
|
fb925886c5 | ||
|
|
f4b94a66b4 | ||
|
|
75d5b7ec67 | ||
|
|
61c4dbc8e8 | ||
|
|
f1270e3ebb |
+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?
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||||
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" href="/asserts/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||||
<title>Vite + Vue</title>
|
<title>LionWebsite</title>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Generated
+1720
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "lionwebsiteformobile",
|
||||||
|
"private": true,
|
||||||
|
"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.15",
|
||||||
|
"qs": "^6.11.0",
|
||||||
|
"vue": "^3.2.37",
|
||||||
|
"vuex": "^4.0.2",
|
||||||
|
"lionwebsite-frontend-shared": "file:../lionwebsite-frontend-shared"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "6.0.9",
|
||||||
|
"vite": "8.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-14
@@ -1,23 +1,71 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import Side from "./components/Side.vue";
|
import {computed, onBeforeUnmount, onMounted} from "vue";
|
||||||
import DashBoard from "./components/DashBoard.vue";
|
import store from "./store/index.js";
|
||||||
|
import TaskView from "./components/TaskView.vue";
|
||||||
|
import SearchView from "./components/SearchView.vue";
|
||||||
|
import SettingsView from "./components/SettingsView.vue";
|
||||||
|
import TabBar from "./components/TabBar.vue";
|
||||||
|
import AuthScreen from "./components/AuthScreen.vue";
|
||||||
|
import TaskDetailSheet from "./components/TaskDetailSheet.vue";
|
||||||
|
import OnlineReader from "./components/OnlineReader.vue";
|
||||||
|
|
||||||
|
const activeTab = computed(() => store.state.activeTab);
|
||||||
|
|
||||||
|
function isSystemDark() {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme is a single class on <html>; colours come from the CSS variables.
|
||||||
|
function adjustForStyle() {
|
||||||
|
const stored = localStorage.getItem("darkConfig");
|
||||||
|
if (stored !== null) {
|
||||||
|
const darkConfig = JSON.parse(stored);
|
||||||
|
document.documentElement.classList.toggle("dark", Boolean(darkConfig.followSystem) && isSystemDark());
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resumeProgress = () => {
|
||||||
|
if (store.state.isAuth && document.visibilityState === "visible") store.dispatch("initWebsocket");
|
||||||
|
};
|
||||||
|
const pauseProgress = () => store.dispatch("disconnectWebsocket");
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
adjustForStyle();
|
||||||
|
store.state.lengthPerPage = Number(localStorage.getItem("lengthPerPage")) || 30;
|
||||||
|
store.commit("_setCategory", localStorage.getItem("category") ?? "myDownload");
|
||||||
|
store.commit("_setSortType", localStorage.getItem("sortType") ?? "createTime");
|
||||||
|
store.commit("_setGalleryNameType", localStorage.getItem("galleryNameType") ?? "shortName");
|
||||||
|
|
||||||
|
const auth = localStorage.getItem("auth");
|
||||||
|
if (auth !== null) store.dispatch("validate", auth);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<el-container>
|
<main class="app-views">
|
||||||
<DashBoard/>
|
<TaskView v-show="activeTab === 'tasks'"/>
|
||||||
<main>
|
<SearchView v-show="activeTab === 'search'"/>
|
||||||
<Side/>
|
<SettingsView v-show="activeTab === 'settings'"/>
|
||||||
</main>
|
</main>
|
||||||
</el-container>
|
<TabBar/>
|
||||||
|
<TaskDetailSheet/>
|
||||||
|
<OnlineReader/>
|
||||||
|
<AuthScreen/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
.app{
|
|
||||||
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
|
||||||
|
const AuthCode = ref("");
|
||||||
|
const isRemember = ref(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 class="auth-screen" v-show="!store.state.loadComplete">
|
||||||
|
<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">验证</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,594 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-drawer class="DashBoard" v-model="store.state.isShowHistory" size="90%" direction="ltr">
|
|
||||||
<span>E站额度本周已用:{{weekUsed.weekUsedAmount}} <br>上次重置时间:{{weekUsed.lastResetAmountTime}}</span><br>
|
|
||||||
<el-button @click="queryWeekUsedAmount">查询用量</el-button>
|
|
||||||
<hr>
|
|
||||||
<el-row>
|
|
||||||
<el-col>
|
|
||||||
<el-input style="width: 200px;" v-model="param" v-if="type !== 'tag'">
|
|
||||||
<template #prepend>
|
|
||||||
链接:
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-button @click="queryRemoteTask" v-show="type === 'link'">解析链接</el-button>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
<hr>
|
|
||||||
<el-button @click="isQuerying = true">里站搜索</el-button>
|
|
||||||
<el-button @click="isAlterAuthCode = true">修改授权码</el-button>
|
|
||||||
<el-button @click="deleteAuthCode">删除本地授权码</el-button>
|
|
||||||
<el-button @click="isConfig = true">配置</el-button><br>
|
|
||||||
<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>
|
|
||||||
<br>
|
|
||||||
<el-button v-if="isLion" @click="resetUndone">重置任务</el-button>
|
|
||||||
<div v-show="thumbnailGallery.url !== undefined">
|
|
||||||
<span>
|
|
||||||
{{thumbnailGallery.shortName}}
|
|
||||||
</span><br>
|
|
||||||
<picture>
|
|
||||||
<el-image :src="thumbnailGallery.url" :preview-src-list="[thumbnailGallery.images[0],]" :initial-index="0" class="preview"
|
|
||||||
style="height: 30vh" fit="contain"/>
|
|
||||||
</picture>
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
|
|
||||||
<el-dialog title="查询本子" v-model="chosenGallery" width="100%">
|
|
||||||
<el-image v-if='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">
|
|
||||||
<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 v-if="chosenGallery.availableResolution">
|
|
||||||
标签:<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="showThumbnail(chosenGallery)">查看封面图</el-button>
|
|
||||||
<el-button @click="deleteGallery">删除</el-button>
|
|
||||||
</tr>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<HentaiSearch :is-querying="isQuerying" @close="isQuerying = false"></HentaiSearch>
|
|
||||||
|
|
||||||
<el-dialog title="修改授权码" v-model="isAlterAuthCode" width="100%">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<el-dialog title="配置" v-model="isConfig" width="100%">
|
|
||||||
<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">
|
|
||||||
</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: 100%">
|
|
||||||
<div style="text-align: center">
|
|
||||||
输入关键字:<el-input style="width: 150px" v-model="tagKeyWord"></el-input><br>
|
|
||||||
<el-button @click="tagKeyWord = ''">重置关键字</el-button>
|
|
||||||
<el-button @click="pullNewTag">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table :data="tags" height="350px" stripe>
|
|
||||||
<el-table-column prop="id" label="id" width="50px" sortable/>
|
|
||||||
<el-table-column prop="tag" label="标签" width="150px"/>
|
|
||||||
<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" style="width: 100px"></el-input>
|
|
||||||
<el-button @click="postTag">创建标签</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<div class="DashBoard" v-show="!loadComplete">
|
|
||||||
<div class="validate">
|
|
||||||
<el-input v-model="AuthCode" placeholder="请输入授权码" />
|
|
||||||
<el-checkbox v-model="isRemember" >是否记住授权码</el-checkbox><br>
|
|
||||||
<el-button @click="validate" type="primary" @keydown.enter="validate">验证</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import store from "../store";
|
|
||||||
import {computed, ref, onMounted} from "vue";
|
|
||||||
import {ElMessage} from "element-plus"
|
|
||||||
import axios from "axios";
|
|
||||||
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("furry yaoi")
|
|
||||||
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:tag.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", "")
|
|
||||||
store.commit("_searchLocalByTag", [''])
|
|
||||||
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 openPanel(){
|
|
||||||
store.commit("_openHistoryPanel")
|
|
||||||
}
|
|
||||||
function onlineGalleryReader(gid){
|
|
||||||
store.dispatch("queryOnlineLinks", gid)
|
|
||||||
}
|
|
||||||
|
|
||||||
//重新给节点发送未完成任务
|
|
||||||
function resetUndone(){
|
|
||||||
store.dispatch("resetUndone").then()
|
|
||||||
}
|
|
||||||
function deleteAuthCode(){
|
|
||||||
localStorage.removeItem('auth')
|
|
||||||
ElMessage("删除授权码完成")
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleStyle(){
|
|
||||||
if(isDark.value)
|
|
||||||
dark()
|
|
||||||
else
|
|
||||||
light()
|
|
||||||
}
|
|
||||||
|
|
||||||
//显示缩略图
|
|
||||||
function showThumbnail(gallery){
|
|
||||||
store.commit("_changeThumbnailGallery", gallery)
|
|
||||||
setTimeout(() => {document.querySelector(".preview > img").click()}, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
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)
|
|
||||||
let galleries = document.querySelectorAll("#gallery");
|
|
||||||
for(let gallery of galleries){
|
|
||||||
gallery.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")
|
|
||||||
let galleries = document.querySelectorAll("#gallery");
|
|
||||||
if(galleries !== undefined && galleries.length !== 0)
|
|
||||||
for(let gallery of galleries){
|
|
||||||
gallery.style.setProperty("background-color", "FloralWhite")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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>
|
|
||||||
.DashBoard{
|
|
||||||
width: auto;
|
|
||||||
height: 90vh;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validate{
|
|
||||||
width: 50vw;
|
|
||||||
display: block;
|
|
||||||
padding-top: 200px;
|
|
||||||
padding-left: 20vw;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.el-input{
|
|
||||||
width: 25vw;
|
|
||||||
}
|
|
||||||
.el-dialogClass .el-dialog__body{
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,149 +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()
|
|
||||||
let isLoading = 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(" ", "+")
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + tempParam)
|
|
||||||
.then((res) => {
|
|
||||||
isLoading = false
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}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")
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustGalleryName(name, length) {
|
|
||||||
let truncated = '';
|
|
||||||
let bytesCount = 0;
|
|
||||||
|
|
||||||
for (const char of name) {
|
|
||||||
const charCode = char.charCodeAt(0);
|
|
||||||
const byteLength = charCode < 0x80 ? 1 : charCode < 0x800 ? 2 : 3;
|
|
||||||
|
|
||||||
if (bytesCount + byteLength <= length) {
|
|
||||||
truncated += char;
|
|
||||||
bytesCount += byteLength;
|
|
||||||
} else {
|
|
||||||
truncated += "..."
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return truncated;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<el-dialog title="里站搜索" v-model="isShowUp" top="0" style="margin-bottom: 0" fullscreen class="el-dialogClass" @close="close">
|
|
||||||
<div style="text-align: center">
|
|
||||||
<el-input v-model="keyword" style="width: 50vw"></el-input> <el-button @click="queryGalleries(null)">查询</el-button> <div id="loading" v-if="isLoading"/>
|
|
||||||
</div>
|
|
||||||
<el-scrollbar height="75vh" ref="scrollBar">
|
|
||||||
<div style="height: 20vh; width: 100%; border-radius: 5px; padding-bottom: 2vh" 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:20vh;width:35vw;float: left;"
|
|
||||||
fit="contain"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<div style="font: bold 16px semi-condensed; height: 25vh; padding-left: 40vw;">
|
|
||||||
<span>{{adjustGalleryName(gallery.name, 80)}}</span><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="position: relative; margin-left: 65%;" @click="param=gallery.link; queryRemoteTask()" type="primary">下载</el-button>
|
|
||||||
</div>
|
|
||||||
</div><br>
|
|
||||||
</el-scrollbar>
|
|
||||||
<div style="padding-top: 10px; 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: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialogClass .el-dialog__body{
|
|
||||||
padding-left: 0;
|
|
||||||
padding-right: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,62 +1,84 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {computed, ref, watch} from "vue";
|
import {computed, ref, watch, onBeforeUnmount} from "vue";
|
||||||
|
import {startImages, settleImage, retryImage} from "lionwebsite-frontend-shared";
|
||||||
import store from "../store/index.js";
|
import store from "../store/index.js";
|
||||||
let props = defineProps(['currentGallery', 'isOnlineReading'])
|
|
||||||
let emit = defineEmits(['close'])
|
|
||||||
let isShowUp = ref()
|
|
||||||
let onlineReadingScrollbar = ref()
|
let onlineReadingScrollbar = ref()
|
||||||
let links = ref()
|
let links = ref()
|
||||||
let index = ref(0)
|
let index = ref(0) //下标
|
||||||
let temp_index = ref(0) //用于跳转
|
let page = ref(0) //页数 下标+1=页数 用于跳转
|
||||||
|
|
||||||
|
let imagesForLoading = ref([])
|
||||||
|
let isReading = ref(false)
|
||||||
|
|
||||||
let max = ref(0)
|
let max = ref(0)
|
||||||
let current_page = 0
|
let current_page = 0
|
||||||
let lengthPerPage = computed(() => {
|
let lengthPerPage = computed(() => {
|
||||||
return store.state.lengthPerPage
|
return Math.max(1, Number(store.state.lengthPerPage) || 10)
|
||||||
|
})
|
||||||
|
let readingGallery = computed(() => {
|
||||||
|
return store.state.readingGallery
|
||||||
|
})
|
||||||
|
watch(() => store.state.isReading, (isReadingVal) => {
|
||||||
|
if(isReadingVal && !isReading.value) {
|
||||||
|
alterPage()
|
||||||
|
isReading.value = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(props, (props)=>{
|
//切换
|
||||||
isShowUp.value = props.isOnlineReading
|
|
||||||
alterPage()
|
|
||||||
})
|
|
||||||
|
|
||||||
//切换本子
|
|
||||||
function alterPage(){
|
function alterPage(){
|
||||||
if(props.currentGallery.images.length > lengthPerPage.value){
|
if(readingGallery.value.images.length > lengthPerPage.value){
|
||||||
links.value = props.currentGallery.images.slice(0, lengthPerPage.value)
|
links.value = readingGallery.value.images.slice(0, lengthPerPage.value)
|
||||||
max.value = Math.ceil(props.currentGallery.images.length / lengthPerPage.value)
|
max.value = Math.ceil(readingGallery.value.images.length / lengthPerPage.value)
|
||||||
}else{
|
}else{
|
||||||
links.value = props.currentGallery.images
|
links.value = readingGallery.value.images
|
||||||
max.value = 0
|
max.value = 0
|
||||||
}
|
}
|
||||||
index.value = 0
|
index.value = 0
|
||||||
temp_index.value = 1
|
page.value = 1
|
||||||
|
imagesForLoading.value = startImages(links.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
//跳转到对应页数
|
//跳转到对应页数
|
||||||
function jump(targetIndex){
|
function jump(targetIndex){
|
||||||
links.value = props.currentGallery.images.slice(targetIndex * lengthPerPage.value, (targetIndex + 1) * lengthPerPage.value)
|
if(targetIndex < 0 || targetIndex >= Math.ceil(readingGallery.value.images.length / lengthPerPage.value)) return
|
||||||
|
clearPageSwitchTimer()
|
||||||
|
links.value = readingGallery.value.images.slice(targetIndex * lengthPerPage.value, (targetIndex + 1) * lengthPerPage.value)
|
||||||
index.value = targetIndex
|
index.value = targetIndex
|
||||||
temp_index.value = targetIndex + 1
|
page.value = targetIndex + 1
|
||||||
|
imagesForLoading.value = startImages(links.value)
|
||||||
onlineReadingScrollbar.value.setScrollTop(0)
|
onlineReadingScrollbar.value.setScrollTop(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function imageResult(item, failed = false){
|
||||||
|
settleImage(imagesForLoading.value, links.value, item, failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageSwitchTimer
|
||||||
|
function clearPageSwitchTimer(){
|
||||||
|
clearInterval(pageSwitchTimer)
|
||||||
|
pageSwitchTimer = undefined
|
||||||
|
}
|
||||||
|
onBeforeUnmount(clearPageSwitchTimer)
|
||||||
|
|
||||||
function closeDialog(){
|
function closeDialog(){
|
||||||
|
clearPageSwitchTimer()
|
||||||
|
imagesForLoading.value = []
|
||||||
onlineReadingScrollbar.value.setScrollTop(0)
|
onlineReadingScrollbar.value.setScrollTop(0)
|
||||||
emit('close')
|
isReading.value = false
|
||||||
|
store.commit("_closeReader")
|
||||||
}
|
}
|
||||||
|
|
||||||
function switch_page(target_page){
|
function switch_page(target_page){
|
||||||
console.log(current_page, target_page)
|
|
||||||
//上一页
|
//上一页
|
||||||
if(target_page > (current_page + 1) && current_page === 0 && index.value > 0) {
|
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()
|
document.querySelector("span.el-image-viewer__btn.el-image-viewer__close").click()
|
||||||
jump(index.value - 1)
|
jump(index.value - 1)
|
||||||
onlineReadingScrollbar.value.setScrollTop(onlineReadingScrollbar.value.wrapRef.scrollHeight)
|
onlineReadingScrollbar.value.setScrollTop(onlineReadingScrollbar.value.wrapRef.scrollHeight)
|
||||||
let top = 0
|
let top = 0
|
||||||
let timer = setInterval(() => {
|
pageSwitchTimer = setInterval(() => {
|
||||||
if(onlineReadingScrollbar.value.scrollTop === top){
|
if(onlineReadingScrollbar.value.scrollTop === top){
|
||||||
clearInterval(timer)
|
clearPageSwitchTimer()
|
||||||
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:last-child > img").click()
|
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:last-child > img").click()
|
||||||
}
|
}
|
||||||
top = onlineReadingScrollbar.value.scrollTop
|
top = onlineReadingScrollbar.value.scrollTop
|
||||||
@@ -65,8 +87,7 @@ function switch_page(target_page){
|
|||||||
|
|
||||||
|
|
||||||
//下一页
|
//下一页
|
||||||
}else if(target_page === 0 && current_page === lengthPerPage.value - 1 && index.value < max.value){
|
}else if(target_page === 0 && current_page === lengthPerPage.value - 1 && index.value < max.value - 1){
|
||||||
console.log("下一页")
|
|
||||||
jump(index.value + 1)
|
jump(index.value + 1)
|
||||||
current_page = 0
|
current_page = 0
|
||||||
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:nth-child(1) > img").click()
|
document.querySelector("div.el-scrollbar__wrap.el-scrollbar__wrap--hidden-default > div > div:nth-child(1) > img").click()
|
||||||
@@ -82,17 +103,22 @@ function set_current_page(page){
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog v-model="isShowUp" @close="closeDialog" width="100%" top="0" fullscreen >
|
<el-dialog v-model="isReading" @close="closeDialog" width="100%" top="0" fullscreen>
|
||||||
<template #header style="padding-bottom: 0">
|
<template #header style="padding-bottom: 0">
|
||||||
在线预览: {{currentGallery.name}} 页数:{{currentGallery.pages}}<br>
|
在线预览: {{readingGallery.name}}<br>
|
||||||
|
页数:{{readingGallery.pages}}<br>
|
||||||
<div style="font-size: 3vh; display: inline" v-if="max > 0">
|
<div style="font-size: 3vh; display: inline" v-if="max > 0">
|
||||||
{{ index + 1 }} - {{ (index) * lengthPerPage + 1 }} ~
|
{{ index + 1 }} - {{ (index) * lengthPerPage + 1 }} ~
|
||||||
{{ (index + 1) * lengthPerPage + 1 > currentGallery.images.length ? currentGallery.images.length : (index + 1) * lengthPerPage }}
|
{{ (index + 1) * lengthPerPage + 1 > readingGallery.images.length ? readingGallery.images.length : (index + 1) * lengthPerPage }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-scrollbar height="75vh" ref="onlineReadingScrollbar">
|
<el-scrollbar height="75vh" ref="onlineReadingScrollbar">
|
||||||
<el-image v-for="(link, i) in links" :src="link" :style="{'width': 'auto', 'text-align': 'center', 'background-color': 'ghostwhite'}"
|
<div v-for="(item, i) in imagesForLoading" :key="item.url + '-' + item.attempt" style="display: inline-block; text-align: center">
|
||||||
:preview-src-list="links" :initial-index="i" @switch="switch_page" @show="set_current_page(i)"/>
|
<el-image :src="item.src" :style="{'width': 'auto', 'text-align': 'center', 'background-color': 'ghostwhite'}"
|
||||||
|
:preview-src-list="links" :initial-index="i" @switch="switch_page" @show="set_current_page(i)" loading="lazy" @load="imageResult(item)" @error="imageResult(item, true)"/>
|
||||||
|
<div v-if="item.failed" role="alert">图片加载失败 <el-button size="small" @click="retryImage(item)">重试</el-button></div> <br>
|
||||||
|
{{lengthPerPage * index + i + 1}}
|
||||||
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
|
|
||||||
<!--五页以下-->
|
<!--五页以下-->
|
||||||
@@ -108,10 +134,10 @@ function set_current_page(page){
|
|||||||
<div v-if="max >= 4" style="text-align: center">
|
<div v-if="max >= 4" style="text-align: center">
|
||||||
<el-button @click="jump(index - 1)" :disabled="index === 0" size="small">上一页</el-button>
|
<el-button @click="jump(index - 1)" :disabled="index === 0" size="small">上一页</el-button>
|
||||||
1 <
|
1 <
|
||||||
<el-input-number v-model="temp_index" :min="1" :max="max"/>
|
<el-input-number v-model="page" :min="1" :max="max"/>
|
||||||
< {{max}}
|
< {{max}}
|
||||||
<el-button @click="jump(index + 1)" :disabled="index === max - 1" size="small">下一页</el-button>
|
<el-button @click="jump(index + 1)" :disabled="index === max - 1" size="small">下一页</el-button>
|
||||||
<el-button @click="jump(temp_index - 1)" size="large">跳转</el-button>
|
<el-button @click="jump(page - 1)" size="large">跳转</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<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 {fetchGalleries, validateLink} from "lionwebsite-frontend-shared";
|
||||||
|
import {ehThumbnailUrl} from "lionwebsite-frontend-shared";
|
||||||
|
|
||||||
|
const mode = ref("local");
|
||||||
|
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 searchResults = computed(() => store.getters.searchResults);
|
||||||
|
const searchTotal = computed(() => store.state.searchTask.length);
|
||||||
|
const searchRemaining = computed(() =>
|
||||||
|
Math.max(0, searchTotal.value - store.state.visiblePages * store.state.length));
|
||||||
|
|
||||||
|
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 openTask(gallery) {
|
||||||
|
store.commit("_setDetailGallery", gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// direction 为 "next"/"prev" 时使用后端返回的翻页游标;href 为空表示新关键词。
|
||||||
|
function queryGalleries(href, direction = null) {
|
||||||
|
if (href == null && remoteKeyword.value.trim() === "") {
|
||||||
|
ElMessage("请输入搜索关键词")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remoteLoading.value = true
|
||||||
|
fetchGalleries(axios, {keyword: remoteKeyword.value, direction, href: href || ""})
|
||||||
|
.then(({galleries, pagination}) => {
|
||||||
|
remotePage.value = pagination
|
||||||
|
remoteGalleries.value.splice(0, remoteGalleries.value.length, ...galleries)
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
ElMessage({message: error.payload || error.message || "搜索失败", type: "error"})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
remoteLoading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteThumbnail(path) {
|
||||||
|
return ehThumbnailUrl(path, store.state.AuthCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand the remote gallery to the shared sheet so the 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);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view">
|
||||||
|
<header class="app-bar">
|
||||||
|
<h1>搜索</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="segmented" role="group" aria-label="搜索来源">
|
||||||
|
<button type="button" :aria-pressed="mode === 'local'" @click="mode = 'local'">本地</button>
|
||||||
|
<button type="button" :aria-pressed="mode === 'ehentai'" @click="mode = 'ehentai'">E站搜索</button>
|
||||||
|
<button type="button" :aria-pressed="mode === 'link'" @click="mode = 'link'">解析链接</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-scroll" v-show="mode === 'local'">
|
||||||
|
<div class="segmented is-sub" 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>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input v-model="localKeyword"
|
||||||
|
type="search"
|
||||||
|
:placeholder="localMode === 'keyword' ? '在我的下载里搜索' : '粘贴已下载任务的链接'"
|
||||||
|
@keydown.enter="runLocalSearch">
|
||||||
|
<button type="button" @click="runLocalSearch">搜索</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="store.state.isSearch">
|
||||||
|
<p class="hint">
|
||||||
|
匹配到 {{ searchTotal }} 项
|
||||||
|
<button type="button" class="hint-action" @click="clearLocalSearch">清除</button>
|
||||||
|
</p>
|
||||||
|
<TaskRow v-for="gallery in searchResults"
|
||||||
|
:key="gallery.gid"
|
||||||
|
:gallery="gallery"
|
||||||
|
:retrying="retryingGids.has(gallery.gid)"
|
||||||
|
@open="openTask"
|
||||||
|
@download="downloadTask"
|
||||||
|
@retry="retryGallery"/>
|
||||||
|
<button v-if="searchRemaining > 0" type="button" class="load-more"
|
||||||
|
@click="store.commit('_showMoreTasks')">
|
||||||
|
继续加载(还有 {{ searchRemaining }} 项)
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<p v-else class="empty-state">没有匹配的任务</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-scroll" v-show="mode === 'ehentai'">
|
||||||
|
<div class="search-bar">
|
||||||
|
<input v-model="remoteKeyword" type="search" placeholder="在 E站搜索画廊" @keydown.enter="queryGalleries(null)">
|
||||||
|
<button type="button" :disabled="remoteLoading" @click="queryGalleries(null)">
|
||||||
|
{{ remoteLoading ? "搜索中" : "搜索" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="hint" v-if="remoteGalleries.length">本页 {{ remoteGalleries.length }} 个结果</p>
|
||||||
|
|
||||||
|
<div class="remote-row" v-for="gallery in remoteGalleries" :key="gallery.link">
|
||||||
|
<button type="button" class="task-row-open" @click="submitRemote(gallery)">
|
||||||
|
<span class="task-thumb">
|
||||||
|
<img :src="remoteThumbnail(gallery.thumbnailUrl)" alt="" loading="lazy">
|
||||||
|
</span>
|
||||||
|
<span class="task-body">
|
||||||
|
<span class="task-title">{{ gallery.name }}</span>
|
||||||
|
<span class="task-meta">{{ gallery.page }} 页 · {{ gallery.type }} · {{ (gallery.uploadTime || "").split(" ")[0] }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="task-side task-side-muted" aria-label="在线看" @click="previewRemote(gallery)">在线看</button>
|
||||||
|
<button type="button" class="task-side task-side-act" aria-label="提交下载" @click="submitRemote(gallery)">提交</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!remoteGalleries.length && !remoteLoading" class="empty-state">没有搜索结果</p>
|
||||||
|
|
||||||
|
<div class="pager-row" v-if="remoteGalleries.length">
|
||||||
|
<button type="button" :disabled="remotePage.first === undefined" @click="queryGalleries(remotePage.first)">首页</button>
|
||||||
|
<button type="button" :disabled="remotePage.previous === undefined" @click="queryGalleries(remotePage.previous, 'prev')">上一页</button>
|
||||||
|
<button type="button" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.next, 'next')">下一页</button>
|
||||||
|
<button type="button" :disabled="remotePage.last === undefined" @click="queryGalleries(remotePage.last)">尾页</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-scroll" v-show="mode === 'link'">
|
||||||
|
<div class="search-bar">
|
||||||
|
<input v-model="linkParam" type="search" placeholder="粘贴 e-hentai 画廊链接" @keydown.enter="parseLink">
|
||||||
|
<button type="button" @click="parseLink">解析</button>
|
||||||
|
</div>
|
||||||
|
<p class="hint">解析成功后可直接选分辨率提交下载</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, onMounted, ref} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
|
||||||
|
const isAlterAuthCode = ref(false);
|
||||||
|
const newAuthCode = ref("");
|
||||||
|
const tempAuthCode = ref("");
|
||||||
|
const isConfig = ref(false);
|
||||||
|
const isDark = ref(false);
|
||||||
|
const darkConfig = ref({followSystem: false});
|
||||||
|
const lengthPerPage = ref(30);
|
||||||
|
const category = ref("myDownload");
|
||||||
|
const sortType = ref("createTime");
|
||||||
|
const galleryNameType = ref("shortName");
|
||||||
|
|
||||||
|
const realAuthCode = computed(() => store.state.AuthCode);
|
||||||
|
const weekUsed = computed(() => store.state.weekUsed);
|
||||||
|
const isAdmin = computed(() => store.state.isAdmin);
|
||||||
|
|
||||||
|
const connectionText = computed(() => ({
|
||||||
|
connected: "已连接",
|
||||||
|
connecting: "连接中",
|
||||||
|
reconnecting: "重连中",
|
||||||
|
disconnected: "已断开",
|
||||||
|
})[store.state.connectionStatus]);
|
||||||
|
|
||||||
|
function isSystemDark() {
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(dark) {
|
||||||
|
document.documentElement.classList.toggle("dark", dark);
|
||||||
|
isDark.value = dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDark(value) {
|
||||||
|
darkConfig.value.followSystem = false;
|
||||||
|
applyTheme(value);
|
||||||
|
localStorage.setItem("darkConfig", JSON.stringify(darkConfig.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFollowSystem(value) {
|
||||||
|
darkConfig.value.followSystem = value;
|
||||||
|
applyTheme(value && isSystemDark());
|
||||||
|
localStorage.setItem("darkConfig", JSON.stringify(darkConfig.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 deleteAuthCode() {
|
||||||
|
localStorage.removeItem("auth");
|
||||||
|
ElMessage("删除授权码完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfig() {
|
||||||
|
const length = Number(lengthPerPage.value);
|
||||||
|
if (!Number.isFinite(length) || length < 1 || length > 30) {
|
||||||
|
ElMessage("分页页数设置错误,范围1~30");
|
||||||
|
lengthPerPage.value = 30;
|
||||||
|
} else {
|
||||||
|
store.state.lengthPerPage = length;
|
||||||
|
localStorage.setItem("lengthPerPage", String(length));
|
||||||
|
}
|
||||||
|
localStorage.setItem("darkConfig", JSON.stringify(darkConfig.value));
|
||||||
|
localStorage.setItem("category", category.value);
|
||||||
|
localStorage.setItem("sortType", sortType.value);
|
||||||
|
localStorage.setItem("galleryNameType", galleryNameType.value);
|
||||||
|
|
||||||
|
store.commit("_setCategory", category.value);
|
||||||
|
store.commit("_setSortType", sortType.value);
|
||||||
|
store.commit("_setGalleryNameType", galleryNameType.value);
|
||||||
|
|
||||||
|
isConfig.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const stored = localStorage.getItem("darkConfig");
|
||||||
|
darkConfig.value = stored === null ? {followSystem: false} : JSON.parse(stored);
|
||||||
|
isDark.value = document.documentElement.classList.contains("dark");
|
||||||
|
lengthPerPage.value = store.state.lengthPerPage || 30;
|
||||||
|
category.value = store.state.category;
|
||||||
|
sortType.value = store.state.sortType;
|
||||||
|
galleryNameType.value = store.state.galleryNameType;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view">
|
||||||
|
<header class="app-bar">
|
||||||
|
<h1>设置</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="list-scroll settings-scroll">
|
||||||
|
<p class="group-label">本周用量</p>
|
||||||
|
<section class="group">
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">已用流量</span>
|
||||||
|
<span class="usage-value">
|
||||||
|
<strong>{{ weekUsed.weekUsedAmount ?? "—" }}</strong>
|
||||||
|
<small>上次重置 {{ weekUsed.lastResetAmountTime ?? "—" }}</small>
|
||||||
|
</span>
|
||||||
|
<button type="button" class="mini-button" @click="store.dispatch('loadWeekUsedAmount')">刷新</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="group-label">节点</p>
|
||||||
|
<section class="group">
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">进度连接</span>
|
||||||
|
<span class="connection" :class="{'is-connected': store.state.connectionStatus === 'connected'}">
|
||||||
|
<i class="connection-dot"></i>{{ connectionText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="setting-row is-button" @click="store.dispatch('reconnect')">
|
||||||
|
<span class="setting-label">重连节点</span>
|
||||||
|
<span class="chevron">›</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="group-label">账户</p>
|
||||||
|
<section class="group">
|
||||||
|
<button type="button" class="setting-row is-button" @click="isAlterAuthCode = true">
|
||||||
|
<span class="setting-label">修改授权码</span>
|
||||||
|
<span class="chevron">›</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="setting-row is-button" @click="deleteAuthCode">
|
||||||
|
<span class="setting-label">删除本地授权码</span>
|
||||||
|
<span class="chevron">›</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="isAdmin" type="button" class="setting-row is-button is-danger"
|
||||||
|
@click="store.dispatch('resetUndone')">
|
||||||
|
<span class="setting-label">重置未完成任务</span>
|
||||||
|
<span class="chevron">›</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="group-label">外观</p>
|
||||||
|
<section class="group">
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">夜间模式</span>
|
||||||
|
<el-switch v-model="isDark" @change="toggleDark"/>
|
||||||
|
</div>
|
||||||
|
<div class="setting-row">
|
||||||
|
<span class="setting-label">跟随系统</span>
|
||||||
|
<el-switch v-model="darkConfig.followSystem" @change="toggleFollowSystem"/>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="setting-row is-button" @click="isConfig = true">
|
||||||
|
<span class="setting-label">默认设置</span>
|
||||||
|
<span class="setting-value">{{ category === "myDownload" ? "我的下载" : category === "myCollect" ? "我的收藏" : "全部" }}</span>
|
||||||
|
<span class="chevron">›</span>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog title="修改授权码" v-model="isAlterAuthCode" width="100%">
|
||||||
|
<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-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="再次输入授权码">
|
||||||
|
<el-input v-model="tempAuthCode" show-password></el-input>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<el-dialog title="默认设置" v-model="isConfig" width="100%">
|
||||||
|
<el-form label-position="top" class="config-form">
|
||||||
|
<h4 class="section-title">在线预览</h4>
|
||||||
|
<el-form-item label="每页图片数量">
|
||||||
|
<el-input-number v-model="lengthPerPage" :min="1" :max="30" :step="1" style="width: 100%"/>
|
||||||
|
<span class="field-hint">1 – 30</span>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<h4 class="section-title">列表默认值</h4>
|
||||||
|
<el-form-item label="分类">
|
||||||
|
<el-select v-model="category" default-first-option style="width: 100%">
|
||||||
|
<el-option label="全部" value="total"/>
|
||||||
|
<el-option label="我的下载" value="myDownload"/>
|
||||||
|
<el-option label="我的收藏" value="myCollect"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序方式">
|
||||||
|
<el-select v-model="sortType" default-first-option style="width: 100%">
|
||||||
|
<el-option label="名字" value="name"/>
|
||||||
|
<el-option label="简洁名字" value="shortName"/>
|
||||||
|
<el-option label="任务创建时间" value="createTime"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="显示类型">
|
||||||
|
<el-select v-model="galleryNameType" default-first-option style="width: 100%">
|
||||||
|
<el-option label="名字" value="name"/>
|
||||||
|
<el-option label="简洁名字" value="shortName"/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button type="primary" @click="saveConfig">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="side">
|
|
||||||
<div v-show="loadComplete" class="load_complete">
|
|
||||||
|
|
||||||
<el-scrollbar max-height="80vh">
|
|
||||||
<div v-for="gallery in currentTasks" :style="{'height': '20vh', 'background': isDark() ? '': 'FloralWhite', 'border-radius': '1%',
|
|
||||||
'margin-bottom': '10px'}"
|
|
||||||
@click="viewInfo(gallery)" id="gallery">
|
|
||||||
<el-image :src="getGalleryThumb(gallery)"
|
|
||||||
style="height: 20vh; width: 35vw; float:left"
|
|
||||||
fit="contain"
|
|
||||||
loading="lazy"
|
|
||||||
></el-image>
|
|
||||||
<div style="font: bold 16px semi-condensed;">
|
|
||||||
{{adjustGalleryName(gallery.name, 95)}}<br>
|
|
||||||
页数:{{gallery.pages}}<br>
|
|
||||||
语言:{{gallery.language}}
|
|
||||||
<span v-if="gallery.status !== '下载完成'">
|
|
||||||
下载进度: {{gallery.progress}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
<el-col>
|
|
||||||
<el-row>
|
|
||||||
<el-button @click="store.state.isShowHistory = true">三</el-button>
|
|
||||||
<el-select style="width: 22vw" v-model="type">
|
|
||||||
<el-option value="keyword" label="关键字"/>
|
|
||||||
<el-option value="tag" label="标签"/>
|
|
||||||
</el-select>
|
|
||||||
<el-input style="width: 60vw" v-model="param" v-show="type === 'keyword'" placeholder="关键字搜索">
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="queryLocalTask">搜索</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-autocomplete v-model="param" :fetch-suggestions="generateQueryTag"
|
|
||||||
@select="handleQueryTagSelect" placeholder="检索标签"
|
|
||||||
style="width: 60vw" v-if="type === 'tag'">
|
|
||||||
<template #append>
|
|
||||||
<el-button :disabled="paramForTags.length === 0" @click="removeAllQueryTag">清空tag</el-button>
|
|
||||||
</template>
|
|
||||||
</el-autocomplete>
|
|
||||||
</el-row>
|
|
||||||
</el-col>
|
|
||||||
<el-tag v-for="tid in paramForTags" closable @close="removeQueryTag(tid)" size="large" style="display: inline-block" v-if="paramForTags.length !== 0">
|
|
||||||
{{store.state.tags.get(tid).tag}}
|
|
||||||
</el-tag>
|
|
||||||
|
|
||||||
<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: 165px">
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-dialog v-model="isEditingTag" title="编辑本子标签" width="100%">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<el-dialog v-model="isViewing" width="100%" top="0" style="padding: 0">
|
|
||||||
<div style="height: 20vh; font: bold 16px semi-expanded">
|
|
||||||
<el-image :src="getGalleryThumb(currentGallery)"
|
|
||||||
style="float: left; width: 40vw; height: 20vh" fit="contain"/>
|
|
||||||
{{adjustGalleryName(currentGallery.name, 150)}}
|
|
||||||
</div>
|
|
||||||
<div style="font: bold 20px semi-expanded">
|
|
||||||
页数:{{currentGallery.pages}} <br>
|
|
||||||
语言:{{currentGallery.language}} <br>
|
|
||||||
下载时间:{{currentGallery.createTimeDisplay}} <br>
|
|
||||||
大小:{{currentGallery.fileSize}} <br>
|
|
||||||
分辨率:{{currentGallery.resolution}} <br>
|
|
||||||
链接:<a :href="currentGallery.link">link</a> <br>
|
|
||||||
下载链接:<a :href="currentGallery.download">link</a><br>
|
|
||||||
标签:{{currentGallery.tag === '' ? '无': currentGallery.tag}} <br>
|
|
||||||
<span v-show="isLion">
|
|
||||||
downloader:{{currentGallery.downloader}}
|
|
||||||
</span>
|
|
||||||
<span v-if="currentGallery.download === undefined">
|
|
||||||
下载进度: {{currentGallery.progress}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="downloadTask(currentGallery.download)" :disabled="currentGallery.status !== '下载完成' || currentGallery.mode === 2"
|
|
||||||
size="large">下载</el-button>
|
|
||||||
<el-button @click="deleteGallery(currentGallery.gid)" :disabled="currentGallery.status !== '下载完成'"
|
|
||||||
size="large">删除</el-button>
|
|
||||||
<el-button @click="shareGallery({gid:currentGallery.gid, shortName:currentGallery.shortName + '.zip'})" v-if="isLion"
|
|
||||||
size="large">分享</el-button>
|
|
||||||
<el-button @click="changeGalleryCollect(currentGallery.gid, currentGallery.isCollect)" :disabled="currentGallery.status !== '下载完成'"
|
|
||||||
size="large">{{currentGallery.isCollect ? '取消收藏' : '收藏'}}</el-button>
|
|
||||||
<el-button @click="onlineGalleryReader" :disabled="currentGallery.status !== '下载完成' || currentGallery.mode === 1"
|
|
||||||
size="large">在线看</el-button>
|
|
||||||
<el-button @click="editGalleryTag(currentGallery)" :disabled="currentGallery.status !== '下载完成'"
|
|
||||||
size="large">编辑标签</el-button>
|
|
||||||
<el-button @click="updateGallery(currentGallery.link)" :disabled="currentGallery.status !== '下载完成'"
|
|
||||||
size="large">更新本子</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<OnlineReader :current-gallery="currentGallery" :isOnlineReading="isOnlineReading" @close="isOnlineReading = false"/>
|
|
||||||
</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 isViewing = 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 type = ref("keyword")
|
|
||||||
let param = ref("")
|
|
||||||
let paramForTags = ref([]) //tidS
|
|
||||||
|
|
||||||
|
|
||||||
let onlineReadingScrollbar = ref(null)
|
|
||||||
|
|
||||||
let loadComplete = computed(() => {
|
|
||||||
return store.state.loadComplete
|
|
||||||
})
|
|
||||||
|
|
||||||
let currentTasks = computed(() => {
|
|
||||||
return store.getters.currentTasks ? store.getters.currentTasks: null
|
|
||||||
})
|
|
||||||
|
|
||||||
//标签
|
|
||||||
let tags = computed(() => {
|
|
||||||
return store.state.tags
|
|
||||||
})
|
|
||||||
let newTag = ref("")
|
|
||||||
|
|
||||||
|
|
||||||
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"})
|
|
||||||
|
|
||||||
//翻页
|
|
||||||
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){
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 generateQueryTag(keyWord, cb){
|
|
||||||
let result = []
|
|
||||||
let hit = false //用于检测是否有重复标签
|
|
||||||
let skip //用于过滤已经有了的标签
|
|
||||||
for(const [key, tag] of store.state.tags){
|
|
||||||
skip = false
|
|
||||||
if(paramForTags.value.length !== 0) {
|
|
||||||
for (let id of paramForTags.value)
|
|
||||||
if (id === key) {
|
|
||||||
if (!hit && tag.tag === keyWord) //是否命中标签
|
|
||||||
hit = true
|
|
||||||
skip = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if(skip)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if(tag.tag.includes(keyWord))
|
|
||||||
result.push({value: tag.tag, tag:tag})
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queryLocalTask()
|
|
||||||
}
|
|
||||||
function removeAllQueryTag(){
|
|
||||||
paramForTags.value.splice(0)
|
|
||||||
queryLocalTask()
|
|
||||||
}
|
|
||||||
function handleQueryTagSelect(data) {
|
|
||||||
console.log(data)
|
|
||||||
paramForTags.value.push(data.tag.id)
|
|
||||||
param.value = ''
|
|
||||||
queryLocalTask()
|
|
||||||
}
|
|
||||||
function queryLocalTask(){
|
|
||||||
switch (type.value){
|
|
||||||
case "keyword":
|
|
||||||
store.commit("_searchLocalByKeyword", param.value)
|
|
||||||
break
|
|
||||||
case "tag":
|
|
||||||
store.commit("_searchLocalByTag", paramForTags.value)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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).then(() => {
|
|
||||||
isViewing.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function onlineGalleryReader(){
|
|
||||||
isOnlineReading.value = true;
|
|
||||||
}
|
|
||||||
function viewInfo(gallery){
|
|
||||||
currentGallery.value = gallery
|
|
||||||
isViewing.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 adjustGalleryName(name, length) {
|
|
||||||
let truncated = '';
|
|
||||||
let bytesCount = 0;
|
|
||||||
|
|
||||||
for (const char of name) {
|
|
||||||
const charCode = char.charCodeAt(0);
|
|
||||||
const byteLength = charCode < 0x80 ? 1 : charCode < 0x800 ? 2 : 3;
|
|
||||||
|
|
||||||
if (bytesCount + byteLength <= length) {
|
|
||||||
truncated += char;
|
|
||||||
bytesCount += byteLength;
|
|
||||||
} else {
|
|
||||||
truncated += "..."
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return truncated;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGalleryThumb(gallery){
|
|
||||||
if(gallery.status !== '下载完成')
|
|
||||||
return ''
|
|
||||||
|
|
||||||
if(gallery.mode === 2 || gallery.mode === 3)
|
|
||||||
return 'https://downloader.lionwebsite.xyz/GalleryManage/onlineImage/0?gid=' + gallery.gid
|
|
||||||
|
|
||||||
if(gallery.thumb_link !== undefined)
|
|
||||||
return 'https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?path=' + gallery.thumb_link
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDark(){
|
|
||||||
return document.querySelector('html').classList.contains('dark')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
span{
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.pageChanger{
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.el-select{
|
|
||||||
width: 135px;
|
|
||||||
}
|
|
||||||
.page{
|
|
||||||
display: inline-block;
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import store from "../store/index.js";
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{key: "tasks", label: "任务"},
|
||||||
|
{key: "search", label: "搜索"},
|
||||||
|
{key: "settings", label: "设置"},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav class="tab-bar" role="tablist" aria-label="主导航">
|
||||||
|
<button v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{'is-active': store.state.activeTab === tab.key}"
|
||||||
|
:aria-selected="store.state.activeTab === tab.key"
|
||||||
|
@click="store.commit('_setActiveTab', tab.key)">
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref, watch} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import {ehThumbnailUrl, pickDefaultResolution} from "lionwebsite-frontend-shared";
|
||||||
|
|
||||||
|
// Two flows share this sheet: a task already in the list, and a remote gallery
|
||||||
|
// that was resolved from a link or from the online search.
|
||||||
|
const isOpen = ref(false);
|
||||||
|
const mode = ref("task");
|
||||||
|
const task = ref(null);
|
||||||
|
const targetResolution = ref("");
|
||||||
|
|
||||||
|
const chosenGallery = computed(() => store.state.chosenGallery);
|
||||||
|
const detailGallery = computed(() => store.state.detailGallery);
|
||||||
|
|
||||||
|
watch(chosenGallery, (gallery) => {
|
||||||
|
if (gallery) {
|
||||||
|
mode.value = "remote";
|
||||||
|
targetResolution.value = pickDefaultResolution(gallery.availableResolution);
|
||||||
|
isOpen.value = true;
|
||||||
|
} else if (mode.value === "remote") {
|
||||||
|
// The remote decision was withdrawn elsewhere: close instead of leaving a
|
||||||
|
// stale task/remote mix behind the same sheet.
|
||||||
|
mode.value = "task";
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(detailGallery, (gallery) => {
|
||||||
|
if (gallery) {
|
||||||
|
mode.value = "task";
|
||||||
|
task.value = gallery;
|
||||||
|
isOpen.value = true;
|
||||||
|
} else if (mode.value === "task") {
|
||||||
|
// Only the task flow closes on an empty detail; clearing the detail while a
|
||||||
|
// remote gallery is on screen must not tear the remote sheet down.
|
||||||
|
task.value = null;
|
||||||
|
isOpen.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin-only fields come from the backend's isAdmin flag, not a hardcoded id.
|
||||||
|
const isAdmin = computed(() => store.state.isAdmin);
|
||||||
|
const isDone = computed(() => (task.value ? task.value.status === "下载完成" : false));
|
||||||
|
const isCollect = computed(() => Boolean(task.value && task.value.isCollect));
|
||||||
|
|
||||||
|
const resolutions = computed(() => {
|
||||||
|
const gallery = chosenGallery.value;
|
||||||
|
if (!gallery || !gallery.availableResolution) return [];
|
||||||
|
return Object.entries(gallery.availableResolution).map(([resolution, fileSize]) => ({
|
||||||
|
resolution,
|
||||||
|
fileSize,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// A gallery resolved through queryGalleryTask already has its thumb_link proxied
|
||||||
|
// by normalizeTask; only a raw upstream path (thumbnailUrl) needs the proxy here.
|
||||||
|
const remoteThumb = computed(() => {
|
||||||
|
const gallery = chosenGallery.value;
|
||||||
|
if (!gallery) return "";
|
||||||
|
return gallery.thumb_link || ehThumbnailUrl(gallery.thumbnailUrl, store.state.AuthCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
isOpen.value = false;
|
||||||
|
if (mode.value === "remote") store.commit("_setChosenGallery", {gallery: false});
|
||||||
|
else store.commit("_setDetailGallery", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTask() {
|
||||||
|
if (!task.value) return;
|
||||||
|
if (task.value.download) window.open(task.value.download);
|
||||||
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOnline(gallery) {
|
||||||
|
if (!gallery) return;
|
||||||
|
close();
|
||||||
|
store.dispatch("readOnlineGallery", gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCollect() {
|
||||||
|
if (!task.value) return;
|
||||||
|
const gallery = task.value;
|
||||||
|
gallery.isCollect = !gallery.isCollect;
|
||||||
|
if (gallery.isCollect) store.dispatch("collectGallery", gallery.gid);
|
||||||
|
else store.dispatch("disCollectGallery", gallery.gid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteTask() {
|
||||||
|
if (!task.value) return;
|
||||||
|
store.dispatch("deleteGallery", task.value.gid).then(close);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitTask() {
|
||||||
|
const gallery = chosenGallery.value;
|
||||||
|
if (!gallery) return;
|
||||||
|
if (targetResolution.value === "") {
|
||||||
|
ElMessage("请选择分辨率再提交");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.dispatch("postGalleryTask", {
|
||||||
|
link: gallery.link,
|
||||||
|
targetResolution: targetResolution.value,
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="sheet-layer" v-show="isOpen">
|
||||||
|
<button type="button" class="sheet-scrim" aria-label="关闭详情" @click="close"></button>
|
||||||
|
|
||||||
|
<section class="sheet" role="dialog" aria-modal="true" aria-label="任务详情">
|
||||||
|
<span class="sheet-grab" aria-hidden="true"></span>
|
||||||
|
|
||||||
|
<template v-if="mode === 'task' && task">
|
||||||
|
<div class="sheet-head">
|
||||||
|
<span class="sheet-thumb">
|
||||||
|
<img v-if="task.thumb_link" :src="task.thumb_link" alt="" loading="lazy">
|
||||||
|
</span>
|
||||||
|
<div class="sheet-heading">
|
||||||
|
<p class="sheet-title">{{ task.name }}</p>
|
||||||
|
<div class="sheet-sub">
|
||||||
|
<span class="status-pill" :class="task.status === '下载完成' ? 'is-done' : (task.status === '下载中' || task.status === '压缩中' ? 'is-running' : 'is-waiting')">
|
||||||
|
{{ task.status }}
|
||||||
|
</span>
|
||||||
|
<span>{{ task.pages }} 页 · {{ task.language }} · {{ task.fileSize }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress-line" v-if="task.progress && task.status !== '下载完成'">
|
||||||
|
<el-progress :percentage="parseInt(task.progress) || 0" :stroke-width="4" :show-text="false"/>
|
||||||
|
<span class="progress-value">{{ task.progress }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sheet-actions">
|
||||||
|
<button type="button" class="sheet-action is-primary" :disabled="!isDone" @click="downloadTask">下载</button>
|
||||||
|
<button type="button" class="sheet-action" @click="readOnline(task)">在线看</button>
|
||||||
|
<button type="button" class="sheet-action" :class="{'is-on': isCollect}" :disabled="!isDone" @click="toggleCollect">
|
||||||
|
{{ isCollect ? "已收藏" : "收藏" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="sheet-facts">
|
||||||
|
<dt>分辨率</dt>
|
||||||
|
<dd>{{ task.resolution || "—" }}</dd>
|
||||||
|
<dt>创建时间</dt>
|
||||||
|
<dd>{{ task.createTimeDisplay || "—" }}</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 class="sheet-foot">
|
||||||
|
<button type="button" class="sheet-button is-danger" :disabled="!isDone" @click="deleteTask">删除任务</button>
|
||||||
|
<button type="button" class="sheet-button" @click="close">关闭</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="mode === 'remote' && chosenGallery">
|
||||||
|
<div class="sheet-head">
|
||||||
|
<span class="sheet-thumb">
|
||||||
|
<img v-if="remoteThumb" :src="remoteThumb" alt="" loading="lazy">
|
||||||
|
</span>
|
||||||
|
<div class="sheet-heading">
|
||||||
|
<p class="sheet-title">{{ chosenGallery.name }}</p>
|
||||||
|
<div class="sheet-sub">
|
||||||
|
<span v-if="chosenGallery.status">{{ chosenGallery.status }}</span>
|
||||||
|
<span v-if="chosenGallery.pages">{{ chosenGallery.pages }} 页</span>
|
||||||
|
<span v-if="chosenGallery.fileSize">{{ chosenGallery.fileSize }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="resolutions.length">
|
||||||
|
<div class="sheet-actions is-single">
|
||||||
|
<button type="button" class="sheet-action" @click="readOnline(chosenGallery)">在线看</button>
|
||||||
|
</div>
|
||||||
|
<p class="sheet-section">目标分辨率</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">
|
||||||
|
{{ item.resolution }} · {{ item.fileSize }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="sheet-foot">
|
||||||
|
<button type="button" class="sheet-button" @click="close">取消</button>
|
||||||
|
<button type="button" class="sheet-button is-accent" @click="submitTask">提交下载</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="sheet-actions">
|
||||||
|
<button type="button" class="sheet-action is-primary" @click="downloadTask">下载文件</button>
|
||||||
|
<button type="button" class="sheet-action" @click="readOnline(chosenGallery)">在线看</button>
|
||||||
|
</div>
|
||||||
|
<div class="sheet-foot">
|
||||||
|
<button type="button" class="sheet-button" @click="close">关闭</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed} from "vue";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
gallery: {type: Object, required: true},
|
||||||
|
retrying: {type: Boolean, default: false},
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(["open", "download", "retry"]);
|
||||||
|
|
||||||
|
// Only a running download reports a percentage; everything else uses the status text.
|
||||||
|
const percent = computed(() => {
|
||||||
|
const gallery = props.gallery;
|
||||||
|
if (!gallery || gallery.status !== "下载中" || !gallery.pages) return null;
|
||||||
|
const done = Number(gallery.proceeding) || 0;
|
||||||
|
return Math.min(100, Math.max(0, Math.round((done / gallery.pages) * 100)));
|
||||||
|
});
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
return store.state.galleryNameType === "name" ? props.gallery.name : props.gallery.shortName;
|
||||||
|
});
|
||||||
|
|
||||||
|
// States arrive as Chinese labels from the backend; map them to pill colours.
|
||||||
|
const statusClass = computed(() => {
|
||||||
|
const status = props.gallery.status;
|
||||||
|
if (status === "下载完成") return "is-done";
|
||||||
|
if (status === "下载中" || status === "压缩中") return "is-running";
|
||||||
|
return "is-waiting";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="task-row">
|
||||||
|
<button type="button" class="task-row-open" @click="$emit('open', gallery)">
|
||||||
|
<span class="task-thumb">
|
||||||
|
<img v-if="gallery.thumb_link" :src="gallery.thumb_link" alt="" loading="lazy">
|
||||||
|
</span>
|
||||||
|
<span class="task-body">
|
||||||
|
<span class="task-title">{{ title }}</span>
|
||||||
|
<span class="task-meta">{{ gallery.pages }} 页 · {{ gallery.language }} · {{ gallery.fileSize }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="task-foot">
|
||||||
|
<span class="status-pill" :class="statusClass">{{ gallery.status }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span v-if="percent !== null" class="task-side task-side-num">{{ percent }}%</span>
|
||||||
|
<button v-else-if="gallery.status === '下载完成'"
|
||||||
|
type="button"
|
||||||
|
class="task-side task-side-act"
|
||||||
|
aria-label="下载文件"
|
||||||
|
@click="$emit('download', gallery)">下载</button>
|
||||||
|
<button v-else
|
||||||
|
type="button"
|
||||||
|
class="task-side task-side-muted"
|
||||||
|
:disabled="retrying"
|
||||||
|
aria-label="重试任务"
|
||||||
|
@click="$emit('retry', gallery)">{{ retrying ? "重试中" : "重试" }}</button>
|
||||||
|
|
||||||
|
<span v-if="percent !== null" class="task-row-edge"><i :style="{width: percent + '%'}"></i></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<script setup>
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import store from "../store/index.js";
|
||||||
|
import TaskRow from "./TaskRow.vue";
|
||||||
|
|
||||||
|
const retryingGids = ref(new Set());
|
||||||
|
const refreshing = ref(false);
|
||||||
|
|
||||||
|
const tasks = computed(() => store.getters.visibleTasks);
|
||||||
|
const total = computed(() => store.getters.taskTotal);
|
||||||
|
const remaining = computed(() => store.getters.taskRemaining);
|
||||||
|
|
||||||
|
const connectionShort = computed(() => ({
|
||||||
|
connected: "已连接",
|
||||||
|
connecting: "连接中",
|
||||||
|
reconnecting: "重连中",
|
||||||
|
disconnected: "已断开",
|
||||||
|
})[store.state.connectionStatus]);
|
||||||
|
|
||||||
|
const isConnected = computed(() => store.state.connectionStatus === "connected");
|
||||||
|
|
||||||
|
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 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.getters.categoryGallery || []).forEach(task => {
|
||||||
|
if (task.downloaderName && !seen.has(task.downloader))
|
||||||
|
seen.set(task.downloader, task.downloaderName);
|
||||||
|
});
|
||||||
|
return [...seen].map(([id, name]) => ({id, name}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const emptyText = computed(() => {
|
||||||
|
if (store.state.category === "myDownload") return "您未下载过";
|
||||||
|
if (store.state.category === "myCollect") return "您未收藏过";
|
||||||
|
return "暂无任务";
|
||||||
|
});
|
||||||
|
|
||||||
|
function openTask(gallery) {
|
||||||
|
store.commit("_setDetailGallery", gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMore() {
|
||||||
|
store.commit("_showMoreTasks");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTasks() {
|
||||||
|
if (refreshing.value) return;
|
||||||
|
refreshing.value = true;
|
||||||
|
try {
|
||||||
|
await store.dispatch("updateGalleryTasks");
|
||||||
|
} finally {
|
||||||
|
refreshing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="view">
|
||||||
|
<header class="app-bar">
|
||||||
|
<h1>任务</h1>
|
||||||
|
<span class="app-bar-count">{{ total }} 项</span>
|
||||||
|
<span class="app-bar-spacer"></span>
|
||||||
|
<span class="connection" :class="{'is-connected': isConnected}" role="status">
|
||||||
|
<i class="connection-dot"></i>{{ connectionShort }}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="filter-bar" v-if="store.state.username !== 'test'">
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">范围</span>
|
||||||
|
<el-select v-model="category" size="small" class="filter-select filter-select-range">
|
||||||
|
<el-option value="myDownload" label="我的下载"/>
|
||||||
|
<el-option value="myCollect" label="我的收藏"/>
|
||||||
|
<el-option value="total" label="全部"/>
|
||||||
|
</el-select>
|
||||||
|
</label>
|
||||||
|
<label class="filter">
|
||||||
|
<span class="filter-label">排序</span>
|
||||||
|
<el-select v-model="sortType" size="small" class="filter-select filter-select-sort">
|
||||||
|
<el-option value="createTime" label="创建时间"/>
|
||||||
|
<el-option value="name" label="名字"/>
|
||||||
|
<el-option value="shortName" label="简洁名"/>
|
||||||
|
</el-select>
|
||||||
|
</label>
|
||||||
|
<label class="filter" v-if="isAdmin">
|
||||||
|
<span class="filter-label">下载人</span>
|
||||||
|
<el-select v-model="downloaderFilter" size="small"
|
||||||
|
class="filter-select filter-select-downloader"
|
||||||
|
clearable placeholder="全部">
|
||||||
|
<el-option v-for="item in downloaderOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:value="item.id"
|
||||||
|
:label="item.name"/>
|
||||||
|
</el-select>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="chip-button" :disabled="refreshing" @click="refreshTasks">
|
||||||
|
{{ refreshing ? "刷新中" : "刷新" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-scroll">
|
||||||
|
<p v-if="tasks.length === 0" class="empty-state">{{ emptyText }}</p>
|
||||||
|
<TaskRow v-for="gallery in tasks"
|
||||||
|
:key="gallery.gid"
|
||||||
|
:gallery="gallery"
|
||||||
|
:retrying="retryingGids.has(gallery.gid)"
|
||||||
|
@open="openTask"
|
||||||
|
@download="downloadTask"
|
||||||
|
@retry="retryGallery"/>
|
||||||
|
<button v-if="remaining > 0" type="button" class="load-more" @click="showMore">
|
||||||
|
继续加载(还有 {{ remaining }} 项)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+11
-2
@@ -1,8 +1,17 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import {
|
||||||
|
ElButton, ElCheckbox, ElDialog, ElForm, ElFormItem, ElImage, ElInput,
|
||||||
|
ElInputNumber, ElOption, ElProgress, ElScrollbar, ElSelect, ElSwitch,
|
||||||
|
} from "element-plus"
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import element from "element-plus"
|
|
||||||
import "./reset.css"
|
import "./reset.css"
|
||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
import "./styles/theme.css"
|
||||||
|
|
||||||
createApp(App).use(element).mount('#app')
|
// 只注册本仓库用到的组件,避免把整个 Element Plus 打进入口包。
|
||||||
|
const app = createApp(App)
|
||||||
|
for (const component of [ElButton, ElCheckbox, ElDialog, ElForm, ElFormItem, ElImage,
|
||||||
|
ElInput, ElInputNumber, ElOption, ElProgress, ElScrollbar, ElSelect, ElSwitch])
|
||||||
|
app.component(component.name, component)
|
||||||
|
app.mount('#app')
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
+15
-734
@@ -2,740 +2,21 @@ import vuex from "vuex"
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import {ElMessage} from "element-plus"
|
import {ElMessage} from "element-plus"
|
||||||
import qs from "qs"
|
import qs from "qs"
|
||||||
const BaseUrl = "https://downloader.lionwebsite.xyz/"
|
import {createGalleryStore} from "lionwebsite-frontend-shared"
|
||||||
const GalleryManageUrl = BaseUrl + "GalleryManage"
|
|
||||||
|
|
||||||
const actions = {
|
// Axios 全局错误处理
|
||||||
updateGalleryTasks(context, type){
|
axios.interceptors.response.use(
|
||||||
axios.get(GalleryManageUrl, {
|
response => response,
|
||||||
params:{
|
error => {
|
||||||
type,
|
ElMessage({message: "网络请求失败: " + (error.message || ""), type: "error"})
|
||||||
AuthCode: state.AuthCode
|
return Promise.reject(error)
|
||||||
}
|
|
||||||
}).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})
|
|
||||||
}
|
|
||||||
|
|
||||||
else if(type === 'undone') {
|
|
||||||
context.dispatch("updateGalleryTasks", "all").then()
|
|
||||||
clearInterval(state.galleryRefreshTimer)
|
|
||||||
state.galleryRefreshTimer = 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
postGalleryTask(context, data){
|
|
||||||
axios.post(GalleryManageUrl, qs.stringify({
|
|
||||||
AuthCode: 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)
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
if(res.data.data)
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
else
|
|
||||||
ElMessage("提交失败")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
queryGalleryTask(context, link){
|
|
||||||
axios.get(GalleryManageUrl, {
|
|
||||||
params:{
|
|
||||||
param: link,
|
|
||||||
type:'link',
|
|
||||||
AuthCode: state.AuthCode
|
|
||||||
}
|
|
||||||
}).then((res) => {
|
|
||||||
if(res.data.result === 'success')
|
|
||||||
context.commit("_setChosenGallery", {gallery: JSON.parse(res.data.data)})
|
|
||||||
|
|
||||||
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){
|
|
||||||
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))
|
|
||||||
|
|
||||||
//手机不需要设置宽度
|
|
||||||
}
|
|
||||||
else
|
|
||||||
context.commit("_unAuthed")
|
|
||||||
})
|
|
||||||
},
|
|
||||||
loadWeekUsedAmount(context){
|
|
||||||
axios.get(GalleryManageUrl + "/weekUsedAmount", {
|
|
||||||
params: {
|
|
||||||
AuthCode: 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
|
|
||||||
})).then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
if(res.data.result === 'success')
|
|
||||||
context.commit("_collectGallery", gid)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
disCollectGallery(context, gid){
|
|
||||||
axios.post(GalleryManageUrl + "/disCollect?" + qs.stringify(
|
|
||||||
{
|
|
||||||
gid,
|
|
||||||
AuthCode:state.AuthCode
|
|
||||||
})).then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
if(res.data.result === 'success')
|
|
||||||
context.commit("_disCollectGallery", gid)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
updateGalleryTag(context, data){
|
|
||||||
axios.post(GalleryManageUrl + "/tag?" + qs.stringify(data)).then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
if(res.data.result === 'success')
|
|
||||||
context.commit("_updateGalleryTag", data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
deleteGallery(context, gid){
|
|
||||||
axios.delete(GalleryManageUrl, {
|
|
||||||
params:{
|
|
||||||
AuthCode:state.AuthCode,
|
|
||||||
gid
|
|
||||||
}}).then((res) => {
|
|
||||||
if(res.data.result === "success"){
|
|
||||||
ElMessage("删除成功")
|
|
||||||
context.commit("_deleteGallery", gid)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
alterAuthCode(context, AuthCode){
|
|
||||||
axios.put(BaseUrl + "AuthCode?" + qs.stringify({'AuthCode': state.AuthCode, 'newAuthCode': AuthCode}))
|
|
||||||
.then((res) => {
|
|
||||||
if(res.data.result === 'success') {
|
|
||||||
ElMessage("修改成功")
|
|
||||||
if(localStorage.getItem("auth") === state.AuthCode)
|
|
||||||
localStorage.setItem("auth", AuthCode)
|
|
||||||
state.AuthCode = AuthCode
|
|
||||||
}
|
|
||||||
else
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
resetUndone() {
|
|
||||||
axios.post(GalleryManageUrl + "/reset?AuthCode=big+lion").then((res) => {
|
|
||||||
ElMessage(res.data.data)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
const mutations = {
|
// 移动端与桌面端共用同一份任务业务核心,移动端用“继续加载”代替分页条。
|
||||||
_collectGallery(state, gid){
|
export default new vuex.Store(createGalleryStore({
|
||||||
let tasks = state.totalGalleryTask
|
variant: "mobile",
|
||||||
for(let i=0; i< tasks.length; i++){
|
axios,
|
||||||
if(!tasks[i].isCollect && tasks[i].gid === gid){
|
qs,
|
||||||
tasks[i].isCollect = true
|
Message: ElMessage,
|
||||||
state.collectGallery.push(tasks[i])
|
}))
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_disCollectGallery(state, gid){
|
|
||||||
let index
|
|
||||||
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') {
|
|
||||||
state.totalGalleryTask.splice(0)
|
|
||||||
state.collectGallery.slice(0)
|
|
||||||
state.downloadGallery.splice(0)
|
|
||||||
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
//处理名字
|
|
||||||
task.shortName = getShortname(task.name)
|
|
||||||
|
|
||||||
//处理进度相关
|
|
||||||
switch (task.status) {
|
|
||||||
case "已提交":
|
|
||||||
task.progress = "已提交"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
else
|
|
||||||
task.isCollect = false
|
|
||||||
|
|
||||||
//处理是否下载
|
|
||||||
if(task.downloader === state.userId)
|
|
||||||
state.downloadGallery.push(task)
|
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if(state.isAuth && !state.loadComplete){
|
|
||||||
state.loadComplete = true
|
|
||||||
ElMessage("加载完成")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_changePage(state, targetPage){
|
|
||||||
state.page = targetPage
|
|
||||||
},
|
|
||||||
_authed(state, data){
|
|
||||||
state.AuthCode = data.AuthCode
|
|
||||||
state.userId = data.userId
|
|
||||||
state.username = data.username
|
|
||||||
state.isAuth = true
|
|
||||||
ElMessage("验证成功,加载中")
|
|
||||||
},
|
|
||||||
_unAuthed(state){
|
|
||||||
state.isAuth = false
|
|
||||||
state.AuthCode = ""
|
|
||||||
state.userId = -1
|
|
||||||
state.username = ""
|
|
||||||
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 name = null
|
|
||||||
|
|
||||||
if(state.showType === "gallery")
|
|
||||||
gid = link.split("/")[4]
|
|
||||||
|
|
||||||
if(gid === null)
|
|
||||||
for (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) {
|
|
||||||
state.page = Math.floor(i / state.length) + 1
|
|
||||||
name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!name)
|
|
||||||
ElMessage("未找到此任务")
|
|
||||||
else
|
|
||||||
ElMessage("已跳转到该任务所在页数,任务名:" + name)
|
|
||||||
},
|
|
||||||
_searchLocalByKeyword(state, keyword){
|
|
||||||
state.searchTask.splice(0)
|
|
||||||
if(keyword.trim() !== '') {
|
|
||||||
state.page = 1
|
|
||||||
let tasks = state.currentTasks
|
|
||||||
tasks.forEach((task) => {
|
|
||||||
if (task.name.includes(keyword))
|
|
||||||
state.searchTask.push(task)
|
|
||||||
})
|
|
||||||
if (state.searchTask.length === 0) {
|
|
||||||
ElMessage("未找到该关键字的任务")
|
|
||||||
state.isSearch = false
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
state.isSearch = true
|
|
||||||
}
|
|
||||||
}else {
|
|
||||||
state.isSearch = false
|
|
||||||
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.chosenGallery.downloader = state.userId
|
|
||||||
state.totalGalleryTask.push(state.chosenGallery)
|
|
||||||
state.downloadGallery.push(state.chosenGallery)
|
|
||||||
}
|
|
||||||
state.chosenGallery = data.gallery
|
|
||||||
},
|
|
||||||
_setCategory(state, category){
|
|
||||||
state.category = category
|
|
||||||
confirmCurrentTask(state)
|
|
||||||
sortTasks(state)
|
|
||||||
},
|
|
||||||
_setSortType(state, sortType){
|
|
||||||
state.sortType = sortType
|
|
||||||
sortTasks(state)
|
|
||||||
},
|
|
||||||
_setShowNameType(state, type){
|
|
||||||
if(type === "shortName")
|
|
||||||
state.length = state.shortLength
|
|
||||||
else
|
|
||||||
state.length = state.defaultLength
|
|
||||||
},
|
|
||||||
_openHistoryPanel(state){
|
|
||||||
state.isShowHistory = true
|
|
||||||
},
|
|
||||||
_closeHistoryPanel(state){
|
|
||||||
state.isShowHistory = false
|
|
||||||
},
|
|
||||||
_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,]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
totalGalleryTask: [], //存放本子数据的数组
|
|
||||||
chosenGallery: false, //准备下载的本子
|
|
||||||
thumbnailGallery: {}, //预览本子
|
|
||||||
collectGallery: [], //收藏的本子
|
|
||||||
downloadGallery: [], //下载的本子
|
|
||||||
tags: new Map(), //可用tag
|
|
||||||
isSearch: false, //用于决定是否显示搜索结果
|
|
||||||
|
|
||||||
currentGid: "", //当前GID
|
|
||||||
lengthPerPage: 0, //在线预览每页图片数量
|
|
||||||
|
|
||||||
page: 1, //当前页数
|
|
||||||
length: 5, //每页能有多少个链接
|
|
||||||
defaultLength: 4, //默认个数
|
|
||||||
shortLength: 5, //简洁个数
|
|
||||||
|
|
||||||
userId: -1, //用户id
|
|
||||||
username: ",", //用户名
|
|
||||||
isAuth: false, //是否授权
|
|
||||||
AuthCode: '', //授权码
|
|
||||||
loadComplete: false, //是否加载完成
|
|
||||||
galleryRefreshTimer: 0, //本子更新计时器id
|
|
||||||
|
|
||||||
isInclude: false, //是否搜索到任务
|
|
||||||
searchTask: [], //搜索到的任务
|
|
||||||
isShowHistory: false, //是否打开面板
|
|
||||||
category: 'myDownload', //分类
|
|
||||||
sortType:'shortName', //排序类型
|
|
||||||
currentTasks: [], //当前任务
|
|
||||||
weekUsed: {}, //每周用量
|
|
||||||
}
|
|
||||||
|
|
||||||
const getters = {
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
min(){
|
|
||||||
return 1
|
|
||||||
},
|
|
||||||
max(state){
|
|
||||||
let max = 0
|
|
||||||
let tasks
|
|
||||||
if(state.isSearch)
|
|
||||||
tasks = state.searchTask
|
|
||||||
else
|
|
||||||
tasks = state.currentTasks
|
|
||||||
|
|
||||||
if(!tasks)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
max = Math.floor(tasks.length/state.length)
|
|
||||||
if(tasks.length % state.length !== 0 || max === 0)
|
|
||||||
max += 1
|
|
||||||
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new vuex.Store({
|
|
||||||
actions,
|
|
||||||
mutations,
|
|
||||||
state,
|
|
||||||
getters
|
|
||||||
})
|
|
||||||
|
|
||||||
function getShortname(name){
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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 confirmCurrentTask(state){
|
|
||||||
switch (state.category){
|
|
||||||
case 'myDownload':
|
|
||||||
state.currentTasks = state.downloadGallery
|
|
||||||
break
|
|
||||||
case 'myCollect':
|
|
||||||
state.currentTasks = state.collectGallery
|
|
||||||
break
|
|
||||||
case 'total':
|
|
||||||
state.currentTasks = state.totalGalleryTask
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortTasks(state){
|
|
||||||
switch (state.sortType) {
|
|
||||||
case "name":
|
|
||||||
state.currentTasks = state.currentTasks.sort((before, after) => {
|
|
||||||
return before.name > after.name ? 1: -1
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case "shortName":
|
|
||||||
state.currentTasks = state.currentTasks.sort((before, after) => {
|
|
||||||
return before.shortName > after.shortName ? 1: -1
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case "createTime":
|
|
||||||
state.currentTasks = state.currentTasks.sort((before, after) => {
|
|
||||||
return before.createTime - after.createTime
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteTask(tasks, key, value){
|
|
||||||
for(let j=0; j < tasks.length; j++)
|
|
||||||
for(let i=0; i<tasks[j].length; i++)
|
|
||||||
if(tasks[j][i][key] === value){
|
|
||||||
tasks[j].splice(i, 1)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
|||||||
|
import test from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import {buildSearchParams, fetchGalleries, readResponse} from "lionwebsite-frontend-shared"
|
||||||
|
|
||||||
|
test("keyword request never hand-builds the query string", () => {
|
||||||
|
assert.deepEqual(buildSearchParams({keyword: "cat girl"}), {keyword: "cat girl"})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("next page forwards the upstream cursor instead of the keyword", () => {
|
||||||
|
const params = buildSearchParams({
|
||||||
|
keyword: "cat girl", direction: "next",
|
||||||
|
href: "https://exhentai.org/?f_search=cat+girl&next=777",
|
||||||
|
})
|
||||||
|
assert.equal(params.next, "777")
|
||||||
|
assert.equal(params.keyword, "cat girl")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fetchGalleries decodes object and JSON-string payloads identically", async () => {
|
||||||
|
const asObject = {get: async () => ({data: {result: "success", data: [{gid: 1}], next: "n1"}})}
|
||||||
|
const asString = {get: async () => ({data: {result: "success", data: '[{"gid":1}]', next: "n1"}})}
|
||||||
|
const one = await fetchGalleries(asObject, {keyword: "k"})
|
||||||
|
const two = await fetchGalleries(asString, {keyword: "k"})
|
||||||
|
assert.deepEqual(one.galleries, two.galleries)
|
||||||
|
assert.equal(one.pagination.next, "n1")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fetchGalleries surfaces the backend message on failure", async () => {
|
||||||
|
const http = {get: async () => ({data: {result: "failure", data: "没有搜索到结果"}})}
|
||||||
|
await assert.rejects(() => fetchGalleries(http, {keyword: "k"}),
|
||||||
|
error => error.payload === "没有搜索到结果")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("readResponse keeps all four pagination cursors", () => {
|
||||||
|
const parsed = readResponse({data: {result: "success", data: "[]",
|
||||||
|
first: "f", previous: "p", next: "n", last: "l"}})
|
||||||
|
assert.deepEqual(parsed.pagination, {first: "f", previous: "p", next: "n", last: "l"})
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import test from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
|
||||||
|
// 直接加载真实的移动端 store;在 Node 里 import 到的就是下发到浏览器的那份配置。
|
||||||
|
const store = (await import("../src/store/index.js")).default
|
||||||
|
|
||||||
|
const tasks = () => [
|
||||||
|
{gid: 1, name: "beta [tag]", status: "下载完成", pages: 10, proceeding: 10,
|
||||||
|
thumb_link: "", createTime: 2, downloader: 7, isCollect: true},
|
||||||
|
{gid: 2, name: "alpha", status: "下载中", pages: 10, proceeding: 5,
|
||||||
|
thumb_link: "", createTime: 1, downloader: 7, isCollect: false},
|
||||||
|
{gid: 3, name: "gamma", status: "已提交", pages: 10, proceeding: 0,
|
||||||
|
thumb_link: "", createTime: 3, downloader: 8, isCollect: false},
|
||||||
|
]
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
store.state.AuthCode = "test-auth"
|
||||||
|
store.state.userId = 7
|
||||||
|
store.state.category = "total"
|
||||||
|
store.state.sortType = "createTime"
|
||||||
|
store.state.length = 2
|
||||||
|
store.state.visiblePages = 1
|
||||||
|
store.state.isAdmin = false
|
||||||
|
store.state.downloaderFilter = null
|
||||||
|
store.commit("_updateGalleryTasks", tasks())
|
||||||
|
}
|
||||||
|
|
||||||
|
test("mobile variant shares one task list with the desktop core", () => {
|
||||||
|
reset()
|
||||||
|
assert.equal(store.state.totalGalleryTask.length, 3)
|
||||||
|
assert.equal(store.getters.downloadGallery.length, 2)
|
||||||
|
assert.equal(store.getters.collectGallery.length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("continue-loading reveals every task beyond the first page", () => {
|
||||||
|
reset()
|
||||||
|
assert.equal(store.getters.visibleTasks.length, 2)
|
||||||
|
assert.equal(store.getters.taskRemaining, 1)
|
||||||
|
store.commit("_showMoreTasks")
|
||||||
|
assert.equal(store.getters.visibleTasks.length, 3)
|
||||||
|
assert.equal(store.getters.taskRemaining, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("json-string and object payloads decode to the same store result", () => {
|
||||||
|
reset()
|
||||||
|
const fromObjects = store.state.totalGalleryTask.map(task => task.gid)
|
||||||
|
store.state.totalGalleryTask = []
|
||||||
|
store.commit("_updateGalleryTasks", JSON.parse(JSON.stringify(tasks())))
|
||||||
|
assert.deepEqual(store.state.totalGalleryTask.map(task => task.gid), fromObjects)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("cancelling a collect for a task missing from the list never throws", () => {
|
||||||
|
reset()
|
||||||
|
// 旧实现会在 collectGallery 里找不到 gid 时抛 TypeError。
|
||||||
|
assert.doesNotThrow(() => store.commit("_setCollect", {gid: 999, collect: false}))
|
||||||
|
store.commit("_setCollect", {gid: 1, collect: false})
|
||||||
|
assert.equal(store.getters.collectGallery.length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("sorting never reorders the single source array", () => {
|
||||||
|
reset()
|
||||||
|
const before = store.state.totalGalleryTask.map(task => task.gid)
|
||||||
|
store.commit("_setSortType", "name")
|
||||||
|
assert.deepEqual(store.state.totalGalleryTask.map(task => task.gid), before)
|
||||||
|
assert.deepEqual(store.getters.visibleTasks.map(task => task.name), ["alpha", "beta [tag]"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("link search surfaces the match as a search result on mobile", () => {
|
||||||
|
const messages = []
|
||||||
|
reset()
|
||||||
|
store.commit("_searchLocalByLink", "https://example.org/g/3/key/")
|
||||||
|
assert.equal(store.state.isSearch, true)
|
||||||
|
assert.equal(store.getters.searchResults.length, 1)
|
||||||
|
assert.equal(store.getters.searchResults[0].gid, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("thumbnail and online image urls carry the active auth code", () => {
|
||||||
|
store.state.AuthCode = "code with spaces"
|
||||||
|
store.commit("_updateGalleryTasks", [{
|
||||||
|
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.commit("_setReadingGallery", {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,207 @@
|
|||||||
|
import test from "node:test"
|
||||||
|
import assert from "node:assert/strict"
|
||||||
|
import fs from "node:fs"
|
||||||
|
import {registerHooks} from "node:module"
|
||||||
|
import {compileScript, parse} from "@vue/compiler-sfc"
|
||||||
|
import {createRenderer, nextTick} from "vue"
|
||||||
|
import {ehThumbnailUrl} from "lionwebsite-frontend-shared"
|
||||||
|
|
||||||
|
// 移动端详情抽屉的“打开/模式”状态只存在于真实组件里,store 单测覆盖不到。
|
||||||
|
// 这里用编译期 loader hook 直接执行仓库中的 .vue 源码,再挂到最小渲染器上;
|
||||||
|
// 开关与模式都落在普通 DOM 属性上,因此不需要 jsdom/happy-dom。
|
||||||
|
registerHooks({
|
||||||
|
load(url, context, nextLoad) {
|
||||||
|
if (!url.endsWith("/TaskDetailSheet.vue")) return nextLoad(url, context)
|
||||||
|
const source = fs.readFileSync(new URL(url), "utf8")
|
||||||
|
const {descriptor, errors} = parse(source, {filename: url})
|
||||||
|
assert.deepEqual(errors, [], "SFC 解析必须无错")
|
||||||
|
return {
|
||||||
|
format: "module",
|
||||||
|
shortCircuit: true,
|
||||||
|
source: compileScript(descriptor, {id: "sheet-test", inlineTemplate: true, isProd: false}).content,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {default: TaskDetailSheet} = await import("../src/components/TaskDetailSheet.vue")
|
||||||
|
const store = (await import("../src/store/index.js")).default
|
||||||
|
|
||||||
|
// 最小渲染器:只需要能插入节点、读回属性、处理 v-show 的 style。
|
||||||
|
function fakeElement(tag) {
|
||||||
|
return {nodeType: 1, tag, children: [], parentNode: null, style: {}, attrs: {}, listeners: {}}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeOps() {
|
||||||
|
const remove = node => {
|
||||||
|
const parent = node.parentNode
|
||||||
|
if (!parent) return
|
||||||
|
const index = parent.children.indexOf(node)
|
||||||
|
if (index >= 0) parent.children.splice(index, 1)
|
||||||
|
node.parentNode = null
|
||||||
|
}
|
||||||
|
const insert = (node, parent, anchor = null) => {
|
||||||
|
if (node.parentNode) remove(node)
|
||||||
|
const index = anchor ? parent.children.indexOf(anchor) : -1
|
||||||
|
if (index >= 0) parent.children.splice(index, 0, node)
|
||||||
|
else parent.children.push(node)
|
||||||
|
node.parentNode = parent
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
insert,
|
||||||
|
remove,
|
||||||
|
createElement: fakeElement,
|
||||||
|
createText: text => ({nodeType: 3, text, parentNode: null}),
|
||||||
|
createComment: text => ({nodeType: 8, text, parentNode: null}),
|
||||||
|
setText: (node, text) => { node.text = text },
|
||||||
|
setElementText: (node, text) => { node.children = [{nodeType: 3, text, parentNode: node}] },
|
||||||
|
parentNode: node => node.parentNode,
|
||||||
|
nextSibling: node => {
|
||||||
|
const parent = node.parentNode
|
||||||
|
if (!parent) return null
|
||||||
|
const index = parent.children.indexOf(node)
|
||||||
|
return parent.children[index + 1] || null
|
||||||
|
},
|
||||||
|
querySelector: () => null,
|
||||||
|
setScopeId: (el, id) => { el.scopeId = id },
|
||||||
|
cloneNode: el => ({...fakeElement(el.tag), attrs: {...el.attrs}}),
|
||||||
|
insertStaticContent: (content, parent, anchor) => {
|
||||||
|
const node = fakeElement("static")
|
||||||
|
insert(node, parent, anchor)
|
||||||
|
return [node, node]
|
||||||
|
},
|
||||||
|
patchProp: (el, key, previous, next) => {
|
||||||
|
if (key === "style") el.style = next || {}
|
||||||
|
else if (/^on[A-Z]/.test(key)) el.listeners[key] = next
|
||||||
|
else el.attrs[key] = next
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountSheet() {
|
||||||
|
const app = createRenderer(nodeOps()).createApp(TaskDetailSheet)
|
||||||
|
// 真实入口在 main.js 里全局注册 Element Plus;这里只需要占位节点。
|
||||||
|
app.component("el-progress", {render: () => null})
|
||||||
|
const root = fakeElement("root")
|
||||||
|
app.mount(root)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
function descendants(node, out = []) {
|
||||||
|
for (const child of node.children || []) {
|
||||||
|
if (child.nodeType === 1) {
|
||||||
|
out.push(child)
|
||||||
|
descendants(child, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function findByClass(root, className) {
|
||||||
|
return descendants(root).find(node => node.attrs.class === className) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function textOf(root) {
|
||||||
|
const parts = []
|
||||||
|
const walk = node => {
|
||||||
|
if (node.nodeType === 3) { parts.push(node.text); return }
|
||||||
|
for (const child of node.children || []) walk(child)
|
||||||
|
}
|
||||||
|
walk(root)
|
||||||
|
return parts.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOpen(root) {
|
||||||
|
const layer = findByClass(root, "sheet-layer")
|
||||||
|
assert.ok(layer, "抽屉根节点必须存在")
|
||||||
|
// v-show 只在关闭时写 display: none。
|
||||||
|
return layer.style.display !== "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskGallery = () => ({
|
||||||
|
gid: 5, name: "本地任务", status: "已提交", pages: 10, proceeding: 0,
|
||||||
|
thumb_link: "", createTime: 1, downloader: 7,
|
||||||
|
})
|
||||||
|
const remoteGallery = () => ({
|
||||||
|
gid: 9, name: "远端画廊", link: "https://example.org/g/9/k/", thumbnailUrl: "/t/9.jpg",
|
||||||
|
})
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
store.state.AuthCode = "auth"
|
||||||
|
store.state.userId = 7
|
||||||
|
store.state.detailGallery = null
|
||||||
|
store.state.chosenGallery = false
|
||||||
|
}
|
||||||
|
|
||||||
|
test("清空 detailGallery 会关上任务态抽屉", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.detailGallery = taskGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /本地任务/)
|
||||||
|
|
||||||
|
store.state.detailGallery = null
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("清空 chosenGallery 会关上远端态抽屉并复位模式", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.chosenGallery = remoteGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /远端画廊/)
|
||||||
|
|
||||||
|
store.state.chosenGallery = false
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), false)
|
||||||
|
|
||||||
|
// 模式必须回到 task,否则下一次打开任务会渲染成远端面板。
|
||||||
|
store.state.detailGallery = taskGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /删除任务/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("远端态下 detailGallery 变空不得误关抽屉", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.chosenGallery = remoteGallery()
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
|
||||||
|
// 任务流的 detail 被清空(例如列表刷新),此时屏幕上的是远端画廊,必须保持打开。
|
||||||
|
store.state.detailGallery = null
|
||||||
|
await nextTick()
|
||||||
|
assert.equal(isOpen(root), true)
|
||||||
|
assert.match(textOf(root), /远端画廊/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("已规范化的 thumb_link 直接使用,不再二次代理", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
const proxied = ehThumbnailUrl("/t/9.jpg", "auth")
|
||||||
|
store.state.chosenGallery = {
|
||||||
|
...remoteGallery(), thumb_link: proxied, thumbnailUrl: "https://example.org/raw.jpg",
|
||||||
|
}
|
||||||
|
await nextTick()
|
||||||
|
const image = descendants(root).find(node => node.tag === "img")
|
||||||
|
assert.ok(image, "远端态必须渲染缩略图")
|
||||||
|
assert.equal(image.attrs.src, proxied)
|
||||||
|
assert.equal(image.attrs.src.split("ehThumbnail").length - 1, 1)
|
||||||
|
// 外层 path 仍是原始图片路径,没有被嵌套成代理 URL。
|
||||||
|
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "/t/9.jpg")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("只有原始上游地址时才在这里代理一次", async () => {
|
||||||
|
reset()
|
||||||
|
const root = mountSheet()
|
||||||
|
store.state.chosenGallery = {...remoteGallery(), thumbnailUrl: "https://example.org/raw.jpg"}
|
||||||
|
await nextTick()
|
||||||
|
const image = descendants(root).find(node => node.tag === "img")
|
||||||
|
assert.ok(image)
|
||||||
|
assert.equal(image.attrs.src, ehThumbnailUrl("https://example.org/raw.jpg", "auth"))
|
||||||
|
assert.equal(new URL(image.attrs.src).searchParams.get("path"), "https://example.org/raw.jpg")
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
// Nginx 从 /asserts/mobile/ 提供移动端静态文件;部署文件名保持固定,
|
||||||
|
// 以便现有静态路由无需随每次构建改名。
|
||||||
|
base: '/asserts/mobile/',
|
||||||
|
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