feat(webui): update settings, downloader, player, and log page styles

Restyle config components (ab-container, ab-fold-panel, ab-label,
ab-setting) and all config-* setting panels to use the new design
system. Add empty state guides with setup steps for downloader and
player pages. Simplify log page layout.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
EstrellaXD
2026-01-23 17:52:13 +01:00
parent 2a757e8f2c
commit 5a1a1ccfcf
17 changed files with 378 additions and 149 deletions

View File

@@ -25,47 +25,35 @@ withDefaults(
<style lang="scss" scoped>
.container-card {
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
box-shadow: var(--shadow-sm);
transition: border-color var(--transition-normal),
box-shadow var(--transition-normal);
transition: border-color var(--transition-normal);
}
.container-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
height: 42px;
background: var(--color-primary);
color: #fff;
padding: 0 14px;
height: 34px;
background: transparent;
color: var(--color-text-secondary);
border-bottom: 1px solid var(--color-border);
user-select: none;
@include forPC {
height: 48px;
}
transition: color var(--transition-normal),
border-color var(--transition-normal);
}
.container-title {
font-size: 16px;
font-weight: 500;
@include forPC {
font-size: 18px;
}
font-size: 15px;
font-weight: 600;
}
.container-body {
padding: 14px;
padding: 12px 14px;
background: var(--color-surface);
color: var(--color-text);
font-size: 14px;
transition: background-color var(--transition-normal),
color var(--transition-normal);
@include forPC {
padding: 20px;
}
}
</style>

View File

@@ -5,28 +5,28 @@ import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
withDefaults(
defineProps<{
title: string;
defaultOpen?: boolean;
}>(),
{
title: 'title',
defaultOpen: true,
}
);
</script>
<template>
<Disclosure v-slot="{ open }">
<Disclosure v-slot="{ open }" :default-open="defaultOpen">
<div class="fold-panel">
<DisclosureButton class="fold-panel-header">
<div class="fold-panel-title">{{ title }}</div>
<Component :is="open ? Up : Down" :size="18" />
<Component :is="open ? Up : Down" :size="14" />
</DisclosureButton>
<div class="fold-panel-body" :class="[open && 'fold-panel-body--open']">
<div v-show="!open" class="fold-panel-divider"></div>
<DisclosurePanel>
<DisclosurePanel>
<div class="fold-panel-body">
<slot></slot>
</DisclosurePanel>
</div>
</div>
</DisclosurePanel>
</div>
</Disclosure>
</template>
@@ -34,7 +34,6 @@ withDefaults(
<style lang="scss" scoped>
.fold-panel {
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
transition: border-color var(--transition-normal);
}
@@ -44,50 +43,28 @@ withDefaults(
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 20px;
height: 42px;
background: var(--color-primary);
color: #fff;
padding: 0 14px;
height: 34px;
background: transparent;
color: var(--color-text-secondary);
border: none;
border-bottom: 1px solid var(--color-border);
cursor: pointer;
@include forPC {
height: 48px;
}
transition: color var(--transition-normal),
border-color var(--transition-normal);
}
.fold-panel-title {
font-size: 16px;
font-weight: 500;
@include forPC {
font-size: 18px;
}
font-size: 15px;
font-weight: 600;
}
.fold-panel-body {
background: var(--color-surface);
padding: 10px 8px;
padding: 12px 14px;
font-size: 14px;
color: var(--color-text);
transition: background-color var(--transition-normal),
color var(--transition-normal);
&--open {
padding: 16px 20px;
}
@include forPC {
&--open {
padding: 20px;
}
}
}
.fold-panel-divider {
width: 100%;
height: 1px;
background: var(--color-border);
margin: 12px 0;
}
</style>

View File

@@ -24,14 +24,14 @@ const abLabel = computed(() => {
<style lang="scss" scoped>
.label-row {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
min-height: 32px;
}
.label-text {
font-size: 14px;
color: var(--color-text);
padding-top: 4px;
transition: color var(--transition-normal);
}
</style>

View File

@@ -40,6 +40,6 @@ const data = defineModel<any>('data');
</div>
</ab-label>
<div v-if="bottomLine" line my-12></div>
<div v-if="bottomLine" line my-6></div>
</div>
</template>

View File

@@ -65,7 +65,7 @@ const items: SettingItem<Downloader>[] = [
<template>
<ab-fold-panel :title="$t('config.downloader_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -43,7 +43,7 @@ const items: SettingItem<BangumiManage>[] = [
<template>
<ab-fold-panel :title="$t('config.manage_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -51,7 +51,7 @@ const logItems: SettingItem<Log> = {
<template>
<ab-fold-panel :title="$t('config.normal_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in programItems"
:key="i.configKey"

View File

@@ -52,7 +52,7 @@ const items: SettingItem<Notification>[] = [
<template>
<ab-fold-panel :title="$t('config.notification_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -85,7 +85,7 @@ const azureItems: SettingItem<ExperimentalOpenAI>[] = [
<span>{{ $t('config.experimental_openai_set.warning') }}</span>
</div>
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in openAI.api_type === 'azure' ? azureItems : openAIItems"
:key="i.configKey"

View File

@@ -33,7 +33,7 @@ const items: SettingItem<RssParser>[] = [
<template>
<ab-fold-panel :title="$t('config.parser_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -64,7 +64,7 @@ function formatDate(dateString: string | null): string {
<template>
<ab-fold-panel :title="$t('passkey.title')">
<div space-y-12>
<div space-y-8>
<!-- 不支持提示 -->
<div v-if="!isSupported" text-orange-500 text-14>
{{ $t('passkey.not_supported') }}

View File

@@ -4,7 +4,7 @@ const { types, type, url } = storeToRefs(usePlayerStore());
<template>
<ab-fold-panel :title="$t('config.media_player_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-model:data="type"
type="select"

View File

@@ -64,7 +64,7 @@ const items: SettingItem<Proxy>[] = [
<template>
<ab-fold-panel :title="$t('config.proxy_set.title')">
<div space-y-12>
<div space-y-8>
<ab-setting
v-for="i in items"
:key="i.configKey"

View File

@@ -58,7 +58,7 @@ onActivated(() => {
.config-grid {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
gap: 12px;
margin-bottom: auto;
@media (min-width: 1024px) {
@@ -69,14 +69,19 @@ onActivated(() => {
.config-col {
display: flex;
flex-direction: column;
gap: 20px;
gap: 12px;
}
.config-actions {
position: sticky;
bottom: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
margin-top: 16px;
padding: 12px 0;
backdrop-filter: blur(8px);
background: color-mix(in srgb, var(--color-background) 80%, transparent);
}
</style>

View File

@@ -25,13 +25,42 @@ onActivated(() => {
<template>
<div class="page-embed">
<template v-if="isNull">
<div class="embed-empty">
<RouterLink to="/config" class="embed-link">
{{ $t('downloader.hit') }}
</RouterLink>
<div v-if="isNull" class="empty-guide">
<div class="empty-guide-header anim-fade-in">
<div class="empty-guide-title">{{ $t('downloader.empty.title') }}</div>
<div class="empty-guide-subtitle">{{ $t('downloader.empty.subtitle') }}</div>
</div>
</template>
<div class="empty-guide-steps">
<div class="empty-guide-step anim-slide-up" style="--delay: 0.15s">
<div class="empty-guide-step-number">1</div>
<div class="empty-guide-step-content">
<div class="empty-guide-step-title">{{ $t('downloader.empty.step1_title') }}</div>
<div class="empty-guide-step-desc">{{ $t('downloader.empty.step1_desc') }}</div>
</div>
</div>
<div class="empty-guide-step anim-slide-up" style="--delay: 0.3s">
<div class="empty-guide-step-number">2</div>
<div class="empty-guide-step-content">
<div class="empty-guide-step-title">{{ $t('downloader.empty.step2_title') }}</div>
<div class="empty-guide-step-desc">{{ $t('downloader.empty.step2_desc') }}</div>
</div>
</div>
<div class="empty-guide-step anim-slide-up" style="--delay: 0.45s">
<div class="empty-guide-step-number">3</div>
<div class="empty-guide-step-content">
<div class="empty-guide-step-title">{{ $t('downloader.empty.step3_title') }}</div>
<div class="empty-guide-step-desc">{{ $t('downloader.empty.step3_desc') }}</div>
</div>
</div>
</div>
<RouterLink to="/config" class="empty-guide-action anim-slide-up" style="--delay: 0.6s">
{{ $t('sidebar.config') }}
</RouterLink>
</div>
<iframe
v-else
@@ -51,26 +80,6 @@ onActivated(() => {
flex-direction: column;
}
.embed-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.embed-link {
font-size: 24px;
color: var(--color-primary);
text-decoration: none;
transition: color var(--transition-fast);
&:hover {
text-decoration: underline;
color: var(--color-primary-hover);
}
}
.embed-frame {
width: 100%;
height: 100%;
@@ -78,4 +87,129 @@ onActivated(() => {
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
}
.empty-guide {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 24px;
}
.empty-guide-header {
text-align: center;
margin-bottom: 32px;
}
.empty-guide-title {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 6px;
}
.empty-guide-subtitle {
font-size: 14px;
color: var(--color-text-secondary);
}
.empty-guide-steps {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 400px;
width: 100%;
}
.empty-guide-step {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-surface);
transition: background-color var(--transition-normal),
border-color var(--transition-normal);
}
.empty-guide-step-number {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.empty-guide-step-content {
flex: 1;
min-width: 0;
}
.empty-guide-step-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 4px;
}
.empty-guide-step-desc {
font-size: 13px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.empty-guide-action {
margin-top: 24px;
padding: 8px 24px;
border-radius: var(--radius-md);
background: var(--color-primary);
color: #fff;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: background-color var(--transition-fast);
&:hover {
background: var(--color-primary-hover);
}
}
.anim-fade-in {
animation: fadeIn 0.5s ease both;
}
.anim-slide-up {
animation: slideUp 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: var(--delay, 0s);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@@ -182,18 +182,18 @@ onDeactivated(() => {
}
.log-layout {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: 1fr;
gap: 12px;
align-items: start;
@media (min-width: 1024px) {
grid-template-columns: 3fr 2fr;
}
}
.log-main {
flex-grow: 1;
min-width: 0;
@media (min-width: 1024px) {
min-width: 660px;
}
}
.log-viewer {
@@ -202,16 +202,11 @@ onDeactivated(() => {
overflow: auto;
padding: 10px;
max-height: 60vh;
min-height: 20vh;
transition: border-color var(--transition-normal);
}
.log-content {
min-width: 0;
@media (min-width: 1024px) {
min-width: 450px;
}
}
.log-entry {
@@ -265,15 +260,10 @@ onDeactivated(() => {
}
.log-sidebar {
flex-grow: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 20px;
@media (min-width: 1024px) {
min-width: 400px;
}
gap: 12px;
}
.contact-list {

View File

@@ -8,13 +8,43 @@ const { url } = storeToRefs(usePlayerStore());
<template>
<div class="page-embed">
<template v-if="url === ''">
<div class="embed-empty">
<RouterLink to="/config" class="embed-link">
{{ $t('player.hit') }}
</RouterLink>
<div v-if="url === ''" class="empty-guide">
<div class="empty-guide-header anim-fade-in">
<div class="empty-guide-title">{{ $t('player.empty.title') }}</div>
<div class="empty-guide-subtitle">{{ $t('player.empty.subtitle') }}</div>
</div>
</template>
<div class="empty-guide-steps">
<div class="empty-guide-step anim-slide-up" style="--delay: 0.15s">
<div class="empty-guide-step-number">1</div>
<div class="empty-guide-step-content">
<div class="empty-guide-step-title">{{ $t('player.empty.step1_title') }}</div>
<div class="empty-guide-step-desc">{{ $t('player.empty.step1_desc') }}</div>
</div>
</div>
<div class="empty-guide-step anim-slide-up" style="--delay: 0.3s">
<div class="empty-guide-step-number">2</div>
<div class="empty-guide-step-content">
<div class="empty-guide-step-title">{{ $t('player.empty.step2_title') }}</div>
<div class="empty-guide-step-desc">{{ $t('player.empty.step2_desc') }}</div>
</div>
</div>
<div class="empty-guide-step anim-slide-up" style="--delay: 0.45s">
<div class="empty-guide-step-number">3</div>
<div class="empty-guide-step-content">
<div class="empty-guide-step-title">{{ $t('player.empty.step3_title') }}</div>
<div class="empty-guide-step-desc">{{ $t('player.empty.step3_desc') }}</div>
</div>
</div>
</div>
<RouterLink to="/config" class="empty-guide-action anim-slide-up" style="--delay: 0.6s">
{{ $t('sidebar.config') }}
</RouterLink>
</div>
<iframe
v-else
:src="url"
@@ -33,26 +63,6 @@ const { url } = storeToRefs(usePlayerStore());
flex-direction: column;
}
.embed-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.embed-link {
font-size: 24px;
color: var(--color-primary);
text-decoration: none;
transition: color var(--transition-fast);
&:hover {
text-decoration: underline;
color: var(--color-primary-hover);
}
}
.embed-frame {
width: 100%;
height: 100%;
@@ -60,4 +70,129 @@ const { url } = storeToRefs(usePlayerStore());
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
}
.empty-guide {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 24px;
}
.empty-guide-header {
text-align: center;
margin-bottom: 32px;
}
.empty-guide-title {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 6px;
}
.empty-guide-subtitle {
font-size: 14px;
color: var(--color-text-secondary);
}
.empty-guide-steps {
display: flex;
flex-direction: column;
gap: 16px;
max-width: 400px;
width: 100%;
}
.empty-guide-step {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-surface);
transition: background-color var(--transition-normal),
border-color var(--transition-normal);
}
.empty-guide-step-number {
flex-shrink: 0;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--color-primary);
color: #fff;
font-size: 13px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.empty-guide-step-content {
flex: 1;
min-width: 0;
}
.empty-guide-step-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 4px;
}
.empty-guide-step-desc {
font-size: 13px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.empty-guide-action {
margin-top: 24px;
padding: 8px 24px;
border-radius: var(--radius-md);
background: var(--color-primary);
color: #fff;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: background-color var(--transition-fast);
&:hover {
background: var(--color-primary-hover);
}
}
.anim-fade-in {
animation: fadeIn 0.5s ease both;
}
.anim-slide-up {
animation: slideUp 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: var(--delay, 0s);
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>