mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-02-03 02:04:06 +08:00
feat(webui): complete UI redesign with design system, dark mode, and accessibility
Implement a comprehensive UI overhaul using CSS custom properties for theming, scoped SCSS for all components and pages, dark/light mode toggle with flash prevention, page transitions, ARIA accessibility attributes, and responsive layout fixes. Fix VueUse auto-import configuration and dev proxy target. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,11 +4,24 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="theme-color" content="#FAFAFA" />
|
||||
<link rel="icon" href="/images/logo.svg" />
|
||||
<link rel="apple-touch-icon" href="/images/apple-touch-icon.png" />
|
||||
<meta name="description" content="Automated Bangumi Download Tool" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" />
|
||||
<title>AutoBangumi</title>
|
||||
<script>
|
||||
// Apply dark mode before render to prevent flash
|
||||
(function() {
|
||||
const saved = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (saved === 'dark' || (!saved && prefersDark)) {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,40 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { type GlobalThemeOverrides, NConfigProvider } from 'naive-ui';
|
||||
|
||||
const theme: GlobalThemeOverrides = {
|
||||
Spin: {
|
||||
color: '#fff',
|
||||
},
|
||||
DataTable: {
|
||||
thColor: 'rgba(255, 255, 255, 0)',
|
||||
thColorHover: 'rgba(255, 255, 255, 0)',
|
||||
tdColorHover: 'rgba(255, 255, 255, 0)',
|
||||
},
|
||||
Checkbox: {
|
||||
colorChecked: '#4e3c94',
|
||||
borderFocus: '#4e3c94',
|
||||
boxShadowFocus: '0 0 0 2px rgba(78, 60, 148, 0.2)',
|
||||
borderChecked: '1px solid #4e3c94',
|
||||
},
|
||||
};
|
||||
import { computed } from 'vue';
|
||||
import { type GlobalThemeOverrides, NConfigProvider, darkTheme } from 'naive-ui';
|
||||
|
||||
const { isDark } = useDarkMode();
|
||||
const { refresh, isLoggedIn } = useAuth();
|
||||
|
||||
if (isLoggedIn.value) {
|
||||
refresh();
|
||||
}
|
||||
|
||||
const lightOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#6C4AB6',
|
||||
primaryColorHover: '#563A92',
|
||||
primaryColorPressed: '#4A3291',
|
||||
bodyColor: '#FAFAFA',
|
||||
cardColor: '#FFFFFF',
|
||||
borderColor: '#E2E8F0',
|
||||
textColorBase: '#1E293B',
|
||||
textColor1: '#1E293B',
|
||||
textColor2: '#64748B',
|
||||
textColor3: '#94A3B8',
|
||||
},
|
||||
Spin: {
|
||||
color: '#6C4AB6',
|
||||
},
|
||||
DataTable: {
|
||||
thColor: 'transparent',
|
||||
thColorHover: 'transparent',
|
||||
tdColorHover: 'var(--color-surface-hover)',
|
||||
borderColor: 'var(--color-border)',
|
||||
},
|
||||
Checkbox: {
|
||||
colorChecked: '#6C4AB6',
|
||||
borderFocus: '#6C4AB6',
|
||||
boxShadowFocus: '0 0 0 2px rgba(108, 74, 182, 0.2)',
|
||||
borderChecked: '1px solid #6C4AB6',
|
||||
},
|
||||
};
|
||||
|
||||
const darkOverrides: GlobalThemeOverrides = {
|
||||
common: {
|
||||
primaryColor: '#8B6CC7',
|
||||
primaryColorHover: '#A78BDB',
|
||||
primaryColorPressed: '#7B5CB7',
|
||||
bodyColor: '#0F172A',
|
||||
cardColor: '#1E293B',
|
||||
borderColor: '#334155',
|
||||
textColorBase: '#F1F5F9',
|
||||
textColor1: '#F1F5F9',
|
||||
textColor2: '#94A3B8',
|
||||
textColor3: '#64748B',
|
||||
},
|
||||
Spin: {
|
||||
color: '#8B6CC7',
|
||||
},
|
||||
DataTable: {
|
||||
thColor: 'transparent',
|
||||
thColorHover: 'transparent',
|
||||
tdColorHover: 'var(--color-surface-hover)',
|
||||
borderColor: 'var(--color-border)',
|
||||
},
|
||||
Checkbox: {
|
||||
colorChecked: '#8B6CC7',
|
||||
borderFocus: '#8B6CC7',
|
||||
boxShadowFocus: '0 0 0 2px rgba(139, 108, 199, 0.2)',
|
||||
borderChecked: '1px solid #8B6CC7',
|
||||
},
|
||||
};
|
||||
|
||||
const themeOverrides = computed(() => isDark.value ? darkOverrides : lightOverrides);
|
||||
const naiveTheme = computed(() => isDark.value ? darkTheme : null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Suspense>
|
||||
<NConfigProvider :theme-overrides="theme">
|
||||
<NConfigProvider :theme="naiveTheme" :theme-overrides="themeOverrides">
|
||||
<RouterView></RouterView>
|
||||
</NConfigProvider>
|
||||
</Suspense>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './style/transition';
|
||||
@import './style/var';
|
||||
@import './style/transition';
|
||||
@import './style/global';
|
||||
</style>
|
||||
|
||||
@@ -16,48 +16,36 @@ defineEmits(['click']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="type === 'primary'" w-150 is-btn @click="() => $emit('click')">
|
||||
<div rounded-4 overflow-hidden poster-shandow rel>
|
||||
<div w-full h-210>
|
||||
<template v-if="bangumi.poster_link">
|
||||
<img :src="bangumi.poster_link" alt="poster" wh-full />
|
||||
</template>
|
||||
<!-- Grid poster card -->
|
||||
<div
|
||||
v-if="type === 'primary'"
|
||||
class="card"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="`Edit ${bangumi.official_title}`"
|
||||
@click="() => $emit('click')"
|
||||
@keydown.enter="() => $emit('click')"
|
||||
>
|
||||
<div class="card-poster">
|
||||
<template v-if="bangumi.poster_link">
|
||||
<img :src="bangumi.poster_link" :alt="bangumi.official_title" class="card-img" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="card-placeholder">
|
||||
<ErrorPicture theme="outline" size="24" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div wh-full f-cer border="1 white">
|
||||
<ErrorPicture theme="outline" size="24" fill="#333" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
abs
|
||||
f-cer
|
||||
z-1
|
||||
inset-0
|
||||
opacity-0
|
||||
transition="all duration-300"
|
||||
hover="backdrop-blur-2 bg-white bg-opacity-30 opacity-100"
|
||||
active="duration-0 bg-opacity-60"
|
||||
class="group"
|
||||
>
|
||||
<div
|
||||
text-white
|
||||
rounded="1/2"
|
||||
wh-44
|
||||
f-cer
|
||||
bg-theme-row
|
||||
group-active="poster-pen-active"
|
||||
>
|
||||
<Write size="20" />
|
||||
<div class="card-overlay">
|
||||
<div class="card-edit-btn">
|
||||
<Write size="18" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div py-4>
|
||||
<div text-h3 truncate>{{ bangumi.official_title }}</div>
|
||||
|
||||
<div space-x-5>
|
||||
<div class="card-info">
|
||||
<div class="card-title">{{ bangumi.official_title }}</div>
|
||||
<div class="card-tags">
|
||||
<ab-tag :title="`Season ${bangumi.season}`" type="primary" />
|
||||
<ab-tag
|
||||
v-if="bangumi.group_name"
|
||||
@@ -67,42 +55,25 @@ defineEmits(['click']);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="type === 'search'"
|
||||
w-480
|
||||
rounded-12
|
||||
p-4
|
||||
shadow
|
||||
bg="#eee5f4"
|
||||
transition="opacity ease-in-out duration-300"
|
||||
>
|
||||
<div bg-white rounded-8 p-12 fx-cer justify-between gap-x-16>
|
||||
<div w-400 gap-x-16 fx-cer>
|
||||
<div h-44 w-72 rounded-6 overflow-hidden>
|
||||
<template v-if="bangumi.poster_link">
|
||||
<img
|
||||
:src="bangumi.poster_link"
|
||||
alt="poster"
|
||||
w-full
|
||||
translate-y="-25%"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Search result card -->
|
||||
<div v-else-if="type === 'search'" class="search-card">
|
||||
<div class="search-card-inner">
|
||||
<div class="search-card-content">
|
||||
<div class="search-card-thumb">
|
||||
<template v-if="bangumi.poster_link">
|
||||
<img :src="bangumi.poster_link" :alt="bangumi.official_title" class="search-card-img" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div wh-full f-cer border="1 white">
|
||||
<ErrorPicture theme="outline" size="24" fill="#333" />
|
||||
<div class="card-placeholder card-placeholder--small">
|
||||
<ErrorPicture theme="outline" size="20" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div flex="~ col gap-y-4">
|
||||
<div w-300 text="h3 primary" truncate>
|
||||
{{ bangumi.official_title }}
|
||||
</div>
|
||||
<div flex="~ gap-x-8">
|
||||
<template
|
||||
v-for="i in ['season', 'group_name', 'subtitle']"
|
||||
:key="i"
|
||||
>
|
||||
<div class="search-card-meta">
|
||||
<div class="search-card-title">{{ bangumi.official_title }}</div>
|
||||
<div class="card-tags">
|
||||
<template v-for="i in ['season', 'group_name', 'subtitle']" :key="i">
|
||||
<ab-tag
|
||||
v-if="bangumi[i]"
|
||||
:title="i === 'season' ? `Season ${bangumi[i]}` : bangumi[i]"
|
||||
@@ -116,3 +87,165 @@ defineEmits(['click']);
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Grid poster card
|
||||
.card {
|
||||
width: 150px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.card-poster {
|
||||
position: relative;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
|
||||
.card:hover & {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-img {
|
||||
width: 100%;
|
||||
height: 210px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-placeholder {
|
||||
width: 100%;
|
||||
height: 210px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-surface-hover);
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: background-color var(--transition-normal);
|
||||
|
||||
&--small {
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(2px);
|
||||
transition: opacity var(--transition-normal);
|
||||
|
||||
.card:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card:active & {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.card-edit-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: transform var(--transition-fast);
|
||||
|
||||
.card:active & {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
padding: 8px 2px 4px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 4px;
|
||||
transition: color var(--transition-normal);
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
// Search result card
|
||||
.search-card {
|
||||
width: 480px;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 4px;
|
||||
background: var(--color-primary-light);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.search-card-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px;
|
||||
transition: background-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.search-card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-card-thumb {
|
||||
width: 72px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-card-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.search-card-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,24 +10,62 @@ withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div rounded-10 overflow-hidden>
|
||||
<div
|
||||
bg-theme-row
|
||||
w-full
|
||||
text-white
|
||||
px-20
|
||||
h="38 pc:45"
|
||||
fx-cer
|
||||
justify-between
|
||||
select-none
|
||||
>
|
||||
<div text="h3 pc:h2">{{ title }}</div>
|
||||
|
||||
<div class="container-card">
|
||||
<div class="container-header">
|
||||
<div class="container-title">{{ title }}</div>
|
||||
<slot name="title-right"></slot>
|
||||
</div>
|
||||
|
||||
<div p="14 pc:20" bg-white text="14 inherit">
|
||||
<div class="container-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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);
|
||||
}
|
||||
|
||||
.container-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
height: 42px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
|
||||
@include forPC {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
@include forPC {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.container-body {
|
||||
padding: 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>
|
||||
|
||||
@@ -14,28 +14,14 @@ withDefaults(
|
||||
|
||||
<template>
|
||||
<Disclosure v-slot="{ open }">
|
||||
<div rounded-10 overflow-hidden h-max>
|
||||
<DisclosureButton
|
||||
bg-theme-row
|
||||
w-full
|
||||
text-white
|
||||
fx-cer
|
||||
px-20
|
||||
h="38 pc:45"
|
||||
justify-between
|
||||
>
|
||||
<div text="h3 pc:h2">{{ title }}</div>
|
||||
|
||||
<Component :is="open ? Up : Down" size="24" />
|
||||
<div class="fold-panel">
|
||||
<DisclosureButton class="fold-panel-header">
|
||||
<div class="fold-panel-title">{{ title }}</div>
|
||||
<Component :is="open ? Up : Down" :size="18" />
|
||||
</DisclosureButton>
|
||||
|
||||
<div
|
||||
bg-white
|
||||
py="10 pc:20"
|
||||
text="14 inherit"
|
||||
:class="[open ? 'px-20' : 'px-8']"
|
||||
>
|
||||
<div v-show="!open" line my-12></div>
|
||||
<div class="fold-panel-body" :class="[open && 'fold-panel-body--open']">
|
||||
<div v-show="!open" class="fold-panel-divider"></div>
|
||||
|
||||
<DisclosurePanel>
|
||||
<slot></slot>
|
||||
@@ -44,3 +30,64 @@ withDefaults(
|
||||
</div>
|
||||
</Disclosure>
|
||||
</template>
|
||||
|
||||
<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);
|
||||
}
|
||||
|
||||
.fold-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
height: 42px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
@include forPC {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
.fold-panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
@include forPC {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.fold-panel-body {
|
||||
background: var(--color-surface);
|
||||
padding: 10px 8px;
|
||||
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>
|
||||
|
||||
@@ -15,9 +15,23 @@ const abLabel = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ items-start justify-between">
|
||||
<div>{{ abLabel }}</div>
|
||||
|
||||
<div class="label-row">
|
||||
<div class="label-text">{{ abLabel }}</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.label-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
padding-top: 4px;
|
||||
transition: color var(--transition-normal);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,7 +30,7 @@ function close() {
|
||||
|
||||
<template>
|
||||
<TransitionRoot appear :show="show" as="template">
|
||||
<Dialog as="div" class="relative z-10" @close="close">
|
||||
<Dialog as="div" class="popup-dialog" @close="close">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
@@ -40,11 +40,11 @@ function close() {
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div fixed inset-0 bg="black opacity-25" />
|
||||
<div class="popup-backdrop" />
|
||||
</TransitionChild>
|
||||
|
||||
<div fixed inset-0 overflow-y-auto>
|
||||
<div flex="~ items-center justify-center" min-h-full p-4 text-center>
|
||||
<div class="popup-wrapper">
|
||||
<div class="popup-center">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
@@ -65,3 +65,32 @@ function close() {
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.popup-dialog {
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
}
|
||||
|
||||
.popup-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.popup-wrapper {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.popup-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -31,34 +31,25 @@ function onSelect(site: string) {
|
||||
@select="() => (showProvider = !showProvider)"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-show="showProvider"
|
||||
v-on-click-outside="() => (showProvider = false)"
|
||||
abs
|
||||
top-84
|
||||
left-540
|
||||
w-100
|
||||
rounded-12
|
||||
shadow
|
||||
bg-white
|
||||
z-99
|
||||
overflow-hidden
|
||||
>
|
||||
<transition name="dropdown">
|
||||
<div
|
||||
v-for="site in providers"
|
||||
:key="site"
|
||||
hover:bg-theme-row
|
||||
is-btn
|
||||
@click="() => onSelect(site)"
|
||||
v-show="showProvider"
|
||||
v-on-click-outside="() => (showProvider = false)"
|
||||
class="provider-dropdown"
|
||||
>
|
||||
<div text="h3 primary" hover="text-white" p-12 truncate>
|
||||
<div
|
||||
v-for="site in providers"
|
||||
:key="site"
|
||||
class="provider-item"
|
||||
@click="() => onSelect(site)"
|
||||
>
|
||||
{{ site }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div v-on-click-outside="clearSearch" abs top-84 left-192 z-8>
|
||||
<transition-group name="fade-list" tag="ul" space-y-12>
|
||||
<div v-on-click-outside="clearSearch" class="search-results">
|
||||
<transition-group name="fade-list" tag="ul" class="search-results-list">
|
||||
<li v-for="bangumi in bangumiList" :key="bangumi.order">
|
||||
<ab-bangumi-card
|
||||
:bangumi="bangumi.value"
|
||||
@@ -70,4 +61,52 @@ function onSelect(site: string) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.provider-dropdown {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
left: 540px;
|
||||
width: 120px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
transition: background-color var(--transition-normal),
|
||||
border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
left: 192px;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.search-results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -33,67 +33,152 @@ function abLabel(label: string | (() => string)) {
|
||||
|
||||
<template>
|
||||
<Menu>
|
||||
<div rel>
|
||||
<div fx-cer space-x="pc:16 10" text="pc:24 20">
|
||||
<International
|
||||
theme="outline"
|
||||
size="1em"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
<div class="status-bar">
|
||||
<div class="status-bar-actions">
|
||||
<button
|
||||
class="status-bar-btn"
|
||||
aria-label="Switch language"
|
||||
@click="() => $emit('changeLang')"
|
||||
/>
|
||||
>
|
||||
<International theme="outline" size="1em" />
|
||||
</button>
|
||||
|
||||
<AddOne
|
||||
theme="outline"
|
||||
size="1em"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
<button
|
||||
class="status-bar-btn"
|
||||
aria-label="Add RSS subscription"
|
||||
@click="() => $emit('clickAdd')"
|
||||
/>
|
||||
>
|
||||
<AddOne theme="outline" size="1em" />
|
||||
</button>
|
||||
|
||||
<MenuButton bg-transparent is-btn btn-click>
|
||||
<System theme="outline" size="1em" fill="#fff" />
|
||||
<MenuButton class="status-bar-btn" aria-label="System menu">
|
||||
<System theme="outline" size="1em" />
|
||||
</MenuButton>
|
||||
|
||||
<ab-status :running="running" />
|
||||
</div>
|
||||
|
||||
<MenuItems
|
||||
abs
|
||||
top="pc:50 40"
|
||||
left="pc:32 0"
|
||||
w-120
|
||||
rounded-8
|
||||
bg-white
|
||||
overflow-hidden
|
||||
shadow
|
||||
z-99
|
||||
>
|
||||
<MenuItems class="status-menu">
|
||||
<MenuItem v-for="i in items" :key="i.id" v-slot="{ active }">
|
||||
<div
|
||||
w-full
|
||||
h-32
|
||||
px-12
|
||||
fx-cer
|
||||
gap-x-8
|
||||
is-btn
|
||||
hover="text-white bg-primary"
|
||||
class="group"
|
||||
:class="[active ? 'text-white bg-theme-row' : 'text-black']"
|
||||
class="status-menu-item"
|
||||
:class="[active && 'status-menu-item--active']"
|
||||
@click="() => i.handle && i.handle()"
|
||||
>
|
||||
<div
|
||||
group-hover="text-white"
|
||||
:class="[active ? 'text-white' : 'text-primary']"
|
||||
>
|
||||
<div class="status-menu-item-icon">
|
||||
<Component :is="i.icon" size="16"></Component>
|
||||
</div>
|
||||
<div text-main>{{ abLabel(i.label) }}</div>
|
||||
<div class="status-menu-item-label">{{ abLabel(i.label) }}</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</div>
|
||||
</Menu>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 20px;
|
||||
|
||||
@include forMobile {
|
||||
gap: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-bar-btn {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--color-text-secondary);
|
||||
transition: color var(--transition-fast), transform var(--transition-fast);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-menu {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 0;
|
||||
width: 160px;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
animation: dropdown-in 150ms ease-out;
|
||||
transform-origin: top right;
|
||||
transition: background-color var(--transition-normal),
|
||||
border-color var(--transition-normal);
|
||||
|
||||
@include forPC {
|
||||
top: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-4px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.status-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
transition: color var(--transition-fast), background-color var(--transition-fast);
|
||||
|
||||
&:hover,
|
||||
&--active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
.status-menu-item-icon {
|
||||
color: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-menu-item-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,60 +15,95 @@ defineEmits(['click']);
|
||||
const buttonSize = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'large':
|
||||
return 'wh-36';
|
||||
return 'add-btn--large';
|
||||
case 'medium':
|
||||
return 'wh-24';
|
||||
return 'add-btn--medium';
|
||||
case 'small':
|
||||
return 'wh-12';
|
||||
}
|
||||
});
|
||||
|
||||
const lineSize = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'large':
|
||||
return 'w-18 h-4';
|
||||
case 'medium':
|
||||
return 'w-3 h-12';
|
||||
case 'small':
|
||||
return 'w-2 h-6';
|
||||
return 'add-btn--small';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:rounded="round ? '1/2' : '8'"
|
||||
f-cer
|
||||
rel
|
||||
transition-colors
|
||||
class="box"
|
||||
:class="[`type-${type}`, buttonSize]"
|
||||
class="add-btn"
|
||||
:class="[buttonSize, round && 'add-btn--round']"
|
||||
aria-label="Add"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div :class="[`type-${type}`, lineSize]" class="line" abs />
|
||||
<div :class="[`type-${type}`, lineSize]" class="line" abs rotate-90></div>
|
||||
<svg viewBox="0 0 24 24" fill="none" class="add-btn-icon">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$normal: #4e3c94;
|
||||
$hover: #281e52;
|
||||
$active: #8e8a9c;
|
||||
|
||||
.box {
|
||||
background: $normal;
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast),
|
||||
transform var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: $hover;
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $normal;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
&--round {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&--large {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
.add-btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
&--medium {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
.add-btn-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&--small {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 4px;
|
||||
|
||||
.add-btn-icon {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&--round.add-btn--large {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&--round.add-btn--medium {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&--round.add-btn--small {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.line {
|
||||
border-radius: 1px;
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,93 +22,38 @@ defineEmits(['click']);
|
||||
|
||||
const selected = ref<string>(props.selections[0]);
|
||||
const showSelections = ref<boolean>(false);
|
||||
|
||||
const buttonSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'rounded-10 text-h1 w-276 h-55 text-h1';
|
||||
case 'normal':
|
||||
return 'rounded-6 w-170 h-36';
|
||||
case 'small':
|
||||
return 'rounded-6 w-86 h-28 text-main';
|
||||
}
|
||||
});
|
||||
|
||||
const selectboxSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'w-276 rounded-10 text-h1';
|
||||
case 'normal':
|
||||
return 'w-170 rounded-6';
|
||||
case 'small':
|
||||
return 'w-86 rounded-6 text-main';
|
||||
}
|
||||
});
|
||||
|
||||
const loadingSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'large';
|
||||
case 'normal':
|
||||
return 'small';
|
||||
case 'small':
|
||||
return 18;
|
||||
}
|
||||
});
|
||||
|
||||
function onSelect(selection: string) {
|
||||
selected.value = selection;
|
||||
showSelections.value = false;
|
||||
console.log(selected.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="buttonSize" f-cer overflow-hidden>
|
||||
<div class="btn-multi" :class="[`btn-multi--${size}`, `btn-multi--${type}`]">
|
||||
<Component
|
||||
:is="link !== null ? 'a' : 'button'"
|
||||
:href="link"
|
||||
text-white
|
||||
outline-none
|
||||
wh-full
|
||||
pl-12
|
||||
:class="[`type-${type}`]"
|
||||
class="btn-multi-main"
|
||||
@click="$emit('click', selected)"
|
||||
>
|
||||
<NSpin :show="loading" :size="loadingSize">
|
||||
<div text-main>{{ selected }}</div>
|
||||
<NSpin :show="loading" :size="size === 'big' ? 'large' : 'small'">
|
||||
<div class="btn-multi-label">{{ selected }}</div>
|
||||
</NSpin>
|
||||
</Component>
|
||||
<div
|
||||
is-btn
|
||||
px-12
|
||||
h-full
|
||||
f-cer
|
||||
:class="[`selector-${type}`]"
|
||||
class="btn-multi-arrow"
|
||||
@click="() => (showSelections = !showSelections)"
|
||||
>
|
||||
<Down fill="white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="showSelections"
|
||||
abs
|
||||
z-70
|
||||
:class="selectboxSize"
|
||||
overflow-hidden
|
||||
class="select-box"
|
||||
class="btn-multi-dropdown"
|
||||
:class="[`btn-multi--${size}`, `btn-multi--${type}`]"
|
||||
>
|
||||
<div
|
||||
v-for="selection in selections"
|
||||
:key="selection"
|
||||
is-btn
|
||||
wh-full
|
||||
f-cer
|
||||
text-main
|
||||
py-8
|
||||
text-white
|
||||
:class="[`type-${type}`]"
|
||||
@click="onSelect(selection)"
|
||||
class="btn-multi-option"
|
||||
@click="() => { selected = selection; showSelections = false; }"
|
||||
>
|
||||
{{ selection }}
|
||||
</div>
|
||||
@@ -116,25 +61,108 @@ function onSelect(selection: string) {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.type {
|
||||
&-primary {
|
||||
@include bg-mouse-event(#4e3c94, #281e52, #8e8a9c);
|
||||
.btn-multi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
|
||||
&--big {
|
||||
border-radius: var(--radius-md);
|
||||
width: 276px;
|
||||
height: 55px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&-warn {
|
||||
@include bg-mouse-event(#943c61, #521e2a, #9c8a93);
|
||||
&--normal {
|
||||
border-radius: var(--radius-sm);
|
||||
width: 170px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.selector {
|
||||
&-primary {
|
||||
@include bg-mouse-event(#4e3c94, #281e52, #8e8a9c);
|
||||
|
||||
&--small {
|
||||
border-radius: var(--radius-sm);
|
||||
width: 86px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&-warn {
|
||||
@include bg-mouse-event(#943c61, #521e2a, #9c8a93);
|
||||
|
||||
&--primary {
|
||||
.btn-multi-main,
|
||||
.btn-multi-arrow,
|
||||
.btn-multi-option {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
.btn-multi-main:hover,
|
||||
.btn-multi-arrow:hover,
|
||||
.btn-multi-option:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&--warn {
|
||||
.btn-multi-main,
|
||||
.btn-multi-arrow,
|
||||
.btn-multi-option {
|
||||
background: var(--color-danger);
|
||||
}
|
||||
.btn-multi-main:hover,
|
||||
.btn-multi-arrow:hover,
|
||||
.btn-multi-option:hover {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.select-box {
|
||||
transform: TranslateY(80%) TranslateX(-111%);
|
||||
.btn-multi-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding-left: 12px;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-multi-label {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.btn-multi-arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 12px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn-multi-dropdown {
|
||||
position: absolute;
|
||||
z-index: 70;
|
||||
overflow: hidden;
|
||||
transform: translateY(80%) translateX(-111%);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.btn-multi-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: #fff;
|
||||
font-size: inherit;
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,22 +21,11 @@ defineEmits(['click']);
|
||||
const buttonSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'rounded-10 text-h1 w-276 h-55 text-h1';
|
||||
return 'btn--big';
|
||||
case 'normal':
|
||||
return 'rounded-6 w-170 h-36';
|
||||
return 'btn--normal';
|
||||
case 'small':
|
||||
return 'rounded-6 w-86 h-28 text-main';
|
||||
}
|
||||
});
|
||||
|
||||
const loadingSize = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'big':
|
||||
return 'large';
|
||||
case 'normal':
|
||||
return 'small';
|
||||
case 'small':
|
||||
return 18;
|
||||
return 'btn--small';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -45,26 +34,74 @@ const loadingSize = computed(() => {
|
||||
<Component
|
||||
:is="link !== null ? 'a' : 'button'"
|
||||
:href="link"
|
||||
text-white
|
||||
outline-none
|
||||
f-cer
|
||||
:class="[`type-${type}`, buttonSize]"
|
||||
class="btn"
|
||||
:class="[`btn--${type}`, buttonSize]"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<NSpin :show="loading" :size="loadingSize">
|
||||
<NSpin :show="loading" :size="size === 'big' ? 'large' : 'small'">
|
||||
<slot></slot>
|
||||
</NSpin>
|
||||
</Component>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.type {
|
||||
&-primary {
|
||||
@include bg-mouse-event(#4e3c94, #281e52, #8e8a9c);
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background-color var(--transition-fast),
|
||||
transform var(--transition-fast),
|
||||
box-shadow var(--transition-fast);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
&-warn {
|
||||
@include bg-mouse-event(#943c61, #521e2a, #9c8a93);
|
||||
// Sizes
|
||||
&--big {
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 18px;
|
||||
width: 276px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
&--normal {
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
width: 170px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
&--small {
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
width: 86px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
// Types
|
||||
&--primary {
|
||||
background: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-hover);
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--color-primary) 30%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&--warn {
|
||||
background: var(--color-danger);
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.9);
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--color-danger) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,28 +15,22 @@ const checked = defineModel<boolean>({ default: false });
|
||||
|
||||
<template>
|
||||
<Switch v-model="checked" as="template">
|
||||
<div flex="~ items-center gap-x-8" is-btn>
|
||||
<div class="checkbox-wrapper">
|
||||
<slot name="before"></slot>
|
||||
|
||||
<div
|
||||
rel
|
||||
f-cer
|
||||
bg-white
|
||||
border="solid #3c239f"
|
||||
class="checkbox"
|
||||
:class="[
|
||||
small ? 'wh-16 border-2 rounded-4' : 'wh-32 border-4 rounded-6',
|
||||
!checked && 'group',
|
||||
small ? 'checkbox--small' : 'checkbox--normal',
|
||||
checked && 'checkbox--checked',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
rounded-2
|
||||
transition="all duration-300"
|
||||
class="checkbox-inner"
|
||||
:class="[
|
||||
small ? 'wh-8' : 'wh-16',
|
||||
checked ? 'bg-[#3c239f]' : 'bg-transparent',
|
||||
small ? 'checkbox-inner--small' : 'checkbox-inner--normal',
|
||||
checked && 'checkbox-inner--checked',
|
||||
]"
|
||||
group-hover:bg="#cccad4"
|
||||
group-active:bg="#3c239f"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -44,3 +38,68 @@ const checked = defineModel<boolean>({ default: false });
|
||||
</div>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-surface);
|
||||
border: 2px solid var(--color-primary);
|
||||
transition: border-color var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
|
||||
&--normal {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
&--small {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
&--checked {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:hover:not(.checkbox--checked) {
|
||||
.checkbox-inner {
|
||||
background: var(--color-border-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-inner {
|
||||
border-radius: 2px;
|
||||
transition: background-color var(--transition-fast);
|
||||
background: transparent;
|
||||
|
||||
&--normal {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
&--small {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&--checked {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,8 +10,41 @@ withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div fx-cer gap-x-12>
|
||||
<div text="pc:h1 h2">{{ title }}</div>
|
||||
<div w-160 h-3 bg-theme-row rounded-full></div>
|
||||
<div class="page-title">
|
||||
<h1 class="page-title-text">{{ title }}</h1>
|
||||
<div class="page-title-accent"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-title-text {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
transition: color var(--transition-normal);
|
||||
|
||||
@include forMobile {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title-accent {
|
||||
width: 120px;
|
||||
height: 3px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-primary-hover));
|
||||
opacity: 0.6;
|
||||
|
||||
@include forMobile {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { Down, Search } from '@icon-park/vue-next';
|
||||
import { NSpin } from 'naive-ui';
|
||||
import { watch } from 'vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
@@ -17,65 +16,119 @@ withDefaults(
|
||||
defineEmits(['select', 'search']);
|
||||
|
||||
const inputValue = defineModel<string>('inputValue');
|
||||
|
||||
watch(inputValue, (val) => {
|
||||
console.log(val);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
bg="#7752B4"
|
||||
text-white
|
||||
fx-cer
|
||||
rounded-12
|
||||
h-36
|
||||
pl-12
|
||||
gap-x-12
|
||||
w-400
|
||||
overflow-hidden
|
||||
shadow-inner
|
||||
>
|
||||
<Search
|
||||
<div class="search-input" role="search">
|
||||
<button
|
||||
v-if="!loading"
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#fff"
|
||||
is-btn
|
||||
btn-click
|
||||
class="search-icon-btn"
|
||||
aria-label="Search"
|
||||
@click="$emit('search')"
|
||||
/>
|
||||
<NSpin v-else :size="20" />
|
||||
>
|
||||
<Search theme="outline" size="20" class="search-icon" />
|
||||
</button>
|
||||
<NSpin v-else :size="18" />
|
||||
|
||||
<input
|
||||
v-model="inputValue"
|
||||
type="text"
|
||||
:placeholder="$t('topbar.search.placeholder')"
|
||||
input-reset
|
||||
class="search-field"
|
||||
aria-label="Search anime"
|
||||
@keyup.enter="$emit('search')"
|
||||
/>
|
||||
<div
|
||||
h-full
|
||||
f-cer
|
||||
justify-between
|
||||
px-12
|
||||
w-100
|
||||
class="provider"
|
||||
is-btn
|
||||
|
||||
<button
|
||||
class="search-provider"
|
||||
aria-label="Select search provider"
|
||||
@click="$emit('select')"
|
||||
>
|
||||
<div text-h3 truncate>
|
||||
{{ provider }}
|
||||
</div>
|
||||
<div class="provider">
|
||||
<Down />
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-provider-label">{{ provider }}</div>
|
||||
<Down :size="14" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.provider {
|
||||
background: #4e2a94;
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding-left: 12px;
|
||||
gap: 10px;
|
||||
width: 360px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--color-surface-hover);
|
||||
border: 1px solid var(--color-border);
|
||||
overflow: hidden;
|
||||
transition: border-color var(--transition-fast),
|
||||
background-color var(--transition-normal);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-surface);
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: var(--color-text-muted);
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
.search-icon-btn:hover & {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.search-field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.search-provider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
padding: 0 12px;
|
||||
min-width: 80px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.search-provider-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,26 +64,17 @@ watchEffect(() => {
|
||||
|
||||
<template>
|
||||
<Listbox v-slot="{ open }" v-model="selected">
|
||||
<div
|
||||
rel
|
||||
flex="inline col"
|
||||
rounded-6
|
||||
border="1 black"
|
||||
text-main
|
||||
p="y-4 x-12"
|
||||
>
|
||||
<ListboxButton bg-transparent fx-cer justify-between gap-x-24>
|
||||
<div>
|
||||
{{ label }}
|
||||
</div>
|
||||
<div class="select-wrapper">
|
||||
<ListboxButton class="select-button">
|
||||
<div class="select-value">{{ label }}</div>
|
||||
<div :class="[{ hidden: open }]">
|
||||
<Down />
|
||||
<Down :size="14" />
|
||||
</div>
|
||||
</ListboxButton>
|
||||
|
||||
<ListboxOptions mt-8>
|
||||
<div flex="~ items-end justify-between gap-x-24">
|
||||
<div flex="~ col gap-y-8">
|
||||
<ListboxOptions class="select-options">
|
||||
<div class="select-options-inner">
|
||||
<div class="select-options-list">
|
||||
<ListboxOption
|
||||
v-for="item in otherItems"
|
||||
v-slot="{ active }"
|
||||
@@ -92,9 +83,10 @@ watchEffect(() => {
|
||||
:disabled="getDisabled(item)"
|
||||
>
|
||||
<div
|
||||
class="select-option"
|
||||
:class="[
|
||||
{ 'text-primary': active },
|
||||
getDisabled(item) ? 'is-disabled' : 'is-btn',
|
||||
active && 'select-option--active',
|
||||
getDisabled(item) && 'select-option--disabled',
|
||||
]"
|
||||
>
|
||||
{{ getLabel(item) }}
|
||||
@@ -102,9 +94,75 @@ watchEffect(() => {
|
||||
</ListboxOption>
|
||||
</div>
|
||||
|
||||
<div :class="[{ hidden: !open }]"><Up /></div>
|
||||
<div :class="[{ hidden: !open }]"><Up :size="14" /></div>
|
||||
</div>
|
||||
</ListboxOptions>
|
||||
</div>
|
||||
</Listbox>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: 12px;
|
||||
padding: 4px 12px;
|
||||
transition: border-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.select-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-text);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select-value {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.select-options {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.select-options-inner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.select-options-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.select-option {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--color-text-secondary);
|
||||
transition: color var(--transition-fast);
|
||||
|
||||
&--active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,14 +12,59 @@ withDefaults(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div f-cer :style="{ width: size, height: size }">
|
||||
<div rounded="1/2" f-cer border="2 solid white" wh-full>
|
||||
<div
|
||||
class="status-indicator"
|
||||
:style="{ width: size, height: size }"
|
||||
role="status"
|
||||
:aria-label="running ? 'System running' : 'System stopped'"
|
||||
>
|
||||
<div class="status-ring">
|
||||
<div
|
||||
:class="[running ? 'bg-running' : 'bg-stopped']"
|
||||
rounded="1/2"
|
||||
wh-10
|
||||
transition-colors
|
||||
class="status-dot"
|
||||
:class="[running ? 'status-dot--running' : 'status-dot--stopped']"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.status-ring {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-border);
|
||||
transition: border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
&--running {
|
||||
background: var(--color-success);
|
||||
box-shadow: 0 0 6px color-mix(in srgb, var(--color-success) 40%, transparent);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&--stopped {
|
||||
background: var(--color-danger);
|
||||
box-shadow: 0 0 6px color-mix(in srgb, var(--color-danger) 40%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,54 +8,44 @@ const checked = defineModel<boolean>('checked', {
|
||||
|
||||
<template>
|
||||
<Switch v-model="checked" as="template">
|
||||
<div
|
||||
is-btn
|
||||
w-48
|
||||
h-28
|
||||
rounded-full
|
||||
rel
|
||||
flex="inline items-center"
|
||||
transition-colors
|
||||
duration-300
|
||||
p-3
|
||||
shadow="~ inset"
|
||||
class="box"
|
||||
:class="{ checked }"
|
||||
>
|
||||
<div class="switch-track" :class="{ 'switch-track--checked': checked }">
|
||||
<div
|
||||
wh-22
|
||||
rounded="1/2"
|
||||
transition-all
|
||||
duration-300
|
||||
class="slider"
|
||||
:class="{ checked, 'translate-x-20': checked }"
|
||||
class="switch-thumb"
|
||||
:class="{ 'switch-thumb--checked': checked }"
|
||||
></div>
|
||||
</div>
|
||||
</Switch>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scope>
|
||||
$bg-unchecked: #929292;
|
||||
$bg-checked: #1c1259;
|
||||
<style lang="scss" scoped>
|
||||
.switch-track {
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-full);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--color-border-hover);
|
||||
transition: background-color var(--transition-fast);
|
||||
|
||||
$slider-unchecked: #ececef;
|
||||
$slider-checked: #fff;
|
||||
|
||||
.box {
|
||||
background: $bg-unchecked;
|
||||
|
||||
&.checked {
|
||||
background: $bg-checked;
|
||||
&--checked {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
&:not(.checked) {
|
||||
background: $slider-unchecked;
|
||||
}
|
||||
.switch-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
transition: transform var(--transition-fast);
|
||||
|
||||
&.checked {
|
||||
background: $slider-checked;
|
||||
&--checked {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
const props = withDefaults(
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
type: 'primary' | 'warn' | 'inactive' | 'active' | 'notify';
|
||||
title: string;
|
||||
@@ -9,67 +9,59 @@ const props = withDefaults(
|
||||
title: 'title',
|
||||
}
|
||||
);
|
||||
|
||||
const InnerStyle = computed(() => {
|
||||
return `${props.type}-inner`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div p-1 rounded-16 inline-flex :class="type">
|
||||
<div bg-white rounded-12 px-8 text-10 truncate max-w-72 :class="InnerStyle">
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="tag" :class="`tag--${type}`">
|
||||
{{ title }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$primary-map: (
|
||||
border: linear-gradient(90.5deg, #492897 1.53%, #783674 96.48%),
|
||||
inner: #eee5f4,
|
||||
font: #000000,
|
||||
);
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border: 1px solid;
|
||||
transition: background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
|
||||
$warn-map: (
|
||||
border: #892f2f,
|
||||
inner: #ffdfdf,
|
||||
font: #892f2f,
|
||||
);
|
||||
&--primary {
|
||||
background: var(--color-primary-light);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
$inactive-map: (
|
||||
border: #797979,
|
||||
inner: #e0e0e0,
|
||||
font: #3f3f3f,
|
||||
);
|
||||
&--warn {
|
||||
background: color-mix(in srgb, var(--color-danger) 10%, transparent);
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
$active-map: (
|
||||
border: #104931,
|
||||
inner: #e5f4e0,
|
||||
font: #4c6643,
|
||||
);
|
||||
&--inactive {
|
||||
background: var(--color-surface-hover);
|
||||
border-color: var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
$notify-map: (
|
||||
border: #f5c451,
|
||||
inner: #fff4db,
|
||||
font: #a76e18,
|
||||
);
|
||||
&--active {
|
||||
background: color-mix(in srgb, var(--color-success) 10%, transparent);
|
||||
border-color: var(--color-success);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
$types-map: (
|
||||
primary: $primary-map,
|
||||
warn: $warn-map,
|
||||
inactive: $inactive-map,
|
||||
active: $active-map,
|
||||
notify: $notify-map,
|
||||
);
|
||||
|
||||
@each $type, $colors in $types-map {
|
||||
.#{$type} {
|
||||
background: map-get($colors, border);
|
||||
|
||||
&-inner {
|
||||
background: map-get($colors, inner);
|
||||
color: map-get($colors, font);
|
||||
}
|
||||
&--notify {
|
||||
background: color-mix(in srgb, var(--color-warning) 10%, transparent);
|
||||
border-color: var(--color-warning);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
Log,
|
||||
Logout,
|
||||
MenuUnfold,
|
||||
Moon,
|
||||
Play,
|
||||
SettingTwo,
|
||||
Sun,
|
||||
} from '@icon-park/vue-next';
|
||||
import InlineSvg from 'vue-inline-svg';
|
||||
|
||||
@@ -24,14 +26,15 @@ const { t } = useMyI18n();
|
||||
const { logout } = useAuth();
|
||||
const route = useRoute();
|
||||
const { isMobile } = useBreakpointQuery();
|
||||
const { isDark, toggle: toggleDark } = useDarkMode();
|
||||
|
||||
const show = ref(props.open);
|
||||
const toggle = () => (show.value = !show.value);
|
||||
|
||||
const RSS = h(
|
||||
'span',
|
||||
{ class: ['rel', 'left-2'] },
|
||||
h(InlineSvg, { src: './images/RSS.svg' })
|
||||
{ style: { display: 'flex', alignItems: 'center', justifyContent: 'center', width: '20px', height: '20px' } },
|
||||
h(InlineSvg, { src: './images/RSS.svg', width: '16', height: '16' })
|
||||
);
|
||||
|
||||
const items = [
|
||||
@@ -86,21 +89,13 @@ function Exit() {
|
||||
<div
|
||||
title="logout"
|
||||
class={[
|
||||
`
|
||||
mt-auto
|
||||
fx-cer
|
||||
gap-x-42
|
||||
px-24
|
||||
is-btn
|
||||
transition-colors
|
||||
`,
|
||||
isMobile.value ? 'h-40' : 'h-48',
|
||||
'sidebar-item sidebar-item--action',
|
||||
isMobile.value ? 'h-40' : '',
|
||||
]}
|
||||
hover="bg-[#F1F5FA] text-[#2A1C52]"
|
||||
onClick={logout}
|
||||
>
|
||||
<Logout size={24} />
|
||||
{!isMobile.value && <div class="text-h2">{t('sidebar.logout')}</div>}
|
||||
<Logout size={20} />
|
||||
{!isMobile.value && show.value && <div class="sidebar-item-label">{t('sidebar.logout')}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -111,91 +106,261 @@ const mobileItems = computed(() => items.filter((i) => i.id !== 4));
|
||||
<template>
|
||||
<media-query>
|
||||
<div
|
||||
:class="[show ? 'w-240' : 'w-72']"
|
||||
bg-theme-col
|
||||
text-white
|
||||
transition-width
|
||||
pb-12
|
||||
rounded="pc:16 10"
|
||||
class="sidebar"
|
||||
:class="[show ? 'sidebar--expanded' : 'sidebar--collapsed']"
|
||||
>
|
||||
<div overflow-hidden wh-full flex="~ col">
|
||||
<div
|
||||
w-full
|
||||
h-60
|
||||
is-btn
|
||||
f-cer
|
||||
rounded-t-10
|
||||
bg="#E7E7E7"
|
||||
text="#2A1C52"
|
||||
rel
|
||||
<div class="sidebar-inner">
|
||||
<!-- Toggle header -->
|
||||
<button
|
||||
class="sidebar-header"
|
||||
:aria-label="show ? 'Collapse sidebar' : 'Expand sidebar'"
|
||||
:aria-expanded="show"
|
||||
@click="toggle"
|
||||
>
|
||||
<div :class="[!show && 'abs opacity-0']" transition-opacity>
|
||||
<div text-h1>{{ $t('sidebar.title') }}</div>
|
||||
<div v-show="show" class="sidebar-title">
|
||||
{{ $t('sidebar.title') }}
|
||||
</div>
|
||||
|
||||
<MenuUnfold
|
||||
theme="outline"
|
||||
size="24"
|
||||
fill="#2A1C52"
|
||||
abs
|
||||
left-24
|
||||
:class="[show && 'rotate-y-180']"
|
||||
size="20"
|
||||
class="sidebar-toggle-icon"
|
||||
:class="[show && 'sidebar-toggle-icon--open']"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="sidebar-nav">
|
||||
<RouterLink
|
||||
v-for="i in items"
|
||||
:key="i.id"
|
||||
:to="i.path"
|
||||
replace
|
||||
:title="i.label()"
|
||||
class="sidebar-item"
|
||||
:class="[
|
||||
route.path === i.path && 'sidebar-item--active',
|
||||
i.hidden && 'hidden',
|
||||
]"
|
||||
>
|
||||
<Component :is="i.icon" :size="20" />
|
||||
<div v-show="show" class="sidebar-item-label">{{ i.label() }}</div>
|
||||
</RouterLink>
|
||||
</nav>
|
||||
|
||||
<!-- Bottom actions -->
|
||||
<div class="sidebar-footer">
|
||||
<button
|
||||
class="sidebar-item sidebar-item--action sidebar-item--theme"
|
||||
:title="isDark ? 'Light mode' : 'Dark mode'"
|
||||
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||
@click="toggleDark"
|
||||
>
|
||||
<Moon v-if="!isDark" :size="20" />
|
||||
<Sun v-else :size="20" />
|
||||
<div v-show="show" class="sidebar-item-label">
|
||||
{{ isDark ? 'Light' : 'Dark' }}
|
||||
</div>
|
||||
</button>
|
||||
<Exit />
|
||||
</div>
|
||||
|
||||
<RouterLink
|
||||
v-for="i in items"
|
||||
:key="i.id"
|
||||
:to="i.path"
|
||||
replace
|
||||
:title="i.label()"
|
||||
fx-cer
|
||||
px-24
|
||||
gap-x-42
|
||||
h-48
|
||||
is-btn
|
||||
transition-colors
|
||||
hover="bg-[#F1F5FA] text-[#2A1C52]"
|
||||
:class="[
|
||||
route.path === i.path && 'bg-[#F1F5FA] text-[#2A1C52]',
|
||||
i.hidden && 'hidden',
|
||||
]"
|
||||
>
|
||||
<Component :is="i.icon" :size="24" />
|
||||
|
||||
<div text-h2 whitespace-nowrap>{{ i.label() }}</div>
|
||||
</RouterLink>
|
||||
|
||||
<Exit />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #mobile>
|
||||
<div bg-white flex rounded-10 overflow-hidden>
|
||||
<div class="mobile-nav">
|
||||
<RouterLink
|
||||
v-for="i in mobileItems"
|
||||
:key="i.id"
|
||||
:to="i.path"
|
||||
replace
|
||||
flex-1
|
||||
fx-cer
|
||||
px-24
|
||||
gap-x-42
|
||||
h-40
|
||||
is-btn
|
||||
transition-colors
|
||||
rounded-10
|
||||
class="mobile-nav-item"
|
||||
:class="[
|
||||
route.path === i.path && 'bg-theme-row text-white',
|
||||
route.path === i.path && 'mobile-nav-item--active',
|
||||
i.hidden && 'hidden',
|
||||
]"
|
||||
>
|
||||
<Component :is="i.icon" :size="24" />
|
||||
<Component :is="i.icon" :size="20" />
|
||||
</RouterLink>
|
||||
|
||||
<div
|
||||
class="mobile-nav-item"
|
||||
@click="toggleDark"
|
||||
>
|
||||
<Moon v-if="!isDark" :size="20" />
|
||||
<Sun v-else :size="20" />
|
||||
</div>
|
||||
|
||||
<Exit />
|
||||
</div>
|
||||
</template>
|
||||
</media-query>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebar {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: width var(--transition-normal),
|
||||
background-color var(--transition-normal),
|
||||
border-color var(--transition-normal);
|
||||
overflow: hidden;
|
||||
|
||||
&--expanded {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
&--collapsed {
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
padding: 0 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: transparent;
|
||||
transition: border-color var(--transition-normal),
|
||||
background-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar-toggle-icon {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
color: var(--color-text-secondary);
|
||||
transition: transform var(--transition-normal);
|
||||
|
||||
&--open {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--color-text-secondary);
|
||||
transition: color var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&--action {
|
||||
color: var(--color-text-muted);
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-danger);
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
&--theme:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
gap: 2px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: auto;
|
||||
transition: border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
// Mobile bottom nav
|
||||
.mobile-nav {
|
||||
display: flex;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background-color var(--transition-normal),
|
||||
border-color var(--transition-normal);
|
||||
}
|
||||
|
||||
.mobile-nav-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--color-text-muted);
|
||||
border-radius: var(--radius-md);
|
||||
transition: color var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -67,12 +67,12 @@ const items = [
|
||||
},
|
||||
];
|
||||
|
||||
const { isDark } = useDarkMode();
|
||||
const onSearchFocus = ref(false);
|
||||
|
||||
function addSearchResult(bangumi: BangumiRule) {
|
||||
showAddRSS.value = true;
|
||||
searchRule.value = bangumi;
|
||||
console.log('searchRule', searchRule.value);
|
||||
}
|
||||
|
||||
watch(showAddRSS, (val) => {
|
||||
@@ -94,33 +94,28 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
h="pc:60 50"
|
||||
bg-theme-row
|
||||
text-white
|
||||
rounded="pc:16 10"
|
||||
fx-cer
|
||||
px="pc:24 15"
|
||||
>
|
||||
<div flex="~ gap-x-16">
|
||||
<div fx-cer gap-x="pc:16 10">
|
||||
<img src="/images/logo-light.svg" alt="favicon" wh="pc:24 20" />
|
||||
<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="/images/AutoBangumi.svg"
|
||||
:src="isDark ? '/images/AutoBangumi.svg' : '/images/AutoBangumi-dark.svg'"
|
||||
alt="AutoBangumi"
|
||||
rel
|
||||
h="18 pc:24"
|
||||
pc:top-2
|
||||
class="topbar-wordmark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div hidden pc:block>
|
||||
<div class="topbar-search">
|
||||
<ab-search-bar @add-bangumi="addSearchResult" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ml-auto>
|
||||
<div class="topbar-right">
|
||||
<ab-status-bar
|
||||
:items="items"
|
||||
:running="running"
|
||||
@@ -128,6 +123,7 @@ onUnmounted(() => {
|
||||
@change-lang="changeLocale"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ab-change-account v-model:show="showAccount"></ab-change-account>
|
||||
<ab-add-rss
|
||||
v-model:show="showAddRSS"
|
||||
@@ -135,3 +131,69 @@ onUnmounted(() => {
|
||||
></ab-add-rss>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
padding: 0 20px;
|
||||
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background-color var(--transition-normal),
|
||||
border-color var(--transition-normal),
|
||||
box-shadow var(--transition-normal);
|
||||
|
||||
@include forMobile {
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.topbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.topbar-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
@include forMobile {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-wordmark {
|
||||
height: 20px;
|
||||
position: relative;
|
||||
|
||||
@include forMobile {
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-search {
|
||||
display: none;
|
||||
|
||||
@include forPC {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.topbar-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
49
webui/src/hooks/useDarkMode.ts
Normal file
49
webui/src/hooks/useDarkMode.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { createSharedComposable, usePreferredDark } from '@vueuse/core';
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
export const useDarkMode = createSharedComposable(() => {
|
||||
const prefersDark = usePreferredDark();
|
||||
const stored = localStorage.getItem('theme') as ThemeMode | null;
|
||||
const mode = ref<ThemeMode>(stored || 'system');
|
||||
|
||||
const isDark = computed(() => {
|
||||
if (mode.value === 'system') return prefersDark.value;
|
||||
return mode.value === 'dark';
|
||||
});
|
||||
|
||||
function applyTheme() {
|
||||
const html = document.documentElement;
|
||||
if (isDark.value) {
|
||||
html.classList.add('dark');
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
function setMode(newMode: ThemeMode) {
|
||||
mode.value = newMode;
|
||||
if (newMode === 'system') {
|
||||
localStorage.removeItem('theme');
|
||||
} else {
|
||||
localStorage.setItem('theme', newMode);
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
setMode(isDark.value ? 'light' : 'dark');
|
||||
}
|
||||
|
||||
watch(isDark, applyTheme, { immediate: true });
|
||||
watch(prefersDark, () => {
|
||||
if (mode.value === 'system') applyTheme();
|
||||
});
|
||||
|
||||
return {
|
||||
mode,
|
||||
isDark,
|
||||
setMode,
|
||||
toggle,
|
||||
};
|
||||
});
|
||||
@@ -7,18 +7,22 @@ definePage({
|
||||
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<a href="#main-content" class="skip-link">Skip to content</a>
|
||||
|
||||
<ab-topbar />
|
||||
|
||||
<main class="layout-main">
|
||||
<ab-sidebar />
|
||||
|
||||
<div class="layout-content">
|
||||
<div id="main-content" class="layout-content">
|
||||
<ab-page-title :title="$route.name"></ab-page-title>
|
||||
|
||||
<RouterView v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
<transition name="page" mode="out-in">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</transition>
|
||||
</RouterView>
|
||||
</div>
|
||||
</main>
|
||||
@@ -36,7 +40,8 @@ definePage({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background: #f0f0f0;
|
||||
background: var(--color-bg);
|
||||
transition: background-color var(--transition-normal);
|
||||
|
||||
@include forPC {
|
||||
min-width: 1024px;
|
||||
@@ -51,10 +56,10 @@ definePage({
|
||||
|
||||
.layout-main {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
gap: var(--layout-gap);
|
||||
|
||||
overflow: hidden;
|
||||
height: calc(100vh - 2 * var(--layout-padding) - 60px - var(--layout-gap));
|
||||
height: calc(100vh - 2 * var(--layout-padding) - 56px - var(--layout-gap));
|
||||
|
||||
@include forMobile {
|
||||
flex-direction: column-reverse;
|
||||
@@ -69,5 +74,24 @@ definePage({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: var(--layout-gap);
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 16px;
|
||||
z-index: 100;
|
||||
padding: 8px 16px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
transition: top var(--transition-fast);
|
||||
|
||||
&:focus {
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,45 +15,60 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div overflow-auto mt-12 flex-grow>
|
||||
<div>
|
||||
<transition-group
|
||||
name="bangumi"
|
||||
tag="div"
|
||||
flex="~ wrap"
|
||||
gap="20"
|
||||
:class="{ 'justify-center': isMobile }"
|
||||
>
|
||||
<ab-bangumi-card
|
||||
v-for="i in bangumi"
|
||||
:key="i.id"
|
||||
:class="[i.deleted && 'grayscale']"
|
||||
:bangumi="i"
|
||||
type="primary"
|
||||
@click="() => openEditPopup(i)"
|
||||
></ab-bangumi-card>
|
||||
</transition-group>
|
||||
<div class="page-bangumi">
|
||||
<transition-group
|
||||
name="bangumi"
|
||||
tag="div"
|
||||
class="bangumi-grid"
|
||||
:class="{ 'bangumi-grid--centered': isMobile }"
|
||||
>
|
||||
<ab-bangumi-card
|
||||
v-for="i in bangumi"
|
||||
:key="i.id"
|
||||
:class="[i.deleted && 'grayscale']"
|
||||
:bangumi="i"
|
||||
type="primary"
|
||||
@click="() => openEditPopup(i)"
|
||||
></ab-bangumi-card>
|
||||
</transition-group>
|
||||
|
||||
<ab-edit-rule
|
||||
v-model:show="editRule.show"
|
||||
v-model:rule="editRule.item"
|
||||
@enable="(id) => enableRule(id)"
|
||||
@delete-file="
|
||||
(type, { id, deleteFile }) => ruleManage(type, id, deleteFile)
|
||||
"
|
||||
@apply="(rule) => updateRule(rule.id, rule)"
|
||||
></ab-edit-rule>
|
||||
</div>
|
||||
<ab-edit-rule
|
||||
v-model:show="editRule.show"
|
||||
v-model:rule="editRule.item"
|
||||
@enable="(id) => enableRule(id)"
|
||||
@delete-file="
|
||||
(type, { id, deleteFile }) => ruleManage(type, id, deleteFile)
|
||||
"
|
||||
@apply="(rule) => updateRule(rule.id, rule)"
|
||||
></ab-edit-rule>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-bangumi {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.bangumi-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
|
||||
&--centered {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.bangumi-enter-active,
|
||||
.bangumi-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
transition: opacity var(--transition-normal), transform var(--transition-normal);
|
||||
}
|
||||
.bangumi-enter-from,
|
||||
.bangumi-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,46 +12,70 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div overflow-auto mt-12 flex-grow>
|
||||
<div h-full>
|
||||
<div grid="~ pc:cols-2 gap-20" mb-auto>
|
||||
<div space-y-20>
|
||||
<config-normal></config-normal>
|
||||
|
||||
<config-parser></config-parser>
|
||||
|
||||
<config-download></config-download>
|
||||
|
||||
<config-manage></config-manage>
|
||||
</div>
|
||||
|
||||
<div space-y-20>
|
||||
<config-notification></config-notification>
|
||||
|
||||
<config-proxy></config-proxy>
|
||||
|
||||
<config-player></config-player>
|
||||
|
||||
<config-openai></config-openai>
|
||||
</div>
|
||||
<div class="page-config">
|
||||
<div class="config-grid">
|
||||
<div class="config-col">
|
||||
<config-normal></config-normal>
|
||||
<config-parser></config-parser>
|
||||
<config-download></config-download>
|
||||
<config-manage></config-manage>
|
||||
</div>
|
||||
|
||||
<div fx-cer justify-end gap-8 mt-20>
|
||||
<ab-button
|
||||
:class="[{ 'flex-1': isMobile }]"
|
||||
type="warn"
|
||||
@click="getConfig"
|
||||
>
|
||||
{{ $t('config.cancel') }}
|
||||
</ab-button>
|
||||
<ab-button
|
||||
:class="[{ 'flex-1': isMobile }]"
|
||||
type="primary"
|
||||
@click="setConfig"
|
||||
>
|
||||
{{ $t('config.apply') }}
|
||||
</ab-button>
|
||||
<div class="config-col">
|
||||
<config-notification></config-notification>
|
||||
<config-proxy></config-proxy>
|
||||
<config-player></config-player>
|
||||
<config-openai></config-openai>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-actions">
|
||||
<ab-button
|
||||
:class="[{ 'flex-1': isMobile }]"
|
||||
type="warn"
|
||||
@click="getConfig"
|
||||
>
|
||||
{{ $t('config.cancel') }}
|
||||
</ab-button>
|
||||
<ab-button
|
||||
:class="[{ 'flex-1': isMobile }]"
|
||||
type="primary"
|
||||
@click="setConfig"
|
||||
>
|
||||
{{ $t('config.apply') }}
|
||||
</ab-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-config {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: auto;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.config-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,22 +24,58 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div overflow-auto mt-12 flex-grow>
|
||||
<div class="page-embed">
|
||||
<template v-if="isNull">
|
||||
<div wh-full f-cer text-h1 text-primary>
|
||||
<RouterLink to="/config" hover:underline>{{
|
||||
$t('downloader.hit')
|
||||
}}</RouterLink>
|
||||
<div class="embed-empty">
|
||||
<RouterLink to="/config" class="embed-link">
|
||||
{{ $t('downloader.hit') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<iframe
|
||||
v-else
|
||||
:src="url"
|
||||
frameborder="0"
|
||||
allowfullscreen="true"
|
||||
wh-full
|
||||
flex-1
|
||||
rounded-12
|
||||
class="embed-frame"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-embed {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
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%;
|
||||
flex: 1;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,13 +29,13 @@ const formatLog = computed(() => {
|
||||
});
|
||||
|
||||
function typeColor(type: string) {
|
||||
const M = {
|
||||
INFO: '#4e3c94',
|
||||
WARNING: '#A76E18',
|
||||
ERROR: '#C70E0E',
|
||||
DEBUG: '#A0A0A0',
|
||||
const M: Record<string, string> = {
|
||||
INFO: 'var(--color-primary)',
|
||||
WARNING: 'var(--color-warning)',
|
||||
ERROR: 'var(--color-danger)',
|
||||
DEBUG: 'var(--color-text-muted)',
|
||||
};
|
||||
return M[type];
|
||||
return M[type] || 'var(--color-text)';
|
||||
}
|
||||
|
||||
const logContainer = ref<HTMLElement | null>(null);
|
||||
@@ -66,40 +66,27 @@ onDeactivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div overflow-auto mt-12 flex-grow>
|
||||
<div flex="~ wrap gap-12">
|
||||
<ab-container :title="$t('log.title')" w-660 grow>
|
||||
<div
|
||||
ref="logContainer"
|
||||
rounded-10
|
||||
border="1 solid black"
|
||||
overflow-auto
|
||||
p-10
|
||||
max-h-60vh
|
||||
min-h-20vh
|
||||
>
|
||||
<div min-w-450>
|
||||
<div class="page-log">
|
||||
<div class="log-layout">
|
||||
<ab-container :title="$t('log.title')" class="log-main">
|
||||
<div ref="logContainer" class="log-viewer">
|
||||
<div class="log-content">
|
||||
<template v-for="i in formatLog" :key="i.index">
|
||||
<div
|
||||
p="y-10"
|
||||
leading="1.5em"
|
||||
border="0 b-1 solid"
|
||||
last:border-b-0
|
||||
flex="~ items-center gap-20"
|
||||
class="log-entry"
|
||||
:style="{ color: typeColor(i.type) }"
|
||||
>
|
||||
<div flex="~ col items-center gap-10" whitespace-nowrap>
|
||||
<div text="center">{{ i.type }}</div>
|
||||
<div>{{ i.date }}</div>
|
||||
<div class="log-meta">
|
||||
<div class="log-type">{{ i.type }}</div>
|
||||
<div class="log-date">{{ i.date }}</div>
|
||||
</div>
|
||||
|
||||
<div flex-1 break-all>{{ i.content }}</div>
|
||||
<div class="log-message">{{ i.content }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div flex="~ justify-end gap-x-10" mt-12>
|
||||
<div class="log-actions">
|
||||
<ab-button size="small" @click="getLog">
|
||||
{{ $t('log.update_now') }}
|
||||
</ab-button>
|
||||
@@ -114,9 +101,9 @@ onDeactivated(() => {
|
||||
</div>
|
||||
</ab-container>
|
||||
|
||||
<div grow w-500 space-y-20>
|
||||
<div class="log-sidebar">
|
||||
<ab-container :title="$t('log.contact_info')">
|
||||
<div space-y-12>
|
||||
<div class="contact-list">
|
||||
<ab-label label="Github">
|
||||
<ab-button
|
||||
size="small"
|
||||
@@ -137,7 +124,7 @@ onDeactivated(() => {
|
||||
</ab-button>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<ab-label label="X">
|
||||
<ab-button
|
||||
@@ -162,21 +149,17 @@ onDeactivated(() => {
|
||||
</ab-container>
|
||||
|
||||
<ab-container :title="$t('log.bug_repo')">
|
||||
<div space-y-12>
|
||||
<div class="bug-section">
|
||||
<ab-button
|
||||
mx-auto
|
||||
text-16
|
||||
w-300
|
||||
h-46
|
||||
rounded-10
|
||||
class="issues-btn"
|
||||
link="https://github.com/EstrellaXD/Auto_Bangumi/issues"
|
||||
>
|
||||
Github Issues
|
||||
</ab-button>
|
||||
|
||||
<div line></div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div text="center primary h3">
|
||||
<div class="version-info">
|
||||
<span>Version: </span>
|
||||
<span>{{ version }}</span>
|
||||
</div>
|
||||
@@ -186,3 +169,138 @@ onDeactivated(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-log {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.log-layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.log-main {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
min-width: 660px;
|
||||
}
|
||||
}
|
||||
|
||||
.log-viewer {
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
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 {
|
||||
padding: 10px 0;
|
||||
line-height: 1.5;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.log-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.log-date {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.log-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.log-sidebar {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
min-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.bug-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.issues-btn {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
height: 46px;
|
||||
font-size: 16px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.version-info {
|
||||
text-align: center;
|
||||
color: var(--color-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,12 +7,12 @@ const { url } = storeToRefs(usePlayerStore());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div overflow-auto mt-12 flex-grow>
|
||||
<div class="page-embed">
|
||||
<template v-if="url === ''">
|
||||
<div wh-full f-cer text-h1 text-primary>
|
||||
<RouterLink to="/config" hover:underline>{{
|
||||
$t('player.hit')
|
||||
}}</RouterLink>
|
||||
<div class="embed-empty">
|
||||
<RouterLink to="/config" class="embed-link">
|
||||
{{ $t('player.hit') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
<iframe
|
||||
@@ -20,9 +20,44 @@ const { url } = storeToRefs(usePlayerStore());
|
||||
:src="url"
|
||||
frameborder="0"
|
||||
allowfullscreen="true"
|
||||
wh-full
|
||||
flex-1
|
||||
rounded-12
|
||||
class="embed-frame"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-embed {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
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%;
|
||||
flex: 1;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -74,7 +74,7 @@ const RSSTableOptions = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div overflow-auto mt-12 flex-grow>
|
||||
<div class="page-rss">
|
||||
<ab-container :title="$t('rss.title')">
|
||||
<NDataTable
|
||||
v-bind="RSSTableOptions"
|
||||
@@ -82,13 +82,13 @@ const RSSTableOptions = computed(() => {
|
||||
></NDataTable>
|
||||
|
||||
<div v-if="selectedRSS.length > 0">
|
||||
<div line my-12></div>
|
||||
<div flex="~ justify-end gap-x-10">
|
||||
<div class="divider"></div>
|
||||
<div class="rss-actions">
|
||||
<ab-button @click="enableSelected">{{ $t('rss.enable') }}</ab-button>
|
||||
<ab-button @click="disableSelected">{{
|
||||
$t('rss.disable')
|
||||
}}</ab-button>
|
||||
<ab-button class="type-warn" @click="deleteSelected">{{
|
||||
<ab-button type="warn" @click="deleteSelected">{{
|
||||
$t('rss.delete')
|
||||
}}</ab-button>
|
||||
</div>
|
||||
@@ -96,3 +96,23 @@ const RSSTableOptions = computed(() => {
|
||||
</ab-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-rss {
|
||||
overflow: auto;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.rss-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,15 +7,15 @@ const { user, login } = useAuth();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div wh-screen f-cer bg-page>
|
||||
<ab-container :title="$t('login.title')" w-365 max-w="90%">
|
||||
<div space-y-16>
|
||||
<div class="page-login">
|
||||
<ab-container :title="$t('login.title')" class="login-card">
|
||||
<div class="login-form">
|
||||
<ab-label :label="$t('login.username')">
|
||||
<input
|
||||
v-model="user.username"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
ab-input
|
||||
class="login-input"
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
@@ -24,14 +24,14 @@ const { user, login } = useAuth();
|
||||
v-model="user.password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
ab-input
|
||||
class="login-input"
|
||||
@keyup.enter="login"
|
||||
/>
|
||||
</ab-label>
|
||||
|
||||
<div line></div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div flex="~ justify-end">
|
||||
<div class="login-actions">
|
||||
<ab-button size="small" @click="login">
|
||||
{{ $t('login.login_btn') }}
|
||||
</ab-button>
|
||||
@@ -40,3 +40,64 @@ const { user, login } = useAuth();
|
||||
</ab-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-login {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 365px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.login-input {
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
width: 200px;
|
||||
height: 28px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px rgba(108, 74, 182, 0.2);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,36 @@
|
||||
// Base styles
|
||||
|
||||
html {
|
||||
color-scheme: light;
|
||||
|
||||
&.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-bg);
|
||||
transition: color var(--transition-normal), background-color var(--transition-normal);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
// Scrollbar
|
||||
::-webkit-scrollbar {
|
||||
width: var(--scrollbar-size);
|
||||
height: var(--scrollbar-size);
|
||||
}
|
||||
|
||||
/* 滚动槽--外层轨道 */
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-color);
|
||||
}
|
||||
|
||||
/* 内层轨道(不包含滚动块部分) */
|
||||
/* 透明度设置为全透明,使得滚动条背景色为网页颜色 */
|
||||
::-webkit-scrollbar-track-piece {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 滚动条滑块 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: calc(var(--scrollbar-size) / 2);
|
||||
background: var(--scrollbar-thumb-color);
|
||||
@@ -24,18 +40,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条按钮 */
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 横向滚动条和纵向滚动条相交处尖角的颜色 */
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// Remove number input spinners
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// Reduced motion
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus visible ring
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ $min-pc: 1024px;
|
||||
|
||||
@mixin bg-mouse-event($normal, $hover, $active) {
|
||||
background: $normal;
|
||||
transition: background 0.3s;
|
||||
transition: background-color var(--transition-normal);
|
||||
|
||||
&:hover {
|
||||
background: $hover;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// transition
|
||||
// Transitions
|
||||
|
||||
.fade {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
@@ -13,14 +13,71 @@
|
||||
}
|
||||
}
|
||||
|
||||
// transition-group
|
||||
|
||||
.fade-list-enter-active,
|
||||
.fade-list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
transition: all var(--transition-slow);
|
||||
}
|
||||
|
||||
.fade-list-enter-from,
|
||||
.fade-list-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// Slide transitions for sidebar/panels
|
||||
.slide {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: transform var(--transition-normal), opacity var(--transition-normal);
|
||||
}
|
||||
|
||||
&-enter-from {
|
||||
transform: translateX(-8px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-leave-to {
|
||||
transform: translateX(8px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Page route transition
|
||||
.page {
|
||||
&-enter-active {
|
||||
transition: opacity var(--transition-normal), transform var(--transition-normal);
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition: opacity 100ms ease-in;
|
||||
}
|
||||
|
||||
&-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale-fade for dropdowns/menus
|
||||
.dropdown {
|
||||
&-enter-active {
|
||||
transition: opacity var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
&-leave-active {
|
||||
transition: opacity 100ms ease-in, transform 100ms ease-in;
|
||||
}
|
||||
|
||||
&-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px) scale(0.97);
|
||||
}
|
||||
|
||||
&-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px) scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,92 @@
|
||||
$scrollbar-color: #372a87;
|
||||
// Design System Variables
|
||||
// Light theme (default) + Dark theme (.dark class on html)
|
||||
|
||||
:root {
|
||||
// --- Colors ---
|
||||
--color-primary: #6C4AB6;
|
||||
--color-primary-hover: #563A92;
|
||||
--color-primary-light: #E8DEF8;
|
||||
--color-accent: #F97316;
|
||||
--color-success: #22C55E;
|
||||
--color-danger: #EF4444;
|
||||
--color-warning: #F59E0B;
|
||||
|
||||
--color-bg: #FAFAFA;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-surface-hover: #F5F5F5;
|
||||
|
||||
--color-text: #1E293B;
|
||||
--color-text-secondary: #64748B;
|
||||
--color-text-muted: #94A3B8;
|
||||
|
||||
--color-border: #E2E8F0;
|
||||
--color-border-hover: #CBD5E1;
|
||||
|
||||
// --- Shadows ---
|
||||
--shadow-color: rgba(0, 0, 0, 0.08);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// --- Radius ---
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
// --- Transitions ---
|
||||
--transition-fast: 150ms ease-out;
|
||||
--transition-normal: 200ms ease-out;
|
||||
--transition-slow: 300ms ease-out;
|
||||
|
||||
// --- Scrollbar ---
|
||||
--scrollbar-size: 6px;
|
||||
--scrollbar-color: transparent;
|
||||
--scrollbar-thumb-color: #{rgba($scrollbar-color, 0.5)};
|
||||
--scrollbar-thumb-hover-color: #{rgba($scrollbar-color, 1)};
|
||||
--scrollbar-thumb-color: rgba(108, 74, 182, 0.3);
|
||||
--scrollbar-thumb-hover-color: rgba(108, 74, 182, 0.6);
|
||||
|
||||
// --- Layout ---
|
||||
--layout-padding: 16px;
|
||||
--layout-gap: 12px;
|
||||
|
||||
// --- Typography ---
|
||||
--font-family: 'Inter', -apple-system, 'Noto Sans SC', 'Microsoft YaHei', system-ui, sans-serif;
|
||||
|
||||
@include forMobile {
|
||||
--layout-padding: 10px;
|
||||
--layout-padding: 12px;
|
||||
--layout-gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark theme
|
||||
.dark {
|
||||
--color-primary: #8B6CC7;
|
||||
--color-primary-hover: #A78BDB;
|
||||
--color-primary-light: #2D2250;
|
||||
--color-accent: #FB923C;
|
||||
--color-success: #4ADE80;
|
||||
--color-danger: #F87171;
|
||||
--color-warning: #FBBF24;
|
||||
|
||||
--color-bg: #0F172A;
|
||||
--color-surface: #1E293B;
|
||||
--color-surface-hover: #334155;
|
||||
|
||||
--color-text: #F1F5F9;
|
||||
--color-text-secondary: #94A3B8;
|
||||
--color-text-muted: #64748B;
|
||||
|
||||
--color-border: #334155;
|
||||
--color-border-hover: #475569;
|
||||
|
||||
// --- Shadows (darker for dark mode) ---
|
||||
--shadow-color: rgba(0, 0, 0, 0.3);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.2);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
|
||||
|
||||
// --- Scrollbar ---
|
||||
--scrollbar-thumb-color: rgba(139, 108, 199, 0.3);
|
||||
--scrollbar-thumb-hover-color: rgba(139, 108, 199, 0.6);
|
||||
}
|
||||
|
||||
188
webui/types/dts/auto-imports.d.ts
vendored
188
webui/types/dts/auto-imports.d.ts
vendored
@@ -15,56 +15,33 @@ declare global {
|
||||
const apiRSS: typeof import('../../src/api/rss')['apiRSS']
|
||||
const apiSearch: typeof import('../../src/api/search')['apiSearch']
|
||||
const assert: typeof import('vitest')['assert']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const axios: typeof import('../../src/utils/axios')['axios']
|
||||
const beforeAll: typeof import('vitest')['beforeAll']
|
||||
const beforeEach: typeof import('vitest')['beforeEach']
|
||||
const chai: typeof import('vitest')['chai']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createPinia: typeof import('pinia')['createPinia']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineLoader: typeof import('vue-router/auto')['defineLoader']
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['_definePage']
|
||||
const defineStore: typeof import('pinia')['defineStore']
|
||||
const describe: typeof import('vitest')['describe']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const expect: typeof import('vitest')['expect']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getActivePinia: typeof import('pinia')['getActivePinia']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const it: typeof import('vitest')['it']
|
||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
@@ -78,37 +55,20 @@ declare global {
|
||||
const onBeforeRouteUpdate: typeof import('vue-router/auto')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const setActivePinia: typeof import('pinia')['setActivePinia']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
@@ -116,188 +76,42 @@ declare global {
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||
const suite: typeof import('vitest')['suite']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const test: typeof import('vitest')['test']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useApi: typeof import('../../src/hooks/useApi')['useApi']
|
||||
const useAppInfo: typeof import('../../src/hooks/useAppInfo')['useAppInfo']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useAuth: typeof import('../../src/hooks/useAuth')['useAuth']
|
||||
const useBangumiStore: typeof import('../../src/store/bangumi')['useBangumiStore']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpointQuery: typeof import('../../src/hooks/useBreakpointQuery')['useBreakpointQuery']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfigStore: typeof import('../../src/store/config')['useConfigStore']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useDarkMode: typeof import('../../src/hooks/useDarkMode')['useDarkMode']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useLogStore: typeof import('../../src/store/log')['useLogStore']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMessage: typeof import('../../src/hooks/useMessage')['useMessage']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useMyI18n: typeof import('../../src/hooks/useMyI18n')['useMyI18n']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePlayerStore: typeof import('../../src/store/player')['usePlayerStore']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const useProgramStore: typeof import('../../src/store/program')['useProgramStore']
|
||||
const useRSSStore: typeof import('../../src/store/rss')['useRSSStore']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router/auto')['useRoute']
|
||||
const useRouter: typeof import('vue-router/auto')['useRouter']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSearchStore: typeof import('../../src/store/search')['useSearchStore']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const vi: typeof import('vitest')['vi']
|
||||
const vitest: typeof import('vitest')['vitest']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import presetRemToPx from '@unocss/preset-rem-to-px';
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetUno(),
|
||||
presetUno({
|
||||
dark: 'class',
|
||||
}),
|
||||
presetRemToPx({
|
||||
baseFontSize: 4,
|
||||
}),
|
||||
@@ -32,38 +34,58 @@ export default defineConfig({
|
||||
pc: '1024px',
|
||||
},
|
||||
colors: {
|
||||
primary: '#493475',
|
||||
running: '#A3D491',
|
||||
stopped: '#DF7F91',
|
||||
page: '#F0F0F0',
|
||||
// Semantic colors via CSS variables (support light/dark)
|
||||
primary: 'var(--color-primary)',
|
||||
'primary-hover': 'var(--color-primary-hover)',
|
||||
'primary-light': 'var(--color-primary-light)',
|
||||
accent: 'var(--color-accent)',
|
||||
success: 'var(--color-success)',
|
||||
danger: 'var(--color-danger)',
|
||||
warning: 'var(--color-warning)',
|
||||
surface: 'var(--color-surface)',
|
||||
'surface-hover': 'var(--color-surface-hover)',
|
||||
'text-primary': 'var(--color-text)',
|
||||
'text-secondary': 'var(--color-text-secondary)',
|
||||
'text-muted': 'var(--color-text-muted)',
|
||||
border: 'var(--color-border)',
|
||||
'border-hover': 'var(--color-border-hover)',
|
||||
page: 'var(--color-bg)',
|
||||
|
||||
// Legacy aliases (for gradual migration)
|
||||
running: 'var(--color-success)',
|
||||
stopped: 'var(--color-danger)',
|
||||
},
|
||||
},
|
||||
rules: [
|
||||
[
|
||||
'bg-theme-row',
|
||||
{
|
||||
background: 'linear-gradient(90.5deg, #492897 1.53%, #783674 96.48%)',
|
||||
background: 'linear-gradient(90.5deg, var(--color-primary) 1.53%, var(--color-primary-hover) 96.48%)',
|
||||
},
|
||||
],
|
||||
[
|
||||
'bg-theme-col',
|
||||
{
|
||||
background: 'linear-gradient(180deg, #492897 0%, #783674 100%)',
|
||||
background: 'linear-gradient(180deg, var(--color-primary) 0%, var(--color-primary-hover) 100%)',
|
||||
},
|
||||
],
|
||||
[
|
||||
'poster-shandow',
|
||||
{
|
||||
filter: 'drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.1))',
|
||||
filter: 'drop-shadow(2px 2px 2px var(--shadow-color, rgba(0, 0, 0, 0.1)))',
|
||||
},
|
||||
],
|
||||
[
|
||||
'poster-pen-active',
|
||||
{
|
||||
background: '#B4ABC6',
|
||||
'box-shadow': '2px 2px 4px rgba(0, 0, 0, 0.25)',
|
||||
background: 'var(--color-primary-light)',
|
||||
'box-shadow': '2px 2px 4px var(--shadow-color, rgba(0, 0, 0, 0.25))',
|
||||
},
|
||||
],
|
||||
// Shadows
|
||||
['shadow-sm', { 'box-shadow': 'var(--shadow-sm)' }],
|
||||
['shadow-md', { 'box-shadow': 'var(--shadow-md)' }],
|
||||
['shadow-lg', { 'box-shadow': 'var(--shadow-lg)' }],
|
||||
],
|
||||
shortcuts: [
|
||||
[/^wh-(.*)$/, ([, t]) => `w-${t} h-${t}`],
|
||||
@@ -87,17 +109,24 @@ export default defineConfig({
|
||||
'text-h2': 'text-20',
|
||||
'text-h3': 'text-16',
|
||||
'text-main': 'text-12',
|
||||
'text-body': 'text-14',
|
||||
'text-sm': 'text-12',
|
||||
'text-xs': 'text-10',
|
||||
},
|
||||
|
||||
// input
|
||||
{
|
||||
'ab-input': `outline-none min-w-0 w-200 h-28
|
||||
px-12 text-main text-right
|
||||
rounded-6 shadow-inset
|
||||
border-1 border-black hover:border-color-[#7A46AE]
|
||||
rounded-6
|
||||
border-1 border-border
|
||||
bg-surface text-text-primary
|
||||
hover:border-primary
|
||||
focus:border-primary focus:ring-2 focus:ring-primary/20
|
||||
transition-colors duration-150
|
||||
`,
|
||||
|
||||
'input-error': 'border-color-[#CA0E0E]',
|
||||
'input-error': 'border-danger',
|
||||
'input-reset': 'bg-transparent min-w-0 flex-1 outline-none',
|
||||
},
|
||||
|
||||
@@ -105,12 +134,12 @@ export default defineConfig({
|
||||
{
|
||||
'is-btn': 'cursor-pointer select-none',
|
||||
'btn-click': 'hover:scale-110 active:scale-100',
|
||||
'is-disabled': 'cursor-not-allowed select-none',
|
||||
'is-disabled': 'cursor-not-allowed select-none opacity-50',
|
||||
},
|
||||
|
||||
// other
|
||||
{
|
||||
line: 'w-full h-1 bg-[#DFE1EF]',
|
||||
line: 'w-full h-1 bg-border',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -29,7 +29,16 @@ export default defineConfig(({ mode }) => ({
|
||||
'vue',
|
||||
'vitest',
|
||||
'pinia',
|
||||
'@vueuse/core',
|
||||
{
|
||||
'@vueuse/core': [
|
||||
'createSharedComposable',
|
||||
'useBreakpoints',
|
||||
'usePreferredDark',
|
||||
'useClipboard',
|
||||
'useLocalStorage',
|
||||
'useIntervalFn',
|
||||
],
|
||||
},
|
||||
VueRouterAutoImports,
|
||||
'vue-i18n',
|
||||
],
|
||||
@@ -107,8 +116,8 @@ export default defineConfig(({ mode }) => ({
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'^/api/.*': 'http://192.168.0.100:7892',
|
||||
'^/posters/.*': 'http://192.168.0.100:7892',
|
||||
'^/api/.*': 'http://localhost:7892',
|
||||
'^/posters/.*': 'http://localhost:7892',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user