1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 04:20:43 +08:00

chore: 清理旧文件并重构项目基础结构

- 删除大量废弃的旧代码、依赖和配置文件
- 新增基础项目配置、工具函数、页面模板和请求库
- 重构目录结构,统一项目规范
- 添加commitlint、husky等代码规范工具
This commit is contained in:
小莫唐尼
2026-06-11 02:13:47 +08:00
parent de142c4729
commit 3945ee611d
587 changed files with 34429 additions and 120475 deletions
+40
View File
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { ref } from 'vue'
import FgTabbar from '@/tabbar/index.vue'
import { isPageTabbar } from './tabbar/store'
import { currRoute } from './utils'
const isCurrentPageTabbar = ref(true)
onShow(() => {
const { path } = currRoute()
// “蜡笔小开心”提到本地是 '/pages/index/index',线上是 '/' 导致线上 tabbar 不见了
// 所以这里需要判断一下,如果是 '/' 就当做首页,也要显示 tabbar
if (path === '/') {
isCurrentPageTabbar.value = true
}
else {
isCurrentPageTabbar.value = isPageTabbar(path)
}
})
const helloKuRoot = ref('Hello AppKuVue')
const exposeRef = ref('this is form app.Ku.vue')
defineExpose({
exposeRef,
})
</script>
<template>
<view>
<!-- 这个先隐藏了知道这样用就行 -->
<view class="hidden text-center">
{{ helloKuRoot }}这里可以配置全局的东西
</view>
<KuRootView />
<FgTabbar v-if="isCurrentPageTabbar" />
</view>
</template>
+52
View File
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { getCurrentInstance, onMounted, onUnmounted } from 'vue'
import { navigateToInterceptor } from '@/router/interceptor'
import { tabbarStore } from '@/tabbar/store'
import { permission } from '@/router/permission'
const { proxy } = (getCurrentInstance() || {}) as any
const router = proxy?.$router
router && permission.install(router)
onLaunch((options) => {
console.log('App.vue onLaunch', options)
})
onShow((options) => {
console.log('App.vue onShow', options)
// 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
// https://github.com/unibest-tech/unibest/issues/192
if (options?.path) {
navigateToInterceptor.invoke({ url: `/${options.path}`, query: options.query })
}
else {
navigateToInterceptor.invoke({ url: '/' })
}
})
onHide(() => {
console.log('App Hide')
})
// #ifdef H5
function syncTabbarWhenPageVisible() {
if (document.visibilityState === 'visible') {
tabbarStore.syncCurIdxByCurrentPageAsync()
}
}
onMounted(() => {
document.addEventListener('visibilitychange', syncTabbarWhenPageVisible)
window.addEventListener('pageshow', syncTabbarWhenPageVisible)
})
onUnmounted(() => {
document.removeEventListener('visibilitychange', syncTabbarWhenPageVisible)
window.removeEventListener('pageshow', syncTabbarWhenPageVisible)
})
// #endif
</script>
<style lang="scss">
</style>
+17
View File
@@ -0,0 +1,17 @@
import { API_DOMAINS, http } from '@/http/alova'
export interface IFoo {
id: number
name: string
}
export function foo() {
return http.Get<IFoo>('/foo', {
params: {
name: '菲鸽',
page: 1,
pageSize: 10,
},
meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址
})
}
+43
View File
@@ -0,0 +1,43 @@
import { http } from '@/http/http'
export interface IFoo {
id: number
name: string
}
export function foo() {
return http.Get<IFoo>('/foo', {
params: {
name: '菲鸽',
page: 1,
pageSize: 10,
},
})
}
export interface IFooItem {
id: string
name: string
}
/** GET 请求 */
export async function getFooAPI(name: string) {
return await http.get<IFooItem>('/foo', { name })
}
/** GET 请求;支持 传递 header 的范例 */
export function getFooAPI2(name: string) {
return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
}
/** POST 请求 */
export function postFooAPI(name: string) {
return http.post<IFooItem>('/foo', { name })
}
/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
export function postFooAPI2(name: string) {
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 })
}
/** POST 请求;支持 传递 header 的范例 */
export function postFooAPI3(name: string) {
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 }, { 'Content-Type-100': '100' })
}
+42
View File
@@ -0,0 +1,42 @@
import { http } from '@/http/alova'
import qs from 'qs'
import type { CategoryV1alpha1PublicApiQueryCategoriesRequest, CategoryV1alpha1PublicApiQueryCategoryByNameRequest, CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest, CategoryVo, CategoryVoList, ListedPostVoList } from '@halo-dev/api-client'
const EndpointBase = '/apis/api.content.halo.run/v1alpha1/categories'
/**
* 查询分类列表
* @param params 查询参数
* @returns 分类列表
*/
export function queryCategories(params: CategoryV1alpha1PublicApiQueryCategoriesRequest) {
// const paramString = qs.stringify(params, {
// allowDots: true,
// encodeValuesOnly: true,
// skipNulls: true,
// encode: false,
// arrayFormat: 'repeat',
// })
return http.Get<CategoryVoList>(EndpointBase, { params, meta: { isHalo: true } })
}
/**
* 根据分类名称查询分类
* @param params 查询参数
* @returns 分类
*/
export function queryCategoryByName(params: CategoryV1alpha1PublicApiQueryCategoryByNameRequest) {
return http.Get<CategoryVo>(`${EndpointBase}/${params.name}`, { params, meta: { isHalo: true } })
}
/**
* 根据分类名称查询分类下的文章列表
* @param params 查询参数
* @returns 文章列表
*/
export function queryPostsByCategoryName(params: CategoryV1alpha1PublicApiQueryPostsByCategoryNameRequest) {
return http.Get<ListedPostVoList>(`${EndpointBase}/${params.name}/posts`, {
params,
meta: { isHalo: true },
})
}
+72
View File
@@ -0,0 +1,72 @@
import { http } from '@/http/alova'
import type { Comment, CommentV1alpha1PublicApiCreateComment1Request, CommentV1alpha1PublicApiCreateReply1Request, CommentV1alpha1PublicApiGetCommentRequest, CommentV1alpha1PublicApiListCommentRepliesRequest, CommentV1alpha1PublicApiListComments1Request, CommentVoList, CommentWithReplyVoList, Reply, ReplyVoList } from '@halo-dev/api-client'
const EndpointBase = '/apis/api.halo.run/v1alpha1/comments'
/**
* 创建评论
* @param data 创建评论请求参数
* @returns 创建评论响应参数
*/
export function createComment(data: CommentV1alpha1PublicApiCreateComment1Request) {
return http.Post<Comment>(`${EndpointBase}`, data, {
meta: {
isHalo: true,
},
})
}
/**
* 创建回复
* @param data 创建回复请求参数
* @returns 创建回复响应参数
*/
export function createReply(data: CommentV1alpha1PublicApiCreateReply1Request) {
return http.Post<Reply>(`${EndpointBase}/${data.name}/reply`, data, {
meta: {
isHalo: true,
},
})
}
/**
* 获取评论列表
* @param params 获取评论列表请求参数
* @returns 获取评论列表响应参数
*/
export function listComments(params: CommentV1alpha1PublicApiListComments1Request) {
return http.Get<CommentWithReplyVoList>(`${EndpointBase}`, {
params,
meta: {
isHalo: true,
},
})
}
/**
* 获取评论回复列表
* @param params 获取评论回复列表请求参数
* @returns 获取评论回复列表响应参数
*/
export function listCommentReplies(params: CommentV1alpha1PublicApiListCommentRepliesRequest) {
return http.Get<ReplyVoList>(`${EndpointBase}/${params.name}/reply`, {
params,
meta: {
isHalo: true,
},
})
}
/**
* 获取评论详情
* @param params 获取评论详情请求参数
* @returns 获取评论详情响应参数
*/
export function getComment(params: CommentV1alpha1PublicApiGetCommentRequest) {
return http.Get<CommentVoList>(`${EndpointBase}/${params.name}`, {
params,
meta: {
isHalo: true,
},
})
}
+24
View File
@@ -0,0 +1,24 @@
import { http } from '@/http/alova'
import type { IndexV1alpha1PublicApiIndicesSearchRequest, SearchResult } from '@halo-dev/api-client'
/**
* 检查插件是否可用
* @param pluginId 插件ID
* @returns 插件是否可用
*/
export function checkPluginAvailable(pluginId: string) {
return http.Get<boolean>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${pluginId}/available`, {
meta: { isHalo: true },
})
}
/**
* 内容搜索(全局搜索)
* @param data 搜索参数
* @returns 搜索结果
*/
export function indicesSearch(data: IndexV1alpha1PublicApiIndicesSearchRequest) {
return http.Post<SearchResult>(`/apis/api.halo.run/v1alpha1/indices/-/search`, data, {
meta: { isHalo: true },
})
}
+43
View File
@@ -0,0 +1,43 @@
import { http } from '@/http/alova'
import { getNoLoginEmail, getOpenid } from '@/utils/auth'
import type { ListedPostVoList, NavigationPostVo, PostV1alpha1PublicApiQueryPostByNameRequest, PostV1alpha1PublicApiQueryPostsRequest, PostVo } from '@halo-dev/api-client'
const EndpointBase = '/apis/api.content.halo.run/v1alpha1/posts'
/**
* 查询文章列表
* @param params 查询参数
* @returns 文章列表
*/
export function queryPosts(params?: PostV1alpha1PublicApiQueryPostsRequest) {
return http.Get<ListedPostVoList>(EndpointBase, {
params,
meta: { isHalo: true },
})
}
/**
* 根据名称查询文章
* @param params 查询参数
* @returns 文章
*/
export function queryPostByName(params: PostV1alpha1PublicApiQueryPostByNameRequest) {
return http.Get<PostVo>(`${EndpointBase}/${params.name}`, {
meta: { isHalo: true },
headers: {
'Wechat-Session-Id': getOpenid(),
'nologin-email': getNoLoginEmail(),
},
})
}
/**
* 根据名称查询文章导航(上一篇下一篇数据)
* @param params 查询参数
* @returns 文章导航
*/
export function queryPostNavigationByName(params: PostV1alpha1PublicApiQueryPostByNameRequest) {
return http.Get<NavigationPostVo>(`${EndpointBase}/${params.name}/navigation`, {
meta: { isHalo: true },
})
}
+19
View File
@@ -0,0 +1,19 @@
import { http } from '@/http/alova'
export interface SiteStatsVo {
category: number
comment: number
post: number
upvote: number
visit: number
}
/**
* 获取站点统计信息
* @returns 站点统计信息
*/
export function getSiteStats() {
return http.Get<SiteStatsVo>(`/apis/api.halo.run/v1alpha1/stats/-`, {
meta: { isHalo: true },
})
}
+34
View File
@@ -0,0 +1,34 @@
import { http } from '@/http/alova'
import type { ListedPostVoList, TagV1alpha1PublicApiQueryPostsByTagNameRequest, TagV1alpha1PublicApiQueryTagByNameRequest, TagV1alpha1PublicApiQueryTagsRequest, TagVo, TagVoList } from '@halo-dev/api-client'
const EndpointBase = '/apis/api.content.halo.run/v1alpha1/tags'
/**
* 查询标签列表
* @param params 查询参数
* @returns 标签列表
*/
export function queryTags(params: TagV1alpha1PublicApiQueryTagsRequest) {
return http.Get<TagVoList>(EndpointBase, { params, meta: { isHalo: true } })
}
/**
* 根据标签名称查询标签
* @param params 查询参数
* @returns 标签
*/
export function queryTagByName(params: TagV1alpha1PublicApiQueryTagByNameRequest) {
return http.Get<TagVo>(`${EndpointBase}/${params.name}`, { params, meta: { isHalo: true } })
}
/**
* 根据标签名称查询标签下的文章列表
* @param params 查询参数
* @returns 文章列表
*/
export function queryPostsByTagName(params: TagV1alpha1PublicApiQueryPostsByTagNameRequest) {
return http.Get<ListedPostVoList>(`${EndpointBase}/${params.name}/posts`, {
params,
meta: { isHalo: true },
})
}
+52
View File
@@ -0,0 +1,52 @@
import { http } from '@/http/alova'
const EndpointBase = '/apis/api.halo.run/v1alpha1/trackers'
export interface CounterRequest {
group: string
hostname: string
language: string
name: string
plural: string
referrer: string
screen: string
}
export interface VoteRequest {
group: string
name: string
plural: string
}
/**
* 提交点赞
* @param data 点赞参数
* @returns 点赞结果
*/
export function upvote(data: VoteRequest) {
return http.Post<boolean>(`${EndpointBase}/upvote`, data, {
meta: { isHalo: true },
})
}
/**
* 取消点赞
* @param data 点赞参数
* @returns 点赞结果
*/
export function downvote(data: VoteRequest) {
return http.Post<boolean>(`${EndpointBase}/downvote`, data, {
meta: { isHalo: true },
})
}
/**
* 提交访问量(统计)
* @param data 访问量参数
* @returns 访问量结果
*/
export function trackersCounter(data: CounterRequest) {
return http.Post<boolean>(`${EndpointBase}/counter`, data, {
meta: { isHalo: true },
})
}
+24
View File
@@ -0,0 +1,24 @@
import { http } from '@/http/alova'
/**
* 获取图表统计数据
* @description - 标签、分类、文章发布趋势、评论活跃用户、获取热门文章top10
*/
export function getChartData() {
return http.Get<any>(`/apis/api.data.statistics.xhhao.com/v1alpha1/chart/data`, {
meta: {
isHalo: true,
},
})
}
/**
* 获取Github配置信息
*/
export function getGithubConfig() {
return http.Get<any>('/apis/api.data.statistics.xhhao.com/v1alpha1/github/config', {
meta: {
isHalo: true,
},
})
}
+11
View File
@@ -0,0 +1,11 @@
import { http } from '@/http/alova'
/**
* 获取豆瓣电影详情
* @param params 获取豆瓣电影详情请求参数
* @param params.url 豆瓣电影URL
* @returns 获取豆瓣电影详情响应参数
*/
export function queryDoubanDetailByUrl(params: { url: string }) {
return http.Get<any>(`/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/getDoubanDetail`, { params })
}
+19
View File
@@ -0,0 +1,19 @@
import { http } from '@/http/alova'
/**
* 获取链接组分组列表
* @param params 获取链接分分组列表请求参数
* @returns 获取链接分分组组列表响应参数
*/
export function queryLinkGroups(params: any) {
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/linkgroups`, { params })
}
/**
* 获取链接列表
* @param params 获取链接列表请求参数
* @returns 获取链接列表响应参数
*/
export function queryLinks(params: any) {
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/links`, { params })
}
+23
View File
@@ -0,0 +1,23 @@
import { http } from '@/http/alova'
const EndpointBase = '/apis/api.moment.halo.run/v1alpha1/moments'
/**
* 获取瞬间列表
* @param params 获取瞬间列表请求参数
* @returns 获取瞬间列表响应参数
*/
export function queryMoments(params: any) {
return http.Get<any>(`${EndpointBase}`, {
params,
})
}
/**
* 获取瞬间详情
* @param name 瞬间名称
* @returns 获取瞬间详情响应参数
*/
export function queryMomentByName(name: string) {
return http.Get<any>(`${EndpointBase}/${name}`, {})
}
+55
View File
@@ -0,0 +1,55 @@
/**
* muyin 插件相关api
*/
import { http } from '@/http/alova'
import { getNoLoginEmail, getOpenid } from '@/utils/auth'
/**
* 限制阅读检查
* @param restrictType 限制类型
* @param code 验证码
* @param keyId 键ID
* @returns 限制阅读检查响应参数
*/
export function requestRestrictReadCheck(restrictType: string, code: string, keyId: string) {
const params = {
code,
templateType: 'post',
restrictType,
keyId,
}
return http.Post(`/apis/tools.muyin.site/v1alpha1/restrict-read/check`, params, {
headers: {
'Authorization': 'getAppConfigs().pluginConfig.toolsPlugin?.Authorization',
'Wechat-Session-Id': getOpenid(),
'nologin-email': getNoLoginEmail(),
},
})
}
/**
* 创建验证码
* @returns 创建验证码响应参数
*/
export function createVerificationCode() {
return http.Get(`/apis/tools.muyin.site/v1alpha1/restrict-read/create`, {
headers: {
'Authorization': 'getAppConfigs().pluginConfig.toolsPlugin?.Authorization',
'Wechat-Session-Id': getOpenid(),
},
})
}
/**
* 提交链接
* @param form 提交链接请求参数
* @returns 提交链接响应参数
*/
export function submitLink(form: any) {
return http.Post(`/apis/linkssubmit.muyin.site/v1alpha1/submit`, form, {
headers: {
'Authorization': 'getAppConfigs().pluginConfig.linksSubmitPlugin?.Authorization',
'Wechat-Session-Id': getOpenid(),
},
})
}
+56
View File
@@ -0,0 +1,56 @@
import { http } from '@/http/alova'
/**
* 获取图库分组列表
* @param params 获取图库分组列表请求参数
* @returns 获取图库分组列表响应参数
*/
export function queryPhotoGroups(params: any) {
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/photogroups`, {
params,
meta: {
isHalo: true,
},
})
}
/**
* 获取图库标签列表 列出所有不重复的标签名称及对应图片数量,支持可选的 name 参数进行大小写不敏感模糊过滤
* @param params 获取图库标签列表请求参数
* @returns 获取图库标签列表响应参数
*/
export function queryPhotoTags(params: any) {
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/tags`, {
params,
meta: {
isHalo: true,
},
})
}
/**
* 获取图库列表 分页列出图片,支持 group、ungrouped、tag、keyword、labelSelector、fieldSelector、sort、page、size 查询参数
* @param params 获取图库列表请求参数
* @returns 获取图库列表响应参数
*/
export function queryPhotos(params: any) {
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/photos`, {
params,
meta: {
isHalo: true,
},
})
}
/**
* 根据 metadata.name 获取单张图片,不存在或已软删除时返回 404
* @param name 图片名称
* @returns 获取图片响应参数
*/
export function queryPhotoByName(name: string) {
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/photos/${name}`, {
meta: {
isHalo: true,
},
})
}
+39
View File
@@ -0,0 +1,39 @@
import { http } from '@/http/alova'
/**
* 获取 uni-halo 插件配置
* @returns uni-halo 插件配置响应参数
*/
export function queryUniHaloPluginConfigs() {
return http.Get(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getConfigs`, {
meta: {
isHalo: true,
},
})
}
/**
* 获取 uni-halo 插件二维码信息
* @param key 二维码key
* @returns uni-halo 插件二维码信息响应参数
*/
export function queryQRCodeInfo(key: string) {
return http.Get(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getQRCodeInfo/${key}`, {
meta: {
isHalo: true,
},
})
}
/**
* 获取 uni-halo 插件二维码图片
* @param metadataName 文章idmetadata.name 值
* @returns uni-halo 插件二维码图片响应参数
*/
export function queryQRCodeImg(metadataName: string) {
return http.Get(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getQRCodeImg/${metadataName}`, {
meta: {
isHalo: true,
},
})
}
+45
View File
@@ -0,0 +1,45 @@
import { http } from '@/http/alova'
const EndpointBase = '/apis/api.vote.kunkunyu.com/v1alpha1/votes'
/**
* 获取投票列表
* @param params 获取投票列表请求参数
* @returns 获取投票列表响应参数
*/
export function queryVotes(params: any) {
return http.Get<any>(EndpointBase, { params })
}
/**
* 获取投票详情
* @param name 投票名称
* @returns 获取投票详情响应参数
*/
export function queryVoteDetail(name: string) {
return http.Get<any>(`${EndpointBase}/${name}/detail`, {})
}
/**
* 获取投票用户列表
* @param name 投票名称
* @returns 获取投票用户列表响应参数
*/
export function queryVoteUsers(name: string) {
return http.Get<any>(`${EndpointBase}/${name}/user-list`, {})
}
/**
* 提交投票
* @param name 投票名称
* @param data 提交投票请求参数
* @param canAnonymously 是否匿名匿名提交
* @returns 提交投票响应参数
*/
export function submitVote(name: string, data: any, canAnonymously = true) {
return http.Post<any>(`${EndpointBase}/${name}/submit`, data, {
meta: {
token: canAnonymously ? undefined : 'getToken()',
},
})
}
+85
View File
@@ -0,0 +1,85 @@
import type { IAuthLoginRes, ICaptcha, IDoubleTokenRes, IUpdateInfo, IUpdatePassword, IUserInfoRes } from './types/login'
import { http } from '@/http/http'
/**
* 登录表单
*/
export interface ILoginForm {
username: string
password: string
}
/**
* 获取验证码
* @returns ICaptcha 验证码
*/
export function getCode() {
return http.get<ICaptcha>('/user/getCode')
}
/**
* 用户登录
* @param loginForm 登录表单
*/
export function login(loginForm: ILoginForm) {
return http.post<IAuthLoginRes>('/auth/login', loginForm)
}
/**
* 刷新token
* @param refreshToken 刷新token
*/
export function refreshToken(refreshToken: string) {
return http.post<IDoubleTokenRes>('/auth/refreshToken', { refreshToken })
}
/**
* 获取用户信息
*/
export function getUserInfo() {
return http.get<IUserInfoRes>('/user/info')
}
/**
* 退出登录
*/
export function logout() {
return http.get<void>('/auth/logout')
}
/**
* 修改用户信息
*/
export function updateInfo(data: IUpdateInfo) {
return http.post('/user/updateInfo', data)
}
/**
* 修改用户密码
*/
export function updateUserPassword(data: IUpdatePassword) {
return http.post('/user/updatePassword', data)
}
/**
* 获取微信登录凭证
* @returns Promise 包含微信登录凭证(code)
*/
export function getWxCode() {
return new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => resolve(res),
fail: err => reject(new Error(err)),
})
})
}
/**
* 微信登录
* @param params 微信登录参数,包含code
* @returns Promise 包含登录结果
*/
export function wxLogin(data: { code: string }) {
return http.post<IAuthLoginRes>('/auth/wxLogin', data)
}
+102
View File
@@ -0,0 +1,102 @@
// 认证模式类型
export type AuthMode = 'single' | 'double'
// 单Token响应类型
export interface ISingleTokenRes {
token: string
expiresIn: number // 有效期(秒)
}
// 双Token响应类型
export interface IDoubleTokenRes {
accessToken: string
refreshToken: string
accessExpiresIn: number // 访问令牌有效期(秒)
refreshExpiresIn: number // 刷新令牌有效期(秒)
}
/**
* 登录返回的信息,其实就是 token 信息
*/
export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
/**
* 用户信息
*/
export type UserRole = string
export interface IUserInfoRes {
userId: number
username: string
nickname: string
avatar?: string
/** 同时支持单角色和多角色,你自行选择一种就行 */
role?: UserRole
roles?: UserRole[]
[key: string]: any // 允许其他扩展字段
}
// 认证存储数据结构
export interface AuthStorage {
mode: AuthMode
tokens: ISingleTokenRes | IDoubleTokenRes
userInfo?: IUserInfoRes
loginTime: number // 登录时间戳
}
/**
* 获取验证码
*/
export interface ICaptcha {
captchaEnabled: boolean
uuid: string
image: string
}
/**
* 上传成功的信息
*/
export interface IUploadSuccessInfo {
fileId: number
originalName: string
fileName: string
storagePath: string
fileHash: string
fileType: string
fileBusinessType: string
fileSize: number
}
/**
* 更新用户信息
*/
export interface IUpdateInfo {
id: number
name: string
sex: string
}
/**
* 更新用户信息
*/
export interface IUpdatePassword {
id: number
oldPassword: string
newPassword: string
confirmPassword: string
}
/**
* 判断是否为单Token响应
* @param tokenRes 登录响应数据
* @returns 是否为单Token响应
*/
export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
return 'token' in tokenRes && !('refreshToken' in tokenRes)
}
/**
* 判断是否为双Token响应
* @param tokenRes 登录响应数据
* @returns 是否为双Token响应
*/
export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
}
View File
@@ -0,0 +1,444 @@
/*
* uCharts®
* 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址:
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 通用配置项
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
const color = [
'#1890FF',
'#91CB74',
'#FAC858',
'#EE6666',
'#73C0DE',
'#3CA272',
'#FC8452',
'#9A60B4',
'#ea7ccc',
]
const cfe = {
// demotype为自定义图表类型
type: [
'pie',
'ring',
'rose',
'funnel',
'line',
'column',
'area',
'radar',
'gauge',
'candle',
'demotype',
],
// 增加自定义图表类型,如果需要categories,请在这里加入您的图表类型例如最后的"demotype"
categories: ['line', 'column', 'area', 'radar', 'gauge', 'candle', 'demotype'],
// instance为实例变量承载属性,option为eopts承载属性,不要删除
instance: {},
option: {},
// 下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
formatter: {
tooltipDemo1(res) {
let result = ''
for (const i in res) {
if (i == 0) {
result += `${res[i].axisValueLabel}年销售额`
}
let value = '--'
if (res[i].data !== null) {
value = res[i].data
}
// #ifdef H5
result += `\n${res[i].seriesName}${value} 万元`
// #endif
// #ifdef APP-PLUS
result += `<br/>${res[i].marker}${res[i].seriesName}${value} 万元`
// #endif
}
return result
},
legendFormat(name) {
return `自定义图例+${name}`
},
yAxisFormatDemo(value, index) {
return `${value}`
},
seriesFormatDemo(res) {
return `${res.name}${res.value}`
},
},
// 这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在eopts参数,会将demotype与eopts中option合并后渲染图表。
demotype: {
color,
// 在这里填写echarts的option即可
},
// 下面是自定义配置,请添加项目所需的通用配置
column: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'axis',
},
grid: {
top: 30,
bottom: 50,
right: 15,
left: 40,
},
legend: {
bottom: 'left',
},
toolbox: {
show: false,
},
xAxis: {
type: 'category',
axisLabel: {
color: '#666666',
},
axisLine: {
lineStyle: {
color: '#CCCCCC',
},
},
boundaryGap: true,
data: [],
},
yAxis: {
type: 'value',
axisTick: {
show: false,
},
axisLabel: {
color: '#666666',
},
axisLine: {
lineStyle: {
color: '#CCCCCC',
},
},
},
seriesTemplate: {
name: '',
type: 'bar',
data: [],
barwidth: 20,
label: {
show: true,
color: '#666666',
position: 'top',
},
},
},
line: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'axis',
},
grid: {
top: 30,
bottom: 50,
right: 15,
left: 40,
},
legend: {
bottom: 'left',
},
toolbox: {
show: false,
},
xAxis: {
type: 'category',
axisLabel: {
color: '#666666',
},
axisLine: {
lineStyle: {
color: '#CCCCCC',
},
},
boundaryGap: true,
data: [],
},
yAxis: {
type: 'value',
axisTick: {
show: false,
},
axisLabel: {
color: '#666666',
},
axisLine: {
lineStyle: {
color: '#CCCCCC',
},
},
},
seriesTemplate: {
name: '',
type: 'line',
data: [],
barwidth: 20,
label: {
show: true,
color: '#666666',
position: 'top',
},
},
},
area: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'axis',
},
grid: {
top: 30,
bottom: 50,
right: 15,
left: 40,
},
legend: {
bottom: 'left',
},
toolbox: {
show: false,
},
xAxis: {
type: 'category',
axisLabel: {
color: '#666666',
},
axisLine: {
lineStyle: {
color: '#CCCCCC',
},
},
boundaryGap: true,
data: [],
},
yAxis: {
type: 'value',
axisTick: {
show: false,
},
axisLabel: {
color: '#666666',
},
axisLine: {
lineStyle: {
color: '#CCCCCC',
},
},
},
seriesTemplate: {
name: '',
type: 'line',
data: [],
areaStyle: {},
label: {
show: true,
color: '#666666',
position: 'top',
},
},
},
pie: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'item',
},
grid: {
top: 40,
bottom: 30,
right: 15,
left: 15,
},
legend: {
bottom: 'left',
},
seriesTemplate: {
name: '',
type: 'pie',
data: [],
radius: '50%',
label: {
show: true,
color: '#666666',
position: 'top',
},
},
},
ring: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'item',
},
grid: {
top: 40,
bottom: 30,
right: 15,
left: 15,
},
legend: {
bottom: 'left',
},
seriesTemplate: {
name: '',
type: 'pie',
data: [],
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: true,
color: '#666666',
position: 'top',
},
labelLine: {
show: true,
},
},
},
rose: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'item',
},
legend: {
top: 'bottom',
},
seriesTemplate: {
name: '',
type: 'pie',
data: [],
radius: '55%',
center: ['50%', '50%'],
roseType: 'area',
},
},
funnel: {
color,
title: {
text: '',
},
tooltip: {
trigger: 'item',
formatter: '{b} : {c}%',
},
legend: {
top: 'bottom',
},
seriesTemplate: {
name: '',
type: 'funnel',
left: '10%',
top: 60,
bottom: 60,
width: '80%',
min: 0,
max: 100,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 2,
label: {
show: true,
position: 'inside',
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: 'solid',
},
},
itemStyle: {
bordercolor: '#fff',
borderwidth: 1,
},
emphasis: {
label: {
fontSize: 20,
},
},
data: [],
},
},
gauge: {
color,
tooltip: {
formatter: '{a} <br/>{b} : {c}%',
},
seriesTemplate: {
name: '业务指标',
type: 'gauge',
detail: { formatter: '{value}%' },
data: [{ value: 50, name: '完成率' }],
},
},
candle: {
xAxis: {
data: [],
},
yAxis: {},
color,
title: {
text: '',
},
dataZoom: [
{
type: 'inside',
xAxisIndex: [0, 1],
start: 10,
end: 100,
},
{
show: true,
xAxisIndex: [0, 1],
type: 'slider',
bottom: 10,
start: 10,
end: 100,
},
],
seriesTemplate: {
name: '',
type: 'k',
data: [],
},
},
}
export default cfe
@@ -0,0 +1,675 @@
/*
* uCharts®
* 高性能跨平台图表库,支持H5、APP、小程序(微信/支付宝/百度/头条/QQ/360)、Vue、Taro等支持canvas的框架平台
* Copyright (c) 2021 QIUN®秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释,感谢支持开源!
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址:
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
// 主题颜色配置:如每个图表类型需要不同主题,请在对应图表类型上更改color属性
const color = [
'#1890FF',
'#91CB74',
'#FAC858',
'#EE6666',
'#73C0DE',
'#3CA272',
'#FC8452',
'#9A60B4',
'#ea7ccc',
]
// 事件转换函数,主要用作格式化x轴为时间轴,根据需求自行修改
const formatDateTime = (timeStamp, returnType) => {
const date = new Date()
date.setTime(timeStamp * 1000)
const y = date.getFullYear()
let m = date.getMonth() + 1
m = m < 10 ? `0${m}` : m
let d = date.getDate()
d = d < 10 ? `0${d}` : d
let h = date.getHours()
h = h < 10 ? `0${h}` : h
let minute = date.getMinutes()
let second = date.getSeconds()
minute = minute < 10 ? `0${minute}` : minute
second = second < 10 ? `0${second}` : second
if (returnType == 'full') {
return `${y}-${m}-${d} ${h}:${minute}:${second}`
}
if (returnType == 'y-m-d') {
return `${y}-${m}-${d}`
}
if (returnType == 'h:m') {
return `${h}:${minute}`
}
if (returnType == 'h:m:s') {
return `${h}:${minute}:${second}`
}
return [y, m, d, h, minute, second]
}
const cfu = {
// demotype为自定义图表类型,一般不需要自定义图表类型,只需要改根节点上对应的类型即可
type: [
'pie',
'ring',
'rose',
'word',
'funnel',
'map',
'arcbar',
'line',
'column',
'mount',
'bar',
'area',
'radar',
'gauge',
'candle',
'mix',
'tline',
'tarea',
'scatter',
'bubble',
'demotype',
],
range: [
'饼状图',
'圆环图',
'玫瑰图',
'词云图',
'漏斗图',
'地图',
'圆弧进度条',
'折线图',
'柱状图',
'山峰图',
'条状图',
'区域图',
'雷达图',
'仪表盘',
'K线图',
'混合图',
'时间轴折线',
'时间轴区域',
'散点图',
'气泡图',
'自定义类型',
],
// 增加自定义图表类型,如果需要categories,请在这里加入您的图表类型,例如最后的"demotype"
// 自定义类型时需要注意"tline","tarea","scatter","bubble"等时间轴(矢量x轴)类图表,没有categories,不需要加入categories
categories: [
'line',
'column',
'mount',
'bar',
'area',
'radar',
'gauge',
'candle',
'mix',
'demotype',
],
// instance为实例变量承载属性,不要删除
instance: {},
// option为opts及eopts承载属性,不要删除
option: {},
// 下面是自定义format配置,因除H5端外的其他端无法通过props传递函数,只能通过此属性对应下标的方式来替换
formatter: {
yAxisDemo1(val, index, opts) {
return `${val}`
},
yAxisDemo2(val, index, opts) {
return val.toFixed(2)
},
xAxisDemo1(val, index, opts) {
return `${val}`
},
xAxisDemo2(val, index, opts) {
return formatDateTime(val, 'h:m')
},
seriesDemo1(val, index, series, opts) {
return `${val}`
},
tooltipDemo1(item, category, index, opts) {
if (index == 0) {
return `随便用${item.data}`
}
return `其他我没改${item.data}`
},
pieDemo(val, index, series, opts) {
if (index !== undefined) {
return `${series[index].name}${series[index].data}`
}
},
},
// 这里演示了自定义您的图表类型的option,可以随意命名,之后在组件上 type="demotype" 后,组件会调用这个花括号里的option,如果组件上还存在opts参数,会将demotype与opts中option合并后渲染图表。
demotype: {
// 我这里把曲线图当做了自定义图表类型,您可以根据需要随意指定类型或配置
type: 'line',
color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: 'dash',
dashLength: 2,
},
legend: {},
extra: {
line: {
type: 'curve',
width: 2,
},
},
},
// 下面是自定义配置,请添加项目所需的通用配置
pie: {
type: 'pie',
color,
padding: [5, 5, 5, 5],
extra: {
pie: {
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: '#FFFFFF',
},
},
},
ring: {
type: 'ring',
color,
padding: [5, 5, 5, 5],
rotate: false,
dataLabel: true,
legend: {
show: true,
position: 'right',
lineHeight: 25,
},
title: {
name: '收益率',
fontSize: 15,
color: '#666666',
},
subtitle: {
name: '70%',
fontSize: 25,
color: '#7cb5ec',
},
extra: {
ring: {
ringWidth: 30,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: true,
borderWidth: 3,
borderColor: '#FFFFFF',
},
},
},
rose: {
type: 'rose',
color,
padding: [5, 5, 5, 5],
legend: {
show: true,
position: 'left',
lineHeight: 25,
},
extra: {
rose: {
type: 'area',
minRadius: 50,
activeOpacity: 0.5,
activeRadius: 10,
offsetAngle: 0,
labelWidth: 15,
border: false,
borderWidth: 2,
borderColor: '#FFFFFF',
},
},
},
word: {
type: 'word',
color,
extra: {
word: {
type: 'normal',
autoColors: false,
},
},
},
funnel: {
type: 'funnel',
color,
padding: [15, 15, 0, 15],
extra: {
funnel: {
activeOpacity: 0.3,
activeWidth: 10,
border: true,
borderWidth: 2,
borderColor: '#FFFFFF',
fillOpacity: 1,
labelAlign: 'right',
},
},
},
map: {
type: 'map',
color,
padding: [0, 0, 0, 0],
dataLabel: true,
extra: {
map: {
border: true,
borderWidth: 1,
borderColor: '#666666',
fillOpacity: 0.6,
activeBorderColor: '#F04864',
activeFillColor: '#FACC14',
activeFillOpacity: 1,
},
},
},
arcbar: {
type: 'arcbar',
color,
title: {
name: '百分比',
fontSize: 25,
color: '#00FF00',
},
subtitle: {
name: '默认标题',
fontSize: 15,
color: '#666666',
},
extra: {
arcbar: {
type: 'default',
width: 12,
backgroundColor: '#E9E9E9',
startAngle: 0.75,
endAngle: 0.25,
gap: 2,
},
},
},
line: {
type: 'line',
color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: 'dash',
dashLength: 2,
},
legend: {},
extra: {
line: {
type: 'straight',
width: 2,
activeType: 'hollow',
},
},
},
tline: {
type: 'line',
color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: false,
boundaryGap: 'justify',
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [
{
min: 0,
max: 80,
},
],
},
legend: {},
extra: {
line: {
type: 'curve',
width: 2,
activeType: 'hollow',
},
},
},
tarea: {
type: 'area',
color,
padding: [15, 10, 0, 15],
xAxis: {
disableGrid: true,
boundaryGap: 'justify',
},
yAxis: {
gridType: 'dash',
dashLength: 2,
data: [
{
min: 0,
max: 80,
},
],
},
legend: {},
extra: {
area: {
type: 'curve',
opacity: 0.2,
addLine: true,
width: 2,
gradient: true,
activeType: 'hollow',
},
},
},
column: {
type: 'column',
color,
padding: [15, 15, 0, 5],
xAxis: {
disableGrid: true,
},
yAxis: {
data: [{ min: 0 }],
},
legend: {},
extra: {
column: {
type: 'group',
width: 30,
activeBgColor: '#000000',
activeBgOpacity: 0.08,
},
},
},
mount: {
type: 'mount',
color,
padding: [15, 15, 0, 5],
xAxis: {
disableGrid: true,
},
yAxis: {
data: [{ min: 0 }],
},
legend: {},
extra: {
mount: {
type: 'mount',
widthRatio: 1.5,
},
},
},
bar: {
type: 'bar',
color,
padding: [15, 30, 0, 5],
xAxis: {
boundaryGap: 'justify',
disableGrid: false,
min: 0,
axisLine: false,
},
yAxis: {},
legend: {},
extra: {
bar: {
type: 'group',
width: 30,
meterBorde: 1,
meterFillColor: '#FFFFFF',
activeBgColor: '#000000',
activeBgOpacity: 0.08,
},
},
},
area: {
type: 'area',
color,
padding: [15, 15, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
gridType: 'dash',
dashLength: 2,
},
legend: {},
extra: {
area: {
type: 'straight',
opacity: 0.2,
addLine: true,
width: 2,
gradient: false,
activeType: 'hollow',
},
},
},
radar: {
type: 'radar',
color,
padding: [5, 5, 5, 5],
dataLabel: false,
legend: {
show: true,
position: 'right',
lineHeight: 25,
},
extra: {
radar: {
gridType: 'radar',
gridColor: '#CCCCCC',
gridCount: 3,
opacity: 0.2,
max: 200,
labelShow: true,
},
},
},
gauge: {
type: 'gauge',
color,
title: {
name: '66Km/H',
fontSize: 25,
color: '#2fc25b',
offsetY: 50,
},
subtitle: {
name: '实时速度',
fontSize: 15,
color: '#1890ff',
offsetY: -50,
},
extra: {
gauge: {
type: 'default',
width: 30,
labelColor: '#666666',
startAngle: 0.75,
endAngle: 0.25,
startNumber: 0,
endNumber: 100,
labelFormat: '',
splitLine: {
fixRadius: 0,
splitNumber: 10,
width: 30,
color: '#FFFFFF',
childNumber: 5,
childWidth: 12,
},
pointer: {
width: 24,
color: 'auto',
},
},
},
},
candle: {
type: 'candle',
color,
padding: [15, 15, 0, 15],
enableScroll: true,
enableMarkLine: true,
dataLabel: false,
xAxis: {
labelCount: 4,
itemCount: 40,
disableGrid: true,
gridColor: '#CCCCCC',
gridType: 'solid',
dashLength: 4,
scrollShow: true,
scrollAlign: 'left',
scrollColor: '#A6A6A6',
scrollBackgroundColor: '#EFEBEF',
},
yAxis: {},
legend: {},
extra: {
candle: {
color: {
upLine: '#f04864',
upFill: '#f04864',
downLine: '#2fc25b',
downFill: '#2fc25b',
},
average: {
show: true,
name: ['MA5', 'MA10', 'MA30'],
day: [5, 10, 20],
color: ['#1890ff', '#2fc25b', '#facc14'],
},
},
markLine: {
type: 'dash',
dashLength: 5,
data: [
{
value: 2150,
lineColor: '#f04864',
showLabel: true,
},
{
value: 2350,
lineColor: '#f04864',
showLabel: true,
},
],
},
},
},
mix: {
type: 'mix',
color,
padding: [15, 15, 0, 15],
xAxis: {
disableGrid: true,
},
yAxis: {
disabled: false,
disableGrid: false,
splitNumber: 5,
gridType: 'dash',
dashLength: 4,
gridColor: '#CCCCCC',
padding: 10,
showTitle: true,
data: [],
},
legend: {},
extra: {
mix: {
column: {
width: 20,
},
},
},
},
scatter: {
type: 'scatter',
color,
padding: [15, 15, 0, 15],
dataLabel: false,
xAxis: {
disableGrid: false,
gridType: 'dash',
splitNumber: 5,
boundaryGap: 'justify',
min: 0,
},
yAxis: {
disableGrid: false,
gridType: 'dash',
},
legend: {},
extra: {
scatter: {},
},
},
bubble: {
type: 'bubble',
color,
padding: [15, 15, 0, 15],
xAxis: {
disableGrid: false,
gridType: 'dash',
splitNumber: 5,
boundaryGap: 'justify',
min: 0,
max: 250,
},
yAxis: {
disableGrid: false,
gridType: 'dash',
data: [
{
min: 0,
max: 150,
},
],
},
legend: {},
extra: {
bubble: {
border: 2,
opacity: 0.5,
},
},
},
}
export default cfu
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
import uCharts from '@qiun/ucharts'
export default uCharts
+48
View File
@@ -0,0 +1,48 @@
<template>
<view class="chartsview">
<view class="charts-error"></view>
<view class="charts-font">{{ errorMessage == null ? '请点击重试' : errorMessage }}</view>
</view>
</template>
<script>
export default {
name: 'qiun-error',
props: {
errorMessage: {
type: String,
default: null,
},
},
data() {
return {}
},
}
</script>
<style>
.chartsview {
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.charts-font {
margin-top: 10px;
font-size: 14px;
color: #ccc;
}
.charts-error {
width: 128px;
height: 128px;
background: url('data:image/png;base64,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');
background-repeat: no-repeat;
background-size: 100%;
background-position: center;
}
</style>
+153
View File
@@ -0,0 +1,153 @@
<template>
<view class="container loading1">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading1',
data() {
return {}
},
}
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading1 {
transform: rotate(45deg);
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #e66;
}
.loading1 .shape1 {
animation: animation1shape1 0.5s ease 0s infinite alternate;
}
@keyframes animation1shape1 {
from {
transform: translate(0, 0);
}
to {
transform: translate(16px, 16px);
}
}
@keyframes animation1shape1 {
from {
transform: translate(0, 0);
}
to {
transform: translate(16px, 16px);
}
}
.loading1 .shape2 {
animation: animation1shape2 0.5s ease 0s infinite alternate;
}
@keyframes animation1shape2 {
from {
transform: translate(0, 0);
}
to {
transform: translate(-16px, 16px);
}
}
@keyframes animation1shape2 {
from {
transform: translate(0, 0);
}
to {
transform: translate(-16px, 16px);
}
}
.loading1 .shape3 {
animation: animation1shape3 0.5s ease 0s infinite alternate;
}
@keyframes animation1shape3 {
from {
transform: translate(0, 0);
}
to {
transform: translate(16px, -16px);
}
}
@keyframes animation1shape3 {
from {
transform: translate(0, 0);
}
to {
transform: translate(16px, -16px);
}
}
.loading1 .shape4 {
animation: animation1shape4 0.5s ease 0s infinite alternate;
}
@keyframes animation1shape4 {
from {
transform: translate(0, 0);
}
to {
transform: translate(-16px, -16px);
}
}
@keyframes animation1shape4 {
from {
transform: translate(0, 0);
}
to {
transform: translate(-16px, -16px);
}
}
</style>
+183
View File
@@ -0,0 +1,183 @@
<template>
<view class="container loading2">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading2',
data() {
return {}
},
}
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading2 {
transform: rotate(10deg);
transform: rotate(10deg);
}
.container.loading2 .shape {
border-radius: 5px;
}
.container.loading2 {
animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #e66;
}
.loading2 .shape1 {
animation: animation2shape1 0.5s ease 0s infinite alternate;
animation: animation2shape1 0.5s ease 0s infinite alternate;
}
@keyframes animation2shape1 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
@keyframes animation2shape1 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(20px, 20px);
transform: translate(20px, 20px);
}
}
.loading2 .shape2 {
animation: animation2shape2 0.5s ease 0s infinite alternate;
animation: animation2shape2 0.5s ease 0s infinite alternate;
}
@keyframes animation2shape2 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
@keyframes animation2shape2 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-20px, 20px);
transform: translate(-20px, 20px);
}
}
.loading2 .shape3 {
animation: animation2shape3 0.5s ease 0s infinite alternate;
animation: animation2shape3 0.5s ease 0s infinite alternate;
}
@keyframes animation2shape3 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
@keyframes animation2shape3 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(20px, -20px);
transform: translate(20px, -20px);
}
}
.loading2 .shape4 {
animation: animation2shape4 0.5s ease 0s infinite alternate;
animation: animation2shape4 0.5s ease 0s infinite alternate;
}
@keyframes animation2shape4 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
@keyframes animation2shape4 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-20px, -20px);
transform: translate(-20px, -20px);
}
}
</style>
+190
View File
@@ -0,0 +1,190 @@
<template>
<view class="container loading3">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading3',
data() {
return {}
},
}
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading3 {
animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading3 .shape1 {
border-top-left-radius: 10px;
}
.container.loading3 .shape2 {
border-top-right-radius: 10px;
}
.container.loading3 .shape3 {
border-bottom-left-radius: 10px;
}
.container.loading3 .shape4 {
border-bottom-right-radius: 10px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #e66;
}
.loading3 .shape1 {
animation: animation3shape1 0.5s ease 0s infinite alternate;
animation: animation3shape1 0.5s ease 0s infinite alternate;
}
@keyframes animation3shape1 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
@keyframes animation3shape1 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(5px, 5px);
transform: translate(5px, 5px);
}
}
.loading3 .shape2 {
animation: animation3shape2 0.5s ease 0s infinite alternate;
animation: animation3shape2 0.5s ease 0s infinite alternate;
}
@keyframes animation3shape2 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
@keyframes animation3shape2 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-5px, 5px);
transform: translate(-5px, 5px);
}
}
.loading3 .shape3 {
animation: animation3shape3 0.5s ease 0s infinite alternate;
animation: animation3shape3 0.5s ease 0s infinite alternate;
}
@keyframes animation3shape3 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
@keyframes animation3shape3 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(5px, -5px);
transform: translate(5px, -5px);
}
}
.loading3 .shape4 {
animation: animation3shape4 0.5s ease 0s infinite alternate;
animation: animation3shape4 0.5s ease 0s infinite alternate;
}
@keyframes animation3shape4 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
@keyframes animation3shape4 {
from {
transform: translate(0, 0);
transform: translate(0, 0);
}
to {
transform: translate(-5px, -5px);
transform: translate(-5px, -5px);
}
}
</style>
+250
View File
@@ -0,0 +1,250 @@
<template>
<view class="container loading5">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading5',
data() {
return {}
},
}
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading5 .shape {
width: 15px;
height: 15px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #e66;
}
.loading5 .shape1 {
animation: animation5shape1 2s ease 0s infinite reverse;
}
@keyframes animation5shape1 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
@keyframes animation5shape1 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, 15px);
transform: translate(0, 15px);
}
50% {
transform: translate(15px, 15px);
transform: translate(15px, 15px);
}
75% {
transform: translate(15px, 0);
transform: translate(15px, 0);
}
}
.loading5 .shape2 {
animation: animation5shape2 2s ease 0s infinite reverse;
}
@keyframes animation5shape2 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
@keyframes animation5shape2 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
50% {
transform: translate(-15px, 15px);
transform: translate(-15px, 15px);
}
75% {
transform: translate(0, 15px);
transform: translate(0, 15px);
}
}
.loading5 .shape3 {
animation: animation5shape3 2s ease 0s infinite reverse;
}
@keyframes animation5shape3 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
@keyframes animation5shape3 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(15px, 0);
transform: translate(15px, 0);
}
50% {
transform: translate(15px, -15px);
transform: translate(15px, -15px);
}
75% {
transform: translate(0, -15px);
transform: translate(0, -15px);
}
}
.loading5 .shape4 {
animation: animation5shape4 2s ease 0s infinite reverse;
}
@keyframes animation5shape4 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
@keyframes animation5shape4 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, -15px);
transform: translate(0, -15px);
}
50% {
transform: translate(-15px, -15px);
transform: translate(-15px, -15px);
}
75% {
transform: translate(-15px, 0);
transform: translate(-15px, 0);
}
}
</style>
+259
View File
@@ -0,0 +1,259 @@
<template>
<view class="container loading6">
<view class="shape shape1"></view>
<view class="shape shape2"></view>
<view class="shape shape3"></view>
<view class="shape shape4"></view>
</view>
</template>
<script>
export default {
name: 'loading6',
data() {
return {}
},
}
</script>
<style scoped="true">
.container {
position: relative;
width: 30px;
height: 30px;
}
.container.loading6 {
animation: rotation 1s infinite;
animation: rotation 1s infinite;
}
.container.loading6 .shape {
width: 12px;
height: 12px;
border-radius: 2px;
}
.container .shape {
position: absolute;
width: 10px;
height: 10px;
border-radius: 1px;
}
.container .shape.shape1 {
left: 0;
background-color: #1890ff;
}
.container .shape.shape2 {
right: 0;
background-color: #91cb74;
}
.container .shape.shape3 {
bottom: 0;
background-color: #fac858;
}
.container .shape.shape4 {
right: 0;
bottom: 0;
background-color: #e66;
}
.loading6 .shape1 {
animation: animation6shape1 2s linear 0s infinite normal;
animation: animation6shape1 2s linear 0s infinite normal;
}
@keyframes animation6shape1 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
@keyframes animation6shape1 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, 18px);
transform: translate(0, 18px);
}
50% {
transform: translate(18px, 18px);
transform: translate(18px, 18px);
}
75% {
transform: translate(18px, 0);
transform: translate(18px, 0);
}
}
.loading6 .shape2 {
animation: animation6shape2 2s linear 0s infinite normal;
animation: animation6shape2 2s linear 0s infinite normal;
}
@keyframes animation6shape2 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
@keyframes animation6shape2 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
50% {
transform: translate(-18px, 18px);
transform: translate(-18px, 18px);
}
75% {
transform: translate(0, 18px);
transform: translate(0, 18px);
}
}
.loading6 .shape3 {
animation: animation6shape3 2s linear 0s infinite normal;
animation: animation6shape3 2s linear 0s infinite normal;
}
@keyframes animation6shape3 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
@keyframes animation6shape3 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(18px, 0);
transform: translate(18px, 0);
}
50% {
transform: translate(18px, -18px);
transform: translate(18px, -18px);
}
75% {
transform: translate(0, -18px);
transform: translate(0, -18px);
}
}
.loading6 .shape4 {
animation: animation6shape4 2s linear 0s infinite normal;
animation: animation6shape4 2s linear 0s infinite normal;
}
@keyframes animation6shape4 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
@keyframes animation6shape4 {
0% {
transform: translate(0, 0);
transform: translate(0, 0);
}
25% {
transform: translate(0, -18px);
transform: translate(0, -18px);
}
50% {
transform: translate(-18px, -18px);
transform: translate(-18px, -18px);
}
75% {
transform: translate(-18px, 0);
transform: translate(-18px, 0);
}
}
</style>
@@ -0,0 +1,33 @@
<template>
<view>
<Loading1 v-if="loadingType == 1" />
<Loading2 v-if="loadingType == 2" />
<Loading3 v-if="loadingType == 3" />
<Loading4 v-if="loadingType == 4" />
<Loading5 v-if="loadingType == 5" />
</view>
</template>
<script>
import Loading1 from './loading1.vue'
import Loading2 from './loading2.vue'
import Loading3 from './loading3.vue'
import Loading4 from './loading4.vue'
import Loading5 from './loading5.vue'
export default {
components: { Loading1, Loading2, Loading3, Loading4, Loading5 },
name: 'qiun-loading',
props: {
loadingType: {
type: Number,
default: 2,
},
},
data() {
return {}
},
}
</script>
<style></style>
@@ -0,0 +1,47 @@
<template>
<view class="qiun-title-bar">
<view class="qiun-title-dot"></view>
<view class="qiun-title-text">{{ title }}</view>
</view>
</template>
<script>
export default {
name: 'title-bar',
props: {
title: {
type: String,
default: '',
},
},
mounted() {},
methods: {},
}
</script>
<style>
.qiun-title-bar {
display: flex;
flex-direction: row !important;
flex-wrap: nowrap;
align-items: center;
height: 40px;
}
.qiun-title-dot {
width: 5px;
height: 16px;
margin-left: 8px;
background-color: #409eff;
border-radius: 10px;
}
.qiun-title-text {
height: 22px;
margin-left: 8px;
font-size: 17px;
font-weight: bold;
line-height: 22px;
color: #666;
}
</style>
+41
View File
@@ -0,0 +1,41 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** 微信小程序开发版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
readonly VITE_SERVER_BASEURL__WEIXIN_DEVELOP?: string
/** 微信小程序体验版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
readonly VITE_SERVER_BASEURL__WEIXIN_TRIAL?: string
/** 微信小程序正式版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
readonly VITE_SERVER_BASEURL__WEIXIN_RELEASE?: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
/** H5是否需要代理,需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string
/** 后端是否有统一前缀 /api */
readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
/** 认证模式,'single' | 'double' ==> 单token | 双token */
readonly VITE_AUTH_MODE: 'single' | 'double'
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare const __VITE_APP_PROXY__: 'true' | 'false'
+74
View File
@@ -0,0 +1,74 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent, h } from 'vue'
import useRequest from './useRequest'
/**
* 在 Vue 应用上下文中运行 composable。
* composable 的 ref/computed/onMounted 只能在 setup() 内使用,
* withSetup 通过挂载一个临时组件来提供这个上下文。
*/
function withSetup<T>(composableFn: () => T): T {
let result!: T
const Comp = defineComponent({
setup() {
result = composableFn()
return () => h('div')
},
})
const wrapper = mount(Comp)
wrapper.unmount()
return result
}
describe('useRequest', () => {
it('初始状态:loading=false, error=false, data=undefined', () => {
const asyncFn = vi.fn().mockResolvedValue('data')
const { loading, error, data } = withSetup(() => useRequest(asyncFn))
expect(loading.value).toBe(false)
expect(error.value).toBe(false)
expect(data.value).toBeUndefined()
})
it('initialData:初始 data 使用传入的默认值', () => {
const asyncFn = vi.fn().mockResolvedValue('new')
const { data } = withSetup(() => useRequest(asyncFn, { initialData: 'init' }))
expect(data.value).toBe('init')
})
it('run 成功:loading 先变 true 后变 falsedata 更新为返回值', async () => {
const asyncFn = vi.fn().mockResolvedValue('result')
const { loading, data, run } = withSetup(() => useRequest(asyncFn))
const runPromise = run()
expect(loading.value).toBe(true)
await runPromise
expect(loading.value).toBe(false)
expect(data.value).toBe('result')
})
it('run 失败:抛出错误,error 被设置,loading 重置为 false', async () => {
const err = new Error('network error')
const asyncFn = vi.fn().mockRejectedValue(err)
const { loading, error, run } = withSetup(() => useRequest(asyncFn))
await expect(run()).rejects.toThrow('network error')
expect(loading.value).toBe(false)
expect(error.value).toBe(err)
})
it('immediate=true:组件挂载时立即调用异步函数并更新 data', async () => {
const asyncFn = vi.fn().mockResolvedValue('eager')
const { data } = withSetup(() => useRequest(asyncFn, { immediate: true }))
expect(asyncFn).toHaveBeenCalledTimes(1)
// 等待 Promise 完成
await asyncFn.mock.results[0].value
expect(data.value).toBe('eager')
})
})
+54
View File
@@ -0,0 +1,54 @@
import type { Ref } from 'vue'
import { ref } from 'vue'
interface IUseRequestOptions<T> {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */
initialData?: T
}
interface IUseRequestReturn<T, P = undefined> {
loading: Ref<boolean>
error: Ref<boolean | Error>
data: Ref<T | undefined>
run: (args?: P) => Promise<T | undefined>
}
/**
* useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
* @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
* @param options 包含请求选项的对象 {immediate, initialData}。
* @param options.immediate 是否立即执行请求,默认为false。
* @param options.initialData 初始化数据,默认为undefined。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/
export default function useRequest<T, P = undefined>(
func: (args?: P) => Promise<T>,
options: IUseRequestOptions<T> = { immediate: false },
): IUseRequestReturn<T, P> {
const loading = ref(false)
const error = ref(false)
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
const run = async (args?: P) => {
loading.value = true
return func(args)
.then((res) => {
data.value = res
error.value = false
return data.value
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
loading.value = false
})
}
if (options.immediate) {
(run as (args: P) => Promise<T | undefined>)({} as P)
}
return { loading, error, data, run }
}
+116
View File
@@ -0,0 +1,116 @@
# 上拉刷新和下拉加载更多
在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
场景一 页面滚动
```
definePage({
style: {
navigationBarTitleText: '上拉刷新和下拉加载更多',
enablePullDownRefresh: true,
onReachBottomDistance: 100,
},
})
```
场景二 局部滚动 结合 `scroll-view`
## 关键文件
- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
## `useScroll` Hook
`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。
### 主要功能
- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
### 使用方法
```typescript
import { useScroll } from '@/hooks/useScroll'
import { getList } from '@/service/list' // 你的数据请求API
const {
list, // 响应式的数据列表
loading, // 是否加载中
finished, // 是否已全部加载
error, // 是否加载失败
onScrollToLower, // 滚动到底部时触发的事件
onRefresherRefresh, // 下拉刷新时触发的事件
} = useScroll(getList) // 将获取数据的API函数传入
```
## `scroll-view` 组件
`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
### 关键属性
- `scroll-y`: 允许纵向滚动。
- `refresher-enabled`: 启用下拉刷新。
- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
## 示例代码
以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll``scroll-view` 结合使用。
```vue
<template>
<view class="scroll-page">
<scroll-view
class="scroll-view"
scroll-y
:refresher-enabled="true"
:refresher-triggered="loading"
@scrolltolower="onScrollToLower"
@refresherrefresh="onRefresherRefresh"
>
<view v-for="item in list" :key="item.id" class="scroll-item">
{{ item.name }}
</view>
<!-- 加载状态提示 -->
<view v-if="loading" class="loading-tip">加载中...</view>
<view v-if="finished" class="finished-tip">没有更多了</view>
<view v-if="error" class="error-tip">加载失败请重试</view>
</scroll-view>
</view>
</template>
<script setup lang="ts">
import { useScroll } from '@/hooks/useScroll'
import { getList } from '@/service/list'
const { list, loading, finished, error, onScrollToLower, onRefresherRefresh } = useScroll(getList)
</script>
<style scoped>
/* 样式省略 */
.scroll-page, .scroll-view {
height: 100%;
}
</style>
```
## 实现步骤总结
1. **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。
2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。
3. **模板绑定**:
- 使用 `scroll-view` 组件作为滚动容器。
- 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
- 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
- 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
+74
View File
@@ -0,0 +1,74 @@
import type { Ref } from 'vue'
import { onMounted, ref } from 'vue'
interface UseScrollOptions<T> {
fetchData: (page: number, pageSize: number) => Promise<T[]>
pageSize?: number
}
interface UseScrollReturn<T> {
list: Ref<T[]>
loading: Ref<boolean>
finished: Ref<boolean>
error: Ref<any>
refresh: () => Promise<void>
loadMore: () => Promise<void>
}
export function useScroll<T>({
fetchData,
pageSize = 10,
}: UseScrollOptions<T>): UseScrollReturn<T> {
const list = ref<T[]>([]) as Ref<T[]>
const loading = ref(false)
const finished = ref(false)
const error = ref<any>(null)
const page = ref(1)
const loadData = async () => {
if (loading.value || finished.value)
return
loading.value = true
error.value = null
try {
const data = await fetchData(page.value, pageSize)
if (data.length < pageSize) {
finished.value = true
}
list.value.push(...data)
page.value++
}
catch (err) {
error.value = err
}
finally {
loading.value = false
}
}
const refresh = async () => {
page.value = 1
finished.value = false
list.value = []
await loadData()
}
const loadMore = async () => {
await loadData()
}
onMounted(() => {
refresh()
})
return {
list,
loading,
finished,
error,
refresh,
loadMore,
}
}
+171
View File
@@ -0,0 +1,171 @@
import { ref } from 'vue'
import { getEnvBaseUrl } from '@/utils/index'
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload`
type TfileType = 'image' | 'file'
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
interface TOptions<T extends TfileType> {
formData?: Record<string, any>
maxSize?: number
accept?: T extends 'image' ? TImage[] : TFile[]
fileType?: T
success?: (params: any) => void
error?: (err: any) => void
}
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
const {
formData = {},
maxSize = 5 * 1024 * 1024,
accept = ['*'],
fileType = 'image',
success,
error: onError,
} = options
const loading = ref(false)
const error = ref<Error | null>(null)
const data = ref<any>(null)
const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
if (size > maxSize) {
uni.showToast({
title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
icon: 'none',
})
return
}
// const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
// const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
// if (!isTypeValid) {
// uni.showToast({
// title: `仅支持 ${accept.join(', ')} 格式的文件`,
// icon: 'none',
// })
// return
// }
loading.value = true
uploadFile({
tempFilePath,
formData,
onSuccess: (res) => {
// 修改这里的解析逻辑,适应不同平台的返回格式
let parsedData = res
try {
// 尝试解析为JSON
const jsonData = JSON.parse(res)
// 检查是否包含data字段
parsedData = jsonData.data || jsonData
}
catch (e) {
// 如果解析失败,使用原始数据
console.log('Response is not JSON, using raw data:', res)
}
data.value = parsedData
// console.log('上传成功', res)
success?.(parsedData)
},
onError: (err) => {
error.value = err
onError?.(err)
},
onComplete: () => {
loading.value = false
},
})
}
const run = () => {
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
const chooseFileOptions = {
count: 1,
success: (res: any) => {
console.log('File selected successfully:', res)
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
// h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
// App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
let tempFilePath = ''
let size = 0
// #ifdef MP-WEIXIN
tempFilePath = res.tempFiles[0].tempFilePath
size = res.tempFiles[0].size
// #endif
// #ifndef MP-WEIXIN
tempFilePath = res.tempFilePaths[0]
size = res.tempFiles[0].size
// #endif
handleFileChoose({ tempFilePath, size })
},
fail: (err: any) => {
console.error('File selection failed:', err)
error.value = err
onError?.(err)
},
}
if (fileType === 'image') {
// #ifdef MP-WEIXIN
uni.chooseMedia({
...chooseFileOptions,
mediaType: ['image'],
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage(chooseFileOptions)
// #endif
}
else {
uni.chooseFile({
...chooseFileOptions,
type: 'all',
})
}
}
return { loading, error, data, run }
}
async function uploadFile({
tempFilePath,
formData,
onSuccess,
onError,
onComplete,
}: {
tempFilePath: string
formData: Record<string, any>
onSuccess: (data: any) => void
onError: (err: any) => void
onComplete: () => void
}) {
uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: 'file',
formData,
success: (uploadFileRes) => {
try {
const data = uploadFileRes.data
onSuccess(data)
}
catch (err) {
onError(err)
}
},
fail: (err) => {
console.error('Upload failed:', err)
onError(err)
},
complete: onComplete,
})
}
+13
View File
@@ -0,0 +1,13 @@
# 请求库
目前unibest支持3种请求库:
- 菲鸽简单封装的 `简单版本http`,路径(src/http/http.ts),对应的示例在 src/api/foo.ts
- `alova 的 http`,路径(src/http/alova.ts),对应的示例在 src/api/foo-alova.ts
- `vue-query`, 路径(src/http/vue-query.ts, 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹
## 如何选择
如果您以前用过 alova 或者 vue-query,可以优先使用您熟悉的。
如果您的项目简单,简单版本的http 就够了,也不会增加包体积。(发版的时候可以去掉alova和vue-query,如果没有超过包体积,留着也无所谓 ^_^)
## roadmap
菲鸽最近在优化脚手架,后续可以选择是否使用第三方的请求库,以及选择什么请求库。还在开发中,大概月底出来(8月31号)。
+133
View File
@@ -0,0 +1,133 @@
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 { toLoginPage } from '@/utils/toLoginPage'
import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
// 配置动态Tag
export const API_DOMAINS = {
DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
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,
...AdapterUniapp(),
timeout: 5000,
statesHook: VueHook,
beforeRequest: onAuthRequired((method) => {
// 设置默认 Content-Type
method.config.headers = {
ContentType: ContentTypeEnum.JSON,
Accept: 'application/json, text/plain, */*',
...method.config.headers,
}
const { config } = method
const ignoreAuth = !config.meta?.ignoreAuth
// 处理认证信息 自行处理认证问题
if (ignoreAuth) {
const token = 'getToken()'
if (!token) {
throw new Error('[请求错误]:未登录')
}
// method.config.headers.token = token;
}
// 处理动态域名
if (config.meta?.domain) {
method.baseURL = config.meta.domain
console.log('当前域名', method.baseURL)
}
}),
responded: onResponseRefreshToken((response, method) => {
console.log('response===>', response)
const { config } = method
const { requestType } = config
const {
statusCode,
data: rawData,
errMsg,
} = response as UniNamespace.RequestSuccessCallbackResult
// 处理特殊请求类型(上传/下载)
if (requestType === 'upload' || requestType === 'download') {
return response
}
// 处理 HTTP 状态码错误
if (statusCode !== 200) {
const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
console.error('errorMessage===>', errorMessage)
uni.showToast({
title: errorMessage,
icon: 'error',
})
throw new Error(`${errorMessage}${errMsg}`)
}
if(config.meta?.isHalo){
if(rawData) {
return rawData
}
if (config.meta?.toast !== false) {
uni.showToast({
title:'请求错误:返回数据为空' ,
icon: 'none',
})
return rawData
}
throw new Error('请求错误:返回数据为空')
}
// 处理业务逻辑错误
const { code, message, data } = rawData as IResponse
// 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
if (config.meta?.toast !== false) {
uni.showToast({
title: message,
icon: 'none',
})
}
throw new Error(`请求错误[${code}]${message}`)
}
// 处理成功响应,返回业务数据
return data
}),
})
export const http = alovaInstance
+200
View File
@@ -0,0 +1,200 @@
import type { IDoubleTokenRes } from '@/api/types/login'
import type { CustomRequestOptions, IResponse } from '@/http/types'
import { nextTick } from 'vue'
import { useTokenStore } from '@/store/token'
import { isDoubleTokenMode } from '@/utils'
import { toLoginPage } from '@/utils/toLoginPage'
import { ResultEnum } from './tools/enum'
// 刷新 token 状态管理
let refreshing = false // 防止重复刷新 token 标识
let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
export function http<T>(options: CustomRequestOptions) {
// 1. 返回 Promise 对象
return new Promise<T>((resolve, reject) => {
uni.request({
...options,
dataType: 'json',
// #ifndef MP-WEIXIN
responseType: 'json',
// #endif
// 响应成功
success: async (res) => {
const responseData = res.data as IResponse<T>
const { code } = responseData
// 检查是否是401错误(包括HTTP状态码401或业务码401)
const isTokenExpired = res.statusCode === 401 || code === 401
if (isTokenExpired) {
const tokenStore = useTokenStore()
if (!isDoubleTokenMode) {
// 未启用双token策略,清理用户信息,跳转到登录页
tokenStore.logout()
toLoginPage()
return reject(res)
}
/* -------- 无感刷新 token ----------- */
const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
// token 失效的,且有刷新 token 的,才放到请求队列里
if (refreshToken) {
taskQueue.push(() => {
resolve(http<T>(options))
})
}
// 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
if (refreshToken && !refreshing) {
refreshing = true
try {
// 发起刷新 token 请求(使用 store 的 refreshToken 方法)
await tokenStore.refreshToken()
// 刷新 token 成功
refreshing = false
nextTick(() => {
// 关闭其他弹窗
uni.hideToast()
uni.showToast({
title: 'token 刷新成功',
icon: 'none',
})
})
// 将任务队列的所有任务重新请求
taskQueue.forEach(task => task())
}
catch (refreshErr) {
console.error('刷新 token 失败:', refreshErr)
refreshing = false
// 刷新 token 失败,跳转到登录页
nextTick(() => {
// 关闭其他弹窗
uni.hideToast()
uni.showToast({
title: '登录已过期,请重新登录',
icon: 'none',
})
})
// 清除用户信息
await tokenStore.logout()
// 跳转到登录页
setTimeout(() => {
toLoginPage()
}, 2000)
}
finally {
// 不管刷新 token 成功与否,都清空任务队列
taskQueue = []
}
}
return reject(res)
}
// 处理其他成功状态(HTTP状态码200-299
if (res.statusCode >= 200 && res.statusCode < 300) {
// 处理业务逻辑错误
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
uni.showToast({
icon: 'none',
title: responseData.msg || responseData.message || '请求错误',
})
return reject(responseData.data)
}
return resolve(responseData.data)
}
// 处理其他错误
!options.hideErrorToast
&& uni.showToast({
icon: 'none',
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
*/
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
*/
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
+69
View File
@@ -0,0 +1,69 @@
import type { CustomRequestOptions } from '@/http/types'
import { useTokenStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import { stringifyQuery } from './tools/queryString'
// 请求基准地址
const baseUrl = getEnvBaseUrl()
// 拦截器配置
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 tokenStore = useTokenStore()
const token = tokenStore.updateNowTime().validToken
if (token) {
options.header.Authorization = `Bearer ${token}`
}
return options
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
},
}
+68
View File
@@ -0,0 +1,68 @@
export enum ResultEnum {
// 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
Success0 = 0, // 成功
Success200 = 200, // 成功
Error = 400, // 错误
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},请检查网络或联系管理员!`
}
+29
View File
@@ -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('&')
}
+44
View File
@@ -0,0 +1,44 @@
/**
* 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
*/
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
} & IUniUploadFileOptions // 添加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
message: string
[key: string]: any // 允许额外属性
} | {
code: number
data: T
msg: string
[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
}
+30
View File
@@ -0,0 +1,30 @@
import type { CustomRequestOptions } from '@/http/types'
import { http } from './http'
/*
* openapi-ts-request 工具的 request 跨客户端适配方法
*/
export default function request<T extends { data?: any }>(
url: string,
options: Omit<CustomRequestOptions, 'url'> & {
params?: Record<string, unknown>
headers?: Record<string, unknown>
},
) {
const requestOptions = {
url,
...options,
}
if (options.params) {
requestOptions.query = requestOptions.params
delete requestOptions.params
}
if (options.headers) {
requestOptions.header = options.headers
delete requestOptions.headers
}
return http<T['data']>(requestOptions)
}
+3
View File
@@ -0,0 +1,3 @@
<template>
<slot />
</template>
+12
View File
@@ -0,0 +1,12 @@
# 注意事项
> 文件夹名字必须为 `locale`, 这是 `uniapp` 官方约定的,如果改为别的,标题将不能正常切换多语言(其他内容还是正常)。
>
> `xxx.json``xxx` 多语言标识必须与 `uniapp` 官方约定的一致,否则也会出现 BUG。
>
> 查看截图 `screenshots/i18n.png`
## 参考文档
[uniapp 国际化开发指南](https://uniapp.dcloud.net.cn/tutorial/i18n.html)
[uniapp 国际化-注意事项](https://uniapp.dcloud.net.cn/api/ui/locale.html#onlocalechange) 最下面的注意事项
+13
View File
@@ -0,0 +1,13 @@
{
"tabbar.home": "Home",
"tabbar.gallery": "Gallery",
"tabbar.moments": "Moments",
"tabbar.about": "About",
"tabbar.me": "Me",
"tabbar.i18n": "I18n",
"i18n.title": "En Title",
"alova.title": "Alova Request",
"weight": "{heavy}KG",
"detail": "{0}cm, {1}KG",
"introduction": "I am {name},height:{detail.height},weight:{detail.weight}"
}
+81
View File
@@ -0,0 +1,81 @@
import { createI18n } from 'vue-i18n'
import en from './en.json'
import zhHans from './zh-Hans.json' // 简体中文
const messages = {
en,
'zh-Hans': zhHans, // key 不能乱写,查看uniapp官网
}
const i18n = createI18n({
locale: uni.getLocale(), // 获取已设置的语言,fallback 语言需要再 manifest.config.ts 中设置
messages,
allowComposition: true,
})
console.log(uni.getLocale())
console.log(i18n.global.locale)
/**
* vue 使
* @param { string } key keyeg: "app.name"
* @returns {string} eg: "{heavy}KG"
*/
export function getTemplateByKey(key: string) {
if (!key) {
console.error(`[i18n] Function getTemplateByKey(), key param is required`)
return ''
}
const locale = uni.getLocale()
console.log('locale:', locale)
const message = messages[locale] // 拿到某个多语言的所有模板(是一个对象)
if (Object.keys(message).includes(key)) {
return message[key]
}
try {
const keyList = key.split('.')
return keyList.reduce((pre, cur) => {
return pre[cur]
}, message)
}
catch (error) {
console.error(`[i18n] Function getTemplateByKey(), key param ${key} is not existed.`)
return ''
}
}
/**
* formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
*
* @param template eg: `我是{name}`
* @param {object | undefined} data key与多语言字符串对应eg: `{name:'菲鸽'}`
* @returns
*/
function formatI18n(template: string, data?: any) {
return template.replace(/\{([^}]+)\}/g, (match, key: string) => {
// console.log( match, key) // => { detail.height } detail.height
const arr = key.trim().split('.')
let result = data
while (arr.length) {
const first = arr.shift()
result = result[first]
}
return result
})
}
/**
* t('introduction',{name:'张三',detail:{height:178,weight:'75kg'}})
* => formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
* key的 data
* @param template eg: `我是{name}`
* @param {object | undefined} data key与多语言字符串对应eg: `{name:'菲鸽'}`
* @returns
*/
export function t(key, data?) {
return formatI18n(getTemplateByKey(key), data)
}
export default i18n
+13
View File
@@ -0,0 +1,13 @@
{
"tabbar.home": "首页",
"tabbar.gallery": "相册",
"tabbar.moments": "瞬间",
"tabbar.about": "关于",
"tabbar.me": "我的",
"tabbar.i18n": "语言",
"i18n.title": "中文标题",
"alova.title": "Alova 请求",
"weight": "{heavy}公斤",
"detail": "{0}cm, {1}公斤",
"introduction": "我是 {name},身高:{detail.height},体重:{detail.weight}"
}
+21
View File
@@ -0,0 +1,21 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import { requestInterceptor } from './http/interceptor'
import { routeInterceptor } from './router/interceptor'
import store from './store'
import '@/style/index.scss'
import 'virtual:uno.css'
import i18n from './locale/index'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
app.use(routeInterceptor)
app.use(requestInterceptor)
app.use(i18n)
return {
app,
}
}
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
defineOptions({
name: 'About',
})
definePage({
style: {
navigationBarTitleText: '关于',
},
})
</script>
<template>
<view class="mt-10 text-center text-green-500">
关于页面
</view>
</template>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
defineOptions({
name: 'PageTemplate',
})
definePage({
style: {
navigationBarTitleText: '页面模板',
},
})
</script>
<template>
<view class="mt-10 text-center text-green-500">
页面模板页面
</view>
</template>
@@ -0,0 +1,17 @@
<script lang="ts" setup>
defineOptions({
name: 'PostDetail',
})
definePage({
style: {
navigationBarTitleText: '文章详情',
},
})
</script>
<template>
<view class="min-h-screen w-full">
文章详情页面
</view>
</template>
+132
View File
@@ -0,0 +1,132 @@
<script lang="ts" setup>
import i18n, { t } from '@/locale/index'
import { setTabbarItem } from '@/tabbar/i18n'
import { testI18n } from '@/utils/i18n'
definePage({
style: {
navigationBarTitleText: '%i18n.title%',
},
})
const current = ref(uni.getLocale())
const user = { name: '张三', detail: { height: 178, weight: '75kg' } }
const languages = [
{
value: 'zh-Hans',
name: '中文',
checked: 'true',
},
{
value: 'en',
name: '英文',
},
]
function radioChange(evt) {
// console.log(evt)
current.value = evt.detail.value
// 2
uni.setLocale(evt.detail.value)
i18n.global.locale = evt.detail.value
// tabbar
setTabbarItem()
//
uni.setNavigationBarTitle({
title: t('i18n.title'),
})
}
</script>
<template>
<view class="mt-6 center flex-col">
<view class="p-4 text-red-500 leading-6">
经过我的测试发现小程序里面会有2处BUG
<view>
<text class="line-through"> 1. 页面标题多语言不生效 </text>
<text class="ml-2 text-green-500"> 已解决 </text>
</view>
<view>
<text class="line-through">
2. 多语言传递的参数不生效如下 heavy
</text>
<text class="ml-2 text-green-500"> 已解决 </text>
<view class="ml-2 text-green-500">
$t 改为自定义的 t 即可
</view>
</view>
</view>
<view class="text-green-500">
多语言测试
</view>
<view class="m-4">
{{ $t("i18n.title") }}
</view>
<view class="text-gray-500 italic">
使用$t: {{ $t("weight", { heavy: 100 }) }}
</view>
<view class="m-4">
{{ $t("weight", { heavy: 100 }) }}
</view>
<view class="text-gray-500 italic">
使用t: {{ t("weight", { heavy: 100 }) }}
</view>
<view class="m-4">
{{ t("weight", { heavy: 100 }) }}
</view>
<view class="m-4">
{{ t("introduction", user) }}
</view>
<view class="mt-12 text-green-500">
切换语言
</view>
<view class="uni-list">
<radio-group class="radio-group" @change="radioChange">
<label
v-for="item in languages"
:key="item.value"
class="uni-list-cell uni-list-cell-pd"
>
<view>
<radio :value="item.value" :checked="item.value === current" />
</view>
<view>{{ item.name }}</view>
</label>
</radio-group>
</view>
<!-- http://localhost:9000/#/pages/index/i18n -->
<button class="mb-44 mt-20" @click="testI18n">
测试弹窗
</button>
</view>
</template>
<style lang="scss">
.uni-list {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
background-color: #fff;
border-radius: 12px;
}
.radio-group {
width: 200px;
margin: 10px auto;
border-radius: 12px;
}
.uni-list-cell {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 10px;
background-color: #bcecd1;
}
</style>
@@ -0,0 +1,36 @@
<script lang="ts" setup>
import useRequest from '@/hooks/useRequest'
import { queryPostByName, queryPosts } from '@/api/halo-base/post'
import { completeUrl } from '@/utils/url'
import type { ListedPostVoList, PostV1alpha1PublicApiQueryPostByNameRequest, PostV1alpha1PublicApiQueryPostsRequest, PostVo } from '@halo-dev/api-client'
defineOptions({
name: 'RequestDemo',
})
definePage({
style: {
navigationBarTitleText: '请求演示',
},
})
const { loading, error, data, run } = useRequest<ListedPostVoList, PostV1alpha1PublicApiQueryPostsRequest>(queryPosts)
const { loading: loadingSingle, error: errorSingle, data: dataSingle, run: runSingle } = useRequest<PostVo, PostV1alpha1PublicApiQueryPostByNameRequest>(queryPostByName)
onLoad(() => {
console.log('测试 uni API 自动引入: onLoad')
runSingle({ name: '019eabcd-afe4-709a-a89f-8b129a38f321' })
})
</script>
<template>
<view class="min-h-screen bg-page pt-safe">
<image v-if="dataSingle" :src="completeUrl(dataSingle.spec.cover)" class="h-60 w-full object-cover" />
<view class="mt-4 flex flex-col items-center gap-y-2">
<button @click="run({ page: 1, size: 10 })">
查询文章列表
</button>
</view>
</view>
</template>
+706
View File
@@ -0,0 +1,706 @@
{
"localdata": [
{ "value": 35, "text": "2016", "group": "目标值" },
{ "value": 18, "text": "2016", "group": "完成量" },
{ "value": 36, "text": "2017", "group": "目标值" },
{ "value": 27, "text": "2017", "group": "完成量" },
{ "value": 31, "text": "2018", "group": "目标值" },
{ "value": 21, "text": "2018", "group": "完成量" },
{ "value": 33, "text": "2019", "group": "目标值" },
{ "value": 24, "text": "2019", "group": "完成量" },
{ "value": 13, "text": "2020", "group": "目标值" },
{ "value": 6, "text": "2020", "group": "完成量" },
{ "value": 34, "text": "2021", "group": "目标值" },
{ "value": 28, "text": "2021", "group": "完成量" }
],
"localdataB": [
{ "value": 50, "text": "一班" },
{ "value": 30, "text": "二班" },
{ "value": 20, "text": "三班" },
{ "value": 18, "text": "四班" },
{ "value": 8, "text": "五班" }
],
"TLine": {
"series": [
{
"name": "时间轴1",
"data": [
[10000, 55],
[30000, 25],
[50000, 55],
[70000, 25],
[90000, 55]
]
},
{
"name": "时间轴2",
"data": [
[0, 25],
[20000, 55],
[40000, 25],
[60000, 55],
[80000, 25]
]
},
{
"name": "时间轴3",
"data": [
[0, 55],
[15000, 25],
[30000, 55],
[45000, 25],
[60000, 55]
]
}
]
},
"Scatter": {
"series": [
{
"name": "散点一",
"data": [
[10.0, 8.04],
[8.07, 6.95],
[13.0, 7.58],
[9.05, 8.81],
[11.0, 8.33],
[14.0, 7.66],
[13.4, 6.81],
[10.0, 6.33],
[14.0, 8.96],
[12.5, 6.82]
]
},
{
"name": "散点二",
"data": [
[9.15, 7.2],
[11.5, 7.2],
[3.03, 4.23],
[12.2, 7.83],
[2.02, 4.47],
[1.05, 3.33],
[4.05, 4.96],
[6.03, 7.24],
[12.0, 6.26],
[12.0, 8.84],
[7.08, 5.82],
[5.02, 5.68]
]
}
]
},
"Bubble": {
"series": [
{
"name": "气泡一",
"data": [
[95, 95, 23, "标题1"],
[30, 55, 33, "标题2"]
]
},
{
"name": "气泡二",
"data": [
[130, 30, 30, "标题3"],
[200, 90, 40, "标题4"]
]
}
]
},
"Column": {
"categories": ["2016", "2017", "2018", "2019", "2020", "2021"],
"series": [
{
"name": "目标值",
"data": [35, 36, 31, 33, 13, 34]
},
{
"name": "完成量",
"data": [18, 27, 21, 24, 6, 28]
}
]
},
"ColumnA": {
"categories": ["2016", "2017", "2018", "2019", "2020", "2021"],
"series": [
{
"name": "成交量1",
"data": [15, { "value": 20, "color": "#f04864" }, 45, 37, 43, 34]
},
{
"name": "成交量2",
"data": [30, { "value": 40, "color": "#facc14" }, 25, 14, 34, 18]
}
]
},
"Mix": {
"categories": ["2016", "2017", "2018", "2019", "2020", "2021"],
"series": [
{
"name": "曲面",
"data": [70, 50, 85, 130, 64, 88],
"type": "area",
"style": "curve"
},
{
"name": "柱1",
"index": 1,
"data": [40, { "value": 30, "color": "#f04864" }, 55, 110, 24, 58],
"type": "column"
},
{
"name": "柱2",
"index": 1,
"data": [50, 20, 75, 60, 34, 38],
"type": "column"
},
{
"name": "曲线",
"data": [70, 50, 85, 130, 64, 88],
"type": "line",
"style": "curve",
"color": "#1890ff",
"disableLegend": true
},
{
"name": "折线",
"data": [120, 140, 105, 170, 95, 160],
"type": "line",
"color": "#2fc25b"
},
{
"name": "点",
"index": 2,
"data": [100, 80, 125, 150, 112, 132],
"type": "point",
"color": "#f04864"
}
]
},
"Line": {
"categories": ["2016", "2017", "2018", "2019", "2020", "2021"],
"series": [
{
"name": "成交量A",
"data": [35, 8, 25, 37, 4, 20]
},
{
"name": "成交量B",
"data": [70, 40, 65, 100, 44, 68]
},
{
"name": "成交量C",
"data": [100, 80, 95, 150, 112, 132]
}
]
},
"Pie": {
"series": [
{
"name": "一班",
"data": 50
},
{
"name": "二班",
"data": 30
},
{
"name": "三班",
"data": 20
},
{
"name": "四班",
"data": 18
},
{
"name": "五班",
"data": 8
}
]
},
"PieA": {
"series": [
{
"data": [
{
"name": "一班",
"value": 50
},
{
"name": "二班",
"value": 30
},
{
"name": "三班",
"value": 20
},
{
"name": "四班",
"value": 18
},
{
"name": "五班",
"value": 8
}
]
}
]
},
"Mount": {
"series": [
{
"data": [
{
"name": "一班",
"value": 82
},
{
"name": "二班",
"value": 63
},
{
"name": "三班",
"value": 86
},
{
"name": "四班",
"value": 65
},
{
"name": "五班",
"value": 79
}
]
}
]
},
"Radar": {
"categories": ["维度1", "维度2", "维度3", "维度4", "维度5", "维度6"],
"series": [
{
"name": "成交量1",
"data": [90, 110, 165, 195, 187, 172]
},
{
"name": "成交量2",
"data": [190, 210, 105, 35, 27, 102]
}
]
},
"Arcbar1": {
"series": [
{
"name": "正确率",
"data": 0.8,
"color": "#2fc25b"
}
]
},
"Arcbar2": {
"series": [
{
"name": "一班",
"data": 0.8
},
{
"name": "二班",
"data": 0.6
},
{
"name": "三班",
"data": 0.45
},
{
"name": "四班",
"data": 0.3
},
{
"name": "五班",
"data": 0.15
}
]
},
"Gauge": {
"categories": [
{
"value": 0.2,
"color": "#1890ff"
},
{
"value": 0.8,
"color": "#2fc25b"
},
{
"value": 1,
"color": "#f04864"
}
],
"series": [
{
"name": "完成率",
"data": 0.66
}
]
},
"Candle": {
"categories": [
"2020/1/24",
"2020/1/25",
"2020/1/28",
"2020/1/29",
"2020/1/30",
"2020/1/31",
"2020/2/1",
"2020/2/4",
"2020/2/5",
"2020/2/6",
"2020/2/7",
"2020/2/8",
"2020/2/18",
"2020/2/19",
"2020/2/20",
"2020/2/21",
"2020/2/22",
"2020/2/25",
"2020/2/26",
"2020/2/27",
"2020/2/28",
"2020/3/1",
"2020/3/4",
"2020/3/5",
"2020/3/6",
"2020/3/7",
"2020/3/8",
"2020/3/11",
"2020/3/12",
"2020/3/13",
"2020/3/14",
"2020/3/15",
"2020/3/18",
"2020/3/19",
"2020/3/20",
"2020/3/21",
"2020/3/22",
"2020/3/25",
"2020/3/26",
"2020/3/27",
"2020/3/28",
"2020/3/29",
"2020/4/1",
"2020/4/2",
"2020/4/3",
"2020/4/8",
"2020/4/9",
"2020/4/10",
"2020/4/11",
"2020/4/12",
"2020/4/15",
"2020/4/16",
"2020/4/17",
"2020/4/18",
"2020/4/19",
"2020/4/22",
"2020/4/23",
"2020/4/24",
"2020/4/25",
"2020/4/26",
"2020/5/2",
"2020/5/3",
"2020/5/6",
"2020/5/7",
"2020/5/8",
"2020/5/9",
"2020/5/10",
"2020/5/13",
"2020/5/14",
"2020/5/15",
"2020/5/16",
"2020/5/17",
"2020/5/20",
"2020/5/21",
"2020/5/22",
"2020/5/23",
"2020/5/24",
"2020/5/27",
"2020/5/28",
"2020/5/29",
"2020/5/30",
"2020/5/31",
"2020/6/3",
"2020/6/4",
"2020/6/5",
"2020/6/6",
"2020/6/7",
"2020/6/13"
],
"series": [
{
"name": "上证指数",
"data": [
[2320.26, 2302.6, 2287.3, 2362.94],
[2300, 2291.3, 2288.26, 2308.38],
[2295.35, 2346.5, 2295.35, 2346.92],
[2347.22, 2358.98, 2337.35, 2363.8],
[2360.75, 2382.48, 2347.89, 2383.76],
[2383.43, 2385.42, 2371.23, 2391.82],
[2377.41, 2419.02, 2369.57, 2421.15],
[2425.92, 2428.15, 2417.58, 2440.38],
[2411, 2433.13, 2403.3, 2437.42],
[2432.68, 2434.48, 2427.7, 2441.73],
[2430.69, 2418.53, 2394.22, 2433.89],
[2416.62, 2432.4, 2414.4, 2443.03],
[2441.91, 2421.56, 2415.43, 2444.8],
[2420.26, 2382.91, 2373.53, 2427.07],
[2383.49, 2397.18, 2370.61, 2397.94],
[2378.82, 2325.95, 2309.17, 2378.82],
[2322.94, 2314.16, 2308.76, 2330.88],
[2320.62, 2325.82, 2315.01, 2338.78],
[2313.74, 2293.34, 2289.89, 2340.71],
[2297.77, 2313.22, 2292.03, 2324.63],
[2322.32, 2365.59, 2308.92, 2366.16],
[2364.54, 2359.51, 2330.86, 2369.65],
[2332.08, 2273.4, 2259.25, 2333.54],
[2274.81, 2326.31, 2270.1, 2328.14],
[2333.61, 2347.18, 2321.6, 2351.44],
[2340.44, 2324.29, 2304.27, 2352.02],
[2326.42, 2318.61, 2314.59, 2333.67],
[2314.68, 2310.59, 2296.58, 2320.96],
[2309.16, 2286.6, 2264.83, 2333.29],
[2282.17, 2263.97, 2253.25, 2286.33],
[2255.77, 2270.28, 2253.31, 2276.22],
[2269.31, 2278.4, 2250, 2312.08],
[2267.29, 2240.02, 2239.21, 2276.05],
[2244.26, 2257.43, 2232.02, 2261.31],
[2257.74, 2317.37, 2257.42, 2317.86],
[2318.21, 2324.24, 2311.6, 2330.81],
[2321.4, 2328.28, 2314.97, 2332],
[2334.74, 2326.72, 2319.91, 2344.89],
[2318.58, 2297.67, 2281.12, 2319.99],
[2299.38, 2301.26, 2289, 2323.48],
[2273.55, 2236.3, 2232.91, 2273.55],
[2238.49, 2236.62, 2228.81, 2246.87],
[2229.46, 2234.4, 2227.31, 2243.95],
[2234.9, 2227.74, 2220.44, 2253.42],
[2232.69, 2225.29, 2217.25, 2241.34],
[2196.24, 2211.59, 2180.67, 2212.59],
[2215.47, 2225.77, 2215.47, 2234.73],
[2224.93, 2226.13, 2212.56, 2233.04],
[2236.98, 2219.55, 2217.26, 2242.48],
[2218.09, 2206.78, 2204.44, 2226.26],
[2199.91, 2181.94, 2177.39, 2204.99],
[2169.63, 2194.85, 2165.78, 2196.43],
[2195.03, 2193.8, 2178.47, 2197.51],
[2181.82, 2197.6, 2175.44, 2206.03],
[2201.12, 2244.64, 2200.58, 2250.11],
[2236.4, 2242.17, 2232.26, 2245.12],
[2242.62, 2184.54, 2182.81, 2242.62],
[2187.35, 2218.32, 2184.11, 2226.12],
[2213.19, 2199.31, 2191.85, 2224.63],
[2203.89, 2177.91, 2173.86, 2210.58],
[2170.78, 2174.12, 2161.14, 2179.65],
[2179.05, 2205.5, 2179.05, 2222.81],
[2212.5, 2231.17, 2212.5, 2236.07],
[2227.86, 2235.57, 2219.44, 2240.26],
[2242.39, 2246.3, 2235.42, 2255.21],
[2246.96, 2232.97, 2221.38, 2247.86],
[2228.82, 2246.83, 2225.81, 2247.67],
[2247.68, 2241.92, 2231.36, 2250.85],
[2238.9, 2217.01, 2205.87, 2239.93],
[2217.09, 2224.8, 2213.58, 2225.19],
[2221.34, 2251.81, 2210.77, 2252.87],
[2249.81, 2282.87, 2248.41, 2288.09],
[2286.33, 2299.99, 2281.9, 2309.39],
[2297.11, 2305.11, 2290.12, 2305.3],
[2303.75, 2302.4, 2292.43, 2314.18],
[2293.81, 2275.67, 2274.1, 2304.95],
[2281.45, 2288.53, 2270.25, 2292.59],
[2286.66, 2293.08, 2283.94, 2301.7],
[2293.4, 2321.32, 2281.47, 2322.1],
[2323.54, 2324.02, 2321.17, 2334.33],
[2316.25, 2317.75, 2310.49, 2325.72],
[2320.74, 2300.59, 2299.37, 2325.53],
[2300.21, 2299.25, 2294.11, 2313.43],
[2297.1, 2272.42, 2264.76, 2297.1],
[2270.71, 2270.93, 2260.87, 2276.86],
[2264.43, 2242.11, 2240.07, 2266.69],
[2242.26, 2210.9, 2205.07, 2250.63],
[2190.1, 2148.35, 2126.22, 2190.1]
]
}
]
},
"CandleColumn": {
"categories": [
"2020/1/24",
"2020/1/25",
"2020/1/28",
"2020/1/29",
"2020/1/30",
"2020/1/31",
"2020/2/1",
"2020/2/4",
"2020/2/5",
"2020/2/6",
"2020/2/7",
"2020/2/8",
"2020/2/18",
"2020/2/19",
"2020/2/20",
"2020/2/21",
"2020/2/22",
"2020/2/25",
"2020/2/26",
"2020/2/27",
"2020/2/28",
"2020/3/1",
"2020/3/4",
"2020/3/5",
"2020/3/6",
"2020/3/7",
"2020/3/8",
"2020/3/11",
"2020/3/12",
"2020/3/13",
"2020/3/14",
"2020/3/15",
"2020/3/18",
"2020/3/19",
"2020/3/20",
"2020/3/21",
"2020/3/22",
"2020/3/25",
"2020/3/26",
"2020/3/27",
"2020/3/28",
"2020/3/29",
"2020/4/1",
"2020/4/2",
"2020/4/3",
"2020/4/8",
"2020/4/9",
"2020/4/10",
"2020/4/11",
"2020/4/12",
"2020/4/15",
"2020/4/16",
"2020/4/17",
"2020/4/18",
"2020/4/19",
"2020/4/22",
"2020/4/23",
"2020/4/24",
"2020/4/25",
"2020/4/26",
"2020/5/2",
"2020/5/3",
"2020/5/6",
"2020/5/7",
"2020/5/8",
"2020/5/9",
"2020/5/10",
"2020/5/13",
"2020/5/14",
"2020/5/15",
"2020/5/16",
"2020/5/17",
"2020/5/20",
"2020/5/21",
"2020/5/22",
"2020/5/23",
"2020/5/24",
"2020/5/27",
"2020/5/28",
"2020/5/29",
"2020/5/30",
"2020/5/31",
"2020/6/3",
"2020/6/4",
"2020/6/5",
"2020/6/6",
"2020/6/7",
"2020/6/13"
],
"series": [
{
"name": "成交量1",
"data": [
15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20,
45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37,
43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15,
20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45,
37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43, 15, 20, 45, 37, 43,
15, 20, 45
]
}
]
},
"Word": {
"series": [
{
"name": "跨全端图表",
"textSize": 25
},
{
"name": "微信小程序",
"textSize": 20
},
{
"name": "支付宝小程序",
"textSize": 20
},
{
"name": "百度小程序",
"textSize": 20
},
{
"name": "QQ小程序",
"textSize": 20
},
{
"name": "头条小程序",
"textSize": 20
},
{
"name": "抖音小程序",
"textSize": 20
},
{
"name": "360小程序",
"textSize": 20
},
{
"name": "跨全端",
"textSize": 10
},
{
"name": "跨全端",
"textSize": 12
},
{
"name": "跨全端",
"textSize": 10
},
{
"name": "跨全端",
"textSize": 12
},
{
"name": "跨全端",
"textSize": 10
},
{
"name": "跨全端",
"textSize": 12
},
{
"name": "跨全端",
"textSize": 10
},
{
"name": "跨全端",
"textSize": 12
}
]
}
}
+100
View File
@@ -0,0 +1,100 @@
<template>
<view class="content">
<qiun-title-bar title="tooltip提示窗format" />
<view class="charts-box">
<qiun-data-charts
type="column"
:echarts-h5="true"
:echarts-app="true"
:chart-data="chartsDataLine1"
tooltip-format="tooltipDemo1"
/>
</view>
<qiun-title-bar title="图例的format" />
<view class="charts-box">
<qiun-data-charts
type="line"
:echarts-h5="true"
:echarts-app="true"
:eopts="{ legend: { format: 'legendFormat' } }"
:chart-data="chartsDataColumn2"
/>
</view>
<qiun-title-bar title="Y轴format方式1" />
<view class="charts-box">
<qiun-data-charts
type="line"
:echarts-h5="true"
:echarts-app="true"
:eopts="{ yAxis: { axisLabel: { format: 'yAxisFormatDemo' } } }"
:chart-data="chartsDataLine1"
/>
</view>
<qiun-title-bar title="Y轴format方式2" />
<view class="charts-box">
<qiun-data-charts
type="line"
:echarts-h5="true"
:echarts-app="true"
:eopts="{ yAxis: { axisLabel: { formatter: '{value} 元' } } }"
:chart-data="chartsDataLine1"
/>
</view>
<qiun-title-bar title="series数据点format方法1" />
<view class="charts-box">
<qiun-data-charts
type="line"
:echarts-h5="true"
:echarts-app="true"
:eopts="{ seriesTemplate: { label: { format: 'seriesFormatDemo' } } }"
:chart-data="chartsDataLine1"
/>
</view>
<qiun-title-bar title="series数据点format方法2" />
<view class="charts-box">
<qiun-data-charts
type="line"
:echarts-h5="true"
:echarts-app="true"
:eopts="{ seriesTemplate: { label: { formatter: '{b}年{c}元' } } }"
:chart-data="chartsDataLine1"
/>
</view>
</view>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import demodata from './data.json'
definePage({
style: { navigationBarTitleText: 'ucharts 图表' },
})
const chartsDataLine1 = ref({})
const chartsDataColumn2 = ref({})
function getServerData() {
setTimeout(() => {
chartsDataLine1.value = JSON.parse(JSON.stringify(demodata.Line))
chartsDataColumn2.value = JSON.parse(JSON.stringify(demodata.Column))
}, 1500)
}
onMounted(() => {
getServerData()
})
</script>
<style>
.content {
display: flex;
flex: 1;
flex-direction: column;
}
.charts-box {
width: 100%;
height: 300px;
}
</style>
+20
View File
@@ -0,0 +1,20 @@
# 登录页
需要输入账号、密码/验证码的登录页。
## 适用性
本页面主要用于 `h5``APP`
小程序通常有平台的登录方式 `uni.login` 通常用不到登录页,所以不适用于 `小程序`。(即默认情况下,小程序环境是不会走登录拦截逻辑的。)
但是如果您的小程序也需要现实的 `登录页` 那也是可以使用的。
`src/router/config.ts` 中有一个变量 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在小程序中使用 `H5的登录页`
更多信息请看 `src/router` 文件夹的内容。
## 登录跳转
目前登录的跳转逻辑主要在 `src/router/interceptor.ts``src/pages/login/login.vue` 里面,默认会在登录后自动重定向到来源/配置的页面。
如果与您的业务不符,您可以自行修改。
+44
View File
@@ -0,0 +1,44 @@
<script lang="ts" setup>
import { useTokenStore } from '@/store/token'
definePage({
style: {
navigationBarTitleText: '登录',
},
})
const tokenStore = useTokenStore()
async function doLogin() {
if (tokenStore.hasLogin) {
uni.navigateBack()
return
}
try {
//
await tokenStore.login({
username: '菲鸽',
password: '123456',
})
uni.navigateBack()
}
catch (error) {
console.log('登录失败', error)
}
}
</script>
<template>
<view class="login">
<!-- 本页面是非MP的登录页主要用于 h5 APP -->
<view class="text-center">
登录页
</view>
<button class="mt-4 w-40 text-center" @click="doLogin">
点击模拟登录
</button>
</view>
</template>
<style lang="scss" scoped>
//
</style>
+34
View File
@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { LOGIN_PAGE } from '@/router/config'
definePage({
style: {
navigationBarTitleText: '注册',
},
})
function doRegister() {
uni.showToast({
title: '注册成功',
})
//
uni.navigateTo({
url: LOGIN_PAGE,
})
}
</script>
<template>
<view class="login">
<view class="text-center">
注册页
</view>
<button class="mt-4 w-40 text-center" @click="doRegister">
点击模拟注册
</button>
</view>
</template>
<style lang="scss" scoped>
//
</style>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
defineOptions({
name: 'Gallery',
})
definePage({
style: {
navigationBarTitleText: '相册',
},
})
</script>
<template>
<view class="mt-10 text-center text-green-500">
相册页面
</view>
</template>
+37
View File
@@ -0,0 +1,37 @@
<script lang="ts" setup>
defineOptions({
name: 'Home',
})
definePage({
type: 'home',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '首页',
},
})
function toRequestDemo() {
uni.navigateTo({
url: '/pages-demo/request-demo/request-demo',
})
}
onLoad(() => {
console.log('测试 uni API 自动引入: onLoad')
})
</script>
<template>
<view class="min-h-screen flex flex-col items-center justify-center gap-y-6 bg-page pt-safe">
<image class="h-24 w-24 rounded-lg object-cover" src="/static/logo.png" />
<view class="text-2xl text-blue-500 font-bold">
Uni Halo v3.0.0
</view>
<view class="mt-4 flex flex-col items-center gap-y-2">
<button @click="toRequestDemo">
请求演示
</button>
</view>
</view>
</template>
+79
View File
@@ -0,0 +1,79 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { LOGIN_PAGE } from '@/router/config'
import { useUserStore } from '@/store'
import { useTokenStore } from '@/store/token'
definePage({
style: {
navigationBarTitleText: '我的',
},
})
const userStore = useUserStore()
const tokenStore = useTokenStore()
// 使storeToRefsuserInfo
const { userInfo } = storeToRefs(userStore)
//
async function handleLogin() {
// #ifdef MP-WEIXIN
//
await tokenStore.wxLogin()
// #endif
// #ifndef MP-WEIXIN
uni.navigateTo({
url: `${LOGIN_PAGE}`,
})
// #endif
}
function handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
//
useTokenStore().logout()
// 退
uni.showToast({
title: '退出登录成功',
icon: 'success',
})
// #ifdef MP-WEIXIN
//
// uni.reLaunch({ url: '/pages/index/index' })
// #endif
// #ifndef MP-WEIXIN
//
// uni.navigateTo({ url: LOGIN_PAGE })
// #endif
}
},
})
}
</script>
<template>
<view class="profile-container">
<view class="mt-3 break-all px-3 text-center text-green-500">
{{ userInfo.username ? '已登录' : '未登录' }}
</view>
<view class="mt-3 break-all px-3">
{{ JSON.stringify(userInfo, null, 2) }}
</view>
<view class="mt-[60vh] px-3">
<view class="m-auto w-160px text-center">
<button v-if="tokenStore.hasLogin" type="warn" class="w-full" @click="handleLogout">
退出登录
</button>
<button v-else type="primary" class="w-full" @click="handleLogin">
登录
</button>
</view>
</view>
</view>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts" setup>
defineOptions({
name: 'Moments',
})
definePage({
style: {
navigationBarTitleText: '瞬间',
},
})
</script>
<template>
<view class="mt-10 text-center text-green-500">
瞬间页面
</view>
</template>
+55
View File
@@ -0,0 +1,55 @@
# 登录 说明
## 登录 2种策略
- 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
- 默认需要登录策略: DEFAULT_NEED_LOGIN
### 默认无需登录策略: DEFAULT_NO_NEED_LOGIN
进入任何页面都不需要登录,只有进入到黑名单中的页面/或者页面中某些动作需要登录,才需要登录。
比如大部分2C的应用,美团、今日头条、抖音等,都可以直接浏览,只有点赞、评论、分享等操作或者去特殊页面(比如个人中心),才需要登录。
### 默认需要登录策略: DEFAULT_NEED_LOGIN
进入任何页面都需要登录,只有进入到白名单中的页面,才不需要登录。默认进入应用需要先去登录页。
比如大部分2B和后台管理类的应用,比如企业微信、钉钉、飞书、内部报表系统、CMS系统等,都需要登录,只有登录后,才能使用。
### EXCLUDE_LOGIN_PATH_LIST
`EXCLUDE_LOGIN_PATH_LIST` 表示排除的路由列表。
`默认无需登录策略: DEFAULT_NO_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才需要登录,相当于黑名单。
`默认需要登录策略: DEFAULT_NEED_LOGIN` 中,只有路由在 `EXCLUDE_LOGIN_PATH_LIST` 中,才不需要登录,相当于白名单。
### excludeLoginPath
definePage 中可以通过 `excludeLoginPath` 来配置路由是否需要登录。(类似过去的 needLogin 的功能)
```ts
definePage({
style: {
navigationBarTitleText: '关于',
},
// 登录授权(可选):跟以前的 needLogin 类似功能,但是同时支持黑白名单,详情请见 src/router 文件夹
excludeLoginPath: true,
// 角色授权(可选):如果需要根据角色授权,就配置这个
roleAuth: {
field: 'role',
value: 'admin',
redirect: '/pages/auth/403',
},
})
```
## 登录注册页路由
登录页 `login.vue` 对应路由是 `/pages/login/login`.
注册页 `register.vue` 对应路由是 `/pages/login/register`.
## 登录注册页适用性
登录注册页主要适用于 `h5``App`,默认不适用于 `小程序`,因为 `小程序` 通常会使用平台提供的快捷登录。
特殊情况例外,如业务需要跨平台复用登录注册页时,也可以用在 `小程序` 上,所以主要还是看业务需求。
通过一个参数 `LOGIN_PAGE_ENABLE_IN_MP` 来控制是否在 `小程序` 中使用 `H5登录页` 的登录逻辑。
+30
View File
@@ -0,0 +1,30 @@
import { getAllPages } from '@/utils'
export const LOGIN_STRATEGY_MAP = {
DEFAULT_NO_NEED_LOGIN: 0, // 黑名单策略,默认可以进入APP
DEFAULT_NEED_LOGIN: 1, // 白名单策略,默认不可以进入APP,需要强制登录
}
// TODO: 1/3 登录策略,默认使用`无需登录策略`,即默认不需要登录就可以访问
export const LOGIN_STRATEGY = LOGIN_STRATEGY_MAP.DEFAULT_NO_NEED_LOGIN
export const isNeedLoginMode = LOGIN_STRATEGY === LOGIN_STRATEGY_MAP.DEFAULT_NEED_LOGIN
export const LOGIN_PAGE = '/pages/auth/login'
export const REGISTER_PAGE = '/pages/auth/register'
export const LOGIN_PAGE_LIST = [LOGIN_PAGE, REGISTER_PAGE]
// 在 definePage 里面配置了 excludeLoginPath 的页面,功能与 EXCLUDE_LOGIN_PATH_LIST 相同
export const excludeLoginPathList = getAllPages('excludeLoginPath').map(page => page.path)
// 排除在外的列表,白名单策略指白名单列表,黑名单策略指黑名单列表
// TODO: 2/3 在 definePage 配置 excludeLoginPath,或者在下面配置 EXCLUDE_LOGIN_PATH_LIST
export const EXCLUDE_LOGIN_PATH_LIST = [
'/pages/xxx/index', // 示例值
'/pages-sub/xxx/index', // 示例值
...excludeLoginPathList, // 都是以 / 开头的 path
]
// 在小程序里面是否使用H5的登录页,默认为 false
// 如果为 true 则复用 h5 的登录逻辑
// TODO: 3/3 确定自己的登录页是否需要在小程序里面使用
export const LOGIN_PAGE_ENABLE_IN_MP = false
+145
View File
@@ -0,0 +1,145 @@
import { isMp } from '@uni-helper/uni-env'
/**
* by on 2025-08-19
*
* config.ts EXCLUDE_LOGIN_PATH_LIST
*/
import { useTokenStore } from '@/store/token'
import { isPageTabbar, tabbarStore } from '@/tabbar/store'
import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index'
import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP } from './config'
export const FG_LOG_ENABLE = false
export function judgeIsExcludePath(path: string) {
const isDev = import.meta.env.DEV
if (!isDev) {
return EXCLUDE_LOGIN_PATH_LIST.includes(path)
}
const allExcludeLoginPages = getAllPages('excludeLoginPath') // dev 环境下,需要每次都重新获取,否则新配置就不会生效
return EXCLUDE_LOGIN_PATH_LIST.includes(path) || (isDev && allExcludeLoginPages.some(page => page.path === path))
}
export const navigateToInterceptor = {
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
// 增加对相对路径的处理,BY 网友 @ideal
invoke({ url, query }: { url: string, query?: Record<string, string> }) {
if (url === undefined) {
return
}
let { path, query: _query } = parseUrlToObj(url)
FG_LOG_ENABLE && console.log('\n\n路由拦截器:-------------------------------------')
FG_LOG_ENABLE && console.log('路由拦截器 1: url->', url, ', query ->', query)
const myQuery = { ..._query, ...query }
// /pages/route-interceptor/index?name=feige&age=30
FG_LOG_ENABLE && console.log('路由拦截器 2: path->', path, ', _query ->', _query)
FG_LOG_ENABLE && console.log('路由拦截器 3: myQuery ->', myQuery)
// 处理相对路径
if (!path.startsWith('/')) {
const currentPath = getLastPage()?.route || ''
const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
path = `${baseDir}/${path}`
}
// // 处理路由不存在的情况
// if (path !== '/' && !getAllPages().some(page => page.path === path)) {
// console.warn('路由不存在:', path)
// return false // 明确表示阻止原路由继续执行
// }
// // 插件页面
// if (url.startsWith('plugin://')) {
// FG_LOG_ENABLE && console.log('路由拦截器 4: plugin:// 路径 ==>', url)
// path = url
// }
// 处理直接进入路由非首页时,tabbarIndex 不正确的问题
tabbarStore.setAutoCurIdx(path)
// 小程序里面使用平台自带的登录,则不走下面的逻辑
if (isMp && !LOGIN_PAGE_ENABLE_IN_MP) {
return true // 明确表示允许路由继续执行
}
const tokenStore = useTokenStore()
FG_LOG_ENABLE && console.log('tokenStore.hasLogin:', tokenStore.hasLogin)
// 不管黑白名单,登录了就直接去吧(但是当前不能是登录页)
if (tokenStore.hasLogin) {
if (path !== LOGIN_PAGE) {
return true // 明确表示允许路由继续执行
}
else {
console.log('已经登录,但是还在登录页', myQuery.redirect)
const url = myQuery.redirect || HOME_PAGE
if (isPageTabbar(url)) {
uni.switchTab({ url })
}
else {
uni.navigateTo({ url })
}
return false // 明确表示阻止原路由继续执行
}
}
let fullPath = path
if (Object.keys(myQuery).length) {
fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}`
}
const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}`
// #region 1/2 默认需要登录的情况(白名单策略) ---------------------------
if (isNeedLoginMode) {
// 需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示白名单,可以直接通过
if (judgeIsExcludePath(path)) {
return true // 明确表示允许路由继续执行
}
// 否则需要重定向到登录页
else {
if (path === LOGIN_PAGE) {
return true // 明确表示允许路由继续执行
}
FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
uni.navigateTo({ url: redirectUrl })
return false // 明确表示阻止原路由继续执行
}
}
// #endregion 1/2 默认需要登录的情况(白名单策略) ---------------------------
// #region 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
else {
// 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
if (judgeIsExcludePath(path)) {
FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
uni.navigateTo({ url: redirectUrl })
return false // 修改为false,阻止原路由继续执行
}
return true // 明确表示允许路由继续执行
}
// #endregion 2/2 默认不需要登录的情况(黑名单策略) ---------------------------
},
}
// 针对 chooseLocation 的特殊处理
export const chooseLocationInterceptor = {
invoke(options: any) {
// 直接放行 chooseLocation 调用
FG_LOG_ENABLE && console.log('chooseLocation 调用,直接放行:', options)
return true
},
}
export const routeInterceptor = {
install() {
uni.addInterceptor('navigateTo', navigateToInterceptor)
uni.addInterceptor('reLaunch', navigateToInterceptor)
uni.addInterceptor('redirectTo', navigateToInterceptor)
uni.addInterceptor('switchTab', navigateToInterceptor)
// 添加 chooseLocation 的拦截器,确保直接放行
uni.addInterceptor('chooseLocation', chooseLocationInterceptor)
},
}
+11
View File
@@ -0,0 +1,11 @@
import { tabbarStore } from '@/tabbar/store'
export const permission = {
install(router) {
router.beforeEach((to, from, next) => {
const path = to.path
tabbarStore.setAutoCurIdx(path)
next()
})
},
}
+6
View File
@@ -0,0 +1,6 @@
/* eslint-disable */
// @ts-ignore
export * from './types';
export * from './listAll';
export * from './info';
+14
View File
@@ -0,0 +1,14 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions_ } from '@/http/types';
import * as API from './types';
/** 用户信息 GET /user/info */
export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
return request<API.InfoUsingGetResponse>('/user/info', {
method: 'GET',
...(options || {}),
});
}
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable */
// @ts-ignore
import request from '@/http/vue-query';
import { CustomRequestOptions_ } from '@/http/types';
import * as API from './types';
/** 用户列表 GET /user/listAll */
export function listAllUsingGet({
options,
}: {
options?: CustomRequestOptions_;
}) {
return request<API.ListAllUsingGetResponse>('/user/listAll', {
method: 'GET',
...(options || {}),
});
}
+29
View File
@@ -0,0 +1,29 @@
/* eslint-disable */
// @ts-ignore
export type InfoUsingGetResponse = {
code: number;
msg: string;
data: UserItem;
};
export type InfoUsingGetResponses = {
200: InfoUsingGetResponse;
};
export type ListAllUsingGetResponse = {
code: number;
msg: string;
data: UserItem[];
};
export type ListAllUsingGetResponses = {
200: ListAllUsingGetResponse;
};
export type UserItem = {
userId: number;
username: string;
nickname: string;
avatar: string;
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+33
View File
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_图层_2" data-name="图层 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 113.39 113.39">
<defs>
<style>
.cls-1 {
fill: none;
}
.cls-2 {
fill: #d14328;
}
.cls-3 {
fill: #2c8d3a;
}
</style>
</defs>
<g id="_图层_1-2" data-name="图层 1">
<g>
<rect class="cls-1" width="113.39" height="113.39" />
<g>
<path class="cls-3"
d="M86.31,11.34H25.08c-8.14,0-14.74,6.6-14.74,14.74v61.23c0,8.14,6.6,14.74,14.74,14.74h61.23c.12,0,.24-.02,.37-.02-9.76-.2-17.64-8.18-17.64-17.99,0-.56,.03-1.12,.08-1.67H34.1c-1.57,0-2.83-1.27-2.83-2.83V32.43c0-.78,.63-1.42,1.42-1.42h9.17c.78,0,1.42,.63,1.42,1.42v36.52c0,.78,.63,1.42,1.42,1.42h22.02c.78,0,1.42-.63,1.42-1.42V32.43c0-.78,.63-1.42,1.42-1.42h9.17c.78,0,1.42,.63,1.42,1.42v34.99c2.13-.89,4.47-1.39,6.92-1.39,5.66,0,10.7,2.63,14.01,6.72V26.08c0-8.14-6.6-14.74-14.74-14.74Z" />
<g>
<path class="cls-2"
d="M87.04,68.03c-8.83,0-16.01,7.18-16.01,16.01s7.18,16.01,16.01,16.01,16.01-7.18,16.01-16.01-7.18-16.01-16.01-16.01Zm-.27,24.84h-7.2v-3h1.18v-10.48h4.58v2.81h1.42c.84,0,1.46-.16,1.88-.48s.62-.87,.62-1.64c0-.69-.25-1.17-.74-1.45s-1.19-.42-2.09-.42h-6.84v-3h7.2c2.38,0,4.15,.38,5.31,1.15,1.16,.77,1.74,1.93,1.74,3.48,0,1.71-.83,2.93-2.5,3.64,1.07,.4,1.87,.95,2.39,1.65s.79,1.56,.79,2.58c0,3.44-2.58,5.16-7.73,5.16Z" />
<path class="cls-2"
d="M86.49,85.17h-1.16v4.7h1.8c.81,0,1.46-.18,1.94-.55s.72-.95,.72-1.73c0-.86-.25-1.48-.74-1.85s-1.35-.56-2.56-.56Z" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1762219859937" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8816" id="mx_n_1762219859938" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" p-id="8817"></path><path d="M517.6 351.3c53 0 89 33.8 93 83.4 0.3 4.2 3.8 7.4 8 7.4h56.7c2.6 0 4.7-2.1 4.7-4.7 0-86.7-68.4-147.4-162.7-147.4C407.4 290 344 364.2 344 486.8v52.3C344 660.8 407.4 734 517.3 734c94 0 162.7-58.8 162.7-141.4 0-2.6-2.1-4.7-4.7-4.7h-56.8c-4.2 0-7.6 3.2-8 7.3-4.2 46.1-40.1 77.8-93 77.8-65.3 0-102.1-47.9-102.1-133.6v-52.6c0.1-87 37-135.5 102.2-135.5z" p-id="8818"></path></svg>

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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