功能:新增电影简介、导演和主演信息展示,优化刮削日志样式

This commit is contained in:
liRQ 2026-06-27 13:14:42 +08:00
parent e4a0ac71cc
commit a9066b9046
5 changed files with 920 additions and 29 deletions

View File

@ -513,6 +513,19 @@
<div v-if="scrapeStatusData.tags" class="scrape-status-tags"> <div v-if="scrapeStatusData.tags" class="scrape-status-tags">
<span v-for="tag in (typeof scrapeStatusData.tags === 'string' ? scrapeStatusData.tags.split(',') : scrapeStatusData.tags)" :key="tag">{{ tag }}</span> <span v-for="tag in (typeof scrapeStatusData.tags === 'string' ? scrapeStatusData.tags.split(',') : scrapeStatusData.tags)" :key="tag">{{ tag }}</span>
</div> </div>
<!-- 电影简介 -->
<div v-if="scrapeStatusData.overview" class="scrape-status-overview">
<span class="scrape-status-label">简介</span>
<span class="scrape-status-value">{{ scrapeStatusData.overview }}</span>
</div>
<!-- 导演 -->
<div v-if="scrapeStatusData.directors" class="scrape-status-meta">
<span>导演{{ scrapeStatusData.directors }}</span>
</div>
<!-- 主演 -->
<div v-if="scrapeStatusData.casts" class="scrape-status-meta">
<span>主演{{ scrapeStatusData.casts }}</span>
</div>
</div> </div>
<div v-else class="scrape-status-loading"> <div v-else class="scrape-status-loading">
正在查询刮削状态... 正在查询刮削状态...
@ -614,12 +627,23 @@
<!-- 详细日志可展开 --> <!-- 详细日志可展开 -->
<Transition name="slide-down"> <Transition name="slide-down">
<div v-if="log.showDetail && log.detailList && log.detailList.length > 0" class="log-entry__detail"> <div v-if="log.showDetail && log.detailList && log.detailList.length > 0" class="log-entry__detail">
<div v-for="(item, idx) in log.detailList" :key="idx" class="detail-item"> <template v-for="(item, idx) in log.detailList" :key="idx">
<div class="detail-item detail-item--user">
<span class="detail-item__name">👤 {{ item.username || '用户 ' + (item.userId || idx) }}</span> <span class="detail-item__name">👤 {{ item.username || '用户 ' + (item.userId || idx) }}</span>
<span class="detail-item__status detail-item__status--info"> <span class="detail-item__status detail-item__status--info">
处理: {{ item.processed || 0 }} | 成功: {{ item.success || 0 }} | 失败: {{ item.failed || 0 }} | 跳过: {{ item.skipped || 0 }} 处理: {{ item.processed || 0 }} | 成功: {{ item.success || 0 }} | 失败: {{ item.failed || 0 }} | 跳过: {{ item.skipped || 0 }}
</span> </span>
</div> </div>
<!-- 显示具体的消息列表 -->
<div v-if="item.messages && item.messages.length > 0" class="detail-item__messages">
<div v-for="(msg, msgIdx) in item.messages" :key="msgIdx" class="message-item">
<span :class="['message-item__indicator', getMessageStatus(msg)]">
{{ getMessageStatus(msg) === 'success' ? '✓' : getMessageStatus(msg) === 'failed' ? '✗' : '⊘' }}
</span>
<span class="message-item__text">{{ msg }}</span>
</div>
</div>
</template>
</div> </div>
</Transition> </Transition>
</div> </div>
@ -1923,10 +1947,11 @@ function openEditRecord(item) {
editingRecord.value = { editingRecord.value = {
id: item.id, id: item.id,
title: item.title || '', title: item.title || '',
subtitle: item.subtitle || '',
originalName: item.originalName || '', originalName: item.originalName || '',
description: item.description || '', description: item.description || '',
category: item.category || '', category: item.category || '',
directors: item.directors || '',
casts: item.casts || '',
updatedAt: item.updatedAt || '', updatedAt: item.updatedAt || '',
poster: item.poster || '', poster: item.poster || '',
tags: [...(item.tags || [])], tags: [...(item.tags || [])],
@ -2001,12 +2026,13 @@ function handleAddFile(parentId) {
editingRecord.value = { editingRecord.value = {
id: null, id: null,
title: '', title: '',
subtitle: '',
tags: [], tags: [],
score: 0, score: 0,
originalName: '', originalName: '',
description: '', description: '',
category: '', category: '',
directors: '',
casts: '',
sheetId: activeSheetId.value, sheetId: activeSheetId.value,
parentId: parentId || null, parentId: parentId || null,
type: 'file', type: 'file',
@ -2331,11 +2357,12 @@ async function handleSaveRecord(record) {
const result = await updateMovieInDrive(driveId, record.id, { const result = await updateMovieInDrive(driveId, record.id, {
title: record.title, title: record.title,
originalName: record.originalName, originalName: record.originalName,
subtitle: record.subtitle,
description: record.description, description: record.description,
rating: record.score, rating: record.score,
tags: Array.isArray(record.tags) ? record.tags.join(',') : '', tags: Array.isArray(record.tags) ? record.tags.join(',') : '',
category: record.category, category: record.category,
directors: record.directors,
casts: record.casts,
}) })
const updatedData = result?.data || {} const updatedData = result?.data || {}
@ -2343,9 +2370,10 @@ async function handleSaveRecord(record) {
...record, ...record,
title: updatedData.title || record.title, title: updatedData.title || record.title,
originalName: updatedData.originalName || record.originalName, originalName: updatedData.originalName || record.originalName,
subtitle: updatedData.subtitle ?? record.subtitle,
description: updatedData.description ?? record.description, description: updatedData.description ?? record.description,
category: updatedData.category ?? record.category, category: updatedData.category ?? record.category,
directors: updatedData.directors ?? record.directors,
casts: updatedData.casts ?? record.casts,
score: Number(updatedData.rating ?? record.score ?? 0), score: Number(updatedData.rating ?? record.score ?? 0),
tags: updatedData.tags ?? record.tags ?? [], tags: updatedData.tags ?? record.tags ?? [],
updatedAt: updatedData.updateTime || updatedData.createTime || record.updatedAt || new Date().toLocaleString('zh-CN', { hour12: false }), updatedAt: updatedData.updateTime || updatedData.createTime || record.updatedAt || new Date().toLocaleString('zh-CN', { hour12: false }),
@ -2488,7 +2516,6 @@ async function loadRecommendMovies() {
recommendMovies.value = result.data.list.map(movie => ({ recommendMovies.value = result.data.list.map(movie => ({
id: movie.id, id: movie.id,
title: movie.title || '未知标题', title: movie.title || '未知标题',
subtitle: movie.subtitle || '',
description: movie.description || '', description: movie.description || '',
posterUrl: movie.posterUrl || '', posterUrl: movie.posterUrl || '',
rating: movie.rating || 0, rating: movie.rating || 0,
@ -2621,6 +2648,9 @@ function handleScrapeStatus(node) {
rating: data.rating, rating: data.rating,
category: data.category, category: data.category,
tags: data.tags, tags: data.tags,
overview: data.overview || '',
casts: data.casts || '',
directors: data.directors || '',
error: data.error || data.message, error: data.error || data.message,
} }
} else { } else {
@ -2640,10 +2670,37 @@ function getStatusText(status) {
scraping: '刮削中', scraping: '刮削中',
success: '刮削成功', success: '刮削成功',
failed: '刮削失败', failed: '刮削失败',
partial: '部分成功',
skipped: '已跳过',
} }
return statusMap[status] || '未知状态' return statusMap[status] || '未知状态'
} }
/**
* 判断刮削消息的状态
* @param {string} msg - 消息文本
* @returns {string} 'success' | 'failed' | 'skipped'
*/
function getMessageStatus(msg) {
if (!msg) return 'skipped'
//
if (msg.includes('刮削成功') || msg.includes('成功') || msg.includes('Successfully')) {
return 'success'
}
//
if (msg.includes('刮削失败') || msg.includes('失败') ||
msg.includes('未找到') || msg.includes('错误') ||
msg.includes('Error') || msg.includes('Failed') ||
msg.includes('不存在') || msg.includes('跳过')) {
return 'failed'
}
//
return 'skipped'
}
// //
function toggleMultiSelectMode() { function toggleMultiSelectMode() {
multiSelectMode.value = !multiSelectMode.value multiSelectMode.value = !multiSelectMode.value

View File

@ -16,10 +16,6 @@
<span>文件名</span> <span>文件名</span>
<input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" /> <input v-model.trim="localRecord.title" type="text" placeholder="请输入电影文件名,如 xxx.mp4" />
</label> </label>
<label>
<span>原始文件名</span>
<input v-model.trim="localRecord.originalName" type="text" placeholder="用于 TMDB 刮削纠错" />
</label>
<label> <label>
<span>所属网盘</span> <span>所属网盘</span>
<select v-model="localRecord.sheetId"> <select v-model="localRecord.sheetId">
@ -31,12 +27,12 @@
<input v-model.number="localRecord.score" type="number" min="0" max="100" placeholder="0-100" /> <input v-model.number="localRecord.score" type="number" min="0" max="100" placeholder="0-100" />
</label> </label>
<label> <label>
<span>副标题</span> <span>导演</span>
<input v-model.trim="localRecord.subtitle" type="text" placeholder="可选" /> <input v-model.trim="localRecord.directors" type="text" placeholder="如 导演1, 导演2" />
</label> </label>
<label> <label>
<span>分类标签</span> <span>主演</span>
<input v-model.trim="localRecord.category" type="text" placeholder="如 科幻 / 动作" /> <input v-model.trim="localRecord.casts" type="text" placeholder="如 主演1, 主演2" />
</label> </label>
<label class="field-span-2"> <label class="field-span-2">
<span>标签</span> <span>标签</span>
@ -98,12 +94,13 @@ function createEmptyRecord() {
return { return {
id: null, id: null,
title: '', title: '',
subtitle: '',
tags: [], tags: [],
score: 0, score: 0,
originalName: '', originalName: '',
description: '', description: '',
category: '', category: '',
directors: '',
casts: '',
sheetId: props.activeSheetId || props.sheets[0]?.id || '', sheetId: props.activeSheetId || props.sheets[0]?.id || '',
parentId: null, parentId: null,
} }
@ -118,9 +115,10 @@ function handleSave() {
...localRecord.value, ...localRecord.value,
title: localRecord.value.title?.trim() || '', title: localRecord.value.title?.trim() || '',
originalName: localRecord.value.originalName?.trim() || '', originalName: localRecord.value.originalName?.trim() || '',
subtitle: localRecord.value.subtitle?.trim() || '',
description: localRecord.value.description?.trim() || '', description: localRecord.value.description?.trim() || '',
category: localRecord.value.category?.trim() || '', category: localRecord.value.category?.trim() || '',
directors: localRecord.value.directors?.trim() || '',
casts: localRecord.value.casts?.trim() || '',
}) })
closeDialog() closeDialog()
} }

View File

@ -0,0 +1,698 @@
<template>
<div class="settings-view">
<div class="settings-header">
<h2>设置</h2>
<p>配置 OpenList 后端接口绑定您的云盘账户</p>
</div>
<div class="settings-card">
<h3 class="settings-card__title">
<span class="settings-card__icon">🔗</span>
OpenList 绑定
</h3>
<form class="settings-form" @submit.prevent="handleBind">
<!-- OpenList 地址 -->
<div class="form-group">
<label class="form-label" for="openlist-url">
<span class="form-label__icon">🌐</span>
<span class="form-label__text">OpenList 地址</span>
</label>
<input
id="openlist-url"
v-model="formData.url"
type="url"
class="form-input"
placeholder="例如https://olist.example.com 或 http://localhost:5575"
required
autocomplete="off"
/>
<span class="form-help">请填写 OpenList 服务的完整访问地址包含协议头</span>
</div>
<!-- 账号 -->
<div class="form-group">
<label class="form-label" for="openlist-username">
<span class="form-label__icon">👤</span>
<span class="form-label__text">账号</span>
</label>
<input
id="openlist-username"
v-model="formData.username"
type="text"
class="form-input"
placeholder="请输入 OpenList 用户名"
required
autocomplete="username"
/>
</div>
<!-- 密码 -->
<div class="form-group">
<label class="form-label" for="openlist-password">
<span class="form-label__icon">🔒</span>
<span class="form-label__text">密码</span>
</label>
<div class="password-input-wrapper">
<input
id="openlist-password"
v-model="formData.password"
:type="showPassword ? 'text' : 'password'"
class="form-input"
placeholder="请输入 OpenList 密码"
required
autocomplete="current-password"
/>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
:title="showPassword ? '隐藏密码' : '显示密码'"
>
{{ showPassword ? '🙈' : '👁️' }}
</button>
</div>
</div>
<!-- 绑定按钮 -->
<div class="form-actions">
<button
type="submit"
class="btn-bind"
:disabled="binding"
:class="{ 'btn-bind--loading': binding }"
>
<span v-if="!binding">🔗 绑定 OpenList</span>
<span v-else class="loading-spinner"> 绑定中...</span>
</button>
<button
type="button"
class="btn-reset"
@click="resetForm"
:disabled="binding"
>
重置
</button>
</div>
</form>
<!-- 绑定状态提示 -->
<Transition name="fade">
<div v-if="bindStatus" class="bind-status" :class="`bind-status--${bindStatus.type}`">
<span class="bind-status__icon">{{ bindStatus.icon }}</span>
<span class="bind-status__text">{{ bindStatus.message }}</span>
</div>
</Transition>
<!-- 已绑定信息 -->
<Transition name="fade">
<div v-if="boundInfo" class="bound-info">
<div class="bound-info__header">
<span class="bound-info__icon"></span>
<span class="bound-info__text">已绑定 OpenList</span>
</div>
<div class="bound-info__details">
<div class="bound-info__item">
<span class="bound-info__label">地址</span>
<span class="bound-info__value">{{ boundInfo.url }}</span>
</div>
<div class="bound-info__item">
<span class="bound-info__label">账号</span>
<span class="bound-info__value">{{ boundInfo.username }}</span>
</div>
<div class="bound-info__item">
<span class="bound-info__label">绑定时间</span>
<span class="bound-info__value">{{ boundInfo.boundAt }}</span>
</div>
</div>
<button
type="button"
class="btn-unbind"
@click="handleUnbind"
>
🗑 解除绑定
</button>
</div>
</Transition>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { bindOpenList, unbindOpenList, fetchOpenListStatus } from '../api'
//
const formData = reactive({
url: '',
username: '',
password: '',
})
//
const binding = ref(false)
const showPassword = ref(false)
const bindStatus = ref(null)
const boundInfo = ref(null)
/**
* 加载已绑定的 OpenList 信息
*/
async function loadBoundInfo() {
try {
const result = await fetchOpenListStatus()
if (result?.code === 200 && result?.data?.bound) {
boundInfo.value = {
url: result.data.url || '',
username: result.data.username || '',
boundAt: result.data.boundAt || '-',
}
}
} catch (err) {
console.warn('加载绑定信息失败:', err)
}
}
/**
* 处理绑定表单提交
*/
async function handleBind() {
if (!formData.url || !formData.username || !formData.password) {
setBindStatus('error', '请填写所有必填字段')
return
}
// URL
try {
new URL(formData.url)
} catch {
setBindStatus('error', '请输入有效的 URL 地址(需包含协议头,如 https://')
return
}
binding.value = true
setBindStatus(null, '')
try {
const result = await bindOpenList({
url: formData.url,
username: formData.username,
password: formData.password,
})
if (result?.code === 200) {
setBindStatus('success', '绑定成功!')
boundInfo.value = {
url: formData.url,
username: formData.username,
boundAt: new Date().toLocaleString('zh-CN'),
}
//
formData.password = ''
//
await loadBoundInfo()
} else {
setBindStatus('error', result?.message || '绑定失败,请稍后重试')
}
} catch (err) {
console.error('绑定 OpenList 失败:', err)
const errorMsg = err?.message || err?.payload?.message || '网络错误,请检查后端连接'
setBindStatus('error', errorMsg)
} finally {
binding.value = false
}
}
/**
* 处理解除绑定
*/
async function handleUnbind() {
if (!confirm('确定要解除 OpenList 绑定吗?')) {
return
}
try {
const result = await unbindOpenList()
if (result?.code === 200) {
setBindStatus('success', '已解除绑定')
boundInfo.value = null
resetForm()
setTimeout(() => {
setBindStatus(null, '')
}, 3000)
} else {
setBindStatus('error', result?.message || '解除绑定失败')
}
} catch (err) {
console.error('解除绑定失败:', err)
setBindStatus('error', err?.message || '网络错误')
}
}
/**
* 重置表单
*/
function resetForm() {
formData.url = ''
formData.username = ''
formData.password = ''
setBindStatus(null, '')
}
/**
* 设置绑定状态提示
*/
function setBindStatus(type, message) {
if (!type) {
bindStatus.value = null
return
}
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: '',
}
bindStatus.value = {
type,
message,
icon: icons[type] || '',
}
// 3
if (type !== 'success') {
setTimeout(() => {
bindStatus.value = null
}, 3000)
}
}
//
onMounted(() => {
loadBoundInfo()
})
</script>
<style scoped>
.settings-view {
max-width: 720px;
margin: 0 auto;
padding: 24px 16px;
}
.settings-header {
margin-bottom: 24px;
}
.settings-header h2 {
font-size: 24px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 8px 0;
}
.settings-header p {
font-size: 14px;
color: #666;
margin: 0;
}
/* 卡片样式 */
.settings-card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid #e8e8e8;
}
.settings-card__title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #1a1a1a;
margin: 0 0 24px 0;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.settings-card__icon {
font-size: 20px;
}
/* 表单样式 */
.form-group {
margin-bottom: 20px;
}
.form-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.form-label__icon {
font-size: 16px;
}
.form-input {
width: 100%;
padding: 10px 14px;
font-size: 14px;
border: 1px solid #d9d9d9;
border-radius: 8px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
.form-input:hover {
border-color: #409eff;
}
.form-input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.15);
}
.form-input::placeholder {
color: #bfbfbf;
}
.form-help {
display: block;
margin-top: 6px;
font-size: 12px;
color: #999;
}
/* 密码输入框 */
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-input-wrapper .form-input {
padding-right: 44px;
}
.password-toggle {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 18px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.password-toggle:hover {
background-color: #f5f5f5;
}
/* 按钮组 */
.form-actions {
display: flex;
gap: 12px;
margin-top: 28px;
padding-top: 20px;
border-top: 1px solid #f0f0f0;
}
.btn-bind {
flex: 1;
padding: 12px 24px;
font-size: 15px;
font-weight: 500;
color: #fff;
background: linear-gradient(135deg, #409eff, #337ecc);
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
min-width: 140px;
}
.btn-bind:hover:not(:disabled) {
background: linear-gradient(135deg, #66b1ff, #409eff);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.btn-bind:active:not(:disabled) {
transform: translateY(0);
}
.btn-bind:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-bind--loading {
background: linear-gradient(135deg, #909399, #69696d);
}
.btn-reset {
padding: 12px 24px;
font-size: 14px;
color: #666;
background: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
min-width: 80px;
}
.btn-reset:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
background: #ecf5ff;
}
.btn-reset:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 状态提示 */
.bind-status {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
margin-top: 20px;
border-radius: 8px;
font-size: 14px;
}
.bind-status--success {
background: #f0f9ff;
border: 1px solid #b3e19c;
color: #67c23a;
}
.bind-status--error {
background: #fef0f0;
border: 1px solid #fbc4c4;
color: #f56c6c;
}
.bind-status__icon {
font-size: 16px;
}
/* 已绑定信息 */
.bound-info {
margin-top: 20px;
padding: 16px;
background: #f0f9ff;
border: 1px solid #b3e19c;
border-radius: 8px;
}
.bound-info__header {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 500;
color: #67c23a;
margin-bottom: 12px;
}
.bound-info__details {
margin-bottom: 16px;
}
.bound-info__item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #666;
margin-bottom: 6px;
}
.bound-info__label {
color: #999;
min-width: 70px;
}
.bound-info__value {
color: #333;
word-break: break-all;
}
.btn-unbind {
width: 100%;
padding: 8px 16px;
font-size: 13px;
color: #f56c6c;
background: #fff;
border: 1px solid #fbc4c4;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-unbind:hover {
background: #fef0f0;
border-color: #f56c6c;
}
/* 加载动画 */
.loading-spinner {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
.settings-header h2 {
color: #e5e5e5;
}
.settings-header p {
color: #999;
}
.settings-card {
background: #2a2a2a;
border-color: #3a3a3a;
}
.settings-card__title {
color: #e5e5e5;
border-bottom-color: #3a3a3a;
}
.form-label {
color: #ccc;
}
.form-input {
background: #1a1a1a;
border-color: #3a3a3a;
color: #e5e5e5;
}
.form-input:hover {
border-color: #409eff;
}
.form-input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
.form-input::placeholder {
color: #666;
}
.password-toggle:hover {
background-color: #333;
}
.btn-reset {
color: #ccc;
background: #333;
border-color: #444;
}
.btn-reset:hover:not(:disabled) {
color: #409eff;
border-color: #409eff;
background: #1a2a3a;
}
.bound-info {
background: #1a2a3a;
border-color: #2a4a5a;
}
.bound-info__item {
color: #999;
}
.bound-info__value {
color: #ccc;
}
.btn-unbind {
color: #f56c6c;
background: #2a1a1a;
border-color: #4a2a2a;
}
.btn-unbind:hover {
background: #3a1a1a;
border-color: #f56c6c;
}
}
/* 响应式 */
@media (max-width: 600px) {
.settings-view {
padding: 16px 12px;
}
.settings-card {
padding: 16px;
}
.form-actions {
flex-direction: column;
}
.btn-bind,
.btn-reset {
width: 100%;
}
}
</style>

View File

@ -886,7 +886,7 @@ canvas {
.hero-carousel { .hero-carousel {
position: relative; position: relative;
padding-bottom: 6px; padding-bottom: 0;
} }
/* ==================== 主海报横幅 ==================== */ /* ==================== 主海报横幅 ==================== */
@ -1625,6 +1625,42 @@ canvas {
margin-left: auto; margin-left: auto;
} }
/* 刮削状态面板 - 新增字段样式 */
.scrape-status-overview {
margin-top: 12px;
padding: 12px 16px;
background: rgba(99, 102, 241, 0.05);
border-radius: 8px;
border-left: 3px solid #6366f1;
}
.scrape-status-label {
font-weight: 700;
color: #6366f1;
margin-right: 8px;
font-size: 14px;
}
.scrape-status-value {
color: #374151;
font-size: 14px;
line-height: 1.6;
}
.scrape-status-meta {
margin-top: 8px;
font-size: 14px;
color: #4b5563;
}
.scrape-status-meta span {
display: inline-block;
padding: 4px 12px;
background: rgba(99, 102, 241, 0.08);
border-radius: 6px;
font-weight: 500;
}
.folder-tree-actions__btn { .folder-tree-actions__btn {
padding: 10px 20px; padding: 10px 20px;
border: 2px dashed var(--line); border: 2px dashed var(--line);
@ -2285,6 +2321,91 @@ canvas {
max-width: 300px; max-width: 300px;
} }
/* 用户详情项 */
.detail-item--user {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: rgba(249, 250, 251, 0.8);
border-radius: 8px;
font-size: 13px;
margin-bottom: 8px;
}
/* 消息列表容器 */
.detail-item__messages {
margin-top: 8px;
padding: 12px;
background: rgba(255, 255, 255, 0.6);
border-radius: 8px;
border: 1px solid rgba(99, 102, 241, 0.08);
display: flex;
flex-direction: column;
gap: 6px;
}
/* 单条消息项 */
.message-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
transition: background 0.15s ease;
}
.message-item:hover {
background: rgba(249, 250, 251, 0.5);
}
/* 消息状态指示器 */
.message-item__indicator {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 12px;
font-weight: 700;
}
/* 成功消息 */
.message-item__indicator.success {
background: rgba(34, 197, 94, 0.12);
color: #15803d;
}
/* 失败消息 */
.message-item__indicator.failed {
background: rgba(239, 68, 68, 0.12);
color: #b91c1c;
}
/* 跳过/未知消息 */
.message-item__indicator.skipped {
background: rgba(100, 116, 139, 0.1);
color: #64748b;
}
/* 消息文本 */
.message-item__text {
flex: 1;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.message-item__text:hover {
white-space: normal;
word-break: break-word;
}
.log-time { .log-time {
font-weight: 600; font-weight: 600;
color: var(--ink); color: var(--ink);
@ -3507,7 +3628,6 @@ canvas {
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
max-height: 300px;
} }
.scrape-status-poster img, .scrape-status-poster img,
@ -3518,8 +3638,22 @@ canvas {
display: block; display: block;
} }
/* 海报图 - 竖版展示(标准海报比例 2:3 */
.scrape-status-poster { .scrape-status-poster {
max-height: 400px; max-height: 500px;
aspect-ratio: 2 / 3;
max-width: 280px;
margin: 0 auto 16px;
}
.scrape-status-poster img {
aspect-ratio: 2 / 3;
}
/* 背景图 - 横版展示(宽屏比例 16:9 */
.scrape-status-backdrop {
max-height: 320px;
aspect-ratio: 16 / 9;
} }
.scrape-status-meta { .scrape-status-meta {
@ -4077,8 +4211,9 @@ body.dark-mode .hero-sub__thumb {
} }
body.dark-mode .hero-sub__card.active .hero-sub__thumb { body.dark-mode .hero-sub__card.active .hero-sub__thumb {
border-color: #6366f1; /* 移除选中状态下的蓝色光圈 */
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.4), 0 4px 16px rgba(0, 0, 0, 0.4); border-color: rgba(51, 65, 85, 0.5);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
} }
body.dark-mode .hero-sub__title { body.dark-mode .hero-sub__title {

View File

@ -8,7 +8,8 @@ export function filterRows(rows, keyword) {
return rows.filter((item) => { return rows.filter((item) => {
const merged = [ const merged = [
item.title, item.title,
item.subtitle, item.directors,
item.casts,
item.accountName, item.accountName,
item.owner, item.owner,
item.sheetName, item.sheetName,
@ -135,7 +136,6 @@ export function createRecord(record = {}, fallbackSheetName = '') {
return { return {
id: record.id || `record-${Date.now()}`, id: record.id || `record-${Date.now()}`,
title: normalizeText(record.title), title: normalizeText(record.title),
subtitle: normalizeText(record.subtitle),
accountName: normalizeText(record.accountName), accountName: normalizeText(record.accountName),
owner: normalizeText(record.owner), owner: normalizeText(record.owner),
updatedAt: normalizeText(record.updatedAt), updatedAt: normalizeText(record.updatedAt),
@ -149,6 +149,8 @@ export function createRecord(record = {}, fallbackSheetName = '') {
originalName: normalizeText(record.originalName), originalName: normalizeText(record.originalName),
description: normalizeText(record.description), description: normalizeText(record.description),
category: normalizeText(record.category), category: normalizeText(record.category),
directors: normalizeText(record.directors),
casts: normalizeText(record.casts),
driveId: record.driveId ?? null, driveId: record.driveId ?? null,
folderId: record.folderId ?? null, folderId: record.folderId ?? null,
minioUrl: normalizeText(record.minioUrl), minioUrl: normalizeText(record.minioUrl),
@ -163,7 +165,6 @@ export function mapMovieToRecord(movie = {}, fallbackSheetName = '') {
return createRecord({ return createRecord({
id: movie.id, id: movie.id,
title: movie.title, title: movie.title,
subtitle: movie.subtitle || '',
accountName: movie.resourceLibrary || '', accountName: movie.resourceLibrary || '',
owner: movie.maintainer || '', owner: movie.maintainer || '',
updatedAt: movie.updateTime || '', updatedAt: movie.updateTime || '',
@ -176,6 +177,8 @@ export function mapMovieToRecord(movie = {}, fallbackSheetName = '') {
type: 'file', type: 'file',
category: movie.category, category: movie.category,
description: movie.description || '', description: movie.description || '',
directors: movie.directors || '',
casts: movie.casts || '',
}, fallbackSheetName) }, fallbackSheetName)
} }