create-unibest 4.0.2 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -3
- package/features/i18n/files/src/locale/README.md +12 -0
- package/features/i18n/files/src/locale/en.json +10 -0
- package/features/i18n/files/src/locale/index.ts +81 -0
- package/features/i18n/files/src/locale/zh-Hans.json +10 -0
- package/features/i18n/files/src/pages/i18n/index.vue +132 -0
- package/features/i18n/files/src/store/token.ts +323 -0
- package/features/i18n/files/src/tabbar/TabbarItem.vue +51 -0
- package/features/i18n/files/src/tabbar/config.ts +145 -0
- package/features/i18n/files/src/tabbar/i18n.ts +29 -0
- package/features/i18n/files/src/tabbar/index.vue +140 -0
- package/features/i18n/files/src/types/i18n.d.ts +8 -0
- package/features/i18n/files/src/utils/i18n.ts +10 -0
- package/features/i18n/files/src/utils/index.ts +179 -0
- package/features/i18n/hooks.ts +9 -0
- package/features/i18n/package.json +9 -0
- package/features/i18n/schema.json +13 -0
- package/features/login/files/src/pages/auth/README.md +20 -0
- package/features/login/files/src/pages/auth/login.vue +44 -0
- package/features/login/files/src/pages/auth/register.vue +34 -0
- package/features/login/files/src/pages/me.vue +79 -0
- package/features/login/files/src/router/config.ts +30 -0
- package/features/login/files/src/router/interceptor.ts +145 -0
- package/features/login/hooks.ts +9 -0
- package/features/login/package.json +6 -0
- package/features/login/schema.json +13 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { green as green6, yellow as yellow5 } from "kolorist";
|
|
|
6
6
|
import minimist from "minimist";
|
|
7
7
|
|
|
8
8
|
// package.json
|
|
9
|
-
var version = "4.0.
|
|
9
|
+
var version = "4.0.3";
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "create-unibest",
|
|
12
12
|
type: "module",
|
|
@@ -97,7 +97,7 @@ import path from "path";
|
|
|
97
97
|
import { fileURLToPath } from "url";
|
|
98
98
|
var __filename = fileURLToPath(import.meta.url);
|
|
99
99
|
var __dirname = path.dirname(__filename);
|
|
100
|
-
var FEATURES_DIR = path.resolve(__dirname, "
|
|
100
|
+
var FEATURES_DIR = path.resolve(__dirname, "..", "..", "features");
|
|
101
101
|
async function loadFeatureHooks(featureName) {
|
|
102
102
|
const hooksPath = path.join(FEATURES_DIR, featureName, "hooks.ts");
|
|
103
103
|
if (!fs.existsSync(hooksPath)) {
|
|
@@ -121,7 +121,7 @@ import { dirname } from "path";
|
|
|
121
121
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
122
122
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
123
123
|
var __dirname2 = dirname(__filename2);
|
|
124
|
-
var FEATURES_PATH = path2.join(__dirname2, "..", "..", "features");
|
|
124
|
+
var FEATURES_PATH = path2.join(__dirname2, "..", "..", "..", "features");
|
|
125
125
|
var FeatureInjector = class {
|
|
126
126
|
constructor(projectPath) {
|
|
127
127
|
this.projectPath = projectPath;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 注意事项
|
|
2
|
+
|
|
3
|
+
> 文件夹名字必须为 `locale`, 这是 `uniapp` 官方约定的,如果改为别的,标题将不能正常切换多语言(其他内容还是正常)。
|
|
4
|
+
>
|
|
5
|
+
> `xxx.json` 的 `xxx` 多语言标识必须与 `uniapp` 官方约定的一致,否则也会出现 BUG。
|
|
6
|
+
>
|
|
7
|
+
> 查看截图 `screenshots/i18n.png`。
|
|
8
|
+
|
|
9
|
+
## 参考文档
|
|
10
|
+
|
|
11
|
+
[uniapp 国际化开发指南](https://uniapp.dcloud.net.cn/tutorial/i18n.html)
|
|
12
|
+
[uniapp 国际化-注意事项](https://uniapp.dcloud.net.cn/api/ui/locale.html#onlocalechange) 最下面的注意事项
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tabbar.home": "Home",
|
|
3
|
+
"tabbar.about": "About",
|
|
4
|
+
"tabbar.me": "Me",
|
|
5
|
+
"i18n.title": "En Title",
|
|
6
|
+
"alova.title": "Alova Request",
|
|
7
|
+
"weight": "{heavy}KG",
|
|
8
|
+
"detail": "{0}cm, {1}KG",
|
|
9
|
+
"introduction": "I am {name},height:{detail.height},weight:{detail.weight}"
|
|
10
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createI18n } from 'vue-i18n'
|
|
2
|
+
|
|
3
|
+
import en from './en.json'
|
|
4
|
+
import zhHans from './zh-Hans.json' // 简体中文
|
|
5
|
+
|
|
6
|
+
const messages = {
|
|
7
|
+
en,
|
|
8
|
+
'zh-Hans': zhHans, // key 不能乱写,查看uniapp官网
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const i18n = createI18n({
|
|
12
|
+
locale: uni.getLocale(), // 获取已设置的语言,fallback 语言需要再 manifest.config.ts 中设置
|
|
13
|
+
messages,
|
|
14
|
+
allowComposition: true,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
console.log(uni.getLocale())
|
|
18
|
+
console.log(i18n.global.locale)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 可以拿到原始的语言模板,非 vue 文件使用这个方法,
|
|
22
|
+
* @param { string } key 多语言的key,eg: "app.name"
|
|
23
|
+
* @returns {string} 返回原始的多语言模板,eg: "{heavy}KG"
|
|
24
|
+
*/
|
|
25
|
+
export function getTemplateByKey(key: string) {
|
|
26
|
+
if (!key) {
|
|
27
|
+
console.error(`[i18n] Function getTemplateByKey(), key param is required`)
|
|
28
|
+
return ''
|
|
29
|
+
}
|
|
30
|
+
const locale = uni.getLocale()
|
|
31
|
+
console.log('locale:', locale)
|
|
32
|
+
|
|
33
|
+
const message = messages[locale] // 拿到某个多语言的所有模板(是一个对象)
|
|
34
|
+
if (Object.keys(message).includes(key)) {
|
|
35
|
+
return message[key]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const keyList = key.split('.')
|
|
40
|
+
return keyList.reduce((pre, cur) => {
|
|
41
|
+
return pre[cur]
|
|
42
|
+
}, message)
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(`[i18n] Function getTemplateByKey(), key param ${key} is not existed.`)
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
|
|
52
|
+
* 暂不支持数组
|
|
53
|
+
* @param template 多语言模板字符串,eg: `我是{name}`
|
|
54
|
+
* @param {object | undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}`
|
|
55
|
+
* @returns
|
|
56
|
+
*/
|
|
57
|
+
function formatI18n(template: string, data?: any) {
|
|
58
|
+
return template.replace(/\{([^}]+)\}/g, (match, key: string) => {
|
|
59
|
+
// console.log( match, key) // => { detail.height } detail.height
|
|
60
|
+
const arr = key.trim().split('.')
|
|
61
|
+
let result = data
|
|
62
|
+
while (arr.length) {
|
|
63
|
+
const first = arr.shift()
|
|
64
|
+
result = result[first]
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* t('introduction',{name:'张三',detail:{height:178,weight:'75kg'}})
|
|
72
|
+
* => formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
|
|
73
|
+
* 没有key的,可以不传 data;暂不支持数组
|
|
74
|
+
* @param template 多语言模板字符串,eg: `我是{name}`
|
|
75
|
+
* @param {object | undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}`
|
|
76
|
+
* @returns
|
|
77
|
+
*/
|
|
78
|
+
export function t(key, data?) {
|
|
79
|
+
return formatI18n(getTemplateByKey(key), data)
|
|
80
|
+
}
|
|
81
|
+
export default i18n
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import i18n, { t } from '@/locale/index'
|
|
3
|
+
import { setTabbarItem } from '@/tabbar/i18n'
|
|
4
|
+
import { testI18n } from '@/utils/i18n'
|
|
5
|
+
|
|
6
|
+
definePage({
|
|
7
|
+
style: {
|
|
8
|
+
navigationBarTitleText: '%i18n.title%',
|
|
9
|
+
},
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const current = ref(uni.getLocale())
|
|
13
|
+
const user = { name: '张三', detail: { height: 178, weight: '75kg' } }
|
|
14
|
+
const languages = [
|
|
15
|
+
{
|
|
16
|
+
value: 'zh-Hans',
|
|
17
|
+
name: '中文',
|
|
18
|
+
checked: 'true',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
value: 'en',
|
|
22
|
+
name: '英文',
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function radioChange(evt) {
|
|
27
|
+
// console.log(evt)
|
|
28
|
+
current.value = evt.detail.value
|
|
29
|
+
// 下面2句缺一不可!!!
|
|
30
|
+
uni.setLocale(evt.detail.value)
|
|
31
|
+
i18n.global.locale = evt.detail.value
|
|
32
|
+
|
|
33
|
+
// 底部tabbar需要重新设置一下
|
|
34
|
+
setTabbarItem()
|
|
35
|
+
// 本页的标题也需要重新设置一下
|
|
36
|
+
uni.setNavigationBarTitle({
|
|
37
|
+
title: t('i18n.title'),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<view class="mt-6 center flex-col">
|
|
44
|
+
<view class="p-4 text-red-500 leading-6">
|
|
45
|
+
经过我的测试发现,小程序里面会有2处BUG:
|
|
46
|
+
<view>
|
|
47
|
+
<text class="line-through"> 1. 页面标题多语言不生效 </text>
|
|
48
|
+
<text class="ml-2 text-green-500"> 已解决 </text>
|
|
49
|
+
</view>
|
|
50
|
+
<view>
|
|
51
|
+
<text class="line-through">
|
|
52
|
+
2. 多语言传递的参数不生效,如下 heavy
|
|
53
|
+
</text>
|
|
54
|
+
<text class="ml-2 text-green-500"> 已解决 </text>
|
|
55
|
+
<view class="ml-2 text-green-500">
|
|
56
|
+
把 $t 改为自定义的 t 即可
|
|
57
|
+
</view>
|
|
58
|
+
</view>
|
|
59
|
+
</view>
|
|
60
|
+
<view class="text-green-500">
|
|
61
|
+
多语言测试
|
|
62
|
+
</view>
|
|
63
|
+
<view class="m-4">
|
|
64
|
+
{{ $t("i18n.title") }}
|
|
65
|
+
</view>
|
|
66
|
+
<view class="text-gray-500 italic">
|
|
67
|
+
使用$t: {{ $t("weight", { heavy: 100 }) }}
|
|
68
|
+
</view>
|
|
69
|
+
<view class="m-4">
|
|
70
|
+
{{ $t("weight", { heavy: 100 }) }}
|
|
71
|
+
</view>
|
|
72
|
+
<view class="text-gray-500 italic">
|
|
73
|
+
使用t: {{ t("weight", { heavy: 100 }) }}
|
|
74
|
+
</view>
|
|
75
|
+
<view class="m-4">
|
|
76
|
+
{{ t("weight", { heavy: 100 }) }}
|
|
77
|
+
</view>
|
|
78
|
+
<view class="m-4">
|
|
79
|
+
{{ t("introduction", user) }}
|
|
80
|
+
</view>
|
|
81
|
+
|
|
82
|
+
<view class="mt-12 text-green-500">
|
|
83
|
+
切换语言
|
|
84
|
+
</view>
|
|
85
|
+
<view class="uni-list">
|
|
86
|
+
<radio-group class="radio-group" @change="radioChange">
|
|
87
|
+
<label
|
|
88
|
+
v-for="item in languages"
|
|
89
|
+
:key="item.value"
|
|
90
|
+
class="uni-list-cell uni-list-cell-pd"
|
|
91
|
+
>
|
|
92
|
+
<view>
|
|
93
|
+
<radio :value="item.value" :checked="item.value === current" />
|
|
94
|
+
</view>
|
|
95
|
+
<view>{{ item.name }}</view>
|
|
96
|
+
</label>
|
|
97
|
+
</radio-group>
|
|
98
|
+
</view>
|
|
99
|
+
|
|
100
|
+
<!-- http://localhost:9000/#/pages/index/i18n -->
|
|
101
|
+
<button class="mb-44 mt-20" @click="testI18n">
|
|
102
|
+
测试弹窗
|
|
103
|
+
</button>
|
|
104
|
+
</view>
|
|
105
|
+
</template>
|
|
106
|
+
|
|
107
|
+
<style lang="scss">
|
|
108
|
+
.uni-list {
|
|
109
|
+
position: relative;
|
|
110
|
+
display: flex;
|
|
111
|
+
flex-direction: column;
|
|
112
|
+
width: 100%;
|
|
113
|
+
background-color: #fff;
|
|
114
|
+
border-radius: 12px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.radio-group {
|
|
118
|
+
width: 200px;
|
|
119
|
+
margin: 10px auto;
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.uni-list-cell {
|
|
124
|
+
position: relative;
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: row;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
padding: 10px;
|
|
130
|
+
background-color: #bcecd1;
|
|
131
|
+
}
|
|
132
|
+
</style>
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ILoginForm,
|
|
3
|
+
} from '@/api/login'
|
|
4
|
+
import type { IAuthLoginRes } from '@/api/types/login'
|
|
5
|
+
import { defineStore } from 'pinia'
|
|
6
|
+
import { computed, ref } from 'vue' // 修复:导入 computed
|
|
7
|
+
import {
|
|
8
|
+
login as _login,
|
|
9
|
+
logout as _logout,
|
|
10
|
+
refreshToken as _refreshToken,
|
|
11
|
+
wxLogin as _wxLogin,
|
|
12
|
+
getWxCode,
|
|
13
|
+
} from '@/api/login'
|
|
14
|
+
import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
|
|
15
|
+
import { useUserStore } from './user'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 是否是双token模式
|
|
19
|
+
*/
|
|
20
|
+
export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
|
|
21
|
+
// 初始化状态
|
|
22
|
+
const tokenInfoState = isDoubleTokenMode
|
|
23
|
+
? {
|
|
24
|
+
accessToken: '',
|
|
25
|
+
accessExpiresIn: 0,
|
|
26
|
+
refreshToken: '',
|
|
27
|
+
refreshExpiresIn: 0,
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
token: '',
|
|
31
|
+
expiresIn: 0,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const useTokenStore = defineStore(
|
|
35
|
+
'token',
|
|
36
|
+
() => {
|
|
37
|
+
// 定义用户信息
|
|
38
|
+
const tokenInfo = ref<IAuthLoginRes>({ ...tokenInfoState })
|
|
39
|
+
|
|
40
|
+
// 添加一个时间戳 ref 作为响应式依赖
|
|
41
|
+
const nowTime = ref(Date.now())
|
|
42
|
+
/**
|
|
43
|
+
* 更新响应式数据:now
|
|
44
|
+
* 确保isTokenExpired/isRefreshTokenExpired重新计算,而不是用错误过期缓存值
|
|
45
|
+
* 可useTokenStore内部适时调用;也可链式调用:tokenStore.updateNowTime().hasLogin
|
|
46
|
+
* @returns 最新的tokenStore实例
|
|
47
|
+
*/
|
|
48
|
+
const updateNowTime = () => {
|
|
49
|
+
nowTime.value = Date.now()
|
|
50
|
+
return useTokenStore()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 设置用户信息
|
|
54
|
+
const setTokenInfo = (val: IAuthLoginRes) => {
|
|
55
|
+
updateNowTime()
|
|
56
|
+
tokenInfo.value = val
|
|
57
|
+
|
|
58
|
+
// 计算并存储过期时间
|
|
59
|
+
const now = Date.now()
|
|
60
|
+
if (isSingleTokenRes(val)) {
|
|
61
|
+
// 单token模式
|
|
62
|
+
const expireTime = now + val.expiresIn * 1000
|
|
63
|
+
uni.setStorageSync('accessTokenExpireTime', expireTime)
|
|
64
|
+
}
|
|
65
|
+
else if (isDoubleTokenRes(val)) {
|
|
66
|
+
// 双token模式
|
|
67
|
+
const accessExpireTime = now + val.accessExpiresIn * 1000
|
|
68
|
+
const refreshExpireTime = now + val.refreshExpiresIn * 1000
|
|
69
|
+
uni.setStorageSync('accessTokenExpireTime', accessExpireTime)
|
|
70
|
+
uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 判断token是否过期
|
|
76
|
+
*/
|
|
77
|
+
const isTokenExpired = computed(() => {
|
|
78
|
+
if (!tokenInfo.value) {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const now = nowTime.value
|
|
83
|
+
const expireTime = uni.getStorageSync('accessTokenExpireTime')
|
|
84
|
+
|
|
85
|
+
if (!expireTime)
|
|
86
|
+
return true
|
|
87
|
+
return now >= expireTime
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 判断refreshToken是否过期
|
|
92
|
+
*/
|
|
93
|
+
const isRefreshTokenExpired = computed(() => {
|
|
94
|
+
if (!isDoubleTokenMode)
|
|
95
|
+
return true
|
|
96
|
+
|
|
97
|
+
const now = nowTime.value
|
|
98
|
+
const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime')
|
|
99
|
+
|
|
100
|
+
if (!refreshExpireTime)
|
|
101
|
+
return true
|
|
102
|
+
return now >= refreshExpireTime
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 登录成功后处理逻辑
|
|
107
|
+
* @param tokenInfo 登录返回的token信息
|
|
108
|
+
*/
|
|
109
|
+
async function _postLogin(tokenInfo: IAuthLoginRes) {
|
|
110
|
+
setTokenInfo(tokenInfo)
|
|
111
|
+
const userStore = useUserStore()
|
|
112
|
+
await userStore.fetchUserInfo()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 用户登录
|
|
117
|
+
* 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
|
|
118
|
+
* (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
|
|
119
|
+
* @param loginForm 登录参数
|
|
120
|
+
* @returns 登录结果
|
|
121
|
+
*/
|
|
122
|
+
const login = async (loginForm: ILoginForm) => {
|
|
123
|
+
try {
|
|
124
|
+
const res = await _login(loginForm)
|
|
125
|
+
console.log('普通登录-res: ', res)
|
|
126
|
+
await _postLogin(res)
|
|
127
|
+
uni.showToast({
|
|
128
|
+
title: '登录成功',
|
|
129
|
+
icon: 'success',
|
|
130
|
+
})
|
|
131
|
+
return res
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.error('登录失败:', error)
|
|
135
|
+
uni.showToast({
|
|
136
|
+
title: '登录失败,请重试',
|
|
137
|
+
icon: 'error',
|
|
138
|
+
})
|
|
139
|
+
throw error
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
updateNowTime()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 微信登录
|
|
148
|
+
* 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
|
|
149
|
+
* (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
|
|
150
|
+
* @returns 登录结果
|
|
151
|
+
*/
|
|
152
|
+
const wxLogin = async () => {
|
|
153
|
+
try {
|
|
154
|
+
// 获取微信小程序登录的code
|
|
155
|
+
const code = await getWxCode()
|
|
156
|
+
console.log('微信登录-code: ', code)
|
|
157
|
+
const res = await _wxLogin(code)
|
|
158
|
+
console.log('微信登录-res: ', res)
|
|
159
|
+
await _postLogin(res)
|
|
160
|
+
uni.showToast({
|
|
161
|
+
title: '登录成功',
|
|
162
|
+
icon: 'success',
|
|
163
|
+
})
|
|
164
|
+
return res
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error('微信登录失败:', error)
|
|
168
|
+
uni.showToast({
|
|
169
|
+
title: '微信登录失败,请重试',
|
|
170
|
+
icon: 'error',
|
|
171
|
+
})
|
|
172
|
+
throw error
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
updateNowTime()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 退出登录 并 删除用户信息
|
|
181
|
+
*/
|
|
182
|
+
const logout = async () => {
|
|
183
|
+
try {
|
|
184
|
+
// TODO 实现自己的退出登录逻辑
|
|
185
|
+
await _logout()
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error('退出登录失败:', error)
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
updateNowTime()
|
|
192
|
+
|
|
193
|
+
// 无论成功失败,都需要清除本地token信息
|
|
194
|
+
// 清除存储的过期时间
|
|
195
|
+
uni.removeStorageSync('accessTokenExpireTime')
|
|
196
|
+
uni.removeStorageSync('refreshTokenExpireTime')
|
|
197
|
+
console.log('退出登录-清除用户信息')
|
|
198
|
+
tokenInfo.value = { ...tokenInfoState }
|
|
199
|
+
uni.removeStorageSync('token')
|
|
200
|
+
const userStore = useUserStore()
|
|
201
|
+
userStore.clearUserInfo()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 刷新token
|
|
207
|
+
* @returns 刷新结果
|
|
208
|
+
*/
|
|
209
|
+
const refreshToken = async () => {
|
|
210
|
+
if (!isDoubleTokenMode) {
|
|
211
|
+
console.error('单token模式不支持刷新token')
|
|
212
|
+
throw new Error('单token模式不支持刷新token')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// 安全检查,确保refreshToken存在
|
|
217
|
+
if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) {
|
|
218
|
+
throw new Error('无效的refreshToken')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const refreshToken = tokenInfo.value.refreshToken
|
|
222
|
+
const res = await _refreshToken(refreshToken)
|
|
223
|
+
console.log('刷新token-res: ', res)
|
|
224
|
+
setTokenInfo(res)
|
|
225
|
+
return res
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.error('刷新token失败:', error)
|
|
229
|
+
throw error
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
updateNowTime()
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 获取有效的token
|
|
238
|
+
* 注意:在computed中不直接调用异步函数,只做状态判断
|
|
239
|
+
* 实际的刷新操作应由调用方处理
|
|
240
|
+
* 建议这样使用 tokenStore.updateNowTime().validToken
|
|
241
|
+
*/
|
|
242
|
+
const getValidToken = computed(() => {
|
|
243
|
+
// token已过期,返回空
|
|
244
|
+
if (isTokenExpired.value) {
|
|
245
|
+
return ''
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!isDoubleTokenMode) {
|
|
249
|
+
return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : ''
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : ''
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 检查是否有登录信息(不考虑token是否过期)
|
|
258
|
+
*/
|
|
259
|
+
const hasLoginInfo = computed(() => {
|
|
260
|
+
if (!tokenInfo.value) {
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
if (isDoubleTokenMode) {
|
|
264
|
+
return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 检查是否已登录且token有效
|
|
273
|
+
* 建议这样使用tokenStore.updateNowTime().hasLogin
|
|
274
|
+
*/
|
|
275
|
+
const hasValidLogin = computed(() => {
|
|
276
|
+
console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value)
|
|
277
|
+
return hasLoginInfo.value && !isTokenExpired.value
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 尝试获取有效的token,如果过期且可刷新,则刷新token
|
|
282
|
+
* @returns 有效的token或空字符串
|
|
283
|
+
*/
|
|
284
|
+
const tryGetValidToken = async (): Promise<string> => {
|
|
285
|
+
updateNowTime()
|
|
286
|
+
if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) {
|
|
287
|
+
try {
|
|
288
|
+
await refreshToken()
|
|
289
|
+
return getValidToken.value
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
console.error('尝试刷新token失败:', error)
|
|
293
|
+
return ''
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return getValidToken.value
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
// 核心API方法
|
|
301
|
+
login,
|
|
302
|
+
wxLogin,
|
|
303
|
+
logout,
|
|
304
|
+
|
|
305
|
+
// 认证状态判断(最常用的)
|
|
306
|
+
hasLogin: hasValidLogin,
|
|
307
|
+
|
|
308
|
+
// 内部系统使用的方法
|
|
309
|
+
refreshToken,
|
|
310
|
+
tryGetValidToken,
|
|
311
|
+
validToken: getValidToken,
|
|
312
|
+
|
|
313
|
+
// 调试或特殊场景可能需要直接访问的信息
|
|
314
|
+
tokenInfo,
|
|
315
|
+
setTokenInfo,
|
|
316
|
+
updateNowTime,
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
// 添加持久化配置,确保刷新页面后token信息不丢失
|
|
321
|
+
persist: true,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CustomTabBarItem } from './types'
|
|
3
|
+
import { getI18nText } from './i18n'
|
|
4
|
+
import { tabbarStore } from './store'
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
item: CustomTabBarItem
|
|
8
|
+
index: number
|
|
9
|
+
isBulge?: boolean
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
function getImageByIndex(index: number, item: CustomTabBarItem) {
|
|
13
|
+
if (!item.iconActive) {
|
|
14
|
+
console.warn('image 模式下,需要配置 iconActive (高亮时的图片),否则无法切换高亮图片')
|
|
15
|
+
return item.icon
|
|
16
|
+
}
|
|
17
|
+
return tabbarStore.curIdx === index ? item.iconActive : item.icon
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<view class="flex flex-col items-center justify-center">
|
|
23
|
+
<template v-if="item.iconType === 'uiLib'">
|
|
24
|
+
<!-- TODO: 以下内容请根据选择的UI库自行替换 -->
|
|
25
|
+
<!-- 如:<wd-icon name="home" /> (https://wot-design-uni.cn/component/icon.html) -->
|
|
26
|
+
<!-- 如:<uv-icon name="home" /> (https://www.uvui.cn/components/icon.html) -->
|
|
27
|
+
<!-- 如:<sar-icon name="image" /> (https://sard.wzt.zone/sard-uniapp-docs/components/icon)(sar没有home图标^_^) -->
|
|
28
|
+
<!-- <wd-icon :name="item.icon" size="20" /> -->
|
|
29
|
+
</template>
|
|
30
|
+
<template v-if="item.iconType === 'unocss' || item.iconType === 'iconfont'">
|
|
31
|
+
<view :class="[item.icon, isBulge ? 'text-80px' : 'text-20px']" />
|
|
32
|
+
</template>
|
|
33
|
+
<template v-if="item.iconType === 'image'">
|
|
34
|
+
<image :src="getImageByIndex(index, item)" mode="scaleToFill" :class="isBulge ? 'h-80px w-80px' : 'h-24px w-24px'" />
|
|
35
|
+
</template>
|
|
36
|
+
<view v-if="!isBulge" class="mt-2px text-12px">
|
|
37
|
+
{{ getI18nText(item.text) }}
|
|
38
|
+
</view>
|
|
39
|
+
<!-- 角标显示 -->
|
|
40
|
+
<view v-if="item.badge">
|
|
41
|
+
<template v-if="item.badge === 'dot'">
|
|
42
|
+
<view class="absolute right-0 top-0 h-2 w-2 rounded-full bg-#f56c6c" />
|
|
43
|
+
</template>
|
|
44
|
+
<template v-else>
|
|
45
|
+
<view class="absolute top-0 box-border h-5 min-w-5 center rounded-full bg-#f56c6c px-1 text-center text-xs text-white -right-3">
|
|
46
|
+
{{ item.badge > 99 ? '99+' : item.badge }}
|
|
47
|
+
</view>
|
|
48
|
+
</template>
|
|
49
|
+
</view>
|
|
50
|
+
</view>
|
|
51
|
+
</template>
|