feat: 登录过期自动续登并校验同一账号
This commit is contained in:
@@ -2,12 +2,17 @@ import { Component, PropsWithChildren } from 'react'
|
||||
import { ThemeProvider } from './context/ThemeContext'
|
||||
import { getTheme, resolveTheme } from './utils/store'
|
||||
import { applyPageBackground } from './utils/themeBackground'
|
||||
import { setSessionRefreshHandler } from './utils/request'
|
||||
import { refreshSession, ensureSessionFresh } from './utils/session'
|
||||
import './app.scss'
|
||||
|
||||
class App extends Component<PropsWithChildren> {
|
||||
componentDidMount() {
|
||||
// 启动最早时机同步页面背景,避免自定义导航区在深色模式下闪白
|
||||
applyPageBackground(resolveTheme(getTheme()))
|
||||
// 全局 401 自动续登 + 启动时主动校验登录态
|
||||
setSessionRefreshHandler(refreshSession)
|
||||
ensureSessionFresh()
|
||||
}
|
||||
|
||||
componentDidShow() {}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
}
|
||||
|
||||
.login-guard-icon {
|
||||
font-size: 80px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import { View, Text, Image } from '@tarojs/components'
|
||||
import { isAuthVerified } from '../../utils/authState'
|
||||
import { THEME_CHANGE_EVENT } from '../../context/ThemeContext'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
type GuardState = 'checking' | 'ok' | 'denied'
|
||||
|
||||
@@ -51,7 +52,7 @@ export default function LoginGuard(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<View className='login-guard'>
|
||||
<View className='login-guard-inner'>
|
||||
<Text className='login-guard-icon'>⏳</Text>
|
||||
<Image className='login-guard-icon' src={assetUrl('/icon/设置.png')} mode='aspectFit' />
|
||||
<Text className='login-guard-title'>校验登录状态</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -62,7 +63,7 @@ export default function LoginGuard(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<View className='login-guard'>
|
||||
<View className='login-guard-inner'>
|
||||
<Text className='login-guard-icon'>🔒</Text>
|
||||
<Image className='login-guard-icon' src={assetUrl('/icon/个人.png')} mode='aspectFit' />
|
||||
<Text className='login-guard-title'>请先登录</Text>
|
||||
<Text className='login-guard-desc'>登录后查看您的专属内容</Text>
|
||||
<View className='btn-gradient login-guard-btn' onTap={() => Taro.switchTab({ url: '/pages/profile/index' })}>
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
}
|
||||
|
||||
.theme-icon {
|
||||
font-size: 36px;
|
||||
font-size: 26px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function ThemeToggle() {
|
||||
|
||||
return (
|
||||
<View className='theme-toggle' onTap={handleToggle}>
|
||||
<Text className='theme-icon'>{theme === 'light' ? '🌙' : '☀️'}</Text>
|
||||
<Text className='theme-icon'>{theme === 'light' ? '深色' : '浅色'}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ff9a9e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2.5L14.8 9.2 21.5 12 14.8 14.8 12 21.5 9.2 14.8 2.5 12 9.2 9.2z"/></svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#ff9a9e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.5 15.5l4.5 4.5"/></svg>
|
||||
|
After Width: | Height: | Size: 227 B |
@@ -124,9 +124,9 @@ export default function AddressPage() {
|
||||
</Text>
|
||||
</View>
|
||||
<View className='address-actions'>
|
||||
<Text className='address-act' onTap={() => openEdit(item)}>✏️ 编辑</Text>
|
||||
<Text className='address-act' onTap={() => handleCopy(item)}>📋 复制</Text>
|
||||
<Text className='address-act delete' onTap={() => handleDelete(item.id)}>🗑️ 删除</Text>
|
||||
<Text className='address-act' onTap={() => openEdit(item)}>编辑</Text>
|
||||
<Text className='address-act' onTap={() => handleCopy(item)}>复制</Text>
|
||||
<Text className='address-act delete' onTap={() => handleDelete(item.id)}>删除</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function DesignListPage() {
|
||||
const style = STATUS_STYLE[item.status]
|
||||
const rawIcon = item.productIcon?.startsWith('/icon/') || /^https?:/i.test(item.productIcon || '')
|
||||
? item.productIcon
|
||||
: PRODUCT_ICON_MAP[item.productId] || '/icon/四角星.png'
|
||||
: PRODUCT_ICON_MAP[item.productId] || '/icon/四角星.svg'
|
||||
const iconSrc = assetUrl(rawIcon)
|
||||
const isSelected = selected.has(item.id)
|
||||
return (
|
||||
|
||||
@@ -325,7 +325,7 @@ export default function DIYPage() {
|
||||
<Text className='add-label'>添加</Text>
|
||||
</View>
|
||||
</View>
|
||||
{hasOverlap && <Text className='overlap-hint'>⚠️ 有贴纸重叠,请调整位置</Text>}
|
||||
{hasOverlap && <Text className='overlap-hint'>有贴纸重叠,请调整位置</Text>}
|
||||
</View>
|
||||
|
||||
{/* 控制工具栏 */}
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function Index() {
|
||||
<View className='search-card dashed-card'>
|
||||
<View className='star-badge' />
|
||||
<View className='search-inner'>
|
||||
<Image className='search-icon-img' src={assetUrl('/icon/搜索.png')} mode='aspectFit' />
|
||||
<Image className='search-icon-img' src={assetUrl('/icon/搜索.svg')} mode='aspectFit' />
|
||||
<Input
|
||||
className='search-input'
|
||||
type='text'
|
||||
@@ -132,7 +132,7 @@ export default function Index() {
|
||||
<View className='category-img-wrapper'>
|
||||
<Image
|
||||
className='category-img-real'
|
||||
src={PRODUCT_IMG_MAP[cat.id] || assetUrl('/icon/四角星.png')}
|
||||
src={PRODUCT_IMG_MAP[cat.id] || assetUrl('/icon/四角星.svg')}
|
||||
mode='aspectFill'
|
||||
/>
|
||||
</View>
|
||||
@@ -144,7 +144,7 @@ export default function Index() {
|
||||
</View>
|
||||
{filteredCategories.length === 0 && (
|
||||
<View className='empty-category dashed-card'>
|
||||
<Image className='empty-icon-img' src={assetUrl('/icon/搜索.png')} mode='aspectFit' />
|
||||
<Image className='empty-icon-img' src={assetUrl('/icon/搜索.svg')} mode='aspectFit' />
|
||||
<Text className='empty-text'>未找到匹配的品类</Text>
|
||||
<Text className='empty-sub'>尝试搜索“笔记本”、“杯垫”等</Text>
|
||||
</View>
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function OrderDetailPage() {
|
||||
<View className='product-icon-wrapper'>
|
||||
<Image
|
||||
className='product-icon-img'
|
||||
src={assetUrl(order.productIcon?.startsWith('/icon/') || /^https?:/i.test(order.productIcon || '') ? order.productIcon : PRODUCT_ICON_MAP[order.productId] || '/icon/四角星.png')}
|
||||
src={assetUrl(order.productIcon?.startsWith('/icon/') || /^https?:/i.test(order.productIcon || '') ? order.productIcon : PRODUCT_ICON_MAP[order.productId] || '/icon/四角星.svg')}
|
||||
mode='aspectFit'
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function OrdersPage() {
|
||||
const status = STATUS_MAP[order.statusCode] || STATUS_MAP.pending
|
||||
const rawIcon = order.productIcon?.startsWith('/icon/') || /^https?:/i.test(order.productIcon || '')
|
||||
? order.productIcon
|
||||
: '/icon/四角星.png'
|
||||
: '/icon/四角星.svg'
|
||||
const iconSrc = assetUrl(rawIcon)
|
||||
return (
|
||||
<View key={order.id} className='order-card dashed-card' onTap={() => goDetail(order)}>
|
||||
|
||||
@@ -22,7 +22,7 @@ const ICON_MAP: Record<string, string> = {
|
||||
|
||||
const STATUS_MAP = [
|
||||
{ key: 'toDesign', label: '待设计', icon: assetUrl('/icon/三角尺.png') },
|
||||
{ key: 'pending', label: '待付款', icon: assetUrl('/icon/四角星.png') },
|
||||
{ key: 'pending', label: '待付款', icon: assetUrl('/icon/四角星.svg') },
|
||||
{ key: 'paid', label: '待发货', icon: assetUrl('/icon/包裹.png') },
|
||||
{ key: 'shipping', label: '待收货', icon: assetUrl('/icon/杯子.png') },
|
||||
{ key: 'done', label: '已完成', icon: assetUrl('/icon/书本.png') }
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
}
|
||||
|
||||
.udb-lock-icon {
|
||||
font-size: 80px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View, Text, Input, Button } from '@tarojs/components'
|
||||
import { View, Text, Image, Input, Button } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useState, useEffect } from 'react'
|
||||
import './index.scss'
|
||||
@@ -14,6 +14,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
export default function UserDatabasePage() {
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
@@ -87,7 +88,7 @@ export default function UserDatabasePage() {
|
||||
<View className='udb-page'>
|
||||
<View className='udb-lock-card dashed-card mt-20'>
|
||||
<View className='star-badge' />
|
||||
<Text className='udb-lock-icon'>🔐</Text>
|
||||
<Image className='udb-lock-icon' src={assetUrl('/icon/账号管理.png')} mode='aspectFit' />
|
||||
<Text className='udb-lock-title'>管理员验证</Text>
|
||||
<Text className='udb-lock-desc'>请输入密码以访问用户数据库</Text>
|
||||
<Input
|
||||
|
||||
@@ -104,7 +104,8 @@
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
font-size: 80px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@@ -190,7 +191,8 @@
|
||||
}
|
||||
|
||||
.generating-icon {
|
||||
font-size: 120px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@@ -277,7 +279,8 @@
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
export default function WordCloudPage() {
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
@@ -92,7 +93,7 @@ export default function WordCloudPage() {
|
||||
<Image className='preview-image' src={baseImage} mode='aspectFit' />
|
||||
) : (
|
||||
<View className='upload-placeholder'>
|
||||
<Text className='upload-icon'>📷</Text>
|
||||
<Image className='upload-icon' src={assetUrl('/icon/词云生成.png')} mode='aspectFit' />
|
||||
<Text className='upload-title'>点击上传底图</Text>
|
||||
<Text className='upload-desc'>支持照片、Logo、图案等</Text>
|
||||
<Text className='upload-tip'>系统将用名字填充到深色区域</Text>
|
||||
@@ -148,7 +149,7 @@ export default function WordCloudPage() {
|
||||
<View className='step-content'>
|
||||
{isGenerating ? (
|
||||
<View className='generating-panel'>
|
||||
<Text className='generating-icon'>✨</Text>
|
||||
<Image className='generating-icon' src={assetUrl('/icon/词云生成.png')} mode='aspectFit' />
|
||||
<Text className='generating-title'>正在生成词云...</Text>
|
||||
<Text className='generating-subtitle'>AI正在将名字融入图案</Text>
|
||||
<View className='progress-bar'>
|
||||
@@ -166,15 +167,15 @@ export default function WordCloudPage() {
|
||||
</View>
|
||||
<View className='result-actions'>
|
||||
<View className='action-btn' onTap={handleExportImage}>
|
||||
<Text className='action-icon'>💾</Text>
|
||||
<Image className='action-icon' src={assetUrl('/icon/设置.png')} mode='aspectFit' />
|
||||
<Text className='action-label'>保存图片</Text>
|
||||
</View>
|
||||
<View className='action-btn' onTap={handleGenerate}>
|
||||
<Text className='action-icon'>🔄</Text>
|
||||
<Image className='action-icon' src={assetUrl('/icon/词云生成.png')} mode='aspectFit' />
|
||||
<Text className='action-label'>重新生成</Text>
|
||||
</View>
|
||||
<View className='action-btn' onTap={() => setStep(2)}>
|
||||
<Text className='action-icon'>✏️</Text>
|
||||
<Image className='action-icon' src={assetUrl('/icon/调色盘.png')} mode='aspectFit' />
|
||||
<Text className='action-label'>修改名单</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export function assetUrl(path?: string | null): string {
|
||||
/** 从旧存储中解析商品图标:兼容本地 /icon、远程 URL 和老版 emoji 缺省值 */
|
||||
export function resolveStoredIcon(
|
||||
value?: string,
|
||||
fallback = '/icon/四角星.png',
|
||||
fallback = '/icon/四角星.svg',
|
||||
): string {
|
||||
if (!value) return assetUrl(fallback)
|
||||
if (value.startsWith('/icon/') || /^https?:/i.test(value)) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 产品品类配置文件
|
||||
*
|
||||
* ⚠️ 重要:当实际产品尺寸确定后,请修改本文件中的 mask 参数
|
||||
* 注意:当实际产品尺寸确定后,请修改本文件中的 mask 参数
|
||||
* 修改后页面会自动生效,无需改动其他代码
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ const PRODUCTS_LOCAL: ProductCategory[] = [
|
||||
id: 'notebook-small',
|
||||
name: '微雕笔记本(小)',
|
||||
desc: '便携随行,记录点滴',
|
||||
icon: '📓',
|
||||
icon: '/icon/书本.png',
|
||||
iconImg: '/icon/书本.png',
|
||||
price: 12,
|
||||
originalPrice: 18,
|
||||
@@ -45,7 +45,7 @@ const PRODUCTS_LOCAL: ProductCategory[] = [
|
||||
id: 'notebook-large',
|
||||
name: '微雕笔记本(大)',
|
||||
desc: '大开本,更多创作空间',
|
||||
icon: '📔',
|
||||
icon: '/icon/书本_大.png',
|
||||
iconImg: '/icon/书本_大.png',
|
||||
price: 45,
|
||||
originalPrice: 58,
|
||||
@@ -77,7 +77,7 @@ const PRODUCTS_LOCAL: ProductCategory[] = [
|
||||
id: 'coaster',
|
||||
name: '铜质杯垫',
|
||||
desc: '金属质感,桌面艺术',
|
||||
icon: '☕',
|
||||
icon: '/icon/杯子.png',
|
||||
iconImg: '/icon/杯子.png',
|
||||
price: 25,
|
||||
originalPrice: 35,
|
||||
@@ -108,7 +108,7 @@ const PRODUCTS_LOCAL: ProductCategory[] = [
|
||||
id: 'penbox',
|
||||
name: '竹制笔盒',
|
||||
desc: '自然竹纹,文房雅器',
|
||||
icon: '✏️',
|
||||
icon: '/icon/笔盒.png',
|
||||
iconImg: '/icon/笔盒.png',
|
||||
price: 75,
|
||||
originalPrice: 98,
|
||||
@@ -140,7 +140,7 @@ const PRODUCTS_LOCAL: ProductCategory[] = [
|
||||
id: 'booklamp',
|
||||
name: '书本型灯',
|
||||
desc: '温暖光影,点亮心意',
|
||||
icon: '💡',
|
||||
icon: '/icon/书灯.png',
|
||||
iconImg: '/icon/书灯.png',
|
||||
price: 45,
|
||||
originalPrice: 68,
|
||||
@@ -207,6 +207,6 @@ export const PRODUCT_ICON_MAP: Record<string, string> = {
|
||||
*/
|
||||
export function getProductIconImg(product: any): string {
|
||||
if (product?.iconImg) return assetUrl(product.iconImg)
|
||||
if (product?.id) return assetUrl(PRODUCT_ICON_MAP[product.id] || '/icon/四角星.png')
|
||||
return assetUrl('/icon/四角星.png')
|
||||
if (product?.id) return assetUrl(PRODUCT_ICON_MAP[product.id] || '/icon/四角星.svg')
|
||||
return assetUrl('/icon/四角星.svg')
|
||||
}
|
||||
|
||||
+56
-22
@@ -51,6 +51,8 @@ export interface RequestOptions {
|
||||
headers?: Record<string, string>
|
||||
/** 是否自动携带 token,默认 true */
|
||||
auth?: boolean
|
||||
/** 内部参数:会话刷新内部的请求不再触发 401 自动刷新 */
|
||||
skipAuthRefresh?: boolean
|
||||
}
|
||||
|
||||
// ---------- request ----------
|
||||
@@ -59,37 +61,75 @@ export interface RequestOptions {
|
||||
* 发请求。成功(code===0)返回 data;业务失败抛 Error(message);HTTP 401 清 token 并抛错。
|
||||
* 调用方可自行 catch 或用 onUnauthorized 钩子统一处理跳登录。
|
||||
*/
|
||||
export let onUnauthorized: (() => void) | null = null
|
||||
export function setOnUnauthorized(handler: () => void): void {
|
||||
onUnauthorized = handler
|
||||
}
|
||||
|
||||
let sessionRefreshHandler: (() => Promise<boolean>) | null = null
|
||||
export function setSessionRefreshHandler(handler: (() => Promise<boolean>) | null): void {
|
||||
sessionRefreshHandler = handler
|
||||
}
|
||||
|
||||
async function sendRequest(
|
||||
path: string,
|
||||
method: string,
|
||||
data: unknown,
|
||||
finalHeaders: Record<string, string>,
|
||||
): Promise<Taro.request.SuccessCallbackResult> {
|
||||
return Taro.request({
|
||||
url: `${BASE_URL}${path}`,
|
||||
method,
|
||||
data,
|
||||
header: finalHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
export async function request<T = unknown>(
|
||||
path: string,
|
||||
options: RequestOptions = {},
|
||||
): Promise<T> {
|
||||
const { method = 'GET', data, headers = {}, auth = true } = options
|
||||
const { method = 'GET', data, headers = {}, auth = true, skipAuthRefresh = false } = options
|
||||
|
||||
const token = getToken()
|
||||
const finalHeaders: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(auth && token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...headers,
|
||||
const buildHeaders = (): Record<string, string> => {
|
||||
const token = getToken()
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
...(auth && token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...headers,
|
||||
}
|
||||
}
|
||||
|
||||
let res: Taro.request.SuccessCallbackResult
|
||||
try {
|
||||
res = await Taro.request({
|
||||
url: `${BASE_URL}${path}`,
|
||||
method,
|
||||
data,
|
||||
header: finalHeaders,
|
||||
})
|
||||
res = await sendRequest(path, method, data, buildHeaders())
|
||||
} catch (e) {
|
||||
// 网络层异常
|
||||
throw new Error('网络异常,请稍后重试')
|
||||
const err = new Error('网络异常,请稍后重试') as Error & { statusCode?: number }
|
||||
err.statusCode = -1
|
||||
throw err
|
||||
}
|
||||
|
||||
// HTTP 未授权:清 token,交回调处理
|
||||
// HTTP 未授权:先尝试自动续登一次,成功则用新 token 重试原请求
|
||||
if (res.statusCode === 401 && !skipAuthRefresh && sessionRefreshHandler) {
|
||||
const refreshed = await sessionRefreshHandler()
|
||||
if (refreshed) {
|
||||
try {
|
||||
res = await sendRequest(path, method, data, buildHeaders())
|
||||
} catch (e) {
|
||||
const err = new Error('网络异常,请稍后重试') as Error & { statusCode?: number }
|
||||
err.statusCode = -1
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 仍然未授权:清 token,交回调处理
|
||||
if (res.statusCode === 401) {
|
||||
clearToken()
|
||||
onUnauthorized?.()
|
||||
throw new Error('登录已过期,请重新登录')
|
||||
const err = new Error('登录已过期,请重新登录') as Error & { statusCode?: number }
|
||||
err.statusCode = 401
|
||||
throw err
|
||||
}
|
||||
|
||||
const body = res.data as ApiResponse<T>
|
||||
@@ -102,12 +142,6 @@ export async function request<T = unknown>(
|
||||
return (body && typeof body.code === 'number' ? body.data : body) as T
|
||||
}
|
||||
|
||||
/** 401 钩子,由业务方注册(如跳转登录页) */
|
||||
export let onUnauthorized: (() => void) | null = null
|
||||
export function setOnUnauthorized(handler: () => void): void {
|
||||
onUnauthorized = handler
|
||||
}
|
||||
|
||||
// ---------- 便捷方法 ----------
|
||||
|
||||
export const http = {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import Taro from '@tarojs/taro'
|
||||
import http, { getToken, setToken, clearToken } from './request'
|
||||
import { getUserInfoRaw, clearUserInfo, isMockOpenid } from './store'
|
||||
|
||||
let refreshInflight: Promise<boolean> | null = null
|
||||
|
||||
/**
|
||||
* 重新获取登录态:wx.login 换新 code → 后端续签 accessToken →
|
||||
* getMe 校验 openid 与本地账号一致才放行,确保不会串号。
|
||||
*/
|
||||
export function refreshSession(): Promise<boolean> {
|
||||
if (refreshInflight) return refreshInflight
|
||||
refreshInflight = doRefreshSession().finally(() => {
|
||||
refreshInflight = null
|
||||
})
|
||||
return refreshInflight
|
||||
}
|
||||
|
||||
async function doRefreshSession(): Promise<boolean> {
|
||||
const localUser = getUserInfoRaw()
|
||||
if (!localUser?.openid || isMockOpenid(localUser.openid)) return false
|
||||
|
||||
try {
|
||||
const wxLogin = await Taro.login()
|
||||
if (!wxLogin.code) return false
|
||||
|
||||
const result = await http.post<{ accessToken?: string }>(
|
||||
'/api/auth/login',
|
||||
{ code: wxLogin.code },
|
||||
{ auth: false, skipAuthRefresh: true },
|
||||
)
|
||||
if (!result?.accessToken) return false
|
||||
setToken(result.accessToken)
|
||||
|
||||
const me = await http.get<{ openid?: string | null }>('/api/users/me', {
|
||||
skipAuthRefresh: true,
|
||||
})
|
||||
if (me?.openid === localUser.openid) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 新 token 对应了不同账号,不能静默切换,清掉旧会话
|
||||
clearToken()
|
||||
clearUserInfo()
|
||||
return false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序启动时主动校验一次:token 还在就先 getMe 验签,
|
||||
* 只有 401 才触发自动续登;网络异常时保留本地会话,后续 401 再续。
|
||||
*/
|
||||
export async function ensureSessionFresh(): Promise<boolean> {
|
||||
const localUser = getUserInfoRaw()
|
||||
if (!localUser?.openid || isMockOpenid(localUser.openid)) return false
|
||||
if (!getToken()) return refreshSession()
|
||||
|
||||
try {
|
||||
await http.get('/api/users/me', { skipAuthRefresh: true })
|
||||
return true
|
||||
} catch (e) {
|
||||
const statusCode = (e as Error & { statusCode?: number })?.statusCode
|
||||
if (statusCode === 401) return refreshSession()
|
||||
return !!getToken()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -176,7 +176,7 @@ export function setDesignList(list: DesignItem[]) {
|
||||
|
||||
export function addDesign(product: any, count: number): DesignItem {
|
||||
const list = getDesignList()
|
||||
const iconImg = assetUrl(product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.png')
|
||||
const iconImg = assetUrl(product?.iconImg || PRODUCT_ICON_MAP[product?.id] || '/icon/四角星.svg')
|
||||
const item: DesignItem = {
|
||||
id: 'DSG' + Date.now(),
|
||||
productId: product.id,
|
||||
@@ -229,7 +229,7 @@ export function designToOrder(designId: string): OrderItem | null {
|
||||
|
||||
const rawIcon = design.productIcon?.startsWith('/icon/') || /^https?:/i.test(design.productIcon || '')
|
||||
? design.productIcon
|
||||
: PRODUCT_ICON_MAP[design.productId] || '/icon/四角星.png'
|
||||
: PRODUCT_ICON_MAP[design.productId] || '/icon/四角星.svg'
|
||||
|
||||
const iconImg = assetUrl(rawIcon)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user