桌面端下载器调优:任务列表信息层级与操作区重排

- 统一 theme.css 视觉变量与卡片、状态标签样式
- DashBoard 与 Side 的任务行重排:状态标签与进度对齐,排序/显示条件更明确
- 新增 AppIcon 图标组件,store 同步空名短标题修复
This commit is contained in:
root
2026-09-20 18:41:09 +08:00
parent c115b6529d
commit e836becda6
6 changed files with 969 additions and 311 deletions
+4 -14
View File
@@ -24,7 +24,10 @@ import DashBoard from "./components/DashBoard.vue";
<template>
<el-container class="app-layout">
<el-aside width="720px" class="side-area">
<!-- The task table needs room for name + status + progress + actions; the
column grows with the viewport but never squeezes the table. -->
<!-- Fixed columns need ~380px; the name column keeps at least 200px. -->
<el-aside width="clamp(700px, 52vw, 1000px)" class="side-area">
<Side/>
</el-aside>
<el-main class="main-area">
@@ -34,17 +37,4 @@ import DashBoard from "./components/DashBoard.vue";
</template>
<style>
.app-layout {
height: 100vh;
}
.side-area {
overflow: hidden;
border-right: 1px solid var(--border-color);
background: var(--bg-surface);
}
.main-area {
padding: 0;
overflow: hidden;
background-color: var(--bg-body);
}
</style>
+47
View File
@@ -0,0 +1,47 @@
<script setup>
import { computed } from "vue";
// Small stroke-based icon set (24x24 grid) so the UI needs no extra dependency.
// Brand-ish icons are stroked; star/sun/moon render filled for legibility.
const FILLED = new Set(['star', 'sun', 'moon']);
const PATHS = {
download: ["M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", "M7 10l5 5 5-5", "M12 15V3"],
eye: ["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z", "M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"],
star: ["M12 2.5l2.9 5.9 6.5.95-4.7 4.6 1.1 6.45L12 17.35 6.2 20.4l1.1-6.45-4.7-4.6 6.5-.95L12 2.5z"],
refresh: ["M21 12a9 9 0 0 1-15.5 6.2L3 16", "M3 21v-5h5", "M3 12a9 9 0 0 1 15.5-6.2L21 8", "M21 3v5h-5"],
trash: ["M3 6h18", "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6", "M10 11v6", "M14 11v6", "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"],
search: ["M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14z", "M20 20l-4-4"],
external: ["M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6", "M15 3h6v6", "M10 14L21 3"],
"chevron-left": ["M15 18l-6-6 6-6"],
"chevron-right": ["M9 6l6 6-6 6"],
"chevrons-left": ["M11 17l-5-5 5-5", "M18 17l-5-5 5-5"],
"chevrons-right": ["M13 17l5-5-5-5", "M6 17l5-5-5-5"],
sun: ["M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8z", "M12 2v2", "M12 20v2", "M4.5 4.5l1.4 1.4", "M18.1 18.1l1.4 1.4", "M2 12h2", "M20 12h2", "M4.5 19.5l1.4-1.4", "M18.1 5.9l1.4-1.4"],
moon: ["M20 14.5A8.5 8.5 0 1 1 9.5 4a7 7 0 0 0 10.5 10.5z"],
};
const props = defineProps({
name: { type: String, required: true },
size: { type: [Number, String], default: 16 },
filled: { type: Boolean, default: false },
});
const paths = computed(() => PATHS[props.name] || []);
const isFilled = computed(() => props.filled || FILLED.has(props.name));
</script>
<template>
<svg class="app-icon"
:width="size"
:height="size"
viewBox="0 0 24 24"
:fill="isFilled ? 'currentColor' : 'none'"
stroke="currentColor"
:stroke-width="isFilled ? 0 : 1.8"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
focusable="false">
<path v-for="(d, i) in paths" :key="i" :d="d" />
</svg>
</template>
+157 -108
View File
@@ -2,12 +2,20 @@
<!-- Auth Screen -->
<div v-show="!loadComplete" class="auth-container">
<div class="auth-card">
<h1>LionWebsite</h1>
<p class="auth-subtitle">请输入授权码以访问下载管理器</p>
<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" />
<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" @keydown.enter="validate" class="auth-btn">验证</el-button>
<el-button @click="validate" type="primary" size="large" class="auth-btn">验证</el-button>
</div>
</div>
</div>
@@ -15,138 +23,179 @@
<!-- Dashboard Main -->
<div v-show="loadComplete" class="dashboard">
<div class="control-card">
<h3>用量统计</h3>
<div class="card-row">
<span class="usage-text">本周已用: <b>{{ weekUsed.weekUsedAmount }}</b> | 上次重置: {{ weekUsed.lastResetAmountTime }}</span>
<el-button @click="queryWeekUsedAmount" size="small">查询用量</el-button>
<div class="card-head">
<h3 class="section-title">本周用量</h3>
<el-button @click="queryWeekUsedAmount" size="small" text>刷新</el-button>
</div>
<div class="usage-line">
<span class="usage-value">{{ weekUsed.weekUsedAmount ?? '—' }}</span>
<span class="usage-note">上次重置 {{ weekUsed.lastResetAmountTime ?? '—' }}</span>
</div>
</div>
<div class="control-card">
<h3>查询任务</h3>
<div class="card-row">
<el-select v-model="type" @change="resetLocalQuery" size="small" style="width: 90px">
<el-option value="link" label="链接"/>
<el-option value="keyword" label="关键字"/>
</el-select>
<el-input v-model="param" size="small" placeholder="输入参数" style="width: 160px" />
<el-button @click="queryRemoteTask" v-show="type === 'link'" size="small">远程查询</el-button>
<el-button @click="queryLocalTask" size="small">当前页查询</el-button>
<div class="dashboard-grid">
<div class="control-card">
<div class="card-head">
<h3 class="section-title">查询任务</h3>
</div>
<div class="card-row">
<el-select v-model="type" @change="resetLocalQuery" size="small" style="width: 92px">
<el-option value="link" label="链接"/>
<el-option value="keyword" label="关键字"/>
</el-select>
<el-input v-model="param"
size="small"
placeholder="输入链接或关键字"
style="flex: 1; min-width: 150px"
@keydown.enter="type === 'link' ? queryRemoteTask() : queryLocalTask()" />
<el-button @click="queryRemoteTask" v-show="type === 'link'" size="small" type="primary">远程查询</el-button>
<el-button @click="queryLocalTask" size="small">当前页查询</el-button>
</div>
</div>
<div class="control-card">
<div class="card-head">
<h3 class="section-title">管理</h3>
</div>
<div class="card-row">
<el-button @click="isAlterAuthCode = true" size="small">修改授权码</el-button>
<el-button @click="deleteAuthCode" size="small">删除本地授权码</el-button>
<el-button @click="isConfig = true" size="small">配置</el-button>
</div>
</div>
<div class="control-card">
<div class="card-head">
<h3 class="section-title">系统</h3>
</div>
<div class="card-row">
<el-button @click="isQuerying = true" size="small" type="primary">在线搜索</el-button>
<el-button @click="reconnect" size="small">重连节点</el-button>
<el-button v-if="isLion" @click="resetUndone" size="small" type="danger" plain>重置任务</el-button>
</div>
</div>
<div class="control-card">
<div class="card-head">
<h3 class="section-title">外观</h3>
</div>
<div class="setting-row">
<span>夜间模式</span>
<el-switch v-model="isDark" @change="toggleStyle" />
</div>
</div>
</div>
<div class="control-card">
<h3>管理</h3>
<div class="card-row">
<el-button @click="isAlterAuthCode = true" size="small">修改授权码</el-button>
<el-button @click="deleteAuthCode" size="small">删除授权码</el-button>
<el-button @click="isConfig = true" size="small">配置</el-button>
<span style="font-size:13px;color:var(--text-secondary)">夜间模式</span>
<el-switch v-model="isDark" @change="toggleStyle" size="small" />
<div v-show="thumbnailGallery.thumb_link !== undefined" class="control-card">
<div class="card-head">
<h3 class="section-title">缩略图</h3>
</div>
</div>
<div class="control-card">
<h3>系统</h3>
<div class="card-row">
<el-button @click="isQuerying = true" size="small">在线搜索</el-button>
<el-button @click="reconnect" size="small">重连节点</el-button>
<el-button v-if="isLion" @click="resetUndone" size="small">重置任务</el-button>
</div>
</div>
<div v-show="thumbnailGallery.thumb_link !== undefined" class="control-card card-wide">
<h3>缩略图</h3>
<div class="thumbnail-body">
<p class="gallery-name">{{ thumbnailGallery.shortName }}</p>
<el-image :src="thumbnailGallery.thumb_link" :preview-src-list="[thumbnailGallery.thumb_link]" style="width:100%;height:500px" fit="contain" />
<div class="thumbnail-frame">
<el-image :src="thumbnailGallery.thumb_link"
:preview-src-list="[thumbnailGallery.thumb_link]"
style="width: 100%; height: 420px"
fit="contain" />
</div>
</div>
</div>
</div>
<el-dialog title="查询" v-model="chosenGallery" style="height: 350px">
<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" style="width: 200px">
<el-option v-for="(fileSize, resolution) in chosenGallery.availableResolution" :value="resolution"
:label="resolution + ' ' + fileSize">
</el-option>
</el-select>
</tr>
</table>
<el-dialog title="任务详情" v-model="chosenGallery" width="560px">
<div class="detail-layout">
<el-image v-show="chosenGallery.thumb_link !== undefined"
class="detail-thumb"
fit="contain"
:src="chosenGallery.thumb_link !== undefined ? thumbnailUrl(chosenGallery.thumb_link) : ''" />
<dl class="detail-list">
<dt>名字</dt>
<dd>{{ chosenGallery.name }}</dd>
<dt>页数</dt>
<dd>{{ chosenGallery.pages }}</dd>
<dt>语言</dt>
<dd>{{ chosenGallery.language }}</dd>
<dt>大小</dt>
<dd>{{ chosenGallery.fileSize }}</dd>
<dt>状态</dt>
<dd>{{ chosenGallery.status }}</dd>
<template v-if="chosenGallery.availableResolution">
<dt>目标分辨率</dt>
<dd>
<el-select v-model="targetResolution" style="width: 220px">
<el-option v-for="(fileSize, resolution) in chosenGallery.availableResolution"
:key="resolution"
:value="resolution"
:label="resolution + ' ' + fileSize" />
</el-select>
</dd>
</template>
</dl>
</div>
<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="postTask" v-if="chosenGallery.availableResolution" type="primary">提交下载</el-button>
<el-button @click="downloadChosenGallery" v-else-if="chosenGallery.status === '下载完成'" type="primary">下载文件</el-button>
<el-button @click="readOnlineGallery(chosenGallery)">在线预览</el-button>
<el-button v-if="chosenGallery.status === '下载完成'" @click="deleteGallery">删除</el-button>
<el-button v-if="chosenGallery.status === '下载完成'" type="danger" plain @click="deleteGallery">删除</el-button>
</template>
</el-dialog>
<el-dialog title="修改授权码" v-model="isAlterAuthCode">
<el-form>
<el-form-item>
<template #label>当前授权码</template>
<template #default>{{realAuthCode}}</template>
<el-form label-position="top">
<el-form-item label="当前授权码">
<span class="mono-value">{{ realAuthCode }}</span>
</el-form-item>
<el-form-item>
<template #label>新的授权码</template>
<template #default>
<el-input v-model="newAuthCode"></el-input>
</template>
<el-form-item label="新的授权码">
<el-input v-model="newAuthCode" show-password></el-input>
</el-form-item>
<el-form-item>
<template #label>再次输入授权码</template>
<template #default>
<el-input v-model="tempAuthCode"></el-input>
</template>
<el-form-item label="再次输入授权码">
<el-input v-model="tempAuthCode" show-password></el-input>
</el-form-item>
</el-form>
<el-footer>
<el-button @click="alterAuthCode">提交</el-button>
</el-footer>
<template #footer>
<el-button @click="isAlterAuthCode = false">取消</el-button>
<el-button type="primary" @click="alterAuthCode">提交</el-button>
</template>
</el-dialog>
<HentaiSearch v-model:is-querying="isQuerying" @close="isQuerying = false"></HentaiSearch>
<el-dialog title="配置" v-model="isConfig">
<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>
<el-form label-position="top" class="config-form">
<h4 class="section-title">外观</h4>
<div class="setting-row">
<span>夜间模式跟随系统</span>
<el-switch v-model="darkConfig.followSystem"></el-switch>
</div>
<h4 class="section-title">在线预览</h4>
<el-form-item label="每页图片数量">
<el-input-number v-model="lengthPerPage" :min="1" :max="30" :step="1" style="width: 160px" />
<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: 220px">
<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: 220px">
<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: 220px">
<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>
+221 -111
View File
@@ -1,103 +1,164 @@
<template>
<div class="side-table-wrapper">
<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-table :data="currentTasks"
:height="tableHeight"
:empty-text="emptyText"
:row-key="gallery=>gallery.gid"
class="table"
@cellMouseEnter="showThumbnail"
>
<div class="panel-head">
<h2 class="section-title">任务</h2>
<span class="head-meta">{{ currentTasks.length }} 项</span>
<span class="panel-head-spacer"></span>
<el-tag :type="connectionTagType" size="small" effect="light" role="status">
{{ connectionText }}
</el-tag>
</div>
<el-table-column type="expand">
<template #default="props">
<div class="detail-info">
名字:{{ props.row.name}} <br>
链接:<el-link :href="props.row.link">链接</el-link> <br>
语言:{{props.row.language}} <br>
页数:{{props.row.pages}} <br>
文件大小:{{props.row.fileSize}}<br>
分辨率:{{props.row.resolution}}<br>
任务创建时间:{{props.row.createTimeDisplay}}<br>
<span v-show="isLion">
downloader:{{props.row.downloader}}
</span>
</div>
<div class="table-scroll">
<el-table :data="currentTasks"
:empty-text="emptyText"
:row-key="gallery=>gallery.gid"
class="task-table"
:max-height="tableMaxHeight"
@cellMouseEnter="showThumbnail"
>
<el-button @click="deleteGallery(props.row.gid)" :disabled="props.row.status !== '下载完成'">删除</el-button>
</template>
</el-table-column>
<el-table-column label="名字" min-width="200">
<template #default="scoped">
{{galleryNameType === 'shortName' ? scoped.row.shortName: scoped.row.name}}
</template>
</el-table-column>
<el-table-column label="操作" width="360">
<template #default="scoped">
<span>
<el-button @click="downloadTask(scoped.row.download)" :disabled="scoped.row.status !== '下载完成'">下载</el-button>
<el-button @click="changeGalleryCollect(scoped.row.gid, scoped.row.isCollect)">{{scoped.row.isCollect ? '取消收藏' : '收藏'}}</el-button>
<el-button @click="readOnlineGallery(scoped.row)">在线看</el-button>
<el-button v-if="scoped.row.status !== '下载完成'"
@click="retryGallery(scoped.row.gid)"
:loading="retryingGids.has(scoped.row.gid)">
重试
</el-button>
</span>
</template>
</el-table-column>
<el-table-column label="进度" width="80">
<template #default="scoped">
{{ scoped.row.progress }}
</template>
</el-table-column>
</el-table>
<el-row class="pageChanger">
<el-col v-if="username !== 'test'">
<el-select v-model="category" @change="changeCategory">
<template #prefix>
分类
<el-table-column type="expand" width="40">
<template #default="props">
<dl class="detail-info">
<dt>名字</dt>
<dd>{{ props.row.name }}</dd>
<dt>链接</dt>
<dd><el-link :href="props.row.link" target="_blank" rel="noopener">打开原始页面</el-link></dd>
<dt>语言</dt>
<dd>{{ props.row.language }}</dd>
<dt>页数</dt>
<dd>{{ props.row.pages }}</dd>
<dt>文件大小</dt>
<dd>{{ props.row.fileSize }}</dd>
<dt>分辨率</dt>
<dd>{{ props.row.resolution }}</dd>
<dt>创建时间</dt>
<dd>{{ props.row.createTimeDisplay }}</dd>
<template v-if="isLion">
<dt>downloader</dt>
<dd>{{ props.row.downloader }}</dd>
</template>
<div class="detail-actions">
<el-button size="small"
type="danger"
plain
:icon="TrashIcon"
@click="deleteGallery(props.row.gid)"
:disabled="props.row.status !== '下载完成'">删除任务</el-button>
</div>
</dl>
</template>
<el-option value="myCollect" label="我的收藏"/>
<el-option value="myDownload" label="我的下载"/>
<el-option value="total" label="全部"/>
</el-select>
<el-select v-model="sortType" @change="changeSortType" style="width: 170px">
<template #prefix>
排序
</template>
<el-option value="name" label="名字"/>
<el-option value="shortName" label="简洁名字"/>
<el-option value="createTime" label="任务创建时间"/>
</el-select>
<el-select v-model="galleryNameType" @change="changeGalleryNameType">
<template #prefix>
显示
</template>
<el-option value="name" label="名字"/>
<el-option value="shortName" label="简洁名字"/>
</el-select>
</el-col>
<el-col>
<el-button @click="toMin">{{min}}</el-button>
<el-button @click="previous">-</el-button>
<el-input v-model="targetPage"
@change="changePage"
v-show="isEditingPage"
@blur="reverseEditMode"
class="page"
ref="inputNode"></el-input>
<span @click="reverseEditMode" v-show="!isEditingPage" class="page">{{page}}</span>
<el-button @click="next">+</el-button>
<el-button @click="toMax">{{max}}</el-button>
</el-col>
</el-row>
</el-table-column>
<el-table-column label="名字" min-width="220">
<template #default="scoped">
<span class="task-name">
{{galleryNameType === 'shortName' ? scoped.row.shortName : scoped.row.name}}
</span>
<span class="task-size">{{ scoped.row.fileSize }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="176">
<template #default="scoped">
<div class="state-cell">
<span class="status-pill" :class="statusClass(scoped.row.status)">{{ scoped.row.status }}</span>
<template v-if="progressPercent(scoped.row) !== null">
<el-progress :percentage="progressPercent(scoped.row)"
:stroke-width="5"
:show-text="false" />
<span class="progress-value">{{ progressLabel(scoped.row) }}</span>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="168" align="right">
<template #default="scoped">
<div class="row-actions">
<el-tooltip content="下载文件" placement="top" :show-after="400">
<el-button size="small"
type="primary"
:icon="DownloadIcon"
:disabled="scoped.row.status !== '下载完成'"
@click="downloadTask(scoped.row.download)"
aria-label="下载文件"></el-button>
</el-tooltip>
<el-tooltip content="在线看" placement="top" :show-after="400">
<el-button size="small"
:icon="EyeIcon"
@click="readOnlineGallery(scoped.row)"
aria-label="在线看"></el-button>
</el-tooltip>
<el-tooltip :content="scoped.row.isCollect ? '取消收藏' : '收藏'"
placement="top"
:show-after="400">
<el-button size="small"
:icon="StarIcon"
:class="{ 'is-collected': scoped.row.isCollect }"
@click="changeGalleryCollect(scoped.row.gid, scoped.row.isCollect)"
:aria-label="scoped.row.isCollect ? '取消收藏' : '收藏'"></el-button>
</el-tooltip>
<el-tooltip content="重试任务" placement="top" :show-after="400">
<el-button v-if="scoped.row.status !== '下载完成'"
size="small"
:icon="RefreshIcon"
:loading="retryingGids.has(scoped.row.gid)"
@click="retryGallery(scoped.row.gid)"
aria-label="重试任务"></el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="list-toolbar">
<template v-if="username !== 'test'">
<el-select v-model="category" @change="changeCategory" style="width: 128px">
<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: 150px">
<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" style="width: 132px">
<template #prefix>显示</template>
<el-option value="name" label="名字"/>
<el-option value="shortName" label="简洁名字"/>
</el-select>
</template>
<span class="toolbar-spacer"></span>
<div class="pager">
<el-button size="small" :icon="FirstIcon" :disabled="page <= min" @click="toMin" aria-label="第一页"></el-button>
<el-button size="small" :icon="PrevIcon" :disabled="page <= min" @click="previous" aria-label="上一页"></el-button>
<span class="pager-readout">
<el-input v-model="targetPage"
@change="changePage"
v-show="isEditingPage"
@blur="reverseEditMode"
class="page-input"
size="small"
ref="inputNode"></el-input>
<button v-show="!isEditingPage"
type="button"
class="page-current"
@click="reverseEditMode">{{ page }}</button>
<span class="page-total">/ {{ max }}</span>
</span>
<el-button size="small" :icon="NextIcon" :disabled="page >= max" @click="next" aria-label="下一页"></el-button>
<el-button size="small" :icon="LastIcon" :disabled="page >= max" @click="toMax" aria-label="最后一页"></el-button>
</div>
</div>
</div>
<OnlineReader/>
@@ -107,8 +168,22 @@
<script setup>
import store from "../store";
import {computed, ref, watch} from "vue";
import {computed, h, ref, watch} from "vue";
import OnlineReader from "./OnlineReader.vue";
import AppIcon from "./AppIcon.vue";
// el-button takes a component through :icon, so wrap the shared icon component.
const icon = (name) => ({ render: () => h(AppIcon, { name }) });
const DownloadIcon = icon('download');
const EyeIcon = icon('eye');
const StarIcon = icon('star');
const RefreshIcon = icon('refresh');
const TrashIcon = icon('trash');
const FirstIcon = icon('chevrons-left');
const PrevIcon = icon('chevron-left');
const NextIcon = icon('chevron-right');
const LastIcon = icon('chevrons-right');
//输入
let inputNode = ref(null)
@@ -141,7 +216,40 @@ let currentTasks = computed(() => {
return store.getters.currentTasks ? store.getters.currentTasks: null
})
let tableHeight = computed(() => 'calc(100vh - 130px)')
const connectionText = computed(() => ({
connected: '进度已连接',
connecting: '正在连接进度',
reconnecting: '连接中断,正在重连',
disconnected: '进度连接已断开',
})[store.state.connectionStatus])
const connectionTagType = computed(() =>
store.state.connectionStatus === 'connected' ? 'success' : 'warning'
)
// Reserve room for the header and the toolbar so a long list scrolls internally
// instead of pushing the pager out of the viewport.
const tableMaxHeight = computed(() => 'calc(100vh - 170px)')
// States come from the backend as Chinese labels; map them to pill colours.
function statusClass(status) {
if (status === '下载完成') return 'is-done'
if (status === '下载中' || status === '压缩中') return 'is-running'
return 'is-waiting'
}
// Only in-flight downloads have a meaningful numeric percentage.
function progressPercent(task) {
if (task.status !== '下载中' || !task.pages) return null
const done = Number(task.proceeding) || 0
return Math.min(100, Math.max(0, Math.round((done / task.pages) * 100)))
}
function progressLabel(task) {
const percent = progressPercent(task)
if (percent === null) return task.progress ?? ''
return `${percent}% · ${task.proceeding ?? 0}/${task.pages} 页`
}
let min = computed(() => {
return store.getters.min
@@ -257,8 +365,17 @@ function showThumbnail(gallery){
<style scoped>
.side-table-wrapper {
height: 100%;
padding: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.load_complete {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
padding: 16px 16px 0;
overflow: hidden;
}
.loading-text {
@@ -269,25 +386,18 @@ function showThumbnail(gallery){
font-size: 14px;
}
.pageChanger {
margin-top: 24px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.page {
display: inline-block;
width: 50px;
width: 46px;
cursor: pointer;
text-align: center;
}
.detail-info {
line-height: 1.8;
.page.el-input {
width: 56px;
}
.detail-info > span {
display: block;
.page:hover {
color: var(--accent);
}
</style>
+6 -2
View File
@@ -498,8 +498,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){
+534 -76
View File
@@ -1,55 +1,100 @@
/* ========== CSS Custom Properties ========== */
/* ========== Design Tokens ========== */
:root {
--bg-body: #f0f2f5;
/* Neutrals: warm-tinted grey, keeps the UI quiet on long sessions. */
--bg-body: #f5f5f4;
--bg-surface: #ffffff;
--bg-card: #ffffff;
--bg-hover: #f5f7fa;
--bg-hover: #f2f2f0;
--bg-sunken: #fafaf9;
--text-primary: #1d2129;
--text-secondary: #86909c;
--text-disabled: #c9cdd4;
--text-primary: #1f2328;
--text-secondary: #6b7280;
--text-disabled: #c2c6cd;
--border-color: #e5e6eb;
--border-color: #e4e4e2;
--border-strong: #d5d6d3;
--border-radius: 8px;
--border-radius-sm: 4px;
--border-radius-sm: 6px;
--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.08);
--shadow-elevated: 0 4px 16px rgba(0, 0, 0, 0.12);
/* Accent: one blue, used for primary actions and progress only. */
--accent: #2563eb;
--accent-hover: #1d4ed8;
--accent-soft: #eff4ff;
--transition-fast: 0.2s ease;
--transition-normal: 0.3s ease;
/* Status colours keep state readable without shouting. */
--status-success: #15803d;
--status-success-soft: #eaf7ee;
--status-warning: #b45309;
--status-warning-soft: #fdf3e3;
--status-info: #1d4ed8;
--status-info-soft: #eaf1fe;
--status-danger: #b42318;
--status-danger-soft: #fdeceb;
--shadow-card: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06);
--shadow-elevated: 0 8px 24px rgba(16, 24, 40, 0.12);
--transition-fast: 0.15s ease;
--transition-normal: 0.25s ease;
}
html.dark {
--bg-body: #1a1a2e;
--bg-surface: #16213e;
--bg-card: #1a1a2e;
--bg-hover: #1f2b47;
--bg-body: #12141a;
--bg-surface: #191c23;
--bg-card: #191c23;
--bg-hover: #22262f;
--bg-sunken: #15181e;
--text-primary: #e5e6eb;
--text-secondary: #8b95a5;
--text-disabled: #4e5969;
--text-primary: #e8eaee;
--text-secondary: #9aa3b2;
--text-disabled: #565d69;
--border-color: #2c3e50;
--shadow-card: 0 1px 4px rgba(0, 0, 0, 0.3);
--shadow-elevated: 0 4px 16px rgba(0, 0, 0, 0.4);
--border-color: #2a2f3a;
--border-strong: #363c49;
--accent: #3b82f6;
--accent-hover: #60a5fa;
--accent-soft: #1c2534;
--status-success: #4ade80;
--status-success-soft: #16281d;
--status-warning: #fbbf24;
--status-warning-soft: #2a2113;
--status-info: #60a5fa;
--status-info-soft: #172234;
--status-danger: #f87171;
--status-danger-soft: #2b1a1a;
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.5);
}
/* ========== Global Base ========== */
* {
box-sizing: border-box;
}
html,
body,
#app {
height: 100%;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-body);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 16px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
"PingFang SC", "Microsoft YaHei", Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
transition: background-color var(--transition-normal), color var(--transition-normal);
}
#app {
min-height: 100vh;
background-color: var(--bg-body);
transition: background-color var(--transition-normal);
min-height: 100%;
}
/* ========== Auth Screen ========== */
@@ -60,116 +105,529 @@ body {
display: flex;
justify-content: center;
align-items: center;
padding: 24px;
background: var(--bg-body);
}
.auth-card {
background: var(--bg-surface);
border-radius: 16px;
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: var(--shadow-elevated);
padding: 56px 48px 44px;
width: 440px;
max-width: 90vw;
text-align: center;
padding: 40px 36px 32px;
width: 400px;
max-width: 100%;
}
.auth-card h1 {
margin: 0 0 8px;
font-size: 32px;
.auth-brand {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 24px;
}
.auth-brand .auth-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 8px;
background: var(--accent);
color: #fff;
font-size: 15px;
font-weight: 700;
flex: none;
}
.auth-brand .auth-name {
font-size: 17px;
font-weight: 600;
color: var(--text-primary);
}
.auth-card h1 {
margin: 0 0 6px;
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
text-align: left;
}
.auth-card .auth-subtitle {
margin: 0 0 36px;
font-size: 15px;
margin: 0 0 24px;
font-size: 13px;
color: var(--text-secondary);
text-align: left;
}
.auth-card .auth-form {
display: flex;
flex-direction: column;
gap: 18px;
gap: 14px;
}
.auth-card .auth-form .el-checkbox {
height: auto;
}
.auth-card .auth-btn {
width: 100%;
font-size: 15px;
padding: 12px 0;
margin-top: 4px;
}
/* ========== Dashboard Grid ========== */
.dashboard {
padding: 24px;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
height: 100%;
/* ========== Page Shell ========== */
.app-layout {
height: 100vh;
overflow: hidden;
box-sizing: border-box;
}
.side-area {
background: var(--bg-surface);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-area {
padding: 0;
overflow-y: auto;
overflow-x: hidden;
background-color: var(--bg-body);
}
.section-title {
margin: 0;
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.04em;
text-transform: uppercase;
}
/* ========== Task List (left column) ========== */
.scroll-panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
padding: 16px 16px 0;
overflow: hidden;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-bottom: 12px;
flex: none;
}
.panel-head .head-meta {
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
}
.panel-head .panel-head-spacer {
flex: 1;
min-width: 8px;
}
.table-scroll {
flex: 1;
min-height: 0;
}
.task-table {
width: 100%;
}
.task-table .el-table__cell {
padding: 10px 0;
}
.task-table .el-table__body-wrapper,
.task-table .el-table__header-wrapper {
overflow-x: hidden;
}
.task-table .cell {
padding-left: 10px;
padding-right: 10px;
}
.task-name {
font-size: 14px;
color: var(--text-primary);
line-height: 1.45;
word-break: break-word;
/* Two-line clamp keeps every row the same height for easy scanning. */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.task-size {
display: block;
margin-top: 2px;
font-size: 12px;
color: var(--text-secondary);
}
.row-actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.status-pill {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.status-pill.is-done {
color: var(--status-success);
background: var(--status-success-soft);
}
.status-pill.is-running {
color: var(--status-info);
background: var(--status-info-soft);
}
.status-pill.is-waiting {
color: var(--status-warning);
background: var(--status-warning-soft);
}
.state-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.state-cell .el-progress {
width: 100%;
}
.state-cell .progress-value {
font-size: 12px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
.detail-info {
display: grid;
grid-template-columns: 84px 1fr;
gap: 4px 12px;
padding: 10px 12px 12px 44px;
font-size: 13px;
line-height: 1.7;
}
.detail-info dt {
color: var(--text-secondary);
}
.detail-info dd {
margin: 0;
color: var(--text-primary);
word-break: break-all;
}
.detail-actions {
grid-column: 1 / -1;
padding-top: 8px;
}
/* ========== Toolbar under the list ========== */
.list-toolbar {
flex: none;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
padding: 12px 0 16px;
margin-top: 12px;
border-top: 1px solid var(--border-color);
}
.list-toolbar .toolbar-spacer {
flex: 1;
min-width: 8px;
}
.pager {
display: inline-flex;
align-items: center;
gap: 4px;
}
.pager .el-button + .el-button {
margin-left: 0;
}
.pager-readout {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0 6px;
font-size: 13px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
.pager-readout .page-total {
white-space: nowrap;
}
button.page-current {
min-width: 34px;
height: 24px;
padding: 0 6px;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-surface);
color: var(--text-primary);
font-size: 13px;
font-weight: 600;
font-family: inherit;
font-variant-numeric: tabular-nums;
cursor: pointer;
transition: border-color var(--transition-fast), color var(--transition-fast);
}
button.page-current:hover {
border-color: var(--accent);
color: var(--accent);
}
.page-input {
width: 56px;
}
/* Icon-only row actions keep the operation column narrow. */
.row-actions .el-button.is-circle,
.row-actions .el-button {
width: 32px;
height: 32px;
padding: 0;
}
.row-actions .el-button + .el-button {
margin-left: 0;
}
.row-actions .el-button.is-collected {
color: var(--status-warning);
border-color: var(--status-warning);
background: var(--status-warning-soft);
}
/* ========== Main column ========== */
.dashboard {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
max-width: 1100px;
margin: 0 auto;
padding: 20px;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
align-items: start;
}
.control-card {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-card);
padding: 24px;
transition: box-shadow var(--transition-fast);
align-self: start;
padding: 16px;
}
.control-card:hover {
box-shadow: var(--shadow-elevated);
}
.card-wide {
grid-column: 1 / -1;
}
.control-card h3 {
margin: 0 0 16px;
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
.control-card .card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.control-card .card-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
gap: 8px;
align-items: center;
}
.control-card .usage-text {
font-size: 15px;
.usage-line {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 14px;
}
.usage-line .usage-value {
font-size: 22px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.6;
font-variant-numeric: tabular-nums;
}
.control-card .thumbnail-body {
text-align: center;
.usage-line .usage-note {
font-size: 12px;
color: var(--text-secondary);
}
.control-card .gallery-name {
margin: 0 0 12px;
font-size: 14px;
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 13px;
color: var(--text-primary);
}
.thumbnail-body {
display: flex;
flex-direction: column;
gap: 10px;
}
.thumbnail-body .gallery-name {
margin: 0;
font-size: 13px;
color: var(--text-secondary);
word-break: break-all;
}
/* ========== Element Plus Overrides ========== */
/* increase default button/input text size */
.thumbnail-frame {
background: var(--bg-sunken);
border-radius: var(--border-radius-sm);
overflow: hidden;
}
/* ========== Dialogs ========== */
.detail-layout {
display: grid;
grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
gap: 20px;
align-items: start;
}
.detail-thumb {
width: 100%;
height: 200px;
border-radius: var(--border-radius-sm);
background: var(--bg-sunken);
}
.detail-list {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 6px 12px;
margin: 0;
font-size: 13px;
line-height: 1.6;
}
.detail-list dt {
color: var(--text-secondary);
}
.detail-list dd {
margin: 0;
color: var(--text-primary);
word-break: break-word;
}
.mono-value {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
word-break: break-all;
}
.config-form .section-title {
margin: 20px 0 12px;
}
.config-form .section-title:first-child {
margin-top: 0;
}
.config-form .el-form-item {
margin-bottom: 14px;
}
.field-hint {
margin-left: 10px;
font-size: 12px;
color: var(--text-secondary);
}
/* ========== Element Plus harmonisation ========== */
.el-button {
font-weight: 500;
}
.el-button--small {
font-size: 13px;
}
.el-button--primary {
--el-button-bg-color: var(--accent);
--el-button-border-color: var(--accent);
--el-button-hover-bg-color: var(--accent-hover);
--el-button-hover-border-color: var(--accent-hover);
}
.el-select {
font-size: 13px;
}
.el-input__inner {
font-size: 14px;
}
.el-table {
font-size: 14px;
--el-table-border-color: var(--border-color);
--el-table-header-bg-color: var(--bg-surface);
--el-table-row-hover-bg-color: var(--bg-hover);
}
.el-table th.el-table__cell {
background-color: var(--bg-surface);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.el-dialog {
border-radius: 12px;
}
.el-dialog__title {
font-size: 16px;
font-weight: 600;
}