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
+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
+171
View File
@@ -0,0 +1,171 @@
import type { CustomRequestOptions, IResponse } from '../types'
import { useUserStore } from '@/store/userStore'
import { toLoginPage } from '@/utils/toLoginPage'
import { ResultEnum } from '../tools/enum'
// 配置 baseURL
const BASE_URL = import.meta.env.VITE_API_BASE_URL || ''
const API_PREFIX = import.meta.env.VITE_APP_BASE_API || ''
export function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return new Promise<T>((resolve, reject) => {
// 如果 url 不包含 http:// 或 https://,则添加 baseURL 和 API_PREFIX
let url = options.url
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `${BASE_URL}${API_PREFIX}${url}`
}
uni.request({
...options,
url,
dataType: 'json',
// #ifndef MP-WEIXIN
responseType: 'json',
// #endif
// 响应成功
success: async (res) => {
const userStore = useUserStore()
const responseData = res.data as IResponse<T>
const { code } = responseData
// 检查是否是401错误(包括HTTP状态码401或业务码401)
const isTokenExpired = res.statusCode === 401 || code === 401
if (isTokenExpired) {
// 清理用户信息,跳转到登录页
userStore.clearAll()
userStore.logout()
toLoginPage()
return reject(res)
}
// 处理其他成功状态(HTTP状态码200-299)
if (res.statusCode >= 200 && res.statusCode < 300) {
// 处理业务逻辑错误
if (code !== ResultEnum.Success0) {
uni.showToast({
icon: 'error',
title: responseData.msg || responseData.message || '请求错误',
})
return reject(responseData)
}
return resolve(responseData.data)
}
// 处理其他错误
if (!options.hideErrorToast) {
uni.showToast({
icon: 'error',
title: (res.data as any).msg || '请求错误',
})
}
reject(res)
},
// 响应失败
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}
/**
* GET 请求
* @param url 后台地址
* @param query 请求query参数
* @param header 请求头,默认为json格式
* @returns 返回包含响应数据的 Promise
*/
export function httpGet<T>(
url: string,
query?: Record<string, any>,
header?: Record<string, any>,
options?: Partial<CustomRequestOptions>,
) {
return http<T>({
url,
query,
method: 'GET',
header,
...options,
})
}
/**
* POST 请求
* @param url 后台地址
* @param data 请求body参数
* @param query 请求query参数,post请求也支持query,很多微信接口都需要
* @param header 请求头,默认为json格式
* @returns 返回包含响应数据的 Promise
*/
export function httpPost<T>(
url: string,
data?: Record<string, any>,
query?: Record<string, any>,
header?: Record<string, any>,
options?: Partial<CustomRequestOptions>,
) {
return http<T>({
url,
query,
data,
method: 'POST',
header,
...options,
})
}
/**
* PUT 请求
*/
export function httpPut<T>(
url: string,
data?: Record<string, any>,
query?: Record<string, any>,
header?: Record<string, any>,
options?: Partial<CustomRequestOptions>,
) {
return http<T>({
url,
data,
query,
method: 'PUT',
header,
...options,
})
}
/**
* DELETE 请求(无请求体,仅 query)
*/
export function httpDelete<T>(
url: string,
query?: Record<string, any>,
header?: Record<string, any>,
options?: Partial<CustomRequestOptions>,
) {
return http<T>({
url,
query,
method: 'DELETE',
header,
...options,
})
}
// 支持与 axios 类似的API调用
http.get = httpGet
http.post = httpPost
http.put = httpPut
http.delete = httpDelete
// 支持与 AlovaJS 类似的API调用
http.Get = httpGet
http.Post = httpPost
http.Put = httpPut
http.Delete = httpDelete
+15
View File
@@ -0,0 +1,15 @@
// 导出类型
import type { CustomRequestOptions, IResponse } from './types'
// 导出请求适配器
import { http as alovaHttp } from './adapters/alova'
import { http as nativeHttp } from './adapters/http'
// 导出默认请求实例(默认使用 alova 请求实例)
export const http = alovaHttp
// 导出所有类型
export type { CustomRequestOptions, IResponse }
// 导出请求适配器,允许手动选择使用哪种请求方式
export { alovaHttp, nativeHttp }
+69
View File
@@ -0,0 +1,69 @@
import type { CustomRequestOptions } from '@/http/types'
import { useUserStore } from '@/store/userStore'
import { stringifyQuery } from './tools/queryString'
const userStore = useUserStore()
// 请求基准地址
const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: CustomRequestOptions) {
// 如果您使用了alova,则请把下面的代码放开注释
// alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded
// return options
// 非 alova 请求,正常执行
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
const queryStr = stringifyQuery(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`
}
else {
options.url += `?${queryStr}`
}
}
// 非 http 开头需拼接地址
if (!options.url.startsWith('http')) {
// #ifdef H5
if (JSON.parse(import.meta.env.VITE_APP_PROXY_ENABLE)) {
// 自动拼接代理前缀
options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
}
else {
options.url = baseUrl + options.url
}
// #endif
// 非H5正常拼接
// #ifndef H5
options.url = baseUrl + options.url
// #endif
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
}
// 1. 请求超时
options.timeout = 60000 // 60s
// 2. (可选)添加小程序端请求头标识
options.header = {
...options.header,
}
// 3. 添加 token 请求头标识
const token = userStore.getAccessToken()
if (token) {
options.header.Authorization = `Bearer ${token}`
}
return options
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
},
}
+68
View File
@@ -0,0 +1,68 @@
export enum ResultEnum {
// 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
Success0 = 0, // 成功
Success200 = 200, // 成功
Error = -1, // 错误
Unauthorized = 401, // 未授权
Forbidden = 403, // 禁止访问(原为forbidden)
NotFound = 404, // 未找到(原为notFound)
MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
RequestTimeout = 408, // 请求超时(原为requestTimeout)
InternalServerError = 500, // 服务器错误(原为internalServerError)
NotImplemented = 501, // 未实现(原为notImplemented)
BadGateway = 502, // 网关错误(原为badGateway)
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
}
export enum ContentTypeEnum {
JSON = 'application/json;charset=UTF-8',
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}
/**
* 根据状态码,生成对应的错误信息
* @param {number|string} status 状态码
* @returns {string} 错误信息
*/
export function ShowMessage(status: number | string): string {
let message: string
switch (status) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 403:
message = '拒绝访问(403)'
break
case 404:
message = '请求出错(404)'
break
case 408:
message = '请求超时(408)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
case 504:
message = '网络超时(504)'
break
case 505:
message = 'HTTP版本不受支持(505)'
break
default:
message = `连接出错(${status})!`
}
return `${message},请检查网络或联系管理员!`
}
@@ -0,0 +1,29 @@
/**
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
* 支持基本类型值和数组,不支持嵌套对象
* @param obj 要序列化的对象
* @returns 序列化后的查询字符串
*/
export function stringifyQuery(obj: Record<string, any>): string {
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
return ''
return Object.entries(obj)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
// 对键进行编码
const encodedKey = encodeURIComponent(key)
// 处理数组类型
if (Array.isArray(value)) {
return value
.filter(item => item !== undefined && item !== null)
.map(item => `${encodedKey}=${encodeURIComponent(item)}`)
.join('&')
}
// 处理基本类型
return `${encodedKey}=${encodeURIComponent(value)}`
})
.join('&')
}
+50
View File
@@ -0,0 +1,50 @@
/**
* 在 uniapp 的 RequestOptions 和 UploadFileOption 基础上,添加自定义参数
*/
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
} & UniApp.UploadFileOption // 添加uni.uploadFile参数类型
/** 主要提供给 openapi-ts-request 生成的代码使用 */
export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
export interface HttpRequestResult<T> {
promise: Promise<T>
requestTask: UniApp.RequestTask
}
// 通用响应格式(兼容 msg + message 字段)
export type IResponse<T = any>
= | {
code: number
data: T
msg: string
status_code: number
success: boolean
[key: string]: any // 允许额外属性
}
| {
code: number
data: T
msg: string
status_code: number
success: boolean
[key: string]: any // 允许额外属性
}
// 分页请求参数
export interface PageParams {
page: number
pageSize: number
[key: string]: any
}
// 分页响应数据
export interface PageResult<T> {
list: T[]
total: number
page: number
pageSize: number
}
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts" setup>
</script>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<slot />
</template>
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts" setup>
const router = useRouter()
const route = useRoute()
const { activeTabbar, getTabbarItemValue, setTabbarItemActive, tabbarList } = useTabbar()
function handleTabbarChange({ value }: { value: string }) {
setTabbarItemActive(value)
router.pushTab({ name: value })
}
onMounted(() => {
// #ifdef APP
uni.hideTabBar()
// #endif
nextTick(() => {
if (route.name && route.name !== activeTabbar.value.name) {
setTabbarItemActive(route.name)
}
})
})
</script>
<script lang="ts">
export default {
options: {
addGlobalClass: true,
virtualHost: true,
styleIsolation: 'shared',
},
}
</script>
<template>
<slot />
<wd-gap safe-area-bottom height="var(--wot-tabbar-height, 50px)" />
<wd-tabbar
:model-value="activeTabbar.name" bordered safe-area-inset-bottom fixed
@change="handleTabbarChange"
>
<wd-tabbar-item
v-for="(item, index) in tabbarList" :key="index" :name="item.name"
:value="getTabbarItemValue(item.name)" :title="item.title" :icon="item.icon"
/>
</wd-tabbar>
</template>
+15
View File
@@ -0,0 +1,15 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import router from './router'
import 'uno.css'
const pinia = createPinia()
pinia.use(persistPlugin)
export function createApp() {
const app = createSSRApp(App)
app.use(router)
app.use(pinia)
return {
app,
}
}
+83
View File
@@ -0,0 +1,83 @@
{
"name": "wot-starter",
"appid": "__UNI__1208592",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"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": {},
"sdkConfigs": {}
}
},
"app-harmony": {
"distribute": {}
},
"mp-harmony": {
"distribute": {}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true,
"optimization": {
"subPackages": true
},
"darkmode": true,
"themeLocation": "theme.json"
},
"mp-alipay": {
"usingComponents": true,
"compileOptions": {
"globalObjectMode": "enable",
"treeShaking": true
}
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"darkmode": true,
"themeLocation": "theme.json"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"format": 2,
"compileOptions": {
"globalObjectMode": "enable"
}
}
+290
View File
@@ -0,0 +1,290 @@
{
"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
},
"pages": [
// #ifdef APP || MP-WEIXIN
{
"path": "pages/login/index",
"type": "page"
},
// #endif
{
"path": "pages/login/index",
"type": "page",
"name": "login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/index/index",
"type": "home",
"name": "home",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/mine/index",
"type": "page",
"name": "mine",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/work/index",
"type": "page",
"name": "work",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "工作台"
}
}
],
"subPackages": [
{
"root": "subPages",
"pages": [
// GENERATED BY UNI-PAGES, PLATFORM: APP || H5
{
"path": "ci/index",
"type": "page",
"name": "ci",
"style": {
"navigationBarTitleText": "CI 持续集成"
}
},
{
"path": "create-uni/index",
"type": "page",
"name": "create-uni",
"style": {
"navigationBarTitleText": "CreateUni 脚手架"
}
},
{
"path": "feedback/index",
"type": "page",
"name": "feedback",
"style": {
"navigationBarTitleText": "全局反馈"
}
},
{
"path": "icon/index",
"type": "page",
"name": "icon",
"style": {
"navigationBarTitleText": "图标演示"
}
},
{
"path": "pinia/index",
"type": "page",
"name": "pinia",
"style": {
"navigationBarTitleText": "Pinia 演示"
}
},
{
"path": "request/index",
"type": "page",
"name": "request",
"style": {
"navigationBarTitleText": "网络请求"
}
},
{
"path": "router/demo-aftereach",
"type": "page",
"name": "demo-aftereach",
"style": {
"navigationBarTitleText": "afterEach 钩子演示"
}
},
{
"path": "router/demo-guard",
"type": "page",
"name": "demo-guard",
"style": {
"navigationBarTitleText": "导航守卫演示"
}
},
{
"path": "router/demo-object",
"type": "page",
"name": "demo-object",
"style": {
"navigationBarTitleText": "对象路径跳转"
}
},
{
"path": "router/demo-params",
"type": "page",
"name": "demo-params",
"style": {
"navigationBarTitleText": "参数接收演示"
}
},
{
"path": "router/demo-protected",
"type": "page",
"name": "demo-protected",
"style": {
"navigationBarTitleText": "受保护页面"
}
},
{
"path": "router/demo-query",
"type": "page",
"name": "demo-query",
"style": {
"navigationBarTitleText": "查询参数接收演示"
}
},
{
"path": "router/demo-string",
"type": "page",
"name": "demo-string",
"style": {
"navigationBarTitleText": "字符串路径跳转"
}
},
{
"path": "router/index",
"type": "page",
"name": "router",
"style": {
"navigationBarTitleText": "路由管理"
}
},
{
"path": "styles/index",
"type": "page",
"name": "styles",
"style": {
"navigationBarTitleText": "UnoCSS 演示"
}
},
{
"path": "uni-ku-root/index",
"type": "page",
"name": "root",
"style": {
"navigationBarTitleText": "uni-ku/root"
}
},
// #ifdef H5
{
"path": "about/index",
"type": "page",
"name": "about",
"style": {
"navigationBarTitleText": "关于"
}
},
// #endif
// #ifdef H5
{
"path": "account/index",
"type": "page",
"name": "account",
"style": {
"navigationBarTitleText": "账号设置"
}
},
// #endif
// #ifdef H5
{
"path": "asyncEcharts/index",
"type": "page",
"name": "echartsAsync",
"style": {
"navigationBarTitleText": "echarts 异步跨包示例"
}
},
// #endif
// #ifdef H5
{
"path": "echarts/index",
"type": "page",
"name": "echarts",
"style": {
"navigationBarTitleText": "Echarts 演示"
}
},
// #endif
// #ifdef H5
{
"path": "profile/index",
"type": "page",
"name": "profile",
"style": {
"navigationBarTitleText": "个人中心"
}
},
// #endif
// #ifdef H5
{
"path": "settings/index",
"type": "page",
"name": "settings",
"style": {
"navigationBarTitleText": "系统设置"
}
},
// #endif
// #ifdef H5
{
"path": "skills/index",
"type": "page",
"name": "skills",
"layout": "default",
"style": {
"navigationBarTitleText": "Agent Skills",
"navigationBarBackgroundColor": "#f9fafb",
"navigationBarTextStyle": "black"
}
}
// #endif
]
}
],
"tabBar": {
"custom": true,
"customize": true,
"overlay": true,
"height": "0",
"color": "@tabColor",
"selectedColor": "@tabSelectedColor",
"backgroundColor": "@tabBgColor",
"borderStyle": "@tabBorderStyle",
"list": [
// #ifdef APP || H5 || MP-WEIXIN
{
"pagePath": "pages/index/index"
},
// #endif
{
"pagePath": "pages/work/index"
},
{
"pagePath": "pages/mine/index"
}
]
}
}
+331
View File
@@ -0,0 +1,331 @@
<script setup lang="ts">
import { onReady, onShow } from '@dcloudio/uni-app'
import { reactive, ref } from 'vue'
import LineChart from '@/subPages/echarts/components/LineChart.vue'
defineOptions({
componentPlaceholder: {
LineChart: 'view',
},
})
definePage({
name: 'home',
layout: 'tabbar',
style: {
navigationBarTitleText: '首页',
},
})
const current = ref<number>(0)
/**
* 访问统计数据
*/
interface VisitStatsVO {
todayUvCount: number
uvGrowthRate: number
totalUvCount: number
todayPvCount: number
pvGrowthRate: number
totalPvCount: number
}
/**
* 导航项
*/
interface NavItem {
icon: string
title: string
url: string
prem: string
}
/**
* 快捷导航列表
*/
const NAV_LIST: NavItem[] = [
{
icon: '/static/icons/user.png',
title: '用户管理',
url: '/pages/work/user/index',
prem: 'sys:user:query',
},
{
icon: '/static/icons/role.png',
title: '角色管理',
url: '/pages/work/role/index',
prem: 'sys:role:query',
},
{
icon: '/static/icons/notice.png',
title: '通知公告',
url: '/pages/work/notice/index',
prem: 'sys:notice:query',
},
{
icon: '/static/icons/setting.png',
title: '系统配置',
url: '/pages/work/config/index',
prem: 'sys:config:query',
},
]
/**
* 默认访问统计数据
*/
const DEFAULT_VISIT_STATS: VisitStatsVO = {
todayUvCount: 1234,
uvGrowthRate: 15.6,
totalUvCount: 45678,
todayPvCount: 5678,
pvGrowthRate: 23.4,
totalPvCount: 123456,
}
// 通知公告文本
const NOTICE_TEXT = '通知公告: fastapp 是一个基于 Vue3 + UniApp 的前端模板项目,提供了一套完整的前端解决方案'
// 默认日期范围(天数)
const DEFAULT_DAYS_RANGE = 7
// 访问统计数据
const visitStatsData = ref<VisitStatsVO>(DEFAULT_VISIT_STATS)
// 图表数据
const chartData = ref({})
// 日期范围
const recentDaysRange = ref(DEFAULT_DAYS_RANGE)
// 轮播图列表
const swiperList = ref([
'/static/images/banner01.jpg',
'/static/images/banner02.jpg',
])
// 快捷导航列表
const navList = reactive(NAV_LIST)
// 导航到指定页面
function navigateTo(url: string) {
// 避免空 URL 导航
if (!url)
return
// 根据 URL 类型选择不同的导航方式
if (
url.startsWith('/pages/')
&& (url.includes('/index/')
|| url.includes('/mine/')
|| url.includes('/work/'))
) {
uni.switchTab({
url,
})
}
else {
uni.navigateTo({
url,
})
}
}
// 生成静态的访问趋势数据
function generateStaticTrendData(days: number) {
const ipList = []
const pvList = []
const today = new Date()
for (let i = days - 1; i >= 0; i--) {
const date = new Date(today)
date.setDate(today.getDate() - i)
// 生成模拟数据
ipList.push(Math.floor(Math.random() * 500) + 200)
pvList.push(Math.floor(Math.random() * 1000) + 500)
}
return {
ipList,
pvList,
}
}
// 加载访问统计数据(使用静态数据)
async function loadVisitStatsData() {
// 模拟异步加载
setTimeout(() => {
visitStatsData.value = { ...DEFAULT_VISIT_STATS }
}, 100)
}
// 加载访问趋势数据(使用静态数据)
function loadVisitTrendData() {
// 模拟异步加载
setTimeout(() => {
const data = generateStaticTrendData(recentDaysRange.value)
const res = {
series: [
{
name: 'UV',
data: data.ipList,
},
{
name: 'PV',
data: data.pvList,
},
],
}
chartData.value = JSON.parse(JSON.stringify(res))
}, 100)
}
onReady(() => {
loadVisitStatsData()
loadVisitTrendData()
})
onShow(() => {
// 确保 tabbar 状态正确
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage.route === 'pages/index/index') {
// 通过事件通知 tabbar 布局更新状态
uni.$emit('updateTabbar', 'index')
}
}
})
</script>
<template>
<view class="app-container">
<!-- 轮播图 -->
<view class="swiper-container">
<wd-swiper
v-model:current="current"
:list="swiperList"
autoplay
indicator
image-mode="scaleToFill"
/>
</view>
<!-- 快捷导航 -->
<view class="nav-grid">
<wd-grid clickable :column="4">
<wd-grid-item
v-for="(item, index) in navList"
:key="index"
@click="navigateTo(item.url)"
>
<template #icon>
<image class="icon-image" :src="item.icon" lazy-load />
</template>
<template #text>
<view class="nav-title">
<wd-text :text="item.title" />
</view>
</template>
</wd-grid-item>
</wd-grid>
</view>
<!-- 通知公告 -->
<view class="notice-text">
<wd-notice-bar
type="warning"
prefix="warn-bold"
:text="NOTICE_TEXT"
/>
</view>
<!-- 数据统计 -->
<view class="stats-grid">
<view class="stats-item">
<image class="stats-icon" src="/static/icons/visitor.png" lazy-load />
<view class="stats-info">
<wd-text text="访客数" />
<view class="stats-value">
<wd-text type="warning" :text="visitStatsData.todayUvCount" bold />
</view>
</view>
</view>
<view class="stats-item">
<image class="stats-icon" src="/static/icons/browser.png" lazy-load />
<view class="stats-info">
<wd-text text="浏览量" />
<view class="stats-value">
<wd-text type="success" :text="visitStatsData.todayPvCount" bold />
</view>
</view>
</view>
</view>
<!-- 访问趋势 -->
<view class="mb-1 rounded-2 bg-gray-50 p-3 shadow-sm">
<LineChart />
</view>
</view>
</template>
<style lang="scss" scoped>
.app-container {
padding: 20rpx;
.swiper-container {
margin-bottom: 20rpx;
border-radius: 16rpx;
overflow: hidden;
}
.nav-grid {
margin-bottom: 20rpx;
border-radius: 16rpx;
.icon-image {
width: 64rpx;
height: 64rpx;
border-radius: 8rpx;
}
.nav-title {
margin-bottom: 20rpx;
border-radius: 16rpx;
font-size: 24rpx;
text-align: center;
margin-top: 12rpx;
}
}
.notice-text {
margin-bottom: 20rpx;
border-radius: 16rpx;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10rpx;
margin-bottom: 20rpx;
}
.stats-item {
display: flex;
align-items: center;
padding: 20rpx;
background-color: var(--bg-color-2);
border-radius: 16rpx;
}
.stats-icon {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 20rpx;
}
}
</style>
+386
View File
@@ -0,0 +1,386 @@
<script lang="ts" setup>
import type { CaptchaInfo, LoginFormData } from '@/api/auth'
import { onLoad } from '@dcloudio/uni-app'
import { onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import AuthAPI from '@/api/auth'
import { useUserStore } from '@/store/userStore'
definePage({
name: 'login',
style: {
navigationBarTitleText: '登录',
},
})
// 主题相关
const loginFormRef = ref()
const toast = useToast()
const loading = ref(false)
const userStore = useUserStore()
// 登录表单数据
const loginFormData = reactive<LoginFormData>({
username: 'admin',
password: '123456',
captcha: '',
captcha_key: '',
remember: true,
login_type: '移动端',
})
// 验证码状态
const captchaState = reactive<CaptchaInfo>({
enable: false,
key: '',
img_base: '',
})
// 防重复请求标志
const isCaptchaLoading = ref(false)
// 获取验证码
async function getLoginCaptcha() {
if (isCaptchaLoading.value) {
return
}
isCaptchaLoading.value = true
try {
const result = await AuthAPI.getCaptcha()
if (result && typeof result === 'object') {
captchaState.enable = Boolean(result.enable)
captchaState.key = result.key || ''
captchaState.img_base = result.img_base || result.img || ''
if (captchaState.enable) {
loginFormData.captcha = ''
loginFormData.captcha_key = captchaState.key
}
}
else {
captchaState.enable = false
}
}
catch (error: any) {
console.error('获取验证码失败:', error)
toast.error(error?.message || '验证码获取失败')
captchaState.img_base = ''
captchaState.enable = false
}
finally {
isCaptchaLoading.value = false
}
}
// 仅使用 query 作为重定向来源
const redirect = ref('/pages/index/index')
// 页面加载时初始化
onLoad((options) => {
uni.setNavigationBarTitle({ title: '登录' })
// 处理重定向逻辑
const fromQuery
= options && options.redirect ? decodeURIComponent(options.redirect) : ''
if (fromQuery && fromQuery !== '/pages/login/index') {
redirect.value = fromQuery
}
// 获取验证码
getLoginCaptcha()
})
// 页面挂载时确保验证码正确显示
onMounted(() => {
// 如果验证码未启用或未获取到,重新获取
setTimeout(() => {
if (!captchaState.enable || !captchaState.img_base) {
getLoginCaptcha()
}
// 强制清除输入框背景色,优化视觉效果
const inputs = document.querySelectorAll('input')
inputs.forEach((input) => {
input.style.backgroundColor = 'transparent'
input.style.boxShadow = 'none'
})
}, 100)
})
// 账号密码登录
async function handleSubmit() {
try {
const { valid, errors } = await loginFormRef.value.validate()
if (!valid)
return toast.error(errors[0].message)
loading.value = true
await userStore.login(loginFormData)
toast.success('登录成功')
// 登录成功后跳转到指定页面
setTimeout(() => {
uni.reLaunch({
url: redirect.value,
success: () => toast.success('跳转成功'),
fail: err => toast.error(`跳转失败: ${err.errMsg}`),
})
}, 500)
}
catch (error: any) {
toast.error(error?.message || '登录失败')
getLoginCaptcha() // 刷新验证码
}
finally {
loading.value = false
}
}
// 忘记密码
async function handleForgotPassword() {
toast.error('忘记密码功能开发中...')
}
// 注册账号
async function handleRegister() {
toast.error('注册账号功能开发中...')
}
</script>
<template>
<view class="app-container">
<!-- 背景图 -->
<wd-img
src="/static/images/login-bg.svg"
mode="aspectFill"
class="login-bg"
/>
<view class="login-card">
<!-- Logo和标题区域 -->
<view class="header">
<wd-img src="/static/logo.png" class="logo" mode="aspectFit" />
<text class="title theme-text-inverse">
FastApp管理系统
</text>
<text class="subtitle theme-text-inverse">
欢迎使用移动端管理平台
</text>
</view>
<!-- 登录表单区域 -->
<wd-form
ref="loginFormRef"
:model="loginFormData"
class="login-form"
>
<wd-cell-group border class="form-group">
<wd-input
v-model="loginFormData.username"
label="账号"
label-width="100px"
prefix-icon="user"
type="text"
prop="username"
placeholder="请输入账号"
clearable
center
clear-trigger="focus"
:rules="[{ required: true, message: '请输入账号' }]"
/>
<wd-input
v-model="loginFormData.password"
label="密码"
label-width="100px"
prefix-icon="lock-on"
type="text"
prop="password"
show-password
placeholder="请输入密码"
clear-trigger="focus"
clearable
center
:rules="[{ required: true, message: '请输入密码' }]"
/>
<!-- 验证码输入框 -->
<view v-if="captchaState.enable" class="captcha">
<wd-input
v-model="loginFormData.captcha"
label="验证码"
label-width="100px"
prefix-icon="lock-on"
class="captcha-input"
type="text"
prop="captcha"
placeholder="请输入验证码"
clear-trigger="focus"
clearable
center
:rules="[{ required: true, message: '请输入验证码' }]"
/>
<wd-img
:src="captchaState.img_base"
class="captcha-image"
mode="aspectFit"
@click="getLoginCaptcha"
/>
<view
v-if="!captchaState.img_base && captchaState.enable"
@click="getLoginCaptcha"
>
<text>点击加载</text>
</view>
</view>
<wd-divider />
<!-- 忘记密码和注册账号 -->
<view class="auth-links">
<!-- 记住我 -->
<view class="remember-me">
<wd-checkbox v-model="loginFormData.remember">
<wd-text size="24rpx" text="记住我" />
</wd-checkbox>
</view>
<view class="forgot-password">
<wd-text
type="primary"
text="忘记密码?"
size="24rpx"
@click="handleForgotPassword"
/>
</view>
</view>
<!-- 登录按钮 -->
<view class="footer">
<wd-button
type="primary"
size="large"
block
class="login-button"
:loading="loading"
loading-size="24"
@click="handleSubmit"
>
{{ loading ? "登录中..." : "账号登录" }}
</wd-button>
</view>
<view class="register-link">
<wd-text size="24rpx" text="您没有账号?" />
<wd-text
type="primary"
text="注册账号"
size="24rpx"
@click="handleRegister"
/>
</view>
<!-- 版权信息 -->
<view class="copyright">
<wd-text
text="Copyright © 2025-2026 service.fastapiadmin.com 版权所有 陕ICP备2025069493号-1"
size="20rpx"
/>
</view>
</wd-cell-group>
</wd-form>
</view>
</view>
</template>
<style lang="scss" scoped>
.app-container {
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
.login-bg {
position: absolute;
width: 100%;
height: 100%;
}
.login-card {
z-index: 2;
margin-top: 40rpx;
width: 92%;
.header {
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30rpx;
.logo {
width: 250rpx;
height: 250rpx;
}
.title {
margin-bottom: 20rpx;
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
}
.subtitle {
font-size: 28rpx;
color: #ffffff;
text-align: center;
}
}
.login-form {
margin-top: 40rpx;
padding: 24rpx;
.form-group {
padding: 24rpx;
}
.captcha {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.captcha-input {
flex: 1;
min-width: 0;
}
.captcha-image {
width: 160rpx;
height: 60rpx;
}
.footer {
margin-top: 32rpx;
}
.auth-links {
display: flex;
justify-content: space-between;
margin-top: 32rpx;
}
.register-link {
display: flex;
justify-content: center;
align-items: center;
margin-top: 32rpx;
}
.copyright {
margin-top: 32rpx;
text-align: center;
}
}
}
}
</style>
+168
View File
@@ -0,0 +1,168 @@
<!-- layout: tabbar -->
<script lang="ts" setup>
definePage({
name: 'mine',
layout: 'tabbar',
style: {
navigationBarTitleText: '我的',
},
})
const router = useRouter()
const toast = useToast()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const isLogin = computed(() => !!userInfo.value)
const defaultAvatar = '/static/images/default-avatar.png'
const isLoading = ref(false)
// 退出登录
function handleLogout() {
uni.showModal({
title: '提示',
content: '确认退出登录吗?',
success(res) {
if (res.confirm) {
userStore.logout()
toast.show('已退出登录')
}
},
})
}
// 页面跳转方法
function navigateTo(name: string) {
router.push({
name,
})
}
function openUrl(url: string) {
window.open(url, '_blank')
}
// 消息按钮点击处理
function handleMessagesClick(e?: any) {
if (e) {
e.stopPropagation()
e.preventDefault()
}
navigateToSection('messages')
}
// 导航到各个板块
function navigateToSection(section: string, subSection?: string, e?: any) {
if (e) {
e.stopPropagation()
e.preventDefault()
}
// 这里可以根据需要实现具体的导航逻辑
uni.showToast({
title: '功能开发中',
icon: 'none',
})
}
onShow(() => {
// 确保 tabbar 状态正确
const pages = getCurrentPages()
if (pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage.route === 'pages/mine/index') {
// 通过事件通知 tabbar 布局更新状态
uni.$emit('updateTabbar', 'mine')
}
}
// 每次显示页面时都检查并刷新用户信息
loadUserInfo()
})
// 加载用户信息
async function loadUserInfo() {
if (isLogin.value) {
isLoading.value = true
try {
await userStore.getInfo()
}
catch (error) {
console.error('获取用户信息失败', error)
}
finally {
isLoading.value = false
}
}
}
// 监听用户信息变化,确保数据及时更新
watch(
() => userInfo.value,
() => {},
{
deep: true,
immediate: true,
},
)
</script>
<template>
<view class="p-3">
<!-- 用户信息卡片 -->
<wd-card type="rectangle">
<wd-row class="flex items-center">
<wd-col :span="6">
<wd-badge v-if="true" model-value="99+" @click="handleMessagesClick">
<wd-avatar
size="medium"
shape="round"
:src="isLogin ? userInfo!.avatar : defaultAvatar"
/>
</wd-badge>
</wd-col>
<wd-col :span="16">
<view class="mt-2 flex flex-col">
<wd-text type="success" :text="userInfo?.name || '匿名用户'" size="16" bold />
<wd-text :text="`ID: ${userInfo?.username}`" size="12" />
</view>
</wd-col>
<wd-col :span="2">
<wd-icon name="setting" size="22" color="#0083ff" @click="navigateTo('settings')" />
</wd-col>
</wd-row>
</wd-card>
<!-- 常用工具 -->
<wd-card type="rectangle" title="常用工具">
<wd-grid clickable border :gutter="10" :column="4">
<wd-grid-item is-dot icon="user" text="用户管理" clickable @click="navigateTo('user')" />
<wd-grid-item is-dot icon="help-circle" text="岗位管理" clickable @click="navigateTo('position')" />
<wd-grid-item is-dot icon="check-circle" text="角色管理" clickable @click="navigateTo('role')" />
<wd-grid-item is-dot icon="info-circle" text="参数管理" clickable @click="navigateTo('parameter')" />
<wd-grid-item is-dot icon="user" text="字典管理" clickable @click="navigateTo('dictionary')" />
<wd-grid-item is-dot icon="help-circle" text="部门管理" clickable @click="navigateTo('department')" />
<wd-grid-item is-dot icon="check-circle" text="菜单管理" clickable @click="navigateTo('role')" />
<wd-grid-item is-dot icon="info-circle" text="日志管理" clickable @click="navigateTo('log')" />
</wd-grid>
</wd-card>
<!-- 推荐服务 -->
<wd-card type="rectangle" title="推荐服务">
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell title="项目官网" is-link @click="openUrl('https://service.fastapiadmin.com/')" />
<wd-cell title="问题反馈" is-link @click="navigateTo('feedback')" />
<wd-cell title="个人资料" is-link @click="navigateTo('profile')" />
<wd-cell title="账号设置" is-link @click="navigateTo('account')" />
</wd-cell-group>
</wd-card>
<!-- 退出登录按钮 -->
<wd-button v-if="isLogin" class="p-3" size="large" block @click="handleLogout">
退出登录
</wd-button>
</view>
</template>
<style lang="scss" scoped>
</style>
+54
View File
@@ -0,0 +1,54 @@
<script setup lang="ts">
definePage({
name: 'work',
layout: 'tabbar',
style: {
navigationBarTitleText: '工作台',
},
})
const router = useRouter()
// 页面跳转方法
function navigateTo(name: string) {
router.push({
name,
})
}
function openUrl(url: string) {
window.open(url, '_blank')
}
</script>
<template>
<view class="box-border py-3">
<view class="mx-3 box-border rounded-3 bg-white px-4 py-6 text-center dark:bg-[var(--wot-dark-background2)]">
<text class="mb-3 block text-left text-5 text-gray-800 font-bold dark:text-gray-200">
Wot Starter
</text>
<text class="mb-3 block text-left text-30rpx text-gray-600 leading-relaxed dark:text-gray-300">
⚡️ 基于 vitesse-uni-app 由 vite & uni-app 驱动的、深度整合 Wot UI 组件库的快速启动模板
</text>
<text class="block text-left text-3 text-gray-400 leading-relaxed dark:text-gray-400">
背靠 Uni Helper、Wot UI 团队,告别 HBuilderX ,拥抱现代前端开发工具链
</text>
</view>
<demo-block title="工具链介绍" transparent>
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell title="🧩 WotUI组件库" is-link @click="openUrl('https://wot-ui.cn/')" />
<wd-cell title="🧠 Agent Skills" is-link @click="navigateTo('skills')" />
<wd-cell title="🚦 Router 路由管理" is-link @click="navigateTo('router')" />
<wd-cell title="🌐 Alova 网络请求" is-link @click="navigateTo('request')" />
<wd-cell title="🎨 Icon 图标" is-link @click="navigateTo('icon')" />
<wd-cell title="✨ Unocss 原子化" is-link @click="navigateTo('styles')" />
<wd-cell title="🍍 Pinia 持久化" is-link @click="navigateTo('pinia')" />
<wd-cell title="🌱 CreateUni 脚手架" is-link @click="navigateTo('create-uni') " />
<wd-cell title="🔄 CI/CD 持续集成" is-link @click="navigateTo('ci')" />
<wd-cell title="🦾 uni-ku/root" is-link @click="navigateTo('root')" />
<wd-cell title="📊 uni-echarts" is-link @click="navigateTo('echarts')" />
</wd-cell-group>
</demo-block>
</view>
</template>
+118
View File
@@ -0,0 +1,118 @@
/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />
import { pages, subPackages } from 'virtual:uni-pages'
import { useUserStore } from '@/store/userStore'
function generateRoutes() {
const routes = pages.map((page) => {
const newPath = `/${page.path}`
return { ...page, path: newPath }
})
if (subPackages && subPackages.length > 0) {
subPackages.forEach((subPackage) => {
const subRoutes = subPackage.pages.map((page: any) => {
const newPath = `/${subPackage.root}/${page.path}`
return { ...page, path: newPath }
})
routes.push(...subRoutes)
})
}
return routes
}
const router = createRouter({
routes: generateRoutes(),
})
router.beforeEach((to, from, next) => {
console.log('🚀 beforeEach 守卫触发:', { to, from })
// 演示:基本的导航日志记录
if (to.path && from.path) {
console.log(`📍 导航: ${from.path} → ${to.path}`)
}
// 跳过登录页面的检查
if (to.path === '/pages/login/index') {
return next()
}
// 检查登录状态
try {
const userStore = useUserStore()
const accessToken = userStore.getAccessToken()
if (!accessToken) {
uni.showToast({
title: '🔐 Token不存在,跳转到登录页面',
icon: 'error',
})
// 跳转到登录页面
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent(to.path)}`,
fail: (error) => {
console.error('跳转登录页面失败:', error)
// 如果 navigateTo 失败,尝试使用 reLaunch
uni.reLaunch({
url: '/pages/login/index',
})
},
})
return
}
}
catch (error) {
console.error('检查登录状态时发生错误:', error)
// 发生错误时,跳转到登录页面
uni.reLaunch({
url: '/pages/login/index',
})
return
}
// 演示:对受保护页面的简单拦截
if (to.name === 'demo-protected') {
const { confirm: showConfirm } = useGlobalMessage()
console.log('🛡️ 检测到访问受保护页面')
return new Promise<void>((resolve, reject) => {
showConfirm({
title: '守卫拦截演示',
msg: '这是一个受保护的页面,需要确认后才能访问',
confirmButtonText: '允许访问',
cancelButtonText: '取消',
success() {
console.log('✅ 用户确认访问,允许导航')
next()
resolve()
},
fail() {
console.log('❌ 用户取消访问,阻止导航')
next(false)
reject(new Error('用户取消访问'))
},
})
})
}
// 继续导航
next()
})
router.afterEach((to, from) => {
console.log('🎯 afterEach 钩子触发:', { to, from })
// 演示:简单的页面切换记录
if (to.path) {
console.log(`📄 页面切换完成: ${to.path}`)
}
// 演示:针对 afterEach 演示页面的简单提示
if (to.name === 'demo-aftereach') {
const { show: showToast } = useGlobalToast()
console.log('📊 进入 afterEach 演示页面')
setTimeout(() => {
showToast('afterEach 钩子已触发!')
}, 500)
}
})
export default router
+6
View File
@@ -0,0 +1,6 @@
export {}
declare module 'vue' {
type Hooks = App.AppInstance & Page.PageInstance
interface ComponentCustomOptions extends Hooks {}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><!-- Icon from Carbon by IBM - undefined --><path fill="currentColor" d="M16.612 2.214a1.01 1.01 0 0 0-1.242 0L1 13.419l1.243 1.572L4 13.621V26a2.004 2.004 0 0 0 2 2h20a2.004 2.004 0 0 0 2-2V13.63L29.757 15L31 13.428ZM18 26h-4v-8h4Zm2 0v-8a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v8H6V12.062l10-7.79l10 7.8V26Z"/></svg>

After

Width:  |  Height:  |  Size: 392 B

@@ -0,0 +1,54 @@
<svg width="100%" height="100%" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg">
<!-- 背景渐变 -->
<defs>
<!-- 主背景渐变 - 蓝色系 -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#94BFFF" />
<stop offset="100%" stop-color="#165DFF" />
</linearGradient>
<!-- 图形渐变 - 白色半透明 -->
<linearGradient id="shapeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.3" />
<stop offset="100%" stop-color="#ffffff" stop-opacity="0.15" />
</linearGradient>
<!-- 底部波浪渐变 -->
<linearGradient id="waveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.05" />
<stop offset="100%" stop-color="#FFFFFF" stop-opacity="0.1" />
</linearGradient>
<!-- 为暗黑模式提供的额外效果 -->
<filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="15" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- 顶部背景 -->
<rect width="100%" height="100%" fill="#1A1A1A" opacity="0" class="dark-mode-bg" />
<rect width="100%" height="50%" fill="url(#bgGradient)" />
<!-- 左侧装饰图形 -->
<rect x="100" y="150" width="120" height="120" rx="15" fill="url(#shapeGradient)" transform="rotate(-10, 160, 210)" opacity="0.7" />
<rect x="190" y="90" width="80" height="80" rx="10" fill="url(#shapeGradient)" transform="rotate(15, 230, 130)" opacity="0.6" />
<rect x="60" y="250" width="100" height="100" rx="10" fill="url(#shapeGradient)" transform="rotate(-5, 110, 300)" opacity="0.5" />
<!-- 右侧装饰图形 -->
<circle cx="750" cy="150" r="60" fill="url(#shapeGradient)" opacity="0.7" />
<circle cx="820" cy="230" r="90" fill="url(#shapeGradient)" opacity="0.5" />
<circle cx="690" cy="250" r="40" fill="url(#shapeGradient)" opacity="0.6" />
<!-- 底部波浪形状,适配深色和浅色模式 -->
<path d="M0,900 C200,800 350,950 550,870 C750,790 850,900 1000,850 L1000,1000 L0,1000 Z" fill="url(#waveGradient)" class="wave-light" />
<path d="M0,900 C200,800 350,950 550,870 C750,790 850,900 1000,850 L1000,1000 L0,1000 Z" fill="#1A1A1A" opacity="0" class="wave-dark" />
<style>
@media (prefers-color-scheme: dark) {
.dark-mode-bg { opacity: 1; }
.wave-light { opacity: 0; }
.wave-dark { opacity: 0.7; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><!-- Icon from Carbon by IBM - undefined --><path fill="currentColor" d="M16 4a5 5 0 1 1-5 5a5 5 0 0 1 5-5m0-2a7 7 0 1 0 7 7a7 7 0 0 0-7-7m10 28h-2v-5a5 5 0 0 0-5-5h-6a5 5 0 0 0-5 5v5H6v-5a7 7 0 0 1 7-7h6a7 7 0 0 1 7 7z"/></svg>

After

Width:  |  Height:  |  Size: 311 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><!-- Icon from Carbon by IBM - undefined --><path fill="currentColor" d="M16 17v8H6v-8zm0-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2m11-9v5H17V6zm0-2H17a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2m0 13v5h-5v-5zm0-2h-5a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2v-5a2 2 0 0 0-2-2M11 6v5H6V6zm0-2H6a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h5a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2"/></svg>

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

+138
View File
@@ -0,0 +1,138 @@
import type { ThemeColorOption, ThemeMode, ThemeState } from '@/composables/types/theme'
import { defineStore } from 'pinia'
import { themeColorOptions } from '@/composables/types/theme'
/**
* 完整版主题状态管理
* 支持手动切换主题、主题色选择、跟随系统主题等完整功能
*/
export const useManualThemeStore = defineStore('manualTheme', {
state: (): ThemeState => ({
theme: 'light',
followSystem: true, // 是否跟随系统主题
hasUserSet: false, // 用户是否手动设置过主题
currentThemeColor: themeColorOptions[0],
themeVars: {
darkBackground: '#0f0f0f',
darkBackground2: '#1a1a1a',
darkBackground3: '#242424',
darkBackground4: '#2f2f2f',
darkBackground5: '#3d3d3d',
darkBackground6: '#4a4a4a',
darkBackground7: '#606060',
darkColor: '#ffffff',
darkColor2: '#e0e0e0',
darkColor3: '#a0a0a0',
colorTheme: themeColorOptions[0].primary,
},
}),
getters: {
isDark: state => state.theme === 'dark',
},
actions: {
/**
* 手动切换主题
* @param mode 指定主题模式,不传则自动切换
* @param isFollw 是否是跟随系统
*/
toggleTheme(mode?: ThemeMode, isFollw: boolean = false) {
this.theme = mode || (this.theme === 'light' ? 'dark' : 'light')
if (!isFollw) {
// 如果不是跟随系统,是手动切换
this.hasUserSet = true // 标记用户已手动设置
this.followSystem = false // 不再跟随系统
}
this.setNavigationBarColor()
},
/**
* 设置是否跟随系统主题
* @param follow 是否跟随系统
*/
setFollowSystem(follow: boolean) {
this.followSystem = follow
if (follow) {
this.hasUserSet = false
this.initTheme() // 重新获取系统主题
}
},
/**
* 设置导航栏颜色
*/
setNavigationBarColor() {
uni.setNavigationBarColor({
frontColor: this.theme === 'light' ? '#000000' : '#ffffff',
backgroundColor: this.theme === 'light' ? '#ffffff' : '#000000',
})
},
/**
* 设置主题色
* @param color 主题色选项
*/
setCurrentThemeColor(color: ThemeColorOption) {
this.currentThemeColor = color
this.themeVars.colorTheme = color.primary
},
/**
* 获取系统主题
* @returns 系统主题模式
*/
getSystemTheme(): ThemeMode {
try {
// #ifdef MP-WEIXIN
// 微信小程序使用 getAppBaseInfo
const appBaseInfo = uni.getAppBaseInfo()
if (appBaseInfo && appBaseInfo.theme) {
return appBaseInfo.theme as ThemeMode
}
// #endif
// #ifndef MP-WEIXIN
// 其他平台使用 getSystemInfoSync
const systemInfo = uni.getSystemInfoSync()
if (systemInfo && systemInfo.theme) {
return systemInfo.theme as ThemeMode
}
// #endif
}
catch (error) {
console.warn('获取系统主题失败:', error)
}
return 'light' // 默认返回 light
},
/**
* 初始化主题
*/
initTheme() {
// 如果用户已手动设置且不跟随系统,保持当前主题
if (this.hasUserSet && !this.followSystem) {
console.log('使用用户设置的主题:', this.theme)
this.setNavigationBarColor()
return
}
// 获取系统主题
const systemTheme = this.getSystemTheme()
// 如果是首次启动或跟随系统,使用系统主题
if (!this.hasUserSet || this.followSystem) {
this.theme = systemTheme
if (!this.hasUserSet) {
this.followSystem = true
console.log('首次启动,使用系统主题:', this.theme)
}
else {
console.log('跟随系统主题:', this.theme)
}
}
this.setNavigationBarColor()
},
},
})
+38
View File
@@ -0,0 +1,38 @@
/*
* @Author: weisheng
* @Date: 2025-06-23 22:23:05
* @LastEditTime: 2025-06-24 19:03:21
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/src/store/persist.ts
* 记得注释
*/
import type { PiniaPluginContext } from 'pinia'
function persist({ store }: PiniaPluginContext, excludedIds: string[]) {
// 检查当前store的id是否在排除列表中
const isExcluded = excludedIds.includes(store.$id)
// 如果当前store的id在排除列表中,则不进行持久化
if (isExcluded) {
return
}
// 暂存State
let persistState = CommonUtil.deepClone(store.$state)
// 从缓存中读取
const storageState = uni.getStorageSync(store.$id)
if (storageState) {
persistState = storageState
}
store.$state = persistState
store.$subscribe(() => {
// 在存储变化的时候将store缓存
uni.setStorageSync(store.$id, CommonUtil.deepClone(store.$state))
})
}
export function persistPlugin(context: PiniaPluginContext) {
// 调用persist函数,并传入排除列表
persist(context, ['temp'])
}
+74
View File
@@ -0,0 +1,74 @@
import type { SystemThemeState, ThemeMode } from '@/composables/types/theme'
import { defineStore } from 'pinia'
import { themeColorOptions } from '@/composables/types/theme'
/**
* 简化版系统主题状态管理
* 仅支持跟随系统主题,不提供手动切换功能
* 导航栏颜色通过 theme.json 自动处理
*/
export const useThemeStore = defineStore('theme', {
state: (): SystemThemeState => ({
theme: 'light',
themeVars: {
darkBackground: '#0f0f0f',
darkBackground2: '#1a1a1a',
darkBackground3: '#242424',
darkBackground4: '#2f2f2f',
darkBackground5: '#3d3d3d',
darkBackground6: '#4a4a4a',
darkBackground7: '#606060',
darkColor: '#ffffff',
darkColor2: '#e0e0e0',
darkColor3: '#a0a0a0',
colorTheme: themeColorOptions[0].primary,
},
}),
getters: {
isDark: state => state.theme === 'dark',
},
actions: {
/**
* 获取系统主题
* @returns 系统主题模式
*/
getSystemTheme(): ThemeMode {
// #ifdef MP-WEIXIN
// 微信小程序使用 getAppBaseInfo
const appBaseInfo = uni.getAppBaseInfo()
if (appBaseInfo && appBaseInfo.theme) {
return appBaseInfo.theme as ThemeMode
}
// #endif
// #ifndef MP-WEIXIN
// 其他平台使用 getSystemInfoSync
const systemInfo = uni.getSystemInfoSync()
if (systemInfo && systemInfo.theme) {
return systemInfo.theme as ThemeMode
}
// #endif
return 'light' // 默认返回 light
},
/**
* 设置主题(仅内部使用)
* @param theme 主题模式
*/
setTheme(theme: ThemeMode) {
this.theme = theme
},
/**
* 初始化系统主题
*/
initSystemTheme() {
const systemTheme = this.getSystemTheme()
this.theme = systemTheme
console.log('初始化系统主题:', this.theme)
},
},
})
+213
View File
@@ -0,0 +1,213 @@
import type { LoginFormData, LoginResult, LogoutBody } from '@/api/auth'
import type { UserInfo } from '@/api/user'
import { defineStore } from 'pinia'
import AuthAPI from '@/api/auth'
import UserAPI from '@/api/user'
import {
APP_ACCESS_TOKEN_KEY,
APP_REFRESH_TOKEN_KEY,
APP_USER_INFO,
} from '@/constants'
import { Storage } from '@/utils/storage'
export const useUserStore = defineStore('appUserInfo', {
state: () => ({
userInfo: Storage.get<UserInfo>(APP_USER_INFO),
isLoggingIn: false,
}),
getters: {
isLogin: state => !!state.userInfo,
},
// 统一的登录处理方法
actions: {
// 获取访问 token
getAccessToken(): string | null {
return Storage.get<string>(APP_ACCESS_TOKEN_KEY) || null
},
// 设置访问 token
setAccessToken(token: string): void {
Storage.set(APP_ACCESS_TOKEN_KEY, token)
},
// 获取刷新 token
getRefreshToken(): string | null {
return Storage.get<string>(APP_REFRESH_TOKEN_KEY) || null
},
// 设置刷新 token
setRefreshToken(token: string): void {
Storage.set(APP_REFRESH_TOKEN_KEY, token)
},
// 清除所有 token
clearTokens(): void {
Storage.remove(APP_ACCESS_TOKEN_KEY)
Storage.remove(APP_REFRESH_TOKEN_KEY)
},
// 获取用户信息
getUserInfo(): UserInfo | undefined {
return Storage.get<UserInfo>(APP_USER_INFO)
},
// 设置用户信息
setUserInfo(userInfo: Partial<UserInfo>): void {
Storage.set(APP_USER_INFO, userInfo)
},
// 清除用户信息
clearUserInfo(): void {
Storage.remove(APP_USER_INFO)
},
// 清除所有缓存信息
clearAll(): void {
this.clearTokens()
this.clearUserInfo()
},
// 检查用户登录状态,未登录则跳转到登录页面
checkLogin(silent: boolean = false): boolean {
const userStore = useUserStore()
const accessToken = this.getAccessToken()
// 检查 token 和用户信息是否都存在
const isLoggedIn = !!(accessToken && userStore.userInfo)
if (!isLoggedIn && !silent) {
try {
// 获取当前页面路径
let currentPagePath = '/pages/index/index' // 默认路径
const pages = getCurrentPages()
if (pages && pages.length > 0) {
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route) {
currentPagePath = `/${currentPage.route}`
// 处理页面参数 - 使用类型断言
const pageOptions = (currentPage as any).options
if (pageOptions && Object.keys(pageOptions).length > 0) {
const params = new URLSearchParams(
pageOptions as Record<string, string>,
)
currentPagePath += `?${params.toString()}`
}
}
}
// 跳转到登录页面
uni.navigateTo({
url: `/pages/login/index?redirect=${encodeURIComponent(currentPagePath)}`,
fail: (error) => {
console.error('跳转登录页面失败:', error)
// 如果 navigateTo 失败,尝试使用 reLaunch
uni.reLaunch({
url: '/pages/login/index',
})
},
})
}
catch (error) {
console.error('检查登录状态时发生错误:', error)
// 发生错误时,尝试直接跳转到登录页
uni.reLaunch({
url: '/pages/login/index',
})
}
}
return isLoggedIn
},
// 检查用户是否已登录(静默检查,不跳转)
isLoggedIn(): boolean {
return this.checkLogin(true)
},
// 强制用户登录,清除无效的登录状态
requireLogin(): void {
const userStore = useUserStore()
const accessToken = this.getAccessToken()
if (!accessToken || !userStore.userInfo) {
// 清除可能存在的无效状态
this.clearTokens()
userStore.logout()
// 跳转到登录页
uni.reLaunch({
url: '/pages/login/index',
})
}
},
async handleLogin(loginFn: () => Promise<LoginResult>, loginType: string): Promise<LoginResult> {
if (this.isLoggingIn)
throw new Error('登录中,请稍后')
this.isLoggingIn = true
try {
const result = await loginFn()
this.setAccessToken(result.access_token)
this.setRefreshToken(result.refresh_token)
// 登录成功后获取用户信息
await this.getInfo()
return result
}
catch (error: any) {
console.error(`${loginType}登录失败`, error)
throw error
}
finally {
this.isLoggingIn = false
}
},
// 账号密码登录
async login(data: LoginFormData): Promise<LoginResult> {
return this.handleLogin(() => AuthAPI.login(data), '账号密码')
},
// 获取用户信息
async getInfo(): Promise<UserInfo | null> {
try {
const userInfoData = await UserAPI.getCurrentUserInfo()
this.setUserInfo(userInfoData)
// 确保响应式数据更新
this.userInfo = userInfoData
return userInfoData
}
catch (error) {
console.error('获取用户信息失败', error)
return null
}
},
// 登出
async logout(): Promise<void> {
try {
const logoutBody: LogoutBody = {
token: this.getAccessToken() || '',
}
await AuthAPI.logout(logoutBody) // 调用后台注销接口
}
catch (error) {
console.error('登出失败', error)
}
finally {
this.clearAll() // 清除本地的 token
this.userInfo = {} // 清空用户信息
// 跳转到登录页面
uni.reLaunch({
url: '/pages/login/index',
})
}
},
},
})
+355
View File
@@ -0,0 +1,355 @@
/**
* 主样式文件
* 统一管理所有样式,保持简洁清晰
*/
html,
body,
#app {
height: 100%;
padding: 0;
margin: 0;
}
.app-container {
padding: 16rpx;
:deep(.custom-item) {
height: 100px !important;
}
:deep(.wd-card) {
margin: 16rpx 0 !important;
}
}
/* ===== 全局主题变量 ===== */
:root {
/* 主题色系 */
--primary-color: #165dff;
--primary-color-light: #94bfff;
--primary-color-dark: #0e3c9b;
--wot-color-theme: #165dff;
/* 功能色系 */
--success-color: #07c160;
--warning-color: #ff7d00;
--danger-color: #f53f3f;
--info-color: #86909c;
/* 文字颜色 */
--text-color: #1d2129;
--text-color-2: #4e5969;
--text-color-3: #86909c;
--text-color-placeholder: #c9cdd4;
--text-color-inverse: #ffffff;
/* wot-design-uni 兼容变量 */
--wot-color-text: #1d2129;
--wot-color-dark-color: #ffffff;
/* 背景颜色 */
--bg-color: #ffffff;
--bg-color-2: #f8f8f8;
--bg-color-3: #f2f3f5;
--card-bg-color: #ffffff;
/* 边框颜色 */
--border-color: #e5e6eb;
--border-color-light: #f2f3f5;
/* 遮罩颜色 */
--mask-color: rgba(0, 0, 0, 0.6);
/* 阴影 */
--shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
--shadow-medium: 0 4rpx 16rpx rgba(0, 0, 0, 0.1);
--shadow-heavy: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
/* 圆角 */
--radius-sm: 4rpx;
--radius-md: 8rpx;
--radius-lg: 16rpx;
--radius-xl: 24rpx;
--radius-full: 9999rpx;
/* 间距 */
--spacing-xs: 8rpx;
--spacing-sm: 16rpx;
--spacing-md: 24rpx;
--spacing-lg: 32rpx;
--spacing-xl: 48rpx;
--spacing-xxl: 64rpx;
/* 字体大小 */
--font-xs: 24rpx;
--font-sm: 28rpx;
--font-md: 32rpx;
--font-lg: 36rpx;
--font-xl: 40rpx;
--font-xxl: 48rpx;
--font-xxxl: 56rpx;
/* 动画 */
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--transition-slow: 0.5s ease;
}
/* ===== 暗黑模式变量 ===== */
.dark {
--text-color: #ffffff;
--text-color-2: #e0e0e0;
--text-color-3: #a0a0a0;
--text-color-placeholder: #606060;
--text-color-inverse: #1d2129;
--wot-color-text: #ffffff;
--wot-color-dark-color: #ffffff;
--bg-color: #0f0f0f;
--bg-color-2: #1a1a1a;
--bg-color-3: #242424;
--card-bg-color: #1a1a1a;
--border-color: #2f2f2f;
--border-color-light: #3d3d3d;
--mask-color: rgba(0, 0, 0, 0.8);
--shadow-light: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
--shadow-medium: 0 4rpx 16rpx rgba(0, 0, 0, 0.4);
--shadow-heavy: 0 8rpx 32rpx rgba(0, 0, 0, 0.5);
}
/* ===== 主题适配类 ===== */
.theme-adaptive {
color: var(--text-color);
background-color: var(--bg-color);
border-color: var(--border-color);
transition:
color var(--transition-normal),
background-color var(--transition-normal),
border-color var(--transition-normal);
}
.theme-card {
background-color: var(--card-bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-light);
transition: all var(--transition-normal);
&:hover {
box-shadow: var(--shadow-medium);
}
}
.theme-text-primary {
color: var(--text-color);
}
.theme-text-secondary {
color: var(--text-color-2);
}
.theme-text-tertiary {
color: var(--text-color-3);
}
.theme-text-muted {
color: var(--text-color-placeholder);
}
.theme-text-inverse {
color: var(--text-color-inverse);
}
/* ===== 常用工具类 ===== */
.text-primary {
color: var(--primary-color) !important;
}
.text-success {
color: var(--success-color) !important;
}
.text-warning {
color: var(--warning-color) !important;
}
.text-danger {
color: var(--danger-color) !important;
}
.text-info {
color: var(--info-color) !important;
}
.text-muted {
color: var(--text-color-3) !important;
}
.bg-primary {
color: var(--text-color-inverse);
background-color: var(--primary-color) !important;
}
.bg-card {
background-color: var(--card-bg-color) !important;
}
.bg-light {
background-color: var(--bg-color-2) !important;
}
.rounded {
border-radius: var(--radius-md);
}
.rounded-lg {
border-radius: var(--radius-lg);
}
.rounded-full {
border-radius: var(--radius-full);
}
.shadow {
box-shadow: var(--shadow-light);
}
.shadow-lg {
box-shadow: var(--shadow-medium);
}
.transition {
transition: all var(--transition-normal);
}
.flex {
display: flex !important;
}
.items-center {
align-items: center !important;
}
.justify-center {
justify-content: center !important;
}
.justify-between {
justify-content: space-between !important;
}
.text-center {
text-align: center !important;
}
.font-bold {
font-weight: 700 !important;
}
/* ===== 间距工具类 ===== */
.p-xs {
padding: var(--spacing-xs) !important;
}
.p-sm {
padding: var(--spacing-sm) !important;
}
.p-md {
padding: var(--spacing-md) !important;
}
.p-lg {
padding: var(--spacing-lg) !important;
}
.p-xl {
padding: var(--spacing-xl) !important;
}
.m-xs {
margin: var(--spacing-xs) !important;
}
.m-sm {
margin: var(--spacing-sm) !important;
}
.m-md {
margin: var(--spacing-md) !important;
}
.m-lg {
margin: var(--spacing-lg) !important;
}
.m-xl {
margin: var(--spacing-xl) !important;
}
.mt-sm {
margin-top: var(--spacing-sm) !important;
}
.mt-md {
margin-top: var(--spacing-md) !important;
}
.mt-lg {
margin-top: var(--spacing-lg) !important;
}
.mt-xl {
margin-top: var(--spacing-xl) !important;
}
.mb-sm {
margin-bottom: var(--spacing-sm) !important;
}
.mb-md {
margin-bottom: var(--spacing-md) !important;
}
.mb-lg {
margin-bottom: var(--spacing-lg) !important;
}
.mb-xl {
margin-bottom: var(--spacing-xl) !important;
}
/* ===== 字体大小类 ===== */
.text-xs {
font-size: var(--font-xs) !important;
}
.text-sm {
font-size: var(--font-sm) !important;
}
.text-md {
font-size: var(--font-md) !important;
}
.text-lg {
font-size: var(--font-lg) !important;
}
.text-xl {
font-size: var(--font-xl) !important;
}
.text-2xl {
font-size: var(--font-xxl) !important;
}
/* ===== H5 环境下导航栏返回箭头样式 ===== */
// #ifdef H5
/* 返回箭头使用CSS变量自动适配主题 */
.uni-page-head__left,
.uni-page-head__back,
.uni-page-head__btn-left,
.uni-page-head__hd,
.uni-page-head__left *,
.uni-page-head__back *,
.uni-page-head__btn-left *,
.uni-page-head__hd * {
color: var(--text-color) !important;
fill: var(--text-color) !important;
stroke: var(--text-color) !important;
}
/* 暗黑主题下的返回箭头(确保优先级) */
.dark .uni-page-head__left,
.dark .uni-page-head__back,
.dark .uni-page-head__btn-left,
.dark .uni-page-head__hd,
.dark .uni-page-head__left *,
.dark .uni-page-head__back *,
.dark .uni-page-head__btn-left *,
.dark .uni-page-head__hd *,
.wot-theme-dark .uni-page-head__left,
.wot-theme-dark .uni-page-head__back,
.wot-theme-dark .uni-page-head__btn-left,
.wot-theme-dark .uni-page-head__hd,
.wot-theme-dark .uni-page-head__left *,
.wot-theme-dark .uni-page-head__back *,
.wot-theme-dark .uni-page-head__btn-left *,
.wot-theme-dark .uni-page-head__hd * {
color: var(--text-color) !important;
fill: var(--text-color) !important;
stroke: var(--text-color) !important;
}
// #endif
+140
View File
@@ -0,0 +1,140 @@
<script setup lang="ts">
definePage({
name: 'about',
style: {
navigationBarTitleText: '关于',
},
})
// 核心团队成员
const coreTeam = [
{
name: '不如摸鱼去',
avatar: 'https://avatars.githubusercontent.com/u/26426873?v=4',
role: '前端打工仔',
desc: '负责WotUI组件库的开发和维护',
github: 'https://github.com/Moonofweisheng',
},
{
name: '二狗',
avatar: 'https://avatars.githubusercontent.com/u/50100966?v=4',
role: '灵活就业大师',
desc: '问题毁灭者,总能迅速解决各种技术难题',
github: 'https://github.com/810505339',
},
{
name: 'RJQingHuan',
avatar: 'https://avatars.githubusercontent.com/u/53939074?v=4',
role: 'Pull Shark',
desc: '热衷参与开源组件建设',
github: 'https://github.com/RJQingHuan',
},
{
name: 'skiyee',
avatar: 'https://avatars.githubusercontent.com/u/120664167?v=4',
role: 'uni-ku 的创立者,重金雇佣兵',
desc: '精通 JS 和 TS 的全能攻城狮',
github: 'https://github.com/skiyee',
},
{
name: 'jasper-ops',
avatar: 'https://avatars.githubusercontent.com/u/85024227?v=4',
role: '新技术狂热分子',
desc: '始终走在技术前沿,热衷于探索最新的开发趋势',
github: 'https://github.com/jasper-ops',
},
]
function openUrl(url: string) {
window.open(url, '_blank')
}
// 打开公众号二维码
function openWeChat() {
uni.previewImage({
urls: ['https://wot-ui.cn/wechatPublicAccount.png'],
})
}
// 打开捐赠二维码
function donate() {
uni.previewImage({
urls: ['https://wot-ui.cn/weixinQrcode.jpg'],
})
}
</script>
<template>
<view class="min-h-screen bg-gray-100 py-3 dark:bg-[var(--wot-dark-background)]">
<!-- 头部介绍 -->
<view class="mx-3 mb-3 flex flex-col gap-2">
<text class="text-6 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
关于我们
</text>
<text class="text-3.5 text-gray-600 leading-snug dark:text-[var(--wot-dark-color2)]">
我是不如摸鱼去,一个前端打工仔,我和我的小伙伴们正在致力于开发轻量、高效的uni-app组件库和高效、易用的uni-app快速开发模板。
</text>
</view>
<!-- 核心团队 -->
<demo-block title="核心团队" transparent>
<view class="grid grid-cols-2 gap-3">
<view
v-for="member in coreTeam"
:key="member.name"
class="rounded-2 bg-white p-4 text-center dark:bg-[var(--wot-dark-background2)]"
@click="openUrl(member.github)"
>
<image
:src="member.avatar"
class="mx-auto mb-2 h-16 w-16 border-2 border-blue-200 rounded-full dark:border-blue-800"
/>
<view class="mb-1 text-3.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ member.name }}
</view>
<view class="mb-2 text-2.5 text-blue-600 dark:text-blue-400">
{{ member.role }}
</view>
<view class="text-2.5 text-gray-600 leading-snug dark:text-[var(--wot-dark-color2)]">
{{ member.desc }}
</view>
</view>
</view>
</demo-block>
<!-- 关于 uni-helper -->
<demo-block title="关于 uni-helper 团队" transparent>
<view class="rounded-3 bg-white p-5 dark:bg-[var(--wot-dark-background2)]">
<text class="mb-3 block text-3.5 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
<text class="text-blue-600" @click="openUrl('https://uni-helper.cn/')">
uni-helper
</text>
是一个旨在增强 uni-app 系列产品的开发体验为爱发电的非官方组织。作为靠爱发电的非官方项目,uni-helper 提供了打包工具插件支持、编辑器扩展支持、NPM 包等并尽力维护它们。
</text>
<text class="text-3.5 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
在此我们特别向 uni-helper 团队表示感谢,他们为 uni-app 系列产品提供了强大的支持,包括打包工具插件支持、编辑器扩展支持等,这使我们得以站在巨人的巨人的肩膀上完成此项目。
</text>
</view>
</demo-block>
<!-- 更多信息 -->
<demo-block title="更多信息" transparent>
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell
title="关注公众号"
title-width="200px"
label="uni-app教程、组件库讯息一手掌握!"
is-link
@click="openWeChat"
/>
<wd-cell
title="捐赠"
title-width="200px"
label="每一份捐赠都是对我们莫大的鼓励!"
is-link
@click="donate"
/>
</wd-cell-group>
</demo-block>
</view>
</template>
+225
View File
@@ -0,0 +1,225 @@
<script setup lang="ts">
import type { PasswordChangeForm, UserInfo } from '@/api/user'
import { computed, onMounted, reactive, ref } from 'vue'
import UserAPI from '@/api/user'
definePage({
name: 'account',
style: {
navigationBarTitleText: '账号设置',
},
})
const userProfile = ref<UserInfo>() // 用户信息
const passwordChangeForm = reactive<PasswordChangeForm>({
old_password: '',
new_password: '',
confirm_password: '',
})
const passwordChangeFormRef = ref()
// 本页面中所有的校验规则
const rules = reactive({
oldPassword: [{ required: true, message: '请填写原密码' }],
newPassword: [{ required: true, message: '请填写新密码' }],
confirmPassword: [
{
required: true,
message: '请确认密码',
validator: (value: string) => {
if (!value) {
return Promise.reject(new Error('请确认密码'))
}
else {
if (value !== passwordChangeForm.new_password) {
return Promise.reject(new Error('两次输入的密码不一致'))
}
else {
return Promise.resolve()
}
}
},
},
],
mobile: [
{
required: true,
pattern: /^1[3-9]\d{9}$/,
message: '请填写正确的手机号码',
},
],
code: [{ required: true, message: '请填写验证码' }],
email: [
{
required: true,
pattern: /\w[-\w.+]*@([A-Z0-9][-A-Z0-9]+\.)+[A-Z]{2,14}/i,
message: '请填写正确的邮箱地址',
},
],
})
enum DialogType {
PASSWORD = 'password',
MOBILE = 'mobile',
EMAIL = 'email',
}
const dialog = reactive({
visible: false,
type: '' as DialogType,
})
const getDialogTitle = computed(() => {
switch (dialog?.type) {
case DialogType.PASSWORD:
return '修改密码'
case DialogType.MOBILE:
return '绑定手机'
case DialogType.EMAIL:
return '绑定邮箱'
default:
return '账号设置'
}
})
async function loadUserProfile() {
userProfile.value = await UserAPI.getCurrentUserInfo()
}
/**
* 打开弹窗
* @param type 弹窗类型 ACCOUNT: 账号资料 PASSWORD: 修改密码 MOBILE: 绑定手机 EMAIL: 绑定邮箱
*/
function handleOpenDialog(type: DialogType) {
dialog.type = type
dialog.visible = true
switch (type) {
case DialogType.PASSWORD:
passwordChangeForm.old_password = ''
passwordChangeForm.new_password = ''
passwordChangeForm.confirm_password = ''
break
}
}
// 提交表单
function handleSubmit() {
if (dialog.type === DialogType.PASSWORD) {
passwordChangeFormRef.value
.validate()
.then(({ valid }: { valid: boolean }) => {
if (valid) {
UserAPI.changeCurrentUserPassword(passwordChangeForm)
.then(() => {
uni.showToast({ title: '密码修改成功', icon: 'success' })
dialog.visible = false
})
.catch((error: any) => {
uni.showToast({
title: error?.message || '密码修改失败',
icon: 'error',
})
})
}
})
}
}
onMounted(() => {
loadUserProfile()
})
</script>
<template>
<view class="app-container">
<wd-card custom-style="margin-top: 20rpx" class="theme-card">
<wd-cell-group border>
<wd-cell
title="账户密码"
label="定期修改密码有助于保护账户安全"
value="修改"
is-link
@click="handleOpenDialog(DialogType.PASSWORD)"
>
<template #title>
<text class="theme-text-primary">
账户密码
</text>
</template>
<template #label>
<text class="theme-text-secondary">
定期修改密码有助于保护账户安全
</text>
</template>
<template #value>
<text class="theme-text-secondary">
修改
</text>
</template>
</wd-cell>
</wd-cell-group>
</wd-card>
<!-- 用户信息编辑弹出框 -->
<wd-popup
v-model="dialog.visible"
position="bottom"
custom-style="border-top-left-radius: 16rpx; border-top-right-radius: 16rpx;"
>
<view class="popup-header">
<wd-cell :title="getDialogTitle" center>
<template #right-icon>
<wd-icon name="close" size="20" @click="dialog.visible = false" />
</template>
</wd-cell>
</view>
<wd-divider />
<wd-form
v-if="dialog.type === DialogType.PASSWORD"
ref="passwordChangeFormRef"
:model="passwordChangeForm"
custom-class="edit-form"
>
<wd-cell-group border>
<wd-input
v-model="passwordChangeForm.old_password"
label="原密码"
label-width="160rpx"
show-password
clearable
placeholder="请输入原密码"
prop="old_password"
:rules="rules.oldPassword"
/>
<wd-input
v-model="passwordChangeForm.new_password"
label="新密码"
label-width="160rpx"
show-password
clearable
placeholder="请输入新密码"
prop="new_password"
:rules="rules.newPassword"
/>
<wd-input
v-model="passwordChangeForm.confirm_password"
label="确认密码"
label-width="160rpx"
show-password
clearable
placeholder="请确认新密码"
prop="confirm_password"
:rules="rules.confirmPassword"
/>
</wd-cell-group>
<view class="p-6">
<wd-button type="primary" size="large" block @click="handleSubmit">
提交
</wd-button>
</view>
</wd-form>
</wd-popup>
</view>
</template>
@@ -0,0 +1,128 @@
<script setup lang="ts">
import BarChart from '@/subPages/echarts/components/BarChart.vue'
import DonutChart from '@/subPages/echarts/components/DonutChart.vue'
import FunnelChart from '@/subPages/echarts/components/FunnelChart.vue'
import GaugeChart from '@/subPages/echarts/components/GaugeChart.vue'
import LineChart from '@/subPages/echarts/components/LineChart.vue'
import LiquidFillChart from '@/subPages/echarts/components/LiquidFillChart.vue'
import MiniLineChart from '@/subPages/echarts/components/MiniLineChart.vue'
import PieChart from '@/subPages/echarts/components/PieChart.vue'
import RadarChart from '@/subPages/echarts/components/RadarChart.vue'
import ScatterChart from '@/subPages/echarts/components/ScatterChart.vue'
import StackedBarChart from '@/subPages/echarts/components/StackedBarChart.vue'
defineOptions({
componentPlaceholder: {
BarChart: 'view',
DonutChart: 'view',
FunnelChart: 'view',
GaugeChart: 'view',
LineChart: 'view',
LiquidFillChart: 'view',
MiniLineChart: 'view',
PieChart: 'view',
RadarChart: 'view',
ScatterChart: 'view',
StackedBarChart: 'view',
},
})
definePage({
name: 'echartsAsync',
style: {
navigationBarTitleText: 'echarts 异步跨包示例',
},
})
</script>
<template>
<view class="bg-gray-50 p-5">
<!-- 饼图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
饼图示例
</view>
<PieChart />
</view>
<!-- 柱状图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
柱状图示例
</view>
<BarChart />
</view>
<!-- 折线图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
折线图示例
</view>
<LineChart />
</view>
<!-- 雷达图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
雷达图示例
</view>
<RadarChart />
</view>
<!-- 散点图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
散点图示例
</view>
<ScatterChart />
</view>
<!-- 环形图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
环形图示例
</view>
<DonutChart />
</view>
<!-- 漏斗图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
漏斗图示例
</view>
<FunnelChart />
</view>
<!-- 仪表盘图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
仪表盘示例
</view>
<GaugeChart />
</view>
<!-- 迷你折线图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
迷你折线图示例
</view>
<MiniLineChart />
</view>
<!-- 堆叠柱状图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
堆叠柱状图示例
</view>
<StackedBarChart />
</view>
<!-- 水球图 -->
<view class="mb-5 rounded-3 bg-white p-5 shadow-sm">
<view class="mb-5 text-center text-base text-gray-800 font-medium">
水球图示例
</view>
<LiquidFillChart />
</view>
</view>
</template>
+428
View File
@@ -0,0 +1,428 @@
<script setup lang="ts">
definePage({
name: 'ci',
style: {
navigationBarTitleText: 'CI 持续集成',
},
})
const { success: showSuccess } = useGlobalToast()
// 配置示例数据
const configExample = ref(`{
"weixin": {
"appid": "微信小程序appid",
"privateKeyPath": "key/private.appid.key",
"projectPath": "dist/build/mp-weixin"
},
"alipay": {
"appid": "支付宝小程序appid",
"toolId": "支付宝开放平台工具ID",
"privateKey": "私钥文本内容",
"projectPath": "dist/build/mp-alipay",
"autoincrement": true
},
"dd": {
"appid": "钉钉小程序appid",
"token": "钉钉开发者后台的 API Token",
"projectPath": "dist/build/mp-alipay",
"autoincrement": true
},
"version": "0.0.1",
"desc": "版本描述"
}`)
const packageScripts = ref(`{
"scripts": {
"upload:weixin": "minici --platform weixin",
"upload:alipay": "minici --platform alipay",
"upload:dd": "minici --platform dd",
"upload:mp-weixin": "uni build -p mp-weixin && minici --platform weixin",
"upload:mp-alipay": "uni build -p mp-alipay && minici --platform alipay",
"upload:mp-dingtalk": "uni build -p mp-dingtalk && minici --platform dd"
}
}`)
// 特性列表
const features = ref([
{
icon: '📱',
title: '多平台支持',
desc: '支持微信、支付宝、钉钉小程序的上传',
},
{
icon: '🔢',
title: '版本管理',
desc: '支持指定版本号和版本号自增',
},
{
icon: '⚙️',
title: '简单配置',
desc: '通过配置文件轻松管理多平台参数',
},
{
icon: '🔄',
title: 'CI/CD集成',
desc: '完美集成持续集成和持续部署流程',
},
{
icon: '🛠️',
title: '命令行工具',
desc: '支持全局和局部安装,灵活使用',
},
{
icon: '📦',
title: '打包上传',
desc: '支持打包和上传命令组合使用',
},
])
// 平台配置
const platforms = ref([
{
name: 'weixin',
title: '微信小程序',
icon: '💚',
color: 'text-green-600',
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
features: ['支持企业微信', 'CI机器人选择', '编译设置', '代码压缩保护'],
},
{
name: 'alipay',
title: '支付宝小程序',
icon: '💙',
color: 'text-blue-600',
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
features: ['多终端支持', '版本号自增', '私钥认证', '开放平台集成'],
},
{
name: 'dd',
title: '钉钉小程序',
icon: '💜',
color: 'text-purple-600',
bgColor: 'bg-purple-50',
borderColor: 'border-purple-200',
features: ['企业应用', '版本号自增', 'Token认证', '多应用类型'],
},
])
// 复制代码到剪贴板
function copyCode(code: string) {
uni.setClipboardData({
data: code,
showToast: false,
success: () => {
uni.hideToast()
showSuccess({ msg: '代码已复制到剪贴板' })
},
})
}
// 链接导航处理
function handleNavigate(url: string) {
// #ifdef H5
window.open(url, '_blank')
// #endif
// #ifndef H5
uni.setClipboardData({
data: url,
showToast: false,
success: () => {
uni.hideToast()
showSuccess({ msg: `${url} 已复制到剪贴板` })
},
})
// #endif
}
</script>
<template>
<view class="min-h-screen bg-gray-100 py-3 dark:bg-[var(--wot-dark-background)]">
<!-- 头部介绍 -->
<view class="mx-3 mb-3">
<view class="rounded-3 bg-white px-5 py-8 text-center dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 text-10">
🚀
</view>
<view class="mb-2 text-6 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
uni-mini-ci
</view>
<view class="mb-2 text-3.5 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
小程序持续集成的插件
</view>
<view class="text-3 text-gray-500 dark:text-[var(--wot-dark-color2)]">
支持上传微信、企业微信、钉钉、支付宝平台的小程序
</view>
</view>
</view>
<!-- 核心特性 -->
<demo-block title="核心特性" transparent>
<view class="grid grid-cols-2 gap-3">
<view
v-for="feature in features"
:key="feature.title"
class="rounded-2 bg-white p-4 text-center dark:bg-[var(--wot-dark-background2)]"
>
<view class="mb-2 text-6">
{{ feature.icon }}
</view>
<view class="mb-1 text-3.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ feature.title }}
</view>
<view class="text-3 text-gray-600 leading-snug dark:text-[var(--wot-dark-color2)]">
{{ feature.desc }}
</view>
</view>
</view>
</demo-block>
<!-- 安装方式 -->
<demo-block title="安装方式" transparent>
<view class="space-y-4">
<!-- 全局安装 -->
<view class="rounded-2 bg-white p-4 dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 text-4 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
全局安装
</view>
<view class="mb-3 text-3 text-gray-600 dark:text-[var(--wot-dark-color2)]">
推荐在开发环境中全局安装,方便在任何项目中使用
</view>
<view class="space-y-2">
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode('npm i uni-mini-ci -g')">
<text class="flex-1 text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
npm i uni-mini-ci -g
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
</view>
</view>
<!-- 局部安装 -->
<view class="rounded-2 bg-white p-4 dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 text-4 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
局部安装
</view>
<view class="mb-3 text-3 text-gray-600 dark:text-[var(--wot-dark-color2)]">
在项目中作为开发依赖安装,适合团队协作
</view>
<view class="space-y-2">
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode('npm i uni-mini-ci --save-dev')">
<text class="flex-1 text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
npm i uni-mini-ci --save-dev
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode('pnpm add uni-mini-ci -D')">
<text class="flex-1 text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
pnpm add uni-mini-ci -D
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
</view>
</view>
</view>
</demo-block>
<!-- 配置文件 -->
<demo-block title="配置文件" transparent>
<view class="rounded-2 bg-white p-4 dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 text-4 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
.minicirc 配置文件
</view>
<view class="mb-3 text-3 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
在项目根目录创建 <text class="rounded bg-gray-100 px-1 font-mono dark:bg-[var(--wot-dark-background3)]">
.minicirc
</text> 文件,配置各平台的上传参数
</view>
<view class="mb-3 border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode(configExample)">
<view class="mb-2 flex items-center justify-between">
<text class="text-3 text-gray-700 font-bold dark:text-[var(--wot-dark-color)]">
配置示例:
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
<view class="max-h-60 overflow-y-auto">
<pre class="whitespace-pre-wrap text-2.5 text-gray-600 leading-relaxed font-mono dark:text-[var(--wot-dark-color2)]">{{ configExample }}</pre>
</view>
</view>
<view class="border border-orange-200 rounded-2 bg-orange-50 p-3 dark:bg-orange-900/20">
<view class="mb-2 text-3.5 text-orange-700 font-bold dark:text-orange-300">
💡 配置说明
</view>
<view class="text-3 text-orange-600 leading-relaxed space-y-1 dark:text-orange-200">
<view>• version: 上传版本号,不传时默认读取 package.json 的 version 字段</view>
<view>• desc: 上传时的描述信息</view>
<view>• autoincrement: 版本号自增,建议测试环境开启</view>
</view>
</view>
</view>
</demo-block>
<!-- 支持平台 -->
<demo-block title="支持平台" transparent>
<view class="space-y-3">
<view
v-for="platform in platforms"
:key="platform.name"
class="rounded-2 bg-white p-4 dark:bg-[var(--wot-dark-background2)]"
>
<view class="mb-3 flex items-center">
<view class="mr-2 text-6">
{{ platform.icon }}
</view>
<view class="text-4 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ platform.title }}
</view>
</view>
<view class="grid grid-cols-2 mb-3 gap-2">
<view
v-for="feature in platform.features"
:key="feature"
class="rounded bg-gray-100 px-2 py-1 text-center text-2.5 text-gray-600 dark:bg-[var(--wot-dark-background3)] dark:text-[var(--wot-dark-color2)]"
>
{{ feature }}
</view>
</view>
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode(`minici --platform ${platform.name}`)">
<text class="flex-1 text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
minici --platform {{ platform.name }}
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
</view>
</view>
</demo-block>
<!-- 添加命令 -->
<demo-block title="添加命令" transparent>
<view class="rounded-2 bg-white p-4 dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 text-4 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
package.json 脚本配置
</view>
<view class="mb-3 text-3 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
在 package.json 中添加上传脚本,支持单独上传和打包上传组合
</view>
<view class="mb-3 border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode(packageScripts)">
<view class="mb-2 flex items-center justify-between">
<text class="text-3 text-gray-700 font-bold dark:text-[var(--wot-dark-color)]">
脚本配置:
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
<view class="max-h-60 overflow-y-auto">
<pre class="whitespace-pre-wrap text-2.5 text-gray-600 leading-relaxed font-mono dark:text-[var(--wot-dark-color2)]">{{ packageScripts }}</pre>
</view>
</view>
<view class="space-y-2">
<view class="text-3.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
使用命令:
</view>
<view class="grid grid-cols-1 gap-2">
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode('pnpm upload:weixin')">
<text class="flex-1 text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
pnpm upload:weixin
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCode('pnpm upload:mp-weixin')">
<text class="flex-1 text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
pnpm upload:mp-weixin
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
</view>
</view>
</view>
</demo-block>
<!-- 使用步骤 -->
<demo-block title="使用步骤" transparent>
<view class="space-y-3">
<view
v-for="(step, index) in [
{ title: '安装插件', desc: '选择全局或局部安装方式', icon: '📦' },
{ title: '创建配置', desc: '在根目录创建 .minicirc 配置文件', icon: '⚙️' },
{ title: '配置平台', desc: '填写各平台的 appid、密钥等信息', icon: '🔑' },
{ title: '添加脚本', desc: '在 package.json 中添加上传命令', icon: '📝' },
{ title: '执行上传', desc: '运行命令上传到对应平台', icon: '🚀' },
]"
:key="index"
class="rounded-2 bg-white p-4 dark:bg-[var(--wot-dark-background2)]"
>
<view class="flex items-center">
<view class="mr-3 h-8 w-8 flex items-center justify-center rounded-full bg-blue-100 text-3.5 text-blue-600 font-bold dark:bg-blue-900/20 dark:text-blue-400">
{{ index + 1 }}
</view>
<view class="mr-2 text-5">
{{ step.icon }}
</view>
<view class="flex-1">
<view class="text-3.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ step.title }}
</view>
<view class="text-3 text-gray-600 dark:text-[var(--wot-dark-color2)]">
{{ step.desc }}
</view>
</view>
</view>
</view>
</view>
</demo-block>
<!-- 注意事项 -->
<demo-block title="注意事项" transparent>
<view class="space-y-3">
<view class="border border-red-200 rounded-2 bg-red-50 p-4 dark:bg-red-900/20">
<view class="mb-2 text-3.5 text-red-700 font-bold dark:text-red-300">
⚠️ 版本号限制
</view>
<view class="text-3 text-red-600 leading-relaxed dark:text-red-200">
支付宝和钉钉小程序不支持上传的版本号小于或等于现有版本号。建议测试环境开启 autoincrement 字段,生产环境手动指定版本号。
</view>
</view>
<view class="border border-blue-200 rounded-2 bg-blue-50 p-4 dark:bg-blue-900/20">
<view class="mb-2 text-3.5 text-blue-700 font-bold dark:text-blue-300">
💡 最佳实践
</view>
<view class="text-3 text-blue-600 leading-relaxed space-y-1 dark:text-blue-200">
<view>• 测试环境:开启版本号自增,频繁调试更方便</view>
<view>• 生产环境:手动指定版本号,便于版本管理</view>
<view>• 密钥安全:不要将私钥文件提交到代码仓库</view>
<view>• CI/CD:结合打包命令,实现自动化部署</view>
</view>
</view>
<view class="border border-green-200 rounded-2 bg-green-50 p-4 dark:bg-green-900/20">
<view class="mb-2 text-3.5 text-green-700 font-bold dark:text-green-300">
🔒 安全建议
</view>
<view class="text-3 text-green-600 leading-relaxed space-y-1 dark:text-green-200">
<view>• 使用环境变量存储敏感信息</view>
<view>• 将配置文件添加到 .gitignore</view>
<view>• 定期更换密钥和 Token</view>
<view>• 限制 CI 机器人权限范围</view>
</view>
</view>
</view>
</demo-block>
<!-- 相关链接 -->
<demo-block title="相关链接" transparent>
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell title="📦 NPM 包" value="uni-mini-ci" is-link @click="handleNavigate('https://www.npmjs.com/package/uni-mini-ci')" />
<wd-cell title="🐙 GitHub 仓库" value="源码仓库" is-link @click="handleNavigate('https://github.com/Moonofweisheng/uni-mini-ci')" />
<wd-cell title="📱 微信小程序 CI" value="官方文档" is-link @click="handleNavigate('https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html')" />
<wd-cell title="💙 支付宝小程序 CI" value="官方文档" is-link @click="handleNavigate('https://opendocs.alipay.com/mini/02q29z')" />
<wd-cell title="💜 钉钉小程序 CI" value="官方文档" is-link @click="handleNavigate('https://github.com/open-dingtalk/dingtalk-design-cli')" />
</wd-cell-group>
</demo-block>
</view>
</template>
@@ -0,0 +1,252 @@
<script setup lang="ts">
definePage({
name: 'create-uni',
style: {
navigationBarTitleText: 'CreateUni 脚手架',
},
})
const { success: showSuccess } = useGlobalToast()
// 核心优势
const advantages = ref([
{
icon: '🚀',
title: '简单易用',
desc: '一行命令即可创建项目,无需手动配置开发环境',
},
{
icon: '🔧',
title: '功能强大',
desc: '内置多种实用工具和插件,支持一键集成 WotUI',
},
{
icon: '🔄',
title: '持续更新',
desc: '由 uni-helper 团队积极维护,及时跟进最新技术栈',
},
{
icon: '👥',
title: '社区活跃',
desc: 'GitHub 获得众多 star,活跃的开发者社区',
},
{
icon: '🌐',
title: '网络稳定',
desc: '解决官方 CLI 网络不稳定问题,支持国内环境',
},
{
icon: '⚡️',
title: '开发效率',
desc: '提升开发效率 100%,快速搭建现代化项目',
},
])
// 解决的问题
const problems = ref([
{
problem: '网络问题',
desc: 'degit 命令只支持 GitHub,国内网络不稳定',
},
{
problem: '维护问题',
desc: '官方模板维护不够积极,版本往往落后',
},
{
problem: '工程化问题',
desc: '项目配置过于基础,缺少现代工程化支持',
},
{
problem: '集成问题',
desc: '需要手动集成组件库,配置过程繁琐',
},
])
// WotUI 快速创建
const wotQuickStart = ref({
title: '快速创建 WotUI 项目',
command: 'pnpm create uni@latest -t wot-starter <你的项目名称>',
desc: '一键创建集成了 WotUI 的完整项目',
features: [
'✅ TypeScript 项目',
'✅ 集成 Pinia 状态管理',
'✅ 自动配置 WotUI 组件库',
'✅ 添加 ESLint 支持',
],
})
// 其他使用方式
const quickMethods = ref([
{
title: 'GUI 模式',
command: 'pnpm create uni --gui',
desc: '使用图形界面创建项目',
},
{
title: '环境信息',
command: 'npx @create-uni/info@latest',
desc: '获取当前项目环境信息',
},
])
// 复制命令到剪贴板
function copyCommand(command: string) {
uni.setClipboardData({
data: command,
showToast: false,
success: () => {
uni.hideToast()
showSuccess({
msg: `${command}已成功复制到剪贴板`,
})
},
})
}
function handleNavigate(url: string) {
// #ifdef H5
window.open(url, '_blank')
// #endif
// #ifndef H5
copyCommand(url)
// #endif
}
</script>
<template>
<view class="min-h-screen bg-gray-100 py-3 dark:bg-[var(--wot-dark-background)]">
<!-- 头部介绍 -->
<view class="mx-3 mb-3">
<view class="rounded-3 bg-white px-5 py-8 text-center dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 text-10">
🌱
</view>
<view class="mb-2 text-6 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
CreateUni 脚手架
</view>
<view class="mb-2 text-3.5 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
由 uni-helper 团队开发的轻量级脚手架工具
</view>
<view class="text-3 text-gray-500 dark:text-[var(--wot-dark-color2)]">
提升开发效率 100%,告别繁琐配置,一键集成 WotUI 组件库
</view>
</view>
</view>
<!-- 为什么选择 create-uni -->
<demo-block title="为什么选择 create-uni?" transparent>
<view class="mb-3 rounded-3 bg-white p-3 dark:bg-[var(--wot-dark-background2)]">
<text class="text-3.5 text-gray-700 leading-relaxed dark:text-[var(--wot-dark-color)]">
在使用 uni-app 官方 CLI 工具时,我们经常会遇到以下问题:
</text>
</view>
<view class="mb-3">
<view
v-for="item in problems"
:key="item.problem"
class="mb-2 rounded-3 bg-white p-3 dark:bg-[var(--wot-dark-background2)]"
>
<view class="mb-1 text-3.5 text-red-600 font-bold dark:text-red-400">
❌ {{ item.problem }}
</view>
<view class="text-3 text-gray-600 leading-snug dark:text-[var(--wot-dark-color2)]">
{{ item.desc }}
</view>
</view>
</view>
<view class="rounded-3 bg-white p-3 dark:bg-[var(--wot-dark-background2)]">
<text class="text-3.5 text-green-600 font-bold dark:text-green-400">
✅ 而 create-uni 的出现,完美解决了这些问题!
</text>
</view>
</demo-block>
<!-- 核心优势 -->
<demo-block title="核心优势" transparent>
<view class="grid grid-cols-2 gap-3">
<view
v-for="advantage in advantages"
:key="advantage.title"
class="rounded-2 bg-white p-4 text-center dark:bg-[var(--wot-dark-background2)]"
>
<view class="mb-2 text-6">
{{ advantage.icon }}
</view>
<view class="mb-1 text-3.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ advantage.title }}
</view>
<view class="text-3 text-gray-600 leading-snug dark:text-[var(--wot-dark-color2)]">
{{ advantage.desc }}
</view>
</view>
</view>
</demo-block>
<!-- 快速创建 WotUI 项目 -->
<demo-block title="快速创建 WotUI 项目" transparent>
<view class="rounded-3 bg-white p-5 dark:bg-[var(--wot-dark-background2)]">
<view class="mb-3 flex items-center">
<view class="mr-2 text-7">
🎨
</view>
<view class="text-4.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ wotQuickStart.title }}
</view>
</view>
<view class="mb-3 text-3.5 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
{{ wotQuickStart.desc }}
</view>
<view class="mb-3 flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-3 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCommand(wotQuickStart.command)">
<text class="flex-1 break-all text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
{{ wotQuickStart.command }}
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
<view>
<view class="mb-2 text-3.5 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
这个命令会:
</view>
<view
v-for="feature in wotQuickStart.features"
:key="feature"
class="mb-1 text-3 text-gray-600 leading-snug dark:text-[var(--wot-dark-color2)]"
>
{{ feature }}
</view>
</view>
</view>
</demo-block>
<!-- 其他使用方式 -->
<demo-block title="其他使用方式" transparent>
<view
v-for="method in quickMethods"
:key="method.title"
class="mb-3 rounded-2 bg-white p-4 last:mb-0 dark:bg-[var(--wot-dark-background2)]"
>
<view class="mb-2 text-4 text-gray-800 font-bold dark:text-[var(--wot-dark-color)]">
{{ method.title }}
</view>
<view class="mb-2 text-3.5 text-gray-600 leading-relaxed dark:text-[var(--wot-dark-color2)]">
{{ method.desc }}
</view>
<view class="flex items-center justify-between border border-gray-200 rounded-2 bg-gray-50 p-2 dark:border-[var(--wot-dark-border)] dark:bg-[var(--wot-dark-background3)]" @click="copyCommand(method.command)">
<text class="flex-1 break-all text-3 text-gray-700 font-mono dark:text-[var(--wot-dark-color)]">
{{ method.command }}
</text>
<wd-icon name="copy" size="16px" color="#666" />
</view>
</view>
</demo-block>
<!-- 相关链接 -->
<demo-block title="相关链接" transparent>
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
<wd-cell title="📦 NPM 包" value="create uni" is-link @click="handleNavigate('https://www.npmjs.com/package/create-uni')" />
<wd-cell title="🐙 GitHub 仓库" value="create uni" is-link @click="handleNavigate('https://github.com/uni-helper/create-uni')" />
<wd-cell title="🎨 Wot UI" value="UI 组件库" is-link @click="handleNavigate('https://wot-ui.cn/')" />
<wd-cell title="🛠️ Uni Helper" value="先进工具集合" is-link @click="handleNavigate('https://github.com/uni-helper')" />
<wd-cell title="💝 赞助CreateUni" value="支持开发者" is-link @click="handleNavigate('https://github.com/uni-helper/create-uni')" />
</wd-cell-group>
</demo-block>
</view>
</template>
@@ -0,0 +1,72 @@
<!--
* @Author: weisheng
* @Date: 2025-09-15 18:51:03
* @LastEditTime: 2025-09-16 13:12:22
* @LastEditors: weisheng
* @Description:
* @FilePath: /wot-starter/src/subPages/echarts/components/BarChart.vue
* 记得注释
-->
<script setup lang="ts">
import { BarChart } from 'echarts/charts'
import { DatasetComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent,
LegendComponent,
TooltipComponent,
DatasetComponent,
BarChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
legend: {
data: ['销售额', '利润'],
top: 30,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '销售额',
type: 'bar',
data: [120, 200, 150, 80, 70, 110],
itemStyle: {
color: '#5470c6',
},
},
{
name: '利润',
type: 'bar',
data: [20, 40, 30, 15, 12, 22],
itemStyle: {
color: '#91cc75',
},
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,106 @@
<script setup lang="ts">
import { PieChart } from 'echarts/charts'
import { DatasetComponent, GraphicComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
LegendComponent,
TooltipComponent,
DatasetComponent,
PieChart,
CanvasRenderer,
GraphicComponent,
])
const option = ref({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1,
// #endif
},
},
legend: {
orient: 'horizontal',
bottom: 10,
left: 'center',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12,
},
},
series: [
{
name: '用户占比',
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: 18,
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
itemStyle: {
borderWidth: 2,
borderColor: '#ffffff',
},
data: [
{
value: 335,
name: 'iOS',
itemStyle: {
color: '#5470c6',
},
},
{
value: 310,
name: 'Android',
itemStyle: {
color: '#91cc75',
},
},
{
value: 234,
name: 'H5',
itemStyle: {
color: '#fac858',
},
},
{
value: 135,
name: '小程序',
itemStyle: {
color: '#ee6666',
},
},
{
value: 86,
name: '其他',
itemStyle: {
color: '#73c0de',
},
},
],
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,116 @@
<script setup lang="ts">
import { FunnelChart } from 'echarts/charts'
import { LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
LegendComponent,
TooltipComponent,
FunnelChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)',
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1,
// #endif
},
},
legend: {
orient: 'horizontal',
bottom: 10,
left: 'center',
itemWidth: 12,
itemHeight: 12,
textStyle: {
fontSize: 12,
},
},
series: [
{
name: '销售漏斗',
type: 'funnel',
left: '10%',
top: 60,
width: '80%',
height: '60%',
min: 0,
max: 100,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside',
fontSize: 12,
color: '#fff',
fontWeight: 'bold',
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid',
},
},
itemStyle: {
borderColor: '#fff',
borderWidth: 1,
},
emphasis: {
label: {
fontSize: 14,
},
},
data: [
{
value: 100,
name: '访问量',
itemStyle: {
color: '#5470c6',
},
},
{
value: 80,
name: '浏览商品',
itemStyle: {
color: '#91cc75',
},
},
{
value: 60,
name: '加入购物车',
itemStyle: {
color: '#fac858',
},
},
{
value: 40,
name: '提交订单',
itemStyle: {
color: '#ee6666',
},
},
{
value: 20,
name: '支付成功',
itemStyle: {
color: '#73c0de',
},
},
],
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { GaugeChart } from 'echarts/charts'
import { TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
TooltipComponent,
GaugeChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
formatter: '{a} <br/>{b}: {c}%',
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1,
// #endif
},
},
series: [
{
name: '性能指标',
type: 'gauge',
center: ['50%', '60%'],
radius: '75%',
min: 0,
max: 100,
splitNumber: 10,
axisLine: {
lineStyle: {
width: 10,
color: [
[0.3, '#ee6666'],
[0.7, '#fac858'],
[1, '#91cc75'],
],
},
},
pointer: {
itemStyle: {
color: 'auto',
},
},
axisTick: {
distance: -30,
length: 8,
lineStyle: {
color: '#fff',
width: 2,
},
},
splitLine: {
distance: -30,
length: 30,
lineStyle: {
color: '#fff',
width: 4,
},
},
axisLabel: {
color: 'auto',
distance: 40,
fontSize: 12,
},
detail: {
valueAnimation: true,
formatter: '{value}%',
color: 'auto',
fontSize: 20,
fontWeight: 'bold',
offsetCenter: [0, '70%'],
},
data: [
{
value: 75,
name: 'CPU使用率',
},
],
},
],
})
// 模拟数据变化
let timer: NodeJS.Timeout | null = null
onMounted(() => {
timer = setInterval(() => {
const newValue = Math.round(Math.random() * 100)
option.value.series[0].data[0].value = newValue
}, 3000)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,103 @@
<script setup lang="ts">
import { LineChart } from 'echarts/charts'
import { DatasetComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent,
LegendComponent,
TooltipComponent,
DatasetComponent,
LineChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
trigger: 'axis',
},
legend: {
data: ['访问量', '用户数'],
top: 30,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
},
yAxis: {
type: 'value',
},
series: [
{
name: '访问量',
type: 'line',
stack: 'Total',
data: [120, 132, 101, 134, 90, 230, 210],
smooth: true,
itemStyle: {
color: '#5470c6',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.3)',
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.1)',
},
],
},
},
},
{
name: '用户数',
type: 'line',
stack: 'Total',
data: [220, 182, 191, 234, 290, 330, 310],
smooth: true,
itemStyle: {
color: '#91cc75',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(145, 204, 117, 0.3)',
},
{
offset: 1,
color: 'rgba(145, 204, 117, 0.1)',
},
],
},
},
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,142 @@
<script setup lang="ts">
import { PieChart } from 'echarts/charts'
import { TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
TooltipComponent,
PieChart,
CanvasRenderer,
])
const progress = ref(68)
const option = computed(() => ({
tooltip: {
// formatter: () => `完成度: ${progress.value}%`,
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1,
// #endif
},
},
graphic: [
{
type: 'text',
left: 'center',
top: 'center',
style: {
text: `完成度:${progress.value}%`,
fontSize: 24,
fontWeight: 'bold',
fill: '#5470c6',
},
},
],
series: [
{
type: 'pie',
radius: ['60%', '80%'],
center: ['50%', '50%'],
startAngle: 90,
clockwise: true,
silent: true,
label: {
show: false,
},
labelLine: {
show: false,
},
data: [
{
value: progress.value,
itemStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: '#5470c6',
},
{
offset: 1,
color: '#91cc75',
},
],
},
borderRadius: 10,
},
},
{
value: 100 - progress.value,
itemStyle: {
color: '#f0f0f0',
borderRadius: 10,
},
},
],
},
// 内圆背景
{
type: 'pie',
radius: ['0%', '58%'],
center: ['50%', '50%'],
silent: true,
label: {
show: false,
},
labelLine: {
show: false,
},
data: [
{
value: 100,
itemStyle: {
color: {
type: 'radial',
x: 0.5,
y: 0.5,
r: 0.5,
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.1)',
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.05)',
},
],
},
},
},
],
},
],
}))
// 模拟进度变化
let timer: NodeJS.Timeout | null = null
onMounted(() => {
timer = setInterval(() => {
progress.value = Math.round(Math.random() * 100)
}, 4000)
})
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,134 @@
<script setup lang="ts">
import { LineChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent,
TooltipComponent,
LineChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(50,50,50,0.8)',
borderColor: 'transparent',
textStyle: {
color: '#fff',
fontSize: 12,
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1,
// #endif
},
formatter: (params: any) => {
const param = params[0]
return `${param.name}<br/>${param.seriesName}: ${param.value}`
},
},
grid: {
left: '8%',
right: '8%',
top: '25%',
bottom: '15%',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
axisLabel: {
fontSize: 10,
color: '#666',
},
axisLine: {
lineStyle: {
color: '#e0e0e0',
},
},
axisTick: {
show: false,
},
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 10,
color: '#666',
},
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: '#f0f0f0',
type: 'dashed',
},
},
},
series: [
{
name: '访问量',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 3,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: '#5470c6',
},
{
offset: 1,
color: '#91cc75',
},
],
},
},
itemStyle: {
color: '#5470c6',
borderWidth: 2,
borderColor: '#fff',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0,
color: 'rgba(84, 112, 198, 0.3)',
},
{
offset: 1,
color: 'rgba(84, 112, 198, 0.05)',
},
],
},
},
data: [120, 80, 150, 200, 180, 250, 160],
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { PieChart } from 'echarts/charts'
import { DatasetComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
LegendComponent,
TooltipComponent,
DatasetComponent,
PieChart,
CanvasRenderer,
])
const option = ref({
legend: {
top: 10,
left: 'center',
},
tooltip: {
trigger: 'item',
textStyle: {
// #ifdef MP-WEIXIN
// 临时解决微信小程序 tooltip 文字阴影问题
textShadowBlur: 1,
// #endif
},
},
series: [
{
type: 'pie',
radius: ['30%', '52%'],
label: {
show: false,
position: 'center',
},
itemStyle: {
borderWidth: 2,
borderColor: '#ffffff',
borderRadius: 10,
},
emphasis: {
label: {
show: true,
fontSize: 20,
},
},
},
],
dataset: {
dimensions: ['来源', '数量'],
source: [
['Search Engine', 1048],
['Direct', 735],
['Email', 580],
['Union Ads', 484],
['Video Ads', 300],
],
},
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,84 @@
<script setup lang="ts">
import { RadarChart } from 'echarts/charts'
import { LegendComponent, RadarComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
RadarComponent,
LegendComponent,
TooltipComponent,
RadarChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
trigger: 'item',
},
legend: {
data: ['张三', '李四'],
top: 30,
},
radar: {
indicator: [
{ name: '技术能力', max: 100 },
{ name: '沟通能力', max: 100 },
{ name: '学习能力', max: 100 },
{ name: '团队协作', max: 100 },
{ name: '创新能力', max: 100 },
{ name: '执行力', max: 100 },
],
shape: 'polygon',
splitNumber: 5,
axisName: {
color: '#666',
},
splitLine: {
lineStyle: {
color: 'rgba(211, 253, 250, 0.8)',
},
},
splitArea: {
show: false,
},
axisLine: {
lineStyle: {
color: 'rgba(211, 253, 250, 0.8)',
},
},
},
series: [
{
name: '能力评估',
type: 'radar',
data: [
{
value: [85, 75, 90, 80, 70, 85],
name: '张三',
itemStyle: {
color: '#5470c6',
},
areaStyle: {
color: 'rgba(84, 112, 198, 0.3)',
},
},
{
value: [70, 90, 80, 85, 85, 75],
name: '李四',
itemStyle: {
color: '#91cc75',
},
areaStyle: {
color: 'rgba(145, 204, 117, 0.3)',
},
},
],
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>
@@ -0,0 +1,120 @@
<script setup lang="ts">
import { ScatterChart } from 'echarts/charts'
import { DatasetComponent, GridComponent, LegendComponent, TooltipComponent } from 'echarts/components'
import * as echarts from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
GridComponent,
LegendComponent,
TooltipComponent,
DatasetComponent,
ScatterChart,
CanvasRenderer,
])
const option = ref({
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `${params.seriesName}<br/>身高: ${params.value[0]}cm<br/>体重: ${params.value[1]}kg`
},
},
legend: {
data: ['男性', '女性'],
top: 30,
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: {
type: 'value',
name: '身高(cm)',
nameLocation: 'middle',
nameGap: 30,
min: 150,
max: 190,
splitLine: {
lineStyle: {
type: 'dashed',
},
},
},
yAxis: {
type: 'value',
name: '体重(kg)',
nameLocation: 'middle',
nameGap: 30,
min: 40,
max: 90,
splitLine: {
lineStyle: {
type: 'dashed',
},
},
},
series: [
{
name: '男性',
type: 'scatter',
data: [
[175, 70],
[180, 75],
[170, 65],
[185, 80],
[172, 68],
[178, 72],
[182, 78],
[168, 62],
[176, 71],
[174, 69],
],
symbolSize: 8,
itemStyle: {
color: '#5470c6',
opacity: 0.8,
},
emphasis: {
itemStyle: {
borderColor: '#5470c6',
borderWidth: 2,
},
},
},
{
name: '女性',
type: 'scatter',
data: [
[160, 50],
[165, 55],
[158, 48],
[170, 60],
[162, 52],
[168, 58],
[155, 45],
[172, 62],
[164, 54],
[166, 56],
],
symbolSize: 8,
itemStyle: {
color: '#91cc75',
opacity: 0.8,
},
emphasis: {
itemStyle: {
borderColor: '#91cc75',
borderWidth: 2,
},
},
},
],
})
</script>
<template>
<uni-echarts custom-class="h-300px" :option="option" />
</template>

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