chore: 初始化uniapp前端项目基础框架

新增项目配置、静态资源、工具类、状态管理、API接口、组件库集成等全套基础代码,完成项目初始化搭建
This commit is contained in:
zhangtao
2026-08-02 03:49:27 +08:00
parent 7f8320d3ed
commit 3c9b77f994
278 changed files with 55387 additions and 2 deletions
+168
View File
@@ -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 }
+53
View File
@@ -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,
}
}
+91
View File
@@ -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 }
}
+65
View File
@@ -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),
}
}