mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-04-24 02:20:38 +08:00
fix(webui): improve mobile UI/UX layout and responsiveness
Mobile Layout Fixes: - Fix config page horizontal overflow by adding min-width: 0 constraints - Fix sticky action buttons positioning on config page - Add loading states to config save/cancel buttons - Reduce topbar padding and icon spacing for mobile - Make search button fill space between logo and action icons - Fix search modal header layout with close button visibility Component Improvements: - ab-topbar: Flex search button with "Click to search" text on mobile - ab-status-bar: Reduce button sizes and gaps on mobile - ab-fold-panel: Add max-width and overflow constraints - ab-setting: Add max-width: 100% to prevent input overflow - ab-search-modal: Reduce padding and element sizes on mobile - ab-mobile-nav: Increase label font-size from 10px to 11px - ab-sidebar: Fix toggle animation (rotateY → rotate) Calendar & Bangumi Pages: - Add lazy loading to poster images for better performance - Move unknown air day items to separate section below grid - Add actionable CTA button to empty state with useAddRss hook Login Page: - Add will-change: transform for background animation performance i18n: - Add click_to_search translation key for mobile search button - Add add_rss_btn translation for empty state CTA Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,8 @@ withDefaults(
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: border-color var(--transition-normal);
|
||||
min-width: 0; // Allow panel to shrink below content size
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.fold-panel-header {
|
||||
@@ -64,6 +66,7 @@ withDefaults(
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
overflow-x: hidden;
|
||||
transition: background-color var(--transition-normal),
|
||||
color var(--transition-normal);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,14 @@ const data = defineModel<any>('data');
|
||||
<style lang="scss" scoped>
|
||||
.setting-item {
|
||||
width: 100%;
|
||||
|
||||
// Prevent fixed-width inputs from causing horizontal overflow on mobile
|
||||
:deep(input),
|
||||
:deep(select),
|
||||
:deep(.n-select),
|
||||
:deep(.n-input) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-divider {
|
||||
|
||||
@@ -84,11 +84,11 @@ function abLabel(label: string | (() => string)) {
|
||||
.status-bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 2px;
|
||||
font-size: 18px;
|
||||
|
||||
@include forTablet {
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
@@ -104,11 +104,17 @@ function abLabel(label: string | (() => string)) {
|
||||
background: transparent;
|
||||
border: none;
|
||||
// Ensure minimum touch target
|
||||
min-width: var(--touch-target);
|
||||
min-height: var(--touch-target);
|
||||
padding: 8px;
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
padding: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
@include forTablet {
|
||||
min-width: var(--touch-target);
|
||||
min-height: var(--touch-target);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
transform: scale(1.1);
|
||||
|
||||
@@ -123,7 +123,7 @@ const visibleItems = computed(() => navItems.filter((i) => !i.hidden));
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -269,7 +269,7 @@ function Exit() {
|
||||
transition: transform var(--transition-normal);
|
||||
|
||||
&--open {
|
||||
transform: rotateY(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
PlayOne,
|
||||
Power,
|
||||
Refresh,
|
||||
Search,
|
||||
} from '@icon-park/vue-next';
|
||||
import { ruleTemplate } from '#/bangumi';
|
||||
|
||||
const { t, changeLocale } = useMyI18n();
|
||||
const { running, onUpdate, offUpdate } = useAppInfo();
|
||||
const { showAddRss: showAddRSS, closeAddRss } = useAddRss();
|
||||
const { toggleModal: openSearch } = useSearchStore();
|
||||
const { isMobile } = useBreakpointQuery();
|
||||
|
||||
const showAccount = ref(false);
|
||||
const showAddRSS = ref(false);
|
||||
const rssRule = ref(ruleTemplate);
|
||||
|
||||
const { start, pause, shutdown, restart, resetRule } = useProgramStore();
|
||||
@@ -72,6 +75,7 @@ const onSearchFocus = ref(false);
|
||||
watch(showAddRSS, (val) => {
|
||||
if (!val) {
|
||||
rssRule.value = ruleTemplate;
|
||||
closeAddRss();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,26 +90,39 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<div class="topbar">
|
||||
<div class="topbar-left">
|
||||
<div class="topbar-brand">
|
||||
<img
|
||||
:src="isDark ? '/images/logo-light.svg' : '/images/logo.svg'"
|
||||
alt="favicon"
|
||||
class="topbar-logo"
|
||||
/>
|
||||
<img
|
||||
v-show="onSearchFocus === false"
|
||||
:src="isDark ? '/images/AutoBangumi.svg' : '/images/AutoBangumi-dark.svg'"
|
||||
alt="AutoBangumi"
|
||||
class="topbar-wordmark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="topbar-search">
|
||||
<ab-search-bar />
|
||||
</div>
|
||||
<!-- Logo -->
|
||||
<div class="topbar-brand">
|
||||
<img
|
||||
:src="isDark ? '/images/logo-light.svg' : '/images/logo.svg'"
|
||||
alt="favicon"
|
||||
class="topbar-logo"
|
||||
/>
|
||||
<img
|
||||
v-if="!isMobile"
|
||||
v-show="onSearchFocus === false"
|
||||
:src="isDark ? '/images/AutoBangumi.svg' : '/images/AutoBangumi-dark.svg'"
|
||||
alt="AutoBangumi"
|
||||
class="topbar-wordmark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Desktop search bar -->
|
||||
<div class="topbar-search">
|
||||
<ab-search-bar />
|
||||
</div>
|
||||
|
||||
<!-- Mobile search button (fills space) -->
|
||||
<button
|
||||
v-if="isMobile"
|
||||
class="topbar-mobile-search"
|
||||
:aria-label="$t('topbar.search.click_to_search')"
|
||||
@click="openSearch"
|
||||
>
|
||||
<Search theme="outline" size="18" />
|
||||
<span class="topbar-mobile-search-text">{{ $t('topbar.search.click_to_search') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Right side actions -->
|
||||
<div class="topbar-right">
|
||||
<ab-status-bar
|
||||
:items="items"
|
||||
@@ -127,8 +144,9 @@ onUnmounted(() => {
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: var(--topbar-height);
|
||||
padding: 0 12px;
|
||||
padding: 0 8px;
|
||||
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -139,25 +157,26 @@ onUnmounted(() => {
|
||||
box-shadow var(--transition-normal);
|
||||
|
||||
@include forTablet {
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
@include forDesktop {
|
||||
gap: 16px;
|
||||
padding: 0 20px;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.topbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@include forTablet {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-logo {
|
||||
@@ -184,9 +203,50 @@ onUnmounted(() => {
|
||||
|
||||
@include forTablet {
|
||||
display: block;
|
||||
flex: 1;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-mobile-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface-hover);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-mobile-search-text {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topbar-right {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.topbar-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -388,25 +388,37 @@ function clearFilters() {
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
transition: border-color var(--transition-normal);
|
||||
|
||||
@include forTablet {
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0; // Allow shrinking
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
height: 44px;
|
||||
padding-left: 14px;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
padding-left: 12px;
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
transition: border-color var(--transition-fast), background-color var(--transition-normal);
|
||||
|
||||
@include forTablet {
|
||||
gap: 10px;
|
||||
height: 44px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
@@ -451,19 +463,26 @@ function clearFilters() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
padding: 0 14px;
|
||||
min-width: 90px;
|
||||
padding: 0 10px;
|
||||
min-width: 70px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
@include forTablet {
|
||||
gap: 6px;
|
||||
padding: 0 14px;
|
||||
min-width: 90px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
@@ -515,8 +534,8 @@ function clearFilters() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -525,6 +544,11 @@ function clearFilters() {
|
||||
flex-shrink: 0;
|
||||
transition: all var(--transition-fast);
|
||||
|
||||
@include forTablet {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
|
||||
23
webui/src/hooks/useAddRss.ts
Normal file
23
webui/src/hooks/useAddRss.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Composable to manage the Add RSS modal state globally.
|
||||
* Allows triggering the Add RSS modal from anywhere in the app.
|
||||
*/
|
||||
|
||||
// Global reactive state (shared across all component instances)
|
||||
const showAddRss = ref(false);
|
||||
|
||||
export function useAddRss() {
|
||||
const open = () => {
|
||||
showAddRss.value = true;
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
showAddRss.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
showAddRss,
|
||||
openAddRss: open,
|
||||
closeAddRss: close,
|
||||
};
|
||||
}
|
||||
@@ -132,7 +132,8 @@
|
||||
"step2_title": "Configure Downloader",
|
||||
"step2_desc": "Go to Config and set up your downloader (e.g. qBittorrent) connection.",
|
||||
"step3_title": "Sit Back & Enjoy",
|
||||
"step3_desc": "AutoBangumi will automatically download and rename new episodes for you."
|
||||
"step3_desc": "AutoBangumi will automatically download and rename new episodes for you.",
|
||||
"add_rss_btn": "Add RSS Feed"
|
||||
},
|
||||
"rule": {
|
||||
"apply": "Apply",
|
||||
@@ -430,7 +431,8 @@
|
||||
"reset_rule": "Reset Rule",
|
||||
"restart": "Restart",
|
||||
"search": {
|
||||
"placeholder": "Type to search"
|
||||
"placeholder": "Type to search",
|
||||
"click_to_search": "Click to search"
|
||||
},
|
||||
"shutdown": "Shutdown",
|
||||
"start": "Start"
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
"step2_title": "配置下载器",
|
||||
"step2_desc": "前往设置页面,配置你的下载器(如 qBittorrent)连接信息。",
|
||||
"step3_title": "坐享其成",
|
||||
"step3_desc": "AutoBangumi 将自动下载并重命名新剧集。"
|
||||
"step3_desc": "AutoBangumi 将自动下载并重命名新剧集。",
|
||||
"add_rss_btn": "添加 RSS 订阅"
|
||||
},
|
||||
"rule": {
|
||||
"apply": "应用",
|
||||
@@ -430,7 +431,8 @@
|
||||
"reset_rule": "重置规则",
|
||||
"restart": "重启",
|
||||
"search": {
|
||||
"placeholder": "输入关键字搜索"
|
||||
"placeholder": "输入关键字搜索",
|
||||
"click_to_search": "点击搜索"
|
||||
},
|
||||
"shutdown": "关闭",
|
||||
"start": "启动"
|
||||
|
||||
@@ -7,6 +7,7 @@ definePage({
|
||||
|
||||
const { bangumi, showArchived, isLoading, hasLoaded, activeBangumi, archivedBangumi } = storeToRefs(useBangumiStore());
|
||||
const { getAll, openEditPopup } = useBangumiStore();
|
||||
const { openAddRss } = useAddRss();
|
||||
|
||||
// Show skeleton when initially loading (not yet loaded and loading)
|
||||
const showSkeleton = computed(() => !hasLoaded.value && isLoading.value);
|
||||
@@ -130,6 +131,12 @@ function groupNeedsReview(group: BangumiGroup): boolean {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-guide-action anim-slide-up" style="--delay: 0.6s">
|
||||
<ab-button type="primary" size="big" @click="openAddRss">
|
||||
{{ $t('homepage.empty.add_rss_btn') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bangumi grid -->
|
||||
@@ -597,6 +604,10 @@ function groupNeedsReview(group: BangumiGroup): boolean {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-guide-action {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.empty-guide-step {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@@ -156,35 +156,95 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
</div>
|
||||
|
||||
<!-- Desktop: Grid columns -->
|
||||
<div v-else-if="!isMobile" class="calendar-grid">
|
||||
<div
|
||||
v-for="(key, index) in [...DAY_KEYS, 'unknown']"
|
||||
:key="key"
|
||||
class="calendar-column anim-slide-up"
|
||||
:class="{
|
||||
'calendar-column--today': key !== 'unknown' && isToday(index),
|
||||
'calendar-column--unknown': key === 'unknown'
|
||||
}"
|
||||
:style="{ '--delay': `${index * 0.05}s` }"
|
||||
>
|
||||
<!-- Day header -->
|
||||
<div v-else-if="!isMobile" class="calendar-desktop">
|
||||
<div class="calendar-grid">
|
||||
<div
|
||||
class="calendar-day-header"
|
||||
:class="{ 'calendar-day-header--today': key !== 'unknown' && isToday(index) }"
|
||||
v-for="(key, index) in DAY_KEYS"
|
||||
:key="key"
|
||||
class="calendar-column anim-slide-up"
|
||||
:class="{
|
||||
'calendar-column--today': isToday(index),
|
||||
}"
|
||||
:style="{ '--delay': `${index * 0.05}s` }"
|
||||
>
|
||||
<span class="calendar-day-label">{{ getDayLabel(key) }}</span>
|
||||
<span
|
||||
v-if="key !== 'unknown' && isToday(index)"
|
||||
class="calendar-today-badge"
|
||||
>
|
||||
{{ $t('calendar.today') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Anime cards (grouped) -->
|
||||
<div class="calendar-column-items">
|
||||
<!-- Day header -->
|
||||
<div
|
||||
v-for="group in groupedBangumiByDay[key]"
|
||||
class="calendar-day-header"
|
||||
:class="{ 'calendar-day-header--today': isToday(index) }"
|
||||
>
|
||||
<span class="calendar-day-label">{{ getDayLabel(key) }}</span>
|
||||
<span
|
||||
v-if="isToday(index)"
|
||||
class="calendar-today-badge"
|
||||
>
|
||||
{{ $t('calendar.today') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Anime cards (grouped) -->
|
||||
<div class="calendar-column-items">
|
||||
<div
|
||||
v-for="group in groupedBangumiByDay[key]"
|
||||
:key="group.key"
|
||||
class="calendar-card-wrapper"
|
||||
>
|
||||
<div
|
||||
class="calendar-card"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="`Edit ${group.primary.official_title}`"
|
||||
@click="onCardClick(group)"
|
||||
@keydown.enter="onCardClick(group)"
|
||||
>
|
||||
<div class="calendar-card-poster">
|
||||
<img
|
||||
v-if="group.primary.poster_link"
|
||||
:src="posterSrc(group.primary.poster_link)"
|
||||
:alt="group.primary.official_title"
|
||||
class="calendar-card-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="calendar-card-placeholder">
|
||||
<ErrorPicture theme="outline" size="20" />
|
||||
</div>
|
||||
<div class="calendar-card-overlay">
|
||||
<div class="calendar-card-overlay-tags">
|
||||
<ab-tag :title="`S${group.primary.season}`" type="primary" />
|
||||
<ab-tag
|
||||
v-if="group.primary.group_name"
|
||||
:title="group.primary.group_name"
|
||||
type="primary"
|
||||
/>
|
||||
</div>
|
||||
<div class="calendar-card-overlay-title">{{ group.primary.official_title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="group.rules.length > 1" class="group-badge">
|
||||
{{ group.rules.length }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty day -->
|
||||
<div v-if="groupedBangumiByDay[key].length === 0" class="calendar-empty-day">
|
||||
{{ $t('calendar.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unknown air day section (separate from main grid) -->
|
||||
<div
|
||||
v-if="groupedBangumiByDay.unknown.length > 0"
|
||||
class="calendar-unknown-section anim-slide-up"
|
||||
:style="{ '--delay': '0.4s' }"
|
||||
>
|
||||
<div class="calendar-unknown-header">
|
||||
<span class="calendar-day-label">{{ getDayLabel('unknown') }}</span>
|
||||
</div>
|
||||
<div class="calendar-unknown-items">
|
||||
<div
|
||||
v-for="group in groupedBangumiByDay.unknown"
|
||||
:key="group.key"
|
||||
class="calendar-card-wrapper"
|
||||
>
|
||||
@@ -202,6 +262,7 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
:src="posterSrc(group.primary.poster_link)"
|
||||
:alt="group.primary.official_title"
|
||||
class="calendar-card-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="calendar-card-placeholder">
|
||||
<ErrorPicture theme="outline" size="20" />
|
||||
@@ -223,11 +284,6 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
{{ group.rules.length }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty day -->
|
||||
<div v-if="groupedBangumiByDay[key].length === 0" class="calendar-empty-day">
|
||||
{{ $t('calendar.empty') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -272,6 +328,7 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
:src="posterSrc(group.primary.poster_link)"
|
||||
:alt="group.primary.official_title"
|
||||
class="calendar-row-img"
|
||||
loading="lazy"
|
||||
/>
|
||||
<div v-else class="calendar-row-placeholder">
|
||||
<ErrorPicture theme="outline" size="16" />
|
||||
@@ -412,12 +469,19 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
// Desktop layout
|
||||
.calendar-desktop {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// Desktop grid
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.calendar-column {
|
||||
@@ -435,11 +499,28 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
&--unknown {
|
||||
grid-column: span 2;
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
// Unknown air day section
|
||||
.calendar-unknown-section {
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.calendar-unknown-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.calendar-unknown-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.calendar-day-header {
|
||||
@@ -485,12 +566,6 @@ function onRuleSelect(rule: BangumiRule) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
.calendar-column--unknown & {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Card wrapper for badge positioning
|
||||
|
||||
@@ -4,7 +4,28 @@ definePage({
|
||||
});
|
||||
|
||||
const { getConfig, setConfig } = useConfigStore();
|
||||
const { isMobile, isMobileOrTablet } = useBreakpointQuery();
|
||||
const { isMobileOrTablet } = useBreakpointQuery();
|
||||
|
||||
const isSaving = ref(false);
|
||||
const isResetting = ref(false);
|
||||
|
||||
async function handleSave() {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
await setConfig();
|
||||
} finally {
|
||||
isSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReset() {
|
||||
isResetting.value = true;
|
||||
try {
|
||||
await getConfig();
|
||||
} finally {
|
||||
isResetting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
getConfig();
|
||||
@@ -33,16 +54,22 @@ onActivated(() => {
|
||||
|
||||
<div class="config-actions">
|
||||
<ab-button
|
||||
:size="isMobileOrTablet ? 'big' : 'normal'"
|
||||
:class="[{ 'flex-1': isMobileOrTablet }]"
|
||||
type="warn"
|
||||
@click="getConfig"
|
||||
type="secondary"
|
||||
:loading="isResetting"
|
||||
:disabled="isResetting || isSaving"
|
||||
@click="handleReset"
|
||||
>
|
||||
{{ $t('config.cancel') }}
|
||||
</ab-button>
|
||||
<ab-button
|
||||
:size="isMobileOrTablet ? 'big' : 'normal'"
|
||||
:class="[{ 'flex-1': isMobileOrTablet }]"
|
||||
type="primary"
|
||||
@click="setConfig"
|
||||
:loading="isSaving"
|
||||
:disabled="isResetting || isSaving"
|
||||
@click="handleSave"
|
||||
>
|
||||
{{ $t('config.apply') }}
|
||||
</ab-button>
|
||||
@@ -52,15 +79,20 @@ onActivated(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-config {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: auto;
|
||||
flex: 1;
|
||||
min-width: 0; // Allow grid to shrink below content size
|
||||
width: 100%;
|
||||
|
||||
@include forDesktop {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -71,6 +103,8 @@ onActivated(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
min-width: 0; // Allow column to shrink below content size
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
@@ -78,12 +112,35 @@ onActivated(() => {
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 12px 0;
|
||||
backdrop-filter: blur(8px);
|
||||
background: color-mix(in srgb, var(--color-bg) 80%, transparent);
|
||||
@include safeAreaBottom(padding-bottom, 12px);
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
backdrop-filter: blur(12px);
|
||||
background: color-mix(in srgb, var(--color-surface) 90%, transparent);
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
// Override button max-width on mobile to allow flex grow
|
||||
:deep(.btn) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
@include forTablet {
|
||||
justify-content: flex-end;
|
||||
padding: 12px 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
background: color-mix(in srgb, var(--color-bg) 80%, transparent);
|
||||
|
||||
// Restore button max-width on tablet+
|
||||
:deep(.btn) {
|
||||
max-width: 170px;
|
||||
|
||||
&.btn--big {
|
||||
max-width: 276px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -153,6 +153,7 @@ async function handleLogin() {
|
||||
filter: blur(100px);
|
||||
opacity: 0.6;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
&::before {
|
||||
|
||||
1
webui/types/dts/auto-imports.d.ts
vendored
1
webui/types/dts/auto-imports.d.ts
vendored
@@ -86,6 +86,7 @@ declare global {
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAddRss: typeof import('../../src/hooks/useAddRss')['useAddRss']
|
||||
const useApi: typeof import('../../src/hooks/useApi')['useApi']
|
||||
const useAppInfo: typeof import('../../src/hooks/useAppInfo')['useAppInfo']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
|
||||
1
webui/types/dts/components.d.ts
vendored
1
webui/types/dts/components.d.ts
vendored
@@ -54,6 +54,7 @@ declare module '@vue/runtime-core' {
|
||||
ConfigPasskey: typeof import('./../../src/components/setting/config-passkey.vue')['default']
|
||||
ConfigPlayer: typeof import('./../../src/components/setting/config-player.vue')['default']
|
||||
ConfigProxy: typeof import('./../../src/components/setting/config-proxy.vue')['default']
|
||||
ConfigSearchProvider: typeof import('./../../src/components/setting/config-search-provider.vue')['default']
|
||||
MediaQuery: typeof import('./../../src/components/media-query.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
|
||||
Reference in New Issue
Block a user