移动端下载器改版:重构为底部三标签的信息架构
- 以 TabBar + 三个视图(任务/搜索/设置)替换原有 DashBoard/Side/HentaiSearch 单页结构 - 拆分 TaskView / TaskRow / TaskDetailSheet / SearchView / SettingsView / AuthScreen - 统一 theme.css 视觉规范,补 AppIcon 与缩略图工具 thumbnail.js 及其单测 - 提交弹层默认选中最高可用分辨率,缩略图加载失败走占位 - E站搜索结果与远端抽屉补回「在线看」入口
This commit is contained in:
+55
-26
@@ -1,42 +1,71 @@
|
||||
<script setup>
|
||||
import {onMounted, onBeforeUnmount} from "vue";
|
||||
import {computed, onBeforeUnmount, onMounted} from "vue";
|
||||
import store from "./store/index.js";
|
||||
import Side from "./components/Side.vue";
|
||||
import DashBoard from "./components/DashBoard.vue";
|
||||
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");
|
||||
if (store.state.isAuth && document.visibilityState === "visible") store.dispatch("initWebsocket");
|
||||
};
|
||||
const pauseProgress = () => store.dispatch("disconnectWebsocket");
|
||||
|
||||
onMounted(() => {
|
||||
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();
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="app">
|
||||
<el-container>
|
||||
<DashBoard/>
|
||||
<main>
|
||||
<Side/>
|
||||
<main class="app-views">
|
||||
<TaskView v-show="activeTab === 'tasks'"/>
|
||||
<SearchView v-show="activeTab === 'search'"/>
|
||||
<SettingsView v-show="activeTab === 'settings'"/>
|
||||
</main>
|
||||
</el-container>
|
||||
<TabBar/>
|
||||
<TaskDetailSheet/>
|
||||
<OnlineReader/>
|
||||
<AuthScreen/>
|
||||
</div>
|
||||
</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,384 +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">
|
||||
<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="reconnect">重连节点</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>
|
||||
<br>
|
||||
<el-button v-if="isLion" @click="resetUndone">重置任务</el-button>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog title="查询" v-model="chosenGallery" width="100%">
|
||||
<el-image v-show='chosenGallery.thumb_link !== undefined' style="float: right; width: 250px; height: 250px" fit="contain"
|
||||
:src="chosenGallery.thumb_link !== undefined ? thumbnailUrl(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>
|
||||
</table>
|
||||
<template #footer>
|
||||
<el-button @click="postTask" v-if="chosenGallery.availableResolution">下载</el-button>
|
||||
<el-button @click="downloadChosenGallery" v-else-if="chosenGallery.status === '下载完成'">下载文件</el-button>
|
||||
<el-button @click="onlineGalleryReader(chosenGallery)">在线预览</el-button>
|
||||
<el-button v-if="chosenGallery.status === '下载完成'" @click="deleteGallery">删除</el-button>
|
||||
</template>
|
||||
</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>
|
||||
夜间模式<br>
|
||||
<span style="display: inline-block">夜间模式跟随系统</span>
|
||||
<el-switch v-model="darkConfig.followSystem"></el-switch><hr>
|
||||
</div>
|
||||
<div>
|
||||
在线预览<br>
|
||||
<span style="display: inline-block">在线预览分页页数:</span>
|
||||
<input v-model="lengthPerPage"><hr>
|
||||
</div>
|
||||
<div>
|
||||
默认设置<br>
|
||||
<span style="display: inline-block">分类:</span>
|
||||
<el-select v-model="category" default-first-option>
|
||||
<el-option label="全部" value="total"/>
|
||||
<el-option label="我的下载" value="myDownload"/>
|
||||
<el-option label="我的收藏" value="myCollect"/>
|
||||
</el-select> <br>
|
||||
<span style="display: inline-block">排序方式:</span>
|
||||
<el-select v-model="sortType" default-first-option>
|
||||
<el-option label="名字" value="name"/>
|
||||
<el-option label="简洁名字" value="shortName"/>
|
||||
<el-option label="任务创建时间" value="createTime"/>
|
||||
</el-select> <br>
|
||||
<span style="display: inline-block">显示类型:</span>
|
||||
<el-select v-model="galleryNameType" default-first-option>
|
||||
<el-option label="名字" value="name"/>
|
||||
<el-option label="简洁名字" value="shortName"/>
|
||||
</el-select> <br>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="saveConfig">保存</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, watch} from "vue";
|
||||
import {ElMessage} from "element-plus"
|
||||
import HentaiSearch from "./HentaiSearch.vue";
|
||||
import {validateLink} from "../utils/validate.js";
|
||||
|
||||
//授权码相关
|
||||
let AuthCode = ref("")
|
||||
let isRemember = ref(false)
|
||||
let isAlterAuthCode = ref(false)
|
||||
let newAuthCode = ref("")
|
||||
let tempAuthCode = ref("")
|
||||
|
||||
let isQuerying = ref(false)
|
||||
let isConfig = ref(false)
|
||||
let isDark = ref(false)
|
||||
let keyword = ref("furry yaoi")
|
||||
let darkConfig = ref({})
|
||||
let lengthPerPage = ref(0)
|
||||
let category = ref("")
|
||||
let sortType = ref("")
|
||||
let galleryNameType = ref("")
|
||||
|
||||
//查询相关
|
||||
let type = ref("link")
|
||||
let param = ref("")
|
||||
|
||||
let targetResolution = ref("")
|
||||
|
||||
let realAuthCode = computed(() => {
|
||||
return store.state.AuthCode
|
||||
})
|
||||
|
||||
let chosenGallery = computed(() => {
|
||||
return store.state.chosenGallery
|
||||
})
|
||||
|
||||
watch(chosenGallery, () => {
|
||||
param.value = ''
|
||||
targetResolution.value = ''
|
||||
})
|
||||
|
||||
let loadComplete = computed(() => {
|
||||
return store.state.loadComplete
|
||||
})
|
||||
|
||||
let weekUsed = computed(() => {
|
||||
return store.state.weekUsed
|
||||
})
|
||||
|
||||
let thumbnailGallery = computed(() => {
|
||||
return store.state.thumbnailGallery
|
||||
})
|
||||
|
||||
let isLion = computed(() => {
|
||||
return store.state.userId === 3
|
||||
})
|
||||
|
||||
//重连节点
|
||||
function reconnect(){
|
||||
store.dispatch("reconnect")
|
||||
}
|
||||
|
||||
//修改授权码
|
||||
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
|
||||
}
|
||||
store.dispatch("postGalleryTask",
|
||||
{link: chosenGallery.value.link,
|
||||
targetResolution: targetResolution.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 deleteGallery(){
|
||||
store.dispatch("deleteGallery", chosenGallery.value.gid)
|
||||
}
|
||||
|
||||
function downloadChosenGallery(){
|
||||
if(chosenGallery.value.download)
|
||||
window.open(chosenGallery.value.download)
|
||||
else
|
||||
ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"})
|
||||
}
|
||||
|
||||
//验证授权码
|
||||
function validate(){
|
||||
if(AuthCode.value.trim() === ""){
|
||||
ElMessage("请输入授权码后再验证")
|
||||
}
|
||||
else{
|
||||
store.dispatch("validate", AuthCode.value)
|
||||
if(isRemember.value)
|
||||
localStorage.setItem("auth", AuthCode.value)
|
||||
}
|
||||
}
|
||||
|
||||
//打开面板以及在线阅读
|
||||
function onlineGalleryReader(gallery){
|
||||
store.dispatch("readOnlineGallery", gallery)
|
||||
}
|
||||
|
||||
function thumbnailUrl(path) {
|
||||
return "https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?" + new URLSearchParams({
|
||||
path,
|
||||
AuthCode: store.state.AuthCode
|
||||
}).toString()
|
||||
}
|
||||
|
||||
//重新给节点发送未完成任务
|
||||
function resetUndone(){
|
||||
store.dispatch("resetUndone").then()
|
||||
}
|
||||
function deleteAuthCode(){
|
||||
localStorage.removeItem('auth')
|
||||
ElMessage("删除授权码完成")
|
||||
}
|
||||
|
||||
function toggleStyle(){
|
||||
if(isDark.value)
|
||||
dark()
|
||||
else
|
||||
light()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const auth = localStorage.getItem("auth")
|
||||
adjustForStyle()
|
||||
store.state.lengthPerPage = localStorage.getItem("lengthPerPage")
|
||||
store.state.lengthPerPage = store.state.lengthPerPage === null ? 30: Number(store.state.lengthPerPage)
|
||||
category.value = store.state.category = localStorage.getItem("category") === null ? "myDownload" : localStorage.getItem("category")
|
||||
sortType.value = store.state.sortType = localStorage.getItem("sortType") === null ? "createTime" : localStorage.getItem("sortType")
|
||||
galleryNameType.value = store.state.galleryNameType = localStorage.getItem("galleryNameType") === null ? "shortName" : localStorage.getItem("galleryNameType")
|
||||
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 && isSystemDark())
|
||||
dark()
|
||||
else
|
||||
light()
|
||||
}else {
|
||||
light()
|
||||
darkConfig.value = {'followSystem': false}
|
||||
}
|
||||
}
|
||||
function isSystemDark(){
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
function dark(){
|
||||
isDark.value = true
|
||||
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(){
|
||||
isDark.value = false
|
||||
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(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))
|
||||
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
|
||||
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,153 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
import {ref, watch} from "vue";
|
||||
import axios from "axios";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
import {validateLink} from "../utils/validate.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(false)
|
||||
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.value = true
|
||||
|
||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + tempParam)
|
||||
.then((res) => {
|
||||
if (res.data.result === "success") {
|
||||
let tempGalleries = JSON.parse(res.data.data)
|
||||
queryPage.value.first = 'first' in res.data ? res.data.first : undefined
|
||||
queryPage.value.previous = 'previous' in res.data ? res.data.previous : undefined
|
||||
queryPage.value.next = 'next' in res.data ? res.data.next : undefined
|
||||
queryPage.value.last = 'last' in res.data ? res.data.last : undefined
|
||||
|
||||
galleries.value.splice(0)
|
||||
tempGalleries.forEach((gallery) => {
|
||||
galleries.value.push(gallery)
|
||||
})
|
||||
|
||||
scrollBar.value.setScrollTop(0)
|
||||
}else {
|
||||
ElMessage({message: res.data.data, type: "error"})
|
||||
}
|
||||
}).finally(() => {
|
||||
isLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function queryRemoteTask(){
|
||||
if(!validateLink(param.value)){
|
||||
ElMessage("链接错误")
|
||||
return
|
||||
}
|
||||
store.dispatch("queryGalleryTask", param.value)
|
||||
}
|
||||
|
||||
function close(){
|
||||
emit("close")
|
||||
}
|
||||
|
||||
function thumbnailUrl(path) {
|
||||
return "https://downloader.lionwebsite.xyz/GalleryManage/ehThumbnail?" + new URLSearchParams({
|
||||
path,
|
||||
AuthCode: store.state.AuthCode
|
||||
}).toString()
|
||||
}
|
||||
|
||||
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 class="loading" v-show="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="[thumbnailUrl(gallery.thumbnailUrl)]"
|
||||
:src="thumbnailUrl(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-group style=" margin-left: 50%; margin-top: -5vh">
|
||||
<el-button @click="store.dispatch('readOnlineGallery', gallery)">在线看</el-button>
|
||||
<el-button @click="param=gallery.link; queryRemoteTask()">查看详情</el-button>
|
||||
</el-button-group>
|
||||
</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;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.el-dialogClass .el-dialog__body{
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<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 {validateLink} from "../utils/validate.js";
|
||||
import {ehThumbnailUrl} from "../utils/thumbnail.js";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function queryGalleries(link) {
|
||||
let keyword
|
||||
if (link != null) {
|
||||
keyword = new URL(link).search.replace("?f_search=", "")
|
||||
} else {
|
||||
keyword = remoteKeyword.value
|
||||
if (keyword.trim() === "") {
|
||||
ElMessage("请输入搜索关键词")
|
||||
return
|
||||
}
|
||||
}
|
||||
remoteLoading.value = true
|
||||
axios.get("https://downloader.lionwebsite.xyz/query?keyword=" + keyword.replace(" ", "+"))
|
||||
.then((res) => {
|
||||
if (res.data.result === "success") {
|
||||
remotePage.value = {
|
||||
first: res.data.first,
|
||||
previous: res.data.previous,
|
||||
next: res.data.next,
|
||||
last: res.data.last,
|
||||
}
|
||||
remoteGalleries.value.splice(0)
|
||||
JSON.parse(res.data.data).forEach(gallery => remoteGalleries.value.push(gallery))
|
||||
} else {
|
||||
ElMessage({message: res.data.data, 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)">上一页</button>
|
||||
<button type="button" :disabled="remotePage.next === undefined" @click="queryGalleries(remotePage.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 isLion = computed(() => store.state.userId === 3);
|
||||
|
||||
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="isLion" 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,316 +0,0 @@
|
||||
<template>
|
||||
<div class="side">
|
||||
<div v-show="loadComplete" class="load_complete">
|
||||
<el-tag :type="store.state.connectionStatus === 'connected' ? 'success' : 'warning'" role="status">
|
||||
{{ {connected: '进度已连接', connecting: '正在连接进度', reconnecting: '连接中断,正在重连', disconnected: '进度连接已断开'}[store.state.connectionStatus] }}
|
||||
</el-tag>
|
||||
<el-scrollbar max-height="80vh">
|
||||
<div v-if="currentTasks.length === 0">
|
||||
{{emptyText}}
|
||||
</div>
|
||||
<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="gallery.thumb_link"
|
||||
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="link" label="链接"/>
|
||||
<el-option value="keyword" label="关键字"/>
|
||||
</el-select>
|
||||
<el-input style="width: 60vw" v-model="param" placeholder="搜索">
|
||||
<template #append>
|
||||
<el-button @click="queryLocalTask">搜索</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-row class="pageChanger">
|
||||
<el-col v-if="username !== 'test'">
|
||||
<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="isViewing" width="100%" top="0" style="padding: 0">
|
||||
<div style="height: 20vh; font: bold 16px semi-expanded">
|
||||
<el-image :src="currentGallery.thumb_link"
|
||||
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>
|
||||
<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 !== '下载完成'" size="large">下载</el-button>
|
||||
<el-button @click="deleteGallery(currentGallery.gid)" :disabled="currentGallery.status !== '下载完成'" size="large">删除</el-button>
|
||||
<el-button @click="changeGalleryCollect(currentGallery.gid, currentGallery.isCollect)" :disabled="currentGallery.status !== '下载完成'"
|
||||
size="large">{{currentGallery.isCollect ? '取消收藏' : '收藏'}}</el-button>
|
||||
<el-button @click="readOnlineGallery(currentGallery)" size="large">在线看</el-button>
|
||||
<el-button v-if="currentGallery.status !== '下载完成'"
|
||||
@click="retryGallery(currentGallery.gid)"
|
||||
:loading="retryingGids.has(currentGallery.gid)"
|
||||
size="large">
|
||||
重试
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<OnlineReader/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import store from "../store";
|
||||
import {computed, ref} from "vue";
|
||||
import OnlineReader from "./OnlineReader.vue";
|
||||
|
||||
//输入
|
||||
let inputNode = ref(null)
|
||||
//是否正在编辑页数
|
||||
let isEditingPage = ref(false)
|
||||
//是否查看详情
|
||||
let isViewing = ref(false)
|
||||
let retryingGids = ref(new Set())
|
||||
|
||||
let category = computed(() => {
|
||||
return store.state.category
|
||||
})
|
||||
let galleryNameType = computed(() => {
|
||||
return store.state.galleryNameType
|
||||
})
|
||||
let sortType = computed(() => {
|
||||
return store.state.sortType
|
||||
})
|
||||
let targetPage = ref(1) // 当前页数
|
||||
let username = computed(() => {
|
||||
return store.state.username
|
||||
})
|
||||
|
||||
//查询相关
|
||||
let type = ref("keyword")
|
||||
let param = ref("")
|
||||
|
||||
let loadComplete = computed(() => {
|
||||
return store.state.loadComplete
|
||||
})
|
||||
|
||||
let currentTasks = computed(() => {
|
||||
return store.getters.currentTasks ? store.getters.currentTasks: null
|
||||
})
|
||||
|
||||
let min = computed(() => {
|
||||
return store.getters.min
|
||||
})
|
||||
let max = computed(() => {
|
||||
if(targetPage.value > store.getters.max)
|
||||
store.commit("_changePage", store.getters.max)
|
||||
return store.getters.max
|
||||
})
|
||||
let page = computed(() => {
|
||||
targetPage.value = store.state.page
|
||||
return store.state.page
|
||||
})
|
||||
let isLion = computed(() => {
|
||||
return store.state.userId === 3
|
||||
})
|
||||
|
||||
let emptyText = computed(() => {
|
||||
let action = category.value === 'myDownload' ? '下载': '收藏'
|
||||
return '您未' + action + '过'
|
||||
})
|
||||
|
||||
//查看详情
|
||||
let currentGallery = ref({name:"name"})
|
||||
|
||||
//翻页
|
||||
function next() {
|
||||
if(targetPage.value < max.value) {
|
||||
targetPage.value++
|
||||
store.commit("_changePage", targetPage.value)
|
||||
}
|
||||
}
|
||||
function previous() {
|
||||
if(targetPage.value > min.value) {
|
||||
targetPage.value--
|
||||
store.commit("_changePage", targetPage.value)
|
||||
}
|
||||
}
|
||||
function toMax() {
|
||||
store.commit("_changePage", max.value)
|
||||
targetPage.value = max.value
|
||||
}
|
||||
function toMin(){
|
||||
store.commit("_changePage", min.value)
|
||||
targetPage.value = min.value
|
||||
}
|
||||
function changePage(){
|
||||
if(targetPage.value >= min.value && targetPage.value <= max.value)
|
||||
store.commit("_changePage", targetPage.value)
|
||||
}
|
||||
function reverseEditMode(){
|
||||
isEditingPage.value = !isEditingPage.value
|
||||
if(isEditingPage.value){
|
||||
inputNode.value.focus()
|
||||
}
|
||||
targetPage.value = page.value
|
||||
}
|
||||
|
||||
//改变展示类型
|
||||
function changeCategory(value){
|
||||
store.commit("_setCategory", value)
|
||||
}
|
||||
function changeGalleryNameType(value){
|
||||
store.commit("_setGalleryNameType", value)
|
||||
}
|
||||
function changeSortType(value){
|
||||
store.commit("_setSortType", value)
|
||||
}
|
||||
|
||||
//收藏
|
||||
function changeGalleryCollect(gid, isCollect){
|
||||
if(isCollect)
|
||||
store.dispatch("disCollectGallery", gid)
|
||||
else
|
||||
store.dispatch("collectGallery", gid)
|
||||
}
|
||||
function queryLocalTask(){
|
||||
switch (type.value){
|
||||
case "link":
|
||||
store.commit("_searchLocalByLink", param.value)
|
||||
break
|
||||
case "keyword":
|
||||
store.commit("_searchLocalByKeyword", param.value)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//下载,删除,在线看
|
||||
function downloadTask(link){
|
||||
window.open(link)
|
||||
}
|
||||
function deleteGallery(gid){
|
||||
store.dispatch("deleteGallery", gid).then(() => {
|
||||
isViewing.value = false
|
||||
})
|
||||
}
|
||||
async function retryGallery(gid){
|
||||
if(retryingGids.value.has(gid))
|
||||
return
|
||||
retryingGids.value.add(gid)
|
||||
try {
|
||||
await store.dispatch("retryGallery", gid)
|
||||
} finally {
|
||||
retryingGids.value.delete(gid)
|
||||
}
|
||||
}
|
||||
function readOnlineGallery(gallery){
|
||||
store.dispatch("readOnlineGallery", gallery)
|
||||
}
|
||||
function viewInfo(gallery){
|
||||
currentGallery.value = gallery
|
||||
isViewing.value = true;
|
||||
}
|
||||
|
||||
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 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,200 @@
|
||||
<script setup>
|
||||
import {computed, ref, watch} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import store from "../store/index.js";
|
||||
import {ehThumbnailUrl, pickDefaultResolution} from "../utils/thumbnail.js";
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
watch(detailGallery, (gallery) => {
|
||||
if (gallery) {
|
||||
mode.value = "task";
|
||||
task.value = gallery;
|
||||
isOpen.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
const isLion = computed(() => store.state.userId === 3);
|
||||
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 resolved gallery still carries a raw path, so the proxy URL is built here.
|
||||
const remoteThumb = computed(() => {
|
||||
const gallery = chosenGallery.value;
|
||||
if (!gallery) return "";
|
||||
return ehThumbnailUrl(gallery.thumb_link || 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="isLion">
|
||||
<dt>downloader</dt>
|
||||
<dd>{{ 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,119 @@
|
||||
<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 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>
|
||||
<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>
|
||||
@@ -4,5 +4,6 @@ import 'element-plus/dist/index.css'
|
||||
import element from "element-plus"
|
||||
import "./reset.css"
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import "./styles/theme.css"
|
||||
|
||||
createApp(App).use(element).mount('#app')
|
||||
|
||||
+44
-3
@@ -311,6 +311,16 @@ const mutations = {
|
||||
_changePage(state, targetPage){
|
||||
state.page = targetPage
|
||||
},
|
||||
_setActiveTab(state, tab){
|
||||
state.activeTab = tab
|
||||
},
|
||||
_setDetailGallery(state, gallery){
|
||||
state.detailGallery = gallery
|
||||
},
|
||||
// The mobile list grows in place instead of paging through a footer control.
|
||||
_showMoreTasks(state){
|
||||
state.visiblePages += 1
|
||||
},
|
||||
_authed(state, data){
|
||||
state.AuthCode = data.AuthCode
|
||||
state.userId = data.userId
|
||||
@@ -329,12 +339,14 @@ const mutations = {
|
||||
_searchLocalByLink(state, link){
|
||||
let tasks = state.currentTasks
|
||||
let gid = link.split("/")[4]
|
||||
let matched = null
|
||||
let name = null
|
||||
|
||||
if(gid === undefined)
|
||||
for (let i = 0; i < tasks.length; i++) {
|
||||
if (tasks[i].link === link) {
|
||||
state.page = Math.floor(i / state.length) + 1
|
||||
matched = tasks[i]
|
||||
name = tasks[i].name
|
||||
break
|
||||
}
|
||||
@@ -342,19 +354,27 @@ const mutations = {
|
||||
else for (let i = 0; i < tasks.length; i++)
|
||||
if (String(tasks[i].gid) === gid) {
|
||||
state.page = Math.floor(i / state.length) + 1
|
||||
matched = tasks[i]
|
||||
name = state.sortType === "shortName" ? tasks[i].shortName: tasks[i].name
|
||||
break
|
||||
}
|
||||
|
||||
if(!name)
|
||||
ElMessage("未找到此任务")
|
||||
else
|
||||
else {
|
||||
state.searchTask.splice(0)
|
||||
if(matched)
|
||||
state.searchTask.push(matched)
|
||||
state.isSearch = true
|
||||
state.visiblePages = 1
|
||||
ElMessage("已跳转到该任务所在页数,任务名:" + name)
|
||||
}
|
||||
},
|
||||
_searchLocalByKeyword(state, keyword){
|
||||
state.searchTask.splice(0)
|
||||
if(keyword.trim() !== '') {
|
||||
state.page = 1
|
||||
state.visiblePages = 1
|
||||
let tasks = state.currentTasks
|
||||
tasks.forEach((task) => {
|
||||
if (task.name.includes(keyword))
|
||||
@@ -381,11 +401,13 @@ const mutations = {
|
||||
},
|
||||
_setCategory(state, category){
|
||||
state.category = category
|
||||
state.visiblePages = 1
|
||||
confirmCurrentTask(state)
|
||||
sortTasks(state)
|
||||
},
|
||||
_setSortType(state, sortType){
|
||||
state.sortType = sortType
|
||||
state.visiblePages = 1
|
||||
sortTasks(state)
|
||||
},
|
||||
_setGalleryNameType(state, galleryNameType){
|
||||
@@ -451,6 +473,9 @@ const state = {
|
||||
isInclude: false, //是否搜索到任务
|
||||
searchTask: [], //搜索到的任务
|
||||
isShowHistory: false, //是否打开面板
|
||||
activeTab: 'tasks', //移动端底部标签 tasks search settings
|
||||
detailGallery: null, //详情抽屉里正在看的任务
|
||||
visiblePages: 1, //列表已展开的页数,用于“继续加载”
|
||||
galleryNameType: 'shortName', //名字类型 shortName name
|
||||
category: 'myDownload', //分类 myDownload myCollect total
|
||||
sortType:'shortName', //排序类型 shortName name createTime
|
||||
@@ -468,6 +493,18 @@ const getters = {
|
||||
min(){
|
||||
return 1
|
||||
},
|
||||
visibleTasks(state){
|
||||
return (state.currentTasks || []).slice(0, state.visiblePages * state.length)
|
||||
},
|
||||
taskTotal(state){
|
||||
return (state.currentTasks || []).length
|
||||
},
|
||||
taskRemaining(state, getters){
|
||||
return Math.max(0, getters.taskTotal - state.visiblePages * state.length)
|
||||
},
|
||||
searchResults(state){
|
||||
return (state.searchTask || []).slice(0, state.visiblePages * state.length)
|
||||
},
|
||||
max(state){
|
||||
let max = 0
|
||||
let tasks
|
||||
@@ -500,8 +537,12 @@ function getShortname(name){
|
||||
if (!name.includes("[")) return name
|
||||
|
||||
// 截取最后一个 [ 之前的部分,然后移除所有 [...] 和 (...) 标签
|
||||
name = name.substring(0, name.lastIndexOf("["))
|
||||
return name.replace(/\s*\[[^\]]*\]\s*/g, '').replace(/\s*\([^\)]*\)\s*/g, '').trim()
|
||||
const trimmed = name.substring(0, name.lastIndexOf("["))
|
||||
.replace(/\s*\[[^\]]*\]\s*/g, '')
|
||||
.replace(/\s*\([^\)]*\)\s*/g, '')
|
||||
.trim()
|
||||
// 名字以 [...] 开头时截取结果为空,退回原名避免整列空白
|
||||
return trimmed === '' ? name.trim() : trimmed
|
||||
}
|
||||
|
||||
function buildGalleryDownloadUrl(gid, AuthCode){
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
const BaseUrl = "https://downloader.lionwebsite.xyz/";
|
||||
|
||||
// Remote galleries carry a raw thumbnail path; it only resolves through the
|
||||
// backend proxy once the active auth code is attached.
|
||||
export function ehThumbnailUrl(path, AuthCode) {
|
||||
if (!path) return "";
|
||||
return BaseUrl + "GalleryManage/ehThumbnail?" + new URLSearchParams({path, AuthCode}).toString();
|
||||
}
|
||||
|
||||
const UNIT_SCALE = {b: 1, k: 1024, m: 1024 ** 2, g: 1024 ** 3, t: 1024 ** 4};
|
||||
|
||||
function sizeInBytes(text) {
|
||||
const match = String(text).match(/([\d.]+)\s*([kmgt]?)(?:i?b)/i);
|
||||
if (!match) return 0;
|
||||
const scale = UNIT_SCALE[match[2].toLowerCase()] ?? 1;
|
||||
return Number(match[1]) * scale;
|
||||
}
|
||||
|
||||
// Prefer the original file; otherwise take whichever entry reports the most bytes.
|
||||
export function pickDefaultResolution(availableResolution) {
|
||||
const entries = Object.entries(availableResolution || {});
|
||||
if (entries.length === 0) return "";
|
||||
const original = entries.find(([resolution]) => /original/i.test(resolution));
|
||||
if (original) return original[0];
|
||||
return entries.reduce((best, current) =>
|
||||
sizeInBytes(current[1]) > sizeInBytes(best[1]) ? current : best)[0];
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import {ehThumbnailUrl, pickDefaultResolution} from '../src/utils/thumbnail.js'
|
||||
|
||||
test('remote thumbnail paths are wrapped with the auth code', () => {
|
||||
const url = ehThumbnailUrl('samples/thumb.png', 'abc123')
|
||||
assert.match(url, /^https:\/\/downloader\.lionwebsite\.xyz\/GalleryManage\/ehThumbnail\?/)
|
||||
assert.match(url, /path=samples%2Fthumb\.png/)
|
||||
assert.match(url, /AuthCode=abc123/)
|
||||
})
|
||||
|
||||
test('a missing thumbnail path produces no url', () => {
|
||||
assert.equal(ehThumbnailUrl('', 'abc123'), '')
|
||||
assert.equal(ehThumbnailUrl(undefined, 'abc123'), '')
|
||||
})
|
||||
|
||||
test('the original resolution is preselected when offered', () => {
|
||||
const chosen = pickDefaultResolution({
|
||||
'800x': '37.53MiB',
|
||||
'1280x': '66.57MiB',
|
||||
'2560x': '84.01MiB',
|
||||
'Original': '498.2MiB',
|
||||
})
|
||||
assert.equal(chosen, 'Original')
|
||||
})
|
||||
|
||||
test('the original label is matched case-insensitively', () => {
|
||||
assert.equal(pickDefaultResolution({'800x': '1MiB', 'original': '2MiB'}), 'original')
|
||||
})
|
||||
|
||||
test('without an original the largest entry wins across units', () => {
|
||||
const chosen = pickDefaultResolution({
|
||||
'800x': '37.53MiB',
|
||||
'2560x': '84.01MiB',
|
||||
'2160x': '1.2GiB',
|
||||
})
|
||||
assert.equal(chosen, '2160x')
|
||||
})
|
||||
|
||||
test('an empty or absent resolution map selects nothing', () => {
|
||||
assert.equal(pickDefaultResolution({}), '')
|
||||
assert.equal(pickDefaultResolution(undefined), '')
|
||||
})
|
||||
|
||||
test('unparseable sizes fall back to the first entry instead of throwing', () => {
|
||||
assert.equal(pickDefaultResolution({'800x': 'unknown', '1280x': 'unknown'}), '800x')
|
||||
})
|
||||
Reference in New Issue
Block a user