refactor(layout): 重构基础布局头部组件结构
This commit is contained in:
@@ -6,6 +6,10 @@ This file provides guidance to AI when working with code in this repository.
|
|||||||
|
|
||||||
Vue 3 TypeScript application with Vite.
|
Vue 3 TypeScript application with Vite.
|
||||||
|
|
||||||
|
## 开发服务器
|
||||||
|
|
||||||
|
- **不要启动开发服务器**: 开发服务器通常已经由用户启动。除非特别要求,否则不要执行 `pnpm dev` 之类的命令。
|
||||||
|
|
||||||
### Routing & Layouts
|
### Routing & Layouts
|
||||||
|
|
||||||
- **File-based routing**: Uses `unplugin-vue-router` with `.page.vue` and `.page.md` extensions in `src/pages/`
|
- **File-based routing**: Uses `unplugin-vue-router` with `.page.vue` and `.page.md` extensions in `src/pages/`
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const buttonRef = useTemplateRef('buttonRef');
|
|
||||||
const appStore = useAppStore();
|
|
||||||
|
|
||||||
function toggleCollapsed() {
|
|
||||||
// https://github.com/tusen-ai/naive-ui/issues/3688
|
|
||||||
// hover style 鼠标移出就会消失 如果是点击 button 会聚焦需要失去焦点才会恢复
|
|
||||||
buttonRef.value?.$el.blur();
|
|
||||||
appStore.toggleSidebar();
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeLabels: Record<AppThemeMode, string> = {
|
|
||||||
light: '浅色',
|
|
||||||
dark: '深色',
|
|
||||||
system: '跟随系统',
|
|
||||||
};
|
|
||||||
|
|
||||||
const { locale } = useI18n();
|
|
||||||
const languageLabels: Record<string, string> = {
|
|
||||||
'en-US': 'English',
|
|
||||||
'zh-CN': '简体中文',
|
|
||||||
};
|
|
||||||
function toggleLanguage() {
|
|
||||||
locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="h-full flex items-center justify-between px-12px shadow-header dark:shadow-gray-700">
|
|
||||||
<div>
|
|
||||||
<NTooltip :disabled="appStore.isMobile" placement="bottom-start">
|
|
||||||
{{ appStore.sidebarCollapsed ? '展开菜单' : '收起菜单' }}
|
|
||||||
<template #trigger>
|
|
||||||
<NButton ref="buttonRef" quaternary @click="toggleCollapsed">
|
|
||||||
<icon-line-md:menu-fold-right v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
|
|
||||||
<icon-line-md:menu-fold-left v-else w-4.5 h-4.5 />
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
</NTooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center">
|
|
||||||
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
|
||||||
{{ languageLabels[locale] }}
|
|
||||||
<template #trigger>
|
|
||||||
<NButton quaternary @click="toggleLanguage">
|
|
||||||
<icon-clarity:language-line w-4.5 h-4.5 />
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
</NTooltip>
|
|
||||||
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
|
||||||
{{ themeLabels[appStore.themeMode] }}
|
|
||||||
<template #trigger>
|
|
||||||
<NButton quaternary @click="appStore.cycleTheme">
|
|
||||||
<icon-line-md:sunny-filled-loop-to-moon-filled-loop-transition
|
|
||||||
v-if="appStore.themeMode === 'light'"
|
|
||||||
w-4.5
|
|
||||||
h-4.5
|
|
||||||
/>
|
|
||||||
<icon-line-md:moon-filled-to-sunny-filled-loop-transition
|
|
||||||
v-else-if="appStore.themeMode === 'dark'"
|
|
||||||
w-4.5
|
|
||||||
h-4.5
|
|
||||||
/>
|
|
||||||
<icon-line-md:computer v-else w-4.5 h-4.5 />
|
|
||||||
</NButton>
|
|
||||||
</template>
|
|
||||||
</NTooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
|
||||||
|
import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
|
||||||
|
import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-full flex items-center justify-between px-12px shadow-header dark:shadow-gray-700">
|
||||||
|
<ToggleSiderButton />
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<LanguageSwitchButton />
|
||||||
|
<ThemeSwitchButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownOption } from 'naive-ui';
|
||||||
|
|
||||||
|
const { locale, availableLocales } = useI18n({ useScope: 'global' });
|
||||||
|
|
||||||
|
const languageLabels: Record<string, string> = {
|
||||||
|
'en-US': 'English',
|
||||||
|
'zh-CN': '简体中文',
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = computed<DropdownOption[]>(() =>
|
||||||
|
availableLocales.map((lang) => ({
|
||||||
|
label: languageLabels[lang] || lang,
|
||||||
|
key: lang,
|
||||||
|
disabled: locale.value === lang,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleSelect(key: string) {
|
||||||
|
locale.value = key;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NDropdown trigger="hover" placement="bottom-end" :options="options" @select="handleSelect">
|
||||||
|
<NButton quaternary class="flex items-center gap-1">
|
||||||
|
<template #icon>
|
||||||
|
<icon-clarity:language-line w-4.5 h-4.5 />
|
||||||
|
</template>
|
||||||
|
<span>{{ languageLabels[locale] }}</span>
|
||||||
|
</NButton>
|
||||||
|
</NDropdown>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const themeLabels: Record<AppThemeMode, string> = {
|
||||||
|
light: '浅色',
|
||||||
|
dark: '深色',
|
||||||
|
system: '跟随系统',
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NTooltip :disabled="appStore.isMobile" placement="bottom-end">
|
||||||
|
{{ themeLabels[appStore.themeMode] }}
|
||||||
|
<template #trigger>
|
||||||
|
<NButton quaternary @click="appStore.cycleTheme">
|
||||||
|
<icon-line-md:sunny-filled-loop-to-moon-filled-loop-transition
|
||||||
|
v-if="appStore.themeMode === 'light'"
|
||||||
|
w-4.5
|
||||||
|
h-4.5
|
||||||
|
/>
|
||||||
|
<icon-line-md:moon-filled-to-sunny-filled-loop-transition
|
||||||
|
v-else-if="appStore.themeMode === 'dark'"
|
||||||
|
w-4.5
|
||||||
|
h-4.5
|
||||||
|
/>
|
||||||
|
<icon-line-md:computer v-else w-4.5 h-4.5 />
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const buttonRef = useTemplateRef('buttonRef');
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
function toggleCollapsed() {
|
||||||
|
// https://github.com/tusen-ai/naive-ui/issues/3688
|
||||||
|
// hover style 鼠标移出就会消失 如果是点击 button 会聚焦需要失去焦点才会恢复
|
||||||
|
buttonRef.value?.$el.blur();
|
||||||
|
appStore.toggleSidebar();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NTooltip :disabled="appStore.isMobile" placement="bottom-start">
|
||||||
|
{{ appStore.sidebarCollapsed ? '展开菜单' : '收起菜单' }}
|
||||||
|
<template #trigger>
|
||||||
|
<NButton ref="buttonRef" quaternary @click="toggleCollapsed">
|
||||||
|
<icon-line-md:menu-fold-right v-if="appStore.sidebarCollapsed" w-4.5 h-4.5 />
|
||||||
|
<icon-line-md:menu-fold-left v-else w-4.5 h-4.5 />
|
||||||
|
</NButton>
|
||||||
|
</template>
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { AdminLayout } from '@sa/materials';
|
import { AdminLayout } from '@sa/materials';
|
||||||
import BaseLayoutHeader from './base-layout-header.vue';
|
import BaseLayoutHeader from './base-layout-header/base-layout-header.vue';
|
||||||
import BaseLayoutSider from './base-layout-sider.vue';
|
import BaseLayoutSider from './base-layout-sider.vue';
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|||||||
@@ -3,23 +3,23 @@
|
|||||||
* at once using the import syntax
|
* at once using the import syntax
|
||||||
*/
|
*/
|
||||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
import messages from '@intlify/unplugin-vue-i18n/messages';
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
|
|
||||||
export function install({ app }: { app: import('vue').App<Element> }) {
|
export function install({ app }: { app: import('vue').App<Element> }) {
|
||||||
app.use(
|
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
|
||||||
// https://vue-i18n.intlify.dev/guide/essentials/started.html#registering-the-i18n-plugin
|
const i18n = createI18n({
|
||||||
createI18n({
|
legacy: false, // you must set `false`, to use Composition API
|
||||||
legacy: false, // you must set `false`, to use Composition API
|
locale: navigator.language,
|
||||||
locale: navigator.language,
|
fallbackRoot: false,
|
||||||
fallbackRoot: false,
|
// flatJson: true,
|
||||||
// flatJson: true,
|
missing: (locale, key /* , instance, type */) => {
|
||||||
missing: (locale, key /* , instance, type */) => {
|
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
|
||||||
consola.warn(`缺少国际化内容: locale='${locale}', key='${key}'`);
|
return `[${key}]`;
|
||||||
return `[${key}]`;
|
},
|
||||||
},
|
missingWarn: !true,
|
||||||
missingWarn: !true,
|
fallbackWarn: !true,
|
||||||
fallbackWarn: !true,
|
messages,
|
||||||
messages,
|
});
|
||||||
}),
|
app.use(i18n);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,9 @@ export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
|
|||||||
NaiveUiResolver(),
|
NaiveUiResolver(),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// https://github.com/unplugin/unplugin-icons?tab=readme-ov-file
|
||||||
|
// https://icon-sets.iconify.design
|
||||||
Icons({
|
Icons({
|
||||||
autoInstall: true,
|
autoInstall: true,
|
||||||
customCollections: {
|
customCollections: {
|
||||||
|
|||||||
@@ -121,8 +121,18 @@ export default defineConfig(async (configEnv) => {
|
|||||||
|
|
||||||
// https://www.npmjs.com/package/utils4u/v/2.19.2?activeTab=code
|
// https://www.npmjs.com/package/utils4u/v/2.19.2?activeTab=code
|
||||||
manualChunks: (id: string, _meta: ManualChunkMeta) => {
|
manualChunks: (id: string, _meta: ManualChunkMeta) => {
|
||||||
if (!id.includes('node_modules')) return;
|
/* 有一个问题: dynamic import will not move module into another chunk.
|
||||||
|
if (['/src/layouts'].some((prefix) => id.includes(prefix))) {
|
||||||
|
return 'layouts';
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (id.includes('index.page.vue')) {
|
||||||
|
const parentDir = path.basename(path.dirname(id));
|
||||||
|
return `${parentDir}-index.page`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!id.includes('node_modules')) return;
|
||||||
// 处理 pnpm 的特殊路径结构
|
// 处理 pnpm 的特殊路径结构
|
||||||
let packageName;
|
let packageName;
|
||||||
if (id.includes('.pnpm')) {
|
if (id.includes('.pnpm')) {
|
||||||
@@ -140,6 +150,10 @@ export default defineConfig(async (configEnv) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (packageName) {
|
if (packageName) {
|
||||||
|
if (['highlight.js'].includes(packageName)) {
|
||||||
|
return 'lib-hljs';
|
||||||
|
}
|
||||||
|
|
||||||
// 根据包名分组
|
// 根据包名分组
|
||||||
if (['consola', 'lodash', '@juggle+resize-observer'].includes(packageName)) {
|
if (['consola', 'lodash', '@juggle+resize-observer'].includes(packageName)) {
|
||||||
return 'lib-vendor';
|
return 'lib-vendor';
|
||||||
|
|||||||
Reference in New Issue
Block a user