- 左侧导航轨 + 主视图 + 常驻详情检查器,替换原左右两栏平铺卡片结构 - 拆分为 TaskView / TaskRow / SearchView / SettingsView / DetailPanel / NavRail / AuthScreen,删除 DashBoard、Side、HentaiSearch - 在线看改为全屏阅读器:适应高度/宽度、常驻翻页栏、缩略图与标题信息, 翻页改为状态驱动,不再点击内部 DOM;补键盘快捷键与大图查看 - 提交弹层默认选中原图(无原图时选体积最大的一个) - 管理员可见「下载人」筛选与详情里的下载人昵称,判定改用后端 isAdmin - store.deleteGallery 返回 promise,便于删除后清理选中态 - 新增 thumbnail 工具与 4 项单测
255 lines
8.9 KiB
Vue
255 lines
8.9 KiB
Vue
<script setup>
|
|
import {computed, ref, watch} from "vue";
|
|
import {ElMessage} from "element-plus";
|
|
import store from "../store/index.js";
|
|
import AppIcon from "./AppIcon.vue";
|
|
import {ehThumbnailUrl, pickDefaultResolution} from "../utils/thumbnail.js";
|
|
|
|
const targetResolution = ref("");
|
|
const isRetrying = ref(false);
|
|
|
|
// Two flows share this panel: a task already in the list, and a remote gallery
|
|
// that was resolved from a link or from the online search.
|
|
const task = computed(() => store.state.selectedGallery);
|
|
const remote = computed(() => store.state.chosenGallery || null);
|
|
|
|
// A freshly resolved gallery is a pending decision, so it takes the panel over
|
|
// even if a task was selected earlier.
|
|
const showRemote = computed(() => Boolean(remote.value));
|
|
|
|
watch(remote, gallery => {
|
|
targetResolution.value = gallery
|
|
? pickDefaultResolution(gallery.availableResolution)
|
|
: "";
|
|
});
|
|
|
|
// Admin-only fields come from the backend's isAdmin flag, not a hardcoded id.
|
|
const isAdmin = computed(() => store.state.isAdmin);
|
|
|
|
const isDone = computed(() => Boolean(task.value && task.value.status === "下载完成"));
|
|
|
|
const statusClass = computed(() => {
|
|
const status = task.value?.status;
|
|
if (status === "下载完成") return "is-done";
|
|
if (status === "下载中" || status === "压缩中") return "is-running";
|
|
return "is-waiting";
|
|
});
|
|
|
|
const percent = computed(() => {
|
|
const item = task.value;
|
|
if (!item || item.status !== "下载中" || !item.pages) return null;
|
|
const done = Number(item.proceeding) || 0;
|
|
return Math.min(100, Math.max(0, Math.round((done / item.pages) * 100)));
|
|
});
|
|
|
|
const resolutions = computed(() => {
|
|
const gallery = remote.value;
|
|
if (!gallery || !gallery.availableResolution) return [];
|
|
return Object.entries(gallery.availableResolution).map(([resolution, fileSize]) => ({
|
|
resolution,
|
|
fileSize,
|
|
}));
|
|
});
|
|
|
|
// Remote galleries carry a raw path; the proxy URL needs the active auth code.
|
|
const remoteThumb = computed(() => {
|
|
const gallery = remote.value;
|
|
if (!gallery) return "";
|
|
return ehThumbnailUrl(gallery.thumb_link || gallery.thumbnailUrl, store.state.AuthCode);
|
|
});
|
|
|
|
function closeInspector() {
|
|
if (remote.value) store.commit("_setChosenGallery", {gallery: false});
|
|
else store.commit("_selectGallery", null);
|
|
}
|
|
|
|
function downloadTask() {
|
|
if (!task.value) return;
|
|
if (task.value.download) window.open(task.value.download);
|
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
|
}
|
|
|
|
function downloadRemote() {
|
|
const gallery = remote.value;
|
|
if (!gallery) return;
|
|
if (gallery.download) window.open(gallery.download);
|
|
else ElMessage({message: "下载地址不存在,请刷新任务后重试", type: "error"});
|
|
}
|
|
|
|
function readOnline(gallery) {
|
|
if (!gallery) return;
|
|
store.dispatch("readOnlineGallery", gallery);
|
|
}
|
|
|
|
function toggleCollect() {
|
|
if (!task.value) return;
|
|
const gallery = task.value;
|
|
if (gallery.isCollect) store.dispatch("disCollectGallery", gallery.gid);
|
|
else store.dispatch("collectGallery", gallery.gid);
|
|
}
|
|
|
|
function deleteTask() {
|
|
if (!task.value) return;
|
|
store.dispatch("deleteGallery", task.value.gid).then(() => store.commit("_selectGallery", null));
|
|
}
|
|
|
|
async function retryTask() {
|
|
if (!task.value || isRetrying.value) return;
|
|
isRetrying.value = true;
|
|
try {
|
|
await store.dispatch("retryGallery", task.value.gid);
|
|
} finally {
|
|
isRetrying.value = false;
|
|
}
|
|
}
|
|
|
|
function submitTask() {
|
|
const gallery = remote.value;
|
|
if (!gallery) return;
|
|
if (targetResolution.value === "") {
|
|
ElMessage("请选择分辨率再提交");
|
|
return;
|
|
}
|
|
store.dispatch("postGalleryTask", {
|
|
link: gallery.link,
|
|
targetResolution: targetResolution.value,
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<aside class="inspector" aria-label="详情">
|
|
<template v-if="showRemote">
|
|
<header class="inspector-bar">
|
|
<h2 class="section-title">远端任务</h2>
|
|
<span class="view-bar-spacer"></span>
|
|
<button type="button" class="icon-button" title="取消" aria-label="取消" @click="closeInspector">
|
|
<AppIcon name="close" :size="15" />
|
|
</button>
|
|
</header>
|
|
|
|
<div class="inspector-scroll">
|
|
<div class="preview-frame">
|
|
<img v-if="remoteThumb" :src="remoteThumb" alt="" loading="lazy">
|
|
<span v-else class="preview-empty">
|
|
<AppIcon name="image" :size="22" />无缩略图
|
|
</span>
|
|
</div>
|
|
|
|
<h3 class="inspector-title">{{ remote.name }}</h3>
|
|
|
|
<div class="inspector-badges">
|
|
<span v-if="remote.status" class="status-pill is-waiting">{{ remote.status }}</span>
|
|
<span v-if="remote.pages" class="inspector-badge">{{ remote.pages }} 页</span>
|
|
<span v-if="remote.fileSize" class="inspector-badge">{{ remote.fileSize }}</span>
|
|
</div>
|
|
|
|
<template v-if="resolutions.length">
|
|
<p class="section-title inspector-label">目标分辨率</p>
|
|
<div class="resolution-list">
|
|
<button v-for="item in resolutions"
|
|
:key="item.resolution"
|
|
type="button"
|
|
:aria-pressed="targetResolution === item.resolution"
|
|
@click="targetResolution = item.resolution">
|
|
<span>{{ item.resolution }}</span>
|
|
<span class="resolution-size">{{ item.fileSize }}</span>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="inspector-actions is-remote">
|
|
<template v-if="resolutions.length">
|
|
<el-button type="primary" @click="submitTask">提交下载</el-button>
|
|
<el-button @click="readOnline(remote)">
|
|
<AppIcon name="eye" :size="15" />在线看
|
|
</el-button>
|
|
</template>
|
|
<template v-else>
|
|
<el-button type="primary" @click="downloadRemote">
|
|
<AppIcon name="download" :size="15" />下载文件
|
|
</el-button>
|
|
<el-button @click="readOnline(remote)">
|
|
<AppIcon name="eye" :size="15" />在线看
|
|
</el-button>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else-if="task">
|
|
<header class="inspector-bar">
|
|
<h2 class="section-title">任务详情</h2>
|
|
<span class="view-bar-spacer"></span>
|
|
<button type="button" class="icon-button" title="收起详情" aria-label="收起详情" @click="closeInspector">
|
|
<AppIcon name="close" :size="15" />
|
|
</button>
|
|
</header>
|
|
|
|
<div class="inspector-scroll">
|
|
<div class="preview-frame">
|
|
<img v-if="task.thumb_link" :src="task.thumb_link" alt="" loading="lazy">
|
|
<span v-else class="preview-empty">
|
|
<AppIcon name="image" :size="22" />无缩略图
|
|
</span>
|
|
</div>
|
|
|
|
<h3 class="inspector-title">{{ task.name }}</h3>
|
|
|
|
<div class="inspector-badges">
|
|
<span class="status-pill" :class="statusClass">{{ task.status }}</span>
|
|
<span class="inspector-badge">{{ task.pages }} 页</span>
|
|
<span class="inspector-badge">{{ task.language }}</span>
|
|
<span class="inspector-badge">{{ task.fileSize }}</span>
|
|
</div>
|
|
|
|
<div v-if="percent !== null" class="progress-line">
|
|
<el-progress :percentage="percent" :stroke-width="5" :show-text="false" />
|
|
<span class="progress-value">{{ percent }}% · {{ task.proceeding }}/{{ task.pages }} 页</span>
|
|
</div>
|
|
|
|
<dl class="inspector-facts">
|
|
<dt>分辨率</dt>
|
|
<dd>{{ task.resolution || "—" }}</dd>
|
|
<dt>创建时间</dt>
|
|
<dd>{{ task.createTimeDisplay || "—" }}</dd>
|
|
<dt>文件大小</dt>
|
|
<dd>{{ task.fileSize || "—" }}</dd>
|
|
<dt>原始页面</dt>
|
|
<dd>
|
|
<a v-if="task.link" :href="task.link" target="_blank" rel="noopener">打开链接</a>
|
|
<span v-else>—</span>
|
|
</dd>
|
|
<template v-if="isAdmin">
|
|
<dt>下载人</dt>
|
|
<dd>{{ task.downloaderName || `#${task.downloader}` }}</dd>
|
|
</template>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="inspector-actions">
|
|
<el-button type="primary" :disabled="!isDone" @click="downloadTask">
|
|
<AppIcon name="download" :size="15" />下载
|
|
</el-button>
|
|
<el-button @click="readOnline(task)">
|
|
<AppIcon name="eye" :size="15" />在线看
|
|
</el-button>
|
|
<el-button :class="{'is-collected': task.isCollect}" @click="toggleCollect">
|
|
<AppIcon name="star" :size="15" />{{ task.isCollect ? "已收藏" : "收藏" }}
|
|
</el-button>
|
|
<el-button v-if="!isDone" :loading="isRetrying" @click="retryTask">
|
|
<AppIcon name="refresh" :size="15" />重试
|
|
</el-button>
|
|
<el-button type="danger" plain :disabled="!isDone" @click="deleteTask">
|
|
<AppIcon name="trash" :size="15" />删除
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-else class="inspector-empty">
|
|
<AppIcon name="image" :size="26" />
|
|
<p>选择一条任务查看详情</p>
|
|
</div>
|
|
</aside>
|
|
</template>
|