mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-17 06:55:38 +00:00
chore: 初始化uniapp前端项目基础框架
新增项目配置、静态资源、工具类、状态管理、API接口、组件库集成等全套基础代码,完成项目初始化搭建
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* @Author: weisheng
|
||||
* @Date: 2025-09-02 09:42:36
|
||||
* @LastEditTime: 2026-04-10 11:01:37
|
||||
* @LastEditors: weisheng
|
||||
* @Description:
|
||||
* @FilePath: /wot-starter/src/composables/types/theme.ts
|
||||
* 记得注释
|
||||
*/
|
||||
import type { ConfigProviderThemeVars } from '@wot-ui/ui'
|
||||
|
||||
export type PrimaryShadeKey
|
||||
= | 'primary1'
|
||||
| 'primary2'
|
||||
| 'primary3'
|
||||
| 'primary4'
|
||||
| 'primary5'
|
||||
| 'primary6'
|
||||
| 'primary7'
|
||||
| 'primary8'
|
||||
| 'primary9'
|
||||
| 'primary10'
|
||||
|
||||
export type ThemePrimaryShades = Record<PrimaryShadeKey, string>
|
||||
|
||||
/**
|
||||
* 主题色选项接口
|
||||
*/
|
||||
export interface ThemeColorOption {
|
||||
name: string
|
||||
value: string
|
||||
// 主色 6,用于列表和当前主题色圆点展示
|
||||
primary: string
|
||||
// 完整主色阶,用于注入 ConfigProvider 主题变量
|
||||
primaryShades: ThemePrimaryShades
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题类型
|
||||
*/
|
||||
export type ThemeMode = 'light' | 'dark'
|
||||
|
||||
/**
|
||||
* 主题状态接口
|
||||
*/
|
||||
export interface ThemeState {
|
||||
theme: ThemeMode
|
||||
followSystem: boolean
|
||||
hasUserSet: boolean
|
||||
currentThemeColor: ThemeColorOption
|
||||
themeVars: ConfigProviderThemeVars
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统主题状态接口(简化版)
|
||||
*/
|
||||
export interface SystemThemeState {
|
||||
theme: ThemeMode
|
||||
themeVars: ConfigProviderThemeVars
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的主题色选项
|
||||
*/
|
||||
export const themeColorOptions: ThemeColorOption[] = [
|
||||
{
|
||||
name: '默认蓝',
|
||||
value: 'blue',
|
||||
primary: '#1C64FD',
|
||||
primaryShades: {
|
||||
primary1: '#F5F8FF',
|
||||
primary2: '#E5EDFF',
|
||||
primary3: '#B8CFFF',
|
||||
primary4: '#7CA4FF',
|
||||
primary5: '#4480FF',
|
||||
primary6: '#1C64FD',
|
||||
primary7: '#164ED1',
|
||||
primary8: '#1341AD',
|
||||
primary9: '#0F3285',
|
||||
primary10: '#0A235C',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '活力橙',
|
||||
value: 'orange',
|
||||
primary: '#FF7D00',
|
||||
primaryShades: {
|
||||
primary1: '#FFF7F0',
|
||||
primary2: '#FFEAD6',
|
||||
primary3: '#FFD0A8',
|
||||
primary4: '#FFB06E',
|
||||
primary5: '#FF9338',
|
||||
primary6: '#FF7D00',
|
||||
primary7: '#D96800',
|
||||
primary8: '#B35600',
|
||||
primary9: '#8A4200',
|
||||
primary10: '#5F2D00',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '薄荷绿',
|
||||
value: 'green',
|
||||
primary: '#07C160',
|
||||
primaryShades: {
|
||||
primary1: '#F1FCF6',
|
||||
primary2: '#DCF7E8',
|
||||
primary3: '#B4EFD0',
|
||||
primary4: '#7FE2AF',
|
||||
primary5: '#42D28A',
|
||||
primary6: '#07C160',
|
||||
primary7: '#049F4F',
|
||||
primary8: '#028241',
|
||||
primary9: '#016532',
|
||||
primary10: '#014825',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '樱花粉',
|
||||
value: 'pink',
|
||||
primary: '#FF69B4',
|
||||
primaryShades: {
|
||||
primary1: '#FFF2F9',
|
||||
primary2: '#FFE2F1',
|
||||
primary3: '#FFC2E3',
|
||||
primary4: '#FF99D1',
|
||||
primary5: '#FF7FC2',
|
||||
primary6: '#FF69B4',
|
||||
primary7: '#E84E9F',
|
||||
primary8: '#CC3F8A',
|
||||
primary9: '#A9336F',
|
||||
primary10: '#7A2450',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '紫罗兰',
|
||||
value: 'purple',
|
||||
primary: '#8A2BE2',
|
||||
primaryShades: {
|
||||
primary1: '#F7F2FF',
|
||||
primary2: '#EEE2FF',
|
||||
primary3: '#D9BDFF',
|
||||
primary4: '#BC8DFF',
|
||||
primary5: '#A05EFF',
|
||||
primary6: '#8A2BE2',
|
||||
primary7: '#7423BF',
|
||||
primary8: '#5E1D9C',
|
||||
primary9: '#491679',
|
||||
primary10: '#321056',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '朱砂红',
|
||||
value: 'red',
|
||||
primary: '#FF4757',
|
||||
primaryShades: {
|
||||
primary1: '#FFF3F4',
|
||||
primary2: '#FFE3E5',
|
||||
primary3: '#FFC0C6',
|
||||
primary4: '#FF909A',
|
||||
primary5: '#FF6672',
|
||||
primary6: '#FF4757',
|
||||
primary7: '#DB3445',
|
||||
primary8: '#B7293A',
|
||||
primary9: '#931E2E',
|
||||
primary10: '#6F1421',
|
||||
},
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { DialogOptions, DialogResult } from '@wot-ui/ui/components/wd-dialog/types'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type GlobalDialogOptions = DialogOptions & {
|
||||
success?: (res: DialogResult) => void
|
||||
fail?: (res: DialogResult) => void
|
||||
}
|
||||
|
||||
interface GlobalDialog {
|
||||
dialogOptions: GlobalDialogOptions | null
|
||||
currentPage: string
|
||||
}
|
||||
|
||||
type DialogType = NonNullable<DialogOptions['type']>
|
||||
|
||||
function isButtonPropsObject(value: unknown): value is Record<string, any> {
|
||||
return value !== null && CommonUtil.isObj(value)
|
||||
}
|
||||
|
||||
function normalizeButtonProps(props: unknown, text?: string) {
|
||||
if (props === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (isButtonPropsObject(props)) {
|
||||
return {
|
||||
...props,
|
||||
...(text ? { text } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
if (CommonUtil.isString(props) || text) {
|
||||
return {
|
||||
text: text || props,
|
||||
}
|
||||
}
|
||||
|
||||
if (props === undefined) {
|
||||
return {}
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
|
||||
function withDefaultTypeOptions(option: GlobalDialogOptions, type?: DialogType): GlobalDialogOptions {
|
||||
const next: GlobalDialogOptions = {
|
||||
...option,
|
||||
...(type ? { type } : {}),
|
||||
}
|
||||
|
||||
if (next.showCancelButton === undefined) {
|
||||
if (next.type === 'alert') {
|
||||
next.showCancelButton = false
|
||||
}
|
||||
else if (next.type === 'confirm' || next.type === 'prompt') {
|
||||
next.showCancelButton = true
|
||||
}
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
function normalizeDialogOptions(option: GlobalDialogOptions, type?: DialogType): GlobalDialogOptions {
|
||||
const next = withDefaultTypeOptions(option, type)
|
||||
|
||||
next.confirmButtonProps = normalizeButtonProps(next.confirmButtonProps, next.confirmButtonText) as DialogOptions['confirmButtonProps']
|
||||
|
||||
if (next.showCancelButton === false) {
|
||||
next.cancelButtonProps = null
|
||||
}
|
||||
else if (next.showCancelButton === true || next.cancelButtonProps !== undefined || next.cancelButtonText) {
|
||||
next.cancelButtonProps = normalizeButtonProps(next.cancelButtonProps, next.cancelButtonText) as DialogOptions['cancelButtonProps']
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
function normalizeOption(option: GlobalDialogOptions | string, type?: DialogType): GlobalDialogOptions {
|
||||
return normalizeDialogOptions(CommonUtil.isString(option) ? { title: option } : option, type)
|
||||
}
|
||||
|
||||
export const useGlobalDialog = defineStore('global-Dialog', {
|
||||
state: (): GlobalDialog => ({
|
||||
dialogOptions: null,
|
||||
currentPage: '',
|
||||
}),
|
||||
actions: {
|
||||
show(option: GlobalDialogOptions | string, type?: DialogType) {
|
||||
this.currentPage = getCurrentPath()
|
||||
this.dialogOptions = normalizeOption(option, type)
|
||||
},
|
||||
alert(option: GlobalDialogOptions | string) {
|
||||
this.show(option, 'alert')
|
||||
},
|
||||
confirm(option: GlobalDialogOptions | string) {
|
||||
this.show(option, 'confirm')
|
||||
},
|
||||
prompt(option: GlobalDialogOptions | string) {
|
||||
this.show(option, 'prompt')
|
||||
},
|
||||
close() {
|
||||
this.dialogOptions = null
|
||||
this.currentPage = ''
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* @Author: weisheng
|
||||
* @Date: 2025-09-25 20:32:22
|
||||
* @LastEditTime: 2026-04-07 14:06:20
|
||||
* @LastEditors: weisheng
|
||||
* @Description:
|
||||
* @FilePath: /wot-starter/src/composables/useGlobalLoading.ts
|
||||
* 记得注释
|
||||
*/
|
||||
import type { ToastOptions } from '@wot-ui/ui/components/wd-toast/types'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface GlobalLoading {
|
||||
loadingOptions: ToastOptions
|
||||
currentPage: string
|
||||
}
|
||||
|
||||
const defaultOptions: ToastOptions = {
|
||||
show: false,
|
||||
}
|
||||
export const useGlobalLoading = defineStore('global-loading', {
|
||||
state: (): GlobalLoading => ({
|
||||
loadingOptions: defaultOptions,
|
||||
currentPage: '',
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// 加载提示
|
||||
loading(option: ToastOptions | string) {
|
||||
this.currentPage = getCurrentPath()
|
||||
this.loadingOptions = CommonUtil.deepMerge({
|
||||
iconName: 'loading',
|
||||
duration: 0,
|
||||
cover: true,
|
||||
position: 'middle',
|
||||
show: true,
|
||||
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions
|
||||
},
|
||||
// 关闭Toast
|
||||
close() {
|
||||
this.loadingOptions = defaultOptions
|
||||
this.currentPage = ''
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { ToastOptions } from '@wot-ui/ui/components/wd-toast/types'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
interface GlobalToast {
|
||||
toastOptions: ToastOptions
|
||||
currentPage: string
|
||||
}
|
||||
|
||||
const defaultOptions: ToastOptions = {
|
||||
duration: 2000,
|
||||
show: false,
|
||||
}
|
||||
export const useGlobalToast = defineStore('global-toast', {
|
||||
state: (): GlobalToast => ({
|
||||
toastOptions: defaultOptions,
|
||||
currentPage: '',
|
||||
}),
|
||||
getters: {},
|
||||
actions: {
|
||||
// 打开Toast
|
||||
show(option: ToastOptions | string) {
|
||||
this.currentPage = getCurrentPath()
|
||||
const options = CommonUtil.deepMerge(defaultOptions, typeof option === 'string' ? { msg: option } : option) as ToastOptions
|
||||
this.toastOptions = CommonUtil.deepMerge(options, {
|
||||
show: true,
|
||||
position: options.position || 'middle',
|
||||
}) as ToastOptions
|
||||
},
|
||||
// 成功提示
|
||||
success(option: ToastOptions | string) {
|
||||
this.show(CommonUtil.deepMerge({
|
||||
iconName: 'success',
|
||||
duration: 1500,
|
||||
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
|
||||
},
|
||||
// 关闭提示
|
||||
error(option: ToastOptions | string) {
|
||||
this.show(CommonUtil.deepMerge({
|
||||
iconName: 'error',
|
||||
direction: 'vertical',
|
||||
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
|
||||
},
|
||||
// 常规提示
|
||||
info(option: ToastOptions | string) {
|
||||
this.show(CommonUtil.deepMerge({
|
||||
iconName: 'info',
|
||||
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
|
||||
},
|
||||
// 警告提示
|
||||
warning(option: ToastOptions | string) {
|
||||
this.show(CommonUtil.deepMerge({
|
||||
iconName: 'warning',
|
||||
}, typeof option === 'string' ? { msg: option } : option) as ToastOptions)
|
||||
},
|
||||
// 关闭Toast
|
||||
close() {
|
||||
this.toastOptions = defaultOptions
|
||||
this.currentPage = ''
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { ThemeColorOption, ThemeMode } from '@/composables/types/theme'
|
||||
import { themeColorOptions } from '@/composables/types/theme'
|
||||
import { initializeThemeOnce, subscribeSystemThemeChange } from '@/utils/systemTheme'
|
||||
|
||||
/**
|
||||
* 完整版主题管理组合式API
|
||||
*
|
||||
* 功能特性:
|
||||
* - 支持手动切换暗黑模式
|
||||
* - 支持主题色选择
|
||||
* - 支持跟随系统主题
|
||||
* - 自动同步导航栏颜色
|
||||
* - 持久化用户设置
|
||||
*
|
||||
* 适用场景:
|
||||
* - 需要用户手动控制主题的应用
|
||||
* - 需要主题色自定义的应用
|
||||
* - 需要完整主题管理功能的复杂应用
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useManualTheme } from '@/composables/useManualTheme'
|
||||
*
|
||||
* const {
|
||||
* theme,
|
||||
* isDark,
|
||||
* toggleTheme,
|
||||
* openThemeColorPicker,
|
||||
* currentThemeColor,
|
||||
* themeVars
|
||||
* } = useManualTheme()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <wd-config-provider :theme="theme" :theme-vars="themeVars">
|
||||
* <view :class="{ 'dark-mode': isDark }">
|
||||
* <wd-button @click="toggleTheme">切换主题</wd-button>
|
||||
* <wd-button @click="openThemeColorPicker">选择主题色</wd-button>
|
||||
* </view>
|
||||
* </wd-config-provider>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useManualTheme() {
|
||||
const store = useManualThemeStore()
|
||||
const showThemeColorSheet = ref(false)
|
||||
let stopThemeChangeListener: (() => void) | undefined
|
||||
|
||||
/**
|
||||
* 切换暗黑模式
|
||||
* @param mode 指定主题模式,不传则自动切换
|
||||
* @param isFollowSystem 是否跟随系统
|
||||
*/
|
||||
function toggleTheme(mode?: ThemeMode, isFollowSystem: boolean = false) {
|
||||
store.toggleTheme(mode, isFollowSystem)
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开主题色选择器
|
||||
*/
|
||||
function openThemeColorPicker() {
|
||||
showThemeColorSheet.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭主题色选择器
|
||||
*/
|
||||
function closeThemeColorPicker() {
|
||||
showThemeColorSheet.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择主题色
|
||||
* @param option 主题色选项
|
||||
*/
|
||||
function selectThemeColor(option: ThemeColorOption) {
|
||||
store.setCurrentThemeColor(option)
|
||||
closeThemeColorPicker()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化主题
|
||||
*/
|
||||
function initTheme() {
|
||||
store.initTheme()
|
||||
}
|
||||
|
||||
// 组件挂载前初始化主题
|
||||
onBeforeMount(() => {
|
||||
initializeThemeOnce(store, initTheme)
|
||||
stopThemeChangeListener = subscribeSystemThemeChange(store, (res) => {
|
||||
if (store.followSystem) {
|
||||
store.toggleTheme(res.theme, true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 页面显示时更新导航栏颜色,确保每次切换页面时导航栏颜色都是正确的
|
||||
onShow(() => {
|
||||
store.setNavigationBarColor()
|
||||
})
|
||||
|
||||
// 组件卸载时清理监听
|
||||
onUnmounted(() => {
|
||||
stopThemeChangeListener?.()
|
||||
stopThemeChangeListener = undefined
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态
|
||||
theme: computed(() => store.theme),
|
||||
isDark: computed(() => store.isDark),
|
||||
followSystem: computed(() => store.followSystem),
|
||||
hasUserSet: computed(() => store.hasUserSet),
|
||||
currentThemeColor: computed(() => store.currentThemeColor),
|
||||
themeVars: computed(() => store.themeVars),
|
||||
showThemeColorSheet,
|
||||
|
||||
// 常量
|
||||
themeColorOptions,
|
||||
|
||||
// 方法
|
||||
initTheme,
|
||||
toggleTheme,
|
||||
setFollowSystem: store.setFollowSystem,
|
||||
openThemeColorPicker,
|
||||
closeThemeColorPicker,
|
||||
selectThemeColor,
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类型和常量供外部使用
|
||||
export type { ThemeColorOption, ThemeMode }
|
||||
export { themeColorOptions }
|
||||
@@ -0,0 +1,53 @@
|
||||
export interface TabbarItem {
|
||||
name: string
|
||||
value?: number
|
||||
active: boolean
|
||||
title: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const tabbarItems = ref<TabbarItem[]>([
|
||||
{ name: 'home', active: true, title: '首页', icon: 'home' },
|
||||
{ name: 'work', active: false, title: '工作台', icon: 'apps' },
|
||||
{ name: 'mine', active: false, title: '我的', icon: 'user' },
|
||||
])
|
||||
|
||||
export function useTabbar() {
|
||||
const tabbarList = computed(() => tabbarItems.value)
|
||||
|
||||
const activeTabbar = computed(() => {
|
||||
const item = tabbarItems.value.find(item => item.active)
|
||||
return item || tabbarItems.value[0]
|
||||
})
|
||||
|
||||
const getTabbarItemValue = (name: string) => {
|
||||
const item = tabbarItems.value.find(item => item.name === name)
|
||||
return item?.value
|
||||
}
|
||||
|
||||
const setTabbarItem = (name: string, value: number) => {
|
||||
const tabbarItem = tabbarItems.value.find(item => item.name === name)
|
||||
if (tabbarItem) {
|
||||
tabbarItem.value = value
|
||||
}
|
||||
}
|
||||
|
||||
const setTabbarItemActive = (name: string) => {
|
||||
tabbarItems.value.forEach((item) => {
|
||||
if (item.name === name) {
|
||||
item.active = true
|
||||
}
|
||||
else {
|
||||
item.active = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
tabbarList,
|
||||
activeTabbar,
|
||||
getTabbarItemValue,
|
||||
setTabbarItem,
|
||||
setTabbarItemActive,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
export interface TeamMember {
|
||||
avatar: string
|
||||
name: string
|
||||
title: string
|
||||
tags?: string[]
|
||||
desc?: string
|
||||
github?: string
|
||||
twitter?: string
|
||||
}
|
||||
|
||||
type TeamMemberPayload = TeamMember & {
|
||||
status?: string
|
||||
}
|
||||
|
||||
const urls = [
|
||||
'https://sponsor.wot-ui.cn',
|
||||
'https://wot-sponsors.pages.dev',
|
||||
]
|
||||
|
||||
const mockMembers: TeamMember[] = [
|
||||
|
||||
]
|
||||
|
||||
const data = ref<TeamMember[]>([])
|
||||
|
||||
function normalizeMembers(members: TeamMemberPayload[]): TeamMember[] {
|
||||
return members
|
||||
.filter(member => member?.avatar && member?.name && member?.title)
|
||||
.map((member) => {
|
||||
const tags = Array.isArray(member.tags)
|
||||
? member.tags
|
||||
: (member.status ? [member.status] : [])
|
||||
|
||||
return {
|
||||
avatar: member.avatar,
|
||||
name: member.name,
|
||||
title: member.title,
|
||||
tags,
|
||||
desc: member.desc,
|
||||
github: member.github,
|
||||
twitter: member.twitter,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function requestMembers(url: string): Promise<TeamMemberPayload[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: `${url}/team.json?t=${Date.now()}`,
|
||||
method: 'GET',
|
||||
timeout: 5000,
|
||||
success: (response) => {
|
||||
const payload = response.data as { members?: TeamMemberPayload[] } | undefined
|
||||
resolve(Array.isArray(payload?.members) ? payload.members : [])
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchMembers() {
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const members = await requestMembers(url)
|
||||
const normalizedMembers = normalizeMembers(members)
|
||||
|
||||
if (normalizedMembers.length > 0) {
|
||||
return normalizedMembers
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.warn(`Failed to fetch team from ${url}`)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeMembers(mockMembers)
|
||||
}
|
||||
|
||||
export function useTeam() {
|
||||
onMounted(async () => {
|
||||
if (data.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
data.value = await fetchMembers()
|
||||
})
|
||||
|
||||
return { data }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { initializeThemeOnce, subscribeSystemThemeChange } from '@/utils/systemTheme'
|
||||
|
||||
/**
|
||||
* 简化版系统主题管理组合式API
|
||||
*
|
||||
* 功能特性:
|
||||
* - 仅跟随系统主题变化
|
||||
* - 自动响应系统主题切换
|
||||
* - 导航栏颜色通过 theme.json 自动处理
|
||||
* - 轻量级,无额外功能
|
||||
*
|
||||
* 适用场景:
|
||||
* - 只需要系统主题适应的简单应用
|
||||
* - 不需要用户手动控制主题的应用
|
||||
* - 追求轻量级主题管理的应用
|
||||
*
|
||||
* 注意事项:
|
||||
* - 不支持手动切换主题
|
||||
* - 不支持主题色自定义
|
||||
* - 导航栏颜色依赖 theme.json 配置
|
||||
*
|
||||
* @example
|
||||
* ```vue
|
||||
* <script setup>
|
||||
* import { useTheme } from '@/composables/useTheme'
|
||||
*
|
||||
* const { theme, isDark, themeVars } = useTheme()
|
||||
* </script>
|
||||
*
|
||||
* <template>
|
||||
* <wd-config-provider :theme="theme" :theme-vars="themeVars">
|
||||
* <view :class="{ 'dark-mode': isDark }">
|
||||
* <text>当前主题: {{ theme }}</text>
|
||||
* </view>
|
||||
* </wd-config-provider>
|
||||
* </template>
|
||||
* ```
|
||||
*/
|
||||
export function useTheme() {
|
||||
const store = useThemeStore()
|
||||
let stopThemeChangeListener: (() => void) | undefined
|
||||
|
||||
// 组件挂载前初始化系统主题
|
||||
onBeforeMount(() => {
|
||||
initializeThemeOnce(store, () => store.initSystemTheme())
|
||||
stopThemeChangeListener = subscribeSystemThemeChange(store, (res) => {
|
||||
// 系统主题变化时自动更新,导航栏颜色由 theme.json 自动处理
|
||||
store.setTheme(res.theme)
|
||||
console.log('系统主题已切换至:', res.theme)
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时清理监听
|
||||
onUnmounted(() => {
|
||||
stopThemeChangeListener?.()
|
||||
stopThemeChangeListener = undefined
|
||||
})
|
||||
|
||||
return {
|
||||
// 状态(只读)
|
||||
theme: computed(() => store.theme),
|
||||
isDark: computed(() => store.isDark),
|
||||
themeVars: computed(() => store.themeVars),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user