Files
lionwebsite-frontend-desktop/src/components/DetailPanel.vue
T
root 9d36c5d654 桌面端下载器重构为工作台布局,并重做在线看阅读器
- 左侧导航轨 + 主视图 + 常驻详情检查器,替换原左右两栏平铺卡片结构
- 拆分为 TaskView / TaskRow / SearchView / SettingsView / DetailPanel /
  NavRail / AuthScreen,删除 DashBoard、Side、HentaiSearch
- 在线看改为全屏阅读器:适应高度/宽度、常驻翻页栏、缩略图与标题信息,
  翻页改为状态驱动,不再点击内部 DOM;补键盘快捷键与大图查看
- 提交弹层默认选中原图(无原图时选体积最大的一个)
- 管理员可见「下载人」筛选与详情里的下载人昵称,判定改用后端 isAdmin
- store.deleteGallery 返回 promise,便于删除后清理选中态
- 新增 thumbnail 工具与 4 项单测
2026-09-20 22:54:39 +08:00

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>