feat: migrate ice venue module
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { request } from './request'
|
||||
|
||||
const toNumber = (value) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
const toBookingCourse = (backendCourse = {}) => ({
|
||||
id: backendCourse.id ?? null,
|
||||
title: backendCourse.title || '',
|
||||
start_time: backendCourse.start_time || '',
|
||||
end_time: backendCourse.end_time || '',
|
||||
coach_name: backendCourse.coach_name || '',
|
||||
capacity: toNumber(backendCourse.capacity),
|
||||
booked_count: toNumber(backendCourse.booked_count)
|
||||
})
|
||||
|
||||
const toBookingCard = (backendCard) => {
|
||||
if (!backendCard) return null
|
||||
return {
|
||||
id: backendCard.id ?? null,
|
||||
card_name: backendCard.card_name || '',
|
||||
remained_detail: backendCard.remained_detail || ''
|
||||
}
|
||||
}
|
||||
|
||||
const toBookingViewModel = (backendBooking = {}) => ({
|
||||
id: backendBooking.id,
|
||||
status: backendBooking.status || 'booked',
|
||||
checked_in_at: backendBooking.checked_in_at || null,
|
||||
cancelled_at: backendBooking.cancelled_at || null,
|
||||
created_at: backendBooking.created_at || '',
|
||||
course: toBookingCourse(backendBooking.course),
|
||||
card: toBookingCard(backendBooking.card)
|
||||
})
|
||||
|
||||
const toPaginatedBookings = (backendData = {}) => ({
|
||||
items: Array.isArray(backendData.items) ? backendData.items.map(toBookingViewModel) : [],
|
||||
total: toNumber(backendData.total),
|
||||
page: toNumber(backendData.page) || 1,
|
||||
page_size: toNumber(backendData.page_size) || 20
|
||||
})
|
||||
|
||||
const toQuery = (params = {}) => ({
|
||||
status: params.status || 'all',
|
||||
page: params.page || 1,
|
||||
page_size: params.pageSize || 20
|
||||
})
|
||||
|
||||
export const bookingApi = {
|
||||
getBookings(params = {}) {
|
||||
return request({
|
||||
url: '/api/bookings',
|
||||
data: toQuery(params)
|
||||
}).then(toPaginatedBookings)
|
||||
},
|
||||
|
||||
getBookingDetail(id) {
|
||||
return request({
|
||||
url: `/api/bookings/${id}`
|
||||
}).then(toBookingViewModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { request } from './request'
|
||||
|
||||
const toNumber = (value) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
const toMemberCard = (backendCard = {}) => ({
|
||||
id: backendCard.id,
|
||||
card_no: backendCard.card_no || '',
|
||||
card_name: backendCard.card_name || '',
|
||||
card_type: backendCard.card_type || 'times',
|
||||
backend_card_type: backendCard.backend_card_type || '',
|
||||
status: backendCard.status || 'active',
|
||||
total_times: toNumber(backendCard.total_times),
|
||||
remained_times: toNumber(backendCard.remained_times),
|
||||
original_balance: backendCard.original_balance || '0.00',
|
||||
gift_balance: backendCard.gift_balance || '0.00',
|
||||
balance: backendCard.balance || '0.00',
|
||||
start_date: backendCard.start_date || '',
|
||||
expire_date: backendCard.expire_date || '',
|
||||
remained_detail: backendCard.remained_detail || ''
|
||||
})
|
||||
|
||||
const toCardChange = (backendChange = {}) => ({
|
||||
id: backendChange.id,
|
||||
change_type: backendChange.change_type || 'adjustment',
|
||||
backend_change_type: backendChange.backend_change_type || '',
|
||||
delta: backendChange.delta || '0',
|
||||
post_times: toNumber(backendChange.post_times),
|
||||
post_balance: backendChange.post_balance || '0.00',
|
||||
memo: backendChange.memo || '',
|
||||
created_at: backendChange.created_at || ''
|
||||
})
|
||||
|
||||
const toCardHistory = (backendData = {}) => ({
|
||||
card: toMemberCard(backendData.card),
|
||||
items: Array.isArray(backendData.items) ? backendData.items.map(toCardChange) : [],
|
||||
total: toNumber(backendData.total),
|
||||
page: toNumber(backendData.page) || 1,
|
||||
page_size: toNumber(backendData.page_size) || 20
|
||||
})
|
||||
|
||||
export const cardApi = {
|
||||
getCards(status = 'all') {
|
||||
return request({
|
||||
url: '/api/members/me/cards',
|
||||
data: { status }
|
||||
}).then((data = {}) => (Array.isArray(data.items) ? data.items.map(toMemberCard) : []))
|
||||
},
|
||||
|
||||
getCardChanges(cardId, params = {}) {
|
||||
return request({
|
||||
url: `/api/members/me/cards/${cardId}/changes`,
|
||||
data: {
|
||||
page: params.page || 1,
|
||||
page_size: params.pageSize || 20
|
||||
}
|
||||
}).then(toCardHistory)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { request } from './request'
|
||||
|
||||
const toActivityDetail = (backendActivity = {}) => ({
|
||||
id: backendActivity.id,
|
||||
title: backendActivity.title || '',
|
||||
cover: backendActivity.cover || '',
|
||||
created_at: backendActivity.created_at || '',
|
||||
content_html: backendActivity.content_html || ''
|
||||
})
|
||||
|
||||
export const contentApi = {
|
||||
getActivityDetail(id) {
|
||||
return request({
|
||||
url: `/api/contents/activities/${id}`
|
||||
}).then(toActivityDetail)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { request } from './request'
|
||||
|
||||
const toNumber = (value) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
const toCoach = (backendCoach = {}) => ({
|
||||
id: backendCoach.id ?? null,
|
||||
name: backendCoach.name || ''
|
||||
})
|
||||
|
||||
export const toCourseViewModel = (backendCourse = {}) => {
|
||||
const coach = toCoach(backendCourse.coach)
|
||||
return {
|
||||
id: backendCourse.id,
|
||||
title: backendCourse.title || '',
|
||||
start_time: backendCourse.start_time || '',
|
||||
end_time: backendCourse.end_time || '',
|
||||
coach,
|
||||
coach_name: backendCourse.coach_name || coach.name,
|
||||
capacity: toNumber(backendCourse.capacity),
|
||||
booked_count: toNumber(backendCourse.booked_count),
|
||||
available_count: toNumber(backendCourse.available_count),
|
||||
is_booked: backendCourse.is_booked === true,
|
||||
my_booking_id: backendCourse.my_booking_id ?? null,
|
||||
cover: backendCourse.cover || '',
|
||||
detail_html: backendCourse.detail_html || '',
|
||||
}
|
||||
}
|
||||
|
||||
const toPaginatedCourses = (backendData = {}) => ({
|
||||
items: Array.isArray(backendData.items) ? backendData.items.map(toCourseViewModel) : [],
|
||||
total: toNumber(backendData.total),
|
||||
page: toNumber(backendData.page) || 1,
|
||||
page_size: toNumber(backendData.page_size) || 20
|
||||
})
|
||||
|
||||
const toQuery = (params = {}) => {
|
||||
const data = {}
|
||||
if (params.date) data.date = params.date
|
||||
if (params.coachId) data.coach_id = params.coachId
|
||||
if (params.page) data.page = params.page
|
||||
if (params.pageSize) data.page_size = params.pageSize
|
||||
return data
|
||||
}
|
||||
|
||||
export const courseApi = {
|
||||
getCoaches(status = 'active') {
|
||||
return request({
|
||||
url: '/api/coaches',
|
||||
data: { status }
|
||||
}).then((data = {}) => (Array.isArray(data.items) ? data.items.map(toCoach) : []))
|
||||
},
|
||||
|
||||
getCourses(params = {}) {
|
||||
return request({
|
||||
url: '/api/courses',
|
||||
data: toQuery(params)
|
||||
}).then(toPaginatedCourses)
|
||||
},
|
||||
|
||||
getCourseDetail(id) {
|
||||
return request({
|
||||
url: `/api/courses/${id}`
|
||||
}).then(toCourseViewModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { request } from './request'
|
||||
import { toCourseViewModel } from './course'
|
||||
|
||||
const toNumber = (value) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
const toBanner = (backendBanner = {}) => ({
|
||||
id: backendBanner.id,
|
||||
title: backendBanner.title || '',
|
||||
image: backendBanner.image || '',
|
||||
link_type: backendBanner.link_type || 'none',
|
||||
link_value: backendBanner.link_value || '',
|
||||
sort_order: toNumber(backendBanner.sort_order)
|
||||
})
|
||||
|
||||
const toActivitySummary = (backendActivity = {}) => ({
|
||||
id: backendActivity.id,
|
||||
title: backendActivity.title || '',
|
||||
cover: backendActivity.cover || '',
|
||||
created_at: backendActivity.created_at || ''
|
||||
})
|
||||
|
||||
const toHomeViewModel = (backendData = {}) => ({
|
||||
banners: Array.isArray(backendData.banners) ? backendData.banners.map(toBanner) : [],
|
||||
upcomingCourses: Array.isArray(backendData.upcoming_courses)
|
||||
? backendData.upcoming_courses.map(toCourseViewModel)
|
||||
: [],
|
||||
activities: Array.isArray(backendData.activities) ? backendData.activities.map(toActivitySummary) : [],
|
||||
venue: {
|
||||
name: backendData.venue?.name || '奥林匹克滑冰中心',
|
||||
}
|
||||
})
|
||||
|
||||
export const homeApi = {
|
||||
getHome(options = {}) {
|
||||
const data = {
|
||||
course_limit: options.courseLimit || 2,
|
||||
banner_limit: options.bannerLimit || 5
|
||||
}
|
||||
if (options.activityLimit !== undefined) {
|
||||
data.activity_limit = options.activityLimit
|
||||
}
|
||||
if (options.courseDate) data.course_date = options.courseDate
|
||||
|
||||
return request({
|
||||
url: '/api/contents/home',
|
||||
data
|
||||
}).then(toHomeViewModel)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { authStorage, request } from './request'
|
||||
|
||||
// 防腐层:小程序端只在本文件感知后端字段名,页面层统一使用下方 mapper 输出的前端模型。
|
||||
export const memberBackendContract = Object.freeze({
|
||||
wechatLoginResponse: [
|
||||
'access_token',
|
||||
'token_type',
|
||||
'expires_in',
|
||||
'member_bound',
|
||||
'member'
|
||||
],
|
||||
registerOrBindRequest: [
|
||||
'phone_code',
|
||||
'phone',
|
||||
'name',
|
||||
'gender',
|
||||
'birthday'
|
||||
],
|
||||
registerOrBindResponse: [
|
||||
'created',
|
||||
'bound',
|
||||
'member'
|
||||
],
|
||||
memberSummary: [
|
||||
'id',
|
||||
'name',
|
||||
'phone',
|
||||
'gender',
|
||||
'birthday',
|
||||
'joined_at'
|
||||
],
|
||||
memberProfile: [
|
||||
'id',
|
||||
'name',
|
||||
'phone',
|
||||
'gender',
|
||||
'gender_label',
|
||||
'birthday',
|
||||
'joined_at',
|
||||
'sales.id',
|
||||
'sales.name',
|
||||
'card_summary.total',
|
||||
'card_summary.active',
|
||||
'card_summary.disabled',
|
||||
'card_summary.expired'
|
||||
]
|
||||
})
|
||||
|
||||
const GENDER_LABELS = Object.freeze({
|
||||
male: '男',
|
||||
female: '女',
|
||||
unknown: '未设置'
|
||||
})
|
||||
|
||||
const emptyCardSummary = () => ({
|
||||
total: 0,
|
||||
active: 0,
|
||||
disabled: 0,
|
||||
expired: 0
|
||||
})
|
||||
|
||||
const toNumber = (value) => {
|
||||
const numberValue = Number(value)
|
||||
return Number.isFinite(numberValue) ? numberValue : 0
|
||||
}
|
||||
|
||||
const toMemberCardSummary = (backendCardSummary = {}) => ({
|
||||
total: toNumber(backendCardSummary.total),
|
||||
active: toNumber(backendCardSummary.active),
|
||||
disabled: toNumber(backendCardSummary.disabled),
|
||||
expired: toNumber(backendCardSummary.expired)
|
||||
})
|
||||
|
||||
const toMemberViewModel = (backendMember, options = {}) => {
|
||||
if (!backendMember || !backendMember.id) return null
|
||||
|
||||
const cardSummary = options.withProfileFields
|
||||
? toMemberCardSummary(backendMember.card_summary)
|
||||
: emptyCardSummary()
|
||||
|
||||
return {
|
||||
id: backendMember.id,
|
||||
name: backendMember.name || '',
|
||||
phone: backendMember.phone || '',
|
||||
gender: backendMember.gender || 'unknown',
|
||||
genderLabel: backendMember.gender_label || GENDER_LABELS[backendMember.gender] || GENDER_LABELS.unknown,
|
||||
birthday: backendMember.birthday || '',
|
||||
joinDate: backendMember.joined_at || '',
|
||||
salesId: backendMember.sales?.id || null,
|
||||
salesName: backendMember.sales?.name || '无',
|
||||
cardSummary,
|
||||
cardCount: cardSummary.total
|
||||
}
|
||||
}
|
||||
|
||||
const toWechatLoginViewModel = (backendData = {}) => ({
|
||||
accessToken: backendData.access_token || '',
|
||||
tokenType: backendData.token_type || 'Bearer',
|
||||
expiresIn: toNumber(backendData.expires_in),
|
||||
memberBound: backendData.member_bound === true,
|
||||
member: toMemberViewModel(backendData.member)
|
||||
})
|
||||
|
||||
const toRegisterOrBindPayload = (formData = {}) => ({
|
||||
phone_code: formData.phoneCode || null,
|
||||
phone: formData.phone || null,
|
||||
name: formData.name || null,
|
||||
gender: formData.gender || 'unknown',
|
||||
birthday: formData.birthday || null
|
||||
})
|
||||
|
||||
const toRegisterOrBindViewModel = (backendData = {}) => ({
|
||||
created: backendData.created === true,
|
||||
bound: backendData.bound === true,
|
||||
member: toMemberViewModel(backendData.member)
|
||||
})
|
||||
|
||||
const toMemberProfileViewModel = (backendData = {}) => {
|
||||
return toMemberViewModel(backendData, { withProfileFields: true })
|
||||
}
|
||||
|
||||
// 登录注册与绑定 API 接口组
|
||||
export const api = {
|
||||
// 1. 微信小程序登录 (用 code 换 token)
|
||||
wechatLogin(code) {
|
||||
return request({
|
||||
url: '/api/auth/wechat-login',
|
||||
method: 'POST',
|
||||
data: { code }
|
||||
}).then(toWechatLoginViewModel)
|
||||
},
|
||||
|
||||
// 2. 绑定或注册会员
|
||||
registerOrBind(data) {
|
||||
return request({
|
||||
url: '/api/auth/register-or-bind',
|
||||
method: 'POST',
|
||||
data: toRegisterOrBindPayload(data)
|
||||
}).then(toRegisterOrBindViewModel)
|
||||
},
|
||||
|
||||
// 3. 获取当前登录会员信息
|
||||
getMemberMe() {
|
||||
return request({
|
||||
url: '/api/members/me',
|
||||
method: 'GET'
|
||||
}).then(toMemberProfileViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
export { authStorage, request }
|
||||
@@ -0,0 +1,134 @@
|
||||
const KEY_TOKEN = 'ICE_MEMBER_TOKEN'
|
||||
const KEY_TOKEN_EXPIRES_AT = 'ICE_MEMBER_TOKEN_EXPIRES_AT'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_ICE_API_BASE_URL || 'http://127.0.0.1:8000'
|
||||
|
||||
export const authStorage = {
|
||||
setToken(token, expiresIn) {
|
||||
uni.setStorageSync(KEY_TOKEN, token)
|
||||
if (expiresIn) {
|
||||
uni.setStorageSync(KEY_TOKEN_EXPIRES_AT, Date.now() + Number(expiresIn) * 1000)
|
||||
}
|
||||
},
|
||||
getToken() {
|
||||
return uni.getStorageSync(KEY_TOKEN)
|
||||
},
|
||||
isTokenExpired() {
|
||||
const expiresAt = Number(uni.getStorageSync(KEY_TOKEN_EXPIRES_AT) || 0)
|
||||
return expiresAt > 0 && Date.now() >= expiresAt
|
||||
},
|
||||
clear() {
|
||||
uni.removeStorageSync(KEY_TOKEN)
|
||||
uni.removeStorageSync(KEY_TOKEN_EXPIRES_AT)
|
||||
}
|
||||
}
|
||||
|
||||
export const normalizeApiError = (rawError = {}) => {
|
||||
const statusCode = rawError.statusCode
|
||||
const body = rawError.data || rawError
|
||||
const code = body?.code
|
||||
const backendMessage = body?.message || ''
|
||||
|
||||
if (statusCode === 401 || code === 40001) {
|
||||
return {
|
||||
type: 'auth_expired',
|
||||
code,
|
||||
statusCode,
|
||||
message: '登录状态已过期,请重新登录',
|
||||
raw: rawError
|
||||
}
|
||||
}
|
||||
|
||||
if (code === 40002) {
|
||||
return {
|
||||
type: 'profile_required',
|
||||
code,
|
||||
statusCode,
|
||||
message: '请先完善会员资料',
|
||||
raw: rawError
|
||||
}
|
||||
}
|
||||
|
||||
if (code === 40003) {
|
||||
return {
|
||||
type: 'bind_conflict',
|
||||
code,
|
||||
statusCode,
|
||||
message: backendMessage || '该手机号已绑定其他微信账号,请联系场馆前台处理',
|
||||
raw: rawError
|
||||
}
|
||||
}
|
||||
|
||||
if (!statusCode) {
|
||||
return {
|
||||
type: 'network_error',
|
||||
code,
|
||||
statusCode,
|
||||
message: '网络连接失败,请检查后重试',
|
||||
raw: rawError
|
||||
}
|
||||
}
|
||||
|
||||
if (statusCode === 404 || code === 40400) {
|
||||
return {
|
||||
type: 'not_found',
|
||||
code,
|
||||
statusCode,
|
||||
message: backendMessage || '未找到对应信息或无访问权限',
|
||||
raw: rawError
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'api_error',
|
||||
code,
|
||||
statusCode,
|
||||
message: backendMessage || '服务暂时不可用,请稍后重试',
|
||||
raw: rawError
|
||||
}
|
||||
}
|
||||
|
||||
export function request(options = {}) {
|
||||
const token = authStorage.getToken()
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.header || {})
|
||||
}
|
||||
|
||||
if (token) {
|
||||
header.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: options.url.startsWith('http') ? options.url : `${API_BASE_URL}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header,
|
||||
success: (res) => {
|
||||
const body = res.data
|
||||
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
if (body && body.code === 0) {
|
||||
resolve(body.data)
|
||||
return
|
||||
}
|
||||
|
||||
reject(normalizeApiError({
|
||||
statusCode: res.statusCode,
|
||||
data: body
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
reject(normalizeApiError({
|
||||
statusCode: res.statusCode,
|
||||
data: body
|
||||
}))
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(normalizeApiError(err))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { api, authStorage } from './member'
|
||||
|
||||
export const SESSION_STATUS = Object.freeze({
|
||||
CHECKING: 'checking',
|
||||
GUEST: 'guest',
|
||||
EXPIRED: 'expired',
|
||||
PROFILE_REQUIRED: 'profile_required',
|
||||
MEMBER: 'member',
|
||||
NETWORK_ERROR: 'network_error'
|
||||
})
|
||||
|
||||
const createLoginCode = () => new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (loginRes) => {
|
||||
if (loginRes.code) {
|
||||
resolve(loginRes.code)
|
||||
return
|
||||
}
|
||||
reject({
|
||||
type: 'login_error',
|
||||
message: '微信登录没有返回有效凭证,请重新尝试'
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
reject({
|
||||
type: 'login_error',
|
||||
message: '微信登录失败,请稍后重试'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const loginAndStoreToken = async () => {
|
||||
const code = await createLoginCode()
|
||||
const data = await api.wechatLogin(code)
|
||||
if (!data?.accessToken) {
|
||||
throw {
|
||||
type: 'login_error',
|
||||
message: '登录失败,请稍后重试'
|
||||
}
|
||||
}
|
||||
authStorage.setToken(data.accessToken, data.expiresIn)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getMemberSession = async () => {
|
||||
const token = authStorage.getToken()
|
||||
if (!token) {
|
||||
return { status: SESSION_STATUS.GUEST }
|
||||
}
|
||||
|
||||
if (authStorage.isTokenExpired()) {
|
||||
authStorage.clear()
|
||||
return {
|
||||
status: SESSION_STATUS.EXPIRED,
|
||||
message: '登录状态已过期,请重新登录'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const member = await api.getMemberMe()
|
||||
return {
|
||||
status: SESSION_STATUS.MEMBER,
|
||||
member
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.type === 'profile_required' || err?.code === 40002) {
|
||||
return {
|
||||
status: SESSION_STATUS.PROFILE_REQUIRED,
|
||||
message: '请先完善会员资料'
|
||||
}
|
||||
}
|
||||
|
||||
if (err?.type === 'auth_expired' || err?.code === 40001) {
|
||||
authStorage.clear()
|
||||
return {
|
||||
status: SESSION_STATUS.EXPIRED,
|
||||
message: '登录状态已过期,请重新登录'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: SESSION_STATUS.NETWORK_ERROR,
|
||||
message: err?.message || '暂时无法连接服务,请稍后重试',
|
||||
error: err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const isSessionBlockingError = (err) => {
|
||||
return ['auth_expired', 'profile_required'].includes(err?.type) || [40001, 40002].includes(err?.code)
|
||||
}
|
||||
|
||||
export const sessionStatusFromError = (err) => {
|
||||
if (err?.type === 'auth_expired' || err?.code === 40001) {
|
||||
authStorage.clear()
|
||||
return SESSION_STATUS.EXPIRED
|
||||
}
|
||||
if (err?.type === 'profile_required' || err?.code === 40002) {
|
||||
return SESSION_STATUS.PROFILE_REQUIRED
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export const errorMessage = (err, fallback = '操作失败,请稍后重试') => {
|
||||
return err?.message || fallback
|
||||
}
|
||||
Reference in New Issue
Block a user