317 lines
11 KiB
Vue
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>
|