@liujitcn/kratos-uni-app-core 0.0.59 → 0.0.61

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/module.mjs CHANGED
@@ -164,7 +164,8 @@ var zh_CN_default = {
164
164
  "common.value.top_level": "\u9876\u7EA7\u8282\u70B9",
165
165
  "common.resource.default": "{resource}",
166
166
  "common.action.delete_resource": "\u5220\u9664{resource}",
167
- "common.action.set_field_status": "\u8BBE\u7F6E{field}"
167
+ "common.action.set_field_status": "\u8BBE\u7F6E{field}",
168
+ "common.action.load_more": "\u52A0\u8F7D\u66F4\u591A"
168
169
  };
169
170
 
170
171
  // src/locales/en-US.json
@@ -308,7 +309,8 @@ var en_US_default = {
308
309
  "common.value.top_level": "Top level",
309
310
  "common.resource.default": "{resource}",
310
311
  "common.action.delete_resource": "Delete {resource}",
311
- "common.action.set_field_status": "Set {field}"
312
+ "common.action.set_field_status": "Set {field}",
313
+ "common.action.load_more": "Load more"
312
314
  };
313
315
 
314
316
  // src/locales/ja-JP.json
@@ -452,7 +454,8 @@ var ja_JP_default = {
452
454
  "common.value.top_level": "\u30C8\u30C3\u30D7\u30EC\u30D9\u30EB",
453
455
  "common.resource.default": "{resource}",
454
456
  "common.action.delete_resource": "{resource}\u3092\u524A\u9664",
455
- "common.action.set_field_status": "{field}\u3092\u8A2D\u5B9A"
457
+ "common.action.set_field_status": "{field}\u3092\u8A2D\u5B9A",
458
+ "common.action.load_more": "\u3055\u3089\u306B\u8AAD\u307F\u8FBC\u3080"
456
459
  };
457
460
 
458
461
  // src/locales/zh-TW.json
@@ -596,7 +599,8 @@ var zh_TW_default = {
596
599
  "common.value.top_level": "\u9802\u7D1A\u7BC0\u9EDE",
597
600
  "common.resource.default": "{resource}",
598
601
  "common.action.delete_resource": "\u522A\u9664{resource}",
599
- "common.action.set_field_status": "\u8A2D\u5B9A{field}"
602
+ "common.action.set_field_status": "\u8A2D\u5B9A{field}",
603
+ "common.action.load_more": "\u8F09\u5165\u66F4\u591A"
600
604
  };
601
605
 
602
606
  // src/locales/generated.ts
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git@github.com:liujitcn/kratos-admin.git"
9
9
  },
10
- "version": "0.0.59",
10
+ "version": "0.0.61",
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
@@ -15,6 +15,15 @@ defineSlots<{
15
15
 
16
16
  const userStore = useUserStore()
17
17
  const { locale, setLocale, t } = useI18n()
18
+ /** 登录态失效时请求层已弹窗引导重新登录,页面侧不再叠加失败提示。 */
19
+ const isAuthExpiredError = (error: unknown) => {
20
+ if (error && typeof error === 'object') {
21
+ const statusCode = (error as { statusCode?: number }).statusCode
22
+ if (statusCode === 401 || statusCode === 403) return true
23
+ }
24
+ return error instanceof Error && /auth (required|expired)/.test(error.message)
25
+ }
26
+
18
27
  const logoutLoading = ref(false)
19
28
  const mfaDialogVisible = ref(false)
20
29
  const mfaDialogMode = ref<'setup' | 'disable'>('setup')
@@ -136,10 +145,12 @@ const onLogout = () => {
136
145
  await userStore.logout()
137
146
  uni.navigateBack()
138
147
  } catch (error) {
139
- await uni.showToast({
140
- icon: 'none',
141
- title: t('core.settings.logout_failed'),
142
- })
148
+ if (!isAuthExpiredError(error)) {
149
+ await uni.showToast({
150
+ icon: 'none',
151
+ title: t('core.settings.logout_failed'),
152
+ })
153
+ }
143
154
  } finally {
144
155
  logoutLoading.value = false
145
156
  }
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, ref } from 'vue'
2
+ import { computed, onBeforeUnmount, ref, watch } from 'vue'
3
3
  import { useI18n } from '../locales'
4
4
  import { copyText } from '../utils/clipboard'
5
5
 
@@ -23,6 +23,7 @@ const emit = defineEmits<{
23
23
 
24
24
  const { t } = useI18n()
25
25
  const codesText = computed(() => props.codes.join('\n'))
26
+ const confirming = ref(false)
26
27
  const copyState = ref<'idle' | 'copied' | 'failed'>('idle')
27
28
  const copyTextLabel = computed(() => {
28
29
  if (copyState.value === 'copied') return t('core.login.mfa_recovery_codes_copied')
@@ -48,10 +49,20 @@ async function copyRecoveryCodes() {
48
49
 
49
50
  /** 关闭弹窗并通知调用方继续完成绑定流程。 */
50
51
  function handleConfirm() {
52
+ if (confirming.value) return
53
+ confirming.value = true
51
54
  emit('update:modelValue', false)
52
55
  emit('confirm')
53
56
  }
54
57
 
58
+ // 每次重新打开弹窗时重置一次性确认锁。
59
+ watch(
60
+ () => props.modelValue,
61
+ (open) => {
62
+ if (open) confirming.value = false
63
+ },
64
+ )
65
+
55
66
  onBeforeUnmount(() => {
56
67
  if (resetCopyStateTimer) clearTimeout(resetCopyStateTimer)
57
68
  })
@@ -138,5 +138,6 @@
138
138
  "common.value.top_level": "Top level",
139
139
  "common.resource.default": "{resource}",
140
140
  "common.action.delete_resource": "Delete {resource}",
141
- "common.action.set_field_status": "Set {field}"
141
+ "common.action.set_field_status": "Set {field}",
142
+ "common.action.load_more": "Load more"
142
143
  }
@@ -138,5 +138,6 @@
138
138
  "common.value.top_level": "トップレベル",
139
139
  "common.resource.default": "{resource}",
140
140
  "common.action.delete_resource": "{resource}を削除",
141
- "common.action.set_field_status": "{field}を設定"
141
+ "common.action.set_field_status": "{field}を設定",
142
+ "common.action.load_more": "さらに読み込む"
142
143
  }
@@ -138,5 +138,6 @@
138
138
  "common.value.top_level": "顶级节点",
139
139
  "common.resource.default": "{resource}",
140
140
  "common.action.delete_resource": "删除{resource}",
141
- "common.action.set_field_status": "设置{field}"
141
+ "common.action.set_field_status": "设置{field}",
142
+ "common.action.load_more": "加载更多"
142
143
  }
@@ -138,5 +138,6 @@
138
138
  "common.value.top_level": "頂級節點",
139
139
  "common.resource.default": "{resource}",
140
140
  "common.action.delete_resource": "刪除{resource}",
141
- "common.action.set_field_status": "設定{field}"
141
+ "common.action.set_field_status": "設定{field}",
142
+ "common.action.load_more": "載入更多"
142
143
  }
@@ -4,7 +4,7 @@ import type {
4
4
  CreateOauthSessionRequest,
5
5
  CreateOauthSessionResponse,
6
6
  } from '../../rpc/base/v1/oauth'
7
- import type { LoginRequest, LoginResponse } from '../../rpc/base/v1/login'
7
+ import { LoginStatus, type LoginRequest, type LoginResponse } from '../../rpc/base/v1/login'
8
8
  import type { VerifyMfaRequest } from '../../rpc/base/v1/mfa'
9
9
  import { defMfaService } from '../../api/base/v1/mfa'
10
10
  import { defAuthService } from '../../api/system/app/v1/auth'
@@ -69,6 +69,15 @@ export const useUserStore = defineStore(
69
69
  return Boolean(userInfo.value && hasValidToken())
70
70
  }
71
71
 
72
+ /** 仅当登录阶段已签发非空令牌时,才允许写入本地登录态。 */
73
+ function shouldApplyLoginToken(data: LoginResponse | CreateOauthSessionResponse) {
74
+ return (
75
+ (data.status === LoginStatus.LOGIN_STATUS_AUTHENTICATED ||
76
+ data.status === LoginStatus.LOGIN_STATUS_PASSWORD_CHANGE_REQUIRED) &&
77
+ Boolean(data.access_token && data.refresh_token)
78
+ )
79
+ }
80
+
72
81
  /** 保存登录接口返回的认证令牌,并通知已注册的业务扩展。 */
73
82
  async function applyLoginToken(data: LoginResponse | CreateOauthSessionResponse) {
74
83
  const { token_type, access_token, refresh_token, expires_in } = data
@@ -95,8 +104,7 @@ export const useUserStore = defineStore(
95
104
  defLoginService
96
105
  .Login(request)
97
106
  .then(async (data) => {
98
- if (data.status === 1 || data.status === 0 || data.status === 4)
99
- await applyLoginToken(data)
107
+ if (shouldApplyLoginToken(data)) await applyLoginToken(data)
100
108
  resolve(data)
101
109
  })
102
110
  .catch((error) => {
@@ -111,7 +119,7 @@ export const useUserStore = defineStore(
111
119
  defMfaService
112
120
  .VerifyMfa(request)
113
121
  .then(async (data) => {
114
- await applyLoginToken(data)
122
+ if (shouldApplyLoginToken(data)) await applyLoginToken(data)
115
123
  resolve(data)
116
124
  })
117
125
  .catch((error) => reject(error))
@@ -133,8 +141,7 @@ export const useUserStore = defineStore(
133
141
  resolve(data)
134
142
  return
135
143
  }
136
- if (data.status === 1 || data.status === 0 || data.status === 4)
137
- await applyLoginToken(data)
144
+ if (shouldApplyLoginToken(data)) await applyLoginToken(data)
138
145
  resolve(data)
139
146
  })
140
147
  .catch((error) => {
@@ -149,8 +156,7 @@ export const useUserStore = defineStore(
149
156
  defOauthService
150
157
  .BindOauthSession(request)
151
158
  .then(async (data) => {
152
- if (data.status === 1 || data.status === 0 || data.status === 4)
153
- await applyLoginToken(data)
159
+ if (shouldApplyLoginToken(data)) await applyLoginToken(data)
154
160
  resolve(data)
155
161
  })
156
162
  .catch((error) => {
@@ -183,19 +189,13 @@ export const useUserStore = defineStore(
183
189
  /**
184
190
  * 登出
185
191
  */
186
- function logout() {
187
- return new Promise<void>((resolve, reject) => {
188
- defLoginService
189
- .Logout({})
190
- .then(() => {
191
- clearUserData().then(() => {
192
- resolve()
193
- })
194
- })
195
- .catch((error) => {
196
- reject(error)
197
- })
198
- })
192
+ async function logout() {
193
+ try {
194
+ await defLoginService.Logout({})
195
+ } finally {
196
+ // 登出接口失败时仍清理本地登录态,避免残留有效 token 造成“假登录”。
197
+ await clearUserData()
198
+ }
199
199
  }
200
200
 
201
201
  /**
package/src/utils/file.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from '../rpc/base/v1/file'
6
6
  import { formatSrc } from './index'
7
7
  import { getLocaleRequestHeaders, t } from '../locales'
8
- import { getRequestAccessToken } from './http'
8
+ import { getRequestAccessToken, sourceClient } from './http'
9
9
 
10
10
  // 文件上传-兼容小程序端、H5端、App端
11
11
  export const uploadFile = async (
@@ -24,7 +24,7 @@ export const uploadFile = async (
24
24
  },
25
25
  header: {
26
26
  ...getLocaleRequestHeaders(),
27
- 'source-client': 'miniapp',
27
+ 'source-client': sourceClient,
28
28
  ...(token ? { Authorization: token } : {}),
29
29
  },
30
30
  })
@@ -65,7 +65,7 @@ export const multiUploadFile = async (
65
65
  },
66
66
  header: {
67
67
  ...getLocaleRequestHeaders(),
68
- 'source-client': 'miniapp',
68
+ 'source-client': sourceClient,
69
69
  ...(token ? { Authorization: token } : {}),
70
70
  },
71
71
  })
package/src/utils/http.ts CHANGED
@@ -22,7 +22,7 @@ const apiBasePath = import.meta.env.VITE_APP_BASE_API || '/api'
22
22
  const apiTargetUrl = import.meta.env.VITE_APP_API_URL || ''
23
23
  const normalizedApiBasePath = apiBasePath.startsWith('/') ? apiBasePath : `/${apiBasePath}`
24
24
 
25
- let sourceClient = 'uni-h5'
25
+ export let sourceClient = 'uni-h5'
26
26
  // #ifdef MP-WEIXIN
27
27
  sourceClient = 'uni-weapp'
28
28
  // #endif
@@ -37,6 +37,11 @@ const requestOrigin =
37
37
  : apiTargetUrl.replace(/\/$/, '')
38
38
  const baseURL = `${requestOrigin}${normalizedApiBasePath}`
39
39
  export const requestBaseURL = baseURL
40
+ /** 站点根地址(不含 API 基础路径),供事件流等非 API 端点复用。 */
41
+ export const siteBaseURL =
42
+ typeof window !== 'undefined' && window.location?.protocol?.startsWith('http')
43
+ ? window.location.origin
44
+ : apiTargetUrl.replace(/\/$/, '')
40
45
  const SESSION_URL = '/v1/base/session'
41
46
  const REFRESH_TOKEN_URL = '/v1/base/token'
42
47
  const CAPTCHA_URL = '/v1/base/captcha'
@@ -265,18 +270,15 @@ export function handleAuthExpired(authMode: AuthMode = 'required') {
265
270
  }
266
271
 
267
272
  // 刷新 Token 处理
268
- function handleTokenRefresh(authMode: AuthMode) {
273
+ function handleTokenRefresh() {
269
274
  if (refreshTokenPromise) {
270
275
  return refreshTokenPromise
271
276
  }
272
277
  isRefreshing = true
273
278
  refreshTokenPromise = refreshAccessToken()
274
- .catch(async (error) => {
279
+ .catch((error) => {
275
280
  // 刷新失败后立即撤销本地认证,避免弹窗等待期间后台请求反复提交旧刷新令牌。
276
281
  silentClearAuthData()
277
- if (authMode === 'required') {
278
- await promptRelogin()
279
- }
280
282
  throw error
281
283
  })
282
284
  .finally(() => {
@@ -300,7 +302,11 @@ async function getAccessTokenByMode(authMode: AuthMode) {
300
302
  }
301
303
 
302
304
  if (shouldRefreshToken()) {
303
- await handleTokenRefresh(authMode)
305
+ try {
306
+ await handleTokenRefresh()
307
+ } catch {
308
+ // 刷新失败已撤销本地认证,交由下方 hasValidToken 分支按当前调用方 authMode 处理。
309
+ }
304
310
  }
305
311
 
306
312
  if (hasValidToken()) {
@@ -2,7 +2,7 @@
2
2
  import { useSettingStore, useUserStore } from '../../../stores'
3
3
  import type { LoginRequest } from '../../../rpc/base/v1/login'
4
4
  import { LoginStatus, type LoginResponse } from '../../../rpc/base/v1/login'
5
- import { onLoad } from '@dcloudio/uni-app'
5
+ import { onLoad, onUnload } from '@dcloudio/uni-app'
6
6
  import { computed, defineAsyncComponent, nextTick, reactive, ref, watch } from 'vue'
7
7
  import { defLoginService } from '../../../api/base/v1/login'
8
8
  import { defMfaService } from '../../../api/base/v1/mfa'
@@ -102,10 +102,15 @@ const toggleAgreePrivacy = () => {
102
102
  isAgreePrivacy.value = !isAgreePrivacy.value
103
103
  }
104
104
 
105
+ let agreePrivacyShakeTimer: ReturnType<typeof setTimeout> | undefined
106
+ let loginSuccessTimer: ReturnType<typeof setTimeout> | undefined
107
+
105
108
  const triggerAgreePrivacyShake = () => {
106
109
  isAgreePrivacyShakeY.value = true
107
- setTimeout(() => {
110
+ if (agreePrivacyShakeTimer) clearTimeout(agreePrivacyShakeTimer)
111
+ agreePrivacyShakeTimer = setTimeout(() => {
108
112
  isAgreePrivacyShakeY.value = false
113
+ agreePrivacyShakeTimer = undefined
109
114
  }, 500)
110
115
  }
111
116
 
@@ -203,17 +208,8 @@ const refreshMiniCaptcha = async () => {
203
208
  /** 仅将后端明确返回的未绑定错误转为账号绑定流程。 */
204
209
  const isWechatUnboundError = (error: unknown) => {
205
210
  if (!error || typeof error !== 'object') return false
206
- const response = error as {
207
- statusCode?: number
208
- data?: {
209
- reason?: string | number
210
- message?: string
211
- binding_required?: boolean
212
- error?: { reason?: string | number; message?: string }
213
- }
214
- }
215
- const reason = response.data?.reason ?? response.data?.error?.reason
216
- return response.data?.binding_required === true || String(reason || '') === 'UNAUTHENTICATED'
211
+ const response = error as { data?: { binding_required?: boolean } }
212
+ return response.data?.binding_required === true
217
213
  }
218
214
 
219
215
  // 微信授权失败时保留请求层提示,避免异步登录异常冒泡到小程序调试器。
@@ -781,7 +777,9 @@ const loginSuccess = async () => {
781
777
  await userStore.getUserProfile()
782
778
  // 成功提示
783
779
  await uni.showToast({ icon: 'success', title: t('core.login.login_success') })
784
- setTimeout(() => {
780
+ if (loginSuccessTimer) clearTimeout(loginSuccessTimer)
781
+ loginSuccessTimer = setTimeout(() => {
782
+ loginSuccessTimer = undefined
785
783
  const lastRoute = uni.getStorageSync('lastRoute') || homeTabPage
786
784
  if (lastRoute.startsWith(homeTabPage)) {
787
785
  uni.setStorageSync('SwitchTabIndex', true)
@@ -873,6 +871,13 @@ const ensureLoginSettings = () => loadLoginSettings()
873
871
  onLoad(() => {
874
872
  void loadLoginSettings()
875
873
  })
874
+
875
+ onUnload(() => {
876
+ if (agreePrivacyShakeTimer) clearTimeout(agreePrivacyShakeTimer)
877
+ agreePrivacyShakeTimer = undefined
878
+ if (loginSuccessTimer) clearTimeout(loginSuccessTimer)
879
+ loginSuccessTimer = undefined
880
+ })
876
881
  </script>
877
882
 
878
883
  <template>
@@ -41,6 +41,17 @@ const isIframeLoaded = ref(false)
41
41
  const isIframeTimedOut = ref(false)
42
42
  const { t } = useI18n()
43
43
 
44
+ /** 只放行 http(s) 外链,其它协议按非法地址处理。 */
45
+ const isAllowedUrl = (value: string) => /^https?:\/\//i.test(value)
46
+
47
+ const safeDecodeURIComponent = (value: string) => {
48
+ try {
49
+ return decodeURIComponent(value)
50
+ } catch {
51
+ return value
52
+ }
53
+ }
54
+
44
55
  const showFallback = computed(
45
56
  () => !url.value || (isH5.value && isIframeTimedOut.value && !isIframeLoaded.value),
46
57
  )
@@ -52,8 +63,9 @@ const emptyDesc = computed(() => {
52
63
  })
53
64
 
54
65
  onLoad((query) => {
55
- url.value = decodeURIComponent(query?.url || '')
56
- const title = decodeURIComponent(query?.title || '')
66
+ const nextUrl = safeDecodeURIComponent(query?.url || '')
67
+ url.value = isAllowedUrl(nextUrl) ? nextUrl : ''
68
+ const title = safeDecodeURIComponent(query?.title || '')
57
69
  // 调用方传入标题时同步更新原生导航栏,避免继续显示页面注册时的空标题。
58
70
  if (title) {
59
71
  void uni.setNavigationBarTitle({ title })
@@ -81,10 +93,8 @@ const openInBrowser = () => {
81
93
  // #endif
82
94
  }
83
95
 
84
- // 小程序接收消息
85
- const handleMessage = (e: any) => {
86
- console.log('收到H5消息:', e.detail)
87
- }
96
+ // 小程序接收消息(第三方消息不落日志,避免敏感信息泄漏)。
97
+ const handleMessage = () => {}
88
98
  </script>
89
99
 
90
100
  <style scoped>