chore: 清理旧文件并重构项目基础结构
- 删除大量废弃的旧代码、依赖和配置文件 - 新增基础项目配置、工具函数、页面模板和请求库 - 重构目录结构,统一项目规范 - 添加commitlint、husky等代码规范工具
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }, // 用于切换请求地址
|
||||
})
|
||||
}
|
||||
@@ -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' })
|
||||
}
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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}`, {})
|
||||
}
|
||||
@@ -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(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 文章id,metadata.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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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()',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
import uCharts from '@qiun/ucharts'
|
||||
|
||||
export default uCharts
|
||||
@@ -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,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAUz0lEQVR4Xu1de3Bc1X3+zmp3jYVWfkPAgCWwY8zLEglNQCSW0yT9o2SQaDKdNulUykwfM+k09p+J3ImYIPJXi9yZzDSZZiwyaZuZBCwnNG1DMogEmUAAy7xs/MAyNhCMjWWtsK1deU/n23OvtLu6j3Pv3t29d71nxjOSde455/5+3/m9z7kCjRY4BR7eK7fkcmhrasLT37hbTAY+QYADigDHagwFYGhc7gZwHMAUgG4hMPzNe8RoWInTAECAnHl4r+yREt0DXWIbhx3cJ5fHP8TYjntFR4DTBDqUIwBmMrJDCtyPHDoQw0Q8jkeXinCLtECp43Gwh56R22IxTBbu+KFxOTbQJbo9DlW17rYASGdlNySeKl2JADpbkmKiait0mWhoXHZkmzE52CkocmvavvOsbMvl8MhAl+jlQrg2CQzu6BI9NV2Yw+T2AJiVo+DuL2kSeLQ1KfrC8kLcYbkYBv/pbjEWhjUNjUvSpk9KSAicm2tGXxjAaUcbewBkJAm6xeLBp1PJ2os06ttcDl8H0CEEaGnvGegSg2EAQZTW4B0AEntSS2ov0mhgJc5jmwT6IDEWi2E0zNZ2WEFhC4CZjCRxH7GwAfpbkmIkLC9EFQBg20BXeOySsNBGZx2OXkB6Vg5CgAbMZgD7BTDSkhTDOgM3+kSDAr7iANNz8n4hQRdxojUu9kTjVRurtKKAJwBIKZfPZPOuYWFgY6wlgV4hau+GNVjsnQKeAJDOSIp/Wt6lbSKeQG8jSOSdAbV+wisA7FxDSGAqBmwNS5DIiGIucyNwKiGedutTz3/3BgCb4JBJoGqDIJ2VW4REmxRog0S3lGgT/NlfY3RzCgJjQmJSCkxeDuDwBgCb8HAhvQkCIdCbSgQfmSPDmWGDzHsm1UqwjBEUAMbqERCeAEBGz2RknwR2uW0yAZQdL6DR+WEW90syXLmjtW8So0Jg9MoE9tSD4esZANUAAd1M5NAjGOULaaOkAzCKGEaj7Ar7AoBHEGxrSYqdOnxMZ+W3ZA59ZehxnWkC7yMlJkUMIy1x7IyaVPANAAMEHTlgTACO1rYERlqTot+J8nbp58C5VcEBDftnOEpAKAsAgYPAPgNZQbYFP3QeCAybJ/Bg2CVC2QDwCoJUAtudiJKuExCQLoZbPKirAoOHovuIThVByuXii2jE/C9I2TaXBYsfmThyahMtCWy1A4ERbj7rvvRI9aCa3F7pINm3n5XdXgtjFgHAYCQrW4v8bBo6MYFep5cwmEefuSwQpDNSRoq9+osdrqRaGBqXMhfDVi8gWASAdEbuswuyGCKNSLatBygXBHUqAQohMmHESAKrqzSro4TIS2yOq10dVQQAuyKQUoC7BXnIxHQWwwL4ay/qIM/8DHaFJuijv7M99QzaNmAx6hzQFsvhKSmxvakJo7oHUooA4MUA0wHBTDYfQnVUB6bFnLc1JHqiFgPwxPnSzhKjLUn0B+UpsDoqFkOfLvO5HN8AMN5lOJUU2+2IMD0ne0QOtCcq0k7OANe1VGToag7qaBRXeiFFAJjOyBENsV20Jqcgj2FQHgvyJWYvAQfPAJuvAv7198ADm4DMHJBKAmuag5ypemPpGNiVWk2pDcCDDDQCPTU7EOgmjrxMRgA8dgBYmwJOXwBuWgH87m3gz26OLgDy6q9G9RSLvIAymFZUGsaCjJzE7qB1+vvngXRGQebG5QB/P30eaF2iQBHllk8wxdDfGq/eYVLLQJBfEOQNOpk3/Bg86hbA8iAZwt2/a78asX8zsKRJ/fzYQeDttFIHUbcJqi0JnM4FaOX9g2Sw7lgHTgPTs0DHRxTjT5wDtqzTfTr8/aoJArfTwX055P1519q6apGV4v8/XlU6nzv/vo8CvzwK3L0W2LS6Wquo/DzVAoFrMiivyzVSvpUnycIMVAUK2kb28Qm77nn2wQXGRg5uGvEw1AtjaHkpMx/Ro/fqfBliBsOeUbl3fu3lTKko7X/J8vJJOtFuxV/jXKo3Bsna4AI6kbe4BrlTyrYIqsUMi2R1ti0DIxPpT8QnEF5SxQmW8wo5oM5QF9+fLkP4xlcGp2QCaos8VY77yMEK7KOjZ4U7fuN3ShXcTBSbL0BtoHD2vc+JYrS4Ox8yBfD6J/5cX9nB5+5TvapXmh+jQuU9E2Ez0QdgKgu3AfvuXv7RnAu+Nf1c5a2Z6gkGA3xauyPp1yq16rWR/9P/bE5fvXVe06fJv9yxbqI+ZmW4aCPluDbJ1rK8mYq1wEMTEkF4s5Agqo8qCXG/k9TyWqSwfXyT2gKrHjd8gZ3vC62V1B1l6F76qZ1I7nqO1xqCo2OR7c8w9a3kFhkyo5d9h9al+2wzN2t9tPPS3Mntf7Qb2oZqtdpaaf/67XtPZq95fi1mGZm0mo6OaTg4T2TTpJQWPbNly7T01AH4Kp++qmAm3mYwbQ0v0Y4tFQ6T6tq18p2dml/BzV1F/1qxhLCFTbXGxHzS5y9fTg9ys+fxiqy1M4k7xFrXkHVI+6Y6vyXxfkJr3DLkpd3xATVtYkQh2nK9oX7n7NsA+QaVfdnXknGTjrTy9UQy4bV99fPHad0M9RSoO0VgsBKW5Dcy/py99Yd5B1t5FQJfUwvtV0frZGYvFS5Xgh8D8aXc0twT/PBVI3Td2Jp8y1OFfn1KWS7Pj7RagzphcOORcmyifp3YAjc4KJ/eXoIgkBPR7cQXJs8oPb5tyHoVFH1rmiLbt8tZAGt2xR0sQ7R8S0y5t4IF5m8Jg2dR0bXo1Xk8b7dVyUPrTbg1XG7P72Q5iP2p9o1IN+H1e0ulnI7VdWF3a3rfcJq0ZlD8YxpQ0+YrWDR5pD2+9f1bd9JrVkqW7Kk2Wya4QGZP69pUuA70C6j6ddN3E9u7ZlB6w0kI7J55H1pOJ9oAL9GXh4Xnlw7m6qXb2Q8tH3HpgmQWw2A6iH1H2t3M9GZ5zP/2QAytnCRe4TtUffCzPKP1yxW3f0TCJp8i1eYg0pN4h9Qxg+3Zt66I5kYt8dYFM9l6D6GmCqYdPf1okhfO4J3DH9Z2oP4fdpCH0dH1scQ6y9Q0Y2V7+fC9V8qf4c1g3D7m1r5Md2O3q36k7s4p37m6wR/1nsZrV1pgrRmtV2R2k4zJH4AOGi7FQ9gCq7soCw2vIQsmW2qkr7gWXxOAYo5ti8iM0W9nJ67KTYX4K1lNg45XxKfCr5T3jG2w6FqH7h2t9G0lECK7jYY5V0+Up7M+ZCv9DgZ7g2gZ8BYzRQAd2z7L4qf3vbK5kX8+ZyQgK5nB0x0S9W4E1AAeYsMzgO0aG/EDoBUE4bq+nrQfW91bD1K8oE0nQ3VZo3f8bWlPNkS3jP/2yuj+0VwYwU6g2Fgnb8IkC4jk9nLzX9WzTHDkK+oUGd5G4Z4A13rGxf0pEW8yXx0Jsb3AuhQ/3oC6/2t9aU3Tt2Z6b+5OTQ3PLmF4q9VdrC0UsQxOfwG1GQapdwv0tXh4m3Rm9V94+9NfyvA5tdt+9fK2bD1Jp3HO/2rR2u9UR3D5scv5Otxm4y7Y6gG1yXtw1jYxGDHqg2kA1dvvRLR1yYc1TBx5wppNVpjHnQYF7Qn9YJg3lItRz8aQd6nH8p7L5m3gHBK3d4t1e5g2RCdU9ZEn1WwG+it2nNCgaD2yKRGFeP0f6a3p1bF7v4gy2WzFQpJ5Km4qZ8+Kn3KX1Ih1nJ8jB8V6bF4bFhzQy8e8n8vcvV/TH0+cy+YpC9C8q1I9m2/OVrBkVxtm0C3D2t8qoK3nG7Tiiq7uxA1AH7e6S2hZ3gxp5w3q9Y7ZxY9kzK4X5UQwPzgw5OBN55c1juC3B3iGgKsd0hsyV1e14L01/2WwGsfBz8j/6nA6x1C2e9V5P7v1SgE3bXg0bvlqQq3bMreH6kRCpH2F2MC3m2jMNX/ClqffTQ5uMfru9T0rIY5Vq6mFYvdV0x0dXpn+2KCPqHSM8e7+Zb2d6EoS+q9lI2iAI9z+8p8kqGmghjJPb1A6CtE8Pz0j4+2fE2cQyVQ3pHkV0dYwX/4T1l5a8y6n9ws1B7r9ghg4Y/8rWgKq5b9+4YqDk1vF3/8s23aYx2ox5p5fZ+5pZf6g+rB/Ki3feh4iE1sY5S6fiPS7xE3q+7dZPpxbM0ZRU9dtL1Jt9az3N+fYV9UoTRjQHI5OL0zhXf8yI0xvV7KZkVt2I3m1sYkl/YN8x1o0b6p7v0m0vVw6u3rF7VfG7lB7S7aXjVv7x5O3Z7U4wxg49nFl1bGqYy+G5q8vJklSgM6Aq8t9yPqxHqt3pK+rT79Vyoq6aR1muVv8bN9u1zGqcfL6G3xu4j0M3GzVQn1iFS5k8z1K3mS8C2d3Y6b8t/xqGakgs5cBh6z1Cbm+45aKn8c4uh4Y3g+whpY1dPq9GQnO8GKB2c3X0Z9g2z+X7rZ2p+Il3tO5s+tygU0b8f9H0jNWuWcXRg1WbQbrmK3w5xA1jV3I1pYp1PzB67b64O7J3ftmM9OT6kjhH10jHFTOw+tjf0Gm7xgH4+4Rz2fG3WjpEOOxX9tD4qBqMLLIPIOoSGKXkIu8QeDJL0kS+0b2bdVxFz6j0d+F8Y1k6I5C8seBqP+0AL7xbMjbkq5vG8Qhzf98QvDg6rw8EwIQ0bB7uC9j7b2zV6fF9ttH4r6yG8sM0jqzj8ZC4J/T5iP6+0lLykYz1ejQCdMfAsD2P3FasFz3L3dZ5rD7K0x2T4pH0vQ0u8N1aW8S5M0z/4R8sWfoHnQ7T4M7FJ7k+v2o7YzQj0Y6c8rj1t0BP9Yc0u7X9i/8CKR+KpV6FMuIQAAAABJRU5ErkJggg==');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100%;
|
||||
background-position: center;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
@@ -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 后变 false,data 更新为返回值', 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')
|
||||
})
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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` 状态,在列表底部显示不同的提示信息,提升用户体验。
|
||||
|
||||
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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号)。
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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},请检查网络或联系管理员!`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
|
||||
* 支持基本类型值和数组,不支持嵌套对象
|
||||
* @param obj 要序列化的对象
|
||||
* @returns 序列化后的查询字符串
|
||||
*/
|
||||
export function stringifyQuery(obj: Record<string, any>): string {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
||||
return ''
|
||||
|
||||
return Object.entries(obj)
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => {
|
||||
// 对键进行编码
|
||||
const encodedKey = encodeURIComponent(key)
|
||||
|
||||
// 处理数组类型
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.filter(item => item !== undefined && item !== null)
|
||||
.map(item => `${encodedKey}=${encodeURIComponent(item)}`)
|
||||
.join('&')
|
||||
}
|
||||
|
||||
// 处理基本类型
|
||||
return `${encodedKey}=${encodeURIComponent(value)}`
|
||||
})
|
||||
.join('&')
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
@@ -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) 最下面的注意事项
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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 多语言的key,eg: "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
|
||||
@@ -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}"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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` 里面,默认会在登录后自动重定向到来源/配置的页面。
|
||||
|
||||
如果与您的业务不符,您可以自行修改。
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
// 使用storeToRefs解构userInfo
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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登录页` 的登录逻辑。
|
||||
@@ -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
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
export * from './types';
|
||||
|
||||
export * from './listAll';
|
||||
export * from './info';
|
||||
@@ -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 || {}),
|
||||
});
|
||||
}
|
||||
@@ -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 || {}),
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 780 B |
|
After Width: | Height: | Size: 985 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 560 B |
|
After Width: | Height: | Size: 13 KiB |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |