!3 双 token 机制,整体布局

Merge pull request !3 from 芋道源码/develop
This commit is contained in:
芋道源码 2024-10-14 12:17:41 +00:00 committed by Gitee
commit 050553a0e8
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
44 changed files with 1325 additions and 277 deletions

20
env/.env vendored
View File

@ -1,15 +1,25 @@
VITE_APP_TITLE = 'unibest' VITE_APP_TITLE = '芋道管理系统'
VITE_APP_PORT = 9000 VITE_APP_PORT = 9000
VITE_UNI_APPID = 'H57F2ACE4' VITE_UNI_APPID = 'H57F2ACE4'
VITE_WX_APPID = 'wxa2abb91f64032a2b' VITE_WX_APPID = 'wx90bcb2127c1d8720'
# h5部署网站的base配置到 manifest.config.ts 里的 h5.router.base # h5部署网站的base配置到 manifest.config.ts 里的 h5.router.base
VITE_APP_PUBLIC_BASE=/unibest/ VITE_APP_PUBLIC_BASE=/unibest/
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run' VITE_SERVER_BASEURL = 'http://localhost:48080/admin-api'
VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload' VITE_UPLOAD_BASEURL = 'http://localhost:48080/upload'
# h5是否需要配置代理 # h5是否需要配置代理
VITE_APP_PROXY=false VITE_APP_PROXY=false
VITE_APP_PROXY_PREFIX = '/api' VITE_APP_PROXY_PREFIX = '/admin-api'
# 租户开关
VITE_APP_TENANT_ENABLE=true
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=true
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123

View File

@ -107,6 +107,7 @@
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.20.2",
"@iconify-json/carbon": "^1.1.35", "@iconify-json/carbon": "^1.1.35",
"@iconify-json/ic": "^1.2.1",
"@rollup/rollup-darwin-x64": "^4.18.0", "@rollup/rollup-darwin-x64": "^4.18.0",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"@types/wechat-miniprogram": "^3.4.7", "@types/wechat-miniprogram": "^3.4.7",

View File

@ -30,28 +30,34 @@ export default defineUniPages({
list: [ list: [
// 注意tabbar路由需要使用 layout:tabbar 布局 // 注意tabbar路由需要使用 layout:tabbar 布局
{ {
pagePath: 'pages/index/index', pagePath: 'pages/message/index',
text: '首页', text: '消息',
icon: 'home', icon: 'i-ic-outline-message',
iconType: 'wot', iconType: 'unocss',
}, },
{ {
pagePath: 'pages/about/about', pagePath: 'pages/colab/index',
text: '关于', text: '协作',
icon: 'i-carbon-code', icon: 'i-ic-outline-handshake',
iconType: 'unocss',
},
{
pagePath: 'pages/work/index',
text: '工作台',
icon: 'i-ic-baseline-apps',
iconType: 'unocss',
},
{
pagePath: 'pages/contacts/index',
text: '通讯录',
icon: 'i-ic-baseline-contact-page',
iconType: 'unocss', iconType: 'unocss',
}, },
// {
// pagePath: 'pages/my/index',
// text: '我的',
// icon: '/static/logo.svg',
// iconType: 'local',
// },
{ {
pagePath: 'pages/my/index', pagePath: 'pages/my/index',
text: '我的', text: '我的',
icon: 'iconfont icon-my', icon: 'i-ic-baseline-person',
iconType: 'iconfont', iconType: 'unocss',
}, },
], ],
}, },

10
pnpm-lock.yaml generated
View File

@ -78,6 +78,9 @@ importers:
'@iconify-json/carbon': '@iconify-json/carbon':
specifier: ^1.1.35 specifier: ^1.1.35
version: 1.1.35 version: 1.1.35
'@iconify-json/ic':
specifier: ^1.2.1
version: 1.2.1
'@rollup/rollup-darwin-x64': '@rollup/rollup-darwin-x64':
specifier: ^4.18.0 specifier: ^4.18.0
version: 4.18.0 version: 4.18.0
@ -1185,6 +1188,9 @@ packages:
'@iconify-json/carbon@1.1.35': '@iconify-json/carbon@1.1.35':
resolution: {integrity: sha512-zKqioWceqFRiLJvxpjcCpVP3j2YcokYshlbwSAHBhOih5XNUymUS3hm1kpV4KljMI1xWH96UcozHaaf6x4YzdA==} resolution: {integrity: sha512-zKqioWceqFRiLJvxpjcCpVP3j2YcokYshlbwSAHBhOih5XNUymUS3hm1kpV4KljMI1xWH96UcozHaaf6x4YzdA==}
'@iconify-json/ic@1.2.1':
resolution: {integrity: sha512-UjL/bjJP/T5EV881+hTzcfTKVo0KEUjhnMiJcLtPzNgPtU2KZZmRx8BSKKR61H4CN/5FTEbyawGyG0aEt3SwGQ==}
'@iconify/types@2.0.0': '@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@ -6887,6 +6893,10 @@ snapshots:
dependencies: dependencies:
'@iconify/types': 2.0.0 '@iconify/types': 2.0.0
'@iconify-json/ic@1.2.1':
dependencies:
'@iconify/types': 2.0.0
'@iconify/types@2.0.0': {} '@iconify/types@2.0.0': {}
'@iconify/utils@2.1.24': '@iconify/utils@2.1.24':

View File

@ -13,6 +13,10 @@ onHide(() => {
</script> </script>
<style lang="scss"> <style lang="scss">
page {
//
background: #f2f3f7;
}
/* stylelint-disable selector-type-no-unknown */ /* stylelint-disable selector-type-no-unknown */
button::after { button::after {
border: none; border: none;

View File

@ -36,7 +36,7 @@
<script setup lang="ts"> <script setup lang="ts">
// unocss icon // unocss icon
// i-carbon-code // i-ic-outline-message i-ic-outline-handshake i-ic-baseline-apps i-ic-baseline-contact-page i-ic-baseline-person
import { tabBar } from '@/pages.json' import { tabBar } from '@/pages.json'
import { tabbarStore } from './tabbar' import { tabbarStore } from './tabbar'

View File

@ -1,8 +1,11 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import qs from 'qs' import qs from 'qs'
import { useUserStore } from '@/store'
import { platform } from '@/utils/platform' import { platform } from '@/utils/platform'
import { getEvnBaseUrl } from '@/utils' import { getEvnBaseUrl } from '@/utils'
import { getAccessToken, getTenantId } from '@/utils/auth'
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
const whiteList: string[] = ['/login', '/refresh-token', '/system/tenant/get-id-by-name']
export type CustomRequestOptions = UniApp.RequestOptions & { export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any> query?: Record<string, any>
@ -50,10 +53,24 @@ const httpInterceptor = {
...options.header, ...options.header,
} }
// 3. 添加 token 请求头标识 // 3. 添加 token 请求头标识
const userStore = useUserStore() // const userStore = useUserStore()
const { token } = userStore.userInfo as unknown as IUserInfo // const { token } = userStore.userInfo as unknown as IUserInfo
if (token) { // if (token) {
options.header.Authorization = `Bearer ${token}` // options.header.Authorization = `Bearer ${token}`
// }
let isToken = (options!.header || {}).isToken === false
isToken = whiteList.some((allowUrl) => options!.url.indexOf(allowUrl) !== -1)
if (!isToken && getAccessToken()) {
// 能够获取的到accessToken并且不是白名单
options.header.Authorization = `Bearer ${getAccessToken()}`
}
// 4. 添加租户标识
if (tenantEnable && tenantEnable === 'true') {
const tenantId = getTenantId()
if (tenantId) options.header['tenant-id'] = tenantId
} }
}, },
} }

View File

@ -3,24 +3,46 @@
* *
* *
* 便使 * 便使
*
* update 2024-10-09
*
* userStore
*
*
* router
*
* <route lang="json5">
{
layout: 'tabbar',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '首页',
},
needLogin: true
}
</route>
*/ */
import { useUserStore } from '@/store' import { useUserStore } from '@/store'
import { getNeedLoginPages, needLoginPages as _needLoginPages } from '@/utils' import { getNeedLoginPages, needLoginPages as _needLoginPages } from '@/utils'
import { getAccessToken } from '@/utils/auth'
// TODO Check // TODO Check
const loginRoute = '/pages/login/index' const loginRoute = '/pages/login/index'
const isLogined = () => { // const isSetUserInfo = () => {
const userStore = useUserStore() // const userStore = useUserStore()
return userStore.isLogined // return userStore.userInfo.isSetUser
} // }
const isDev = import.meta.env.DEV const isDev = import.meta.env.DEV
// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录) // 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
const navigateToInterceptor = { const navigateToInterceptor = {
// 注意这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同 // 注意这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
invoke({ url }: { url: string }) { // 拦截前触发
async invoke(option) {
const { url } = option
console.log('navigateToInterceptor invoke')
// console.log(url) // /pages/route-interceptor/index?name=feige&age=30 // console.log(url) // /pages/route-interceptor/index?name=feige&age=30
const path = url.split('?')[0] const path = url.split('?')[0]
let needLoginPages: string[] = [] let needLoginPages: string[] = []
@ -31,16 +53,36 @@ const navigateToInterceptor = {
needLoginPages = _needLoginPages needLoginPages = _needLoginPages
} }
const isNeedLogin = needLoginPages.includes(path) const isNeedLogin = needLoginPages.includes(path)
// console.log('the path: {} is needLogin? {}', path, isNeedLogin)
if (!isNeedLogin) { if (!isNeedLogin) {
return true // 当前页面不需要登录
console.log('the path: {} is not needLogin', url)
return option
} }
const hasLogin = isLogined()
if (hasLogin) { // 下面的逻辑跟 PC 端差不多
return true if (getAccessToken()) {
if (path === '/pages/login/index') {
// 这里写死,避免在 login 页面中设置了 needLogin导致死循环
return option
} else {
// todo 获取字典数据并保存到本地
// 获取用户的相关信息
const userStore = useUserStore()
if (!userStore.userInfo.isSetUser) {
// todo 显示加载状态
await userStore.setUserInfoAction()
// todo 菜单权限应该也需从这里进行逻辑上的设置
} else {
return option
}
}
} else {
// 当前页面需要登录但是获取不到token这个时候跳转到登录页面
uni.navigateTo({ url: '/pages/login/index' })
return option
} }
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
uni.navigateTo({ url: redirectRoute })
return false
}, },
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "unibest", "name": "芋道管理系统",
"appid": "H57F2ACE4", "appid": "H57F2ACE4",
"description": "", "description": "",
"versionName": "1.0.0", "versionName": "1.0.0",
@ -83,7 +83,7 @@
}, },
"quickapp": {}, "quickapp": {},
"mp-weixin": { "mp-weixin": {
"appid": "wxa2abb91f64032a2b", "appid": "wx90bcb2127c1d8720",
"setting": { "setting": {
"urlCheck": false "urlCheck": false
}, },

View File

@ -25,41 +25,76 @@
"spacing": "3px", "spacing": "3px",
"list": [ "list": [
{ {
"pagePath": "pages/index/index", "pagePath": "pages/message/index",
"text": "首页", "text": "消息",
"icon": "home", "icon": "i-ic-outline-message",
"iconType": "wot" "iconType": "unocss"
}, },
{ {
"pagePath": "pages/about/about", "pagePath": "pages/colab/index",
"text": "关于", "text": "协作",
"icon": "i-carbon-code", "icon": "i-ic-outline-handshake",
"iconType": "unocss"
},
{
"pagePath": "pages/work/index",
"text": "工作台",
"icon": "i-ic-baseline-apps",
"iconType": "unocss"
},
{
"pagePath": "pages/contacts/index",
"text": "通讯录",
"icon": "i-ic-baseline-contact-page",
"iconType": "unocss" "iconType": "unocss"
}, },
{ {
"pagePath": "pages/my/index", "pagePath": "pages/my/index",
"text": "我的", "text": "我的",
"icon": "iconfont icon-my", "icon": "i-ic-baseline-person",
"iconType": "iconfont" "iconType": "unocss"
} }
] ]
}, },
"pages": [ "pages": [
{ {
"path": "pages/index/index", "path": "pages/work/index",
"type": "home", "type": "home",
"layout": "tabbar", "layout": "tabbar",
"style": { "style": {
"navigationStyle": "custom", "navigationBarTitleText": "工作台"
"navigationBarTitleText": "首页"
} }
}, },
{ {
"path": "pages/about/about", "path": "pages/colab/index",
"type": "page", "type": "page",
"layout": "tabbar", "layout": "tabbar",
"style": { "style": {
"navigationBarTitleText": "关于" "navigationBarTitleText": "协作"
}
},
{
"path": "pages/contacts/index",
"type": "page",
"layout": "tabbar",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "通讯录"
}
},
{
"path": "pages/login/index",
"type": "page",
"style": {
"navigationBarTitleText": "登录页面"
}
},
{
"path": "pages/message/index",
"type": "page",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "消息"
} }
}, },
{ {

View File

@ -1,37 +0,0 @@
<route lang="json5">
{
layout: 'tabbar',
style: {
navigationBarTitleText: '关于',
},
}
</route>
<template>
<view
class="bg-white overflow-hidden pt-2 px-4"
:style="{ marginTop: safeAreaInsets?.top + 'px' }"
>
<view class="text-center text-3xl mt-8">
鸽友们好我是
<text class="text-red-500">菲鸽</text>
</view>
<RequestComp />
<UploadComp />
</view>
</template>
<script lang="ts" setup>
import RequestComp from './components/request.vue'
import UploadComp from './components/upload.vue'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
</script>
<style lang="scss" scoped>
.test-css {
// mt-4=>1rem=>16px;
margin-top: 16px;
}
</style>

View File

@ -1,56 +0,0 @@
<route lang="json5">
{
layout: 'demo',
style: {
navigationBarTitleText: '请求',
},
}
</route>
<template>
<view class="p-6 text-center">
<view class="my-2">使用的是 laf 云后台</view>
<view class="text-green-400">我的推荐码可以获得佣金</view>
<!-- #ifdef H5 -->
<view class="my-2">
<a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="my-2 text-left text-sm">{{ recommendUrl }}</view>
<!-- #endif -->
<!-- http://localhost:9000/#/pages/index/request -->
<wd-button @click="run" class="my-6">发送请求</wd-button>
<view class="h-12">
<view v-if="loading">loading...</view>
<block v-else>
<view class="text-xl">请求数据如下</view>
<view class="text-green leading-8">{{ JSON.stringify(data) }}</view>
</block>
</view>
<wd-button type="error" @click="reset" class="my-6" :disabled="!data">重置数据</wd-button>
</view>
</template>
<script lang="ts" setup>
import { getFooAPI, postFooAPI, IFooItem } from '@/service/index/foo'
const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
// const initialData = {
// name: 'initialData',
// id: '1234',
// }
const initialData = undefined
// Service
const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
immediate: true,
initialData,
})
const reset = () => {
data.value = initialData
}
</script>

View File

@ -1,30 +0,0 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '上传-状态一体化',
},
}
</route>
<template>
<view class="p-4 text-center">
<wd-button @click="run">选择图片并上传</wd-button>
<view v-if="loading" class="text-blue h-10">上传...</view>
<template v-else>
<view class="m-2">上传后返回的接口数据</view>
<view class="m-2">{{ data }}</view>
<view class="h-80 w-full">
<image v-if="data" :src="data || data" mode="scaleToFill" />
</view>
</template>
</view>
</template>
<script lang="ts" setup>
const { loading, data, run } = useUpload({ user: '菲鸽' })
</script>
<style lang="scss" scoped>
//
</style>

23
src/pages/colab/index.vue Normal file
View File

@ -0,0 +1,23 @@
<!-- 协作界面用于 bpm 工作流 -->
<route lang="json5" type="page">
{
layout: 'tabbar',
style: {
navigationBarTitleText: '协作',
},
}
</route>
<template>
<view class="h-full w-full flex justify-center items-center">
<wd-status-tip image="/static/images/empty.png" tip="功能暂未开放" />
</view>
</template>
<script lang="ts" setup>
//
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,41 @@
<!-- 通讯录 item -->
<template>
<view class="flex justify-between w-full">
<!-- 头像 -->
<view class="flex justify-center items-center w-100rpx">
<image
class="w-80rpx h-80rpx rounded-5px overflow-hidden"
:src="props.item.avatar ? props.item.avatar : '/static/images/contacts.png'"
mode="scaleToFill"
/>
</view>
<!-- 右侧信息 -->
<view
class="h-100rpx grow-1 flex flex-col justify-center border-solid border-b-[0.5px] border-x-0 border-t-0 border-[#E3E2E2]"
>
<!-- 如果是个人 -->
<view v-if="props.item.isLeaf">
<view class="text-24rpx">{{ props.item.name }}</view>
<view class="text-22rpx">
{{ props.item.post }}
</view>
</view>
<view v-else>
<view class="text-24rpx">
{{ props.item.name }}
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
//
import { defineProps } from 'vue'
const props = defineProps(['item'])
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,114 @@
<!-- 通讯录界面用于 im 聊天 -->
<route lang="json5" type="page">
{
layout: 'tabbar',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '通讯录',
},
}
</route>
<template>
<!-- 全部的背景 -->
<view class="">
<!-- 安全位置插入 -->
<view class="overflow-hidden" :style="{ marginTop: safeAreaInsets?.top + 'px' }">
<!-- 自定义顶部的导航栏 -->
<view class="h-80rpx flex justify-between items-center px-2 bg-[#F8F8F8]">
<view class="w-50rpx">
<view
class="i-ic-baseline-keyboard-arrow-left"
v-show="historyStack.length > 1"
@click="handleLeftArrow"
></view>
</view>
<view class="text-22rpx grow-1 text-center">通讯录</view>
<view class="w-50rpx"></view>
</view>
<!-- 自定义间距 -->
<view class="bg-[#f2f3f7] h-2 w-full"></view>
<!-- 消息列表 -->
<view class="flex flex-col bg-white p-2">
<view v-for="(item, index) in treeList" :key="index" @click="handleClick(item)">
<ContactsItem :item="item" />
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
//
import { onLoad } from '@dcloudio/uni-app'
import { useToast } from 'wot-design-uni'
import ContactsItem from './components/ContactsItem.vue'
import contactsTree from './mock'
//
const toast = useToast()
defineOptions({
name: 'Contacts',
})
//
const { safeAreaInsets } = uni.getSystemInfoSync()
//
const searchTree = (tree, targetId) => {
console.log('targetId', targetId)
console.log('tree', tree)
if (!targetId) {
return tree
}
let queue = [...tree]
let result = []
while (queue.length) {
const node = queue.shift()
if (node.id === targetId) {
result = node.children
break
}
if (node.children) {
queue = queue.concat(node.children)
}
}
return result
}
const treeList = ref({})
const handleClick = (item) => {
console.log('展示当前的item', item.id)
if (item.isLeaf) {
toast.info('功能尚未开放')
} else {
historyStack.value.push(item.id)
treeList.value = searchTree(contactsTree, item.id)
}
}
const handleLeftArrow = () => {
if (historyStack.value.length > 1) {
historyStack.value.pop()
}
const id = historyStack.value.at(-1)
treeList.value = searchTree(contactsTree, id)
}
//
const historyStack = ref([])
onLoad((_option) => {
//
historyStack.value.push(undefined)
treeList.value = searchTree(contactsTree, undefined)
})
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,70 @@
export default [
{
id: 1,
name: '芋道集团',
isLeaf: false,
children: [
{
id: 11,
parentId: 1,
name: 'IT中心',
isLeaf: false,
children: [
{
id: 111,
parentId: 11,
name: '马青楷',
post: 'CTO',
avatar: '/static/images/avatar3.jpg',
isLeaf: true,
},
{
id: 112,
parentId: 11,
name: '王伟',
post: '前端开发',
avatar: '/static/images/avatar2.jpg',
isLeaf: true,
},
{
id: 113,
parentId: 11,
name: '王博',
post: '技术主管',
avatar: '/static/images/avatar1.jpg',
isLeaf: true,
},
],
},
{
id: 12,
parentId: 1,
name: '行政部门',
isLeaf: true,
},
],
},
{
id: 2,
name: '中软集团',
isLeaf: false,
children: [
{
id: 21,
parentId: 2,
name: '中软IT',
isLeaf: false,
children: [
{
id: 211,
parentId: 21,
name: '马青楷',
post: 'CTO',
isLeaf: true,
avatar: '/static/images/avatar3.jpg',
},
],
},
],
},
]

View File

@ -1,57 +0,0 @@
<!-- 使用 type="home" 属性设置首页其他页面不需要设置默认为page推荐使用json5更强大且允许注释 -->
<route lang="json5" type="home">
{
layout: 'tabbar',
style: {
navigationStyle: 'custom',
navigationBarTitleText: '首页',
},
}
</route>
<template>
<view
class="bg-white overflow-hidden pt-2 px-4"
:style="{ marginTop: safeAreaInsets?.top + 'px' }"
>
<view class="mt-12">
<image src="/static/logo.svg" alt="" class="w-28 h-28 block mx-auto" />
</view>
<view class="text-center text-4xl main-title-color mt-4">unibest</view>
<view class="text-center text-2xl mt-2 mb-8">最好用的 uniapp 开发模板</view>
<view class="text-justify max-w-100 m-auto text-4 indent mb-2">{{ description }}</view>
<view class="text-center mt-8">
当前平台是
<text class="text-green-500">{{ PLATFORM.platform }}</text>
</view>
<view class="text-center mt-4">
模板分支是
<text class="text-green-500">tabbar</text>
</view>
</view>
</template>
<script lang="ts" setup>
import PLATFORM from '@/utils/platform'
defineOptions({
name: 'Home',
})
//
const { safeAreaInsets } = uni.getSystemInfoSync()
const author = ref('菲鸽')
const description = ref(
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite4 + UnoCss + UniUI + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
)
// uni API
onLoad(() => {
console.log(author)
})
</script>
<style>
.main-title-color {
color: #d14328;
}
</style>

View File

@ -0,0 +1,146 @@
<!-- 账号密码登录 -->
<template>
<view class="mt-4">
<view>
<wd-form ref="gennerateForm" :model="loginData.loginForm" class="">
<view class="flex flex-col items-center gap-4">
<view
class="rounded-xl p-10rpx border-2rpx border-blue-400 border-solid"
:class="{
'border-4rpx border-blue-500 p-8rpx': focusInput === 'tenantName',
}"
>
<wd-input
v-if="loginData.tenantEnable"
type="text"
placeholder="请输入租户名称"
v-model="loginData.loginForm.tenantName"
custom-class="w-620rpx"
:no-border="true"
@focus="() => handleInputFocus('tenantName')"
/>
</view>
<view
class="rounded-xl p-10rpx border-2rpx border-blue-400 border-solid"
:class="{
'border-4rpx border-blue-500 p-8rpx': focusInput === 'username',
}"
>
<wd-input
type="text"
v-model="loginData.loginForm.username"
placeholder="请输入账号"
custom-class="w-620rpx"
:no-border="true"
@focus="() => handleInputFocus('username')"
/>
</view>
<view
class="rounded-xl p-10rpx border-2rpx border-blue-400 border-solid"
:class="{
'border-4rpx border-blue-500 p-8rpx': focusInput === 'password',
}"
>
<wd-input
type="password"
v-model="loginData.loginForm.password"
placeholder="请输入密码"
custom-class="w-620rpx"
:no-border="true"
@focus="() => handleInputFocus('password')"
/>
</view>
</view>
</wd-form>
</view>
<view class="w-620rpx mx-auto">
<wd-button custom-class="mt-8" type="primary" block @click="handleLogin">
<text>登录</text>
</wd-button>
</view>
<!-- 提示组件 -->
<wd-toast />
</view>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useToast } from 'wot-design-uni'
import { login, getTenantIdByName } from '@/service/login/LoginAPI'
import { useUserStore } from '@/store'
import * as authUtil from '@/utils/auth'
// TODO @ Qiksy // useToast()
const toast = useToast()
const focusInput = ref('')
const handleInputFocus = (type: string) => {
focusInput.value = type
}
const props = defineProps({
agree: {
type: Boolean,
required: true,
default: false,
},
})
const loginData = reactive({
isShowPassword: false,
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: {
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
captchaVerification: '',
rememberMe: true, //
},
})
// ID // TODO @ Qiksy /** ID */
const getTenantId = async () => {
if (loginData.tenantEnable === 'true') {
const res = (await getTenantIdByName(loginData.loginForm.tenantName)) as string
authUtil.setTenantId(res)
}
}
const handleLogin = async () => {
console.log('账号密码登录')
focusInput.value = ''
if (!props.agree) {
toast.warning('请阅读并同意《用户协议》和《隐私政策》')
return
}
// 1. tenantId
await getTenantId()
// todo form
const res = await login(loginData.loginForm)
if (!res) {
return
}
authUtil.setToken(res)
// store
const userStore = useUserStore()
await userStore.setUserInfoAction()
//
uni.switchTab({
url: '/pages/work/index',
})
console.log('登录成功')
// todo
// todo loading
}
</script>

74
src/pages/login/index.vue Normal file
View File

@ -0,0 +1,74 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '登录页面',
},
}
</route>
<template>
<view class="relative h-full">
<!-- banner区域 -->
<view class="absolute w-full h-480rpx">
<view class="color-white pt-[116rpx] pl-[68rpx] z-10">
<view><text class="text-[64rpx] font-700">你好</text></view>
<view class="mt-[36rpx]"><text class="text-[36rpx]">欢迎登录芋道快速开发平台</text></view>
</view>
<!-- TODO @芋艿后续把静态资源放到 CDN -->
<image
class="absolute w-full top-0 -z-1"
src="/static/images/login-bg.png"
mode="aspectFit"
/>
</view>
<!-- 主要容器区域 -->
<!-- 430rpx 是上面banner的高度50rpx是留给底部的空间 -->
<view
class="top-[430rpx] h-[calc(100vh-430rpx-50rpx)] w-full rounded-t-2xl bg-white flex flex-col items-center absolute px-4 box-border"
>
<!-- tabs 切换 -->
<wd-tabs v-model="tab" custom-class="h-full">
<block :key="0">
<wd-tab title="手机号登录">手机号登录</wd-tab>
</block>
<block :key="1">
<wd-tab title="密码登录">
<LoginForm :agree="agree" />
</wd-tab>
</block>
</wd-tabs>
<!-- 隐私与用户条款 -->
<view class="text-[14px] text-[#999] mt-4 h-50rpx flex justify-center">
<wd-checkbox class="inline-block" v-model="agree"></wd-checkbox>
<text>已阅读并同意</text>
<text class="text-[#0B5EFF]">用户协议</text>
<text></text>
<text class="text-[#0B5EFF]">隐私政策</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import LoginForm from '@/pages/login/components/LoginForm.vue'
import { ref, reactive } from 'vue'
//
const tab = ref(1) // tabs
const agree = ref(false) //
onLoad(() => {
//
})
onMounted(() => {})
</script>
<style lang="scss" scoped>
//
.login-bg {
background-image: url('/static/images/login-bg.png');
}
</style>

View File

@ -0,0 +1,44 @@
<!-- 消息列表组件 -->
<template>
<view class="w-full flex justify-between">
<view class="w-100rpx h-100rpx flex items-center justify-center">
<image
class="w-80rpx h-80rpx rounded-5px overflow-hidden"
:src="props.item.avatar"
mode="scaleToFill"
/>
</view>
<view
class="h-100rpx grow-1 ml-2 flex justify-between py-1 border-b-[0.5px] border-solid border-[#E3E2E2] border-x-0 border-t-0 box-border"
>
<view class="flex flex-col justify-between grow-1">
<view>
{{ props.item.title }}
</view>
<view class="overflow-hidden truncate text-20rpx w-500rpx">
{{ props.item.content }}
</view>
</view>
<view class="flex flex-col justify-between">
<view class="text-20rpx text-[#999] truncate">
{{ props.item.time }}
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
//
const props = defineProps({
item: {
type: Object,
default: () => ({}),
},
})
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,41 @@
<!-- 消息界面用于 im 聊天 -->
<route lang="json5" type="page">
{
layout: 'tabbar',
style: {
navigationBarTitleText: '消息',
},
}
</route>
<template>
<!-- 搜索框 -->
<wd-search
v-model="searchValue"
@focus="searchFocus"
@blur="searchBlur"
@search="search"
maxlength="10"
hide-cancel
light
/>
<view class="px-4 pb-4 pt-0 flex flex-col gap-2">
<MessageItem v-for="(item, index) in messageList" :key="index" :item="item" />
</view>
</template>
<script lang="ts" setup>
//
import { ref } from 'vue'
import MessageItem from './components/MessageItem.vue'
import messageList from './mock'
const searchValue = ref('')
/// /////// ////////////
const searchFocus = () => {}
const searchBlur = () => {}
const search = () => {}
</script>
<style lang="scss" scoped>
//
</style>

92
src/pages/message/mock.js Normal file
View File

@ -0,0 +1,92 @@
export default [
{
title: 'IT中心工作群',
content: '今天下午2点到5点开部门会议请各位提前准备好汇报材料。',
time: '昨天 15:30',
avatar: '/static/images/avatar1.jpg',
},
{
title: '产品设计讨论组',
content: '关于新功能的设计稿,大家有什么建议可以在群里提出来。',
time: '昨天 18:45',
avatar: '/static/images/avatar2.jpg',
},
{
title: '前端开发小组',
content: '明天开始进行项目代码review请各位准备好自己的代码。',
time: '今天 09:00',
avatar: '/static/images/avatar3.jpg',
},
{
title: '公司年会筹备组',
content: '年会节目征集开始了,欢迎大家积极报名参加!',
time: '今天 10:20',
avatar: '/static/images/avatar4.jpg',
},
{
title: '技术分享会',
content: '本周五下午的技术分享会主题是“Vue 3的新特性”不要错过哦。',
time: '今天 14:30',
avatar: '/static/images/avatar5.jpg',
},
{
title: 'IT中心工作群',
content: '今天下午2点到5点开部门会议请各位提前准备好汇报材料。',
time: '昨天 15:30',
avatar: '/static/images/avatar1.jpg',
},
{
title: '产品设计讨论组',
content: '关于新功能的设计稿,大家有什么建议可以在群里提出来。',
time: '昨天 18:45',
avatar: '/static/images/avatar2.jpg',
},
{
title: '前端开发小组',
content: '明天开始进行项目代码review请各位准备好自己的代码。',
time: '今天 09:00',
avatar: '/static/images/avatar3.jpg',
},
{
title: '公司年会筹备组',
content: '年会节目征集开始了,欢迎大家积极报名参加!',
time: '今天 10:20',
avatar: '/static/images/avatar4.jpg',
},
{
title: '技术分享会',
content: '本周五下午的技术分享会主题是“Vue 3的新特性”不要错过哦。',
time: '今天 14:30',
avatar: '/static/images/avatar5.jpg',
},
{
title: 'IT中心工作群',
content: '今天下午2点到5点开部门会议请各位提前准备好汇报材料。',
time: '昨天 15:30',
avatar: '/static/images/avatar2.jpg',
},
{
title: '产品设计讨论组',
content: '关于新功能的设计稿,大家有什么建议可以在群里提出来。',
time: '昨天 18:45',
avatar: '/static/images/avatar1.jpg',
},
{
title: '前端开发小组',
content: '明天开始进行项目代码review请各位准备好自己的代码。',
time: '今天 09:00',
avatar: '/static/images/avatar3.jpg',
},
{
title: '公司年会筹备组',
content: '年会节目征集开始了,欢迎大家积极报名参加!',
time: '今天 10:20',
avatar: '/static/images/avatar2.jpg',
},
{
title: '技术分享会',
content: '本周五下午的技术分享会主题是“Vue 3的新特性”不要错过哦。',
time: '今天 14:30',
avatar: '/static/images/avatar4.jpg',
},
]

View File

@ -8,11 +8,121 @@
</route> </route>
<template> <template>
<view class="pt-40 text-xl text-center text-green-500">我的页面</view> <view class="p-4 flex flex-col gap-4">
<!-- 个人信息-登陆后 -->
<view
class="flex justify-between w-full items-center gap-4 h-150rpx bg-white rounded-xl py-15rpx px-10rpx box-border"
v-if="getAccessToken()"
>
<view class="rounded-full w-120rpx h-120rpx">
<image
:src="user ? user.avatar : '/static/images/user.png'"
class="h-120rpx w-120rpx"
mode="scaleToFill"
/>
</view>
<view class="grow-1 flex flex-col justify-between h-120rpx py-2 box-border">
<view>
<text class="text-2xl font-600">{{ user?.nickname }}</text>
</view>
<view class="text-xs flex items-center">
<text>芋道集团</text>
<view class="i-ic-twotone-shield text-emerald"></view>
</view>
</view>
<view class="h-full">
<!-- todo 设置按钮 -->
<view class="i-ic-outline-settings mr-15rpx"></view>
</view>
</view>
<!-- 个人信息-登录前 -->
<view v-else class="flex justify-between w-full items-start gap-4">
<view class="text-2xl font-600" @click="handleLogin">点击登录</view>
</view>
<!-- // todo -->
<!-- 应用设置 -->
<view class="flex flex-col gap-2">
<view>
<text class="text-lg font-600">应用设置</text>
</view>
<view class="rounded-xl bg-white overflow-hidden">
<wd-cell title="修改密码" is-link v-if="getAccessToken()">
<template #icon>
<view class="self-center flex item-center justify-center mr-2">
<view class="i-ic-outline-lock"></view>
</view>
</template>
</wd-cell>
<wd-cell title="隐私协议" is-link>
<template #icon>
<view class="self-center flex item-center justify-center mr-2">
<view class="i-ic-outline-policy"></view>
</view>
</template>
</wd-cell>
<wd-cell title="关于我们" is-link>
<template #icon>
<view class="self-center flex item-center justify-center mr-2">
<view class="i-ic-outline-info"></view>
</view>
</template>
</wd-cell>
<wd-cell title="投诉与建议" is-link>
<template #icon>
<view class="self-center flex item-center justify-center mr-2">
<view class="i-ic-outline-feedback"></view>
</view>
</template>
</wd-cell>
<!-- 退出登录红色的警告字体 -->
<wd-cell @click="handleLogout" clickable v-if="getAccessToken()">
<template #icon>
<view class="self-center flex item-center justify-center mr-2">
<view class="i-ic-outline-logout text-red-500"></view>
</view>
</template>
<template #title>
<view>
<text class="text-red-500">退出登录</text>
</view>
</template>
</wd-cell>
</view>
</view>
</view>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useUserStore } from '@/store'
import { useMessage } from 'wot-design-uni'
import { getAccessToken } from '@/utils/auth'
const message = useMessage()
// //
const userStore = useUserStore()
const handleLogin = () => {
uni.redirectTo({ url: '/pages/login/index' })
}
const handleLogout = async () => {
message
.confirm({
msg: '确定退出登录?',
title: '退出登录',
})
.then(async () => {
await userStore.LogOut()
uni.redirectTo({ url: '/pages/login/index' })
})
}
const user = ref<any>({})
onLoad((_option) => {
//
user.value = userStore.userInfo.user
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -0,0 +1,37 @@
<!-- 工作台图标 -->
<template>
<view class="flex flex-col items-center gap-1" @click="handleClick">
<view class="bg-blue-500 rounded-5px w-80rpx h-80rpx flex flex-col items-center justify-center">
<view :class="props.icon" class="color-white w-60rpx h-60rpx"></view>
</view>
<view class="text-22rpx">{{ props.title }}</view>
</view>
</template>
<script lang="ts" setup>
//
const props = defineProps({
title: {
type: String,
default: '',
},
icon: {
type: String,
default: '',
},
link: {
type: String,
default: '',
},
})
const handleClick = () => {
uni.navigateTo({
url: props.link,
})
}
</script>
<style lang="scss" scoped>
//
</style>

49
src/pages/work/index.vue Normal file
View File

@ -0,0 +1,49 @@
<!-- 工作台界面用于各种 crud 管理操作 -->
<route lang="json5" type="home">
{
layout: 'tabbar',
style: {
navigationBarTitleText: '工作台',
},
}
</route>
<template>
<!-- 搜索框 -->
<wd-search
v-model="searchValue"
@focus="searchFocus"
@blur="searchBlur"
@search="search"
maxlength="10"
hide-cancel
light
/>
<view class="px-4 box-border">
<!-- 系统管理相关的 -->
<view class="bg-white rounded-5px flex flex-col box-border p-2">
<view class="text-22rpx box-border">系统管理</view>
<view class="flex flex-wrap gap-6 mt-2">
<AppItems title="用户管理" icon="i-ic-baseline-account-box" />
<AppItems title="角色管理" icon="i-ic-round-person-pin" />
<AppItems title="菜单管理" icon="i-ic-twotone-list-alt" />
</view>
</view>
</view>
</template>
<script lang="ts" setup>
//
import { ref } from 'vue'
import AppItems from './components/AppItems.vue'
const searchValue = ref('')
/// /////// ////////////
const searchFocus = () => {}
const searchBlur = () => {}
const search = () => {}
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,21 @@
import { http, httpGet, httpPost } from '@/utils/http'
export const login = (data) => {
return http({
url: '/system/auth/login',
method: 'POST',
data,
})
}
export const loginOut = () => {
return httpPost('/system/auth/logout')
}
export const getInfo = (): Promise<UserInfoVO> => {
return httpGet<UserInfoVO>('/system/auth/get-permission-info')
}
export const getTenantIdByName = (name: string) => {
return httpGet('/system/tenant/get-id-by-name', { name })
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
src/static/images/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,35 +1,74 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { getAccessToken, removeToken } from '@/utils/auth'
import { getInfo, loginOut } from '@/service/login/LoginAPI'
const initState = { nickname: '', avatar: '' } const initState = {
permissions: [],
roles: [],
isSetUser: false,
user: {
id: 0,
avatar: '',
nickname: '',
deptId: 0,
},
}
export const useUserStore = defineStore( export const useUserStore = defineStore(
'user', 'user',
() => { () => {
const userInfo = ref<IUserInfo>({ ...initState }) // state
const setUserInfo = (val: IUserInfo) => { const userInfo = ref<UserInfoVO>({ ...initState })
userInfo.value = val
// actions methods
const setUserInfoAction = async () => {
if (!getAccessToken()) {
// 获取不到accessToken直接返回
resetState()
return
}
const data = await getInfo()
userInfo.value = {
...data,
isSetUser: true,
}
} }
const clearUserInfo = () => { const setUserAvatarAction = async (avatar: string) => {
userInfo.value = { ...initState } userInfo.value.user.avatar = avatar
} }
// 一般没有reset需求不需要的可以删除
const reset = () => {
userInfo.value = { ...initState }
}
const isLogined = computed(() => !!userInfo.value.token)
const setUserNicknameAction = async (nickname: string) => {
userInfo.value.user.nickname = nickname
}
const LogOut = async () => {
await loginOut()
removeToken()
resetState()
}
const resetState = () => {
console.log('initState', initState)
userInfo.value = initState
console.log('重置userInfo', userInfo.value)
}
// 暴露到外面的方法
return { return {
userInfo, userInfo,
setUserInfo, setUserInfoAction,
clearUserInfo, setUserAvatarAction,
isLogined, setUserNicknameAction,
reset, LogOut,
resetState,
} }
}, },
{ {
// 持久化
persist: true, persist: true,
}, },
) )

View File

@ -15,4 +15,8 @@ page {
// 修改按钮背景色 // 修改按钮背景色
// --wot-button-primary-bg-color: green; // --wot-button-primary-bg-color: green;
//
--wot-search-light-bg: #f2f3f7;
--wot-search-input-radius: 5px;
} }

20
src/types/system.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
type UserVO = {
id: number
avatar: string
nickname: string
deptId: number
}
// USER 缓存
type UserInfoVO = {
permissions: string[]
roles: string[]
isSetUser: boolean
user: UserVO
}
declare namespace JSX {
interface IntrinsicElements {
block: any // 或者更具体的类型定义
}
}

View File

@ -4,14 +4,17 @@
// Generated by vite-plugin-uni-pages // Generated by vite-plugin-uni-pages
interface NavigateToOptions { interface NavigateToOptions {
url: "/pages/index/index" | url: "/pages/work/index" |
"/pages/about/about" | "/pages/colab/index" |
"/pages/contacts/index" |
"/pages/login/index" |
"/pages/message/index" |
"/pages/my/index"; "/pages/my/index";
} }
interface RedirectToOptions extends NavigateToOptions {} interface RedirectToOptions extends NavigateToOptions {}
interface SwitchTabOptions { interface SwitchTabOptions {
url: "/pages/index/index" | "/pages/about/about" | "/pages/my/index" url: "/pages/message/index" | "/pages/colab/index" | "/pages/work/index" | "/pages/contacts/index" | "/pages/my/index"
} }
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions; type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;

37
src/utils/auth.ts Normal file
View File

@ -0,0 +1,37 @@
const AccessTokenKey = 'ACCESS_TOKEN'
const RefreshTokenKey = 'REFRESH_TOKEN'
const TenantIdKey = 'tenantId'
// ========== Token 相关 ==========
// 获取 Token
export function getAccessToken() {
return uni.getStorageSync(AccessTokenKey)
}
// 获取 RefreshToken
export function getRefreshToken() {
return uni.getStorageSync(RefreshTokenKey)
}
// 设置 Token
export function setToken(token) {
uni.setStorageSync(AccessTokenKey, token.accessToken)
uni.setStorageSync(RefreshTokenKey, token.refreshToken)
}
// 移除 Token
export function removeToken() {
uni.removeStorageSync(AccessTokenKey)
uni.removeStorageSync(RefreshTokenKey)
}
// ========== 租户相关 ==========
export const getTenantId = () => {
return uni.getStorageSync(TenantIdKey)
}
export const setTenantId = (username: string) => {
uni.setStorageSync(TenantIdKey, username)
}

30
src/utils/common.ts Normal file
View File

@ -0,0 +1,30 @@
/**
*
*
* @param content
*/
export function showConfirm(content: string) {
return new Promise((resolve, reject) => {
uni.showModal({
title: '提示',
content,
cancelText: '取消',
confirmText: '确定',
success: function (res) {
resolve(res)
},
})
})
}
/**
*
*
* @param content
*/
export function toast(content: string) {
uni.showToast({
title: content,
icon: 'none',
})
}

7
src/utils/errorCode.ts Normal file
View File

@ -0,0 +1,7 @@
export default {
'401': '认证失败,无法访问系统资源',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'500': '服务器错误',
default: '系统未知错误,请反馈给管理员',
}

View File

@ -1,8 +1,27 @@
import { CustomRequestOptions } from '@/interceptors/request' import { CustomRequestOptions } from '@/interceptors/request'
import { useUserStore } from '@/store'
import { getAccessToken, getRefreshToken, setToken } from '@/utils/auth'
import { showConfirm, toast } from '@/utils/common'
import { getEvnBaseUrl } from '@/utils'
import errorCode from './errorCode'
/*
token
1. token tenantId
2. http statusCode使token是否过期需要重新获取
3. success res code 401 access token
*/
// 是否正在刷新中
let isRefreshToken = false
// 请求的方法栈
let requestList = []
// 请求基准地址
const baseUrl = getEvnBaseUrl()
export const http = <T>(options: CustomRequestOptions) => { export const http = <T>(options: CustomRequestOptions) => {
// 1. 返回 Promise 对象 // 1. 返回 Promise 对象
return new Promise<IResData<T>>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
uni.request({ uni.request({
...options, ...options,
dataType: 'json', dataType: 'json',
@ -10,25 +29,97 @@ export const http = <T>(options: CustomRequestOptions) => {
responseType: 'json', responseType: 'json',
// #endif // #endif
// 响应成功 // 响应成功
success(res) { async success(res) {
// 状态码 2xx参考 axios 的设计 const resp = res.data as IResData<T>
if (res.statusCode >= 200 && res.statusCode < 300) { const { data, code, msg } = resp
// 2.1 提取核心数据 res.data
resolve(res.data as IResData<T>) if (code === 401) {
} else if (res.statusCode === 401) { // 未授权
// 401错误 -> 清理用户信息,跳转到登录页 const userStore = useUserStore()
// userStore.clearUserInfo()
// uni.navigateTo({ url: '/pages/login/login' }) if (!isRefreshToken) {
reject(res) // 设置为正在刷新
} else { isRefreshToken = true
// 其他错误 -> 根据后端错误信息轻提示
!options.hideErrorToast && // 1. 如果获取不到刷新令牌,则只能执行登出操作
uni.showToast({ if (!getRefreshToken()) {
icon: 'none', showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(
title: (res.data as IResData<T>).msg || '请求错误', (res: any) => {
if (res.confirm) {
// 清除缓存起来的用户信息
userStore.LogOut().then((res) => {
uni.reLaunch({ url: '/pages/login/index' })
})
}
},
)
}
// 2. 刷新accesstoken
try {
const refreshTokenRes: any = await refreshToken()
if (refreshTokenRes.data.code !== 0) {
// 如果获取不到refresh token就直接跳转到登录页面
showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(
(res: any) => {
if (res.confirm) {
// 清除缓存起来的用户信息
userStore.LogOut().then((res) => {
uni.reLaunch({ url: '/pages/login/index' })
})
}
},
)
const rejMsg = '无效的会话,或者会话已过期,请重新登录。'
reject(rejMsg)
}
// 2.1 刷新成功,则回放队列的请求 + 当前请求
setToken(refreshTokenRes.data.data)
options.header.Authorization = 'Bearer ' + getAccessToken()
// 将所有的请求方法都进行调用
requestList.forEach((cb) => {
cb()
})
// 调用之后就清空
requestList = []
// 到达这里,属于是请求已经失败了,需要重新请求的
// 所以这里是一个递归调用
resolve(http(options))
} catch (e) {
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
// 2.2 刷新失败,只回放队列的请求
requestList.forEach((cb) => {
cb()
})
const rejMsg = '无效的会话,或者会话已过期,请重新登录。'
reject(rejMsg)
} finally {
requestList = []
isRefreshToken = false
}
} else {
// 添加到队列,等待刷新获取到新的令牌
return new Promise((resolve) => {
requestList.push(() => {
options.header.Authorization = 'Bearer ' + getAccessToken()
resolve(http(options))
})
}) })
reject(res) }
} else if (code === 500) {
// 服务器错误
toast(msg)
reject(errorCode['500'])
} else if (code !== 0) {
// 其他的错误
// psyudao 的 success 的 code默认是 0 而不是 200
toast(msg)
reject(code)
} }
resolve(data)
}, },
// 响应失败 // 响应失败
fail(err) { fail(err) {
@ -78,3 +169,13 @@ export const httpPost = <T>(
http.get = httpGet http.get = httpGet
http.post = httpPost http.post = httpPost
const refreshToken = async () => {
return await uni.request({
method: 'POST',
url: baseUrl + '/system/auth/refresh-token?refreshToken=' + getRefreshToken(),
header: {
'tenant-id': 1,
},
})
}

View File

@ -131,16 +131,16 @@ export const getEvnBaseUrl = () => {
const { const {
miniProgram: { envVersion }, miniProgram: { envVersion },
} = uni.getAccountInfoSync() } = uni.getAccountInfoSync()
// 开发、体验、正式版 三种不同的小程序都可以配置不同的后端 url
switch (envVersion) { switch (envVersion) {
case 'develop': case 'develop':
baseUrl = 'https://ukw0y1.laf.run' baseUrl = 'http://localhost:48080/admin-api'
break break
case 'trial': case 'trial':
baseUrl = 'https://ukw0y1.laf.run' baseUrl = 'http://localhost:48080/admin-api'
break break
case 'release': case 'release':
baseUrl = 'https://ukw0y1.laf.run' baseUrl = 'http://localhost:48080/admin-api'
break break
} }
} }