financial_system/frontend/src/components/AppearancePanel.vue
2026-06-18 13:21:20 +08:00

317 lines
11 KiB
Vue

<script setup lang="ts">
import { Check, RotateCcw, X } from "@lucide/vue";
import {
themeOptions,
useThemeStore,
type ContentWidthChoice,
type DensityChoice,
type FontChoice,
type LayoutChoice,
type RadiusChoice,
type SidebarChoice,
type ThemeMode,
} from "../stores/theme";
const emit = defineEmits<{
close: [];
}>();
const theme = useThemeStore();
const modeOptions: Array<{ name: ThemeMode; label: string }> = [
{ name: "system", label: "系统" },
{ name: "light", label: "浅色" },
{ name: "dark", label: "深色" },
];
const fontOptions: Array<{ name: FontChoice; label: string }> = [
{ name: "auto", label: "Auto" },
{ name: "sans", label: "Sans" },
{ name: "serif", label: "Serif" },
];
const radiusOptions: Array<{ name: RadiusChoice; label: string; previewRadius: string }> = [
{ name: "auto", label: "Auto", previewRadius: "20px" },
{ name: "0", label: "0", previewRadius: "0px" },
{ name: "0.3", label: "0.3", previewRadius: "8px" },
{ name: "0.5", label: "0.5", previewRadius: "14px" },
{ name: "0.75", label: "0.75", previewRadius: "20px" },
{ name: "1", label: "1.0", previewRadius: "28px" },
];
const densityOptions: Array<{ name: DensityChoice; label: string; lines: number }> = [
{ name: "compact", label: "紧凑", lines: 5 },
{ name: "default", label: "默认", lines: 4 },
{ name: "comfortable", label: "宽松", lines: 3 },
{ name: "large", label: "超大", lines: 1 },
];
const sidebarOptions: Array<{ name: SidebarChoice; label: string }> = [
{ name: "embedded", label: "内嵌" },
{ name: "floating", label: "浮动" },
{ name: "edge", label: "侧边栏" },
];
const layoutOptions: Array<{ name: LayoutChoice; label: string }> = [
{ name: "default", label: "默认" },
{ name: "compact", label: "紧凑" },
{ name: "fullscreen", label: "全屏布局" },
];
const contentWidthOptions: Array<{ name: ContentWidthChoice; label: string }> = [
{ name: "full", label: "全宽" },
{ name: "centered", label: "居中" },
];
</script>
<template>
<div class="appearance-panel-layer" @click.self="emit('close')">
<aside class="appearance-panel" role="dialog" aria-label="外观设置">
<header class="appearance-panel-header">
<div>
<h2>外观</h2>
<p>主题颜色与布局</p>
</div>
<button class="icon-button" type="button" title="关闭" @click="emit('close')">
<X :size="21" aria-hidden="true" />
</button>
</header>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>主题</h3>
<button class="appearance-reset" type="button" title="恢复默认主题" @click="theme.resetTheme">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid mode-grid">
<button
v-for="item in modeOptions"
:key="item.name"
class="appearance-choice theme-mode-choice"
:class="[`is-${item.name}`, { 'is-active': theme.mode === item.name }]"
type="button"
@click="theme.setMode(item.name)"
>
<span class="mode-preview" aria-hidden="true">
<span class="preview-sidebar">
<i />
<i />
<i />
</span>
<span class="preview-chart">
<i />
<i />
<i />
</span>
<span class="preview-pie" />
<span class="preview-card" />
</span>
<strong>{{ item.label }}</strong>
<span v-if="theme.mode === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>颜色预设</h3>
<button class="appearance-reset" type="button" title="恢复默认颜色" @click="theme.resetColor">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid color-preset-grid">
<button
v-for="item in themeOptions"
:key="item.name"
class="color-preset-choice"
:class="{ 'is-active': theme.current === item.name }"
:style="{ '--preset-from': item.gradient[0], '--preset-to': item.gradient[1] }"
type="button"
@click="theme.setTheme(item.name)"
>
<span class="color-preset-swatch" aria-hidden="true" />
<strong>{{ item.label }}</strong>
<span v-if="theme.current === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>字体</h3>
<button class="appearance-reset" type="button" title="恢复默认字体" @click="theme.resetFont">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid font-grid">
<button
v-for="item in fontOptions"
:key="item.name"
class="appearance-choice font-choice"
:class="[`font-${item.name}`, { 'is-active': theme.font === item.name }]"
type="button"
@click="theme.setFont(item.name)"
>
<span>Aa</span>
<strong>{{ item.label }}</strong>
<span v-if="theme.font === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>圆角</h3>
<button class="appearance-reset" type="button" title="恢复默认圆角" @click="theme.resetRadius">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid radius-grid">
<button
v-for="item in radiusOptions"
:key="item.name"
class="appearance-choice radius-choice"
:class="{ 'is-active': theme.radius === item.name }"
:style="{ '--preview-radius': item.previewRadius }"
type="button"
@click="theme.setRadius(item.name)"
>
<span class="radius-preview" aria-hidden="true" />
<strong>{{ item.label }}</strong>
<span v-if="theme.radius === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>密度</h3>
<button class="appearance-reset" type="button" title="恢复默认密度" @click="theme.resetDensity">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid density-grid">
<button
v-for="item in densityOptions"
:key="item.name"
class="appearance-choice density-choice"
:class="{ 'is-active': theme.density === item.name }"
type="button"
@click="theme.setDensity(item.name)"
>
<span class="density-preview" aria-hidden="true">
<i v-for="line in item.lines" :key="line" />
</span>
<strong>{{ item.label }}</strong>
<span v-if="theme.density === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>侧边栏</h3>
<button class="appearance-reset" type="button" title="恢复默认侧边栏" @click="theme.resetSidebar">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid sidebar-grid">
<button
v-for="item in sidebarOptions"
:key="item.name"
class="appearance-choice layout-choice sidebar-choice"
:class="[`is-${item.name}`, { 'is-active': theme.sidebar === item.name }]"
type="button"
@click="theme.setSidebar(item.name)"
>
<span class="layout-preview" aria-hidden="true">
<span class="layout-side">
<i />
<i />
<i />
</span>
<span class="layout-main" />
</span>
<strong>{{ item.label }}</strong>
<span v-if="theme.sidebar === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>布局</h3>
<button class="appearance-reset" type="button" title="恢复默认布局" @click="theme.resetLayout">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid layout-grid">
<button
v-for="item in layoutOptions"
:key="item.name"
class="appearance-choice layout-choice dashboard-layout-choice"
:class="[`is-${item.name}`, { 'is-active': theme.layout === item.name }]"
type="button"
@click="theme.setLayout(item.name)"
>
<span class="dashboard-preview" aria-hidden="true">
<i />
<i />
<i />
<i />
<i />
</span>
<strong>{{ item.label }}</strong>
<span v-if="theme.layout === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
<section class="appearance-section">
<div class="appearance-section-head">
<h3>内容宽度</h3>
<button class="appearance-reset" type="button" title="恢复默认内容宽度" @click="theme.resetContentWidth">
<RotateCcw :size="17" aria-hidden="true" />
</button>
</div>
<div class="appearance-grid content-width-grid">
<button
v-for="item in contentWidthOptions"
:key="item.name"
class="appearance-choice content-width-choice"
:class="[`is-${item.name}`, { 'is-active': theme.contentWidth === item.name }]"
type="button"
@click="theme.setContentWidth(item.name)"
>
<span class="content-width-preview" aria-hidden="true">
<i />
<i />
<i />
</span>
<strong>{{ item.label }}</strong>
<span v-if="theme.contentWidth === item.name" class="choice-check" aria-hidden="true">
<Check :size="18" />
</span>
</button>
</div>
</section>
</aside>
</div>
</template>