feat: 初始化前端项目基础结构
- 添加项目配置文件(tsconfig, eslint, prettier等) - 实现基础路由和页面布局 - 添加全局状态管理和API请求封装 - 集成UI组件库和主题系统 - 添加文档网站和示例页面 - 配置CI/CD和工作流
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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've switched to the design first approach!
|
||||
You can now help us improve the API whether it'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}']
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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've switched to the design first approach!
|
||||
You can now help us improve the API whether it'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;
|
||||
@@ -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 }
|
||||
@@ -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. 模拟数据应包含各种场景,包括成功和失败的情况
|
||||
@@ -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.* - 用户模块演示')
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)]
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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']>
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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('&')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"format": 2,
|
||||
"compileOptions": {
|
||||
"globalObjectMode": "enable"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
type Hooks = App.AppInstance & Page.PageInstance
|
||||
interface ComponentCustomOptions extends Hooks {}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 3.3 MiB |
@@ -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()
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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'])
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||