feat: 新增多语言图标生成脚本与视图切换功能,优化分页与暗黑模式

1. 新增Python/JS版图标生成脚本,批量创建标准化SVG图标
2. 重构页面视图切换逻辑,添加平滑过渡动画
3. 升级登录日志分页系统,支持页码跳转与页大小调整
4. 完善暗黑模式样式适配,覆盖更多UI组件
5. 优化海报展示适配,根据图片比例自动调整布局
This commit is contained in:
liRQ 2026-07-01 18:22:21 +08:00
parent a889b83959
commit 42146aedf4
5 changed files with 1343 additions and 54 deletions

BIN
create_icons.py Normal file

Binary file not shown.

20
gen_icons.js Normal file
View File

@ -0,0 +1,20 @@
const fs = require('fs');
const path = require('path');
const iconsDir = path.join(__dirname, 'public', 'icons');
if (!fs.existsSync(iconsDir)) fs.mkdirSync(iconsDir, { recursive: true });
const icons = {
home: { path: 'M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z M9 22v-10l6 6v-10', label: 'Home' },
search: { d: 'circle cx=11 cy=11 r=8 line x1=21 y1=21 x2=16.65 y2=16.65', label: 'Search' },
user: { d: 'path d=M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2 circle cx=12 cy=7 r=4', label: 'User' },
folder: { d: 'path d=M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z', fill: true, label: 'Folder' },
file: { d: 'path d=M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z polyline points=14 2 14 8 20 8', label: 'File' },
settings: { d: 'circle cx=12 cy=12 r=3 path d=M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0', label: 'Settings' },
close: { d: 'line x1=18 y1=6 x2=6 y2=18 line x1=6 y1=6 x2=18 y2=18', label: 'Close' },
check: { d: 'polyline points=20 6 9 17 4 12', color: '#22c55e', label: 'Check' },
delete: { d: 'polyline points=3 6 5 6 21 6 path d=M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2', color: '#ef4444', label: 'Delete' },
add: { d: 'circle cx=12 cy=12 r=10 line x1=12 y1=8 x2=12 y2=16 line x1=8 y1=12 x2=16 y2=12', label: 'Add' },
download: { d: 'path d=M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 polyline points=7 10 12 15 17 10 line x1=12 y1=15 x2=12 y2=3', label: 'Download' },
upload: { d: 'path d=M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4 polyline points=17 8 12 3 7 8 line x1=12 y1=3 x2=12 y2=15', label: 'Upload' },
share: { d: 'circle cx=18 cy=5 r=3 circle cx=6 cy=12 r=3 circle cx=18 cy=19 r=3 line x1=8.59 y1=13.51 x2=15.42 y2=17.49 line x1=15.41 y1=6.51 x2=8.59 y2=10.49', label: 'Share' },
favorite: { d: 'path d=M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z', fill: true, label: 'Favorite' },
cloud: { d: 'path d=M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z', label: 'Cloud' },

175
gen_icons.py Normal file
View File

@ -0,0 +1,175 @@
# Icon Generator Script
import os
icons_dir = 'public/icons'
os.makedirs(icons_dir, exist_ok=True)
# Color scheme
PRIMARY_COLOR = '#6366f1'
SUCCESS_COLOR = '#22c55e'
DANGER_COLOR = '#ef4444'
def create_svg(filename, elements, color=PRIMARY_COLOR, fill=False):
"""Create an SVG icon file"""
fill_attr = 'fill "{}"'.format(color) if fill else 'fill="none"'
svg_content = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" {} stroke="{}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
{}
</svg>'''.format(fill_attr, color, '\n'.join(elements))
filepath = os.path.join(icons_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(svg_content)
print(f'Created: {filename}')
# Home icon
create_svg('home.svg', [
'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
'<polyline points="9 22 9 12 15 12 15 22"/>'
])
# Search icon
create_svg('search.svg', [
'<circle cx="11" cy="11" r="8"/>',
'<line x1="21" y1="21" x2="16.65" y2="16.65"/>'
])
# Settings icon
create_svg('settings.svg', [
'<circle cx="12" cy="12" r="3"/>',
'<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>'
])
# User icon
create_svg('user.svg', [
'<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>',
'<circle cx="12" cy="7" r="4"/>'
])
# Folder icon (filled)
create_svg('folder.svg', [
'<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>'
], fill=True)
# File icon
create_svg('file.svg', [
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>',
'<polyline points="14 2 14 8 20 8"/>'
])
# Download icon
create_svg('download.svg', [
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
'<polyline points="7 10 12 15 17 10"/>',
'<line x1="12" y1="15" x2="12" y2="3"/>'
])
# Upload icon
create_svg('upload.svg', [
'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>',
'<polyline points="17 8 12 3 7 8"/>',
'<line x1="12" y1="3" x2="12" y2="15"/>'
])
# Share icon
create_svg('share.svg', [
'<circle cx="18" cy="5" r="3"/>',
'<circle cx="6" cy="12" r="3"/>',
'<circle cx="18" cy="19" r="3"/>',
'<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>',
'<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>'
])
# Favorite/Like icon (filled)
create_svg('favorite.svg', [
'<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>'
], fill=True)
# Delete icon
create_svg('delete.svg', [
'<polyline points="3 6 5 6 21 6"/>',
'<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>'
], color=DANGER_COLOR)
# Edit icon
create_svg('edit.svg', [
'<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>',
'<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>'
])
# Add icon
create_svg('add.svg', [
'<circle cx="12" cy="12" r="10"/>',
'<line x1="12" y1="8" x2="12" y2="16"/>',
'<line x1="8" y1="12" x2="16" y2="12"/>'
])
# Back/Arrow Left icon
create_svg('back.svg', [
'<line x1="19" y1="12" x2="5" y2="12"/>',
'<polyline points="12 19 5 12 12 5"/>'
])
# More/Options icon (three dots)
create_svg('more.svg', [
'<circle cx="12" cy="12" r="1"/>',
'<circle cx="19" cy="12" r="1"/>',
'<circle cx="5" cy="12" r="1"/>'
], fill=True)
# Cloud icon
create_svg('cloud.svg', [
'<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>'
])
# Drive/Server icon
create_svg('drive.svg', [
'<rect x="2" y="6" width="20" height="12" rx="2"/>',
'<circle cx="12" cy="12" r="2"/>',
'<path d="M6 12h.01 M18 12h.01"/>'
])
# Log/File Text icon
create_svg('log.svg', [
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>',
'<polyline points="14 2 14 8 20 8"/>',
'<line x1="16" y1="13" x2="8" y2="13"/>',
'<line x1="16" y1="17" x2="8" y2="17"/>',
'<polyline points="10 9 9 9 8 9"/>'
])
# Close icon
create_svg('close.svg', [
'<line x1="18" y1="6" x2="6" y2="18"/>',
'<line x1="6" y1="6" x2="18" y2="18"/>'
])
# Check/Verify icon
create_svg('check.svg', [
'<polyline points="20 6 9 17 4 12"/>'
], color=SUCCESS_COLOR)
# Play icon
create_svg('play.svg', [
'<polygon points="5 3 19 12 5 21 5 3"/>'
], fill=True)
# Pause icon
create_svg('pause.svg', [
'<rect x="6" y="4" width="4" height="16"/>',
'<rect x="14" y="4" width="4" height="16"/>'
])
# Eye icon
create_svg('eye.svg', [
'<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>',
'<circle cx="12" cy="12" r="3"/>'
])
# Eye Off icon
create_svg('eye-off.svg', [
'<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>',
'<line x1="1" y1="1" x2="23" y2="23"/>'
])
print(f'\nTotal icons created: {len(os.listdir(icons_dir)) - 1}') # Exclude test.txt
print('Icon generation complete!')

View File

@ -179,13 +179,10 @@
</button>
</div>
<div class="user-menu__divider"></div>
<button type="button" class="user-menu__item" @click="switchToPoster">
<span class="user-menu__icon">🎬</span>
<span>海报展示</span>
</button>
<button type="button" class="user-menu__item" @click="handleSwitchToManage">
<span class="user-menu__icon">📋</span>
<span>控制台</span>
<button type="button" class="user-menu__item user-menu__item--toggle" @click="toggleView" :title="currentView === 'manage' ? '切换到海报展示' : '切换到控制台'">
<span class="user-menu__icon">{{ currentView === 'manage' ? '🎬' : '📋' }}</span>
<span>{{ currentView === 'manage' ? '海报展示' : '控制台' }}</span>
<span class="user-menu__toggle-icon"></span>
</button>
<button type="button" class="user-menu__item" @click="handleTmdbConfig">
<span class="user-menu__icon"></span>
@ -213,10 +210,12 @@
</section>
<header class="topbar dashboard-topbar">
<div class="dashboard-brand">
<strong>{{ currentView === 'manage' ? '控制台' : '海报展示' }}</strong>
<span>{{ currentView === 'manage' ? '管理网盘资源与账号数据' : '根据网盘内容进行 TMDB 海报刮削展示' }}</span>
</div>
<Transition name="title-slide" mode="out-in">
<div class="dashboard-brand" :key="currentView">
<strong>{{ currentView === 'manage' ? '控制台' : '海报展示' }}</strong>
<span>{{ currentView === 'manage' ? '管理网盘资源与账号数据' : '根据网盘内容进行 TMDB 海报刮削展示' }}</span>
</div>
</Transition>
</header>
<section v-if="currentView === 'poster' && !hasSearched && !isSearchFocused" class="hero-carousel" aria-label="每日推荐海报">
@ -246,7 +245,8 @@
:src="activeFeaturedPoster.posterUrl"
:alt="activeFeaturedPoster.title"
class="hero-bg-image"
@load="heroImageLoaded = true"
:class="{ 'hero-bg-image--wide': isHeroWideDynamic }"
@load="handleHeroImageLoad"
/>
<div class="hero-main__overlay">
@ -299,9 +299,11 @@
</Transition>
<template v-if="!isLoading && currentView === 'manage' && !isSearchFocused">
<!-- 快捷图标区域 -->
<section ref="quickIconsRef" v-if="iconList.length || !hasSearched" class="quick-icons">
<Transition name="view-slide" mode="out-in">
<div :key="currentView">
<template v-if="!isLoading && currentView === 'manage' && !isSearchFocused">
<!-- 快捷图标区域 -->
<section ref="quickIconsRef" v-if="iconList.length || !hasSearched" class="quick-icons">
<div class="quick-icons__header">
<h3>快捷入口</h3>
<div class="quick-icons__actions">
@ -557,27 +559,29 @@
正在查询刮削状态...
</div>
</SimpleDialog>
</template>
</template>
<template v-else-if="!isLoading && currentView === 'poster' && !hasSearched && !isSearchFocused">
<section class="poster-showcase">
<div class="content-toolbar">
<div>
<h2>海报展示</h2>
<p>根据网盘管理中的标题与海报地址进行 TMDB 匹配展示可作为刮削结果预览</p>
<template v-else-if="!isLoading && currentView === 'poster' && !hasSearched && !isSearchFocused">
<section class="poster-showcase">
<div class="content-toolbar">
<div>
<h2>海报展示</h2>
<p>根据网盘管理中的标题与海报地址进行 TMDB 匹配展示可作为刮削结果预览</p>
</div>
</div>
</div>
<div v-if="loadingPosterShowcase" class="poster-list-loading">
正在加载海报数据...
</div>
<div v-else class="poster-list poster-list--showcase">
<div v-for="item in posterShowcaseData" :key="item.id" class="poster-card-wrap">
<PosterCard :item="item" :readonly="true" />
<div v-if="loadingPosterShowcase" class="poster-list-loading">
正在加载海报数据...
</div>
</div>
</section>
</template>
<div v-else class="poster-list poster-list--showcase">
<div v-for="item in posterShowcaseData" :key="item.id" class="poster-card-wrap">
<PosterCard :item="item" :readonly="true" />
</div>
</div>
</section>
</template>
</div>
</Transition>
<!-- 海报展示的搜索结果 -->
<Transition name="fade" mode="out-in">
<template v-if="hasSearched && currentView === 'poster'">
@ -763,21 +767,60 @@
</div>
</div>
<!-- 分页信息和加载更多按钮 -->
<!-- 分页控制栏 -->
<div v-if="logPagination.total > 0" class="log-pagination">
<div class="pagination-info">
已加载 {{ loginLogs.length }} / {{ logPagination.total }}
{{ logPagination.total }}
</div>
<div class="pagination-pagesize">
<select v-model.number="logPagination.pageSize" @change="handleLogPageChange">
<option :value="10">10/</option>
<option :value="20">20/</option>
<option :value="50">50/</option>
<option :value="100">100/</option>
</select>
</div>
<div class="pagination-nav">
<button
type="button"
class="pagination-btn pagination-btn--prev"
:disabled="logPagination.pageNo <= 1"
@click="handleLogPageChangeTo(logPagination.pageNo - 1)"
>
</button>
<span class="pagination-current-page">
{{ logPagination.pageNo }} / {{ logPagination.totalPages }}
</span>
<button
type="button"
class="pagination-btn pagination-btn--next"
:disabled="logPagination.pageNo >= logPagination.totalPages"
@click="handleLogPageChangeTo(logPagination.pageNo + 1)"
>
</button>
</div>
<div class="pagination-go">
前往
<input
v-model.number="logGoToPage"
type="number"
class="go-page-input"
min="1"
:max="logPagination.totalPages"
@keyup.enter="handleGoToPage"
/>
<button
type="button"
class="pagination-btn pagination-btn--go"
@click="handleGoToPage"
:disabled="logGoToPage < 1 || logGoToPage > logPagination.totalPages"
>
确定
</button>
</div>
<button
v-if="logPagination.hasMore"
type="button"
class="load-more-btn"
@click="handleLoadMoreLogs"
:disabled="loadingLogs"
>
{{ loadingLogs ? '加载中...' : '加载更多' }}
</button>
<div v-else class="no-more"> 已加载全部 </div>
</div>
</template>
</div>
@ -974,6 +1017,7 @@ const confirmConfig = reactive({
})
const showLogDialog = ref(false)
const loadingLogs = ref(false)
const logGoToPage = ref(1) //
const logFilter = reactive({
username: '', //
result: '', // success/fail
@ -1070,6 +1114,8 @@ const loadingRecommend = ref(false)
//
const heroImageLoaded = ref(false)
//
const isHeroWideDynamic = ref(false)
let heroImagePreloadTimer = null
//
@ -1701,13 +1747,113 @@ async function handleOpenLogDialog() {
logPagination.pageNo = 1
logPagination.pageSize = 20
logPagination.total = 0
logPagination.hasMore = true
logGoToPage.value = 1
//
rawData.value.loginLogs = []
//
await loadMoreLogs()
await loadLogPage(true)
}
//
const logTotalPages = computed(() => {
if (logPagination.total === 0) return 1
return Math.ceil(logPagination.total / logPagination.pageSize)
})
//
async function loadLogPage(reset = false) {
if (loadingLogs.value) return
loadingLogs.value = true
try {
const params = {
pageNo: logPagination.pageNo,
pageSize: logPagination.pageSize
}
// userType=2
if (currentUser.userType !== 2) {
params.userId = currentUser.userId
}
//
if (logFilter.username.trim()) {
params.username = logFilter.username.trim()
}
if (logFilter.result) {
params.result = logFilter.result
}
if (logFilter.startDate) {
params.startDate = logFilter.startDate
}
if (logFilter.endDate) {
params.endDate = logFilter.endDate
}
const logsRes = await fetchLoginLogs(params)
if (Array.isArray(logsRes?.data?.list)) {
const newLogs = logsRes.data.list
//
if (reset) {
if (rawData.value?.loginLogs) {
rawData.value.loginLogs = newLogs
} else {
rawData.value.loginLogs = newLogs
}
} else {
if (rawData.value?.loginLogs) {
rawData.value.loginLogs.push(...newLogs)
} else {
rawData.value.loginLogs = newLogs
}
}
//
logPagination.total = logsRes.data.total || 0
console.log(`成功加载第 ${logPagination.pageNo} 页,共 ${newLogs.length} 条日志,总计 ${logPagination.total}`)
} else {
console.warn('登录日志返回数据异常', logsRes)
}
} catch (err) {
console.error('加载登录日志失败', err)
setErrorMessage(err?.message || '加载登录日志失败')
} finally {
loadingLogs.value = false
}
}
//
async function handleLogPageChange() {
//
logPagination.pageNo = 1
logGoToPage.value = 1
rawData.value.loginLogs = []
await loadLogPage(true)
}
//
async function handleLogPageChangeTo(page) {
const totalPages = logTotalPages.value
if (page < 1 || page > totalPages) return
logPagination.pageNo = page
logGoToPage.value = page
rawData.value.loginLogs = []
await loadLogPage(true)
}
//
async function handleGoToPage() {
const totalPages = logTotalPages.value
const targetPage = Math.max(1, Math.min(logGoToPage.value || 1, totalPages))
if (targetPage === logPagination.pageNo) return
logGoToPage.value = targetPage
await handleLogPageChangeTo(targetPage)
}
//
@ -2698,15 +2844,30 @@ function preloadImage(url) {
const img = new Image()
img.onload = () => {
// > 1.2
const aspectRatio = img.naturalWidth / img.naturalHeight
isHeroWideDynamic.value = aspectRatio > 1.2
heroImageLoaded.value = true
}
img.onerror = () => {
//
isHeroWideDynamic.value = false
heroImageLoaded.value = true
}
img.src = url
}
//
function handleHeroImageLoad(event) {
const img = event.target
if (img && img.naturalWidth && img.naturalHeight) {
// > 1.2
const aspectRatio = img.naturalWidth / img.naturalHeight
isHeroWideDynamic.value = aspectRatio > 1.2
}
heroImageLoaded.value = true
}
/**
* 刷新当前网盘文件夹列表
*/

File diff suppressed because it is too large Load Diff