桌面端下载器调优:任务列表信息层级与操作区重排
- 统一 theme.css 视觉变量与卡片、状态标签样式 - DashBoard 与 Side 的任务行重排:状态标签与进度对齐,排序/显示条件更明确 - 新增 AppIcon 图标组件,store 同步空名短标题修复
This commit is contained in:
+4
-14
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user