feat: 初始化前端项目基础结构

- 添加项目配置文件(tsconfig, eslint, prettier等)
- 实现基础路由和页面布局
- 添加全局状态管理和API请求封装
- 集成UI组件库和主题系统
- 添加文档网站和示例页面
- 配置CI/CD和工作流
This commit is contained in:
zhangtao
2026-05-01 00:47:48 +08:00
parent ff738007cd
commit 5c29d22aa5
256 changed files with 45257 additions and 2 deletions
Submodule frontend/app deleted from 87d9f70f4f
+31
View File
@@ -0,0 +1,31 @@
---
description: 项目基本信息、工作流和常用命令
globs: package.json, src/**/*
alwaysApply: true
---
# 项目信息与工作流
## 🚀 快速开始
- **开发**: `pnpm dev` (H5), `pnpm dev:mp-weixin` (微信小程序)。
- **构建**: `pnpm build:h5`, `pnpm build:mp-weixin`。
- **Lint**: `pnpm lint:fix`。
- **预览**: `dist/` 目录。
## 📂 目录结构概览
- `src/pages`: 应用视图 (基于文件的路由)。
- `src/components`: 可复用的 UI 组件。
- `src/store`: Pinia 状态管理模块。
- `src/api`: Alova.js API 定义。
- `src/uni_modules`: Uni-app 模块 (包含 wot-design-uni)。
## 🤝 Git 工作流
- **提交**: 符合 Conventional Commits 规范 (`feat:`, `fix:`, `chore:`, `refactor:`)。
- **主要工具**: `commitizen` (通过 `pnpm commit` 使用)。
## 🛠️ 代码生成
- **API**: 使用 `pnpm alova-gen` 重新生成 API 定义。
- **Skills**: 使用 `.agent/skills` 中的 skill 进行脚手架生成:
- `pinia-store-generator`
- `uni-page-generator`
- `alova-api-module`
+35
View File
@@ -0,0 +1,35 @@
---
description: 核心技术栈、架构和代码规范
globs: src/store/**/*, src/api/**/*, src/router/**/*, src/pages/**/*, pages.config.ts, alova.config.ts
alwaysApply: false
---
# 技术栈与架构
## 🏗️ 架构概览
本项目采用分层架构:
- **表现层**: `src/pages` (视图) + `src/components` (UI 逻辑)
- **状态层**: `src/store` (Pinia)
- **数据层**: `src/api` (Alova.js)
## 📦 状态管理 (Pinia)
- **库**: Pinia
- **持久化**: 使用 `src/store/persist.ts` 进行本地存储。
- **模式**: 详见 `pinia-store-generator` skill。
- **规则**: 始终使用 `defineStore` 并遵循 `use{Name}Store` 的命名规范。
- **Skill**: 使用 **`pinia-store-generator`** 快速创建新的 store。
## 🌐 API 层 (Alova.js)
- **库**: Alova.js
- **结构**:
- `src/api/core`: 拦截器和实例配置。
- `src/api/apiDefinitions.ts`: 自动生成的 API 定义。
- **Mock**: 支持在 `src/api/mock` 中编写 Mock 数据。
- **Skill**: 使用 **`alova-api-module`** 创建新的 API 模块和 Mock 数据。
## 🛣️ 路由
- **库**: `@wot-ui/router` (API) + `vite-plugin-uni-pages` (文件系统路由)
- **配置**: `pages.config.ts` 控制 `pages.json` 的生成。
- **导航**: 使用 `useRouter()` 进行 push/replace/back 操作。
- **Skill**: 参考 **`wot-router-usage`** 了解导航模式和守卫用法。
- **Skill**: 使用 **`uni-page-generator`** 创建带路由配置的新页面。
@@ -0,0 +1,33 @@
---
description: UI/UX 指南、样式规范和组件使用
globs: **/*.vue, **/*.scss, src/components/**/*, src/uni_modules/**/*, uno.config.ts
alwaysApply: false
---
# UI 与样式指南
## 🎨 样式系统
- **引擎**: UnoCSS (原子化 CSS) 是**首选**的样式方案。
- **配置**: `uno.config.ts`。
- **预处理**: SCSS 用于复杂的组件样式(极少需要)。
- **主题**: 通过 `src/theme.json` 和 CSS 变量支持亮色/暗色模式切换。
### UnoCSS 约定
- 使用工具类: `flex`, `items-center`, `text-primary`, `m-4`.
- 响应式前缀: `sm:`, `md:` (在移动端优先的 uni-app 中较少使用)。
- 图标: 通过 UnoCSS preset 使用 `i-carbon-{icon-name}`。
## 🧩 组件库
- **核心库**: `wot-design-uni` (`wd-` 前缀)。
- **文档**: [wot-design-uni](https://wot-ui.cn).
- **自定义组件**: 在 `src/components` 中创建。
## 📢 全局反馈
- **Toast/Message**: 请勿直接使用 `uni.showToast`。
- **标准**: 使用 `GlobalToast`, `GlobalMessage`, `GlobalLoading` 组件。
- **Skill**: 参考 **`global-feedback`** skill 查看使用示例。
## 📱 布局
- **系统**: `vite-plugin-uni-layouts`。
- **默认**: `src/layouts/default.vue`。
- **TabBar**: `src/layouts/tabbar.vue`。
@@ -0,0 +1,162 @@
---
name: alova-api-module
description: 快速创建 Alova 请求模块和 Mock 数据
---
# Alova API 模块生成器
基于 Alova 请求库创建 API 模块和 Mock 数据。
## 目录结构
```
src/api/
├── core/
│ └── instance.ts # Alova 实例配置
├── mock/
│ ├── modules/ # Mock 模块目录
│ │ ├── user.ts # 用户相关 Mock
│ │ └── product.ts # 商品相关 Mock
│ ├── mockAdapter.ts # Mock 适配器
│ └── utils/ # Mock 工具函数
├── apiDefinitions.ts # API 定义(自动生成)
├── createApis.ts # API 创建函数
└── index.ts # API 导出入口
```
## 创建 Mock 模块
### 模块模板
```typescript
// src/api/mock/modules/{moduleName}.ts
import { defineMock } from '@alova/mock'
export default defineMock({
// GET 请求
'/api/{moduleName}/list': () => {
return {
code: 200,
message: 'success',
data: {
list: [],
total: 0,
},
}
},
// POST 请求
'[POST]/api/{moduleName}/create': ({ data }) => {
return {
code: 200,
message: '创建成功',
data: { id: Date.now(), ...data },
}
},
// 带路径参数
'/api/{moduleName}/{id}': ({ params }) => {
return {
code: 200,
message: 'success',
data: { id: params.id },
}
},
})
```
### 注册 Mock 模块
```typescript
// src/api/mock/mockAdapter.ts
import userMock from './modules/user'
import productMock from './modules/product'
// 导入新模块
import newModuleMock from './modules/{moduleName}'
export default createAlovaMockAdapter([
userMock,
productMock,
newModuleMock, // 注册新模块
], {
enable: import.meta.env.DEV,
delay: 300,
})
```
## 使用 API
### useRequest Hook
```typescript
// 基础请求
const { data, loading, error, send } = useRequest(
Apis.user.getUserInfo({ id: '123' }),
{ immediate: true }
)
// 手动触发
const { send: fetchData } = useRequest(
(id: string) => Apis.user.getUserInfo({ id }),
{ immediate: false }
)
await fetchData('123')
```
### usePagination Hook
```typescript
const {
data: list,
page,
pageSize,
total,
loading,
isLastPage,
refresh,
reload,
} = usePagination(
(page, pageSize) => Apis.product.getList({ page, pageSize }),
{
initialPage: 1,
initialPageSize: 10,
total: (res) => res.total,
data: (res) => res.list,
}
)
```
## 响应数据类型
```typescript
// src/api/types/{moduleName}.ts
export interface ApiResponse<T> {
code: number
message: string
data: T
}
export interface PaginatedData<T> {
list: T[]
total: number
page: number
pageSize: number
}
export interface UserInfo {
id: string
name: string
avatar: string
}
```
## 配置文件
```typescript
// alova.config.ts
export default {
input: 'https://api.example.com/openapi.json',
output: 'src/api',
// OpenAPI 自动生成配置
}
```
@@ -0,0 +1,177 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
@@ -0,0 +1,42 @@
---
name: frontend-design
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
license: Complete terms in LICENSE.txt
---
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
## Design Thinking
Before coding, understand the context and commit to a BOLD aesthetic direction:
- **Purpose**: What problem does this interface solve? Who uses it?
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
- **Constraints**: Technical requirements (framework, performance, accessibility).
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
- Production-grade and functional
- Visually striking and memorable
- Cohesive with a clear aesthetic point-of-view
- Meticulously refined in every detail
## Frontend Aesthetics Guidelines
Focus on:
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
@@ -0,0 +1,172 @@
---
name: global-feedback
description: 全局反馈组件(Toast/Message/Loading)使用指南
---
# 全局反馈组件
wot-starter 提供了全局 Toast、Message、Loading 组件,支持跨页面调用。
## 组件列表
| 组件 | 用途 | Composable |
|------|------|------------|
| GlobalToast | 轻提示 | `useGlobalToast()` |
| GlobalMessage | 确认弹窗 | `useGlobalMessage()` |
| GlobalLoading | 加载状态 | `useGlobalLoading()` |
## useGlobalToast
### 基础用法
```typescript
const { show, success, error, info, warning, close } = useGlobalToast()
// 显示提示
show('这是一条提示')
// 成功提示
success('操作成功')
// 错误提示
error('操作失败')
// 信息提示
info('提示信息')
// 警告提示
warning('警告信息')
```
### 自定义配置
```typescript
show({
msg: '自定义提示',
duration: 3000, // 持续时间
iconName: 'success', // 图标
position: 'middle', // 位置:'top' | 'middle' | 'bottom'
direction: 'vertical', // 方向:'horizontal' | 'vertical'
})
```
## useGlobalMessage
### 确认弹窗
```typescript
const { confirm } = useGlobalMessage()
confirm({
title: '提示',
msg: '确定要删除吗?',
confirmButtonText: '确定',
cancelButtonText: '取消',
success() {
console.log('用户点击确定')
},
fail() {
console.log('用户点击取消')
},
})
```
### 与 Promise 结合
```typescript
const handleDelete = async () => {
return new Promise((resolve, reject) => {
confirm({
title: '确认删除',
msg: '删除后无法恢复',
success: resolve,
fail: reject,
})
})
}
try {
await handleDelete()
// 用户确认,执行删除
} catch {
// 用户取消
}
```
## useGlobalLoading
### 基础用法
```typescript
const { show, hide } = useGlobalLoading()
// 显示加载
show()
show('加载中...')
// 隐藏加载
hide()
```
### 包装异步操作
```typescript
const fetchData = async () => {
const { show, hide } = useGlobalLoading()
show('数据加载中...')
try {
const data = await api.getData()
return data
} finally {
hide()
}
}
```
## 在路由守卫中使用
```typescript
// src/router/index.ts
router.beforeEach((to, from, next) => {
if (to.name === 'protected-page') {
const { confirm } = useGlobalMessage()
return new Promise((resolve, reject) => {
confirm({
title: '需要登录',
msg: '是否前往登录?',
success() {
next({ name: 'login' })
resolve()
},
fail() {
next(false)
reject()
},
})
})
}
next()
})
```
## 组件配置
全局组件已在 `App.ku.vue` 中配置:
```vue
<!-- App.ku.vue -->
<template>
<GlobalToast />
<GlobalMessage />
<GlobalLoading />
</template>
```
## 注意事项
- 这些 composable 基于 Pinia Store 实现
- 支持跨页面调用
- 自动导入,无需手动 import
- 同时只会显示一个实例
@@ -0,0 +1,183 @@
---
name: pinia-store-generator
description: 创建符合项目规范的 Pinia Store
---
# Pinia Store 生成器
快速创建符合 wot-starter 项目规范的 Pinia Store。
## 目录结构
```
src/store/
├── persist.ts # 持久化配置
├── themeStore.ts # 主题 store 示例
├── manualThemeStore.ts # 手动主题 store 示例
└── {moduleName}Store.ts # 新建的 store
```
## Store 模板
### Options Store(推荐)
```typescript
// src/store/{moduleName}Store.ts
import type { {ModuleName}State } from '@/types/{moduleName}'
import { defineStore } from 'pinia'
/**
* {moduleName} 状态管理
* 描述这个 store 的用途
*/
export const use{ModuleName}Store = defineStore('{moduleName}', {
state: (): {ModuleName}State => ({
// 状态定义
data: null,
loading: false,
error: null,
}),
getters: {
// 计算属性
isLoaded: (state) => state.data !== null,
hasError: (state) => state.error !== null,
},
actions: {
// 同步 action
setData(data: any) {
this.data = data
},
// 异步 action
async fetchData() {
this.loading = true
this.error = null
try {
const { data } = await useRequest(Apis.xxx.getData())
this.data = data.value
} catch (err) {
this.error = err as Error
} finally {
this.loading = false
}
},
// 重置状态
reset() {
this.$reset()
},
},
})
```
### 带持久化的 Store
```typescript
// src/store/{moduleName}Store.ts
import { defineStore } from 'pinia'
import { createPersistedState } from '@/store/persist'
export const use{ModuleName}Store = defineStore('{moduleName}', {
state: () => ({
token: '',
userInfo: null,
}),
actions: {
setToken(token: string) {
this.token = token
},
},
// 启用持久化
persist: createPersistedState({
key: '{moduleName}-storage',
paths: ['token', 'userInfo'], // 只持久化指定字段
}),
})
```
## 类型定义
```typescript
// src/types/{moduleName}.ts
export interface {ModuleName}State {
data: {DataType} | null
loading: boolean
error: Error | null
}
export interface {DataType} {
id: string
name: string
// ...其他字段
}
```
## 使用 Store
### 在组件中使用
```vue
<script setup lang="ts">
const {moduleName}Store = use{ModuleName}Store()
// 访问状态
const data = computed(() => {moduleName}Store.data)
const loading = computed(() => {moduleName}Store.loading)
// 调用 action
onMounted(() => {
{moduleName}Store.fetchData()
})
</script>
```
### 在 composable 中使用
```typescript
// src/composables/use{ModuleName}.ts
export function use{ModuleName}() {
const store = use{ModuleName}Store()
// 封装逻辑
const doSomething = async () => {
await store.fetchData()
}
return {
data: computed(() => store.data),
loading: computed(() => store.loading),
doSomething,
}
}
```
## 持久化配置
```typescript
// src/store/persist.ts
export function createPersistedState(options: {
key: string
paths?: string[]
}) {
return {
key: options.key,
paths: options.paths,
storage: {
getItem: (key: string) => uni.getStorageSync(key),
setItem: (key: string, value: string) => uni.setStorageSync(key, value),
removeItem: (key: string) => uni.removeStorageSync(key),
},
}
}
```
## 注意事项
- Store 文件名使用 `{moduleName}Store.ts` 格式
- 导出的函数使用 `use{ModuleName}Store` 命名
- 复杂逻辑建议封装到 composable 中
- 敏感数据不要持久化到本地存储
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,356 @@
---
name: skill-creator
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
license: Complete terms in LICENSE.txt
---
# Skill Creator
This skill provides guidance for creating effective skills.
## About Skills
Skills are modular, self-contained packages that extend Claude's capabilities by providing
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
domains or tasks—they transform Claude from a general-purpose agent into a specialized agent
equipped with procedural knowledge that no model can fully possess.
### What Skills Provide
1. Specialized workflows - Multi-step procedures for specific domains
2. Tool integrations - Instructions for working with specific file formats or APIs
3. Domain expertise - Company-specific knowledge, schemas, business logic
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
## Core Principles
### Concise is Key
The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?"
Prefer concise examples over verbose explanations.
### Set Appropriate Degrees of Freedom
Match the level of specificity to the task's fragility and variability:
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
### Anatomy of a Skill
Every skill consists of a required SKILL.md file and optional bundled resources:
```
skill-name/
├── SKILL.md (required)
│ ├── YAML frontmatter metadata (required)
│ │ ├── name: (required)
│ │ └── description: (required)
│ └── Markdown instructions (required)
└── Bundled Resources (optional)
├── scripts/ - Executable code (Python/Bash/etc.)
├── references/ - Documentation intended to be loaded into context as needed
└── assets/ - Files used in output (templates, icons, fonts, etc.)
```
#### SKILL.md (required)
Every SKILL.md consists of:
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
#### Bundled Resources (optional)
##### Scripts (`scripts/`)
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments
##### References (`references/`)
Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking.
- **When to include**: For documentation that Claude should reference while working
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
##### Assets (`assets/`)
Files not intended to be loaded into context, but rather used within the output Claude produces.
- **When to include**: When the skill needs files that will be used in the final output
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context
#### What to Not Include in a Skill
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
- README.md
- INSTALLATION_GUIDE.md
- QUICK_REFERENCE.md
- CHANGELOG.md
- etc.
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
### Progressive Disclosure Design Principle
Skills use a three-level loading system to manage context efficiently:
1. **Metadata (name + description)** - Always in context (~100 words)
2. **SKILL.md body** - When skill triggers (<5k words)
3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window)
#### Progressive Disclosure Patterns
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
**Pattern 1: High-level guide with references**
```markdown
# PDF Processing
## Quick start
Extract text with pdfplumber:
[code example]
## Advanced features
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
```
Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
**Pattern 2: Domain-specific organization**
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
```
bigquery-skill/
├── SKILL.md (overview and navigation)
└── reference/
├── finance.md (revenue, billing metrics)
├── sales.md (opportunities, pipeline)
├── product.md (API usage, features)
└── marketing.md (campaigns, attribution)
```
When a user asks about sales metrics, Claude only reads sales.md.
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
```
cloud-deploy/
├── SKILL.md (workflow + provider selection)
└── references/
├── aws.md (AWS deployment patterns)
├── gcp.md (GCP deployment patterns)
└── azure.md (Azure deployment patterns)
```
When the user chooses AWS, Claude only reads aws.md.
**Pattern 3: Conditional details**
Show basic content, link to advanced content:
```markdown
# DOCX Processing
## Creating documents
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
## Editing documents
For simple edits, modify the XML directly.
**For tracked changes**: See [REDLINING.md](REDLINING.md)
**For OOXML details**: See [OOXML.md](OOXML.md)
```
Claude reads REDLINING.md or OOXML.md only when the user needs those features.
**Important guidelines:**
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing.
## Skill Creation Process
Skill creation involves these steps:
1. Understand the skill with concrete examples
2. Plan reusable skill contents (scripts, references, assets)
3. Initialize the skill (run init_skill.py)
4. Edit the skill (implement resources and write SKILL.md)
5. Package the skill (run package_skill.py)
6. Iterate based on real usage
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
### Step 1: Understanding the Skill with Concrete Examples
Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
For example, when building an image-editor skill, relevant questions include:
- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
- "Can you give some examples of how this skill would be used?"
- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
- "What would a user say that should trigger this skill?"
To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
Conclude this step when there is a clear sense of the functionality the skill should support.
### Step 2: Planning the Reusable Skill Contents
To turn concrete examples into an effective skill, analyze each example by:
1. Considering how to execute on the example from scratch
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
1. Rotating a PDF requires re-writing the same code each time
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
### Step 3: Initializing the Skill
At this point, it is time to actually create the skill.
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
Usage:
```bash
scripts/init_skill.py <skill-name> --path <output-directory>
```
The script:
- Creates the skill directory at the specified path
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
- Creates example resource directories: `scripts/`, `references/`, and `assets/`
- Adds example files in each directory that can be customized or deleted
After initialization, customize or remove the generated SKILL.md and example files as needed.
### Step 4: Edit the Skill
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively.
#### Learn Proven Design Patterns
Consult these helpful guides based on your skill's needs:
- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
These files contain established best practices for effective skill design.
#### Start with Reusable Skill Contents
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
#### Update SKILL.md
**Writing Guidelines:** Always use imperative/infinitive form.
##### Frontmatter
Write the YAML frontmatter with `name` and `description`:
- `name`: The skill name
- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill.
- Include both what the Skill does and specific triggers/contexts for when to use it.
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude.
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
Do not include any other fields in YAML frontmatter.
##### Body
Write instructions for using the skill and its bundled resources.
### Step 5: Packaging a Skill
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
```bash
scripts/package_skill.py <path/to/skill-folder>
```
Optional output directory specification:
```bash
scripts/package_skill.py <path/to/skill-folder> ./dist
```
The packaging script will:
1. **Validate** the skill automatically, checking:
- YAML frontmatter format and required fields
- Skill naming conventions and directory structure
- Description completeness and quality
- File organization and resource references
2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
### Step 6: Iterate
After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
**Iteration workflow:**
1. Use the skill on real tasks
2. Notice struggles or inefficiencies
3. Identify how SKILL.md or bundled resources should be updated
4. Implement changes and test again
@@ -0,0 +1,82 @@
# Output Patterns
Use these patterns when skills need to produce consistent, high-quality output.
## Template Pattern
Provide templates for output format. Match the level of strictness to your needs.
**For strict requirements (like API responses or data formats):**
```markdown
## Report structure
ALWAYS use this exact template structure:
# [Analysis Title]
## Executive summary
[One-paragraph overview of key findings]
## Key findings
- Finding 1 with supporting data
- Finding 2 with supporting data
- Finding 3 with supporting data
## Recommendations
1. Specific actionable recommendation
2. Specific actionable recommendation
```
**For flexible guidance (when adaptation is useful):**
```markdown
## Report structure
Here is a sensible default format, but use your best judgment:
# [Analysis Title]
## Executive summary
[Overview]
## Key findings
[Adapt sections based on what you discover]
## Recommendations
[Tailor to the specific context]
Adjust sections as needed for the specific analysis type.
```
## Examples Pattern
For skills where output quality depends on seeing examples, provide input/output pairs:
```markdown
## Commit message format
Generate commit messages following these examples:
**Example 1:**
Input: Added user authentication with JWT tokens
Output:
```
feat(auth): implement JWT-based authentication
Add login endpoint and token validation middleware
```
**Example 2:**
Input: Fixed bug where dates displayed incorrectly in reports
Output:
```
fix(reports): correct date formatting in timezone conversion
Use UTC timestamps consistently across report generation
```
Follow this style: type(scope): brief description, then detailed explanation.
```
Examples help Claude understand the desired style and level of detail more clearly than descriptions alone.
@@ -0,0 +1,28 @@
# Workflow Patterns
## Sequential Workflows
For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md:
```markdown
Filling a PDF form involves these steps:
1. Analyze the form (run analyze_form.py)
2. Create field mapping (edit fields.json)
3. Validate mapping (run validate_fields.py)
4. Fill the form (run fill_form.py)
5. Verify output (run verify_output.py)
```
## Conditional Workflows
For tasks with branching logic, guide Claude through decision points:
```markdown
1. Determine the modification type:
**Creating new content?** → Follow "Creation workflow" below
**Editing existing content?** → Follow "Editing workflow" below
2. Creation workflow: [steps]
3. Editing workflow: [steps]
```
@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""
Skill Initializer - Creates a new skill from template
Usage:
init_skill.py <skill-name> --path <path>
Examples:
init_skill.py my-new-skill --path skills/public
init_skill.py my-api-helper --path skills/private
init_skill.py custom-skill --path /custom/location
"""
import sys
from pathlib import Path
SKILL_TEMPLATE = """---
name: {skill_name}
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
---
# {skill_title}
## Overview
[TODO: 1-2 sentences explaining what this skill enables]
## Structuring This Skill
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
**1. Workflow-Based** (best for sequential processes)
- Works well when there are clear step-by-step procedures
- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing"
- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
**2. Task-Based** (best for tool collections)
- Works well when the skill offers different operations/capabilities
- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text"
- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
**3. Reference/Guidelines** (best for standards or specifications)
- Works well for brand guidelines, coding standards, or requirements
- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features"
- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
**4. Capabilities-Based** (best for integrated systems)
- Works well when the skill provides multiple interrelated features
- Example: Product Management with "Core Capabilities" → numbered capability list
- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
## [TODO: Replace with the first main section based on chosen structure]
[TODO: Add content here. See examples in existing skills:
- Code samples for technical skills
- Decision trees for complex workflows
- Concrete examples with realistic user requests
- References to scripts/templates/references as needed]
## Resources
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
### scripts/
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
**Examples from other skills:**
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments.
### references/
Documentation and reference material intended to be loaded into context to inform Claude's process and thinking.
**Examples from other skills:**
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
- BigQuery: API reference documentation and query examples
- Finance: Schema documentation, company policies
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working.
### assets/
Files not intended to be loaded into context, but rather used within the output Claude produces.
**Examples from other skills:**
- Brand styling: PowerPoint template files (.pptx), logo files
- Frontend builder: HTML/React boilerplate project directories
- Typography: Font files (.ttf, .woff2)
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
---
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
"""
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
"""
Example helper script for {skill_name}
This is a placeholder script that can be executed directly.
Replace with actual implementation or delete if not needed.
Example real scripts from other skills:
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
"""
def main():
print("This is an example script for {skill_name}")
# TODO: Add actual script logic here
# This could be data processing, file conversion, API calls, etc.
if __name__ == "__main__":
main()
'''
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
This is a placeholder for detailed reference documentation.
Replace with actual reference content or delete if not needed.
Example real reference docs from other skills:
- product-management/references/communication.md - Comprehensive guide for status updates
- product-management/references/context_building.md - Deep-dive on gathering context
- bigquery/references/ - API references and query examples
## When Reference Docs Are Useful
Reference docs are ideal for:
- Comprehensive API documentation
- Detailed workflow guides
- Complex multi-step processes
- Information too lengthy for main SKILL.md
- Content that's only needed for specific use cases
## Structure Suggestions
### API Reference Example
- Overview
- Authentication
- Endpoints with examples
- Error codes
- Rate limits
### Workflow Guide Example
- Prerequisites
- Step-by-step instructions
- Common patterns
- Troubleshooting
- Best practices
"""
EXAMPLE_ASSET = """# Example Asset File
This placeholder represents where asset files would be stored.
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
Asset files are NOT intended to be loaded into context, but rather used within
the output Claude produces.
Example asset files from other skills:
- Brand guidelines: logo.png, slides_template.pptx
- Frontend builder: hello-world/ directory with HTML/React boilerplate
- Typography: custom-font.ttf, font-family.woff2
- Data: sample_data.csv, test_dataset.json
## Common Asset Types
- Templates: .pptx, .docx, boilerplate directories
- Images: .png, .jpg, .svg, .gif
- Fonts: .ttf, .otf, .woff, .woff2
- Boilerplate code: Project directories, starter files
- Icons: .ico, .svg
- Data files: .csv, .json, .xml, .yaml
Note: This is a text placeholder. Actual assets can be any file type.
"""
def title_case_skill_name(skill_name):
"""Convert hyphenated skill name to Title Case for display."""
return ' '.join(word.capitalize() for word in skill_name.split('-'))
def init_skill(skill_name, path):
"""
Initialize a new skill directory with template SKILL.md.
Args:
skill_name: Name of the skill
path: Path where the skill directory should be created
Returns:
Path to created skill directory, or None if error
"""
# Determine skill directory path
skill_dir = Path(path).resolve() / skill_name
# Check if directory already exists
if skill_dir.exists():
print(f"❌ Error: Skill directory already exists: {skill_dir}")
return None
# Create skill directory
try:
skill_dir.mkdir(parents=True, exist_ok=False)
print(f"✅ Created skill directory: {skill_dir}")
except Exception as e:
print(f"❌ Error creating directory: {e}")
return None
# Create SKILL.md from template
skill_title = title_case_skill_name(skill_name)
skill_content = SKILL_TEMPLATE.format(
skill_name=skill_name,
skill_title=skill_title
)
skill_md_path = skill_dir / 'SKILL.md'
try:
skill_md_path.write_text(skill_content)
print("✅ Created SKILL.md")
except Exception as e:
print(f"❌ Error creating SKILL.md: {e}")
return None
# Create resource directories with example files
try:
# Create scripts/ directory with example script
scripts_dir = skill_dir / 'scripts'
scripts_dir.mkdir(exist_ok=True)
example_script = scripts_dir / 'example.py'
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
example_script.chmod(0o755)
print("✅ Created scripts/example.py")
# Create references/ directory with example reference doc
references_dir = skill_dir / 'references'
references_dir.mkdir(exist_ok=True)
example_reference = references_dir / 'api_reference.md'
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
print("✅ Created references/api_reference.md")
# Create assets/ directory with example asset placeholder
assets_dir = skill_dir / 'assets'
assets_dir.mkdir(exist_ok=True)
example_asset = assets_dir / 'example_asset.txt'
example_asset.write_text(EXAMPLE_ASSET)
print("✅ Created assets/example_asset.txt")
except Exception as e:
print(f"❌ Error creating resource directories: {e}")
return None
# Print next steps
print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}")
print("\nNext steps:")
print("1. Edit SKILL.md to complete the TODO items and update the description")
print("2. Customize or delete the example files in scripts/, references/, and assets/")
print("3. Run the validator when ready to check the skill structure")
return skill_dir
def main():
if len(sys.argv) < 4 or sys.argv[2] != '--path':
print("Usage: init_skill.py <skill-name> --path <path>")
print("\nSkill name requirements:")
print(" - Hyphen-case identifier (e.g., 'data-analyzer')")
print(" - Lowercase letters, digits, and hyphens only")
print(" - Max 40 characters")
print(" - Must match directory name exactly")
print("\nExamples:")
print(" init_skill.py my-new-skill --path skills/public")
print(" init_skill.py my-api-helper --path skills/private")
print(" init_skill.py custom-skill --path /custom/location")
sys.exit(1)
skill_name = sys.argv[1]
path = sys.argv[3]
print(f"🚀 Initializing skill: {skill_name}")
print(f" Location: {path}")
print()
result = init_skill(skill_name, path)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""
Skill Packager - Creates a distributable .skill file of a skill folder
Usage:
python utils/package_skill.py <path/to/skill-folder> [output-directory]
Example:
python utils/package_skill.py skills/public/my-skill
python utils/package_skill.py skills/public/my-skill ./dist
"""
import sys
import zipfile
from pathlib import Path
from quick_validate import validate_skill
def package_skill(skill_path, output_dir=None):
"""
Package a skill folder into a .skill file.
Args:
skill_path: Path to the skill folder
output_dir: Optional output directory for the .skill file (defaults to current directory)
Returns:
Path to the created .skill file, or None if error
"""
skill_path = Path(skill_path).resolve()
# Validate skill folder exists
if not skill_path.exists():
print(f"❌ Error: Skill folder not found: {skill_path}")
return None
if not skill_path.is_dir():
print(f"❌ Error: Path is not a directory: {skill_path}")
return None
# Validate SKILL.md exists
skill_md = skill_path / "SKILL.md"
if not skill_md.exists():
print(f"❌ Error: SKILL.md not found in {skill_path}")
return None
# Run validation before packaging
print("🔍 Validating skill...")
valid, message = validate_skill(skill_path)
if not valid:
print(f"❌ Validation failed: {message}")
print(" Please fix the validation errors before packaging.")
return None
print(f"✅ {message}\n")
# Determine output location
skill_name = skill_path.name
if output_dir:
output_path = Path(output_dir).resolve()
output_path.mkdir(parents=True, exist_ok=True)
else:
output_path = Path.cwd()
skill_filename = output_path / f"{skill_name}.skill"
# Create the .skill file (zip format)
try:
with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf:
# Walk through the skill directory
for file_path in skill_path.rglob('*'):
if file_path.is_file():
# Calculate the relative path within the zip
arcname = file_path.relative_to(skill_path.parent)
zipf.write(file_path, arcname)
print(f" Added: {arcname}")
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
return skill_filename
except Exception as e:
print(f"❌ Error creating .skill file: {e}")
return None
def main():
if len(sys.argv) < 2:
print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]")
print("\nExample:")
print(" python utils/package_skill.py skills/public/my-skill")
print(" python utils/package_skill.py skills/public/my-skill ./dist")
sys.exit(1)
skill_path = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
print(f"📦 Packaging skill: {skill_path}")
if output_dir:
print(f" Output directory: {output_dir}")
print()
result = package_skill(skill_path, output_dir)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Quick validation script for skills - minimal version
"""
import sys
import os
import re
import yaml
from pathlib import Path
def validate_skill(skill_path):
"""Basic validation of a skill"""
skill_path = Path(skill_path)
# Check SKILL.md exists
skill_md = skill_path / 'SKILL.md'
if not skill_md.exists():
return False, "SKILL.md not found"
# Read and validate frontmatter
content = skill_md.read_text()
if not content.startswith('---'):
return False, "No YAML frontmatter found"
# Extract frontmatter
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not match:
return False, "Invalid frontmatter format"
frontmatter_text = match.group(1)
# Parse YAML frontmatter
try:
frontmatter = yaml.safe_load(frontmatter_text)
if not isinstance(frontmatter, dict):
return False, "Frontmatter must be a YAML dictionary"
except yaml.YAMLError as e:
return False, f"Invalid YAML in frontmatter: {e}"
# Define allowed properties
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'}
# Check for unexpected properties (excluding nested keys under metadata)
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
if unexpected_keys:
return False, (
f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
)
# Check required fields
if 'name' not in frontmatter:
return False, "Missing 'name' in frontmatter"
if 'description' not in frontmatter:
return False, "Missing 'description' in frontmatter"
# Extract name for validation
name = frontmatter.get('name', '')
if not isinstance(name, str):
return False, f"Name must be a string, got {type(name).__name__}"
name = name.strip()
if name:
# Check naming convention (hyphen-case: lowercase with hyphens)
if not re.match(r'^[a-z0-9-]+$', name):
return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)"
if name.startswith('-') or name.endswith('-') or '--' in name:
return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
# Check name length (max 64 characters per spec)
if len(name) > 64:
return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
# Extract and validate description
description = frontmatter.get('description', '')
if not isinstance(description, str):
return False, f"Description must be a string, got {type(description).__name__}"
description = description.strip()
if description:
# Check for angle brackets
if '<' in description or '>' in description:
return False, "Description cannot contain angle brackets (< or >)"
# Check description length (max 1024 characters per spec)
if len(description) > 1024:
return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
return True, "Skill is valid!"
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python quick_validate.py <skill_directory>")
sys.exit(1)
valid, message = validate_skill(sys.argv[1])
print(message)
sys.exit(0 if valid else 1)
@@ -0,0 +1,37 @@
---
name: starter-cleaner
description: 清理 wot-starter 模板,移除示例页面、文档和 monorepo 配置,创建一个最简基础模板。
---
# Starter Cleaner Skill
该技能用于将 `wot-starter` 模板精简至最小状态,适合用于启动不包含示例代码的新项目。
## 功能说明
1. **目录清理**:
* 移除 `docs/`: 文档目录。
* 移除 `src/subPages/`: 示例分包页面。
* 移除 `src/subEcharts/`: Echarts 示例。
* 移除 `src/subAsyncEcharts/`: 异步 Echarts 示例。
* 移除 `src/pages.json`: 项目页面配置文件(由 `pages.config.ts` 生成)。
* 移除 `pnpm-workspace.yaml`: Monorepo 配置文件。
2. **配置清理**:
* **vite.config.ts**:
* 移除 `UniHelperPages` 相关分包配置 (`subEcharts`, `subAsyncEcharts` 等)。
* **注意**:保留 `uni-echarts` 相关配置。
* **package.json**:
* 移除 `docs:` 相关脚本 (`docs:dev`, `docs:build`, `docs:preview`)。
* **注意**:保留 `uni-echarts` 和 `echarts` 依赖。
## 使用方法
要执行清理过程,请使用 `run_command` 工具运行提供的 node 脚本。
```zsh
node .agent/skills/starter-cleaner/scripts/clean.js
```
> [!WARNING]
> 该技能执行**破坏性**操作。它将永久删除文件和目录。运行前请确保相关工作已提交。
@@ -0,0 +1,87 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
/**
* Recursively find the project root by looking for package.json
* @param {string} dir - Current directory to check
* @returns {string} - Path to the project root
*/
function findProjectRoot(dir) {
if (fs.existsSync(path.join(dir, 'package.json'))) {
return dir
}
const parentDir = path.dirname(dir)
if (parentDir === dir) {
throw new Error('Could not find project root (package.json not found)')
}
return findProjectRoot(parentDir)
}
const projectRoot = findProjectRoot(__dirname)
console.log(`Cleaning project at: ${projectRoot}`)
// 1. Remove Directories and Files
const pathsToRemove = [
'src/subPages',
'src/pages.json',
'pnpm-workspace.yaml',
]
pathsToRemove.forEach((p) => {
const fullPath = path.join(projectRoot, p)
if (fs.existsSync(fullPath)) {
console.log(`Removing: ${p}`)
fs.rmSync(fullPath, { recursive: true, force: true })
}
else {
console.log(`Skipping (not found): ${p}`)
}
})
// 2. Modify vite.config.ts
const viteConfigPath = path.join(projectRoot, 'vite.config.ts')
if (fs.existsSync(viteConfigPath)) {
console.log('Modifying vite.config.ts...')
let content = fs.readFileSync(viteConfigPath, 'utf-8')
// Remove from UniHelperPages configuration
// Matches subPackages: [ ... ] structure
const subPackagesRegex = /(subPackages:\s*\[)([\s\S]*?)(\])/
const match = content.match(subPackagesRegex)
if (match) {
const subPackagesContent = match[2]
// Remove lines containing subEcharts or subAsyncEcharts
content = content.replace(subPackagesRegex, `$1${subPackagesContent}$3`)
fs.writeFileSync(viteConfigPath, content, 'utf-8')
}
}
// 3. Modify package.json
const packageJsonPath = path.join(projectRoot, 'package.json')
if (fs.existsSync(packageJsonPath)) {
console.log('Modifying package.json...')
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'))
// Remove docs scripts
let scriptsChanged = false
if (pkg.scripts) {
Object.keys(pkg.scripts).forEach((key) => {
if (key.startsWith('docs:')) {
delete pkg.scripts[key]
scriptsChanged = true
}
})
}
if (scriptsChanged) {
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf-8')
}
}
console.log('Cleanup complete!')
@@ -0,0 +1,105 @@
---
name: uni-page-generator
description: 基于项目规范快速生成 uni-app 页面
---
# uni-app 页面生成器
快速创建符合 wot-starter 项目规范的 uni-app 页面。
## 使用场景
- 创建主包页面 (`src/pages/`)
- 创建分包页面 (`src/subPages/`, `src/subEcharts/` 等)
- 自动配置路由和布局
## 页面模板
### 基础页面
```vue
<script setup lang="ts">
definePage({
name: '页面名称', // 路由 name,用于编程式导航
layout: 'default', // 布局:'default' | 'tabbar'
style: {
navigationBarTitleText: '页面标题',
},
})
const router = useRouter()
</script>
<template>
<view class="p-3">
<!-- 页面内容 -->
</view>
</template>
```
### TabBar 页面
```vue
<script setup lang="ts">
definePage({
name: 'home',
layout: 'tabbar', // 使用 tabbar 布局
style: {
navigationBarTitleText: '首页',
},
})
</script>
<template>
<view class="box-border py-3">
<!-- TabBar 页面内容 -->
</view>
</template>
```
## 目录结构
```
src/
├── pages/ # 主包页面(TabBar 页面)
│ ├── index/
│ │ └── index.vue
│ └── about/
│ └── index.vue
├── subPages/ # 分包页面
│ ├── router/
│ ├── request/
│ └── ...
└── subEcharts/ # ECharts 分包
```
## 创建步骤
1. **确定页面位置**
- TabBar 页面 → `src/pages/{name}/index.vue`
- 普通页面 → `src/subPages/{name}/index.vue`
2. **使用 definePage 宏**
- 配置 `name` 用于编程式导航
- 配置 `layout` 选择布局
- 配置 `style` 设置导航栏
3. **页面跳转**
```typescript
const router = useRouter()
// 使用 name 跳转
router.push({ name: 'detail' })
// 使用 path 跳转
router.push('/subPages/detail/index')
// 带参数跳转
router.push({ name: 'detail', query: { id: '123' } })
```
## 注意事项
- 分包目录需在 `vite.config.ts` 的 `subPackages` 中注册
- 页面文件名固定为 `index.vue`
- 使用 UnoCSS 原子化样式
@@ -0,0 +1,209 @@
---
name: vue-composable-creator
description: 快速创建 Vue 3 组合式函数
---
# Vue Composable 创建器
快速创建符合 wot-starter 项目规范的 Vue 3 组合式函数。
## 目录结构
```
src/composables/
├── types/ # 类型定义目录
│ └── theme.ts
├── useGlobalToast.ts # 全局 Toast
├── useGlobalMessage.ts # 全局 Message
├── useGlobalLoading.ts # 全局 Loading
├── useTheme.ts # 主题管理
├── useManualTheme.ts # 手动主题管理
├── useTabbar.ts # TabBar 控制
└── use{Name}.ts # 新建的 composable
```
## Composable 模板
### 基础模板
```typescript
// src/composables/use{Name}.ts
import { ref, computed, onMounted, onUnmounted } from 'vue'
/**
* {描述这个 composable 的用途}
* @returns {返回值描述}
*/
export function use{Name}() {
// 响应式状态
const state = ref<StateType>(initialValue)
const loading = ref(false)
// 计算属性
const computedValue = computed(() => {
return state.value
})
// 方法
function doSomething() {
// 业务逻辑
}
async function asyncAction() {
loading.value = true
try {
// 异步操作
} finally {
loading.value = false
}
}
// 生命周期
onMounted(() => {
// 初始化
})
onUnmounted(() => {
// 清理
})
return {
state,
loading,
computedValue,
doSomething,
asyncAction,
}
}
```
### 结合 Pinia Store
```typescript
// src/composables/use{Name}.ts
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
export function use{Name}() {
const store = use{Name}Store()
// 使用 storeToRefs 保持响应性
const { data, loading } = storeToRefs(store)
// 封装 action
async function refresh() {
await store.fetchData()
}
return {
data,
loading,
refresh,
}
}
```
### 带参数的 Composable
```typescript
// src/composables/use{Name}.ts
interface Use{Name}Options {
immediate?: boolean
onSuccess?: (data: any) => void
onError?: (error: Error) => void
}
export function use{Name}(options: Use{Name}Options = {}) {
const { immediate = true, onSuccess, onError } = options
const data = ref<DataType | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
async function execute() {
loading.value = true
error.value = null
try {
const result = await fetchData()
data.value = result
onSuccess?.(result)
} catch (err) {
error.value = err as Error
onError?.(err as Error)
} finally {
loading.value = false
}
}
if (immediate) {
execute()
}
return {
data,
loading,
error,
execute,
}
}
```
## 项目示例参考
### useGlobalToast
```typescript
// 全局 Toast 提示
export const useGlobalToast = defineStore('global-toast', {
actions: {
show(option: ToastOptions | string) { /* ... */ },
success(option: ToastOptions | string) { /* ... */ },
error(option: ToastOptions | string) { /* ... */ },
info(option: ToastOptions | string) { /* ... */ },
warning(option: ToastOptions | string) { /* ... */ },
close() { /* ... */ },
},
})
```
### useTheme
```typescript
// 主题管理
export function useTheme() {
const store = useThemeStore()
return {
theme: computed(() => store.theme),
isDark: computed(() => store.isDark),
themeVars: computed(() => store.themeVars),
}
}
```
## 使用方式
```vue
<script setup lang="ts">
// 自动导入,无需手动 import
const { data, loading, execute } = use{Name}({
immediate: false,
onSuccess: (data) => {
console.log('成功:', data)
},
})
// 手动执行
const handleClick = () => {
execute()
}
</script>
```
## 注意事项
- 文件名使用 `use{Name}.ts` 格式
- 导出函数使用 `use{Name}` 命名
- 类型定义放在 `types/` 目录或文件顶部
- 自动导入已配置,无需手动 import
- 复用 Pinia Store 时使用 `storeToRefs` 保持响应性
@@ -0,0 +1,201 @@
---
name: wot-router-usage
description: '@wot-ui/router 轻量级路由库使用指南'
---
# @wot-ui/router 使用指南
专为 uni-app 设计的轻量级路由库,提供类似 Vue Router 的 API。
## 核心特性
- 📝 编程式导航
- 🔄 参数传递(params + query)
- 🛡️ 导航守卫
- 📊 路由信息获取
- 🎯 TypeScript 支持
## 路由配置
```typescript
// src/router/index.ts
import { pages, subPackages } from 'virtual:uni-pages'
function generateRoutes() {
const routes = pages.map((page) => ({
...page,
path: `/${page.path}`,
}))
subPackages?.forEach((sub) => {
sub.pages.forEach((page) => {
routes.push({
...page,
path: `/${sub.root}/${page.path}`,
})
})
})
return routes
}
const router = createRouter({
routes: generateRoutes(),
})
export default router
```
## 基础导航
```typescript
const router = useRouter()
const route = useRoute()
// 字符串路径
router.push('/pages/detail/index')
// 对象形式
router.push({ path: '/pages/detail/index' })
// 命名路由
router.push({ name: 'detail' })
// 替换当前页面
router.replace({ name: 'login' })
// 返回上一页
router.back()
// 返回多级
router.go(-2)
```
## 参数传递
### Query 参数
```typescript
// 跳转
router.push({
name: 'detail',
query: { id: '123', type: 'product' },
})
// 获取
const route = useRoute()
const id = route.query.id
const type = route.query.type
```
### Params 参数
```typescript
// 跳转
router.push({
name: 'detail',
params: { id: '123' },
})
// 获取
const route = useRoute()
const id = route.params.id
```
## 导航守卫
### 全局前置守卫
```typescript
router.beforeEach((to, from, next) => {
console.log(`导航: ${from.path} → ${to.path}`)
// 权限检查
if (to.meta?.requiresAuth && !isLoggedIn()) {
next({ name: 'login' })
return
}
// 异步守卫
if (to.name === 'protected') {
return new Promise((resolve) => {
showConfirm({
title: '确认访问',
success: () => { next(); resolve() },
fail: () => { next(false); resolve() },
})
})
}
next()
})
```
### 全局后置钩子
```typescript
router.afterEach((to, from) => {
console.log(`页面切换完成: ${to.path}`)
// 页面统计
trackPageView(to.path)
})
```
## 路由信息
```typescript
const route = useRoute()
// 当前路径
route.path // '/subPages/detail/index'
// 路由名称
route.name // 'detail'
// 查询参数
route.query // { id: '123' }
// 路径参数
route.params // { id: '123' }
// 完整路径
route.fullPath // '/subPages/detail/index?id=123'
// 路由元信息
route.meta // { requiresAuth: true }
```
## 页面定义
```vue
<script setup lang="ts">
definePage({
name: 'detail', // 路由名称
layout: 'default', // 布局
meta: {
requiresAuth: true, // 自定义元信息
},
style: {
navigationBarTitleText: '详情',
},
})
</script>
```
## TabBar 页面跳转
```typescript
// TabBar 页面使用 reLaunch
router.push({
name: 'home',
reLaunch: true, // 或自动识别 tabBar 页面
})
```
## 注意事项
- uni-app 路由限制仍然存在(如页面栈限制)
- TabBar 页面需要特殊处理
- 导航守卫中的异步操作需要返回 Promise
- 参数过长时考虑使用全局状态传递
@@ -0,0 +1,31 @@
---
description: 项目基本信息、工作流和常用命令
globs: package.json, src/**/*
alwaysApply: true
---
# 项目信息与工作流
## 🚀 快速开始
- **开发**: `pnpm dev` (H5), `pnpm dev:mp-weixin` (微信小程序)。
- **构建**: `pnpm build:h5`, `pnpm build:mp-weixin`。
- **Lint**: `pnpm lint:fix`。
- **预览**: `dist/` 目录。
## 📂 目录结构概览
- `src/pages`: 应用视图 (基于文件的路由)。
- `src/components`: 可复用的 UI 组件。
- `src/store`: Pinia 状态管理模块。
- `src/api`: Alova.js API 定义。
- `src/uni_modules`: Uni-app 模块 (包含 wot-design-uni)。
## 🤝 Git 工作流
- **提交**: 符合 Conventional Commits 规范 (`feat:`, `fix:`, `chore:`, `refactor:`)。
- **主要工具**: `commitizen` (通过 `pnpm commit` 使用)。
## 🛠️ 代码生成
- **API**: 使用 `pnpm alova-gen` 重新生成 API 定义。
- **Skills**: 使用 `.agent/skills` 中的 skill 进行脚手架生成:
- `pinia-store-generator`
- `uni-page-generator`
- `alova-api-module`
+35
View File
@@ -0,0 +1,35 @@
---
description: 核心技术栈、架构和代码规范
globs: src/store/**/*, src/api/**/*, src/router/**/*, src/pages/**/*, pages.config.ts, alova.config.ts
alwaysApply: false
---
# 技术栈与架构
## 🏗️ 架构概览
本项目采用分层架构:
- **表现层**: `src/pages` (视图) + `src/components` (UI 逻辑)
- **状态层**: `src/store` (Pinia)
- **数据层**: `src/api` (Alova.js)
## 📦 状态管理 (Pinia)
- **库**: Pinia
- **持久化**: 使用 `src/store/persist.ts` 进行本地存储。
- **模式**: 详见 `pinia-store-generator` skill。
- **规则**: 始终使用 `defineStore` 并遵循 `use{Name}Store` 的命名规范。
- **Skill**: 使用 **`pinia-store-generator`** 快速创建新的 store。
## 🌐 API 层 (Alova.js)
- **库**: Alova.js
- **结构**:
- `src/api/core`: 拦截器和实例配置。
- `src/api/apiDefinitions.ts`: 自动生成的 API 定义。
- **Mock**: 支持在 `src/api/mock` 中编写 Mock 数据。
- **Skill**: 使用 **`alova-api-module`** 创建新的 API 模块和 Mock 数据。
## 🛣️ 路由
- **库**: `@wot-ui/router` (API) + `vite-plugin-uni-pages` (文件系统路由)
- **配置**: `pages.config.ts` 控制 `pages.json` 的生成。
- **导航**: 使用 `useRouter()` 进行 push/replace/back 操作。
- **Skill**: 参考 **`wot-router-usage`** 了解导航模式和守卫用法。
- **Skill**: 使用 **`uni-page-generator`** 创建带路由配置的新页面。
@@ -0,0 +1,33 @@
---
description: UI/UX 指南、样式规范和组件使用
globs: **/*.vue, **/*.scss, src/components/**/*, src/uni_modules/**/*, uno.config.ts
alwaysApply: false
---
# UI 与样式指南
## 🎨 样式系统
- **引擎**: UnoCSS (原子化 CSS) 是**首选**的样式方案。
- **配置**: `uno.config.ts`。
- **预处理**: SCSS 用于复杂的组件样式(极少需要)。
- **主题**: 通过 `src/theme.json` 和 CSS 变量支持亮色/暗色模式切换。
### UnoCSS 约定
- 使用工具类: `flex`, `items-center`, `text-primary`, `m-4`.
- 响应式前缀: `sm:`, `md:` (在移动端优先的 uni-app 中较少使用)。
- 图标: 通过 UnoCSS preset 使用 `i-carbon-{icon-name}`。
## 🧩 组件库
- **核心库**: `wot-design-uni` (`wd-` 前缀)。
- **文档**: [wot-design-uni](https://wot-ui.cn).
- **自定义组件**: 在 `src/components` 中创建。
## 📢 全局反馈
- **Toast/Message**: 请勿直接使用 `uni.showToast`。
- **标准**: 使用 `GlobalToast`, `GlobalMessage`, `GlobalLoading` 组件。
- **Skill**: 参考 **`global-feedback`** skill 查看使用示例。
## 📱 布局
- **系统**: `vite-plugin-uni-layouts`。
- **默认**: `src/layouts/default.vue`。
- **TabBar**: `src/layouts/tabbar.vue`。
+9
View File
@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
+25
View File
@@ -0,0 +1,25 @@
# 开发环境变量
# 在开发模式下使用 (npm run dev)
# API 基础 URL - 开发环境
# VITE_API_BASE_URL=http://localhost:8001
VITE_API_BASE_URL=https://service.fastapiadmin.com
# 环境名称
VITE_ENV_NAME=development
# 项目名称
VITE_APP_TITLE=fastapiadmin
# 代理前缀
VITE_APP_BASE_API=/api/v1
# 应用端口
VITE_APP_PORT=5180
# 超时时间
VITE_TIMEOUT=10000
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
+23
View File
@@ -0,0 +1,23 @@
# 生产环境变量
# 在生产构建时使用 (npm run build)
# API 基础 URL - 生产环境
VITE_API_BASE_URL=https://service.fastapiadmin.com
# 环境名称
VITE_ENV_NAME=production
# 项目名称
VITE_APP_TITLE=fastapiadmin
# 代理前缀
VITE_APP_BASE_API=/api/v1
# 应用端口
VITE_APP_PORT=5180
# 超时时间
VITE_TIMEOUT=10000
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
+23
View File
@@ -0,0 +1,23 @@
# 测试/预发布环境变量
# 在测试环境构建时使用
# API 基础 URL - 测试环境
VITE_API_BASE_URL=https://staging-api.yourapp.com
# 环境名称
VITE_ENV_NAME=test
# 项目名称
VITE_APP_TITLE=fastapiadmin
# 代理前缀
VITE_APP_BASE_API=/api/v1
# 应用端口
VITE_APP_PORT=5180
# 超时时间
VITE_TIMEOUT=10000
# WebSocket 服务器的 URL,不配置默认关闭 WebSocket
VITE_APP_WS_ENDPOINT= ws://localhost:5180/ws
+70
View File
@@ -0,0 +1,70 @@
{
"disableEmoji": false,
"list": ["test", "feat", "fix", "chore", "docs", "refactor", "style", "ci", "perf", "release", "revert", "build"],
"maxMessageLength": 64,
"minMessageLength": 3,
"questions": ["type", "scope", "subject", "body", "breaking", "issues", "lerna"],
"scopes": [],
"types": {
"chore": {
"description": "Chore | 构建/工程依赖/工具",
"emoji": "🚀",
"value": "chore"
},
"ci": {
"description": "Continuous Integration | CI 配置",
"emoji": "👷",
"value": "ci"
},
"docs": {
"description": "Documentation | 文档",
"emoji": "✏️ ",
"value": "docs"
},
"feat": {
"description": "Features | 新功能",
"emoji": "✨",
"value": "feat"
},
"fix": {
"description": "Bug Fixes | Bug 修复",
"emoji": "🐛",
"value": "fix"
},
"perf": {
"description": "Performance Improvements | 性能优化",
"emoji": "⚡",
"value": "perf"
},
"refactor": {
"description": "Code Refactoring | 代码重构",
"emoji": "♻️ ",
"value": "refactor"
},
"release": {
"description": "Create a release commit | 发版提交",
"emoji": "🏹",
"value": "release"
},
"style": {
"description": "Styles | 风格",
"emoji": "💄",
"value": "style"
},
"revert": {
"description": "Revert | 回退",
"emoji": "⏪",
"value": "revert"
},
"build": {
"description": "Build System | 打包构建",
"emoji": "📦",
"value": "build"
},
"test": {
"description": "Tests | 测试",
"emoji": "✅",
"value": "test"
}
}
}
+1
View File
@@ -0,0 +1 @@
custom: ['https://github.com/Moonofweisheng/sponsors']
+87
View File
@@ -0,0 +1,87 @@
name: 向 Wot Starter 反馈 Bug
description: 创建一个 Issue 描述你遇到的问题。
title: '[Bug 上报] 请在此填写标题'
labels: ['🐞bug: need confirm']
body:
- type: markdown
attributes:
value: |
在向我们提交 Bug 报告前,请优先使用以下方式尝试解决问题:
- 在组件文档 [wot-starter](https://starter.wot-ui.cn/) 确认使用方法是否正确
- 尝试在 [Issue](https://github.com/wot-ui/wot-starter/issues) 列表中搜索相同问题
- type: input
id: version
attributes:
label: Wot Starter 版本号
description: 你正在使用的模板版本号(请填写 package.json 里版本)
placeholder: 例如:0.1.1
validations:
required: true
- type: dropdown
id: platform
attributes:
label: 平台
multiple: true
description: 选择对应的平台
options:
- h5
- 微信小程序
- 支付宝小程序
- APP
- 钉钉小程序
- 其他小程序
validations:
required: true
- type: input
id: reproduce
attributes:
label: 复现Demo地址(如不提供,将被直接关闭)
description: |
我们需要你提供一个最小重现demo,以便于我们帮你排查问题。你可以通过 fork 本项目,快速创建一个wot-design-uni项目,并添加相关复现逻辑来提供。不要随便填写一个东西,这会导致你的 issue 被直接关闭。即使在你看来问题很容易复现,也请认真对待,因为一个完整复现demo可以大大提高我们排查问题的效率。
validations:
required: true
- type: textarea
id: reproduce-steps
attributes:
label: 重现步骤
description: |
请提供一个最简洁清晰的重现步骤,方便我们快速重现问题。
validations:
required: true
- type: textarea
id: expected
attributes:
label: 期望的结果是什么?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: 实际的结果是什么?
validations:
required: true
- type: textarea
id: uni-app
attributes:
label: 环境信息
description: |
在这里填写你的环境信息
- 发行平台: [如 微信小程序、H5平台、App等]
- 操作系统 [如 iOS 12.1.2、Android 7.0]
- HBuilderX版本 [如使用HBuilderX,则需提供 HBuilderX 版本号]
- uni-app版本 [如使用Vue-cli创建/运行项目,则提供`npm run info`的运行结果]
- 设备信息 [如 iPhone8 Plus]
- type: textarea
id: extra
attributes:
label: 其他补充信息
description: |
根据你的分析,出现这个问题的原因可能在哪里,或者你认为可能产生关联的信息:比如 Vue 版本、vite 版本、Node 版本、采用哪种自动引入方案等,或者进行了哪些配置,使用了哪些插件等信息。
@@ -0,0 +1,33 @@
name: 向 Wot Starter 提出新功能需求
description: 创建一个 Issue 描述一下你的功能需求。
title: '[新功能需求] 请在此填写标题'
labels: ['feature: need confirm']
body:
- type: markdown
attributes:
value: |
在提交功能需求前,请注意:
- 确认这是一个通用功能,并且无法通过现有的 API 或 Slot 实现。
- 尝试在 [Issue](https://github.com/wot-ui/wot-starter/issues)列表中搜索,并且没有发现同样的需求。
- 请确保描述清楚你的需求,以便其他开发者更好地理解你的需求。
- type: textarea
id: description
attributes:
label: 这个功能解决了什么问题?
description: 请尽可能详细地说明这个功能的使用场景。
validations:
required: true
- type: textarea
id: api
attributes:
label: 你期望的 API 是什么样子的?
description: 描述一下这个新功能的 API,并提供一些代码示例。
placeholder: |
```xml
<wd-interesting some-prop="xxx" />
```
validations:
required: true
+43
View File
@@ -0,0 +1,43 @@
<!-- (将"[ ]"更新为"[x]"以勾选一个框) -->
### 🤔 这个 PR 的性质是?(至少选择一个)
- [ ] 日常 bug 修复
- [ ] 新特性提交
- [ ] 站点、文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] TypeScript 定义更新
- [ ] CI/CD 改进
- [ ] 包体积优化
- [ ] 性能优化
- [ ] 功能增强
- [ ] 国际化改进
- [ ] 代码重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他改动(是关于什么的改动?)
### 🔗 相关 Issue
<!--
1. 描述相关需求的来源,如相关的 issue 讨论链接。
-->
### 💡 需求背景和解决方案
<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
### ☑️ 请求合并前的自查清单
⚠️ 请自检并全部**勾选全部选项**。⚠️
- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
+119
View File
@@ -0,0 +1,119 @@
name: Build and Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
build-and-test:
runs-on: windows-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.9.0
run_install: false
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies (workspace recursive)
run: pnpm -r install --frozen-lockfile
- name: Test WeChat Mini Program dev mode
shell: bash
run: |
echo "Testing WeChat Mini Program dev mode startup..."
timeout 30s pnpm dev:mp-weixin || true
echo "✅ WeChat Mini Program dev mode test completed"
- name: Test H5 dev mode
shell: bash
run: |
echo "Testing H5 dev mode startup..."
timeout 30s pnpm dev:h5 || true
echo "✅ H5 dev mode test completed"
- name: Test iOS dev mode
shell: bash
run: |
echo "Testing iOS dev mode startup..."
timeout 30s pnpm dev:app-ios || true
echo "✅ iOS dev mode test completed"
- name: Build WeChat Mini Program
run: pnpm build:mp-weixin
- name: Build H5
run: pnpm build:h5
- name: Build iOS
run: pnpm build:app-ios
- name: Upload WeChat Mini Program artifacts
uses: actions/upload-artifact@v4
with:
name: wechat-miniprogram-build
path: dist/build/mp-weixin/
retention-days: 7
- name: Upload H5 artifacts
uses: actions/upload-artifact@v4
with:
name: h5-build
path: dist/build/h5/
retention-days: 7
- name: Upload iOS artifacts
uses: actions/upload-artifact@v4
with:
name: ios-build
path: dist/build/app-ios/
retention-days: 7
- name: Build summary
run: |
echo "## Build and Test Summary" >> $GITHUB_STEP_SUMMARY
echo "### Development Mode Tests" >> $GITHUB_STEP_SUMMARY
echo "✅ WeChat Mini Program dev mode test completed" >> $GITHUB_STEP_SUMMARY
echo "✅ H5 dev mode test completed" >> $GITHUB_STEP_SUMMARY
echo "✅ iOS dev mode test completed" >> $GITHUB_STEP_SUMMARY
echo "### Build Results" >> $GITHUB_STEP_SUMMARY
echo "✅ WeChat Mini Program build completed" >> $GITHUB_STEP_SUMMARY
echo "✅ H5 build completed" >> $GITHUB_STEP_SUMMARY
echo "✅ iOS build completed" >> $GITHUB_STEP_SUMMARY
echo "📦 All artifacts uploaded successfully" >> $GITHUB_STEP_SUMMARY
- name: Build Docs (VitePress)
run: pnpm -C docs run docs:build
- name: Upload Docs artifacts
uses: actions/upload-artifact@v4
with:
name: docs-build
path: docs/.vitepress/dist/
retention-days: 7
+54
View File
@@ -0,0 +1,54 @@
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.9.0
run_install: false
- name: Create Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate GitHub Changelog
run: pnpx changelogithub
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release summary
run: |
echo "## Release Summary" >> $GITHUB_STEP_SUMMARY
echo "### Version: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "### Release Status" >> $GITHUB_STEP_SUMMARY
echo "✅ Release created successfully" >> $GITHUB_STEP_SUMMARY
+21
View File
@@ -0,0 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx commitlint --edit $1
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged --allow-empty $1
+8
View File
@@ -0,0 +1,8 @@
# registry = https://registry.npmjs.org
registry = https://registry.npmmirror.com
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true
ignore-workspace-root-check=true
install-workspace-root=true
+1
View File
@@ -0,0 +1 @@
22
+64
View File
@@ -0,0 +1,64 @@
{
"header": "# 更新日志 \n\n",
"types": [{
"type": "feat",
"section": "✨ Features | 新功能",
"hidden": false
},
{
"type": "fix",
"section": "🐛 Bug Fixes | Bug 修复",
"hidden": false
},
{
"type": "init",
"section": "🎉 Init | 初始化",
"hidden": true
},
{
"type": "docs",
"section": "✏️ Documentation | 文档",
"hidden": false
},
{
"type": "style",
"section": "💄 Styles | 风格",
"hidden": true
},
{
"type": "refactor",
"section": "♻️ Code Refactoring | 代码重构",
"hidden": true
},
{
"type": "perf",
"section": "⚡ Performance Improvements | 性能优化",
"hidden": false
},
{
"type": "test",
"section": "✅ Tests | 测试",
"hidden": true
},
{
"type": "revert",
"section": "⏪ Revert | 回退",
"hidden": true
},
{
"type": "build",
"section": "📦‍ Build System | 打包构建",
"hidden": true
},
{
"type": "chore",
"section": "🚀 Chore | 构建/工程依赖/工具",
"hidden": true
},
{
"type": "ci",
"section": "👷 Continuous Integration | CI 配置",
"hidden": true
}
]
}
+128
View File
@@ -0,0 +1,128 @@
# 更新日志
## [1.4.0](https://github.com/wot-ui/wot-starter/compare/v1.3.2...v1.4.0) (2026-01-25)
### ✏️ Documentation | 文档
* ✏️ remove gitee-vote-2025 ([ed32556](https://github.com/wot-ui/wot-starter/commit/ed32556db46d7922cde1a60d1efc32bfb1c87d63))
### 🐛 Bug Fixes | Bug 修复
* 🐛 仅在微信小程序端开启 optimization 修复运行到支付宝小程序报错的问题 ([420aff4](https://github.com/wot-ui/wot-starter/commit/420aff484878ff88934b913d9aa84916a36c2de8))
### ✨ Features | 新功能
* ✨ 添加基于本项目实际使用场景的 Agent Skills ([f2a58f7](https://github.com/wot-ui/wot-starter/commit/f2a58f748c8758b4a083fa7181862dc1ad97e303))
* ✨ 新增清理演示页面提供精简模板的 skill starter-cleaner ([8b4c4c4](https://github.com/wot-ui/wot-starter/commit/8b4c4c4d0c9abdee1616484a435444c9ae4ce000))
### [1.3.2](https://github.com/wot-ui/wot-starter/compare/v1.3.1...v1.3.2) (2026-01-07)
### ✨ Features | 新功能
* ✨ 升级 @uni-ku/bundle-optimizer 至 2.0 并处理相关迁移配置 ([e0ab94c](https://github.com/wot-ui/wot-starter/commit/e0ab94cfd7e9971b52c929de5d89e7c5ab11eb3a))
### [1.3.1](https://github.com/wot-ui/wot-starter/compare/v1.3.0...v1.3.1) (2026-01-05)
### ✨ Features | 新功能
* ✨ 更新 @wot-ui/router 以修复 route 类型问题和 afterEach多次触发的问题 ([fd4f585](https://github.com/wot-ui/wot-starter/commit/fd4f58524bc580a3f3cc249a9447d7c3d0c556d5))
## [1.3.0](https://github.com/wot-ui/wot-starter/compare/v1.2.2...v1.3.0) (2026-01-04)
### ✨ Features | 新功能
* ✨ 更新 wot-ui 到 v1.14.0 版本 ([4fd2532](https://github.com/wot-ui/wot-starter/commit/4fd25328ba5c3d3e7ea202919071cd8fb98ffd74))
### [1.2.2](https://github.com/wot-ui/wot-starter/compare/v1.2.1...v1.2.2) (2025-12-29)
### ✨ Features | 新功能
* ✨ 替换 uni-mini-router 为 @wot-ui/router ([171f054](https://github.com/wot-ui/wot-starter/commit/171f054ac7bb0976ee26edcbf4028c80cc4387a2))
### ✏️ Documentation | 文档
* ✏️ 调整路由文档和演示demo ([73a9cf3](https://github.com/wot-ui/wot-starter/commit/73a9cf36b5b770a556e2ea74be9c5c21602ff661))
* ✏️ 更新 readme ([6bf2618](https://github.com/wot-ui/wot-starter/commit/6bf261833a6a8485c749c0123e3e8ba7b306156e))
* ✏️ 更新分包调整后demo的地址 ([5e358ec](https://github.com/wot-ui/wot-starter/commit/5e358ec27997c5437043ec3137cfbd4513154fd4))
* ✏️ 更新文档首页介绍内容 ([3a1a0a4](https://github.com/wot-ui/wot-starter/commit/3a1a0a409effcc730064d1acc1edc64fe58fd7d1))
* ✏️ 添加 PageSpy 远程的教程 ([f29a53c](https://github.com/wot-ui/wot-starter/commit/f29a53cd01cdc80c45e858e409f1564cf7716f73))
* ✏️ add about me ([046897a](https://github.com/wot-ui/wot-starter/commit/046897a862e402fd8ebbd993cac525de5e830f25))
* ✏️ add gitee vote 2025 ([14af7a0](https://github.com/wot-ui/wot-starter/commit/14af7a04cf1e26772782f0cad0d426e220dba40a))
### [1.2.1](https://github.com/wot-ui/wot-starter/compare/v1.2.0...v1.2.1) (2025-12-04)
### 🐛 Bug Fixes | Bug 修复
* **manualTheme:** 修复跟随系统自动切换主题失效的问题 ([380c702](https://github.com/wot-ui/wot-starter/commit/380c7026eeb37a12e9a2866b18bd70880efdfecc))
### ✨ Features | 新功能
* **index:** 首页设置中新增"跟随系统"按钮 ([d031788](https://github.com/wot-ui/wot-starter/commit/d031788d4c9b31a7d030f17856f69f2d177eb1b8))
* **logo:** 更新logo ([550caa2](https://github.com/wot-ui/wot-starter/commit/550caa243e423969745d62c3872dfae14976409a))
### ✏️ Documentation | 文档
* ✏️ 更新 logo ([0e57c45](https://github.com/wot-ui/wot-starter/commit/0e57c45b94284320d5fdc3be6c082faf12ddcbad))
* ✏️ 首页添加 uni-ku 插件入口 ([a5b05c9](https://github.com/wot-ui/wot-starter/commit/a5b05c9091f5396c27c556ed23eed87b7f06fef0))
* ✏️ 文档增加显示版本号 ([10b7078](https://github.com/wot-ui/wot-starter/commit/10b707810529af53bd2a85fca6624807b5b2d9ec))
* ✏️ 移动非主包必需示例页面到分包中 ([#43](https://github.com/wot-ui/wot-starter/issues/43)) ([3d7a076](https://github.com/wot-ui/wot-starter/commit/3d7a07619cf4b84c26a91b5028b2635bcc6d44ff)), closes [#35](https://github.com/wot-ui/wot-starter/issues/35)
* ✏️ update logo ([6b9e4f9](https://github.com/wot-ui/wot-starter/commit/6b9e4f9e66d7be10b1e678b46472c9e44a270fc3))
## [1.2.0](https://github.com/wot-ui/wot-starter/compare/v1.1.0...v1.2.0) (2025-11-26)
### ✨ Features | 新功能
* ✨ 合并模板与文档项目开发便利性优化 ([#41](https://github.com/wot-ui/wot-starter/issues/41)) ([646d215](https://github.com/wot-ui/wot-starter/commit/646d2158c96dcf83518ed22bc27cc8e20f2ed0d2)), closes [#35](https://github.com/wot-ui/wot-starter/issues/35)
* ✨ 支持 esm 并更新 unocss 和 [@uni-helper](https://github.com/uni-helper) 插件 ([#39](https://github.com/wot-ui/wot-starter/issues/39)) ([f433b49](https://github.com/wot-ui/wot-starter/commit/f433b49023c572488254b18584a4dbca0ba66336))
### ✏️ Documentation | 文档
* ✏️ 添加 vite base ([b6f5cf8](https://github.com/wot-ui/wot-starter/commit/b6f5cf83084d0cc04bc94c9714722a7a4c9e6327))
* ✏️ 增加更新日志入口 ([7037d8f](https://github.com/wot-ui/wot-starter/commit/7037d8f10a9aa0274d5cc5d7afc8978342f96f1a))
## [1.1.0](https://github.com/wot-ui/wot-starter/compare/v1.0.0...v1.1.0) (2025-11-12)
### ✨ Features | 新功能
* ✨ 添加 uni_modules 插件引入示例 ([8493761](https://github.com/wot-ui/wot-starter/commit/8493761ad6ea4e6478d3b7764b43b813e5178e86))
* ✨ 支持 harmony next 自定义 tabbar ([f71e8ba](https://github.com/wot-ui/wot-starter/commit/f71e8ba62504a4c0b79d02e61979b52e1f538e59))
## 1.0.0 (2025-10-28)
### 🐛 Bug Fixes | Bug 修复
* 🐛 修复分包路由未注册的问题 ([3da843b](https://github.com/wot-ui/wot-starter/commit/3da843ba33bf62d2a8032dabf3061b2ce87e46a9))
### ✏️ Documentation | 文档
* ✏️ 更新 README ([a630274](https://github.com/wot-ui/wot-starter/commit/a63027496f9f75e8106437bf7e4285164a7f91b1))
* ✏️ 更新README ([abde3bc](https://github.com/wot-ui/wot-starter/commit/abde3bca57cbee293d0751dd6f273366425e2474))
* ✏️ 添加分包示例 ([809b65b](https://github.com/wot-ui/wot-starter/commit/809b65b8384029d9ed2c7807709023d65bf6bb4c))
* **README:** ✏️ 更新 vitesse-uni-app 项目链接 ([#11](https://github.com/wot-ui/wot-starter/issues/11)) ([6f1585a](https://github.com/wot-ui/wot-starter/commit/6f1585a6da97a9aeed4071125e6b618f30a50bb7))
### ✨ Features | 新功能
* ✨ 全局反馈组件兼容支付宝小程序 ([7f04d43](https://github.com/wot-ui/wot-starter/commit/7f04d43d44b6eaedabcf32d6c5842b056a0ac8ba))
* ✨ 新增主题切换示例 ([#4](https://github.com/wot-ui/wot-starter/issues/4)) ([c39e756](https://github.com/wot-ui/wot-starter/commit/c39e756821b08ba9934c88f5576d6eabda8fd449))
* ✨ 引入 @uni-ku/root 解决使用 page-meta 和根组件的问题 ([989e9fd](https://github.com/wot-ui/wot-starter/commit/989e9fd05a9c5a3b103608c941e5e30040a19f32))
* ✨ 引入 uni-echarts 支持图表功能,增加分包异步化示例 ([f933a61](https://github.com/wot-ui/wot-starter/commit/f933a6143d5fe02783ade63c669001245970756e))
* ✨ 引入 vite-plugin-uni-pages 的 definePage 宏,优化开发体验 ([498df4f](https://github.com/wot-ui/wot-starter/commit/498df4f26b1a84e8e91827178167ff853ae1f1a9))
* 升级 uni-echarts 版本 ([#17](https://github.com/wot-ui/wot-starter/issues/17)) ([af94964](https://github.com/wot-ui/wot-starter/commit/af9496440e440afae589277c12999644ccfffe3e))
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-PRESENT KeJun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+183
View File
@@ -0,0 +1,183 @@
<div align="center">
<p align="center">
<img src="https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/logo.png" width="200" />
</p>
<h1 align="center">
FastApp
<sup style="background-color: #28a745; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.4em; vertical-align: super; margin-left: 5px;">
v2.0.0
</h1>
<p align="center">
⚡️ FastApp 基于 uni-app + Vue 3 + TypeScript 的现代化移动端跨平台开发模板, ⭐️ 支持一下吧!
</p>
<p align="center">
<img src="https://img.shields.io/badge/Vue-3.5.22-green.svg" alt="Vue">
<img src="https://img.shields.io/badge/TypeScript-5.9.2-blue.svg" alt="TypeScript">
<img src="https://img.shields.io/badge/uni--app-3.0.0-orange.svg" alt="uni-app">
<img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License">
</p>
<p align="center">
<a href="https://service.fastapiadmin.com/app">📱 在线预览</a>
<a href="https://service.fastapiadmin.com/">📖 在线文档</a>
</p>
</div>
## 📖 项目介绍
FastApp 是 FastapiAdmin 项目的移动端应用,基于 uni-app 框架开发,支持一套代码多端运行。采用 Vue 3 + TypeScript + Vite 等现代化技术栈,集成了完善的代码规范和开发工具链,为开发者提供开箱即用的移动端开发解决方案。
## ⭐️ 核心特点
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - 就是快!
- 🐂 [Wot UI](https://github.com/Moonofweisheng/wot-design-uni) - 基于 Vue3 + TypeScript 的 uni-app 组件库,提供 70+ 高质量组件,支持国际化(内置多语言包)、暗黑模式与通过 CSS 变量进行主题定制
- 🚦 [@wot-ui/router](https://github.com/wot-ui/my-uni) - 适用于uni-app&vue3的轻量级路由库
- 🔄 [Uni Mini CI](https://github.com/Moonofweisheng/uni-mini-ci) - 一个小程序端持续集成的插件
- 🌐 [Alova](https://alova.js.org/zh-CN/) - 极致高效的请求工具集
- 🆒 [Uni Ku](https://uni-ku.js.org/) - 非常酷的 uni-app 插件库
- 📊 [Uni Echarts](https://uni-echarts.xiaohe.ink/) - 适用于 uni-app 的 Apache ECharts 组件
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 🦾 [TypeScript](https://www.typescriptlang.org/) & [ESLint](https://eslint.org/) - 保证代码质量
## 🔗 源码仓库
| 平台 | 仓库地址 |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **GitHub** | [FastapiAdmin主工程](https://github.com/fastapiadmin/FastapiAdmin.git) \| [FastDocs官网](https://github.com/fastapiadmin/FastDocs.git) \| [FastApp移动端](https://github.com/fastapiadmin/FastApp.git) |
| **Gitee** | [FastapiAdmin主工程](https://gitee.com/fastapiadmin/FastapiAdmin.git) \| [FastDocs官网](https://gitee.com/fastapiadmin/FastDocs.git) \| [FastApp移动端](https://gitee.com/fastapiadmin/FastApp.git) |
## 📁 项目结构
```bash
FastApp/
├── public/ # 静态资源目录
├── src/
│ ├── api/ # API 接口定义
│ ├── components/ # 公共组件
│ │ ├── DateQuery/ # 日期查询组件
│ │ └── Picker/ # 选择器组件
│ ├── composables/ # 组合式函数
│ ├── constants/ # 常量定义
│ ├── enums/ # 枚举定义
│ ├── http/ # HTTP 请求相关
│ │ ├── adapters/ # 请求适配器
│ │ ├── tools/ # 工具函数
│ │ └── types.ts # 类型定义
│ ├── layouts/ # 布局组件
│ ├── pages/ # 页面目录
│ │ ├── index/ # 首页
│ │ ├── login/ # 登录页
│ │ ├── work/ # 工作台
│ │ └── mine/ # 个人中心
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ │ ├── modules/ # 模块
│ │ └── index.ts # 入口文件
│ ├── styles/ # 样式文件
│ ├── types/ # TypeScript 类型定义
│ ├── utils/ # 工具函数
│ ├── App.vue # 应用根组件
│ ├── main.ts # 应用入口文件
│ ├── manifest.json # 应用配置文件
│ ├── pages.json # 页面路由配置
│ └── theme.json # 主题配置
├── dist/ # 构建输出目录
├── eslint.config.mjs # ESLint 配置
├── vite.config.ts # Vite 配置
├── tsconfig.json # TypeScript 配置
├── pages.config.ts # 页面配置
├── unocss.config.ts # UnoCSS 配置
└── package.json # 项目依赖配置
```
## 📸 项目截图
| 登录 <div style="width:60px"/> | 首页 <div style="width:60px"/> | 个人中心 <div style="width:60px"/> |
|----------|----------|----------|
| ![移动端登录](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/app_login.png) | ![移动端首页](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/app_home.png) | ![移动端个人中心](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/app_mine.png) |
## 🚀 快速开始
### 环境要求
- **Node.js** >= 22
- **pnpm** >= 9
### 安装依赖
```bash
# 安装项目依赖
pnpm install
# 启动 H5 开发服务器
pnpm run dev:h5
# 构建 H5 应用
pnpm run build:h5
# ESLint 检查并自动修复
pnpm run lint:eslint
# Prettier 格式化
pnpm run lint:prettier
# Stylelint 检查样式
pnpm run lint:stylelint
# TypeScript 类型检查
pnpm run type-check
```
---
## 🙏 鸣谢
- [uni-app](https://uniapp.dcloud.net.cn/) - 跨平台应用开发框架
- [Vue 3](https://cn.vuejs.org/) - 渐进式 JavaScript 框架(Composition API)
- [Vite](https://cn.vitejs.dev/) - 下一代前端构建工具
- [uni-helper](https://github.com/uni-helper) - 感谢 uni-helper 团队为 uni-app 开发体验优化做出的贡献。
- [vitesse-uni-app](https://github.com/uni-helper/vitesse-uni-app) - 感谢 vitesse-uni-app 提供的快速起手项目。
- [uni-ku](https://uni-ku.js.org/) - 感谢 uni-ku 团队为 uni-app 插件生态做出的贡献。
---
## 🌟 贡献者们
感谢以下所有给 FastApp 贡献过代码的 [开发者](https://github.com/FastapiAdmin/FastApp/graphs/contributors)。
<a href="https://github.com/FastapiAdmin/FastApp/graphs/contributors">
<img src="https://contrib.rocks/image?repo=FastapiAdmin/FastApp" />
</a>
---
## 🎨 社区交流
| 群组二维码 | 微信支付二维码 |
| --- | --- |
| ![群组二维码](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/group.jpg) | ![微信支付二维码](https://gitee.com/fastapiadmin/FastDocs/raw/master/docs/public/wechatPay.jpg) |
---
## 📄 许可证
本项目采用 [MIT](LICENSE) 许可证。
[![Star History Chart](https://api.star-history.com/svg?repos=FastapiAdmin/FastApp&type=Date)](https://star-history.com/#FastapiAdmin/FastApp&Date)
<div align="center">
**如果这个项目对你有帮助,请给一个 ⭐ Star**
Made with ❤️ by FastApp Team
</div>
+84
View File
@@ -0,0 +1,84 @@
/* eslint-disable no-irregular-whitespace */
import type { Config } from '@alova/wormhole'
// For more config detailed visit:
// https://alova.js.org/tutorial/getting-started/extension-integration
export default <Config>{
generator: [
{
/**
* file input. support:
* 1. openapi json file url
* 2. local file
*/
input: 'https://petstore3.swagger.io/api/v3/openapi.json',
/**
* input file platform. Currently only swagger is supported.
* When this parameter is specified, the input field only needs to specify the document address without specifying the openapi file
*/
platform: 'swagger',
/**
* output path of interface file and type file.
* Multiple generators cannot have the same address, otherwise the generated code will overwrite each other.
*/
output: 'src/api',
/**
* the mediaType of the generated response data. default is `application/json`
*/
responseMediaType: 'application/json',
/**
* the bodyMediaType of the generated request body data. default is `application/json`
*/
bodyMediaType: 'application/json',
/**
* the generated api version. options are `2` or `3`, default is `auto`.
*/
version: 3,
/**
* type of generated code. The options ​​are `auto/ts/typescript/module/commonjs`.
*/
type: 'typescript',
/**
* exported global api name, you can access the generated api globally through this name, default is `Apis`.
* it is required when multiple generators are configured, and it cannot be repeated
*/
global: 'Apis',
/**
* filter or convert the generated api information, return an apiDescriptor, if this function is not specified, the apiDescripor object is not converted
*/
handleApi: (apiDescriptor) => {
// Skip logging to console
// console.log(apiDescriptor)
// Filter out any deprecated APIs if needed
if (apiDescriptor.deprecated) {
return undefined // Skip this API
}
// You can transform the API descriptor here if needed
// For example, add custom headers, modify parameters, etc.
return apiDescriptor
},
},
],
/**
* extension only
* whether to automatically update the interface, enabled by default, check every 5 minutes, closed when set to `false`
*/
autoUpdate: {
// Update when editor is launched
launchEditor: true,
// Check for updates every 5 minutes
interval: 5 * 60 * 1000,
},
}
+12
View File
@@ -0,0 +1,12 @@
/*
* @Author: weisheng
* @Date: 2024-10-29 20:12:08
* @LastEditTime: 2024-10-29 20:12:20
* @LastEditors: weisheng
* @Description:
* @FilePath: \salary-calculator\commitlint.config.js
* 记得注释
*/
export default {
extends: ['@commitlint/config-conventional'],
}
+17
View File
@@ -0,0 +1,17 @@
import uni from '@uni-helper/eslint-config'
export default uni(
{
unocss: true,
rules: {
'no-console': 'off',
'eslint-comments/no-unlimited-disable': 'off',
},
ignores: [
'src/uni_modules/**/*',
'docs/.vitepress/dist',
'docs/.vitepress/cache',
'**/*.md',
],
},
)
+21
View File
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="./public/favicon.png" type="image/x-icon" />
<script>
const coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)')
|| CSS.supports('top: constant(a)'))
document.write(
`<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0${
coverSupport ? ', viewport-fit=cover' : ''}" />`)
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+98
View File
@@ -0,0 +1,98 @@
/*
* @Author: weisheng
* @Date: 2025-08-28 20:59:43
* @LastEditTime: 2025-11-17 14:28:09
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/manifest.config.ts
* 记得注释
*/
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
export default defineManifestConfig({
'name': 'wot-starter',
'appid': '__UNI__1208592',
'description': '',
'versionName': '1.0.0',
'versionCode': '100',
'transformPx': false,
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
permissions: [
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.READ_LOGS"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.CAMERA"/>',
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
'<uses-feature android:name="android.hardware.camera"/>',
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
],
},
/* ios打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {},
},
},
/* 快应用特有相关 */
'quickapp': {},
/* 小程序特有相关 */
'mp-weixin': {
optimization: {
subPackages: true,
},
appid: '',
setting: {
urlCheck: false,
},
usingComponents: true,
darkmode: true,
themeLocation: 'theme.json',
},
'app-harmony': {},
'mp-harmony': {},
'mp-alipay': {
usingComponents: true,
compileOptions: {
globalObjectMode: 'enable',
treeShaking: true,
},
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'h5': {
darkmode: true,
themeLocation: 'theme.json',
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
})
+148
View File
@@ -0,0 +1,148 @@
{
"name": "fastapp",
"type": "module",
"version": "1.4.0",
"private": true,
"packageManager": "pnpm@9.9.0",
"license": "MIT",
"engines": {
"node": ">=20.19.0 || >=22.12.0 || >=24.0.0"
},
"scripts": {
"dev": "uni",
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:h5:development": "uni --mode development",
"dev:h5:test": "uni --mode test",
"dev:h5:production": "uni --mode production",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build": "uni build",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:h5:development": "uni build --mode development",
"build:h5:test": "uni build --mode test",
"build:h5:production": "uni build --mode production",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:docs": "eslint docs --ext .js,.ts,.vue,.mts,.cts --ignore-pattern '**/*.md'",
"prepare": "husky install",
"commit": "git-cz",
"release-major": "standard-version --release-as major",
"release-minor": "standard-version --release-as minor",
"release-patch": "standard-version --release-as patch",
"alova-gen": "alova gen -f",
"docs:dev": "pnpm -F docs docs:dev",
"docs:build": "pnpm run build:h5 && pnpm -F docs docs:build",
"docs:preview": "pnpm -F docs docs:preview"
},
"dependencies": {
"@alova/adapter-uniapp": "^2.0.14",
"@alova/mock": "^2.0.17",
"@alova/shared": "^1.3.1",
"@dcloudio/uni-app": "3.0.0-4080720251210001",
"@dcloudio/uni-app-harmony": "3.0.0-4080720251210001",
"@dcloudio/uni-app-plus": "3.0.0-4080720251210001",
"@dcloudio/uni-components": "3.0.0-4080720251210001",
"@dcloudio/uni-h5": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-alipay": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-baidu": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-harmony": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-jd": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-lark": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-qq": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-weixin": "3.0.0-4080720251210001",
"@dcloudio/uni-mp-xhs": "3.0.0-4080720251210001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4080720251210001",
"@vueuse/core": "^11.0.3",
"@wot-ui/router": "^1.1.2",
"alova": "^3.3.4",
"echarts": "^6.0.0",
"pinia": "^2.3.1",
"uni-echarts": "^2.2.5",
"vue": "~3.4.38",
"vue-i18n": "^9.14.0",
"wot-design-uni": "^1.14.0"
},
"devDependencies": {
"@alova/wormhole": "^1.1.2",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@dcloudio/types": "^3.4.12",
"@dcloudio/uni-automator": "3.0.0-4080720251210001",
"@dcloudio/uni-cli-shared": "3.0.0-4080720251210001",
"@dcloudio/uni-stacktracey": "3.0.0-4080720251210001",
"@dcloudio/uni-vue-devtools": "3.0.0-4020420240722002",
"@dcloudio/vite-plugin-uni": "3.0.0-4080720251210001",
"@iconify-json/carbon": "^1.1.37",
"@mini-types/alipay": "^3.0.14",
"@types/node": "^20.16.2",
"@uni-helper/eslint-config": "^0.5.0",
"@uni-helper/plugin-uni": "^0.1.0",
"@uni-helper/uni-env": "^0.2.0",
"@uni-helper/uni-types": "1.0.0-alpha.6",
"@uni-helper/unocss-preset-uni": "^0.2.11",
"@uni-helper/vite-plugin-uni-components": "^0.2.6",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.11",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.12",
"@uni-helper/vite-plugin-uni-pages": "^0.3.23",
"@uni-helper/volar-service-uni-pages": "^0.3.23",
"@uni-ku/bundle-optimizer": "^2.1.0",
"@uni-ku/root": "^1.4.1",
"@unocss/eslint-config": "^66.5.6",
"@vue/runtime-core": "^3.4.38",
"@vue/tsconfig": "^0.5.1",
"baseline-browser-mapping": "^2.9.19",
"commitizen": "^4.3.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9.39.1",
"git-cz": "^4.9.0",
"husky": "^8.0.3",
"lint-staged": "^15.2.9",
"miniprogram-api-typings": "^3.12.3",
"sass": "1.78.0",
"standard-version": "^9.5.0",
"typescript": "~5.5.4",
"unocss": "66.0.0",
"unplugin-auto-import": "^0.18.2",
"vite": "^5.2.8",
"vue-tsc": "^2.0.29"
},
"pnpm": {
"overrides": {
"unconfig": "7.3.2"
}
},
"lint-staged": {
"*": "eslint --fix"
}
}
+54
View File
@@ -0,0 +1,54 @@
/*
* @Author: weisheng
* @Date: 2025-06-23 22:23:05
* @LastEditTime: 2025-06-27 13:04:54
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/pages.config.ts
* 记得注释
*/
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
export default defineUniPages({
pages: [],
globalStyle: {
// 导航栏配置
navigationBarBackgroundColor: '@navBgColor',
navigationBarTextStyle: '@navTxtStyle',
navigationBarTitleText: 'Wot Starter',
// 页面背景配置
backgroundColor: '@bgColor',
backgroundTextStyle: '@bgTxtStyle',
backgroundColorTop: '@bgColorTop',
backgroundColorBottom: '@bgColorBottom',
// 下拉刷新配置
enablePullDownRefresh: false,
onReachBottomDistance: 50,
// 动画配置
animationType: 'pop-in',
animationDuration: 300,
},
tabBar: {
custom: true,
// #ifdef MP-ALIPAY
customize: true,
// 暂时不生效。4.71.2025061206-alpha已修复:https://uniapp.dcloud.net.cn/release-note-alpha.html#_4-71-2025061206-alpha,我们等正式版发布后更新。
overlay: true,
// #endif
height: '0',
color: '@tabColor',
selectedColor: '@tabSelectedColor',
backgroundColor: '@tabBgColor',
borderStyle: '@tabBorderStyle',
list: [{
pagePath: 'pages/index/index',
}, {
pagePath: 'pages/work/index',
}, {
pagePath: 'pages/mine/index',
}],
},
})
+3
View File
@@ -0,0 +1,3 @@
packages:
- packages/*
- apps/*
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

+3
View File
@@ -0,0 +1,3 @@
{
"extends": ["github>uni-helper/renovate-config"]
}
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
const { themeVars, theme } = useManualTheme()
</script>
<template>
<wd-config-provider :theme-vars="themeVars" :theme="theme" :custom-class="`page-wraper ${theme}`">
<ku-root-view />
<wd-notify />
<wd-message-box />
<wd-toast />
<global-loading />
<global-toast />
<global-message />
<!-- #ifdef MP-WEIXIN -->
<privacy-popup />
<!-- #endif -->
<!-- 文字水印 -->
<wd-watermark content="fastapp" :width="130" :height="130" />
</wd-config-provider>
</template>
+25
View File
@@ -0,0 +1,25 @@
<script setup lang="ts">
onLaunch((options) => {
console.log('App.vue onLaunch', options)
})
onShow((options) => {
console.log('App.vue onShow', options)
})
onHide(() => {
console.log('App Hide')
})
</script>
<style lang="scss">
.page-wraper {
min-height: calc(100vh - var(--window-top));
box-sizing: border-box;
background: #f9f9f9;
}
.wot-theme-dark.page-wraper {
background: #222;
}
</style>
+46
View File
@@ -0,0 +1,46 @@
/// <reference types='./globals.d.ts' />
/* tslint:disable */
/* eslint-disable */
/**
* Swagger Petstore - OpenAPI 3.0 - version 1.0.27
*
* This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we&#x27;ve switched to the design first approach!
You can now help us improve the API whether it&#x27;s by making changes to the definition itself or to the code.
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
Some useful links:
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
*
* OpenAPI version: 3.0.4
*
* Contact:
*
* NOTE: This file is auto generated by the alova's vscode plugin.
*
* https://alova.js.org/devtools/vscode
*
* **Do not edit the file manually.**
*/
export default {
'pet.updatePet': ['PUT', '/pet'],
'pet.addPet': ['POST', '/pet'],
'pet.findPetsByStatus': ['GET', '/pet/findByStatus'],
'pet.findPetsByTags': ['GET', '/pet/findByTags'],
'pet.getPetById': ['GET', '/pet/{petId}'],
'pet.updatePetWithForm': ['POST', '/pet/{petId}'],
'pet.deletePet': ['DELETE', '/pet/{petId}'],
'pet.uploadFile': ['POST', '/pet/{petId}/uploadImage'],
'store.getInventory': ['GET', '/store/inventory'],
'store.placeOrder': ['POST', '/store/order'],
'store.getOrderById': ['GET', '/store/order/{orderId}'],
'store.deleteOrder': ['DELETE', '/store/order/{orderId}'],
'user.createUser': ['POST', '/user'],
'user.createUsersWithListInput': ['POST', '/user/createWithList'],
'user.loginUser': ['GET', '/user/login'],
'user.logoutUser': ['GET', '/user/logout'],
'user.getUserByName': ['GET', '/user/{username}'],
'user.updateUser': ['PUT', '/user/{username}'],
'user.deleteUser': ['DELETE', '/user/{username}']
};
+84
View File
@@ -0,0 +1,84 @@
import { ApiHeader } from '@/enums/api-header.enum'
import { http } from '@/http'
const AUTH_BASE_URL = '/system/auth'
const AuthAPI = {
/**
* 登录
* @param body 登录表单数据
* @returns 登录结果
*/
login(body: LoginFormData): Promise<LoginResult> {
return http.Post(`${AUTH_BASE_URL}/login`, body, {
headers: {
[ApiHeader.KEY]: ApiHeader.FORM,
},
})
},
/**
* 刷新令牌
* @param body 刷新令牌请求体
* @returns 新的访问令牌
*/
refreshToken(body: RefreshToekenBody): Promise<any> {
return http.Post(`${AUTH_BASE_URL}/token/refresh`, body)
},
/**
* 获取验证码
* @returns 验证码信息
*/
getCaptcha(): Promise<any> {
// 添加随机参数防止缓存
const timestamp = new Date().getTime()
return http.Get(`${AUTH_BASE_URL}/captcha/get?timestamp=${timestamp}`)
},
/**
* 登出
* @param body 登出请求体
* @returns 登出结果
*/
logout(body: LogoutBody): Promise<any> {
return http.Post(`${AUTH_BASE_URL}/logout`, body)
},
}
export default AuthAPI
/** 登录表单数据 */
export interface LoginFormData {
username: string
password: string
captcha_key: string
captcha: string
remember: boolean
login_type: string
}
// 刷新令牌
export interface RefreshToekenBody {
refresh_token: string
}
/** 登录响应 */
export interface LoginResult {
access_token: string
refresh_token: string
token_type: string
expires_in: number
}
/** 验证码信息 */
export interface CaptchaInfo {
enable: boolean
key: string
img_base: string
}
/** 退出登录操作 */
export interface LogoutBody {
token: string
}
+106
View File
@@ -0,0 +1,106 @@
/*
* @Author: weisheng
* @Date: 2025-04-17 15:58:11
* @LastEditTime: 2025-06-15 21:47:22
* @LastEditors: weisheng
* @Description: Alova response and error handlers
* @FilePath: /wot-starter/src/api/core/handlers.ts
*/
import type { Method } from 'alova'
import router from '@/router'
// Custom error class for API errors
export class ApiError extends Error {
code: number
data?: any
constructor(message: string, code: number, data?: any) {
super(message)
this.name = 'ApiError'
this.code = code
this.data = data
}
}
// Define a type for the expected API response structure
interface ApiResponse {
code: number
msg?: string
data?: any
success?: boolean
total?: number
more?: boolean
}
// Handle successful responses
export async function handleAlovaResponse(
response: UniApp.RequestSuccessCallbackResult | UniApp.UploadFileSuccessCallbackResult | UniApp.DownloadSuccessData,
) {
const globalToast = useGlobalToast()
// Extract status code and data from UniApp response
const { statusCode, data } = response as UniNamespace.RequestSuccessCallbackResult
// 处理401/403错误(如果不是在handleAlovaResponse中处理的)
if ((statusCode === 401 || statusCode === 403)) {
// 如果是未授权错误,清除用户信息并跳转到登录页
globalToast.error({ msg: '登录已过期,请重新登录!', duration: 500 })
const timer = setTimeout(() => {
clearTimeout(timer)
router.replaceAll({ name: 'login' })
}, 500)
throw new ApiError('登录已过期,请重新登录!', statusCode, data)
}
// Handle HTTP error status codes
if (statusCode >= 400) {
globalToast.error(`Request failed with status: ${statusCode}`)
throw new ApiError(`Request failed with status: ${statusCode}`, statusCode, data)
}
// The data is already parsed by UniApp adapter
const json = data as ApiResponse
// Log response in development
if (import.meta.env.MODE === 'development') {
console.log('[Alova Response]', json)
}
// Return data for successful responses
return json
}
// Handle request errors
export function handleAlovaError(error: any, method: Method) {
const globalToast = useGlobalToast()
// Log error in development
if (import.meta.env.MODE === 'development') {
console.error('[Alova Error]', error, method)
}
// 处理401/403错误(如果不是在handleAlovaResponse中处理的)
if (error instanceof ApiError && (error.code === 401 || error.code === 403)) {
// 如果是未授权错误,清除用户信息并跳转到登录页
globalToast.error({ msg: '登录已过期,请重新登录!', duration: 500 })
const timer = setTimeout(() => {
clearTimeout(timer)
router.replaceAll({ name: 'login' })
}, 500)
throw new ApiError('登录已过期,请重新登录!', error.code, error.data)
}
// Handle different types of errors
if (error.name === 'NetworkError') {
globalToast.error('网络错误,请检查您的网络连接')
}
else if (error.name === 'TimeoutError') {
globalToast.error('请求超时,请重试')
}
else if (error instanceof ApiError) {
globalToast.error(error.message || '请求失败')
}
else {
globalToast.error('发生意外错误')
}
throw error
}
+55
View File
@@ -0,0 +1,55 @@
import AdapterUniapp from '@alova/adapter-uniapp'
import { createAlova } from 'alova'
import vueHook from 'alova/vue'
import mockAdapter from '../mock/mockAdapter'
import { handleAlovaError, handleAlovaResponse } from './handlers'
export const alovaInstance = createAlova({
baseURL: import.meta.env.VITE_API_BASE_URL || 'https://petstore3.swagger.io/api/v3',
...AdapterUniapp({
mockRequest: mockAdapter,
}),
statesHook: vueHook,
beforeRequest: (method) => {
// Add content type for POST/PUT/PATCH requests
if (['POST', 'PUT', 'PATCH'].includes(method.type)) {
method.config.headers['Content-Type'] = 'application/json'
}
// Add timestamp to prevent caching for GET requests
if (method.type === 'GET' && CommonUtil.isObj(method.config.params)) {
method.config.params._t = Date.now()
}
// Log request in development
if (import.meta.env.MODE === 'development') {
console.log(`[Alova Request] ${method.type} ${method.url}`, method.data || method.config.params)
console.log(`[API Base URL] ${import.meta.env.VITE_API_BASE_URL}`)
console.log(`[Environment] ${import.meta.env.VITE_ENV_NAME}`)
}
},
// Response handlers
responded: {
// Success handler
onSuccess: handleAlovaResponse,
// Error handler
onError: handleAlovaError,
// Complete handler - runs after success or error
onComplete: async () => {
// Any cleanup or logging can be done here
},
},
// We'll use the middleware in the hooks
// middleware is not directly supported in createAlova options
// Default request timeout (10 seconds)
timeout: 60000,
// 设置为null即可全局关闭全部请求缓存
cacheFor: null,
})
export default alovaInstance
+92
View File
@@ -0,0 +1,92 @@
/**
* 延迟加载中间件
* 延迟显示加载状态,防止快速请求导致的闪烁
* @param delay 显示加载状态前的延迟时间(毫秒)
* @returns Alova 中间件
*/
export function createDelayLoadingMiddleware(delay = 300) {
return async (context: any, next: any) => {
context.controlLoading()
const { loading } = context.proxyStates
const timer = setTimeout(() => {
loading.v = true
}, delay)
await next()
loading.v = false
clearTimeout(timer)
}
}
/**
* 全局加载中间件
* 为所有请求显示全局加载指示器,支持延迟显示
*
* 使用示例:
* ```typescript
* // 1. 基本用法
* const { send: submit } = useRequest(method, {
* middleware: createGlobalLoadingMiddleware()
* });
*
* // 2. 自定义延迟时间和加载文本
* const { send: submit } = useRequest(method, {
* middleware: createGlobalLoadingMiddleware({
* delay: 500, // 延迟 500ms 显示加载指示器,防止闪烁
* loadingText: '正在提交...', // 自定义加载文本
* })
* });
* ```
*
* @param options 加载选项
* @param options.delay 显示加载指示器前的延迟时间(毫秒),默认 300ms
* @param options.loadingText 加载指示器显示的文本,默认为 'Loading...'
* @returns Alova 中间件
*/
export function createGlobalLoadingMiddleware(options: {
delay?: number
loadingText?: string
} = {}) {
const {
delay = 0,
loadingText = 'Loading...',
} = options
return async (ctx: any, next: any) => {
// 自行控制loading
ctx.controlLoading()
const globalLoading = useGlobalLoading()
let timer: ReturnType<typeof setTimeout> | null = null
// 如果delay为0或未设置,直接显示loading
if (delay <= 0) {
globalLoading.loading(loadingText)
}
else {
// 延迟特定时间显示全局loading
timer = setTimeout(() => {
globalLoading.loading(loadingText)
}, delay)
}
try {
await next()
}
finally {
// 清除定时器并关闭loading
if (timer) {
clearTimeout(timer)
}
globalLoading.close()
}
}
}
// 导出延迟加载中间件作为默认中间件
export const defaultMiddleware = createDelayLoadingMiddleware()
export default defaultMiddleware
+101
View File
@@ -0,0 +1,101 @@
/* tslint:disable */
/* eslint-disable */
/**
* Swagger Petstore - OpenAPI 3.0 - version 1.0.27
*
* This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we&#x27;ve switched to the design first approach!
You can now help us improve the API whether it&#x27;s by making changes to the definition itself or to the code.
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
Some useful links:
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
*
* OpenAPI version: 3.0.4
*
* Contact:
*
* NOTE: This file is auto generated by the alova's vscode plugin.
*
* https://alova.js.org/devtools/vscode
*
* **Do not edit the file manually.**
*/
import type { Alova, MethodType, AlovaGenerics, AlovaMethodCreateConfig } from 'alova';
import { Method } from 'alova';
import apiDefinitions from './apiDefinitions';
const createFunctionalProxy = (array: (string | symbol)[], alovaInstance: Alova<AlovaGenerics>, configMap: any) => {
// create a new proxy instance
return new Proxy(function () {}, {
get(_, property) {
// record the target property, so that it can get the completed accessing paths
const newArray = [...array, property];
// always return a new proxy to continue recording accessing paths.
return createFunctionalProxy(newArray, alovaInstance, configMap);
},
apply(_, __, [config]) {
const apiPathKey = array.join('.') as keyof typeof apiDefinitions;
const apiItem = apiDefinitions[apiPathKey];
if (!apiItem) {
throw new Error(`the api path of \`${apiPathKey}\` is not found`);
}
const mergedConfig = {
...configMap[apiPathKey],
...config
};
const [method, url] = apiItem;
const pathParams = mergedConfig.pathParams;
const urlReplaced = url.replace(/\{([^}]+)\}/g, (_, key) => {
const pathParam = pathParams[key];
return pathParam;
});
delete mergedConfig.pathParams;
let data = mergedConfig.data;
if (Object.prototype.toString.call(data) === '[object Object]' && typeof FormData !== 'undefined') {
let hasBlobData = false;
const formData = new FormData();
for (const key in data) {
formData.append(key, data[key]);
if (data[key] instanceof Blob) {
hasBlobData = true;
}
}
data = hasBlobData ? formData : data;
}
return new Method(method.toUpperCase() as MethodType, alovaInstance, urlReplaced, mergedConfig, data);
}
});
};
export const createApis = (alovaInstance: Alova<AlovaGenerics>, configMap: any) => {
const Apis = new Proxy({} as Apis, {
get(_, property) {
return createFunctionalProxy([property], alovaInstance, configMap);
}
});
return Apis;
};
export const mountApis = (Apis: Apis) => {
// define global variable `Apis`
(globalThis as any).Apis = Apis;
};
type MethodConfig<T> = AlovaMethodCreateConfig<
(typeof import('./index'))['alovaInstance'] extends Alova<infer AG> ? AG : any,
any,
T
>;
type APISofParameters<Tag extends string, Url extends string> = Tag extends keyof Apis
? Url extends keyof Apis[Tag]
? Apis[Tag][Url] extends (...args: any) => any
? Parameters<Apis[Tag][Url]>
: any
: any
: any;
type MethodsConfigMap = {
[P in keyof typeof import('./apiDefinitions').default]?: MethodConfig<
P extends `${infer Tag}.${infer Url}` ? Parameters<NonNullable<APISofParameters<Tag, Url>[0]>['transform']>[0] : any
>;
};
export const withConfigType = <Config extends MethodsConfigMap>(config: Config) => config;
+1058
View File
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
// Import the core alova instance
import alovaInstance from './core/instance'
// Export the global Apis object from the generated code
import { createApis, withConfigType } from './createApis'
// Export the alova instance for direct use if needed
export { alovaInstance }
// Configure method options for specific APIs
export const $$userConfigMap = withConfigType({})
// Create the global Apis object
const Apis = createApis(alovaInstance, $$userConfigMap)
// Export both default and named export for AutoImport
export default Apis
export { Apis }
+107
View File
@@ -0,0 +1,107 @@
# API Mock 数据
本目录包含了项目中使用的 API 模拟数据,用于开发和测试环境。
## 目录结构
```
mock/
├── modules/ # 按模块分类的模拟数据
│ ├── common.ts # 通用模拟处理
│ ├── loginInterface.ts # 登录相关接口的模拟数据
│ ├── mdataInterface.ts # 主数据相关接口的模拟数据
│ ├── vehsaleusesignInterface.ts # 车销使用签收相关接口的模拟数据
│ ├── vehsaleusesignarvInterface.ts # 车销使用签收到货相关接口的模拟数据
│ ├── user.ts # 用户相关的模拟数据
│ ├── university.ts # 大学相关的模拟数据
│ └── feedback.ts # 反馈相关的模拟数据
├── utils/ # 工具目录
│ ├── index.ts # 工具导出文件
│ └── generators.ts # 模拟数据生成工具
└── mockAdapter.ts # 模拟适配器配置
```
## 使用方法
模拟数据已经通过 `@alova/mock` 和 `@alova/adapter-uniapp` 集成到项目中。在开发环境中,API 请求会自动使用模拟数据进行响应。
### 启用/禁用模拟
在 `mockAdapter.ts` 中,可以通过修改 `enable` 选项来启用或禁用模拟:
```typescript
const mockAdapter = createAlovaMockAdapter(allMocks, {
// ...
enable: true, // 设置为 false 可禁用模拟
// ...
})
```
### 添加新的模拟数据
1. 在 `modules` 目录下创建新的模块文件或在现有文件中添加
2. 使用 `defineMock` 函数定义模拟数据
3. 在 `mockAdapter.ts` 中导入并添加到 `allMocks` 数组中
示例:
```typescript
// modules/example.ts
import { defineMock } from '@alova/mock'
// mockAdapter.ts
import exampleMocks from './modules/example'
export default defineMock({
'[GET]/api/example': () => {
return {
code: 200,
data: { /* 模拟数据 */ },
message: 'success'
}
}
})
const allMocks = [
// ...
exampleMocks
]
```
## 模拟数据生成工具
在 `utils/generators.ts` 中提供了一系列用于生成模拟数据的工具函数,可以在各个模块中复用:
```typescript
import { generateMockData } from '../utils'
// 生成随机ID
const id = generateMockData.id()
// 生成随机名称
const name = generateMockData.name('前缀')
// 生成随机数组
const array = generateMockData.array(index => ({
id: generateMockData.id(),
name: generateMockData.name(`项目${index}`)
}), 10)
// 生成基础响应对象
const response = generateMockData.baseResponse(data)
// 生成列表响应对象
const listResponse = generateMockData.listResponse(items, total, more)
// 生成业务对象
const user = generateMockData.user()
const goods = generateMockData.goods(0)
const vehSaleEmp = generateMockData.vehSaleEmp(0)
```
## 注意事项
1. 模拟数据应尽量接近真实数据结构,以便于开发和测试
2. 对于需要保持一致性的数据(如ID引用),可以使用固定值而非随机生成
3. 可以使用请求参数来定制模拟响应,例如分页、筛选等
4. 模拟数据应包含各种场景,包括成功和失败的情况
+436
View File
@@ -0,0 +1,436 @@
import Apis from '@/api'
/**
* Pet模块演示
* 演示宠物相关API的使用
*/
export class PetDemo {
// 获取所有可用的宠物
static async getAvailablePets() {
console.log('=== 获取可用宠物 ===')
try {
const pets = await Apis.pet.findPetsByStatus({
params: { status: 'available' },
}).send()
console.log('可用宠物列表:', pets)
return pets
}
catch (error) {
console.error('获取宠物失败:', error)
}
}
// 获取特定宠物信息
static async getPetById(petId: number) {
console.log(`=== 获取宠物 ${petId} 的信息 ===`)
try {
const pet = await Apis.pet.getPetById({
pathParams: { petId },
}).send()
console.log('宠物信息:', pet)
return pet
}
catch (error) {
console.error('获取宠物信息失败:', error)
}
}
// 添加新宠物
static async addNewPet() {
console.log('=== 添加新宠物 ===')
try {
const newPet = await Apis.pet.addPet({
data: {
name: 'Buddy',
category: { id: 1, name: 'Dogs' },
status: 'available',
photoUrls: ['https://example.com/buddy.jpg'],
tags: [
{ id: 1, name: 'friendly' },
{ id: 2, name: 'trained' },
],
},
}).send()
console.log('新添加的宠物:', newPet)
return newPet
}
catch (error) {
console.error('添加宠物失败:', error)
}
}
// 测试宠物不存在的情况
static async testPetNotFound() {
console.log('=== 测试宠物不存在的情况 ===')
try {
await Apis.pet.getPetById({
pathParams: { petId: 404 },
}).send()
}
catch (error) {
console.log('预期的404错误:', error)
}
}
// 删除宠物(需要API密钥)
static async deletePet(petId: number, apiKey: string = 'special-key') {
console.log(`=== 删除宠物 ${petId} ===`)
try {
const result = await Apis.pet.deletePet({
pathParams: { petId },
headers: { api_key: apiKey },
}).send()
console.log('删除结果:', result)
return result
}
catch (error) {
console.error('删除宠物失败:', error)
}
}
}
/**
* Store模块演示
* 演示商店相关API的使用
*/
export class StoreDemo {
// 获取库存信息
static async getInventory() {
console.log('=== 获取库存信息 ===')
try {
const inventory = await Apis.store.getInventory().send()
console.log('库存信息:', inventory)
return inventory
}
catch (error) {
console.error('获取库存失败:', error)
}
}
// 下单购买宠物
static async placeOrder(petId: number, quantity: number = 1) {
console.log('=== 下单购买宠物 ===')
try {
const order = await Apis.store.placeOrder({
data: {
petId,
quantity,
shipDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7天后发货
status: 'placed',
complete: false,
},
}).send()
console.log('订单信息:', order)
return order
}
catch (error) {
console.error('下单失败:', error)
}
}
// 获取订单信息
static async getOrderById(orderId: number) {
console.log(`=== 获取订单 ${orderId} 信息 ===`)
try {
const order = await Apis.store.getOrderById({
pathParams: { orderId },
}).send()
console.log('订单详情:', order)
return order
}
catch (error) {
console.error('获取订单失败:', error)
}
}
// 测试无效订单ID
static async testInvalidOrderId() {
console.log('=== 测试无效订单ID ===')
try {
await Apis.store.getOrderById({
pathParams: { orderId: 999 },
}).send()
}
catch (error) {
console.log('预期的400错误:', error)
}
}
// 删除订单
static async deleteOrder(orderId: number) {
console.log(`=== 删除订单 ${orderId} ===`)
try {
const result = await Apis.store.deleteOrder({
pathParams: { orderId },
}).send()
console.log('删除结果:', result)
return result
}
catch (error) {
console.error('删除订单失败:', error)
}
}
}
/**
* User模块演示
* 演示用户相关API的使用
*/
export class UserDemo {
// 用户登录
static async login(username: string = 'admin', password: string = 'admin') {
console.log('=== 用户登录 ===')
try {
const loginResult = await Apis.user.loginUser({
params: { username, password },
}).send()
console.log('登录结果:', loginResult)
return loginResult
}
catch (error) {
console.error('登录失败:', error)
}
}
// 获取用户信息
static async getUserInfo(username: string) {
console.log(`=== 获取用户 ${username} 信息 ===`)
try {
const user = await Apis.user.getUserByName({
pathParams: { username },
}).send()
console.log('用户信息:', user)
return user
}
catch (error) {
console.error('获取用户信息失败:', error)
}
}
// 创建新用户
static async createUser(userData: any = {}) {
console.log('=== 创建新用户 ===')
try {
const newUser = await Apis.user.createUser({
data: {
username: 'newuser',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
password: 'password123',
phone: '1234567890',
userStatus: 1,
...userData,
},
}).send()
console.log('新用户:', newUser)
return newUser
}
catch (error) {
console.error('创建用户失败:', error)
}
}
// 更新用户信息
static async updateUser(username: string, updateData: any) {
console.log(`=== 更新用户 ${username} 信息 ===`)
try {
const updatedUser = await Apis.user.updateUser({
pathParams: { username },
data: updateData,
}).send()
console.log('更新后的用户:', updatedUser)
return updatedUser
}
catch (error) {
console.error('更新用户失败:', error)
}
}
// 测试用户不存在的情况
static async testUserNotFound() {
console.log('=== 测试用户不存在的情况 ===')
try {
await Apis.user.getUserByName({
pathParams: { username: 'notfound' },
}).send()
}
catch (error) {
console.log('预期的404错误:', error)
}
}
// 批量创建用户
static async createUsersWithArray() {
console.log('=== 批量创建用户(数组) ===')
try {
const result = await Apis.user.createUsersWithListInput({
data: [
{
username: 'user1',
firstName: 'User',
lastName: 'One',
email: 'user1@example.com',
password: 'password',
phone: '1111111111',
userStatus: 1,
},
{
username: 'user2',
firstName: 'User',
lastName: 'Two',
email: 'user2@example.com',
password: 'password',
phone: '2222222222',
userStatus: 1,
},
],
}).send()
console.log('批量创建结果:', result)
return result
}
catch (error) {
console.error('批量创建用户失败:', error)
}
}
// 用户登出
static async logout() {
console.log('=== 用户登出 ===')
try {
const result = await Apis.user.logoutUser().send()
console.log('登出结果:', result)
return result
}
catch (error) {
console.error('登出失败:', error)
}
}
}
/**
* 综合演示
* 演示完整的业务流程
*/
export class FullDemo {
// 完整的宠物商店购买流程
static async completePetStorePurchaseFlow() {
console.log('\n🎯 开始完整的宠物商店购买流程演示\n')
// 1. 用户登录
const loginResult = await UserDemo.login('admin', 'admin')
if (!loginResult)
return
// 2. 查看库存
const inventory = await StoreDemo.getInventory()
console.log('\n📦 当前库存状态:', inventory)
// 3. 浏览可用宠物
const availablePets = await PetDemo.getAvailablePets()
if (!availablePets || availablePets.length === 0) {
console.log('❌ 没有可用的宠物')
return
}
// 4. 选择第一个宠物
const selectedPet = availablePets[0]
console.log('\n🐕 选择的宠物:', selectedPet)
// 5. 查看宠物详情
if (selectedPet?.id) {
await PetDemo.getPetById(selectedPet.id)
}
// 6. 下单购买
const order = selectedPet?.id ? await StoreDemo.placeOrder(selectedPet.id, 1) : null
if (!order)
return
// 7. 查看订单详情
if (order?.id) {
await StoreDemo.getOrderById(order.id)
}
// 8. 获取用户信息
await UserDemo.getUserInfo('admin')
console.log('\n✅ 完整流程演示结束')
}
// 错误处理演示
static async errorHandlingDemo() {
console.log('\n⚠️ 开始错误处理演示\n')
// 测试各种错误情况
await PetDemo.testPetNotFound()
await StoreDemo.testInvalidOrderId()
await UserDemo.testUserNotFound()
console.log('\n✅ 错误处理演示结束')
}
// CRUD操作演示
static async crudDemo() {
console.log('\n🔄 开始CRUD操作演示\n')
// 创建
const newPet = await PetDemo.addNewPet()
const newUser = await UserDemo.createUser({ username: 'testuser' })
// 读取
if (newPet?.id)
await PetDemo.getPetById(newPet.id)
if (newUser)
await UserDemo.getUserInfo('testuser')
// 更新
if (newUser) {
await UserDemo.updateUser('testuser', {
firstName: 'Updated',
lastName: 'User',
})
}
// 删除
if (newPet?.id)
await PetDemo.deletePet(newPet.id)
console.log('\n✅ CRUD操作演示结束')
}
}
// 导出演示运行器
export async function runMockDemo() {
console.log('🚀 开始Mock数据演示\n')
try {
// 运行完整流程演示
await FullDemo.completePetStorePurchaseFlow()
// 运行错误处理演示
await FullDemo.errorHandlingDemo()
// 运行CRUD演示
await FullDemo.crudDemo()
console.log('\n🎉 所有演示完成!')
}
catch (error) {
console.error('演示过程中发生错误:', error)
}
}
// 如果直接运行此文件,执行演示
if (typeof window !== 'undefined') {
// 在浏览器环境中,可以在控制台调用
(window as any).runMockDemo = runMockDemo;
(window as any).PetDemo = PetDemo;
(window as any).StoreDemo = StoreDemo;
(window as any).UserDemo = UserDemo
console.log('Mock演示函数已加载到全局对象,可在控制台调用:')
console.log('- runMockDemo() - 运行完整演示')
console.log('- PetDemo.* - 宠物模块演示')
console.log('- StoreDemo.* - 商店模块演示')
console.log('- UserDemo.* - 用户模块演示')
}
+47
View File
@@ -0,0 +1,47 @@
/*
* @Author: weisheng
* @Date: 2025-04-17 16:21:36
* @LastEditTime: 2025-06-15 22:48:04
* @LastEditors: weisheng
* @Description: Mock适配器配置 - 集成所有模块的mock数据
* @FilePath: /wot-starter/src/api/mock/mockAdapter.ts
* 记得注释
*/
import { uniappMockResponse, uniappRequestAdapter } from '@alova/adapter-uniapp'
import { createAlovaMockAdapter } from '@alova/mock'
// 导入所有mock模块
import commonMocks from './modules/common'
import petMocks from './modules/pet'
import storeMocks from './modules/store'
import userMocks from './modules/user'
// 合并所有mock定义
const allMocks = [
commonMocks,
petMocks,
storeMocks,
userMocks,
]
// 创建mock适配器
const mockAdapter = createAlovaMockAdapter(allMocks, {
// 使用uniapp请求适配器处理非mock请求
httpAdapter: uniappRequestAdapter,
// 使用uniapp mock响应适配器
onMockResponse: uniappMockResponse,
// 根据环境变量启用/禁用mock
enable: true,
// 添加延迟以模拟网络延迟 (200-600ms)
delay: Math.random() * 400 + 200,
// 在开发环境下打印mock请求日志
mockRequestLogger: import.meta.env.MODE === 'development',
// 路径匹配模式 - 使用完整路径匹配
matchMode: 'pathname',
})
export default mockAdapter
@@ -0,0 +1,30 @@
/*
* @Author: weisheng
* @Date: 2023-05-20 10:00:00
* @LastEditTime: 2025-06-26 21:59:35
* @LastEditors: weisheng
* @Description: 通用mock处理
* @FilePath: /wot-starter/src/api/mock/modules/common.ts
*/
import { defineMock } from '@alova/mock'
import { generateMockData } from '../utils/generators'
export default defineMock({
// 通用GET请求处理
'[GET]/*': (_params: any, matchedUrl: string) => {
console.log(`[Mock] GET ${matchedUrl}`, _params)
return generateMockData.baseResponse({
message: `Mock response for GET ${matchedUrl}`,
params: _params,
})
},
// 通用POST请求处理
'[POST]/*': (_params: any, matchedUrl: string) => {
console.log(`[Mock] POST ${matchedUrl}`, _params)
return generateMockData.baseResponse({
message: `Mock response for POST ${matchedUrl}`,
params: _params,
})
},
})
+239
View File
@@ -0,0 +1,239 @@
/*
* @Author: weisheng
* @Date: 2025-06-15 14:25:00
* @LastEditTime: 2025-06-26 21:59:38
* @LastEditors: weisheng
* @Description: Pet Store - Pet相关接口的mock数据
* @FilePath: /wot-starter/src/api/mock/modules/pet.ts
*/
import { defineMock } from '@alova/mock'
import { generateMockData } from '../utils/generators'
// 宠物状态枚举
const PET_STATUS = ['available', 'pending', 'sold'] as const
type PetStatus = typeof PET_STATUS[number]
// 宠物类别
const PET_CATEGORIES = [
{ id: 1, name: 'Dogs' },
{ id: 2, name: 'Cats' },
{ id: 3, name: 'Birds' },
{ id: 4, name: 'Fish' },
{ id: 5, name: 'Reptiles' },
]
// 宠物标签
const PET_TAGS = [
{ id: 1, name: 'friendly' },
{ id: 2, name: 'playful' },
{ id: 3, name: 'calm' },
{ id: 4, name: 'energetic' },
{ id: 5, name: 'trained' },
{ id: 6, name: 'house-trained' },
]
// 生成宠物对象
function generatePet(id?: number, status?: PetStatus) {
const petId = id || generateMockData.number(1, 10000)
const category = PET_CATEGORIES[generateMockData.number(0, PET_CATEGORIES.length - 1)]
const tags = generateMockData.array(() => PET_TAGS[generateMockData.number(0, PET_TAGS.length - 1)], generateMockData.number(1, 3))
return {
id: petId,
category,
name: generateMockData.name('Pet'),
photoUrls: generateMockData.array(
index => `https://example.com/pet/${petId}/photo${index + 1}.jpg`,
generateMockData.number(1, 3),
),
tags,
status: status || PET_STATUS[generateMockData.number(0, PET_STATUS.length - 1)],
}
}
export default defineMock({
// 上传宠物图片
'[POST]/pet/{petId}/uploadImage': ({ params, data }) => {
console.log(`[Mock] POST /pet/${params.petId}/uploadImage`, data)
return {
code: 200,
type: 'success',
message: `Image uploaded successfully for pet ${params.petId}`,
data: {
petId: params.petId,
imageUrl: `https://example.com/pet/${params.petId}/uploaded-${Date.now()}.jpg`,
},
}
},
// 添加新宠物
'[POST]/pet': ({ data }) => {
console.log('[Mock] POST /pet', data)
const newPet = {
...data,
id: generateMockData.number(10001, 20000),
}
return newPet
},
// 更新宠物信息
'[PUT]/pet': ({ data }) => {
console.log('[Mock] PUT /pet', data)
if (!data.id) {
return {
status: 400,
body: {
code: 400,
message: 'Pet ID is required',
},
}
}
return {
...data,
updatedAt: generateMockData.datetime(),
}
},
// 根据状态查找宠物
'[GET]/pet/findByStatus': ({ query }) => {
console.log('[Mock] GET /pet/findByStatus', query)
const status = query.status as PetStatus
const validStatuses = Array.isArray(status) ? status : [status]
// 验证状态
const invalidStatuses = validStatuses.filter(s => !PET_STATUS.includes(s as PetStatus))
if (invalidStatuses.length > 0) {
return {
status: 400,
body: {
code: 400,
message: `Invalid status value: ${invalidStatuses.join(', ')}`,
},
}
}
// 生成符合状态的宠物列表
const pets = generateMockData.array(
index => generatePet(undefined, validStatuses[index % validStatuses.length] as PetStatus),
generateMockData.number(5, 15),
)
return pets
},
// 根据ID获取宠物
'[GET]/pet/{petId}': ({ params }) => {
console.log(`[Mock] GET /pet/${params.petId}`)
const petId = Number.parseInt(params.petId)
if (Number.isNaN(petId)) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid pet ID',
},
}
}
// 模拟宠物不存在的情况
if (petId === 404) {
return {
status: 404,
body: {
code: 404,
message: 'Pet not found',
},
}
}
return generatePet(petId)
},
// 使用表单数据更新宠物
'[POST]/pet/{petId}': ({ params, data }) => {
console.log(`[Mock] POST /pet/${params.petId}`, data)
const petId = Number.parseInt(params.petId)
if (Number.isNaN(petId)) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid pet ID',
},
}
}
// 模拟宠物不存在的情况
if (petId === 404) {
return {
status: 404,
body: {
code: 404,
message: 'Pet not found',
},
}
}
const updatedPet = {
...generatePet(petId),
...data,
updatedAt: generateMockData.datetime(),
}
return updatedPet
},
// 删除宠物
'[DELETE]/pet/{petId}': ({ params, headers }) => {
console.log(`[Mock] DELETE /pet/${params.petId}`, headers)
const petId = Number.parseInt(params.petId)
if (Number.isNaN(petId)) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid pet ID',
},
}
}
// 检查API密钥
if (!headers.api_key) {
return {
status: 401,
body: {
code: 401,
message: 'API key is required',
},
}
}
// 模拟宠物不存在的情况
if (petId === 404) {
return {
status: 404,
body: {
code: 404,
message: 'Pet not found',
},
}
}
return {
code: 200,
message: `Pet ${petId} deleted successfully`,
}
},
}, true)
+173
View File
@@ -0,0 +1,173 @@
/*
* @Author: weisheng
* @Date: 2025-06-15 14:30:00
* @LastEditTime: 2025-06-15 14:30:00
* @LastEditors: weisheng
* @Description: Pet Store - Store相关接口的mock数据
* @FilePath: /wot-starter/src/api/mock/modules/store.ts
*/
import { defineMock } from '@alova/mock'
import { generateMockData } from '../utils/generators'
// 订单状态枚举
const ORDER_STATUS = ['placed', 'approved', 'delivered'] as const
type OrderStatus = typeof ORDER_STATUS[number]
// 生成订单对象
function generateOrder(id?: number, status?: OrderStatus) {
const orderId = id || generateMockData.number(1, 10000)
return {
id: orderId,
petId: generateMockData.number(1, 1000),
quantity: generateMockData.number(1, 10),
shipDate: generateMockData.datetime(generateMockData.number(1, 30)), // 1-30天后发货
status: status || ORDER_STATUS[generateMockData.number(0, ORDER_STATUS.length - 1)],
complete: generateMockData.boolean(),
}
}
export default defineMock({
// 获取库存
'[GET]/store/inventory': () => {
console.log('[Mock] GET /store/inventory')
// 生成随机库存数据
const inventory: Record<string, number> = {}
// 为不同状态生成库存数量
ORDER_STATUS.forEach((status) => {
inventory[status] = generateMockData.number(0, 100)
})
// 添加一些额外的状态
inventory.pending = generateMockData.number(0, 50)
inventory.sold = generateMockData.number(0, 200)
inventory.available = generateMockData.number(10, 300)
return inventory
},
// 下单购买宠物
'[POST]/store/order': ({ data }) => {
console.log('[Mock] POST /store/order', data)
// 验证必填字段
if (!data.petId) {
return {
status: 400,
body: {
code: 400,
message: 'Pet ID is required',
},
}
}
if (!data.quantity || data.quantity <= 0) {
return {
status: 400,
body: {
code: 400,
message: 'Quantity must be greater than 0',
},
}
}
// 创建新订单
const newOrder = {
id: generateMockData.number(10001, 20000),
petId: data.petId,
quantity: data.quantity,
shipDate: data.shipDate || generateMockData.datetime(generateMockData.number(1, 7)), // 默认7天内发货
status: 'placed' as OrderStatus,
complete: false,
}
return newOrder
},
// 根据ID获取订单
'[GET]/store/order/{orderId}': ({ params }) => {
console.log(`[Mock] GET /store/order/${params.orderId}`)
const orderId = Number.parseInt(params.orderId)
if (Number.isNaN(orderId)) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid order ID',
},
}
}
// 模拟订单不存在的情况
if (orderId === 404) {
return {
status: 404,
body: {
code: 404,
message: 'Order not found',
},
}
}
// 模拟无效订单ID的情况(订单ID必须在1-10之间)
if (orderId < 1 || orderId > 10) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid ID supplied',
},
}
}
return generateOrder(orderId)
},
// 删除订单
'[DELETE]/store/order/{orderId}': ({ params }) => {
console.log(`[Mock] DELETE /store/order/${params.orderId}`)
const orderId = Number.parseInt(params.orderId)
if (Number.isNaN(orderId)) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid order ID',
},
}
}
// 模拟订单不存在的情况
if (orderId === 404) {
return {
status: 404,
body: {
code: 404,
message: 'Order not found',
},
}
}
// 模拟无效订单ID的情况
if (orderId < 1) {
return {
status: 400,
body: {
code: 400,
message: 'Invalid ID supplied',
},
}
}
return {
code: 200,
message: `Order ${orderId} deleted successfully`,
}
},
}, true)
+304
View File
@@ -0,0 +1,304 @@
/*
* @Author: weisheng
* @Date: 2025-06-15 14:35:00
* @LastEditTime: 2025-06-27 09:43:25
* @LastEditors: weisheng
* @Description: Pet Store - User相关接口的mock数据
* @FilePath: /wot-starter/src/api/mock/modules/user.ts
*/
import { defineMock } from '@alova/mock'
import { generateMockData } from '../utils/generators'
// 用户状态枚举
const USER_STATUS = [0, 1, 2] // 0: 离线, 1: 在线, 2: 忙碌
type UserStatus = typeof USER_STATUS[number]
// 生成用户对象
function generateUser(username?: string, status?: UserStatus) {
const baseUsername = username || generateMockData.name('user').toLowerCase()
return {
id: generateMockData.number(1, 10000),
username: baseUsername,
firstName: generateMockData.name('First'),
lastName: generateMockData.name('Last'),
email: `${baseUsername}@example.com`,
password: 'password123', // 在实际应用中不应该返回密码
phone: `1${generateMockData.number(1000000000, 9999999999)}`,
userStatus: CommonUtil.isDef(status) ? status : USER_STATUS[generateMockData.number(0, USER_STATUS.length - 1)],
}
}
// 模拟用户数据库
const mockUsers = generateMockData.array(index => generateUser(`user${index + 1}`), 10)
export default defineMock({
// 批量创建用户(数组输入)
'[POST]/user/createWithArray': ({ data }) => {
console.log('[Mock] POST /user/createWithArray', data)
if (!Array.isArray(data)) {
return {
status: 400,
body: {
code: 400,
message: 'Input should be an array of users',
},
}
}
// 验证每个用户对象
for (const user of data) {
if (!user.username) {
return {
status: 400,
body: {
code: 400,
message: 'Username is required for all users',
},
}
}
}
return {
code: 200,
message: `Successfully created ${data.length} users`,
}
},
// 批量创建用户(列表输入)
'[POST]/user/createWithList': ({ data }) => {
console.log('[Mock] POST /user/createWithList', data)
if (!Array.isArray(data)) {
return {
status: 400,
body: {
code: 400,
message: 'Input should be a list of users',
},
}
}
// 验证每个用户对象
for (const user of data) {
if (!user.username) {
return {
status: 400,
body: {
code: 400,
message: 'Username is required for all users',
},
}
}
}
return {
code: 200,
message: `Successfully created ${data.length} users from list`,
}
},
// 用户登录
'[GET]/user/login': ({ query }) => {
console.log('[Mock] GET /user/login', query)
const { username, password } = query
if (!username || !password) {
return {
status: 400,
body: {
code: 400,
message: 'Username and password are required',
},
}
}
// 模拟登录验证
if (username === 'admin' && password === 'admin') {
return {
code: 200,
message: 'logged in user session',
token: `mock_token_${Date.now()}`,
expiresIn: 3600, // 1小时
user: generateUser('admin', 1),
}
}
// 模拟用户名密码错误
if (username === 'invalid' || password === 'invalid') {
return {
status: 400,
body: {
code: 400,
message: 'Invalid username/password supplied',
},
}
}
// 默认成功登录
return {
code: 200,
message: 'logged in user session',
token: `mock_token_${Date.now()}`,
expiresIn: 3600,
user: generateUser(username as string, 1),
}
},
// 用户登出
'[GET]/user/logout': () => {
console.log('[Mock] GET /user/logout')
return {
code: 200,
message: 'ok',
}
},
// 根据用户名获取用户
'[GET]/user/{username}': ({ params }) => {
console.log(`[Mock] GET /user/${params.username}`)
const username = params.username
if (!username) {
return {
status: 400,
body: {
code: 400,
message: 'Username is required',
},
}
}
// 模拟用户不存在的情况
if (username === 'notfound') {
return {
status: 404,
body: {
code: 404,
message: 'User not found',
},
}
}
// 从模拟数据库中查找用户
const existingUser = mockUsers.find(user => user.username === username)
if (existingUser) {
return existingUser
}
// 生成新用户
return generateUser(username)
},
// 更新用户信息
'[PUT]/user/{username}': ({ params, data }) => {
console.log(`[Mock] PUT /user/${params.username}`, data)
const username = params.username
if (!username) {
return {
status: 400,
body: {
code: 400,
message: 'Username is required',
},
}
}
// 模拟用户不存在的情况
if (username === 'notfound') {
return {
status: 404,
body: {
code: 404,
message: 'User not found',
},
}
}
// 更新用户信息
const updatedUser = {
...generateUser(username),
...data,
username, // 确保用户名不被修改
updatedAt: generateMockData.datetime(),
}
return updatedUser
},
// 删除用户
'[DELETE]/user/{username}': ({ params }) => {
console.log(`[Mock] DELETE /user/${params.username}`)
const username = params.username
if (!username) {
return {
status: 400,
body: {
code: 400,
message: 'Username is required',
},
}
}
// 模拟用户不存在的情况
if (username === 'notfound') {
return {
status: 404,
body: {
code: 404,
message: 'User not found',
},
}
}
return {
code: 200,
message: `User ${username} deleted successfully`,
}
},
// 创建用户
'[POST]/user': ({ data }) => {
console.log('[Mock] POST /user', data)
if (!data.username) {
return {
status: 400,
body: {
code: 400,
message: 'Username is required',
},
}
}
// 检查用户名是否已存在
const existingUser = mockUsers.find(user => user.username === data.username)
if (existingUser) {
return {
status: 409,
body: {
code: 409,
message: 'Username already exists',
},
}
}
// 创建新用户
const newUser = {
...generateUser(),
...data,
id: generateMockData.number(20001, 30000),
createdAt: generateMockData.datetime(),
}
return newUser
},
}, true)
@@ -0,0 +1,142 @@
/*
* @Author: weisheng
* @Date: 2023-05-20 10:00:00
* @LastEditTime: 2025-05-21 15:46:14
* @LastEditors: weisheng
* @Description: Mock数据生成工具
* @FilePath: /lsym-cx-mini/src/api/mock/utils/generators.ts
*/
// 模拟数据生成工具函数
export const generateMockData = {
// 生成随机ID
id: (): number => Math.floor(Math.random() * 10000),
// 生成随机名称
name: (prefix = '名称'): string => `${prefix}_${Math.floor(Math.random() * 1000)}`,
// 生成随机代码
code: (prefix = 'CODE'): string => `${prefix}_${Math.floor(Math.random() * 1000)}`,
// 生成随机日期
// 可以传入天数偏移,负数表示过去的日期,正数表示未来的日期
date: (dayOffset = 0): string => {
const date = new Date()
if (dayOffset !== 0) {
date.setDate(date.getDate() + dayOffset)
}
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
},
// 生成随机时间
// 可以传入天数偏移,负数表示过去的日期,正数表示未来的日期
datetime: (dayOffset = 0): string => {
const date = new Date()
if (dayOffset !== 0) {
date.setDate(date.getDate() + dayOffset)
}
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
},
// 生成随机布尔值
boolean: (): boolean => Math.random() > 0.5,
// 生成随机数字
number: (min = 0, max = 100): number => Math.floor(Math.random() * (max - min)) + min,
// 生成随机数组
array: <T>(generator: (index: number) => T, length = 10): T[] => {
const result: T[] = []
for (let i = 0; i < length; i++) {
result.push(generator(i))
}
return result
},
// 生成基础响应对象
baseResponse: <T>(data: T = null as unknown as T, code = 2000, msg = '操作成功') => ({
code,
data,
msg,
}),
// 生成列表响应对象
listResponse: <T>(data: T[] = [], total = data.length, more = false, code = 2000, msg = '操作成功') => ({
code,
data,
total,
more,
msg,
}),
// 生成GCN对象
gcn: (_index?: number) => ({
gid: generateMockData.id(),
code: generateMockData.code('ORG'),
name: generateMockData.name('组织'),
}),
// 生成员工对象
faEmp: (index: number) => ({
gid: generateMockData.id(),
code: generateMockData.code('EMP'),
name: generateMockData.name('员工'),
org: generateMockData.gcn(index),
}),
// 生成车销业务员对象
vehSaleEmp: (index: number) => ({
gid: generateMockData.id(),
code: generateMockData.code('VSE'),
name: generateMockData.name('车销业务员'),
faEmp: generateMockData.faEmp(index),
org: generateMockData.gcn(index),
wms: generateMockData.gcn(index),
}),
// 生成权限对象
permission: (index: number) => ({
module: `module_${index}`,
moduleName: `模块${index}`,
roleId: `role_${index}`,
state: 1,
}),
// 生成代码名称对象
codeName: (index: number, prefix = '线路') => ({
code: generateMockData.code(`LINE_${index}`),
name: `${prefix}${index}`,
}),
// 生成用户对象
user: (roleCode = '01') => ({
permissions: generateMockData.array(generateMockData.permission, 5),
sortLines: generateMockData.array(generateMockData.codeName, 3),
token: `mock_token_${Date.now()}`,
roleCode, // 添加roleCode字段:01-车销业务员,02-仓管
vehSaleEmp: generateMockData.vehSaleEmp(0),
}),
// 生成商品对象
goods: (index: number) => ({
gid: generateMockData.id(),
code: generateMockData.code('GOODS'),
name: generateMockData.name('商品'),
gdCode: generateMockData.code('GDCODE'),
spec: `规格${index}`,
munit: '个',
price: generateMockData.number(1, 1000) / 100,
qpc: generateMockData.number(1, 10),
qpcStr: `${generateMockData.number(1, 10)}个/箱`,
qty: generateMockData.number(1, 100),
qtyStr: `${generateMockData.number(1, 100)}个`,
busInvQty: generateMockData.number(1, 100),
advUseSignQty: generateMockData.number(1, 100),
version: 1,
}),
// 单据状态,可选值为:0 | 100 | 1300 | 300 | 110 | 1310
stat: (): number => {
const stats = [100, 1300, 300, 110, 1310]
return stats[generateMockData.number(0, stats.length - 1)]
},
}
+277
View File
@@ -0,0 +1,277 @@
import { ApiHeader } from '@/enums/api-header.enum'
import { http } from '@/http'
const USER_BASE_URL = '/system/user'
const UserAPI = {
/**
* 个人中心用户信息
*
* @returns 登录用户昵称、头像信息,包括角色和权限
*/
getCurrentUserInfo(): Promise<UserInfo> {
return http.Get(`${USER_BASE_URL}/current/info`)
},
/**
* 当前用户头像上传
*
* @param body
* @returns 上传后的文件路径
*/
uploadCurrentUserAvatar(body: any): Promise<UploadFileResult> {
return http.Post(`${USER_BASE_URL}/current/avatar/upload`, body, {
headers: {
[ApiHeader.KEY]: ApiHeader.MULTIPART,
},
})
},
/**
* 修改个人中心用户信息
*
* @param body
* @returns 修改后的用户信息
*/
updateCurrentUserInfo(body: UserProfileForm): Promise<UserInfo> {
return http.Put(`${USER_BASE_URL}/current/info/update`, body)
},
/**
* 修改个人中心用户密码
*
* @param body
* @returns 修改后的用户信息
*/
changeCurrentUserPassword(body: PasswordChangeForm): Promise<ApiResponse> {
return http.Put(`${USER_BASE_URL}/current/password/change`, body)
},
/**
* 注册用户
*
* @param body
* @returns 忘记密码结果
*/
registerUser(body: RegisterForm): Promise<ApiResponse> {
return http.Post(`${USER_BASE_URL}/register`, body)
},
/**
* 忘记密码
*
* @param body
* @returns 忘记密码结果
*/
forgetPassword(body: ForgetPasswordForm): Promise<ApiResponse> {
return http.Post(`${USER_BASE_URL}/forget/password`, body)
},
/**
* 获取用户分页列表
*POST
* @param queryParams 查询参数
*/
getUserPage(queryParams: UserPageQuery): Promise<PageResult<UserInfo[]>> {
return http.Get(`${USER_BASE_URL}/list`, queryParams)
},
/**
* 获取用户表单详情
*
* @param userId 用户ID
* @returns 用户表单详情
*/
getUserDetail(userId: number): Promise<UserForm> {
return http.Get(`${USER_BASE_URL}/detail/${userId}`)
},
/**
* 添加用户
*
* @param body 用户表单数据
*/
addUser(body: UserForm): Promise<ApiResponse> {
return http.Post(`${USER_BASE_URL}/create`, body)
},
/**
* 修改用户
*
* @param body 用户表单数据
*/
updateUser(body: UserForm): Promise<ApiResponse> {
return http.Put(`${USER_BASE_URL}/update`, body)
},
/**
* 删除用户
*
* @param ids 用户ID数组
*/
deleteUser(ids: number[]): Promise<ApiResponse> {
return http.Delete(`${USER_BASE_URL}/delete`, ids)
},
}
export default UserAPI
/* 忘记密码表单 */
export interface ForgetPasswordForm {
username: string
new_password: string
confirmPassword: string
}
/* 注册表单 */
export interface RegisterForm {
username: string
password: string
confirmPassword: string
}
/* 分页查询表单 */
export interface UserPageQuery extends PageQuery {
username?: string
name?: string
status?: boolean
dept_id?: number
start_time?: string
end_time?: string
}
/* 搜索选择器数据类型 */
export interface searchSelectDataType {
name?: string
status?: string
}
/* 用户表单 */
export interface UserForm {
id?: number
username?: string
name?: string
dept_id?: number
dept_name?: string
role_ids?: number[]
roleNames?: string[]
position_ids?: number[]
positionNames?: string[]
password?: string
gender?: number
email?: string
mobile?: string
is_superuser?: boolean
status?: boolean
description?: string
}
/* 登录用户信息 */
export interface UserInfo {
index?: number
id?: number
username?: string
name?: string
avatar?: string
email?: string
mobile?: string
gender?: string
password?: string
menus?: MenuTable[]
dept?: deptTreeType
dept_id?: deptTreeType['id']
dept_name?: deptTreeType['name']
roles?: roleSelectorType[]
roleNames?: roleSelectorType['name'][]
role_ids?: roleSelectorType['id'][]
positions?: positionSelectorType[]
positionNames?: positionSelectorType['name'][]
position_ids?: positionSelectorType['id'][]
is_superuser?: boolean
status?: boolean
description?: string
last_login?: string
created_at?: string
updated_at?: string
creator?: creatorType
}
/* 菜单表 */
export interface MenuTable {
index?: number
id?: number
name?: string
type?: number
icon?: string
order?: number
permission?: string
route_name?: string
route_path?: string
component_path?: string
redirect?: string
parent_id?: number
parent_name?: string
keep_alive?: boolean
hidden?: boolean
always_show?: boolean
title?: string
params?: { key: string, value: string }[]
affix?: boolean
status?: boolean
description?: string
created_at?: string
updated_at?: string
children?: MenuTable[]
}
/* 部门树 */
export interface deptTreeType {
id?: number
name?: string
parent_id?: number
children?: deptTreeType[]
}
/* 角色选择器 */
export interface roleSelectorType {
id?: number
name?: string
status?: boolean
description?: string
}
/* 职位选择器 */
export interface positionSelectorType {
id?: number
name?: string
status?: boolean
description?: string
}
/* 个人中心用户信息表单 */
export interface UserProfileForm {
id?: number
name?: string
gender?: string
mobile?: string
email?: string
username?: string
dept_name?: string
positions?: positionSelectorType[]
roles?: roleSelectorType[]
avatar?: string
created_at?: string
}
/* 修改密码表单 */
export interface PasswordChangeForm {
old_password: string
new_password: string
confirm_password: string
}
/* 重置密码表单 */
export interface ResetPasswordForm {
id: number
password: string
}
+733
View File
@@ -0,0 +1,733 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const $$userConfigMap: typeof import('./api/index')['$$userConfigMap']
const Apis: typeof import('./api/index')['Apis']
const CommonUtil: typeof import('wot-design-uni')['CommonUtil']
const EffectScope: typeof import('vue')['EffectScope']
const Storage: typeof import('./utils/storage')['Storage']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const adjustColorBrightness: typeof import('./utils/color')['adjustColorBrightness']
const alovaInstance: typeof import('./api/index')['alovaInstance']
const api: typeof import('./api/index')['default']
const apiDefinitions: typeof import('./api/apiDefinitions')['default']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const auth: typeof import('./api/auth')['default']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const checkLogin: typeof import('./utils/auth')['checkLogin']
const clearAll: typeof import('./utils/auth')['clearAll']
const clearTokens: typeof import('./utils/auth')['clearTokens']
const clearUserInfo: typeof import('./utils/auth')['clearUserInfo']
const computed: typeof import('vue')['computed']
const computedAsync: typeof import('@vueuse/core')['computedAsync']
const computedEager: typeof import('@vueuse/core')['computedEager']
const computedInject: typeof import('@vueuse/core')['computedInject']
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApis: typeof import('./api/createApis')['createApis']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createRouter: typeof import('@wot-ui/router')['createRouter']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getAccessToken: typeof import('./utils/auth')['getAccessToken']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentPath: typeof import('./utils/index')['getCurrentPath']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getRefreshToken: typeof import('./utils/auth')['getRefreshToken']
const getUserInfo: typeof import('./utils/auth')['getUserInfo']
const h: typeof import('vue')['h']
const hexToRgb: typeof import('./utils/color')['hexToRgb']
const hideLoading: typeof import('./utils/notification')['hideLoading']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const injectLocal: typeof import('@vueuse/core')['injectLocal']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isLoggedIn: typeof import('./utils/auth')['isLoggedIn']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const mountApis: typeof import('./api/createApis')['mountApis']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites']
const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onError: typeof import('@dcloudio/uni-app')['onError']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onHide: typeof import('@dcloudio/uni-app')['onHide']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch']
const onLoad: typeof import('@dcloudio/uni-app')['onLoad']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
const onMounted: typeof import('vue')['onMounted']
const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']
const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']
const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']
const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']
const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']
const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound']
const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll']
const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh']
const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom']
const onReady: typeof import('@dcloudio/uni-app')['onReady']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onResize: typeof import('@dcloudio/uni-app')['onResize']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage']
const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline']
const onShow: typeof import('@dcloudio/uni-app')['onShow']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap']
const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange']
const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection']
const onUnload: typeof import('@dcloudio/uni-app')['onUnload']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const persistPlugin: typeof import('./store/persist')['persistPlugin']
const provide: typeof import('vue')['provide']
const provideLocal: typeof import('@vueuse/core')['provideLocal']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
const refDefault: typeof import('@vueuse/core')['refDefault']
const refThrottled: typeof import('@vueuse/core')['refThrottled']
const refWithControl: typeof import('@vueuse/core')['refWithControl']
const requireLogin: typeof import('./utils/auth')['requireLogin']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const rgbToHex: typeof import('./utils/color')['rgbToHex']
const setAccessToken: typeof import('./utils/auth')['setAccessToken']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setRefreshToken: typeof import('./utils/auth')['setRefreshToken']
const setUserInfo: typeof import('./utils/auth')['setUserInfo']
const initStore: typeof import('./store/index')['initStore']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const showConfirm: typeof import('./utils/notification')['showConfirm']
const showError: typeof import('./utils/notification')['showError']
const showInput: typeof import('./utils/notification')['showInput']
const showLoading: typeof import('./utils/notification')['showLoading']
const showSuccess: typeof import('./utils/notification')['showSuccess']
const showToast: typeof import('./utils/notification')['showToast']
const store: typeof import('./store/index')['store']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const syncRefs: typeof import('@vueuse/core')['syncRefs']
const templateRef: typeof import('@vueuse/core')['templateRef']
const themeColorOptions: typeof import('./composables/useManualTheme')['themeColorOptions']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toLoginPage: typeof import('./utils/toLoginPage')['toLoginPage']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAnimate: typeof import('@vueuse/core')['useAnimate']
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useDropZone: typeof import('@vueuse/core')['useDropZone']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGamepad: typeof import('@vueuse/core')['useGamepad']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useGlobalLoading: typeof import('./composables/useGlobalLoading')['useGlobalLoading']
const useGlobalMessage: typeof import('./composables/useGlobalMessage')['useGlobalMessage']
const useGlobalToast: typeof import('./composables/useGlobalToast')['useGlobalToast']
const useId: typeof import('vue')['useId']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useImage: typeof import('@vueuse/core')['useImage']
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useManualTheme: typeof import('./composables/useManualTheme')['useManualTheme']
const useManualThemeStore: typeof import('./store/manualThemeStore')['useManualThemeStore']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMessage: typeof import('wot-design-uni')['useMessage']
const useModel: typeof import('vue')['useModel']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNotify: typeof import('wot-design-uni')['useNotify']
const useNow: typeof import('@vueuse/core')['useNow']
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const usePagination: typeof import('alova/client')['usePagination']
const useParallax: typeof import('@vueuse/core')['useParallax']
const useParentElement: typeof import('@vueuse/core')['useParentElement']
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useRequest: typeof import('alova/client')['useRequest']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('@wot-ui/router')['useRoute']
const useRouter: typeof import('@wot-ui/router')['useRouter']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSorted: typeof import('@vueuse/core')['useSorted']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStepper: typeof import('@vueuse/core')['useStepper']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTabbar: typeof import('./composables/useTabbar')['useTabbar']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useTheme: typeof import('./composables/useTheme')['useTheme']
const useThemeStore: typeof import('./store/themeStore')['useThemeStore']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToast: typeof import('wot-design-uni')['useToast']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useUserStore: typeof import('./store/userStore')['useUserStore']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const user: typeof import('./api/user')['default']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
const watchDeep: typeof import('@vueuse/core')['watchDeep']
const watchEffect: typeof import('vue')['watchEffect']
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchPausable: typeof import('@vueuse/core')['watchPausable']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
const withConfigType: typeof import('./api/createApis')['withConfigType']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
import('vue')
}
// for vue template auto import
import { UnwrapRef } from 'vue'
declare module 'vue' {
interface GlobalComponents {}
interface ComponentCustomProperties {
readonly $$userConfigMap: UnwrapRef<typeof import('./api/index')['$$userConfigMap']>
readonly Apis: UnwrapRef<typeof import('./api/index')['Apis']>
readonly CommonUtil: UnwrapRef<typeof import('wot-design-uni')['CommonUtil']>
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
readonly Storage: UnwrapRef<typeof import('./utils/storage')['Storage']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly alovaInstance: UnwrapRef<typeof import('./api/index')['alovaInstance']>
readonly api: UnwrapRef<typeof import('./api/index')['default']>
readonly apiDefinitions: UnwrapRef<typeof import('./api/apiDefinitions')['default']>
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
readonly auth: UnwrapRef<typeof import('./api/auth')['default']>
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
readonly createApis: UnwrapRef<typeof import('./api/createApis')['createApis']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
readonly createRouter: UnwrapRef<typeof import('@wot-ui/router')['createRouter']>
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentPath: UnwrapRef<typeof import('./utils/index')['getCurrentPath']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
readonly mapStores: UnwrapRef<typeof import('pinia')['mapStores']>
readonly mapWritableState: UnwrapRef<typeof import('pinia')['mapWritableState']>
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
readonly mountApis: UnwrapRef<typeof import('./api/createApis')['mountApis']>
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
readonly onAddToFavorites: UnwrapRef<typeof import('@dcloudio/uni-app')['onAddToFavorites']>
readonly onBackPress: UnwrapRef<typeof import('@dcloudio/uni-app')['onBackPress']>
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onError: UnwrapRef<typeof import('@dcloudio/uni-app')['onError']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onHide: UnwrapRef<typeof import('@dcloudio/uni-app')['onHide']>
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
readonly onLaunch: UnwrapRef<typeof import('@dcloudio/uni-app')['onLaunch']>
readonly onLoad: UnwrapRef<typeof import('@dcloudio/uni-app')['onLoad']>
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNavigationBarButtonTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap']>
readonly onNavigationBarSearchInputChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged']>
readonly onNavigationBarSearchInputClicked: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked']>
readonly onNavigationBarSearchInputConfirmed: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed']>
readonly onNavigationBarSearchInputFocusChanged: UnwrapRef<typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged']>
readonly onPageNotFound: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageNotFound']>
readonly onPageScroll: UnwrapRef<typeof import('@dcloudio/uni-app')['onPageScroll']>
readonly onPullDownRefresh: UnwrapRef<typeof import('@dcloudio/uni-app')['onPullDownRefresh']>
readonly onReachBottom: UnwrapRef<typeof import('@dcloudio/uni-app')['onReachBottom']>
readonly onReady: UnwrapRef<typeof import('@dcloudio/uni-app')['onReady']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onResize: UnwrapRef<typeof import('@dcloudio/uni-app')['onResize']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
readonly onShareAppMessage: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareAppMessage']>
readonly onShareTimeline: UnwrapRef<typeof import('@dcloudio/uni-app')['onShareTimeline']>
readonly onShow: UnwrapRef<typeof import('@dcloudio/uni-app')['onShow']>
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
readonly onTabItemTap: UnwrapRef<typeof import('@dcloudio/uni-app')['onTabItemTap']>
readonly onThemeChange: UnwrapRef<typeof import('@dcloudio/uni-app')['onThemeChange']>
readonly onUnhandledRejection: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnhandledRejection']>
readonly onUnload: UnwrapRef<typeof import('@dcloudio/uni-app')['onUnload']>
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
readonly persistPlugin: UnwrapRef<typeof import('./store/persist')['persistPlugin']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
readonly themeColorOptions: UnwrapRef<typeof import('./composables/useManualTheme')['themeColorOptions']>
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
readonly toLoginPage: UnwrapRef<typeof import('./utils/toLoginPage')['toLoginPage']>
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
readonly useGlobalLoading: UnwrapRef<typeof import('./composables/useGlobalLoading')['useGlobalLoading']>
readonly useGlobalMessage: UnwrapRef<typeof import('./composables/useGlobalMessage')['useGlobalMessage']>
readonly useGlobalToast: UnwrapRef<typeof import('./composables/useGlobalToast')['useGlobalToast']>
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
readonly useManualTheme: UnwrapRef<typeof import('./composables/useManualTheme')['useManualTheme']>
readonly useManualThemeStore: UnwrapRef<typeof import('./store/manualThemeStore')['useManualThemeStore']>
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
readonly useMessage: UnwrapRef<typeof import('wot-design-uni')['useMessage']>
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
readonly useNotify: UnwrapRef<typeof import('wot-design-uni')['useNotify']>
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
readonly usePagination: UnwrapRef<typeof import('alova/client')['usePagination']>
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
readonly useRequest: UnwrapRef<typeof import('alova/client')['useRequest']>
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
readonly useRoute: UnwrapRef<typeof import('@wot-ui/router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('@wot-ui/router')['useRouter']>
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
readonly useTabbar: UnwrapRef<typeof import('./composables/useTabbar')['useTabbar']>
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
readonly useTheme: UnwrapRef<typeof import('./composables/useTheme')['useTheme']>
readonly useThemeStore: UnwrapRef<typeof import('./store/themeStore')['useThemeStore']>
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
readonly useToast: UnwrapRef<typeof import('wot-design-uni')['useToast']>
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
readonly useUserStore: UnwrapRef<typeof import('./store/userStore')['useUserStore']>
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
readonly user: UnwrapRef<typeof import('./api/user')['default']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
readonly withConfigType: UnwrapRef<typeof import('./api/createApis')['withConfigType']>
}
}
+54
View File
@@ -0,0 +1,54 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
DateQuery: typeof import('./components/DateQuery.vue')['default']
DemoBlock: typeof import('./components/DemoBlock.vue')['default']
GlobalLoading: typeof import('./components/GlobalLoading.vue')['default']
GlobalMessage: typeof import('./components/GlobalMessage.vue')['default']
GlobalToast: typeof import('./components/GlobalToast.vue')['default']
Picker: typeof import('./components/Picker.vue')['default']
PrivacyPopup: typeof import('./components/PrivacyPopup.vue')['default']
UniEcharts: typeof import('uni-echarts')['default']
WdActionSheet: typeof import('wot-design-uni/components/wd-action-sheet/wd-action-sheet.vue')['default']
WdAvatar: typeof import('wot-design-uni/components/wd-avatar/wd-avatar.vue')['default']
WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default']
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default']
WdCard: typeof import('wot-design-uni/components/wd-card/wd-card.vue')['default']
WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default']
WdCellGroup: typeof import('wot-design-uni/components/wd-cell-group/wd-cell-group.vue')['default']
WdCheckbox: typeof import('wot-design-uni/components/wd-checkbox/wd-checkbox.vue')['default']
WdCol: typeof import('wot-design-uni/components/wd-col/wd-col.vue')['default']
WdConfigProvider: typeof import('wot-design-uni/components/wd-config-provider/wd-config-provider.vue')['default']
WdDivider: typeof import('wot-design-uni/components/wd-divider/wd-divider.vue')['default']
WdForm: typeof import('wot-design-uni/components/wd-form/wd-form.vue')['default']
WdGap: typeof import('wot-design-uni/components/wd-gap/wd-gap.vue')['default']
WdGrid: typeof import('wot-design-uni/components/wd-grid/wd-grid.vue')['default']
WdGridItem: typeof import('wot-design-uni/components/wd-grid-item/wd-grid-item.vue')['default']
WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default']
WdImg: typeof import('wot-design-uni/components/wd-img/wd-img.vue')['default']
WdImgCropper: typeof import('wot-design-uni/components/wd-img-cropper/wd-img-cropper.vue')['default']
WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default']
WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default']
WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default']
WdNoticeBar: typeof import('wot-design-uni/components/wd-notice-bar/wd-notice-bar.vue')['default']
WdNotify: typeof import('wot-design-uni/components/wd-notify/wd-notify.vue')['default']
WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default']
WdRadio: typeof import('wot-design-uni/components/wd-radio/wd-radio.vue')['default']
WdRadioGroup: typeof import('wot-design-uni/components/wd-radio-group/wd-radio-group.vue')['default']
WdRow: typeof import('wot-design-uni/components/wd-row/wd-row.vue')['default']
WdSwiper: typeof import('wot-design-uni/components/wd-swiper/wd-swiper.vue')['default']
WdSwitch: typeof import('wot-design-uni/components/wd-switch/wd-switch.vue')['default']
WdTabbar: typeof import('wot-design-uni/components/wd-tabbar/wd-tabbar.vue')['default']
WdTabbarItem: typeof import('wot-design-uni/components/wd-tabbar-item/wd-tabbar-item.vue')['default']
WdTag: typeof import('wot-design-uni/components/wd-tag/wd-tag.vue')['default']
WdText: typeof import('wot-design-uni/components/wd-text/wd-text.vue')['default']
WdToast: typeof import('wot-design-uni/components/wd-toast/wd-toast.vue')['default']
WdWatermark: typeof import('wot-design-uni/components/wd-watermark/wd-watermark.vue')['default']
}
}
+90
View File
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import type { PropType } from 'vue'
import { ref, watch } from 'vue'
import { dayjs } from 'wot-design-uni'
const props = defineProps({
modelValue: {
type: [Array, String] as PropType<[string, string] | string | undefined>,
default: () => undefined,
},
placeholder: {
type: String,
default: '请选择时间范围',
},
label: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue'])
const dateRange = ref<number[] | number | null>(null)
watch(
() => props.modelValue,
(val) => {
if (Array.isArray(val) && val.length === 2 && val[0] && val[1]) {
dateRange.value = val.map(item => new Date(item).getTime())
}
else if (typeof val === 'string' && val.includes(',')) {
const [startDate, endDate] = val.split(',')
if (
startDate
&& endDate
&& !Number.isNaN(new Date(startDate).getTime())
&& !Number.isNaN(new Date(endDate).getTime())
) {
dateRange.value = [
new Date(startDate).getTime(),
new Date(endDate).getTime(),
]
}
else {
dateRange.value = null
}
}
else {
dateRange.value = null
}
},
{
immediate: true,
},
)
// 确认选择时间
function handleConfirm() {
if (Array.isArray(dateRange.value) && dateRange.value.length === 2) {
const startDate = dayjs(dateRange.value[0]).format('YYYY-MM-DD')
const endDate = dayjs(dateRange.value[1]).format('YYYY-MM-DD')
let newVal: any = [startDate, endDate]
// #ifdef MP-WEIXIN
newVal = `${startDate},${endDate}`
// #endif
console.log('newVal', newVal)
emit('update:modelValue', newVal)
}
}
</script>
<template>
<wd-calendar
v-model="dateRange"
:label="label"
type="daterange"
:placeholder="placeholder"
@confirm="handleConfirm"
/>
</template>
<style scoped>
.time-filter {
padding: 16rpx;
}
</style>
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts" setup>
const props = defineProps({
// 标题
title: {
type: String,
default: '',
},
// 自定义类名
customClass: {
type: String,
default: '',
},
// 垂直间距
ver: {
type: [Number, String],
default: 10,
},
// 水平间距
hor: {
type: [Number, String],
default: 15,
},
// 是否透明
transparent: {
type: Boolean,
default: false,
},
})
const style = computed(() => {
return `margin: 0 ${props.hor}px;padding:${props.ver}px 0;`
})
</script>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<view
class="mb-3 box-border w-full px-3 text-gray-500 last:mb-0 dark:text-gray-300"
:class="[
transparent ? '' : 'bg-white dark:bg-[var(--wot-dark-background2)]',
customClass,
]"
>
<view class="px-4 py-3 text-26rpx">
{{ title }}
</view>
<view :style="transparent ? '' : style">
<slot />
</view>
</view>
</template>
@@ -0,0 +1,366 @@
# GlobalLoading 全局加载组件
## 概述
GlobalLoading 是基于 `wot-design-uni` 的 `wd-toast` 组件封装的全局加载组件,可以结合`axios`、`alova`等请求库使用,显示加载状态,通过 Pinia 状态管理实现全局调用,用于显示数据加载、操作处理等场景的等待状态。
## 组件特性
- 基于 `wd-toast` 组件封装
- 显示加载动画,默认不自动关闭
- 支持遮罩层覆盖,防止用户操作
- 自动页面路径检测,确保在正确页面显示
- 虚拟节点设计,不在 DOM 中创建额外层级
## 安装使用
### 1. 注册全局组件
在 `App.vue` 或根组件中注册:
```vue
<template>
<div>
<!-- 其他内容 -->
<GlobalLoading />
</div>
</template>
```
### 2. 在组件中使用
```typescript
import { useGlobalLoading } from '@/composables/useGlobalLoading'
const loading = useGlobalLoading()
```
## API 文档
### 基础用法
```typescript
// 显示加载(字符串参数)
loading.loading('加载中...')
// 显示加载(配置对象)
loading.loading({
msg: '数据加载中',
cover: true
})
// 关闭加载
loading.close()
```
### 方法说明
#### loading(option)
显示加载状态
```typescript
// 简单文本
loading.loading('请稍候...')
// 自定义配置
loading.loading({
msg: '数据处理中,请稍候',
cover: false,
position: 'middle'
})
```
**默认配置:**
- 图标:loading(旋转动画)
- 持续时间:0(不自动关闭)
- 遮罩:true
- 位置:middle
- 显示:true
#### close()
关闭加载状态
```typescript
loading.close()
```
## 参数说明
### ToastOptions
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| msg | string | - | 加载提示文本 |
| iconName | string | 'loading' | 图标名称(固定为loading) |
| duration | number | 0 | 持续时间(ms),0表示不自动关闭 |
| cover | boolean | true | 是否显示遮罩层 |
| position | string | 'middle' | 显示位置:'top' \| 'middle' \| 'bottom' |
| show | boolean | - | 是否显示(内部使用) |
### 方法参数
支持两种参数类型:
1. **字符串参数**:直接传入加载提示文本
2. **选项对象**:传入完整的 ToastOptions 配置
## 使用示例
### 基础示例
```typescript
export default {
setup() {
const loading = useGlobalLoading()
const handleLoad = async () => {
loading.loading('加载中...')
try {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 2000))
console.log('加载完成')
}
finally {
loading.close()
}
}
return {
handleLoad
}
}
}
```
### 网络请求示例
```typescript
async function fetchData() {
const loading = useGlobalLoading()
try {
loading.loading('获取数据中...')
const response = await api.getData()
return response.data
}
catch (error) {
console.error('获取数据失败:', error)
throw error
}
finally {
loading.close()
}
}
```
### 表单提交示例
```typescript
async function handleSubmit(formData) {
const loading = useGlobalLoading()
const toast = useGlobalToast()
try {
loading.loading('提交中,请稍候...')
await submitForm(formData)
toast.success('提交成功')
}
catch (error) {
toast.error(`提交失败:${error.message}`)
}
finally {
loading.close()
}
}
```
### 文件上传示例
```typescript
async function uploadFile(file) {
const loading = useGlobalLoading()
try {
loading.loading({
msg: '文件上传中...',
cover: true // 防止用户重复操作
})
const result = await uploadToServer(file)
return result
}
finally {
loading.close()
}
}
```
### 批量操作示例
```typescript
async function batchProcess(items) {
const loading = useGlobalLoading()
try {
loading.loading(`处理中 (0/${items.length})`)
for (let i = 0; i < items.length; i++) {
// 更新进度提示
loading.loading(`处理中 (${i + 1}/${items.length})`)
await processItem(items[i])
}
}
finally {
loading.close()
}
}
```
### 自定义样式示例
```typescript
function showCustomLoading() {
loading.loading({
msg: '自定义加载样式',
cover: false, // 不显示遮罩
position: 'top' // 显示在顶部
})
}
```
## 实现原理
### 状态管理
- 使用 Pinia 管理全局状态
- 状态包含:`loadingOptions`(加载配置)、`currentPage`(当前页面路径)
### 页面检测机制
- 调用加载方法时记录当前页面路径
- 组件监听状态变化时验证页面路径
- 确保加载状态在正确的页面显示
### 组件配置
```typescript
export default {
options: {
virtualHost: true, // 虚拟节点
addGlobalClass: true, // 支持全局样式
styleIsolation: 'shared' // 样式隔离共享
}
}
```
## 最佳实践
1. **及时关闭**
```typescript
// ✅ 推荐:使用 try-finally 确保关闭
try {
loading.loading('处理中...')
await doSomething()
}
finally {
loading.close()
}
// ❌ 避免:可能忘记关闭
loading.loading('处理中...')
await doSomething()
loading.close() // 如果上面抛出异常,此行不会执行
```
2. **合理使用遮罩**
```typescript
// 重要操作,防止用户干扰
loading.loading({
msg: '正在保存...',
cover: true
})
// 轻量操作,允许用户其他操作
loading.loading({
msg: '加载中...',
cover: false
})
```
3. **提供清晰的提示文本**
```typescript
// ✅ 清晰明确
loading.loading('正在上传文件...')
loading.loading('正在保存数据...')
// ❌ 模糊不清
loading.loading('请稍候...')
loading.loading('处理中...')
```
4. **长时间操作显示进度**
```typescript
// 对于耗时较长的操作,提供进度信息
for (let i = 0; i < items.length; i++) {
loading.loading(`处理进度:${i + 1}/${items.length}`)
await processItem(items[i])
}
```
5. **避免嵌套调用**
```typescript
// ❌ 避免嵌套
loading.loading('外层操作...')
await outerOperation()
loading.loading('内层操作...') // 会覆盖外层
await innerOperation()
loading.close() // 只关闭了内层
// ✅ 推荐:合并操作或使用状态管理
loading.loading('正在执行操作...')
try {
await outerOperation()
await innerOperation()
}
finally {
loading.close()
}
```
## 使用场景
1. **数据加载**:页面初始化、列表刷新
2. **表单提交**:用户操作提交、数据保存
3. **文件操作**:上传、下载、删除文件
4. **网络请求**:API 调用、数据同步
5. **批量处理**:多个项目的批量操作
6. **页面跳转**:路由切换前的准备工作
## 注意事项
1. **必须手动关闭**:加载组件不会自动关闭,务必在操作完成后调用 `close()`
2. **防止内存泄漏**:在组件销毁前确保关闭加载状态
3. **单例模式**:同时只能显示一个加载状态,新的调用会覆盖旧的
4. **页面路径检测**:组件会自动在对应页面显示,切换页面时会自动关闭
5. **遮罩层影响**:开启遮罩时会阻止用户操作,谨慎使用
## 错误处理
```typescript
// 推荐的错误处理模式
async function safeOperation() {
const loading = useGlobalLoading()
const toast = useGlobalToast()
try {
loading.loading('处理中...')
await riskyOperation()
toast.success('操作成功')
}
catch (error) {
console.error('操作失败:', error)
toast.error('操作失败,请重试')
}
finally {
// 无论成功或失败都要关闭加载
loading.close()
}
}
@@ -0,0 +1,46 @@
<script lang="ts" setup>
const { loadingOptions, currentPage } = storeToRefs(useGlobalLoading())
const { close: closeGlobalLoading } = useGlobalLoading()
const loading = useToast('globalLoading')
const currentPath = getCurrentPath()
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
hackAlipayVisible.value = true
})
// #endif
watch(() => loadingOptions.value, (newVal) => {
if (newVal && newVal.show) {
if (currentPage.value === currentPath) {
loading.loading(loadingOptions.value)
}
}
else {
loading.close()
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<!-- #ifdef MP-ALIPAY -->
<wd-toast v-if="hackAlipayVisible" selector="globalLoading" :closed="closeGlobalLoading" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<wd-toast selector="globalLoading" :closed="closeGlobalLoading" />
<!-- #endif -->
</template>
@@ -0,0 +1,570 @@
# GlobalMessage 全局弹窗组件
## 概述
GlobalMessage 是基于 `wot-design-uni` 的 `wd-message-box` 组件封装的全局弹窗组件,通过 Pinia 状态管理实现全局调用,用于显示提醒、确认、输入等交互场景。
## 组件特性
- 基于 `wd-message-box` 组件封装
- 支持提醒(alert)、确认(confirm)、输入(prompt)三种类型
- 支持成功/失败回调处理
- 自动页面路径检测,确保在正确页面显示
- 虚拟节点设计,不在 DOM 中创建额外层级
- 支持 Promise 链式调用
## 安装使用
### 1. 注册全局组件
在 `App.vue` 或根组件中注册:
```vue
<template>
<div>
<!-- 其他内容 -->
<GlobalMessage />
</div>
</template>
```
### 2. 在组件中使用
```typescript
import { useGlobalMessage } from '@/composables/useGlobalMessage'
const message = useGlobalMessage()
```
## API 文档
### 基础用法
```typescript
// 通用弹窗
message.show({
title: '提示',
message: '这是一条消息'
})
// 字符串参数
message.show('简单提示')
```
### 方法说明
#### show(option)
显示通用弹窗
```typescript
message.show({
title: '标题',
message: '内容',
success: res => console.log('成功', res),
fail: res => console.log('失败', res)
})
```
#### alert(option)
显示提醒弹窗(只有确认按钮)
```typescript
// 简单提醒
message.alert('操作完成')
// 详细配置
message.alert({
title: '提醒',
message: '请注意查看结果',
success: (res) => {
console.log('用户确认了')
}
})
```
#### confirm(option)
显示确认弹窗(有确认和取消按钮)
```typescript
// 简单确认
message.confirm('确定要删除吗?')
// 详细配置
message.confirm({
title: '确认删除',
message: '删除后不可恢复,确定要删除吗?',
success: (res) => {
if (res.action === 'confirm') {
console.log('用户确认删除')
// 执行删除操作
}
},
fail: (res) => {
console.log('用户取消删除')
}
})
```
#### prompt(option)
显示输入弹窗(有输入框和确认取消按钮)
```typescript
// 简单输入
message.prompt('请输入您的姓名')
// 详细配置
message.prompt({
title: '输入信息',
message: '请输入新的名称',
inputValue: '默认值',
inputPlaceholder: '请输入内容',
success: (res) => {
if (res.action === 'confirm') {
console.log('用户输入:', res.value)
}
},
fail: (res) => {
console.log('用户取消输入')
}
})
```
#### close()
手动关闭弹窗
```typescript
message.close()
```
## 参数说明
### GlobalMessageOptions
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| title | string | - | 弹窗标题 |
| message | string | - | 弹窗内容 |
| type | string | - | 弹窗类型:'alert' \| 'confirm' \| 'prompt' |
| showCancelButton | boolean | - | 是否显示取消按钮(自动设置) |
| inputValue | string | - | 输入框默认值(prompt类型) |
| inputPlaceholder | string | - | 输入框占位符(prompt类型) |
| success | Function | - | 成功回调函数 |
| fail | Function | - | 失败回调函数 |
| confirmButtonText | string | '确定' | 确认按钮文本 |
| cancelButtonText | string | '取消' | 取消按钮文本 |
### MessageResult
回调函数接收的参数对象:
| 参数 | 类型 | 说明 |
|------|------|------|
| action | string | 用户操作:'confirm' \| 'cancel' |
| value | string | 输入框的值(仅prompt类型) |
### 方法参数
所有方法都支持两种参数类型:
1. **字符串参数**:直接传入标题文本
2. **选项对象**:传入完整的 GlobalMessageOptions 配置
## 使用示例
### 基础示例
```typescript
export default {
setup() {
const message = useGlobalMessage()
const showAlert = () => {
message.alert('这是一个提醒')
}
const showConfirm = () => {
message.confirm({
title: '确认操作',
message: '确定要继续吗?',
success: (res) => {
if (res.action === 'confirm') {
console.log('用户确认')
}
}
})
}
const showPrompt = () => {
message.prompt({
title: '输入信息',
message: '请输入您的姓名',
success: (res) => {
if (res.action === 'confirm') {
console.log('用户输入:', res.value)
}
}
})
}
return {
showAlert,
showConfirm,
showPrompt
}
}
}
```
### 删除确认示例
```typescript
function handleDelete(item) {
message.confirm({
title: '确认删除',
message: `确定要删除"${item.name}"吗?删除后不可恢复。`,
success: async (res) => {
if (res.action === 'confirm') {
try {
await deleteItem(item.id)
toast.success('删除成功')
// 刷新列表
await fetchList()
}
catch (error) {
toast.error(`删除失败:${error.message}`)
}
}
}
})
}
```
### 批量操作示例
```typescript
function handleBatchDelete(selectedItems) {
message.confirm({
title: '批量删除',
message: `确定要删除选中的 ${selectedItems.length} 个项目吗?`,
success: async (res) => {
if (res.action === 'confirm') {
const loading = useGlobalLoading()
try {
loading.loading('删除中...')
await batchDeleteItems(selectedItems.map(item => item.id))
toast.success('批量删除成功')
await fetchList()
}
catch (error) {
toast.error('批量删除失败')
}
finally {
loading.close()
}
}
}
})
}
```
### 表单验证示例
```typescript
function handleSubmit() {
// 表单验证失败时的提醒
if (!formData.name) {
message.alert({
title: '验证失败',
message: '请输入姓名后再提交',
success: () => {
// 聚焦到输入框
document.getElementById('name-input').focus()
}
})
return
}
// 提交前确认
message.confirm({
title: '确认提交',
message: '确定要提交表单吗?',
success: async (res) => {
if (res.action === 'confirm') {
await submitForm(formData)
}
}
})
}
```
### 修改名称示例
```typescript
function handleEditName(item) {
message.prompt({
title: '修改名称',
message: '请输入新的名称',
inputValue: item.name,
inputPlaceholder: '请输入名称',
success: async (res) => {
if (res.action === 'confirm') {
const newName = res.value.trim()
if (!newName) {
toast.error('名称不能为空')
return
}
if (newName === item.name) {
toast.info('名称未改变')
return
}
try {
await updateItemName(item.id, newName)
toast.success('修改成功')
item.name = newName
}
catch (error) {
toast.error(`修改失败:${error.message}`)
}
}
}
})
}
```
### 退出登录示例
```typescript
function handleLogout() {
message.confirm({
title: '退出登录',
message: '确定要退出当前账号吗?',
confirmButtonText: '退出',
cancelButtonText: '取消',
success: async (res) => {
if (res.action === 'confirm') {
try {
await logout()
// 清除本地数据
clearUserData()
// 跳转到登录页
router.push('/login')
}
catch (error) {
toast.error('退出失败')
}
}
}
})
}
```
### 复杂交互示例
```typescript
function handleComplexOperation() {
// 第一步:确认是否继续
message.confirm({
title: '操作确认',
message: '此操作需要多个步骤,确定要继续吗?',
success: (res) => {
if (res.action === 'confirm') {
// 第二步:输入验证码
message.prompt({
title: '安全验证',
message: '请输入验证码',
inputPlaceholder: '请输入6位验证码',
success: async (res) => {
if (res.action === 'confirm') {
const code = res.value
if (!/^\d{6}$/.test(code)) {
message.alert({
title: '验证失败',
message: '请输入6位数字验证码',
success: () => {
// 重新输入
handleComplexOperation()
}
})
return
}
// 第三步:最终确认
message.confirm({
title: '最终确认',
message: '验证通过,确定要执行操作吗?',
success: async (res) => {
if (res.action === 'confirm') {
try {
await executeComplexOperation(code)
toast.success('操作成功')
}
catch (error) {
toast.error(`操作失败:${error.message}`)
}
}
}
})
}
}
})
}
}
})
}
```
## 实现原理
### 状态管理
- 使用 Pinia 管理全局状态
- 状态包含:`messageOptions`(弹窗配置)、`currentPage`(当前页面路径)
### 页面检测机制
- 调用弹窗方法时记录当前页面路径
- 组件监听状态变化时验证页面路径
- 确保弹窗在正确的页面显示
### Promise 处理
- 使用 `wd-message-box` 的 Promise 接口
- 自动处理成功和失败回调
- 支持链式调用
### 组件配置
```typescript
export default {
options: {
virtualHost: true, // 虚拟节点
addGlobalClass: true, // 支持全局样式
styleIsolation: 'shared' // 样式隔离共享
}
}
```
## 最佳实践
1. **合理选择弹窗类型**
```typescript
// ✅ 根据场景选择
message.alert('操作完成') // 仅通知
message.confirm('确定删除?') // 需要确认
message.prompt('输入名称') // 需要输入
```
2. **提供清晰的标题和内容**
```typescript
// ✅ 清晰明确
message.confirm({
title: '删除确认',
message: '确定要删除这个文件吗?删除后无法恢复。'
})
// ❌ 模糊不清
message.confirm('确定吗?')
```
3. **正确处理回调**
```typescript
// ✅ 完整的回调处理
message.confirm({
title: '确认操作',
message: '确定要继续吗?',
success: (res) => {
if (res.action === 'confirm') {
// 处理确认
}
},
fail: (res) => {
if (res.action === 'cancel') {
// 处理取消
}
}
})
```
4. **输入验证**
```typescript
// ✅ 验证输入内容
message.prompt({
title: '输入信息',
success: (res) => {
if (res.action === 'confirm') {
const value = res.value.trim()
if (!value) {
toast.error('输入不能为空')
}
// 处理输入
}
}
})
```
5. **错误处理**
```typescript
// ✅ 完整的错误处理
message.confirm({
title: '确认操作',
success: async (res) => {
if (res.action === 'confirm') {
try {
await doSomething()
toast.success('操作成功')
}
catch (error) {
toast.error(`操作失败:${error.message}`)
}
}
}
})
```
## 使用场景
1. **确认操作**:删除、修改、提交等重要操作
2. **信息提醒**:操作完成、错误提示、注意事项
3. **数据输入**:快速输入、修改名称、设置参数
4. **流程控制**:多步骤操作、条件判断
5. **安全验证**:密码确认、验证码输入
## 注意事项
1. **回调函数处理**:确保正确处理 success 和 fail 回调
2. **输入验证**:prompt 类型要验证用户输入
3. **页面路径检测**:弹窗会自动在对应页面显示
4. **单例模式**:同时只能显示一个弹窗
5. **按钮样式**:默认按钮为非圆角样式
6. **异步操作**:在回调中执行异步操作要进行错误处理
## 与其他组件结合使用
```typescript
// 与 GlobalLoading 和 GlobalToast 结合使用
function handleCompleteOperation() {
message.confirm({
title: '确认操作',
message: '确定要执行这个操作吗?',
success: async (res) => {
if (res.action === 'confirm') {
const loading = useGlobalLoading()
const toast = useGlobalToast()
try {
loading.loading('处理中...')
await performOperation()
toast.success('操作成功')
}
catch (error) {
toast.error('操作失败')
}
finally {
loading.close()
}
}
}
})
}
```
@@ -0,0 +1,55 @@
<script lang="ts" setup>
import { deepClone, isFunction } from 'wot-design-uni/components/common/util'
const { messageOptions, currentPage } = storeToRefs(useGlobalMessage())
const messageBox = useMessage('globalMessage')
const currentPath = getCurrentPath()
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
hackAlipayVisible.value = true
})
// #endif
watch(() => messageOptions.value, (newVal) => {
if (newVal) {
if (currentPage.value === currentPath) {
const option = deepClone(newVal)
messageBox.show(option).then((res) => {
if (isFunction(option.success)) {
option.success(res)
}
}).catch((err) => {
if (isFunction(option.fail)) {
option.fail(err)
}
})
}
}
else {
messageBox.close()
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<!-- #ifdef MP-ALIPAY -->
<wd-message-box v-if="hackAlipayVisible" selector="globalMessage" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<wd-message-box selector="globalMessage" />
<!-- #endif -->
</template>
+261
View File
@@ -0,0 +1,261 @@
# GlobalToast 全局提示组件
## 概述
GlobalToast 是基于 `wot-design-uni` 的 `wd-toast` 组件封装的全局提示组件,通过 Pinia 状态管理实现全局调用。
## 组件特性
- 基于 `wd-toast` 组件封装
- 支持多种提示类型:成功、错误、信息、警告
- 自动页面路径检测,确保在正确页面显示
- 支持自定义图标、位置、持续时间等
- 虚拟节点设计,不在 DOM 中创建额外层级
## 安装使用
### 1. 注册全局组件
在 `App.vue` 或根组件中注册:
```vue
<template>
<div>
<!-- 其他内容 -->
<GlobalToast />
</div>
</template>
```
### 2. 在组件中使用
```typescript
import { useGlobalToast } from '@/composables/useGlobalToast'
const toast = useGlobalToast()
```
## API 文档
### 基础用法
```typescript
// 普通提示
toast.show('这是一条提示信息')
// 自定义配置
toast.show({
msg: '自定义提示',
duration: 3000,
position: 'top'
})
```
### 快捷方法
#### success(option)
成功提示,显示绿色成功图标
```typescript
toast.success('操作成功')
toast.success({
msg: '保存成功',
duration: 2000
})
```
**默认配置:**
- 图标:success
- 持续时间:1500ms
#### error(option)
错误提示,显示红色错误图标
```typescript
toast.error('操作失败')
toast.error({
msg: '网络错误,请重试',
duration: 3000
})
```
**默认配置:**
- 图标:error
- 方向:vertical(垂直布局)
#### info(option)
信息提示,显示蓝色信息图标
```typescript
toast.info('这是一条信息')
toast.info({
msg: '请注意查看',
position: 'top'
})
```
**默认配置:**
- 图标:info
#### warning(option)
警告提示,显示黄色警告图标
```typescript
toast.warning('警告信息')
toast.warning({
msg: '请谨慎操作',
iconName: 'warning'
})
```
**默认配置:**
- 图标:warning
#### close()
手动关闭提示
```typescript
toast.close()
```
## 参数说明
### ToastOptions
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| msg | string | - | 提示内容 |
| duration | number | 2000 | 持续时间(ms),0表示不自动关闭 |
| position | string | 'middle' | 显示位置:'top' \| 'middle' \| 'bottom' |
| iconName | string | - | 图标名称 |
| direction | string | - | 布局方向:'horizontal' \| 'vertical' |
| cover | boolean | false | 是否显示遮罩 |
| show | boolean | - | 是否显示(内部使用) |
### 方法参数
所有方法都支持两种参数类型:
1. **字符串参数**:直接传入提示文本
2. **选项对象**:传入完整的 ToastOptions 配置
## 使用示例
### 基础示例
```typescript
export default {
setup() {
const toast = useGlobalToast()
const handleSuccess = () => {
toast.success('操作成功!')
}
const handleError = () => {
toast.error('操作失败,请重试')
}
const handleCustom = () => {
toast.show({
msg: '自定义提示',
duration: 5000,
position: 'top',
iconName: 'info'
})
}
return {
handleSuccess,
handleError,
handleCustom
}
}
}
```
### 异步操作示例
```typescript
async function handleSubmit() {
const toast = useGlobalToast()
try {
// 提交数据
await submitData()
toast.success('提交成功')
}
catch (error) {
toast.error(`提交失败:${error.message}`)
}
}
```
### 长文本提示
```typescript
function showLongMessage() {
toast.info({
msg: '这是一条很长的提示信息,可能需要更长的显示时间',
duration: 4000,
position: 'top'
})
}
```
## 实现原理
### 状态管理
- 使用 Pinia 管理全局状态
- 状态包含:`toastOptions`(提示配置)、`currentPage`(当前页面路径)
### 页面检测机制
- 调用提示方法时记录当前页面路径
- 组件监听状态变化时验证页面路径
- 确保提示信息在正确的页面显示
### 组件配置
```typescript
export default {
options: {
virtualHost: true, // 虚拟节点
addGlobalClass: true, // 支持全局样式
styleIsolation: 'shared' // 样式隔离共享
}
}
```
## 最佳实践
1. **选择合适的提示类型**
- 成功操作使用 `success()`
- 错误信息使用 `error()`
- 一般信息使用 `info()`
- 警告信息使用 `warning()`
2. **控制显示时长**
- 简短消息:1500-2000ms
- 长消息:3000-4000ms
- 重要消息:可设置为0,用户手动关闭
3. **位置选择**
- 一般提示:middle(默认)
- 不遮挡内容:top 或 bottom
- 重要提示:middle + cover
4. **避免频繁调用**
```typescript
// ❌ 避免快速连续调用
toast.success('第一条')
toast.success('第二条') // 会覆盖第一条
// ✅ 推荐做法
toast.success('操作完成')
```
## 注意事项
1. 组件需要在应用根组件中注册才能正常使用
2. 提示会自动在对应页面显示,切换页面时会自动关闭
3. 同时只能显示一个提示,新的提示会覆盖旧的提示
4. 使用 `duration: 0` 时记得手动调用 `close()` 关闭
@@ -0,0 +1,46 @@
<script lang="ts" setup>
const { toastOptions, currentPage } = storeToRefs(useGlobalToast())
const { close: closeGlobalToast } = useGlobalToast()
const toast = useToast('globalToast')
const currentPath = getCurrentPath()
// #ifdef MP-ALIPAY
const hackAlipayVisible = ref(false)
nextTick(() => {
hackAlipayVisible.value = true
})
// #endif
watch(() => toastOptions.value, (newVal) => {
if (newVal && newVal.show) {
if (currentPage.value === currentPath) {
toast.show(toastOptions.value)
}
}
else {
toast.close()
}
})
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<!-- #ifdef MP-ALIPAY -->
<wd-toast v-if="hackAlipayVisible" selector="globalToast" :closed="closeGlobalToast" />
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<wd-toast selector="globalToast" :closed="closeGlobalToast" />
<!-- #endif -->
</template>
+188
View File
@@ -0,0 +1,188 @@
<script setup lang="ts">
import type { PickerViewInstance } from 'wot-design-uni/components/wd-picker-view/types'
import { ref, watch } from 'vue'
const props = defineProps({
modelValue: {
type: [Number, String],
},
data: {
required: true,
type: Array as () => OptionType[],
},
label: {
type: String,
default: '',
},
required: {
type: Boolean,
default: false,
},
})
const emits = defineEmits(['update:modelValue'])
// 定义响应式变量
const selectedValues = ref<number[] | string[]>([])
const pickerColumns = ref<
Array<Array<{ label: string, value: string | number }>>
>([])
// 监听 modelValue 的变化,更新 selectedValues
watch(
() => props.modelValue,
(val) => {
selectedValues.value = val ? findTreePath(val) : []
},
)
/**
* 根据节点值查找路径
* 示例数据: [{"value":"1","label":"公司","children":[{"value":"2","label":"研发部"}]}]
* 查找部门ID为2的路径,返回结果:[1, 2]
*/
function findTreePath(value: number | string): number[] | string[] {
const numberPath: number[] = []
const stringPath: string[] = []
const list = props.data
const find = (value: number | string, list: OptionType[]): boolean => {
for (const item of list) {
if (item.value === value) {
typeof value === 'number'
? numberPath.push(value)
: stringPath.push(value)
return true
}
if (item.children?.length) {
typeof item.value === 'number'
? numberPath.push(item.value)
: stringPath.push(item.value)
if (find(value, item.children))
return true
typeof item.value === 'number' ? numberPath.pop() : stringPath.pop()
}
}
return false
}
find(value, list)
return typeof value === 'number' ? numberPath : stringPath
}
/**
* 将树形数据转换为 Picker 所需的 columns 格式
*
* @param treeData 树形数据
* @returns Picker 所需的 columns 格式
*/
function transformTreeToColumns(treeData: OptionType[]): Array<Array<{ label: string, value: string | number }>> {
const columns: Array<Array<{ label: string, value: string | number }>> = []
for (let depth = 0; depth <= selectedValues.value.length; depth++) {
const currentColumn = treeData.map(node => ({
label: node.label,
value: node.value,
}))
if (!currentColumn.length)
break
const selectedId = selectedValues.value[depth]
if (!currentColumn.some(item => item.value === selectedId)) {
selectedValues.value[depth] = currentColumn[0]?.value
}
columns.push(currentColumn)
const selectedNode = treeData.find(
node => node.value === selectedValues.value[depth],
)
treeData = selectedNode?.children || []
}
return columns
}
// 监听 data 的变化,更新 pickerColumns
watch(
() => props.data,
(val) => {
console.log('监听 data 的变化', val)
pickerColumns.value = transformTreeToColumns(val)
},
{
immediate: true,
},
)
/**
* 处理列的变化,动态更新后续列的数据
*/
function handleColumnChange(
pickerView: PickerViewInstance,
value: Record<string, any> | Record<string, any>[],
columnIndex: number,
resolve: () => void,
) {
const selectedValue
= selectedValues.value[selectedValues.value.length - 1] || undefined
emits('update:modelValue', selectedValue)
const item = Array.isArray(value) ? value[columnIndex] : value.value
updatePickerColumns(pickerView, item.value, columnIndex)
resolve()
}
/**
* 动态更新所有后续列的数据
*/
function updatePickerColumns(
pickerView: PickerViewInstance,
parentId: string | number,
columnIndex: number,
) {
const nextColumnIndex = columnIndex + 1
const children = findChildren(parentId, props.data)
if (children.length > 0 && nextColumnIndex < 3) {
pickerView.setColumnData(nextColumnIndex, children)
updatePickerColumns(pickerView, children[0].value, nextColumnIndex)
}
}
/**
* 根据节点value查找其子节点数据
*/
function findChildren(
parentId: string | number,
list: Record<string, any>[],
): Record<string, any>[] {
for (const item of list) {
if (item.value === parentId && item.children) {
return item.children
}
if (item.children) {
const children = findChildren(parentId, item.children)
if (children.length)
return children
}
}
return []
}
// 格式化显示选中项(显示最后一个子节点的 label)
function displayFormat(items: any) {
return items.length > 0 ? items[items.length - 1].label : ''
}
</script>
<template>
<wd-picker
v-model="selectedValues"
:columns="pickerColumns"
:display-format="displayFormat"
:column-change="handleColumnChange"
:label="label"
:placeholder="pickerColumns.length ? '请选择' : '加载中...'"
:required="required"
/>
</template>
@@ -0,0 +1,173 @@
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
interface Props {
title?: string // 标题
desc?: string // 描述
subDesc?: string // 字描述
protocol?: string // 协议名称
}
withDefaults(defineProps<Props>(), {
title: '用户隐私保护提示',
desc: '感谢您使用本应用,您使用本应用的服务之前请仔细阅读并同意',
subDesc: '。当您点击同意并开始时用产品服务时,即表示你已理解并同意该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用相应服务。',
protocol: '《用户隐私保护指引》',
})
const emit = defineEmits(['agree', 'disagree'])
const showPopup = ref<boolean>(false) // 是否展示popup
const privacyResolves = ref(new Set()) // onNeedPrivacyAuthorization的reslove
function privacyHandler(resolve: any) {
showPopup.value = true
privacyResolves.value.add(resolve)
}
onBeforeMount(() => {
// 注册监听
if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization((resolve: any) => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}
})
/**
* 同意隐私协议
*/
function handleAgree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'agree',
buttonId: 'agree-btn',
})
})
privacyResolves.value.clear()
emit('agree')
}
/**
* 拒绝隐私协议
*/
function handleDisagree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'disagree',
})
})
privacyResolves.value.clear()
}
/**
* 打开隐私协议
*/
function openPrivacyContract() {
wx.openPrivacyContract({})
}
/**
* 弹出框关闭时清空
*/
function handleClose() {
privacyResolves.value.clear()
}
</script>
<script lang="ts">
export default {
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<view>
<wd-popup v-model="showPopup" :close-on-click-modal="false" custom-class="wd-privacy-popup" @close="handleClose">
<view class="wd-privacy-popup__header">
<!-- 标题 -->
<view class="wd-picker__title">
{{ title }}
</view>
</view>
<view class="wd-privacy-popup__container">
<text>{{ desc }}</text>
<text class="wd-privacy-popup__container-protocol" @click="openPrivacyContract">
{{ protocol }}
</text>
<text>{{ subDesc }}</text>
</view>
<view class="wd-privacy-popup__footer">
<button id="disagree-btn" class="is-block is-round is-medium is-plain wd-privacy-popup__footer-disagree wd-button" @click="handleDisagree">
拒绝
</button>
<button
id="agree-btn"
class="wd-button is-block is-round is-medium is-primary wd-privacy-popup__footer-agree"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree"
>
同意
</button>
</view>
</wd-popup>
</view>
</template>
<style lang="scss" scoped>
@import 'wot-design-uni/components/wd-button/index.scss';
:deep(.wd-privacy-popup) {
width: 600rpx;
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 32rpx;
overflow: hidden;
}
.wd-privacy-popup {
&__header {
width: 100%;
height: 128rpx;
line-height: 128rpx;
color: rgba(0, 0, 0, 0.85);
font-size: 30rpx;
padding: 0 12rpx;
box-sizing: border-box;
}
&__container {
width: 100%;
box-sizing: border-box;
padding: 0 12rpx;
margin-bottom: 32rpx;
font-size: 28rpx;
line-height: 1.8;
color: #3e3e3e;
text-align: left;
font-weight: 550;
&-protocol {
color: #4d80f0;
}
}
&__footer {
display: flex;
justify-content: space-between;
padding-bottom: 36rpx;
button {
border: none;
outline: none;
}
}
}
</style>
@@ -0,0 +1,46 @@
import type { ConfigProviderThemeVars } from 'wot-design-uni'
/**
* 主题色选项接口
*/
export interface ThemeColorOption {
name: string
value: string
primary: string
}
/**
* 主题类型
*/
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: '#4D7FFF' },
{ name: '活力橙', value: 'orange', primary: '#FF7D00' },
{ name: '薄荷绿', value: 'green', primary: '#07C160' },
{ name: '樱花粉', value: 'pink', primary: '#FF69B4' },
{ name: '紫罗兰', value: 'purple', primary: '#8A2BE2' },
{ name: '朱砂红', value: 'red', primary: '#FF4757' },
]
@@ -0,0 +1,37 @@
import type { ToastOptions } from 'wot-design-uni/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,52 @@
import type { MessageOptions, MessageResult } from 'wot-design-uni/components/wd-message-box/types'
import { defineStore } from 'pinia'
export type GlobalMessageOptions = MessageOptions & {
success?: (res: MessageResult) => void
fail?: (res: MessageResult) => void
}
interface GlobalMessage {
messageOptions: GlobalMessageOptions | null
currentPage: string
}
export const useGlobalMessage = defineStore('global-message', {
state: (): GlobalMessage => ({
messageOptions: null,
currentPage: '',
}),
actions: {
show(option: GlobalMessageOptions | string) {
this.currentPage = getCurrentPath()
this.messageOptions = {
...(CommonUtil.isString(option) ? { title: option } : option),
cancelButtonProps: {
round: false,
},
confirmButtonProps: {
round: false,
},
}
},
alert(option: GlobalMessageOptions | string) {
const messageOptions = CommonUtil.deepMerge({ type: 'alert' }, CommonUtil.isString(option) ? { title: option } : option) as MessageOptions
messageOptions.showCancelButton = false
this.show(messageOptions)
},
confirm(option: GlobalMessageOptions | string) {
const messageOptions = CommonUtil.deepMerge({ type: 'confirm' }, CommonUtil.isString(option) ? { title: option } : option) as MessageOptions
messageOptions.showCancelButton = true
this.show(messageOptions)
},
prompt(option: GlobalMessageOptions | string) {
const messageOptions = CommonUtil.deepMerge({ type: 'prompt' }, CommonUtil.isString(option) ? { title: option } : option) as MessageOptions
messageOptions.showCancelButton = true
this.show(messageOptions)
},
close() {
this.messageOptions = null
this.currentPage = ''
},
},
})
@@ -0,0 +1,61 @@
import type { ToastOptions } from 'wot-design-uni/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,142 @@
import type { ThemeColorOption, ThemeMode } from '@/composables/types/theme'
import { themeColorOptions } from '@/composables/types/theme'
/**
* 完整版主题管理组合式API
*
* 功能特性:
* - 支持手动切换暗黑模式
* - 支持主题色选择
* - 支持跟随系统主题
* - 自动同步导航栏颜色
* - 持久化用户设置
*
* 适用场景:
* - 需要用户手动控制主题的应用
* - 需要主题色自定义的应用
* - 需要完整主题管理功能的复杂应用
*
* @example
* ```vue
* <script setup>
* import { useManualTheme } from '@/composables/useManualTheme'
*
* const {
* theme,
* isDark,
* toggleTheme,
* openThemeColorPicker,
* currentThemeColor,
* themeVars
* } = useManualTheme()
* </script>
*
* <template>
* <wd-config-provider :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)
/**
* 切换暗黑模式
* @param mode 指定主题模式,不传则自动切换
* @param isFollw 是否跟随系统
*/
function toggleTheme(mode?: ThemeMode, isFollw: boolean = false) {
store.toggleTheme(mode, isFollw)
}
/**
* 打开主题色选择器
*/
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(() => {
initTheme()
// 监听系统主题变化
if (typeof uni !== 'undefined' && uni.onThemeChange) {
uni.onThemeChange((res) => {
if (store.followSystem) {
toggleTheme(res.theme as ThemeMode, true)
}
})
}
})
// 页面显示时更新导航栏颜色,确保每次切换页面时导航栏颜色都是正确的
onShow(() => {
store.setNavigationBarColor()
})
// 组件卸载时清理监听
onUnmounted(() => {
if (typeof uni !== 'undefined' && uni.offThemeChange) {
uni.offThemeChange((res) => {
if (store.followSystem) {
toggleTheme(res.theme as ThemeMode, true)
}
})
}
})
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 }
+73
View File
@@ -0,0 +1,73 @@
export interface TabbarItem {
name: string
value: number | null
active: boolean
title: string
icon: string
}
const tabbarItems = ref<TabbarItem[]>([
// { name: 'home', value: null, active: true, title: '首页', icon: 'home' },
// { name: 'about', value: null, active: false, title: '关于', icon: 'user' },
{
name: 'home',
value: null,
active: true,
title: '首页',
icon: 'home',
},
{
name: 'work',
value: null,
active: false,
title: '工作台',
icon: 'app',
},
{
name: 'mine',
value: null,
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 && item.value ? item.value : null
}
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,
}
}
+70
View File
@@ -0,0 +1,70 @@
import type { ThemeMode } from '@/composables/types/theme'
/**
* 简化版系统主题管理组合式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-vars="themeVars">
* <view :class="{ 'dark-mode': isDark }">
* <text>当前主题: {{ theme }}</text>
* </view>
* </wd-config-provider>
* </template>
* ```
*/
export function useTheme() {
const store = useThemeStore()
// 组件挂载前初始化系统主题
onBeforeMount(() => {
store.initSystemTheme()
// 监听系统主题变化
if (typeof uni !== 'undefined' && uni.onThemeChange) {
uni.onThemeChange((res) => {
// 系统主题变化时自动更新,导航栏颜色由 theme.json 自动处理
store.setTheme(res.theme as ThemeMode)
console.log('系统主题已切换至:', res.theme)
})
}
})
// 组件卸载时清理监听
onUnmounted(() => {
if (typeof uni !== 'undefined' && uni.offThemeChange) {
uni.offThemeChange((res) => {
store.setTheme(res.theme as ThemeMode)
})
}
})
return {
// 状态(只读)
theme: computed(() => store.theme),
isDark: computed(() => store.isDark),
themeVars: computed(() => store.themeVars),
}
}
+6
View File
@@ -0,0 +1,6 @@
/**
* 常量统一导出
*/
// 存储相关常量
export * from './storage.constant'
@@ -0,0 +1,19 @@
/**
* 存储键常量统一管理
* 包括 localStorage、sessionStorage 等各种存储的键名
*/
// 访问token
export const APP_ACCESS_TOKEN_KEY = 'appAccessToken'
// 刷新token
export const APP_REFRESH_TOKEN_KEY = 'appRefreshToken'
// 用户缓存相关
export const APP_USER_INFO = 'appUserInfo'
// 手动主题配置
export const APP_MANUAL_THEME_KEY = 'manualTheme'
// 全局主题配置
export const APP_THEME_KEY = 'theme'
@@ -0,0 +1 @@
<view></view>
@@ -0,0 +1,5 @@
// eslint-disable-next-line no-undef
Component({
data: {},
methods: {},
})
+44
View File
@@ -0,0 +1,44 @@
/**
* API响应码枚举
*/
export enum ApiCode {
/**
* 成功
*/
SUCCESS = 0,
/**
* 通用错误
*/
ERROR = 1,
/**
* 异常
*/
EXCEPTION = -1,
/**
* 未授权访问
*/
UNAUTHORIZED = 10403,
/**
* 令牌已过期
*/
TOKEN_EXPIRED = 10401,
/**
* 参数校验失败
*/
PARAM_INVALID = 400,
/**
* 资源不存在
*/
NOT_FOUND = 404,
/**
* 服务器内部错误
*/
INTERNAL_ERROR = 500,
}
+15
View File
@@ -0,0 +1,15 @@
/**
* API 请求头相关枚举封装
*/
export enum ApiHeader {
KEY = 'Content-Type',
/* 表单数据 */
FORM = 'application/x-www-form-urlencoded',
/* JSON 数据 */
JSON = 'application/json',
/* 多部分数据 */
MULTIPART = 'multipart/form-data',
}
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+114
View File
@@ -0,0 +1,114 @@
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
import type { IResponse } from '../types'
import AdapterUniapp from '@alova/adapter-uniapp'
import { createAlova } from 'alova'
import { createServerTokenAuthentication } from 'alova/client'
import VueHook from 'alova/vue'
import { useUserStore } from '@/store/userStore'
// 移除了 @dcloudio/types 导入,该模块不存在
import { toLoginPage } from '@/utils/toLoginPage'
import { ContentTypeEnum, ResultEnum, ShowMessage } from '../tools/enum'
// 配置动态Tag
export const API_DOMAINS = {
DEFAULT: import.meta.env.VITE_API_BASE_URL || '',
SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY || '',
}
/**
* 创建请求实例
*/
const { onAuthRequired, onResponseRefreshToken }
= createServerTokenAuthentication<typeof VueHook, typeof uniappRequestAdapter>({
// 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
refreshTokenOnError: {
isExpired: (error) => {
return error.response?.status === ResultEnum.Unauthorized
},
handler: async () => {
try {
// await authLogin();
}
catch (error) {
// 切换到登录页
toLoginPage({ mode: 'reLaunch' })
throw error
}
},
},
})
/**
* alova 请求实例
*/
const alovaInstance = createAlova({
baseURL: `${API_DOMAINS.DEFAULT}${import.meta.env.VITE_APP_BASE_API || ''}`,
...AdapterUniapp(),
timeout: 10000,
statesHook: VueHook,
beforeRequest: onAuthRequired((method) => {
// 设置默认 Content-Type
method.config.headers = {
ContentType: ContentTypeEnum.JSON,
Accept: 'application/json, text/plain, */*',
...method.config.headers,
}
const { config } = method
// 修复认证逻辑:如果 ignoreAuth 为 true,则忽略认证
// 特殊处理验证码和登录请求,总是跳过认证
const isCaptchaRequest = method.url.includes('/captcha/get')
const isLoginRequest = method.url.includes('/system/auth/login')
const shouldAuth
= !isCaptchaRequest && !isLoginRequest && !config.meta?.ignoreAuth
// 处理认证信息 自行处理认证问题
if (shouldAuth) {
const userStore = useUserStore()
const token = userStore.getAccessToken()
// 只在需要认证时检查 token,验证码请求跳过
if (!token && !isCaptchaRequest) {
throw new Error('[请求错误]:未登录')
}
// 设置认证头
if (token) {
method.config.headers.Authorization = `Bearer ${token}`
}
}
// 处理动态域名
if (config.meta?.domain) {
method.baseURL = config.meta.domain
}
}),
responded: onResponseRefreshToken((response, method) => {
const { config } = method
const { requestType } = config
const { statusCode, data: rawData } = response as any
// 处理特殊请求类型(上传/下载)
if (requestType === 'upload' || requestType === 'download') {
return response
}
// 处理 HTTP 状态码错误
if (statusCode !== 200) {
const errorMessage
= rawData?.msg || rawData?.message || (rawData as any)?.error || ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
throw new Error(errorMessage)
}
// 处理业务逻辑错误
const { code, message, msg, data } = rawData as IResponse
// 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
const errorMessage = msg || message || '请求错误'
throw new Error(errorMessage)
}
// 处理成功响应,返回业务数据
return data
}),
})
export const http = alovaInstance

Some files were not shown because too many files have changed in this diff Show More