完成一键登录和支付功能

This commit is contained in:
future
2026-06-28 14:49:21 +08:00
parent 74876e0923
commit 09e587cb65
94 changed files with 10284 additions and 2666 deletions
+236 -5
View File
@@ -1,5 +1,9 @@
import request from "@/utils/request.js"
/**
* 微信小程序登录
* @param {object} params - 登录参数 { code: string }
*/
export function login(params) {
return request.post('/member/auth/miniapp/login', params)
}
@@ -28,23 +32,44 @@ export function oneClickLogin(params) {
return request.post('/auth/phone/one-click-login', params)
}
/**
* 退出登录
*/
export function logout() {
return request.post('/member/auth/logout')
}
/**
* 获取签到二维码
* @param {object} options - 请求选项 { cache: boolean, cacheTime: number }
*/
export function getQRCode(options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
return request.get('/checkIn/qrcode', {}, options)
}
/**
* 扫码签到
* @param {object} params - { qrcode: string, memberId: number }
*/
export function checkIn(params) {
return request.post('/checkIn/scan', params)
}
/**
* 获取签到记录(分页)
* @param {object} params - { page: number, size: number }
* @param {object} options - 请求选项
*/
export function getCheckInRecords(params = {}, options = {}) {
const { page = 0, size = 5 } = params
return request.post('/checkIn/page', { page, size }, options)
}
/**
* 获取用户未读消息数量
* @param {number} userId - 用户ID
* @param {object} options - 请求选项
*/
export function getUnreadMessageCount(userId, options = {}) {
return request.get(`/messages/user/${userId}/unread`, {}, options)
}
@@ -109,12 +134,17 @@ export function deleteMessage(id, options = {}) {
return request.delete(`/messages/${id}`, {}, options)
}
/**
* 获取用户信息(基础信息缓存)
* @param {object} options - 请求选项 { cache: boolean, cacheTime: number }
*/
export function getUserInfo(options = { cache: true, cacheTime: 30 * 60 * 1000 }) {
return request.get('/member/info', {}, options)
}
/**
* 获取会员详细信息(包含会员卡、积分等)
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getMemberDetail(options = { cache: false }) {
return request.get('/member/info', {}, options)
@@ -122,14 +152,17 @@ export function getMemberDetail(options = { cache: false }) {
/**
* 获取签到统计
* @param {object} params - 查询参数
* @param {string} params.startDate - 开始日期(yyyy-MM-dd
* @param {string} params.endDate - 结束日期(yyyy-MM-dd
* @param {object} params - 查询参数 { startDate: string, endDate: string }
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getCheckInStats(params = {}, options = { cache: false }) {
return request.get('/checkIn/statistics', params, options)
}
/**
* 更新用户信息
* @param {object} params - 用户信息参数
*/
export function updateUserInfo(params) {
return request.put('/member/info', params)
}
@@ -145,10 +178,156 @@ export function purchaseMemberCard(params) {
return request.post('/member-card-records/purchase', params)
}
/**
* 获取我的会员卡列表
* @param {number} memberId - 会员ID
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getMyMemberCards(memberId, options = { cache: false }) {
return request.get(`/member-card-records/my-cards/${memberId}`, {}, options)
}
/**
* 获取会员的所有会员卡(支持状态筛选)
* @param {number} memberId - 会员ID
* @param {string} status - 状态筛选:all-全部, active-有效, expired-已失效
* @param {object} options - 请求选项
*/
export function getMyMemberCardsWithStatus(memberId, status = 'all', options = { cache: false }) {
return request.get(`/member-card-records/my-cards-with-status/${memberId}`, { status }, options)
}
/**
* 获取会员的主要有效会员卡(只返回一条)
* 优先返回临期卡,其次返回普通有效卡
* @param {number} memberId - 会员ID
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getPrimaryMemberCard(memberId, options = { cache: false }) {
return request.get(`/member-card-records/primary/${memberId}`, {}, options)
}
/**
* 获取会员卡交易/流水记录(按会员ID)
* @param {number} memberId - 会员ID
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getMemberCardTransactions(memberId, options = { cache: false }) {
return request.get(`/member-card-transactions/member/${memberId}`, {}, options)
}
/**
* 获取会员卡交易/流水记录(按会员卡记录ID)
* @param {number} recordId - 会员卡记录ID
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getMemberCardTransactionsByRecordId(recordId, options = { cache: false }) {
return request.get(`/member-card-transactions/record/${recordId}`, {}, options)
}
/**
* 获取当前有效的会员卡列表
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getActiveMemberCards(options = { cache: false }) {
return request.get('/member-cards/active', {}, options)
}
/**
* 根据ID获取会员卡详情
* @param {number} cardId - 会员卡ID
* @param {object} options - 请求选项 { cache: boolean }
*/
export function getMemberCardById(cardId, options = { cache: false }) {
return request.get(`/member-cards/${cardId}`, {}, options)
}
// ========== 支付密码相关API ==========
/**
* 检查会员是否已设置支付密码
* @param {number} memberId - 会员ID
* @param {object} options - 请求选项
*/
export function checkPayPasswordSet(memberId, options = {}) {
return request.get(`/pay-password/check/${memberId}`, {}, options)
}
/**
* 设置支付密码
* @param {number} memberId - 会员ID
* @param {string} password - 支付密码
* @param {object} options - 请求选项
*/
export function setPayPassword(memberId, password, options = {}) {
return request.post(`/pay-password/set/${memberId}`, { password }, options)
}
/**
* 验证支付密码
* @param {number} memberId - 会员ID
* @param {string} password - 支付密码
* @param {object} options - 请求选项
*/
export function verifyPayPassword(memberId, password, options = {}) {
return request.post(`/pay-password/verify/${memberId}`, { password }, options)
}
/**
* 重置支付密码
* @param {number} memberId - 会员ID
* @param {string} oldPassword - 旧密码
* @param {string} newPassword - 新密码
* @param {object} options - 请求选项
*/
export function resetPayPassword(memberId, oldPassword, newPassword, options = {}) {
return request.post(`/pay-password/reset/${memberId}`, { oldPassword, newPassword }, options)
}
// ========== 储值卡相关API ==========
/**
* 获取储值卡信息(包含余额、消费记录等)
* @param {number} memberId - 会员ID
* @param {object} options - 请求选项
*/
export function getStoredCardInfo(memberId, options = {}) {
return request.get(`/stored-card/${memberId}`, {}, options)
}
/**
* 获取储值卡余额
* @param {number} memberId - 会员ID
* @param {object} options - 请求选项
*/
export function getStoredCardBalance(memberId, options = {}) {
return request.get(`/stored-card/${memberId}/balance`, {}, options)
}
/**
* 储值卡支付(验证密码并扣减余额)
* @param {number} memberId - 会员ID
* @param {string} password - 支付密码
* @param {number} amount - 支付金额
* @param {object} options - 请求选项
*/
export function payWithStoredCard(memberId, password, amount, options = {}) {
return request.post(`/stored-card/pay/${memberId}`, {
password,
amount
}, options)
}
/**
* 查询储值卡充值记录
* @param {number} memberId - 会员ID
* @param {number} page - 页码
* @param {number} size - 每页数量
*/
export function getStoredCardRechargeRecords(memberId, page = 1, size = 20, options = {}) {
return request.get(`/stored-card/${memberId}/recharges?page=${page}&size=${size}`, {}, options)
}
// ========== 支付相关API ==========
/**
@@ -175,20 +354,50 @@ export function createPayment(params) {
/**
* 创建扫码支付订单
* @param {Object} params - 支付参数
* @param {object} params - 支付参数
*/
export function createQrCodePayment(params) {
return request.post('/payment/qrcode/create', params)
}
/**
* 查询支付状态
* 查询支付状态(本地数据库)
* @param {string} orderId - 订单ID
*/
export function getPaymentStatus(orderId) {
return request.get(`/payment/${orderId}`, {})
}
/**
* 查询汇付官方支付状态
* @param {string} outOrdId - 商户订单号
* @param {string} hfSeqId - 汇付全局流水号(可选)
*/
export function queryHuifuTradeByOrderId(outOrdId, hfSeqId = null) {
let url = `/payment/huifu/query?outOrdId=${outOrdId}`
if (hfSeqId) {
url += `&hfSeqId=${hfSeqId}`
}
return request.get(url, {})
}
/**
* 查询待支付订单
* @param {number} memberId - 会员ID
* @param {string} orderType - 订单类型
*/
export function getPendingOrder(memberId, orderType) {
return request.get(`/payment/pending/${memberId}?orderType=${orderType}`, {})
}
/**
* 关闭订单
* @param {string} orderId - 订单ID
*/
export function closeOrder(orderId) {
return request.post(`/payment/${orderId}/close`, {})
}
/**
* 申请退款
* @param {string} orderId - 订单ID
@@ -198,14 +407,30 @@ export function refundPayment(orderId, refundAmt) {
return request.post(`/payment/${orderId}/refund`, { refundAmt })
}
// ========== 课程相关API ==========
/**
* 获取推荐课程列表
* @param {object} options - 请求选项 { cache: boolean, cacheTime: number }
*/
export function getRecommendCourses(options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
return request.get('/course/recommend', {}, options)
}
/**
* 获取课程详情
* @param {number} id - 课程ID
* @param {object} options - 请求选项 { cache: boolean, cacheTime: number }
*/
export function getCourseDetail(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
return request.get(`/course/${id}`, {}, options)
}
/**
* 获取团课列表(分页)
* @param {object} params - 查询参数 { page: number, size: number, sort: string, order: string, keyword: string }
* @param {object} options - 请求选项 { cache: boolean, cacheTime: number }
*/
export function getGroupCoursePage(params = {}, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
const { page = 0, size = 10, sort = 'id', order = 'asc', keyword } = params
return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
@@ -278,6 +503,12 @@ export default {
updateUserInfo,
purchaseMemberCard,
getMyMemberCards,
getMyMemberCardsWithStatus,
getPrimaryMemberCard,
getStoredCardInfo,
getStoredCardBalance,
payWithStoredCard,
getStoredCardRechargeRecords,
createPayment,
createQrCodePayment,
getPaymentStatus,
+3 -1
View File
@@ -31,7 +31,9 @@ export const PAGE = {
REFERRAL: '/pages/memberInfo/referral',
MY_COURSES: '/pages/memberInfo/myCourses',
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory',
PURCHASE_CARD: '/pages/memberInfo/purchaseCard'
PURCHASE_CARD: '/pages/memberInfo/purchaseCard',
SET_PAY_PASSWORD: '/pages/memberInfo/setPayPassword',
RECHARGE_STORED_CARD: '/pages/memberInfo/rechargeStoredCard'
}
/** 底部 TabBar 页面路径,顺序与 TabBar.vue 一致 */
@@ -0,0 +1,249 @@
/**
* 登录拦截Hook
* 用于在需要登录的操作前检查用户登录状态,未登录则弹出登录框
*
* 使用方式:
* import { useLoginGuard } from '@/common/hooks/useLoginGuard.js'
*
* const { checkLogin, requireLogin } = useLoginGuard()
*
* // 方式1:检查登录状态(返回布尔值)
* if (!checkLogin()) return
*
* // 方式2:需要登录才执行(推荐)
* await requireLogin()
* // 之后的代码只有在登录后才执行
*
* // 方式3:在点击事件中使用
* async function handleClick() {
* await requireLogin()
* // 执行需要登录的操作
* }
*/
import { getToken } from '@/utils/request.js'
import { loadMemberStore } from '@/common/memberInfo/store.js'
// 保存当前页面的登录弹窗引用
let loginModalRef = null
/**
* 注册登录弹窗组件
* 在App.vue或页面onMounted中调用
* @param {Object} modalRef - loginModal组件的ref
*/
export function registerLoginModal(modalRef) {
loginModalRef = modalRef
}
/**
* 获取当前登录状态
* @returns {Boolean}
*/
export function isLoggedIn() {
const token = getToken()
const isLoginStorage = uni.getStorageSync('isLogin')
return !!(token || isLoginStorage)
}
/**
* 获取登录用户信息
* @returns {Object|null}
*/
export function getLoginUser() {
const store = loadMemberStore()
return store.profile || null
}
/**
* 使用登录拦截Hook
* @returns {Object}
*/
export function useLoginGuard() {
/**
* 检查登录状态(同步)
* @returns {Boolean} 是否已登录
*/
function checkLogin() {
return isLoggedIn()
}
/**
* 需要登录才继续执行(异步)
* 如果未登录,弹出登录框,等待用户登录或取消
* @param {Object} options - 配置选项
* @param {String} options.title - 弹窗标题
* @param {String} options.subtitle - 弹窗副标题
* @param {Function} options.onSuccess - 登录成功回调
* @param {Function} options.onCancel - 取消登录回调
* @returns {Promise<Boolean>} 是否已登录(用户登录成功返回true,取消返回false)
*/
async function requireLogin(options = {}) {
// 如果已经登录,直接返回true
if (isLoggedIn()) {
return true
}
// 如果传入了onSuccess回调,先检查
if (options.onSuccess) {
const store = loadMemberStore()
if (store.profile?.id) {
options.onSuccess(store.profile)
return true
}
}
// 显示登录弹窗
return new Promise((resolve) => {
if (!loginModalRef) {
console.warn('[useLoginGuard] 登录弹窗未注册,请先调用 registerLoginModal')
// 尝试跳转到登录页
uni.navigateTo({
url: '/pages/memberInfo/login',
fail: () => {
uni.showToast({
title: '请先登录',
icon: 'none'
})
}
})
resolve(false)
return
}
// 保存resolve函数
let resolvePromise = resolve
// 显示登录弹窗
loginModalRef.show()
// 监听登录成功
const loginSuccessHandler = (result) => {
if (options.onSuccess) {
options.onSuccess(result)
}
resolvePromise(true)
}
// 监听登录失败/取消
const closeHandler = () => {
if (options.onCancel) {
options.onCancel()
}
resolvePromise(false)
}
// 注册事件监听
uni.$once('loginModal:success', loginSuccessHandler)
uni.$once('loginModal:close', closeHandler)
// 设置超时
setTimeout(() => {
uni.$off('loginModal:success', loginSuccessHandler)
uni.$off('loginModal:close', closeHandler)
}, 60000) // 60秒超时
})
}
/**
* 安全执行函数(仅登录后执行)
* @param {Function} fn - 要执行的函数
* @param {Object} options - 配置选项
* @returns {Promise<any>} 函数返回值或null
*/
async function safeExecute(fn, options = {}) {
const loggedIn = await requireLogin(options)
if (loggedIn && typeof fn === 'function') {
return await fn()
}
return null
}
/**
* 包装需要登录的API调用
* @param {Function} apiFn - API函数
* @param {Array} args - API参数
* @param {Object} options - 配置选项
* @returns {Promise<any>}
*/
async function callLoggedInApi(apiFn, args = [], options = {}) {
const loggedIn = await requireLogin(options)
if (loggedIn && typeof apiFn === 'function') {
return await apiFn(...args)
}
throw new Error('用户未登录')
}
return {
checkLogin,
requireLogin,
safeExecute,
callLoggedInApi,
isLoggedIn,
getLoginUser
}
}
/**
* 创建登录拦截Mixin(用于选项式API)
* @returns {Object}
*/
export function loginGuardMixin() {
return {
data() {
return {
isLogin: false
}
},
onLoad() {
this.checkLoginStatus()
},
onShow() {
this.checkLoginStatus()
},
methods: {
checkLoginStatus() {
this.isLogin = isLoggedIn()
},
async ensureLogin() {
if (!this.isLogin) {
if (!loginModalRef) {
uni.navigateTo({
url: '/pages/memberInfo/login'
})
return false
}
loginModalRef.show()
return false
}
return true
}
}
}
}
/**
* 路由守卫配置
* 配置需要登录的页面路径
*/
export const protectedRoutes = [
'/pages/memberInfo/memberInfo',
'/pages/memberInfo/purchaseCard',
'/pages/memberInfo/memberCard',
'/pages/memberInfo/checkIn',
'/pages/memberInfo/booking',
'/pages/memberInfo/coupons',
'/pages/memberInfo/points',
'/pages/memberInfo/userInfo'
]
/**
* 检查当前页面是否需要登录
* @param {String} currentPage - 当前页面路径
* @returns {Boolean}
*/
export function isProtectedRoute(currentPage) {
return protectedRoutes.some(route => currentPage.includes(route))
}
+4 -1
View File
@@ -12,7 +12,10 @@ export {
cancelOngoingBooking,
renewMemberCard,
parseLocalDate,
saveUserProfile
saveUserProfile,
getCurrentMemberId,
getLoginMemberInfo,
getToken
} from './store.js'
export {
getLatestBodyTestRecord,
+67 -30
View File
@@ -1,4 +1,3 @@
import { formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
import {
getDefaultBodyTestState,
mergeBodyTestState,
@@ -10,14 +9,36 @@ import {
mergeModuleState,
finalizeModules
} from './moduleStore.js'
import {
getDefaultCourseCatalog,
import { getDefaultCourseCatalog,
mergeCourseCatalog,
canCancelBooking
} from './bookingStore.js'
import { getMemberId as requestGetMemberId } from '@/utils/request.js'
const STORAGE_KEY = 'gym_member_info_v1'
/**
* 获取当前登录会员ID
* @deprecated 请使用 @/utils/request.js 中的 getMemberId
*/
export function getCurrentMemberId() {
return requestGetMemberId()
}
/**
* 获取登录信息
*/
export function getLoginMemberInfo() {
return uni.getStorageSync('loginMemberInfo') || null
}
/**
* 获取token
*/
export function getToken() {
return uni.getStorageSync('token') || null
}
export function buildCardTip(remainingDays) {
return `距离下次到期还有${remainingDays}天,请及时续费`
}
@@ -41,12 +62,6 @@ export function syncStats(store) {
function finalizeStore(store) {
syncStats(store)
applyCardInfo(store)
if (store.profile?.avatar) {
store.memberProfile.avatar = store.profile.avatar
}
if (store.profile?.phone) {
store.memberProfile.phone = formatMemberCenterPhone(store.profile.phone)
}
const latestBodyTest = getLatestBodyTestRecord(store)
if (latestBodyTest) {
const previous = store.bodyTest.records[1]
@@ -60,11 +75,10 @@ function finalizeStore(store) {
function getDefaultStore() {
return finalizeStore({
profile: {},
memberProfile: {},
stats: {},
cardInfo: {},
card: {},
cards: [],
records: [],
ongoingBookings: [],
historyBookings: [],
@@ -74,7 +88,11 @@ function getDefaultStore() {
modules: getDefaultModuleState(),
courseCatalog: getDefaultCourseCatalog(),
couponPoints: {},
referral: {}
referral: {},
storedCard: {
balance: 0,
zeroVerified: false
}
})
}
@@ -83,11 +101,10 @@ export { getDefaultStore }
function mergeDefaults(saved) {
const defaults = getDefaultStore()
return finalizeStore({
profile: { ...defaults.profile, ...(saved.profile || {}) },
memberProfile: { ...defaults.memberProfile, ...(saved.memberProfile || {}) },
stats: { ...defaults.stats, ...(saved.stats || {}) },
cardInfo: { ...defaults.cardInfo, ...(saved.cardInfo || {}) },
card: { ...defaults.card, ...(saved.card || {}) },
cards: saved.cards?.length ? saved.cards : defaults.cards,
records: saved.records?.length ? saved.records : defaults.records,
ongoingBookings: saved.ongoingBookings ?? defaults.ongoingBookings,
historyBookings: saved.historyBookings ?? defaults.historyBookings,
@@ -97,7 +114,8 @@ function mergeDefaults(saved) {
modules: mergeModuleState(saved.modules),
courseCatalog: mergeCourseCatalog(saved.courseCatalog),
couponPoints: { ...defaults.couponPoints, ...(saved.couponPoints || {}) },
referral: { ...defaults.referral, ...(saved.referral || {}) }
referral: { ...defaults.referral, ...(saved.referral || {}) },
storedCard: { ...defaults.storedCard, ...(saved.storedCard || {}) }
})
}
@@ -193,21 +211,33 @@ export function getBookingPreview(store, limit = 2) {
return store.ongoingBookings.slice(0, limit).map(toBookingPreviewItem)
}
export function getExpiringCards(store, daysThreshold = 30) {
if (!store.cards || store.cards.length === 0) {
return []
}
return store.cards.filter(card => {
const remainingDays = computeRemainingDays(card.validityEnd || card.expireTime)
return remainingDays > 0 && remainingDays <= daysThreshold
}).sort((a, b) => {
const daysA = computeRemainingDays(a.validityEnd || a.expireTime)
const daysB = computeRemainingDays(b.validityEnd || b.expireTime)
return daysA - daysB
})
}
export function getCenterPageData(store) {
const expiringCards = getExpiringCards(store, 30)
return {
userInfo: { ...store.memberProfile },
stats: {
checkInCount: store.stats?.checkInCount ?? 0,
trainingHours: store.stats?.trainingHours ?? 0,
pointsBalance: store.stats?.pointsBalance ?? 0
},
cardInfo: { ...store.cardInfo },
expiringCards: expiringCards,
bookingPreview: getBookingPreview(store),
checkIns: store.checkIns.map((item) => ({ ...item })),
bodyReport: {
...store.bodyReport,
weight: store.profile.weight || store.bodyReport.weight
},
bodyReport: { ...store.bodyReport },
couponPoints: {
...store.couponPoints,
points: store.stats.pointsBalance
@@ -290,16 +320,9 @@ export function renewMemberCard(store, addDays = 90) {
}
export function saveUserProfile(store, profile) {
const phone = normalizePhoneForStore(profile.phone ?? store.profile.phone)
store.profile = { ...store.profile, ...profile, phone }
store.memberProfile = {
...store.memberProfile,
name: store.profile.name,
phone: formatMemberCenterPhone(store.profile.phone),
avatar: store.profile.avatar || store.memberProfile.avatar
}
if (store.profile.weight) {
store.bodyReport.weight = store.profile.weight
// 不再保存profile到store,用户信息从loginMemberInfo获取
if (profile.weight) {
store.bodyReport.weight = profile.weight
}
finalizeStore(store)
saveMemberStore(store)
@@ -311,3 +334,17 @@ export function persistMemberStore(store) {
saveMemberStore(store)
return store
}
export function getStoredCardBalance() {
console.log('[storedCard] 余额不使用缓存,每次从后端读取')
return null
}
export function saveStoredCardBalance(balance) {
console.log('[storedCard] 余额不使用缓存,跳过保存')
return Number(balance) || 0
}
export function clearStoredCardCache() {
console.log('[storedCard] 余额不使用缓存,无需清除')
}
+304
View File
@@ -0,0 +1,304 @@
/**
* 路由权限拦截器
* 自动拦截配置好的路径,未登录时自动弹出登录弹窗
*
* 使用方式:在 App.vue 中 import 并调用 init() 即可
* 无需在每个页面单独配置
*/
import { getToken } from '@/utils/request.js'
/**
* 白名单 - 不需要登录即可访问的页面
*/
const WHITE_LIST = [
'/pages/index/index',
'/pages/course/course',
'/pages/memberInfo/login',
'/pages/memberInfo/register',
'/pages/memberInfo/forgetPassword',
'/pages/about/about',
'/pages/help/help',
'/pages/notice/notice',
'/pages/product/product',
'/pages/store/store',
'/pages/article/article',
'/pages/search/search'
]
/**
* 需要登录的页面路径前缀
* 匹配以下前缀的页面都需要登录
*/
const LOGIN_REQUIRED_PREFIXES = [
'/pages/memberInfo/memberCard',
'/pages/memberInfo/purchaseCard',
'/pages/memberInfo/checkIn',
'/pages/memberInfo/booking',
'/pages/memberInfo/coupons',
'/pages/memberInfo/points',
'/pages/memberInfo/userInfo',
'/pages/memberInfo/order',
'/pages/memberInfo/settings',
'/pages/memberInfo/feedback',
'/pages/memberInfo/invite',
'/pages/memberInfo/recharge'
]
/**
* 需要登录的具体页面路径
*/
const LOGIN_REQUIRED_PATHS = [
'/pages/memberInfo/memberInfo'
]
/**
* 检查当前路径是否需要登录
* @param {String} url - 页面路径(可能包含query参数)
* @returns {Boolean}
*/
function isNeedLogin(url) {
// 去掉query参数
const path = url.split('?')[0]
// 检查白名单
if (WHITE_LIST.includes(path)) {
return false
}
// 检查具体路径
if (LOGIN_REQUIRED_PATHS.includes(path)) {
return true
}
// 检查路径前缀
if (LOGIN_REQUIRED_PREFIXES.some(prefix => path.startsWith(prefix))) {
return true
}
return false
}
/**
* 检查用户是否已登录
* @returns {Boolean}
*/
function isLoggedIn() {
const token = getToken()
const isLoginStorage = uni.getStorageSync('isLogin')
return !!(token || isLoginStorage)
}
/**
* 登录弹窗组件引用(需要在App.vue中注册)
*/
let loginModalRef = null
/**
* 注册登录弹窗组件
* @param {Object} modalRef - LoginModal组件的ref
*/
export function registerLoginModal(modalRef) {
loginModalRef = modalRef
}
/**
* 显示登录弹窗
*/
function showLoginModal() {
if (loginModalRef) {
loginModalRef.show()
} else {
// 如果没有注册弹窗,跳转到登录页
uni.navigateTo({
url: '/pages/memberInfo/login'
})
}
}
/**
* 记录用户原本要访问的页面
* @param {String} url - 页面路径
*/
function saveRedirectUrl(url) {
uni.setStorageSync('redirectUrl', url)
}
/**
* 获取用户原本要访问的页面
* @returns {String|null}
*/
export function getRedirectUrl() {
const url = uni.getStorageSync('redirectUrl')
uni.removeStorageSync('redirectUrl')
return url
}
/**
* 跳转到用户原本要访问的页面
*/
export function redirectToSavedUrl() {
const url = getRedirectUrl()
if (url) {
uni.navigateTo({
url: url,
fail: () => {
uni.switchTab({
url: '/pages/index/index'
})
}
})
}
}
/**
* 初始化路由拦截器
*/
export function initPermissionInterceptor() {
console.log('[PermissionInterceptor] 初始化路由拦截器')
// 拦截 navigateTo
uni.addInterceptor('navigateTo', {
invoke(args) {
const url = args.url
if (isNeedLogin(url) && !isLoggedIn()) {
console.log('[PermissionInterceptor] 拦截 navigateTo:', url)
saveRedirectUrl(url)
showLoginModal()
return false
}
return true
},
success() {
// 跳转成功
},
fail(err) {
console.error('[PermissionInterceptor] navigateTo 失败:', err)
}
})
// 拦截 redirectTo
uni.addInterceptor('redirectTo', {
invoke(args) {
const url = args.url
if (isNeedLogin(url) && !isLoggedIn()) {
console.log('[PermissionInterceptor] 拦截 redirectTo:', url)
saveRedirectUrl(url)
showLoginModal()
return false
}
return true
},
success() {},
fail(err) {
console.error('[PermissionInterceptor] redirectTo 失败:', err)
}
})
// 拦截 reLaunch
uni.addInterceptor('reLaunch', {
invoke(args) {
const url = args.url
if (isNeedLogin(url) && !isLoggedIn()) {
console.log('[PermissionInterceptor] 拦截 reLaunch:', url)
saveRedirectUrl(url)
showLoginModal()
return false
}
return true
},
success() {},
fail(err) {
console.error('[PermissionInterceptor] reLaunch 失败:', err)
}
})
// 拦截 switchTab
uni.addInterceptor('switchTab', {
invoke(args) {
const url = args.url
if (isNeedLogin(url) && !isLoggedIn()) {
console.log('[PermissionInterceptor] 拦截 switchTab:', url)
saveRedirectUrl(url)
showLoginModal()
return false
}
return true
},
success() {},
fail(err) {
console.error('[PermissionInterceptor] switchTab 失败:', err)
}
})
// 拦截 navigateBack
uni.addInterceptor('navigateBack', {
invoke() {
return true
},
success() {},
fail() {}
})
console.log('[PermissionInterceptor] 路由拦截器初始化完成')
}
/**
* 添加需要登录的页面路径
* @param {String|Array} paths - 页面路径或路径数组
*/
export function addLoginRequiredPaths(paths) {
if (Array.isArray(paths)) {
LOGIN_REQUIRED_PATHS.push(...paths)
} else {
LOGIN_REQUIRED_PATHS.push(paths)
}
}
/**
* 添加需要登录的页面路径前缀
* @param {String|Array} prefixes - 路径前缀或前缀数组
*/
export function addLoginRequiredPrefixes(prefixes) {
if (Array.isArray(prefixes)) {
LOGIN_REQUIRED_PREFIXES.push(...prefixes)
} else {
LOGIN_REQUIRED_PREFIXES.push(prefixes)
}
}
/**
* 添加到白名单
* @param {String|Array} paths - 页面路径或路径数组
*/
export function addWhiteList(paths) {
if (Array.isArray(paths)) {
WHITE_LIST.push(...paths)
} else {
WHITE_LIST.push(paths)
}
}
/**
* 获取当前配置的白名单
* @returns {Array}
*/
export function getWhiteList() {
return [...WHITE_LIST]
}
/**
* 获取当前配置的需要登录的路径
* @returns {Array}
*/
export function getLoginRequiredPaths() {
return [...LOGIN_REQUIRED_PATHS]
}
/**
* 获取当前配置的需要登录的路径前缀
* @returns {Array}
*/
export function getLoginRequiredPrefixes() {
return [...LOGIN_REQUIRED_PREFIXES]
}
@@ -360,3 +360,82 @@
flex-grow: 1;
flex-basis: 0;
}
.member-card-section__count {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 12px;
background-color: rgba(255, 255, 255, 0.2);
}
.member-card-section__count-num {
font-size: var(--font-size-md);
font-family: var(--font-family);
font-weight: 700;
color: var(--text-inverse);
}
.member-card-section__count-text {
font-size: var(--font-size-xs);
font-family: var(--font-family);
font-weight: 400;
color: rgba(255, 212, 184, 1);
}
.member-card-preview__tag--expiring {
background-color: #FFE4E4;
}
.member-card-preview__tag--expiring .member-card-preview__tag-text {
color: #E74C3C;
}
.member-card-preview__tag--expired {
background-color: rgba(0, 0, 0, 0.2);
}
.member-card-preview__tag--expired .member-card-preview__tag-text {
color: rgba(255, 255, 255, 0.7);
}
.member-card-preview__tag--active {
background-color: rgba(255, 255, 255, 0.19);
}
.member-card-preview__tag--active .member-card-preview__tag-text {
color: var(--text-inverse);
}
.member-card-preview__days-num--expiring {
color: #FFE4E4;
}
.member-card-preview__days-num--expired {
color: #FF6B6B;
}
.member-card-preview__purchase {
flex-shrink: 0;
display: flex;
flex-direction: row;
align-items: center;
padding: 8px 20px;
border-radius: 20px;
background-color: var(--bg-white);
}
.member-card-preview__purchase-text {
font-size: var(--font-size-base);
font-family: var(--font-family);
font-weight: 600;
color: var(--accent-orange);
width: auto;
height: auto;
position: relative;
flex-shrink: 0;
white-space: nowrap;
flex-grow: 0;
}
-1
View File
@@ -61,7 +61,6 @@ const HIDE_TABBAR_PAGES = [
function getActiveIndexFromRoute() {
const routePath = getCurrentRoutePath()
const index = getTabIndexByRoute(routePath)
console.log('从路由获取索引:', routePath, '->', index)
return index >= 0 ? index : 0
}
@@ -0,0 +1,233 @@
/**
* 登录弹窗组件使用示例
*
* 该组件提供了统一的登录拦截功能,可以在任何页面使用
*/
## 方式一:在页面中直接使用LoginModal组件
```vue
<template>
<view>
<!-- 其他内容 -->
<!-- 引入登录弹窗 -->
<LoginModal
ref="loginModalRef"
v-model="showLoginModal"
title="登录后享受更多服务"
subtitle="登录即表示同意相关协议"
:show-close="true"
:mask-closable="true"
@loginSuccess="onLoginSuccess"
@loginError="onLoginError"
/>
</view>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import { registerLoginModal } from '@/common/hooks/useLoginGuard.js'
import LoginModal from '@/components/global/LoginModal.vue'
const showLoginModal = ref(false)
const loginModalRef = ref(null)
// 注册到全局
onMounted(() => {
nextTick(() => {
if (loginModalRef.value) {
registerLoginModal(loginModalRef.value)
}
})
})
// 显示登录弹窗
function showLogin() {
showLoginModal.value = true
}
// 登录成功回调
function onLoginSuccess(result) {
console.log('登录成功', result)
// 刷新页面数据等
}
// 登录失败回调
function onLoginError(error) {
console.log('登录失败', error)
}
</script>
```
## 方式二:使用 useLoginGuard Hook
```vue
<script setup>
import { useLoginGuard } from '@/common/hooks/useLoginGuard.js'
const { requireLogin, checkLogin } = useLoginGuard()
// 示例1:需要登录才能执行的操作
async function handleBookCourse() {
// 检查是否已登录
if (!checkLogin()) {
return
}
// 执行预约操作...
}
// 示例2:弹窗式登录(推荐)
async function handlePurchase() {
const loggedIn = await requireLogin({
title: '请先登录',
subtitle: '登录后即可购买会员卡',
onSuccess: (user) => {
console.log('登录成功,用户信息:', user)
}
})
if (loggedIn) {
// 执行购买操作
await purchaseCard()
}
}
// 示例3:安全执行函数
async function handleCheckIn() {
const result = await requireLogin().safeExecute(async () => {
// 只有登录后才能执行这里的代码
return await checkInApi()
})
if (result) {
uni.showToast({ title: '签到成功' })
}
}
// 示例4:包装API调用
async function handleGetCoupons() {
const { callLoggedInApi } = useLoginGuard()
try {
const coupons = await callLoggedInApi(getCouponsApi, [], {
title: '领取优惠券'
})
console.log('优惠券列表:', coupons)
} catch (e) {
if (e.message === '用户未登录') {
// 用户未登录,弹窗已显示
}
}
}
</script>
```
## 方式三:在组件中使用登录守卫Mixin
```vue
<script>
import { loginGuardMixin } from '@/common/hooks/useLoginGuard.js'
export default {
mixins: [loginGuardMixin()],
methods: {
async onQuickAction(type) {
// 自动检查登录状态
if (!await this.ensureLogin()) {
return
}
// 已登录,执行操作
if (type === 'book') {
this.goToBooking()
}
}
}
}
</script>
```
## 登录弹窗组件Props说明
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | Boolean | false | 控制显示/隐藏 |
| title | String | '登录后享受更多服务' | 弹窗标题 |
| subtitle | String | '登录即表示同意相关协议' | 弹窗副标题 |
| showClose | Boolean | true | 是否显示关闭按钮 |
| modalStyle | String | 'center' | 弹窗样式:'center'居中,'bottom'底部 |
| maskClosable | Boolean | true | 点击遮罩是否关闭 |
| onLoginSuccess | Function | null | 登录成功回调 |
| onLoginError | Function | null | 登录失败回调 |
## 登录弹窗组件Events
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| update:modelValue | 显示状态变化 | Boolean |
| loginSuccess | 登录成功 | Object: 登录结果 |
| loginError | 登录失败 | Error: 错误对象 |
| close | 弹窗关闭 | - |
## 登录弹窗组件暴露的方法
| 方法名 | 说明 | 参数 |
|--------|------|------|
| show() | 显示弹窗 | - |
| hide() | 隐藏弹窗 | - |
| switchCard(card) | 切换登录方式 | 'phone'一键登录 / 'sms'验证码登录 |
## 全局登录拦截示例
`App.vue` 中注册全局登录弹窗:
```vue
<script>
import LoginModal from '@/components/global/LoginModal.vue'
import { registerLoginModal } from '@/common/hooks/useLoginGuard.js'
export default {
components: { LoginModal },
onLaunch() {
// 注册全局登录弹窗
this.$nextTick(() => {
if (this.$refs.globalLoginModal) {
registerLoginModal(this.$refs.globalLoginModal)
}
})
}
}
</script>
<template>
<view id="app">
<router-view />
<LoginModal ref="globalLoginModal" />
</view>
</template>
```
## API路径配置
登录弹窗组件使用的API路径定义在 `@/api/main.js`
```javascript
// 一键登录
export function phoneLogin(data) {
return request.post('/auth/phone/login', data)
}
// 短信验证码登录
export function smsLogin(data) {
return request.post('/auth/phone/sms/login', data)
}
// 发送验证码
export function sendSmsCode(phone) {
return request.post('/auth/phone/sms/send', { phone })
}
```
如需修改API路径,请编辑 `main.js` 文件中的相应函数。
@@ -0,0 +1,691 @@
<template>
<!-- 登录弹窗组件 -->
<view class="login-modal-mask" v-if="visible" @tap="handleMaskTap">
<view class="login-modal" :class="{ 'login-modal--center': modalStyle === 'center' }" @tap.stop>
<!-- 关闭按钮 -->
<view class="login-modal__close" v-if="showClose" @tap="handleClose">
<text class="icon-close">×</text>
</view>
<!-- 弹窗内容 -->
<view class="login-modal__content">
<!-- 头部 -->
<view class="login-modal__header">
<view class="logo-wrapper">
<image class="logo-image" src="/static/logo.png" mode="aspectFit" />
<text class="app-name">活氧舱</text>
</view>
<text class="modal-title">{{ title }}</text>
<text class="modal-subtitle">{{ subtitle }}</text>
</view>
<!-- 一键登录卡片 -->
<view class="login-card" :class="{ active: activeCard === 'phone' }">
<view class="card-body">
<text class="card-title">本机号码登录</text>
<text class="auth-tip">认证服务由中国移动提供</text>
<button class="login-btn primary" :class="{ disabled: isLoading, loading: isLoading }"
@tap="handlePhoneLogin" :disabled="isLoading">
<text>{{ isLoading ? '登录中...' : '一键登录' }}</text>
</button>
<button class="login-btn switch-btn" @tap="switchToSms">
<text>短信验证码登录</text>
</button>
</view>
</view>
<!-- 短信验证码登录卡片 -->
<view class="login-card sms-card" :class="{ active: activeCard === 'sms' }">
<view class="card-body">
<text class="card-title">验证码登录</text>
<text class="auth-tip">认证服务由中国移动提供</text>
<view class="form-group">
<view class="input-wrapper" :class="{ focused: isPhoneFocused }">
<input v-model="smsPhone" type="number" placeholder="请输入手机号" maxlength="11"
class="phone-input" confirm-type="next" @focus="isPhoneFocused = true"
@blur="isPhoneFocused = false" />
</view>
</view>
<view class="form-group">
<view class="input-wrapper code-input" :class="{ focused: isCodeFocused }">
<input v-model="smsCode" type="number" placeholder="请输入验证码" maxlength="6"
class="phone-input" confirm-type="done" @focus="isCodeFocused = true"
@blur="isCodeFocused = false" />
<button class="send-code-btn" :class="{ disabled: countdown > 0 || isLoading }"
@tap="handleSendCode" :disabled="countdown > 0 || isLoading">
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
</button>
</view>
</view>
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }"
@tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
<text>{{ isLoading ? '登录中...' : '登录' }}</text>
</button>
<button class="login-btn switch-btn" @tap="switchToPhone">
<text>一键登录</text>
</button>
</view>
</view>
<!-- 协议 -->
<view class="agreement-text">
<checkbox-group @change="onAgreementChange">
<checkbox :checked="agreed" value="agreed" color="#FF8C42" />
</checkbox-group>
<text>登录即表示同意</text>
<text class="link" @tap="showAgreement">用户协议</text>
<text></text>
<text class="link" @tap="showPrivacy">隐私政策</text>
</view>
</view>
</view>
<!-- Toast提示 -->
<view class="custom-toast" :class="{ show: toastShow }">
<text class="toast-text">{{ toastMessage }}</text>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { oneClickLogin, loginWithPhone, sendCode } from '@/api/main.js'
// Props定义
const props = defineProps({
// 是否显示弹窗
modelValue: {
type: Boolean,
default: false
},
// 弹窗标题
title: {
type: String,
default: '登录后享受更多服务'
},
// 弹窗副标题
subtitle: {
type: String,
default: '登录即表示同意相关协议'
},
// 是否显示关闭按钮
showClose: {
type: Boolean,
default: true
},
// 弹窗样式: 'center'居中, 'bottom'底部
modalStyle: {
type: String,
default: 'center'
},
// 是否点击遮罩层关闭
maskClosable: {
type: Boolean,
default: true
},
// 登录成功回调
onLoginSuccess: {
type: Function,
default: null
},
// 登录失败回调
onLoginError: {
type: Function,
default: null
}
})
// Emits定义
const emit = defineEmits(['update:modelValue', 'loginSuccess', 'loginError', 'close'])
// 响应式状态
const visible = ref(false)
const activeCard = ref('phone') // 'phone'一键登录, 'sms'验证码登录
const isLoading = ref(false)
const isPhoneFocused = ref(false)
const isCodeFocused = ref(false)
const smsPhone = ref('')
const smsCode = ref('')
const countdown = ref(0)
const agreed = ref(false)
const toastShow = ref(false)
const toastMessage = ref('')
// 计算属性
const canSmsSubmit = computed(() => {
return smsPhone.value.length === 11 && smsCode.value.length === 6 && agreed.value
})
// 监听modelValue变化
watch(() => props.modelValue, (newVal) => {
visible.value = newVal
if (newVal) {
// 打开弹窗时重置状态
resetState()
}
}, { immediate: true })
// 重置状态
function resetState() {
activeCard.value = 'phone'
isLoading.value = false
smsPhone.value = ''
smsCode.value = ''
countdown.value = 0
agreed.value = false
}
// 显示Toast
function showToast(message, duration = 2000) {
toastMessage.value = message
toastShow.value = true
setTimeout(() => {
toastShow.value = false
}, duration)
}
// 显示用户协议
function showAgreement() {
uni.showModal({
title: '用户协议',
content: '这里是用户协议内容...',
showCancel: false
})
}
// 显示隐私政策
function showPrivacy() {
uni.showModal({
title: '隐私政策',
content: '这里隐私政策内容...',
showCancel: false
})
}
// 切换到短信登录
function switchToSms() {
activeCard.value = 'sms'
}
// 切换到一键登录
function switchToPhone() {
activeCard.value = 'phone'
}
// 协议勾选变化
function onAgreementChange(e) {
agreed.value = e.detail.value.includes('agreed')
}
// 发送验证码
async function handleSendCode() {
if (countdown.value > 0 || isLoading.value) return
if (!smsPhone.value || smsPhone.value.length !== 11) {
showToast('请输入正确的手机号')
return
}
if (!agreed.value) {
showToast('请先同意用户协议')
return
}
isLoading.value = true
try {
await sendCode({ phone: smsPhone.value })
showToast('验证码已发送')
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
}
}, 1000)
} catch (e) {
console.error('发送验证码失败:', e)
showToast(e.message || '发送验证码失败')
} finally {
isLoading.value = false
}
}
// 一键登录
async function handlePhoneLogin() {
if (isLoading.value) return
if (!agreed.value) {
showToast('请先同意用户协议')
return
}
isLoading.value = true
try {
// #ifdef APP-PLUS
// App环境下使用一键登录
await new Promise((resolve, reject) => {
uni.login({
provider: 'univerify',
univerifyStyle: {
fullScreen: false,
backgroundColor: '#ffffff',
icon: {
path: '/static/logo.png'
},
closeIcon: {
path: '/static/close.png'
},
phoneNum: {
color: '#333333',
fontSize: '18px'
},
slogan: {
color: '#999999',
fontSize: '12px'
},
authButton: {
normalColor: '#FF8C42',
highlightColor: '#FF7A2C',
disabledColor: '#CCCCCC',
textColor: '#FFFFFF',
title: '本机号码一键登录'
},
otherLoginButton: {
visible: false
},
protocols: []
},
success: async (res) => {
try {
// 调用后端API进行一键登录
const result = await oneClickLogin({
accessToken: res.authResult.access_token,
openid: res.authResult.openid
})
if (result.code === 200 || result.data) {
handleLoginSuccess(result)
resolve(result)
} else {
reject(new Error(result.message || '登录失败'))
}
} catch (e) {
reject(e)
}
},
fail: (err) => {
console.error('一键登录失败:', err)
reject(new Error(err.errMsg || '一键登录失败'))
}
})
})
// #endif
// #ifndef APP-PLUS
// 非App环境(小程序、H5等)提示使用其他方式
uni.showToast({
title: '请使用短信验证码登录',
icon: 'none'
})
activeCard.value = 'sms'
// #endif
} catch (e) {
console.error('一键登录失败:', e)
showToast(e.message || '一键登录失败')
if (props.onLoginError) {
props.onLoginError(e)
}
emit('loginError', e)
} finally {
isLoading.value = false
}
}
// 短信验证码登录
async function handleSmsLogin() {
if (!canSmsSubmit.value || isLoading.value) return
isLoading.value = true
try {
const result = await loginWithPhone({
phone: smsPhone.value,
code: smsCode.value
})
handleLoginSuccess(result)
} catch (e) {
console.error('短信登录失败:', e)
showToast(e.message || '登录失败')
if (props.onLoginError) {
props.onLoginError(e)
}
emit('loginError', e)
} finally {
isLoading.value = false
}
}
// 处理登录成功
function handleLoginSuccess(result) {
console.log('登录成功:', result)
// 保存token和用户信息到本地缓存
if (result.data?.token) {
uni.setStorageSync('token', result.data.token)
}
// 直接缓存登录响应数据
if (result.data) {
uni.setStorageSync('loginMemberInfo', result.data)
}
showToast('登录成功')
if (props.onLoginSuccess) {
props.onLoginSuccess(result)
}
emit('loginSuccess', result)
uni.$emit('loginModal:success', result)
setTimeout(() => {
handleClose()
}, 1500)
}
// 关闭弹窗
function handleClose() {
visible.value = false
emit('update:modelValue', false)
emit('close')
uni.$emit('loginModal:close')
}
// 点击遮罩层
function handleMaskTap() {
if (props.maskClosable) {
handleClose()
}
}
// 对外暴露的方法
defineExpose({
// 显示弹窗
show() {
visible.value = true
emit('update:modelValue', true)
},
// 隐藏弹窗
hide() {
handleClose()
},
// 切换登录方式
switchCard(card) {
activeCard.value = card
}
})
</script>
<style lang="scss" scoped>
.login-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.login-modal {
position: relative;
width: 90%;
max-width: 650rpx;
background: #fff;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
&--center {
display: flex;
flex-direction: column;
align-items: center;
}
}
.login-modal__close {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
.icon-close {
font-size: 48rpx;
color: #999;
line-height: 1;
}
}
.login-modal__content {
width: 100%;
padding: 40rpx 50rpx 50rpx;
}
.login-modal__header {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 40rpx;
}
.logo-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 20rpx;
.logo-image {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
}
.app-name {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
}
.modal-subtitle {
font-size: 24rpx;
color: #999;
}
.login-card {
display: none;
&.active {
display: block;
}
}
.sms-card {
display: none;
&.active {
display: block;
}
}
.card-body {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
text-align: center;
}
.auth-tip {
font-size: 22rpx;
color: #999;
text-align: center;
}
.form-group {
width: 100%;
}
.input-wrapper {
width: 100%;
height: 88rpx;
background: #F5F7FA;
border-radius: 16rpx;
border: 2rpx solid transparent;
display: flex;
align-items: center;
padding: 0 24rpx;
box-sizing: border-box;
transition: all 0.3s;
&.focused {
border-color: #FF8C42;
background: #fff;
}
}
.phone-input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #333;
}
.code-input {
padding-right: 0;
.phone-input {
width: 55%;
}
}
.send-code-btn {
width: 40%;
height: 64rpx;
line-height: 64rpx;
background: #FF8C42;
color: #fff;
font-size: 24rpx;
border-radius: 8rpx;
padding: 0;
margin: 0;
&.disabled {
background: #ccc;
}
&::after {
border: none;
}
}
.login-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
font-weight: 600;
padding: 0;
margin: 0;
&.primary {
background: linear-gradient(135deg, #FF8C42 0%, #FF6B2C 100%);
color: #fff;
border: none;
}
&.disabled {
background: #ccc;
color: #fff;
}
&.loading {
opacity: 0.8;
}
&.switch-btn {
background: transparent;
color: #FF8C42;
border: 2rpx solid #FF8C42;
}
&::after {
border: none;
}
}
.agreement-text {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 8rpx;
margin-top: 24rpx;
font-size: 22rpx;
color: #999;
.link {
color: #FF8C42;
}
}
.custom-toast {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
background: rgba(0, 0, 0, 0.8);
color: #fff;
padding: 24rpx 48rpx;
border-radius: 12rpx;
font-size: 28rpx;
z-index: 99999;
opacity: 0;
transition: all 0.3s;
&.show {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
}
</style>
@@ -0,0 +1,278 @@
<template>
<view
class="verify-code-input"
@tap="focusInput"
:style="{
'--vci-active-color': activeColor,
'--vci-filled-color': filledColor,
'--vci-border-color': borderColor
}"
>
<view class="vci-title" v-if="title">
<text class="vci-title__text">{{ title }}</text>
</view>
<view class="vci-desc" v-if="desc">
<text class="vci-desc__text">{{ desc }}</text>
</view>
<view class="vci-code-box">
<view
v-for="(item, index) in codeLength"
:key="index"
class="vci-code-item"
:class="{
'vci-code-item--active': isFocused && currentIndex === index,
'vci-code-item--filled': code.length > index,
'vci-code-item--underline': type === 'underline',
'vci-code-item--box': type === 'box'
}"
>
<text v-if="code[index] && !mask" class="vci-code-text">{{ code[index] }}</text>
<view v-else-if="code[index] && mask" class="vci-code-dot"></view>
<view v-if="isFocused && currentIndex === index && code.length === index" class="vci-code-cursor"></view>
</view>
</view>
<view class="vci-tips" v-if="errorMsg">
<text class="vci-tips__text">{{ errorMsg }}</text>
</view>
<input
ref="inputRef"
class="vci-hidden-input"
type="number"
:maxlength="codeLength"
:value="code"
:focus="isFocused"
:adjust-position="false"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
/>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
title: {
type: String,
default: ''
},
desc: {
type: String,
default: ''
},
length: {
type: Number,
default: 6
},
mask: {
type: Boolean,
default: false
},
type: {
type: String,
default: 'box'
},
errorMsg: {
type: String,
default: ''
},
autoFocus: {
type: Boolean,
default: false
},
activeColor: {
type: String,
default: '#1677FF'
},
filledColor: {
type: String,
default: '#1A202C'
},
borderColor: {
type: String,
default: '#E2E8F0'
}
})
const emit = defineEmits(['update:modelValue', 'complete', 'focus', 'blur'])
const code = ref(props.modelValue)
const isFocused = ref(false)
const currentIndex = ref(0)
const inputRef = ref(null)
const codeLength = computed(() => props.length)
watch(() => props.modelValue, (val) => {
code.value = val
currentIndex.value = Math.min(val.length, codeLength.value - 1)
})
function focusInput() {
isFocused.value = true
}
function onInput(e) {
let val = e.detail.value.replace(/\D/g, '').slice(0, codeLength.value)
code.value = val
currentIndex.value = Math.min(val.length, codeLength.value - 1)
emit('update:modelValue', val)
if (val.length === codeLength.value) {
emit('complete', val)
}
}
function onFocus() {
isFocused.value = true
currentIndex.value = code.value.length
emit('focus')
}
function onBlur() {
isFocused.value = false
emit('blur')
}
function clear() {
code.value = ''
currentIndex.value = 0
emit('update:modelValue', '')
}
function setFocus(val) {
isFocused.value = val
}
defineExpose({
clear,
focusInput,
setFocus
})
</script>
<style lang="scss" scoped>
.verify-code-input {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.vci-title {
margin-bottom: 8px;
&__text {
font-size: 18px;
font-weight: 600;
color: #1A202C;
}
}
.vci-desc {
margin-bottom: 32px;
&__text {
font-size: 13px;
color: #718096;
}
}
.vci-code-box {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 20px;
}
.vci-code-item {
position: relative;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&--box {
width: 44px;
height: 44px;
background: #FFFFFF;
border: 1px solid var(--vci-border-color, #E2E8F0);
border-radius: 8px;
&.vci-code-item--active {
border-color: var(--vci-active-color, #1677FF);
}
&.vci-code-item--filled {
border-color: var(--vci-filled-color, #1A202C);
}
}
&--underline {
width: 40px;
height: 50px;
border-bottom: 2px solid var(--vci-border-color, #E2E8F0);
&.vci-code-item--active {
border-bottom-color: var(--vci-active-color, #FF8C42);
}
&.vci-code-item--filled {
border-bottom-color: var(--vci-filled-color, #1A202C);
}
}
}
.vci-code-text {
font-size: 24px;
font-weight: 600;
color: var(--vci-filled-color, #1A202C);
line-height: 1;
}
.vci-code-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--vci-filled-color, #1A202C);
}
.vci-code-cursor {
position: absolute;
width: 2px;
height: 24px;
background: var(--vci-active-color, #1677FF);
animation: vci-blink 1s infinite;
}
@keyframes vci-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.vci-tips {
text-align: center;
margin-bottom: 16px;
&__text {
font-size: 13px;
color: #E53E3E;
}
}
.vci-hidden-input {
position: absolute;
left: -9999px;
top: -9999px;
width: 1px;
height: 1px;
opacity: 0;
}
</style>
@@ -7,17 +7,10 @@
我的会员卡
</text>
<view
class="member-card-section__link"
hover-class="mi-tap--hover"
:hover-stay-time="150"
@tap="$emit('view-all')"
class="member-card-section__count"
>
<text
class="member-card-section__link-text"
>
查看全部
</text>
<image class="member-card-section__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright12.png" mode="aspectFit" />
<text class="member-card-section__count-num">{{ cardInfo.activeCount || 0 }}</text>
<text class="member-card-section__count-text">张有效</text>
</view>
</view>
</view>
@@ -51,47 +44,39 @@
<text
class="member-card-preview__name"
>
{{ cardInfo.name }}
{{ cardInfo.hasActiveCard ? cardInfo.name : '暂无会员卡' }}
</text>
</view>
<view
class="member-card-preview__tag"
:class="getTagClass()"
v-if="cardInfo.hasActiveCard"
>
<text class="member-card-preview__tag-text">
{{ cardInfo.detailTag || '详情' }}
{{ getTagText() }}
</text>
</view>
</view>
</view>
<text class="member-card-preview__expire">
{{ cardInfo.expireDate }}
<text class="member-card-preview__expire" v-if="cardInfo.hasActiveCard && cardInfo.expireDate">
有效期至 {{ cardInfo.expireDate }}
</text>
<view class="member-card-preview__footer">
<view class="member-card-preview__footer-inner">
<view
class="member-card-preview__days"
v-if="cardInfo.hasActiveCard"
>
<text
class="member-card-preview__days-num"
:class="getDaysClass()"
>
{{ cardInfo.remainingDays }}
{{ getDisplayDays() }}
</text>
<text
class="member-card-preview__days-unit"
>
天剩余
</text>
</view>
<view
class="member-card-preview__renew"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap.stop="$emit('renew')"
>
<text
class="member-card-preview__renew-text"
>
续费
{{ getDaysUnit() }}
</text>
</view>
<view
@@ -110,29 +95,177 @@
</view>
</view>
</view>
<view class="member-card-tip">
<view class="member-card-tip__inner">
<view class="member-card-tip__content">
<image class="member-card-tip__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png" mode="aspectFit" />
<text
class="member-card-tip__text"
>
{{ cardInfo.tip }}
</text>
</view>
</view>
<view class="member-card-tip__border"></view>
</view>
</view>
</view>
</template>
<script setup>
defineProps({
cardInfo: { type: Object, required: true }
import { reactive, onMounted } from 'vue'
import { getPrimaryMemberCard, getMyMemberCardsWithStatus } from '@/api/main.js'
import { getMemberId } from '@/utils/request.js'
defineEmits(['view-all', 'purchase'])
const cardInfo = reactive({
name: '',
type: '',
remainingTimes: 0,
remainingDays: 0,
expireDate: '',
activeCount: 0,
isExpiring: false,
isExpired: false,
isUsedUp: false,
hasActiveCard: false
})
defineEmits(['view-all', 'renew', 'purchase'])
let isLoading = false
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
} catch (e) {
return dateStr
}
}
// 加载会员卡数据
async function loadMemberCard() {
if (isLoading) {
console.log('[MemberInfoMemberCard] 正在加载中,跳过重复请求')
return
}
isLoading = true
try {
// 使用统一的 getMemberId 方法获取会员ID
const memberId = getMemberId()
console.log('[MemberInfoMemberCard] memberId:', memberId)
if (!memberId) {
console.log('[MemberInfoMemberCard] 未登录,无会员ID')
return
}
console.log('[MemberInfoMemberCard] 加载会员卡数据, memberId:', memberId)
// 并行获取主要会员卡和有效卡列表
const [primaryCardRes, activeCardsRes] = await Promise.all([
getPrimaryMemberCard(memberId),
getMyMemberCardsWithStatus(memberId, 'active')
])
console.log('[MemberInfoMemberCard] getPrimaryMemberCard 返回:', JSON.stringify(primaryCardRes, null, 2))
console.log('[MemberInfoMemberCard] getMyMemberCardsWithStatus(active) 返回:', JSON.stringify(activeCardsRes, null, 2))
// 解析主要会员卡数据
let displayCard = null
if (primaryCardRes?.data) {
displayCard = primaryCardRes.data
} else if (primaryCardRes?.value) {
displayCard = primaryCardRes.value
} else if (primaryCardRes && typeof primaryCardRes === 'object') {
displayCard = primaryCardRes
}
// 计算有效会员卡数量(过滤储值卡)
let activeCount = 0
let allActiveCards = []
if (activeCardsRes?.value) {
allActiveCards = activeCardsRes.value
} else if (Array.isArray(activeCardsRes)) {
allActiveCards = activeCardsRes
} else if (activeCardsRes?.data) {
allActiveCards = Array.isArray(activeCardsRes.data) ? activeCardsRes.data : (activeCardsRes.data.value || [])
}
// 过滤储值卡
allActiveCards = allActiveCards.filter(card => card.memberCardType !== 'STORED_VALUE_CARD')
activeCount = allActiveCards.length
// 计算卡片状态
let diffDays = 0
let isExpiring = false
let isExpired = false
let isUsedUp = false
let displayDays = 0
if (displayCard) {
if (displayCard.status === 'USED_UP') {
isUsedUp = true
displayDays = displayCard.remainingTimes || 0
} else if (displayCard.expireTime) {
const expireTime = new Date(displayCard.expireTime)
const now = new Date()
diffDays = Math.ceil((expireTime - now) / (1000 * 60 * 60 * 24))
isExpiring = diffDays > 0 && diffDays <= 3
isExpired = diffDays <= 0
displayDays = Math.abs(diffDays)
}
}
console.log(displayCard)
// 更新数据
cardInfo.name = displayCard?.memberCardName || '暂无会员卡'
cardInfo.type = displayCard?.memberCardType || ''
cardInfo.remainingTimes = displayCard?.remainingTimes || displayCard?.remainingAmount || 0
cardInfo.remainingDays = displayDays
cardInfo.expireDate = displayCard?.expireTime ? formatDate(displayCard.expireTime) : ''
cardInfo.activeCount = activeCount
cardInfo.isExpiring = isExpiring
cardInfo.isExpired = isExpired
cardInfo.isUsedUp = isUsedUp
cardInfo.hasActiveCard = activeCount > 0
console.log('[MemberInfoMemberCard] 更新后的 cardInfo:', JSON.stringify(cardInfo, null, 2))
} catch (e) {
console.error('[MemberInfoMemberCard] 加载会员卡数据失败:', e)
cardInfo.hasActiveCard = false
} finally {
isLoading = false
}
}
// 组件挂载时加载数据
onMounted(() => {
console.log('[MemberInfoMemberCard] 组件已挂载,开始加载数据')
loadMemberCard()
})
function getTagClass() {
if (cardInfo.isExpiring) return 'member-card-preview__tag--expiring'
if (cardInfo.isExpired) return 'member-card-preview__tag--expired'
if (cardInfo.isUsedUp) return 'member-card-preview__tag--expired'
return 'member-card-preview__tag--active'
}
function getTagText() {
if (cardInfo.isUsedUp) return '已用完'
if (cardInfo.isExpired) return '已失效'
if (cardInfo.isExpiring) return '临期'
return '有效'
}
function getDaysClass() {
if (cardInfo.isExpiring) return 'member-card-preview__days-num--expiring'
if (cardInfo.isExpired) return 'member-card-preview__days-num--expired'
if (cardInfo.isUsedUp) return 'member-card-preview__days-num--expired'
return ''
}
function getDisplayDays() {
if (cardInfo.isUsedUp) return cardInfo.remainingTimes || 0
return cardInfo.remainingDays || 0
}
function getDaysUnit() {
if (cardInfo.isUsedUp) return '次剩余'
return '天剩余'
}
</script>
<style lang="scss">
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -1,9 +1,9 @@
{
"name" : "活氧舱",
"appid" : "__UNI__F058AC0",
"appid" : "__UNI__52E2F0D",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"versionName" : "1.0.1",
"versionCode" : 101,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@@ -17,7 +17,10 @@
"delay" : 0
},
/* */
"modules" : {},
"modules" : {
"Barcode" : {},
"OAuth" : {}
},
/* */
"distribute" : {
/* android */
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

@@ -1,24 +0,0 @@
//
// ATAuthSDK.h
// ATAuthSDK
//
// Created by yangli on 2020/11/11.
// Copyright © 2020. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for ATAuthSDK.
FOUNDATION_EXPORT double ATAuthSDKVersionNumber;
//! Project version string for ATAuthSDK.
FOUNDATION_EXPORT const unsigned char ATAuthSDKVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <ATAuthSDK/PublicHeader.h>
#import "TXCommonHandler.h"
#import "TXCommonUtils.h"
#import "PNSReturnCode.h"
#import "TXCustomModel.h"
#import "PNSReporter.h"
#import "PNSFeatureManager.h"
@@ -1,27 +0,0 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface PNSFeatureManager : NSObject
/// 是否开启供应商级别缓存(默认为私网IP级缓存,开启之后缓存为供应商级别)
@property (class, nonatomic, assign) BOOL enableCacheVendorLevel;
/// 是否开启运营商类型缓存(默认不开启,开启后会缓存上一次sim卡运营商归属信息)
@property (class, nonatomic, assign) BOOL enableCacheLastVendorInfo;
/// 是否开启降低网络请求频次模式(默认NO),开启后将跳过配置、埋点等dypns域名访问,不影响登录核心功能
@property (class, nonatomic, assign) BOOL enableReduceNetworkAccess;
+ (void)enableCacheVendorLevel:(BOOL)enable;
+ (void)enableCacheLastVendorInfo:(BOOL)enable;
+ (void)enableReduceNetworkAccess:(BOOL)enable;
@end
NS_ASSUME_NONNULL_END
@@ -1,37 +0,0 @@
//
// PNSReporter.h
// ATAuthSDK
//
// Created by 刘超的MacBook on 2020/5/21.
// Copyright © 2020. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, PNSLoggerLevel) {
PNSLoggerLevelVerbose = 1,
PNSLoggerLevelDebug,
PNSLoggerLevelInfo,
PNSLoggerLevelWarn,
PNSLoggerLevelError
};
@interface PNSReporter : NSObject
/**
* 控制台日志输出开关,若开启会以PNS_LOGGER为开始标记对日志进行输出,Release模式记得关闭!
* @param enable 开关参数,默认为NO
*/
- (void)setConsolePrintLoggerEnable:(BOOL)enable;
/**
* 设置埋点上传开关,但不会对通过 setupUploader: 接口实现的自定义上传方法起作用
* @param enable 开关设置BOOL值,默认为YES
*/
- (void)setUploadEnable:(BOOL)enable DEPRECATED_MSG_ATTRIBUTE("日志不再上传");;
@end
NS_ASSUME_NONNULL_END
@@ -1,88 +0,0 @@
//
// PNSReturnCode.h
// ATAuthSDK
//
// Created by 刘超的MacBook on 2019/9/4.
// Copyright © 2019. All rights reserved.
//
#ifndef PNSReturnCode_h
#define PNSReturnCode_h
#import <Foundation/Foundation.h>
#pragma mark - 该返回码为阿里云号码认证SDK⾃身的返回码,请注意600011及600012错误内均含有运营商返回码,具体错误在碰到之后查阅 https://help.aliyun.com/document_detail/85351.html?spm=a2c4g.11186623.6.561.32a7360cxvWk6H
/// 接口成功
static NSString * const PNSCodeSuccess = @"600000";
/// 获取运营商配置信息失败
static NSString * const PNSCodeGetOperatorInfoFailed = @"600004";
/// 未检测到sim卡
static NSString * const PNSCodeNoSIMCard = @"600007";
/// 蜂窝网络未开启或不稳定
static NSString * const PNSCodeNoCellularNetwork = @"600008";
/// 无法判运营商
static NSString * const PNSCodeUnknownOperator = @"600009";
/// 未知异常
static NSString * const PNSCodeUnknownError = @"600010";
/// 获取token失败
static NSString * const PNSCodeGetTokenFailed = @"600011";
/// 预取号失败
static NSString * const PNSCodeGetMaskPhoneFailed = @"600012";
/// 运营商维护升级,该功能不可用
static NSString * const PNSCodeInterfaceDemoted = @"600013";
/// 运营商维护升级,该功能已达最大调用次数
static NSString * const PNSCodeInterfaceLimited = @"600014";
/// 接口超时
static NSString * const PNSCodeInterfaceTimeout = @"600015";
/// AppID、Appkey解析失败
static NSString * const PNSCodeDecodeAppInfoFailed = @"600017";
/// 该号码已被运营商管控,目前只有联通号码有该功能
static NSString * const PNSCodePhoneBlack = @"600018";
/// 运营商已切换
static NSString * const PNSCodeCarrierChanged = @"600021";
/// 终端环境检测失败(终端不支持认证 / 终端检测参数错误)
static NSString * const PNSCodeEnvCheckFail = @"600025";
/*************** 号码认证授权页相关返回码 START ***************/
/// 唤起授权页成功
static NSString * const PNSCodeLoginControllerPresentSuccess = @"600001";
/// 唤起授权页失败
static NSString * const PNSCodeLoginControllerPresentFailed = @"600002";
/// 授权页已加载时不允许调用加速或预取号接口
static NSString * const PNSCodeCallPreLoginInAuthPage = @"600026";
/// 点击返回,⽤户取消一键登录
static NSString * const PNSCodeLoginControllerClickCancel = @"700000";
/// 点击切换按钮,⽤户取消免密登录
static NSString * const PNSCodeLoginControllerClickChangeBtn = @"700001";
/// 点击登录按钮事件
static NSString * const PNSCodeLoginControllerClickLoginBtn = @"700002";
/// 点击CheckBox事件
static NSString * const PNSCodeLoginControllerClickCheckBoxBtn = @"700003";
/// 点击协议富文本文字
static NSString * const PNSCodeLoginControllerClickProtocol = @"700004";
/// 中断页面消失的时候,也就是suspendDisMissVC设置为YES的时候,点击左上角返回按钮时透出的状态码
static NSString * const PNSCodeLoginControllerSuspendDisMissVC = @"700010";
/// 授权页已销毁
static NSString * const PNSCodeLoginControllerDeallocVC = @"700020";
/*************** 号码认证授权页相关返回码 FINISH ***************/
/*************** 二次授权页返回code码 START ***************/
/// 点击一键登录拉起授权页二次弹窗
static NSString * const PNSCodeLoginClickPrivacyAlertView = @"700006";
/// 隐私协议二次弹窗关闭
static NSString * const PNSCodeLoginPrivacyAlertViewClose = @"700007";
/// 隐私协议二次弹窗点击确认并继续
static NSString * const PNSCodeLoginPrivacyAlertViewClickContinue = @"700008";
/// 点击隐私协议二次弹窗上的协议富文本文字
static NSString * const PNSCodeLoginPrivacyAlertViewPrivacyContentClick = @"700009";
/*************** 二次授权页返回code码 FINISH ***************/
#endif /* PNSReturnCode_h */
@@ -1,138 +0,0 @@
//
// TXCommonHandler.h
// ATAuthSDK
//
// Created by yangli on 15/03/2018.
#import <Foundation/Foundation.h>
#import "TXCustomModel.h"
#import "PNSReporter.h"
typedef NS_ENUM(NSInteger, PNSAuthType) {
PNSAuthTypeVerifyToken = 1, //本机号码校验
PNSAuthTypeLoginToken = 2 //一键登录
};
@interface TXCommonHandler : NSObject
/**
* 获取该类的单例实例对象
* @return 单例实例对象
*/
+ (instancetype _Nonnull )sharedInstance;
/**
* 获取当前SDK版本号
* @return 字符串,sdk版本号
*/
- (NSString *_Nonnull)getVersion;
/**
* SDK鉴权,app生命周期内调用一次
* @param info app对应的秘钥
* @param complete 结果异步回调到主线程,成功时resultDic=@{resultCode:600000, msg:...},其他情况时"resultCode"值请参考PNSReturnCode
* @note 重复调用时以最新info信息为准
*/
- (void)setAuthSDKInfo:(NSString * _Nonnull)info complete:(void(^_Nullable)(NSDictionary * _Nonnull resultDic))complete;
/**
* 检查当前环境是否支持一键登录或号码认证,resultDic 返回 PNSCodeSuccess 说明当前环境支持
* @param authType 服务类型 PNSAuthTypeVerifyToken 本机号码校验流程,PNSAuthTypeLoginToken 一键登录流程
* @param complete 结果异步回调到主线程,成功时resultDic=@{resultCode:600000, msg:...},其他情况时"resultCode"值请参考PNSReturnCode,只有成功回调才能保障后续接口调用
*/
- (void)checkEnvAvailableWithAuthType:(PNSAuthType)authType complete:(void (^_Nullable)(NSDictionary * _Nullable resultDic))complete;
/**
* 加速获取本机号码校验token,防止调用 getVerifyTokenWithTimeout:complete: 获取token时间过长
* @param timeout 接口超时时间,单位s,默认为3.0s
* @param complete 结果异步回调到主线程,成功时resultDic=@{resultCode:600000, token:..., msg:...},其他情况时"resultCode"值请参考PNSReturnCode
*/
- (void)accelerateVerifyWithTimeout:(NSTimeInterval)timeout complete:(void (^_Nullable)(NSDictionary * _Nonnull resultDic))complete;
/**
* 获取本机号码校验Token
* @param timeout 接口超时时间,单位s,默认为3.0s
* @param complete 结果异步回调到主线程,成功时resultDic=@{resultCode:600000, token:..., msg:...},其他情况时"resultCode"值请参考PNSReturnCode
*/
- (void)getVerifyTokenWithTimeout:(NSTimeInterval)timeout complete:(void (^_Nullable)(NSDictionary * _Nonnull resultDic))complete;
/**
* 加速一键登录授权页弹起,防止调用 getLoginTokenWithTimeout:controller:model:complete: 等待弹起授权页时间过长
* @param timeout 接口超时时间,单位s,默认为3.0s
* @param complete 结果异步回调到主线程,成功时resultDic=@{resultCode:600000, msg:...},其他情况时"resultCode"值请参考PNSReturnCode
*/
- (void)accelerateLoginPageWithTimeout:(NSTimeInterval)timeout complete:(void (^_Nullable)(NSDictionary * _Nonnull resultDic))complete;
/**
* 获取一键登录Token,调用该接口首先会弹起授权页,点击授权页的登录按钮获取Token
* @warning 注意的是,如果前面没有调用 accelerateLoginPageWithTimeout:complete: 接口,该接口内部会自动先帮我们调用,成功后才会弹起授权页,所以有一个明显的等待过程
* @param timeout 接口超时时间,单位s,默认为3.0s
* @param controller 唤起自定义授权页的容器,内部会对其进行验证,检查是否符合条件
* @param model 自定义授权页面选项,可为nil,采用默认的授权页面,具体请参考TXCustomModel.h文件
* @param complete 结果异步回调到主线程,"resultDic"里面的"resultCode"值请参考PNSReturnCode,如下:
*
* 授权页控件点击事件:700000(点击授权页返回按钮)、700001(点击切换其他登录方式)、
* 700002(点击登录按钮事件,根据返回字典里面的 "isChecked"字段来区分check box是否被选中,只有被选中的时候内部才会去获取Token)、700003(点击check box事件)、700004(点击协议富文本文字)
接口回调其他事件:600001(授权页唤起成功)、600002(授权页唤起失败)、600000(成功获取Token)、600011(获取Token失败)、
* 600015(获取Token超时)、600013(运营商维护升级,该功能不可用)、600014(运营商维护升级,该功能已达最大调用次数).....
*/
- (void)getLoginTokenWithTimeout:(NSTimeInterval)timeout controller:(UIViewController *_Nonnull)controller model:(TXCustomModel *_Nullable)model complete:(void (^_Nullable)(NSDictionary * _Nonnull resultDic))complete;
/**
* 此接口仅用于开发期间用于一键登录页面不同机型尺寸适配调试(可支持模拟器),非正式页面,手机掩码为0,不能正常登录,请开发者注意下
* @param controller 唤起自定义授权页的容器,内部会对其进行验证,检查是否符合条件
* @param model 自定义授权页面选项,可为nil,采用默认的授权页面,具体请参考TXCustomModel.h文件
* @param complete 结果异步回调到主线程,"resultDic"里面的"resultCode"值请参考PNSReturnCode
*/
- (void)debugLoginUIWithController:(UIViewController *_Nonnull)controller model:(TXCustomModel *_Nullable)model complete:(void (^_Nullable)(NSDictionary * _Nonnull resultDic))complete;
/**
* 授权页弹起后,修改checkbox按钮选中状态,当checkout按钮隐藏时,设置不生效
*/
- (void)setCheckboxIsChecked:(BOOL)isChecked;
/**
* 查询授权页checkbox是否勾选,YES:勾选,NO:未勾选
*/
- (BOOL)queryCheckBoxIsChecked;
/**
* 授权页协议内容动画执行,注意:必须设置privacyAnimation属性,才会执行动画
*/
- (void)privacyAnimationStart;
/**
* 授权页checkbox动画执行,注意:必须设置checkboxAnimation属性,才会执行动画
*/
- (void)checkboxAnimationStart;
/**
* 手动隐藏一键登录获取登录Token之后的等待动画,默认为自动隐藏,当设置 TXCustomModel 实例 autoHideLoginLoading = NO 时, 可调用该方法手动隐藏
*/
- (void)hideLoginLoading;
/**
* 注销授权页,建议用此方法,对于移动卡授权页的消失会清空一些数据
* @param flag 是否添加动画
* @param complete 成功返回
*/
- (void)cancelLoginVCAnimated:(BOOL)flag complete:(void (^_Nullable)(void))complete;
/**
* 获取日志埋点相关控制对象
*/
- (PNSReporter * _Nonnull)getReporter;
/**
* 关闭二次授权弹窗页
*/
- (void)closePrivactAlertView;
/**
* 检查及准备调用环境,resultDic返回PNSCodeSuccess才能调用下面的功能接口
* @param complete 结果异步回调到主线程,成功时resultDic=@{resultCode:600000, msg:...},其他情况时"resultCode"值请参考PNSReturnCode,只有成功回调才能保障后续接口调用
*/
- (void)checkEnvAvailableWithComplete:(void (^_Nullable)(NSDictionary * _Nullable resultDic))complete DEPRECATED_MSG_ATTRIBUTE("Please use checkEnvAvailableWithAuthType:complete: instead");
@end
@@ -1,81 +0,0 @@
//
// TXCommonUtils.h
// authsdk
//
// Created by yangli on 12/03/2018.
#import <Foundation/Foundation.h>
@interface TXCommonUtils : NSObject
/**
判断当前设备蜂窝数据网络是否开启,即3G/4G
@return 结果
*/
+ (BOOL)checkDeviceCellularDataEnable;
/**
判断当前上网卡运营商是否是中国联通
@return 结果
*/
+ (BOOL)isChinaUnicom;
/**
判断当前上网卡运营商是否是中国移动
@return 结果
*/
+ (BOOL)isChinaMobile;
/**
判断当前上网卡运营商是否是中国电信
@return 结果
*/
+ (BOOL)isChinaTelecom;
/**
获取当前上网卡运营商名称,比如中国移动、中国电信、中国联通
@return 结果
*/
+ (NSString *)getCurrentCarrierName;
/**
获取当前上网卡网络类型,比如WiFi,4G
@return 结果
*/
+ (NSString *)getNetworktype;
/**
判断当前设备是否有SIM卡
@return 结果
*/
+ (BOOL)simSupportedIsOK;
/**
判断wwan是否开着(通过p0网卡判断,无wifi或有wifi情况下都能检测到)
@return 结果
*/
+ (BOOL)isWWANOpen;
/**
判断wwan是否开着(仅无wifi情况下)
@return 结果
*/
+ (BOOL)reachableViaWWAN;
/**
获取设备当前网络私网IP地址
@return 结果
*/
+ (NSString *)getMobilePrivateIPAddress:(BOOL)preferIPv4;
/**
获取当前设备的唯一标识ID
*/
+ (NSString *)getUniqueID;
/**
通过颜色设置生成图片,支持弧度设置,比如一键登录按钮背景图片
*/
+ (UIImage *)imageWithColor:(UIColor *)color size:(CGSize)size isRoundedCorner:(BOOL )isRounded radius:(CGFloat)radius;
@end
@@ -1,453 +0,0 @@
//
// TXCustomModel.h
// ATAuthSDK
//
// Created by yangli on 2019/4/4.
// Copyright © 2019. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, PNSPresentationDirection){
PNSPresentationDirectionBottom = 0,
PNSPresentationDirectionRight,
PNSPresentationDirectionTop,
PNSPresentationDirectionLeft,
};
/**
* 构建控件的frame,view布局时会调用该block得到控件的frame
* @param screenSize 屏幕的size,可以通过该size来判断是横屏还是竖屏
* @param superViewSize 该控件的super view的size,可以通过该size,辅助该控件重新布局
* @param frame 控件默认的位置
* @return 控件新设置的位置
*/
typedef CGRect(^PNSBuildFrameBlock)(CGSize screenSize, CGSize superViewSize, CGRect frame);
@interface TXCustomModel : NSObject
/**
* 说明,可设置的Y轴距离,waring: 以下所有关于Y轴的设置<=0都将不生效,请注意
* 全屏模式:默认是以375x667pt为基准,其他屏幕尺寸可以根据(ratio = 屏幕高度/667)比率来适配,比如 Y*ratio
*/
#pragma mark- 全屏、弹窗模式设置
/**
* 授权页面中,渲染并显示所有控件的view,称content view,不实现该block默认为全屏模式
* 实现弹窗的方案 x >= 0 || y >= 0 width <= 屏幕宽度 || height <= 屏幕高度
*/
@property (nonatomic, copy) PNSBuildFrameBlock contentViewFrameBlock;
#pragma mark- 竖屏、横屏模式设置
/** 屏幕是否支持旋转方向,默认UIInterfaceOrientationMaskPortrait,注意:在刘海屏,UIInterfaceOrientationMaskPortraitUpsideDown属性慎用! */
@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations;
#pragma mark- 仅弹窗模式属性
/** 底部蒙层背景颜色,默认黑色 */
@property (nonatomic, strong) UIColor *alertBlurViewColor;
/** 底部蒙层背景透明度,默认0.5 */
@property (nonatomic, assign) CGFloat alertBlurViewAlpha;
/** contentView背景颜色,默认白色 */
@property (nonatomic, strong) UIColor *alertContentViewColor;
/** contentView背景透明度,默认1.0 ,即不透明*/
@property (nonatomic, assign) CGFloat alertContentViewAlpha;
/** contentView的四个圆角值,顺序为左上,左下,右下,右上,需要填充4个值,不足4个值则无效,如果值<=0则为直角 */
@property (nonatomic, copy) NSArray<NSNumber *> *alertCornerRadiusArray;
/** 标题栏背景颜色 */
@property (nonatomic, strong) UIColor *alertTitleBarColor;
/** 标题栏是否隐藏,默认NO */
@property (nonatomic, assign) BOOL alertBarIsHidden;
/** 标题栏标题,内容、字体、大小、颜色 */
@property (nonatomic, copy) NSAttributedString *alertTitle;
/** 标题栏右侧关闭按钮图片设置*/
@property (nonatomic, strong) UIImage *alertCloseImage;
/** 标题栏右侧关闭按钮是否显示,默认NO*/
@property (nonatomic, assign) BOOL alertCloseItemIsHidden;
/** 构建标题栏的frame,view布局或布局发生变化时调用,不实现则按默认处理,实现时仅有height生效 */
@property (nonatomic, copy) PNSBuildFrameBlock alertTitleBarFrameBlock;
/** 构建标题栏标题的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock alertTitleFrameBlock;
/** 构建标题栏右侧关闭按钮的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock alertCloseItemFrameBlock;
/** 弹窗位置是否根据键盘弹起关闭动态调整,仅在键盘弹起后遮挡弹窗的情况生效,调整后弹窗将居于键盘上方,默认NO*/
@property (nonatomic, assign) BOOL alertFrameChangeWithKeyboard;
#pragma mark- 导航栏(只对全屏模式有效)
/**授权页显示中,导航栏是否隐藏,默认NO*/
@property (nonatomic, assign) BOOL navIsHidden;
/**授权页push到其他页面后,导航栏是否隐藏,默认NO*/
@property (nonatomic, assign) BOOL navIsHiddenAfterLoginVCDisappear;
/**是否需要中断返回,如果设置为YES,则点击左上角返回按钮的时候默认页面不消失,同时透出状态码700010,需要自己调用TXCommonHandler cancelLoginVCAnimated方法隐藏页面,默认为NO*/
@property (nonatomic, assign) BOOL suspendDisMissVC;
/** 导航栏主题色 */
@property (nonatomic, strong) UIColor *navColor;
/** 导航栏标题,内容、字体、大小、颜色 */
@property (nonatomic, copy) NSAttributedString *navTitle;
/** 导航栏返回图片 */
@property (nonatomic, strong) UIImage *navBackImage;
/** 是否隐藏授权页导航栏返回按钮,默认不隐藏 */
@property (nonatomic, assign) BOOL hideNavBackItem;
/** 导航栏右侧自定义控件,可以在创建该VIEW的时候添加手势操作,或者创建按钮或其他赋值给VIEW */
@property (nonatomic, strong) UIView *navMoreView;
/** 构建导航栏返回按钮的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock navBackButtonFrameBlock;
/** 构建导航栏标题的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock navTitleFrameBlock;
/** 构建导航栏右侧more view的frame,view布局或布局发生变化时调用,不实现则按默认处理,边界 CGRectGetMinX(frame) >= (superViewSizeViewSize / 0.3) && CGRectGetWidth(frame) <= (superViewSize.width / 3.0) */
@property (nonatomic, copy) PNSBuildFrameBlock navMoreViewFrameBlock;
#pragma mark- 全屏、弹窗模式共同属性
#pragma mark- 授权页动画相关
/** 授权页弹出方向,默认PNSPresentationDirectionBottom,该属性只对自带动画起效,不影响自定义动画 */
@property (nonatomic, assign) PNSPresentationDirection presentDirection;
/** 授权页显示和消失动画时间,默认为0.25s,<= 0 时关闭动画,该属性只对自带动画起效,不影响自定义动画 **/
@property (nonatomic, assign) CGFloat animationDuration;
/** 授权页显示动画(弹窗 & 全屏),不设置或设置为nil默认使用自带动画,SDK内部会主动更改动画的一些属性(包括:removedOnCompletion = NO、fillMode = kCAFillModeForwards 及 delegate **/
@property (nonatomic, strong, nullable) CAAnimation *entryAnimation;
/** 授权页消失动画(弹窗 & 全屏),不设置或设置为nil默认使用自带动画,SDK内部会主动更改动画的一些属性(包括:removedOnCompletion = NO、fillMode = kCAFillModeForwards 及 delegate **/
@property (nonatomic, strong, nullable) CAAnimation *exitAnimation;
/** 授权页显示时的背景动画(仅弹窗),不设置或设置为nil默认使用自带动画,SDK内部会主动更改动画的一些属性(包括:removedOnCompletion = NO、fillMode = kCAFillModeForwards 及 delegate **/
@property (nonatomic, strong, nullable) CAAnimation *bgEntryAnimation;
/** 授权页消失时的背景动画(仅弹窗),不设置或设置为nil默认使用自带动画,SDK内部会主动更改动画的一些属性(包括:removedOnCompletion = NO、fillMode = kCAFillModeForwards 及 delegate **/
@property (nonatomic, strong, nullable) CAAnimation *bgExitAnimation;
#pragma mark- 状态栏
/** 状态栏是否隐藏,默认NO */
@property (nonatomic, assign) BOOL prefersStatusBarHidden;
/** 状态栏主题风格,默认UIStatusBarStyleDefault */
@property (nonatomic, assign) UIStatusBarStyle preferredStatusBarStyle;
#pragma mark- 背景
/** 授权页背景色 */
@property (nonatomic, strong) UIColor *backgroundColor;
/** 授权页背景图片 */
@property (nonatomic, strong) UIImage *backgroundImage;
/** 授权页背景图片view的 content mode,默认为 UIViewContentModeScaleAspectFill */
@property (nonatomic, assign) UIViewContentMode backgroundImageContentMode;
/** 点击授权页背景是否关闭授权页,只有在弹窗模式下生效,默认NO*/
@property (nonatomic, assign) BOOL tapAuthPageMaskClosePage;
#pragma mark- logo图片
/** logo图片设置 */
@property (nonatomic, strong) UIImage *logoImage;
/** logo是否隐藏,默认NO */
@property (nonatomic, assign) BOOL logoIsHidden;
/** 构建logo的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock logoFrameBlock;
/** logo的宽设置 */
@property (nonatomic, assign) CGFloat logoWidth DEPRECATED_MSG_ATTRIBUTE("Please use logoFrameBlock instead");
/** logo的高设置 */
@property (nonatomic, assign) CGFloat logoHeight DEPRECATED_MSG_ATTRIBUTE("Please use logoFrameBlock instead");
/** logo相对导航栏底部或标题栏底部的Y轴距离 */
@property (nonatomic, assign) CGFloat logoTopOffetY DEPRECATED_MSG_ATTRIBUTE("Please use logoFrameBlock instead");
#pragma mark- slogan
/** slogan文案,内容、字体、大小、颜色 */
@property (nonatomic, copy) NSAttributedString *sloganText;
/** slogan是否隐藏,默认NO */
@property (nonatomic, assign) BOOL sloganIsHidden;
/** 构建slogan的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock sloganFrameBlock;
/** slogan相对导航栏底部或标题栏底部的Y轴距离 */
@property (nonatomic, assign) CGFloat sloganTopOffetY DEPRECATED_MSG_ATTRIBUTE("Please use sloganFrameBlock instead");
#pragma mark- 号码
/** 号码颜色设置 */
@property (nonatomic, strong) UIColor *numberColor;
/** 号码字体设置,大小小于16则不生效 */
@property (nonatomic, strong) UIFont *numberFont;
/**
* 构建号码的frame,view布局或布局发生变化时调用,只有x、y生效,不实现则按默认处理,
* 注:设置不能超出父视图 content view
*/
@property (nonatomic, copy) PNSBuildFrameBlock numberFrameBlock;
/**
* 号码相对导航栏底部或标题栏底部的Y轴距离,不设置则按默认处理
* 注:设置超出父视图 content view 时不生效
*/
@property (nonatomic, assign) CGFloat numberTopOffetY DEPRECATED_MSG_ATTRIBUTE("Please use numberFrameBlock instead");
/**
* 号码相对屏幕中线的X轴偏移距离,不设置则按默认处理,默认为0水平居中
* 注:设置不能超出父视图 content view
*/
@property (nonatomic, assign) CGFloat numberOffetX DEPRECATED_MSG_ATTRIBUTE("Please use numberFrameBlock instead");
#pragma mark- 登录
/** 登陆按钮文案,内容、字体、大小、颜色*/
@property (nonatomic, strong) NSAttributedString *loginBtnText;
/** 登录按钮背景图片组,默认高度50.0pt,@[激活状态的图片,失效状态的图片,高亮状态的图片] */
@property (nonatomic, strong) NSArray<UIImage *> *loginBtnBgImgs;
/**
* 是否显示获取登录Token等待过程的loading组件,默认为YES;
* 设置为NO时,SDK内部不会自动展示loading,适用于完全不需要loading的场景
*/
@property (nonatomic, assign) BOOL showLoginLoading;
/**
* 是否自动隐藏点击登录按钮之后授权页上转圈的 loading, 默认为YES,在获取登录Token成功后自动隐藏
* 如果设置为 NO,需要自己手动调用 [[TXCommonHandler sharedInstance] hideLoginLoading] 隐藏
*/
@property (nonatomic, assign) BOOL autoHideLoginLoading;
/**
* loading组件背景图片,默认为nil不设置;
* 如果设置,显示loading时会在转圈指示器下方显示该背景图
*/
@property (nonatomic, strong, nullable) UIImage *loadingBackgroundImage;
/**
* 构建登录按钮的frame,view布局或布局发生变化时调用,不实现则按默认处理
* 注:不能超出父视图 content viewheight不能小于20width不能小于父视图宽度的一半
*/
@property (nonatomic, copy) PNSBuildFrameBlock loginBtnFrameBlock;
/**
* 登录按钮相对导航栏底部或标题栏底部的Y轴距离,不设置则按默认处理
* 注:设置超出父视图 content view 时不生效
*/
@property (nonatomic, assign) CGFloat loginBtnTopOffetY DEPRECATED_MSG_ATTRIBUTE("Please use loginBtnFrameBlock instead");
/** 登录按钮高度,小于20.0pt不生效,不设置则按默认处理 */
@property (nonatomic, assign) CGFloat loginBtnHeight DEPRECATED_MSG_ATTRIBUTE("Please use loginBtnFrameBlock instead");
/** 登录按钮相对content view的左右边距,按钮宽度必须大于等于屏幕的一半,不设置则按默认处理 */
@property (nonatomic, assign) CGFloat loginBtnLRPadding DEPRECATED_MSG_ATTRIBUTE("Please use loginBtnFrameBlock instead");
#pragma mark- 协议
/** checkBox图片组,[uncheckedImg,checkedImg]*/
@property (nonatomic, copy) NSArray<UIImage *> *checkBoxImages;
/** checkBox图片距离控件边框的填充,默认为 UIEdgeInsetsZero,确保控件大小减去内填充大小为资源图片大小情况下,图片才不会变形 **/
@property (nonatomic, assign) UIEdgeInsets checkBoxImageEdgeInsets;
/** checkBox是否勾选,默认NO */
@property (nonatomic, assign) BOOL checkBoxIsChecked;
/** checkBox是否隐藏,默认NO */
@property (nonatomic, assign) BOOL checkBoxIsHidden;
/** checkBox大小,高宽一样,必须大于0 */
@property (nonatomic, assign) CGFloat checkBoxWH;
/** checkBox是否和协议内容垂直居中,默认NO,即顶部对齐 */
@property (nonatomic, assign) BOOL checkBoxVerticalCenter;
/** 协议1,[协议名称,协议Url],注:三个协议名称不能相同 */
@property (nonatomic, copy) NSArray<NSString *> *privacyOne;
/** 协议2,[协议名称,协议Url],注:三个协议名称不能相同 */
@property (nonatomic, copy) NSArray<NSString *> *privacyTwo;
/** 协议3,[协议名称,协议Url],注:三个协议名称不能相同 */
@property (nonatomic, copy) NSArray<NSString *> *privacyThree;
/** 协议名称之间连接字符串数组,默认 ["和","、","、"] ,即第一个为"和",其他为"、",按顺序读取,为空则取默认 */
@property (nonatomic, copy) NSArray<NSString *> *privacyConectTexts;
/** 协议内容颜色数组,[非点击文案颜色,点击文案颜色] */
@property (nonatomic, copy) NSArray<UIColor *> *privacyColors;
/** 运营商协议内容颜色 ,优先级最高,如果privacyOperatorColors不设置,则取privacyColors中的点击文案颜色,privacyColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyOperatorColor;
/** 协议1内容颜色,优先级最高,如果privacyOneColors不设置,则取privacyColors中的点击文案颜色,privacyColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyOneColor;
/** 协议2内容颜色,优先级最高,如果privacyTwoColors不设置,则取privacyColors中的点击文案颜色,privacyColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyTwoColor;
/** 协议3内容颜色,优先级最高,如果privacyThreeColors不设置,则取privacyColors中的点击文案颜色,privacyColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyThreeColor;
/** 协议文案支持居中、居左、居右设置,默认居左 */
@property (nonatomic, assign) NSTextAlignment privacyAlignment;
/** 协议整体文案,前缀部分文案 */
@property (nonatomic, copy) NSString *privacyPreText;
/** 协议整体文案,后缀部分文案 */
@property (nonatomic, copy) NSString *privacySufText;
/** 运营商协议名称前缀文案,仅支持 <([《(【『 */
@property (nonatomic, copy) NSString *privacyOperatorPreText;
/** 运营商协议名称后缀文案,仅支持 >)]》)】』*/
@property (nonatomic, copy) NSString *privacyOperatorSufText;
/** 运营商协议指定显示顺序,默认0,即第1个协议显示,最大值可为3,即第4个协议显示*/
@property (nonatomic, assign) NSInteger privacyOperatorIndex;
/** 协议整体文案字体,小于10.0不生效 */
@property (nonatomic, strong) UIFont *privacyFont;
/** 协议整体文案行间距,默认0 */
@property (nonatomic, assign) CGFloat privacyLineSpaceDp;
/** 运营商协议文案字体,仅对运营商协议本体文案和前后缀生效,小于10.0不生效 */
@property (nonatomic, strong) UIFont *privacyOperatorFont;
/** 运营商协议文案下划线,仅对运营商协议本体文案和前后缀生效,YES:展示下划线;NO:不展示下划线,默认不展示 */
@property (nonatomic, assign) BOOL privacyOperatorUnderline;
/** checkBox是否扩大按钮可交互范围至"协议前缀部分文案(默认:我已阅读并同意)"区域,默认NO */
@property (nonatomic, assign) BOOL expandAuthPageCheckedScope;
/**
* 构建协议整体(包括checkBox)的frame,view布局或布局发生变化时调用,不实现则按默认处理
* 如果设置的width小于checkBox的宽则不生效,最小x、y为0,最大width、height为父试图宽高
* 最终会根据设置进来的width对协议文本进行自适应,得到的size是协议控件的最终大小
*/
@property (nonatomic, copy) PNSBuildFrameBlock privacyFrameBlock;
/**
* 未同意协议时点击登录按钮,协议整体文案的动画效果,不设置或设置为nil默认没有动画,SDK内部会主动更改动画的一些属性(包括:removedOnCompletion = NO、fillMode = kCAFillModeRemoved 及 delegate
*/
@property (nonatomic, strong, nullable) CAAnimation *privacyAnimation;
/**
* 未同意协议时点击登录按钮,checkbox的动画效果,不设置或设置为nil默认没有动画,SDK内部会主动更改动画的一些属性(包括:removedOnCompletion = NO、fillMode = kCAFillModeRemoved 及 delegate
*/
@property (nonatomic, strong, nullable) CAAnimation *checkboxAnimation;
/** 协议整体相对屏幕底部的Y轴距离,与其他有区别!!不能小于0 */
@property (nonatomic, assign) CGFloat privacyBottomOffetY DEPRECATED_MSG_ATTRIBUTE("Please use privacyFrameBlock instead");
/** 协议整体(包括checkBox)相对content view的左右边距,当协议整体宽度小于(content view宽度-2*左右边距)且居中模式,则左右边距设置无效,不能小于0 */
@property (nonatomic, assign) CGFloat privacyLRPadding DEPRECATED_MSG_ATTRIBUTE("Please use privacyFrameBlock instead");
#pragma mark- 切换到其他方式
/** changeBtn标题,内容、字体、大小、颜色 */
@property (nonatomic, copy) NSAttributedString *changeBtnTitle;
/** changeBtn是否隐藏,默认NO*/
@property (nonatomic, assign) BOOL changeBtnIsHidden;
/** 构建changeBtn的frameview布局或布局发生变化时调用,不实现则按默认处理 */
@property (nonatomic, copy) PNSBuildFrameBlock changeBtnFrameBlock;
/** changeBtn相对导航栏底部或标题栏底部的Y轴距离 */
@property (nonatomic, assign) CGFloat changeBtnTopOffetY DEPRECATED_MSG_ATTRIBUTE("Please use changeBtnFrameBlock instead");
#pragma mark- 协议详情页
/** 协议详情页容器是否自定义,默认NO,若为YES,则根据 PNSCodeLoginControllerClickProtocol 返回码获取协议点击详情信息 */
@property (nonatomic, assign) BOOL privacyVCIsCustomized;
/** 导航栏背景颜色设置 */
@property (nonatomic, strong) UIColor *privacyNavColor;
/** 导航栏标题字体、大小 */
@property (nonatomic, strong) UIFont *privacyNavTitleFont;
/** 导航栏标题颜色 */
@property (nonatomic, strong) UIColor *privacyNavTitleColor;
/** 导航栏返回图片 */
@property (nonatomic, strong) UIImage *privacyNavBackImage;
#pragma mark- 其他自定义控件添加及布局
/**
* 自定义控件添加,注意:自定义视图的创建初始化和添加到父视图,都需要在主线程!!
* @param superCustomView 父视图
*/
@property (nonatomic, copy) void(^customViewBlock)(UIView *superCustomView);
/**
* 每次授权页布局完成时会调用该block,可以在该block实现里面可设置自定义添加控件的frame
* @param screenSize 屏幕的size
* @param contentViewFrame content view的frame
* @param navFrame 导航栏的frame,仅全屏时有效
* @param titleBarFrame 标题栏的frame,仅弹窗时有效
* @param logoFrame logo图片的frame
* @param sloganFrame slogan的frame
* @param numberFrame 号码栏的frame
* @param loginFrame 登录按钮的frame
* @param changeBtnFrame 切换到其他方式按钮的frame
* @param privacyFrame 协议整体(包括checkBox)的frame
*/
@property (nonatomic, copy) void(^customViewLayoutBlock)(CGSize screenSize, CGRect contentViewFrame, CGRect navFrame, CGRect titleBarFrame, CGRect logoFrame, CGRect sloganFrame, CGRect numberFrame, CGRect loginFrame, CGRect changeBtnFrame, CGRect privacyFrame);
#pragma mark - 二次隐私协议弹窗设置
/** 二次隐私协议弹窗是否需要显示, 默认NO */
@property (nonatomic, assign) BOOL privacyAlertIsNeedShow;
/** 二次隐私协议弹窗点击按钮是否需要执行登陆,默认YES */
@property (nonatomic, assign) BOOL privacyAlertIsNeedAutoLogin;
/** 二次隐私协议弹窗显示/隐藏动画时长,单位秒,默认0.3s,<=0时使用默认值 */
@property (nonatomic, assign) CGFloat privacyAlertAnimationDuration;
/** 二次隐私协议弹窗显示自定义动画,默认从下往上位移动画 */
@property (nonatomic, strong, nullable) CAAnimation *privacyAlertEntryAnimation;
/** 二次隐私协议弹窗隐藏自定义动画,默认从上往下位移动画 */
@property (nonatomic, strong, nullable) CAAnimation *privacyAlertExitAnimation;
/** 二次隐私协议弹窗的四个圆角值,顺序为左上,左下,右下,右上,需要填充4个值,不足4个值则无效,如果值<=0则为直角 ,默认0*/
@property (nonatomic, copy) NSArray<NSNumber *> *privacyAlertCornerRadiusArray;
/** 二次隐私协议弹窗背景颜色,默认为白色 */
@property (nonatomic, strong) UIColor *privacyAlertBackgroundColor;
/** 二次隐私协议弹窗透明度,默认不透明1.0 ,设置范围0.3~1.0之间 */
@property (nonatomic, assign) CGFloat privacyAlertAlpha;
/** 二次隐私协议弹窗标题文字内容,默认"请阅读并同意以下条款" */
@property (nonatomic, copy) NSString *privacyAlertTitleContent;
/** 二次隐私协议弹窗标题文字字体,最小10,默认10 */
@property (nonatomic, strong) UIFont *privacyAlertTitleFont;
/** 二次隐私协议弹窗标题文字颜色,默认黑色 */
@property (nonatomic, strong) UIColor *privacyAlertTitleColor;
/** 二次隐私协议弹窗标题背景颜色,默认白色*/
@property (nonatomic, strong) UIColor *privacyAlertTitleBackgroundColor;
/** 二次隐私协议弹窗标题位置,默认居中*/
@property (nonatomic, assign) NSTextAlignment privacyAlertTitleAlignment;
/** 二次隐私协议弹窗协议内容文字字体,最小10,默认10 */
@property (nonatomic, strong) UIFont *privacyAlertContentFont;
/** 二次隐私协议弹窗协议内容行间距,默认0 */
@property (nonatomic, assign) CGFloat privacyAlertLineSpaceDp;
/** 二次隐私协议弹窗协议内容背景颜色,默认白色 */
@property (nonatomic, strong) UIColor *privacyAlertContentBackgroundColor;
/** 二次隐私协议弹窗协议内容颜色数组,[非点击文案颜色,点击文案颜色],默认[0x999999,0x1890FF] */
@property (nonatomic, copy) NSArray<UIColor *> *privacyAlertContentColors;
/** 二次隐私协议弹窗运营商协议内容文字字体,仅对运营商协议部分的文本生效,最小10,默认10 */
@property (nonatomic, strong) UIFont *privacyAlertContentOperatorFont;
/** 二次隐私协议弹窗运营商协议内容文字下划线,仅对运营商协议部分的文本生效,YES:展示下划线,NO:不展示下划线,默认不展示 */
@property (nonatomic, assign) BOOL privacyAlertContentUnderline;
/** 二次隐私协议弹窗协议运营商协议内容颜色,优先级最高,如果privacyAlertOperatorColors不设置,则取privacyAlertContentColors中的点击文案颜色,privacyAlertContentColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyAlertOperatorColor;
/** 二次隐私协议弹窗协议协议1内容颜色 ,优先级最高,如果privacyAlertOneColors不设置,则取privacyAlertContentColors中的点击文案颜色,privacyAlertContentColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyAlertOneColor;
/** 二次隐私协议弹窗协议协议2内容颜色 ,优先级最高,如果privacyAlertTwoColors不设置,则取privacyAlertContentColors中的点击文案颜色,privacyAlertContentColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyAlertTwoColor;
/** 二次隐私协议弹窗协议协议3内容颜色 ,优先级最高,如果privacyAlertThreeColors不设置,则取privacyAlertContentColors中的点击文案颜色,privacyAlertContentColors不设置,则是默认色*/
@property (nonatomic, strong) UIColor *privacyAlertThreeColor;
/** 二次隐私协议弹窗协议文案支持居中、居左、居右设置,默认居左 */
@property (nonatomic, assign) NSTextAlignment privacyAlertContentAlignment;
/** 二次隐私协议弹窗协议整体文案,前缀部分文案 ,如果不赋值,默认使用privacyPreText*/
@property (nonatomic, copy) NSString *privacyAlertPreText;
/** 二次隐私协议弹窗协议整体文案,后缀部分文案 如果不赋值,默认使用privacySufText*/
@property (nonatomic, copy) NSString *privacyAlertSufText;
/** 二次隐私协议弹窗按钮文字内容 默认“同意”*/
@property (nonatomic, copy) NSString *privacyAlertBtnContent;
/** 二次隐私协议弹窗登录按钮的圆角值,如果值<=0则为直角 ,默认0*/
@property (nonatomic, assign) CGFloat privacyAlertBtnCornerRadius;
/** 二次隐私协议弹窗按钮按钮背景图片 ,默认高度50.0pt,@[激活状态的图片,高亮状态的图片] */
@property (nonatomic, copy) NSArray<UIImage *> *privacyAlertBtnBackgroundImages;
/** 二次隐私协议弹窗按钮文字颜色,默认黑色, @[激活状态的颜色,高亮状态的颜色] */
@property (nonatomic, copy) NSArray<UIColor *> *privacyAlertButtonTextColors;
/** 二次隐私协议弹窗按钮文字字体,最小10,默认18*/
@property (nonatomic, strong) UIFont *privacyAlertButtonFont;
/** 二次隐私协议弹窗关闭按钮是否显示,默认显示 */
@property (nonatomic, assign) BOOL privacyAlertCloseButtonIsNeedShow;
/** 二次隐私协议弹窗右侧关闭按钮图片设置,默认内置的X图片*/
@property (nonatomic, strong) UIImage *privacyAlertCloseButtonImage;
/** 二次隐私协议弹窗背景蒙层是否显示 ,默认YES*/
@property (nonatomic, assign) BOOL privacyAlertMaskIsNeedShow;
/** 二次隐私协议弹窗点击背景蒙层是否关闭弹窗 ,默认YES*/
@property (nonatomic, assign) BOOL tapPrivacyAlertMaskCloseAlert;
/** 二次隐私协议弹窗蒙版背景颜色,默认黑色 */
@property (nonatomic, strong) UIColor *privacyAlertMaskColor;
/** 二次隐私协议弹窗蒙版透明度 设置范围0.3~1.0之间 ,默认0.5*/
@property (nonatomic, assign) CGFloat privacyAlertMaskAlpha;
/** 二次隐私协议弹窗蒙版显示动画,默认渐显动画 */
@property (nonatomic, strong) CAAnimation *privacyAlertMaskEntryAnimation;
/** 二次隐私协议弹窗蒙版消失动画,默认渐隐动画 */
@property (nonatomic, strong) CAAnimation *privacyAlertMaskExitAnimation;
/** 二次隐私协议弹窗尺寸设置,不能超出父视图 content viewheight不能小于50width不能小于0,默认屏幕居中,宽为屏幕的宽度减掉80,高为200 */
@property (nonatomic, copy) PNSBuildFrameBlock privacyAlertFrameBlock;
/** 二次隐私协议弹窗标题尺寸,默认x=0,y=0,width=弹窗宽度,最小宽度为100,height=根据文本计算的高度,最小高度为15,不能超出父视图 */
@property (nonatomic, copy) PNSBuildFrameBlock privacyAlertTitleFrameBlock;
/** 二次隐私协议弹窗内容尺寸,默认为从标题顶部位置开始,最终会根据设置进来的width对协议文本进行自适应,得到的size是协议控件的最终大小。不能超出父视图 */
@property (nonatomic, copy) PNSBuildFrameBlock privacyAlertPrivacyContentFrameBlock;
/** 二次隐私协议弹窗确认按钮尺寸,默认为父视图的宽度一半,居中显示。高度默认50, */
@property (nonatomic, copy) PNSBuildFrameBlock privacyAlertButtonFrameBlock;
/** 二次隐私协议弹窗右侧关闭按钮尺寸,默认宽高44,居弹窗右侧15,居弹窗顶部0*/
@property (nonatomic, copy) PNSBuildFrameBlock privacyAlertCloseFrameBlock;
/**
* 二次授权页弹窗自定义控件添加,注意:自定义视图的创建初始化和添加到父视图,都需要在主线程!!
* @param superPrivacyAlertCustomView 父视图
*/
@property (nonatomic, copy) void(^privacyAlertCustomViewBlock)(UIView *superPrivacyAlertCustomView);
/**
* 二次授权页弹窗布局完成时会调用该block,可以在该block实现里面可设置自定义添加控件的frame
* @param privacyAlertFrame 二次授权页弹窗frame
* @param privacyAlertTitleFrame 二次授权页弹窗标题frame
* @param privacyAlertPrivacyContentFrame 二次授权页弹窗协议内容frame
* @param privacyAlertButtonFrame 二次授权页弹窗确认按钮frame
* @param privacyAlertCloseFrame 二次授权页弹窗右上角关闭按钮frame
*/
@property (nonatomic, copy) void(^privacyAlertCustomViewLayoutBlock)(CGRect privacyAlertFrame, CGRect privacyAlertTitleFrame, CGRect privacyAlertPrivacyContentFrame, CGRect privacyAlertButtonFrame, CGRect privacyAlertCloseFrame);
@end
NS_ASSUME_NONNULL_END
@@ -1,6 +0,0 @@
framework module ATAuthSDK {
umbrella header "ATAuthSDK.h"
export *
module * { export * }
}
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDataTypes</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -1,48 +0,0 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// 日志级别
extern NSString * const ACM_LOGGER_LEVEL_VERBOSE;
extern NSString * const ACM_LOGGER_LEVEL_DEBUG;
extern NSString * const ACM_LOGGER_LEVEL_INFO;
extern NSString * const ACM_LOGGER_LEVEL_WARN;
extern NSString * const ACM_LOGGER_LEVEL_ERROR;
extern NSString * const ACM_LOGGER_LEVEL_REALTIME;
@interface ACMLogger : NSObject
- (instancetype)init NS_UNAVAILABLE;
/// 日志是否入库,默认不入库
@property (nonatomic, assign) BOOL enterDatabase;;
/// 日志是否允许上传,默认不上传
@property (nonatomic, assign) BOOL isAllowUpload;
/**
* 日志入库
* @param obj 日志的具体内容
* @param level 日志等级
*/
- (BOOL)logger:(id)obj level:(NSString *)level ;
/**
* 上传日志
* @param startDate 日志开始时间,如果传nil则查询不加该条件
* @param endDate 日志结束时间,如果传nil则查询不加该条件
* @param levels 日志等级数组,里面包含对应的日志等级字符串,如果传nil则查询不加该条件
*/
- (void)uploadLoggersWithLevels:(NSArray <NSString *>* _Nullable)levels
startDate:(NSDate * _Nullable)startDate
endDate:(NSDate * _Nullable)endDate;
/**
* 上传失败的日志,一般放在重启应用后
*/
- (void)uploadFailedRecords;
@end
NS_ASSUME_NONNULL_END
@@ -1,73 +0,0 @@
#import <Foundation/Foundation.h>
#import <YTXMonitor/ACMLogger.h>
#import <YTXMonitor/ACMMonitor.h>
#import <YTXMonitor/ACMProtocol.h>
NS_ASSUME_NONNULL_BEGIN
@interface ACMManager : NSObject
/// 日志操作对象
@property (nonatomic, strong, readonly) ACMLogger *logger;
/// 埋点操作对象
@property (nonatomic, strong, readonly) ACMMonitor *monitor;
- (instancetype)init NS_UNAVAILABLE;
/**
* 初始化
* @param databaseName 数据库名,不指定则默认为 “ACMDatabase”
* @param monitorTableName 埋点表名,必须要指定,用来区分不同产品数据
* @param loggerTablename 日志表名,必须要指定,用来区分不同产品数据
* @param limitKeyPrefix 限流信息存储到本地key的前缀,用来区分不同产品的限流缓存
*/
- (instancetype)initWithDatabaseName:(NSString * _Nullable)databaseName
monitorTableName:(NSString *)monitorTableName
loggerTableName:(NSString *)loggerTablename
limitKeyPrefix:(NSString *)limitKeyPrefix;
/**
* 初始化
* @param databaseName 数据库名,不指定则默认为 “ACMDatabase”
* @param monitorTableName 埋点表名,必须要指定,用来区分不同产品数据
* @param loggerTablename 日志表名,必须要指定,用来区分不同产品数据
* @param limitKeyPrefix 限流信息存储到本地key的前缀,用来区分不同产品的限流缓存
* @param uploadCount 每次上传条数
* @param retryRightNow 上传失败是否立马重试,默认立马重试
* @param uploadOnce 是否只执行一轮上传,默认NO
*/
- (instancetype)initWithDatabaseName:(NSString * _Nullable)databaseName
monitorTableName:(NSString *)monitorTableName
loggerTableName:(NSString *)loggerTablename
limitKeyPrefix:(NSString *)limitKeyPrefix
uploadCount:(NSInteger)uploadCount
retryRightNow:(BOOL)retryRightNow
uploadOnce:(BOOL)uploadOnce;
/**
* 获取组件当前版本号
*/
- (NSString *)getVersion;
/**
* 设置日志埋点上传代理对象
* 注:这里是强引用
* @param uploadDelegate 代理对象,需要实现 ACMProtocol 协议
*/
- (void)setUploadDelegate:(id<ACMProtocol> _Nullable)uploadDelegate;
/**
* 更新限流相关
* @param isLimit 是否限流
* @param limitTimeHour 限流区间大小
* @param limitCount 区间内限流次数
*/
- (void)updateLimitConfig:(BOOL)isLimit
limitTimeHour:(NSInteger)limitTimeHour
limitCount:(NSInteger)limitCount;
@end
NS_ASSUME_NONNULL_END
@@ -1,55 +0,0 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSInteger, ACM_DELETE_TYPE) {
ACM_DELETE_TYPE_ALL,
ACM_DELETE_TYPE_FAILED,
ACM_DELETE_TYPE_UNUPLOAD
};
@interface ACMMonitor : NSObject
- (instancetype)init NS_UNAVAILABLE;
/// 埋点是否入库,默认入库
@property (nonatomic, assign) BOOL enterDatabase;;
/// 埋点是否允许上传,默认上传
@property (nonatomic, assign) BOOL isAllowUpload;
/**
* 非实时埋点入库
* @param obj 埋点具体内容
*/
- (BOOL)monitor:(id)obj;
/**
* 实时埋点入库
* @param obj 埋点的具体内容
*/
- (BOOL)monitorRealtime:(id)obj;
/**
* 手动触发埋点上传,只上传未上传过的埋点
*/
- (void)uploadMonitorByManual;
/**
* 上传失败的埋点,一般放在重启应用后
*/
- (void)uploadFailedRecords;
/**
* 删除埋点
* @param type 删除类型
* @param block 结果的异步回调
*/
- (void)deleteRecordsByType:(ACM_DELETE_TYPE)type block:(void (^)(BOOL))block;
@end
NS_ASSUME_NONNULL_END
@@ -1,17 +0,0 @@
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ACMProtocol <NSObject>
@required
/// 埋点抛出,可在抛出里面进行上传
- (BOOL)uploadMonitors:(NSArray<NSDictionary *> *)monitors;
/// 日志抛出,可在抛出里面进行上传
- (BOOL)uploadLoggers:(NSArray<NSDictionary *> *)loggers;
@end
NS_ASSUME_NONNULL_END
@@ -1,4 +0,0 @@
#import <YTXMonitor/ACMManager.h>
#import <YTXMonitor/ACMProtocol.h>
@@ -1,6 +0,0 @@
framework module YTXMonitor {
umbrella header "YTXMonitor.h"
export *
module * { export * }
}
@@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDataTypes</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -1,100 +0,0 @@
//
// YTXNetUtils.h
// YTXOperators
//
// Created by yangli on 2020/11/9.
// Copyright © 2020 com.alicom. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface YTXNetUtils : NSObject
/**
判断当前设备蜂窝数据网络是否开启,即3G/4G
@return 结果
*/
- (BOOL)checkDeviceCellularDataEnable;
/**
判断当前上网卡运营商是否是中国联通
@return 结果
*/
- (BOOL)isChinaUnicom;
/**
判断当前上网卡运营商是否是中国移动
@return 结果
*/
- (BOOL)isChinaMobile;
/**
判断当前上网卡运营商是否是中国电信
@return 结果
*/
- (BOOL)isChinaTelecom;
/**
获取当前上网卡运营商名称,比如中国移动、中国电信、中国联通
@return 结果
*/
- (NSString *)getCurrentCarrierName;
/**
获取当前上网卡运营商编码,比如46000、46001、46003
@return 结果
*/
- (NSString *)getCurrentCarrierCode API_DEPRECATED("废弃,完成不可用,返回空字符串", ios(4.0, 16.0));
/**
获取当前上网卡网络类型,比如WiFi,4G
@return 结果
*/
- (NSString *)getNetworktype;
/**
判断当前设备是否有SIM卡
@return 结果
*/
- (BOOL)simSupportedIsOK;
/**
判断wwan是否开着(通过p0网卡判断,无wifi或有wifi情况下都能检测到)
@return 结果
*/
- (BOOL)isWWANOpen;
/**
判断WiFi是否开着
@return 结果
*/
- (BOOL)isWiFiOpen;
/**
判断wwan是否开着(仅无wifi情况下)
@return 结果
*/
- (BOOL)reachableViaWWAN;
/**
获取设备当前网络私网IP地址
@return 结果
*/
- (NSString *)getMobilePrivateIPAddress:(BOOL)preferIPv4;
/**
获取双卡设备下,非上网卡信息
@return 结果
*/
- (NSString *)getOptionalCarrierInfo API_DEPRECATED("废弃,完成不可用,返回空字符串", ios(4.0, 16.0));;
/**
获取当前蜂网络Ip地址
*/
- (NSString *)getCellularIp;
@end
NS_ASSUME_NONNULL_END
@@ -1,20 +0,0 @@
//
// YTXOperators.h
// YTXOperators
//
// Created by yangli on 2020/11/9.
// Copyright © 2020 com.alicom. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for YTXOperators.
FOUNDATION_EXPORT double YTXOperatorsVersionNumber;
//! Project version string for YTXOperators.
FOUNDATION_EXPORT const unsigned char YTXOperatorsVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <YTXOperators/PublicHeader.h>
#import <YTXOperators/YTXVendorService.h>
#import <YTXOperators/YTXNetUtils.h>
@@ -1,85 +0,0 @@
//
// YTXVendorService.h
// ATAuthSDK
//
// Created by 刘超的MacBook on 2020/1/15.
// Copyright © 2020. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface YTXRequest : NSObject
/// 接口调用超时时间,注:目前内部限制最小超时时间为5s,小于5s则按5s设置
@property (nonatomic, assign) NSTimeInterval timeout;
/// 是否是蜂窝网络
@property (nonatomic, assign) BOOL isReachableViaWWAN;
@end
@interface YTXVendorConfig : NSObject
/// 当前供应商标识:中移互联(cm_zyhl),联通小沃(cu_xw),联通在线(cu_zx),电信世纪龙(ct_sjl
@property (nonatomic, copy) NSString *vendorKey;
/// 供应商二级标识
@property (nonatomic, copy) NSString *vendorSubKey;
/// 供应商 access id
@property (nonatomic, copy) NSString *vendorAccessId;
/// 供应商 access secret
@property (nonatomic, copy) NSString *vendorAccessSecret;
@end
@interface YTXVendorService : NSObject
/**
* 获取SDK版本号
*/
+ (NSString *)getVersion;
/**
* 获取供应商SDK版本号
*/
+ (NSDictionary *)getVendorsVersion;
/**
* 初始化或更新各个供应商的接口调用对象,根据各个供应商的配置信息
* @param vendorConfigs 各个供应商配置信息
*/
- (void)updateVendorHandlers:(NSArray<YTXVendorConfig *> *)vendorConfigs;
/**
* 获取本机号码校验Token
* @param request 请求参数结构体
* @param vendorConfig 当前供应商配置信息
* @param complete 结果回调
*/
- (void)getVerifyTokenWithRequest:(YTXRequest *)request
vendorConfig:(YTXVendorConfig *)vendorConfig
complete:(void(^)(NSDictionary *response))complete;
/**
* 获取手机掩码
* @param request 请求参数结构体
* @param vendorConfig 当前供应商配置信息
* @param complete 结果回调
*/
- (void)getMaskNumberWithRequest:(YTXRequest *)request
vendorConfig:(YTXVendorConfig *)vendorConfig
complete:(void(^)(NSDictionary *response))complete;
/**
* 电信/联通获取一键登录Token
* @param request 请求参数结构体
* @param vendorConfig 当前供应商配置信息
* @param complete 结果回调
* @abstract 移动的获取登录Token不走这个回调,走弹起授权页的回调
*/
- (void)getLoginTokenWithRequest:(YTXRequest *)request
vendorConfig:(YTXVendorConfig *)vendorConfig
complete:(void(^)(NSDictionary *response))complete;
- (void)deleteCacheWithVendorConfigs:(NSArray<YTXVendorConfig *> *)vendorConfigs;
@end
NS_ASSUME_NONNULL_END
@@ -1,6 +0,0 @@
framework module YTXOperators {
umbrella header "YTXOperators.h"
export *
module * { export * }
}
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array>
<dict>
<key>NSPrivacyCollectedDataType</key>
<string>NSPrivacyCollectedDataTypeOtherDataTypes</string>
<key>NSPrivacyCollectedDataTypeLinked</key>
<false/>
<key>NSPrivacyCollectedDataTypeTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypePurposes</key>
<array>
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
</array>
</dict>
</array>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [ Alibaba ] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -1,80 +0,0 @@
{
"name": "阿里云号码认证SDK",
"id": "AliCloud-NirvanaPns",
"version": "2.1.9",
"description": "阿里云号码认证SDK",
"_dp_type":"nativeplugin",
"_dp_nativeplugin":{
"android": {
"plugins": [
{
"type": "module",
"name": "AliCloud-NirvanaPns",
"class": "com.nirvana.prd.auth.ALiLoginManager"
}
],
"hooksClass": "",
"integrateType": "aar",
"dependencies": [],
"compileOptions": {
"sourceCompatibility": "1.8",
"targetCompatibility": "1.8"
},
"abis": [
"armeabi-v7a","arm64-v8a","x86"
],
"minSdkVersion": "21",
"useAndroidX": true,
"permissions": [
"android.permission.INTERNET",
"android.permission.ACCESS_WIFI_STATE",
"android.permission.CHANGE_NETWORK_STATE",
"android.permission.READ_PHONE_STATE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.ACCESS_NETWORK_STATE"
],
"parameters": {
}
},
"ios": {
"plugins": [
{
"type": "module",
"name": "AliCloud-NirvanaPns",
"class": "ATAuthSDKModule"
}
],
"integrateType": "framework",
"hooksClass": "",
"frameworks": [
"ATAuthSDK.framework", "YTXMonitor.framework", "YTXOperators.framework","Network.framework"
],
"embedFrameworks": [
],
"capabilities": {
"entitlements": {
},
"plists": {
}
},
"plists": {
},
"assets": [
],
"privacies": [
""
],
"embedSwift": false,
"deploymentTarget": "9.0",
"validArchitectures": [
"armv7","arm64","x86_64"
],
"parameters": {
},
"resources": [
"ATAuthSDK.bundle"
]
}
}
}
+33 -22
View File
@@ -1,19 +1,6 @@
{
"pages": [
{
"path": "pages/memberInfo/login",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/test/test",
"style": {
"navigationBarTitleText": "测试页面"
}
},
{
"path": "pages/index/index",
"style": {
@@ -25,6 +12,18 @@
}
}
},
{
"path": "pages/memberInfo/purchaseCard",
"style": {
"navigationBarTitleText": "购买会员卡"
}
},
{
"path": "pages/memberInfo/login",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/course/index",
"style": {
@@ -40,7 +39,6 @@
{
"path": "pages/memberInfo/memberInfo",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的",
"app-plus": {
"animationType": "fade-in",
@@ -203,10 +201,29 @@
}
},
{
"path": "pages/memberInfo/purchaseCard",
"path": "pages/memberInfo/cardDetail",
"style": {
"navigationBarTitleText": "会员卡详情"
}
},
{
"path": "pages/memberInfo/confirmPayment",
"style": {
"navigationBarTitleText": "确认支付"
}
},
{
"path": "pages/memberInfo/setPayPassword",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "购买会员卡"
"navigationBarTitleText": "设置支付密码"
}
},
{
"path": "pages/memberInfo/rechargeStoredCard",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "储值卡充值"
}
},
{
@@ -289,12 +306,6 @@
"navigationStyle": "custom",
"navigationBarTitleText": "消息详情"
}
},
{
"path": "pages/webview/webview",
"style": {
"navigationBarTitleText": ""
}
}
],
"globalStyle": {
+2 -3
View File
@@ -98,6 +98,7 @@ import { onLoad, onUnload } from '@dcloudio/uni-app'
// 引入状态组件(路径与你保持一致)
import QrStatus from '@/components/QRCode/StatusCard.vue'
import { getQRCode, getMyMemberCards } from '@/api/main.js'
import { getMemberId } from '@/utils/request.js'
let image = ref("")
let width = ref(0)
@@ -212,9 +213,7 @@ import { getQRCode, getMyMemberCards } from '@/api/main.js'
})
try {
const memberInfo = uni.getStorageSync('memberInfo')
console.log(memberInfo)
const memberId = memberInfo?.id || memberInfo?.memberId
const memberId = getMemberId()
console.log('[签到页面] 会员ID:', memberId)
if (!memberId) {
@@ -138,10 +138,10 @@ import { ref, computed } from 'vue'
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
import {
loadMemberStore,
cancelOngoingBooking,
formatUpcomingAlert
} from '@/common/memberInfo/store.js'
import { getMemberId } from '@/utils/request.js'
import { canCancelBooking, canSigninCourse } from '@/common/memberInfo/bookingStore.js'
import { signinGroupCourse, cancelBooking as cancelBookingApi } from '@/api/main.js'
@@ -184,8 +184,7 @@ function setActiveTab(tab) {
// 签到团课
async function signinCourse(item) {
const store = loadMemberStore()
const memberId = store.memberProfile?.id || store.profile?.id
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
@@ -0,0 +1,398 @@
<template>
<view class="scroll-container theme-light">
<view class="card-detail-page">
<view v-if="card" class="card-detail-page__body">
<view class="cd-hero">
<view class="cd-hero__card">
<view class="cd-hero__card-header">
<text class="cd-hero__card-name">{{ card.memberCardName || card.cardName }}</text>
<view class="cd-hero__card-tag" :class="'cd-hero__card-tag--' + getTagClass(card.memberCardType || card.cardType)">
<text class="cd-hero__card-tag-text">{{ getCardTypeName(card.memberCardType || card.cardType) }}</text>
</view>
</view>
<view class="cd-hero__card-body">
<view class="cd-hero__card-item" v-if="card.memberCardValidityDays || card.validityDays">
<text class="cd-hero__card-label">有效期</text>
<text class="cd-hero__card-value">{{ card.memberCardValidityDays || card.validityDays }}</text>
</view>
<view class="cd-hero__card-item" v-if="(card.memberCardType === 'COUNT_CARD' || card.cardType === 'COUNT') && (card.memberCardTotalTimes || card.totalTimes)">
<text class="cd-hero__card-label">总次数</text>
<text class="cd-hero__card-value">{{ card.memberCardTotalTimes || card.totalTimes }}</text>
</view>
<view class="cd-hero__card-item" v-if="(card.memberCardType === 'STORED_VALUE_CARD' || card.cardType === 'BALANCE') && (card.memberCardAmount || card.amount)">
<text class="cd-hero__card-label">储值金额</text>
<text class="cd-hero__card-value">{{ card.memberCardAmount || card.amount }}</text>
</view>
</view>
<view class="cd-hero__card-footer">
<text class="cd-hero__card-price">¥{{ card.memberCardPrice || card.price }}</text>
</view>
</view>
</view>
<view class="cd-section">
<text class="cd-section__title">会员卡权益</text>
<view class="cd-benefits">
<view class="cd-benefit">
<view class="cd-benefit__icon">💪</view>
<view class="cd-benefit__content">
<text class="cd-benefit__title">全场器械使用</text>
<text class="cd-benefit__desc">免费使用所有健身器械和设备</text>
</view>
</view>
<view class="cd-benefit">
<view class="cd-benefit__icon">🏋</view>
<view class="cd-benefit__content">
<text class="cd-benefit__title">免费团课</text>
<text class="cd-benefit__desc">预约参加各类团课次卡除外</text>
</view>
</view>
<view class="cd-benefit">
<view class="cd-benefit__icon">🧴</view>
<view class="cd-benefit__content">
<text class="cd-benefit__title">淋浴服务</text>
<text class="cd-benefit__desc">免费使用淋浴间和储物柜</text>
</view>
</view>
<view class="cd-benefit">
<view class="cd-benefit__icon">📱</view>
<view class="cd-benefit__content">
<text class="cd-benefit__title">在线预约</text>
<text class="cd-benefit__desc">APP在线预约课程和签到</text>
</view>
</view>
</view>
</view>
<view class="cd-section" v-if="card.extraConfig">
<text class="cd-section__title">详细解读</text>
<view class="cd-desc">
<text class="cd-desc__text">{{ parseExtraConfig(card.extraConfig) }}</text>
</view>
</view>
<view class="cd-section">
<text class="cd-section__title">购买须知</text>
<view class="cd-notes">
<text class="cd-notes__item">1. 会员卡一经购买非特殊情况不予退款</text>
<text class="cd-notes__item">2. 请在有效期内使用过期自动失效</text>
<text class="cd-notes__item">3. 会员卡仅限本人使用不得转借他人</text>
<text class="cd-notes__item">4. 支付订单有效时间为15分钟超时自动取消</text>
</view>
</view>
<view class="cd-bottom">
<view class="cd-bottom__total">
<text class="cd-bottom__total-label">价格</text>
<text class="cd-bottom__total-price">¥{{ card.memberCardPrice || card.price }}</text>
</view>
<view class="cd-bottom__btn" @tap="handleSelectCard">
<text class="cd-bottom__btn-text">选择该卡</text>
</view>
</view>
</view>
<view v-else class="cd-loading">
<text>{{ loading ? '加载中...' : '暂无数据' }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { getMemberCardById } from '@/api/main.js'
const card = ref(null)
const loading = ref(false)
function getCardTypeName(type) {
const map = {
TIME_CARD: '时长卡',
DURATION: '时长卡',
COUNT_CARD: '次卡',
COUNT: '次卡',
STORED_VALUE_CARD: '储值卡',
BALANCE: '储值卡'
}
return map[type] || type
}
function getTagClass(type) {
if (type === 'TIME_CARD' || type === 'DURATION') return 'time'
if (type === 'COUNT_CARD' || type === 'COUNT') return 'count'
if (type === 'STORED_VALUE_CARD' || type === 'BALANCE') return 'value'
return 'default'
}
function parseExtraConfig(config) {
if (!config) return ''
try {
const obj = JSON.parse(config)
return obj.description || obj.desc || config
} catch (e) {
return config
}
}
function handleSelectCard() {
if (!card.value) return
uni.navigateTo({ url: `/pages/memberInfo/purchaseCard?cardId=${card.value.memberCardId || card.value.id}` })
}
async function loadCardDetail(cardId) {
if (!cardId) return
loading.value = true
try {
const res = await getMemberCardById(cardId)
if (res && res.memberCardId) {
card.value = res
} else if (res && res.code === 200 && res.data) {
card.value = res.data
} else {
uni.showToast({ title: res?.message || '加载失败', icon: 'none' })
}
} catch (e) {
console.error('加载会员卡详情失败:', e)
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
onMounted(() => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
const cardId = options.cardId || options.id
if (cardId) {
loadCardDetail(cardId)
}
})
onUnmounted(() => {
})
</script>
<style lang="scss">
@import '@/common/style/base.css';
@import '@/common/style/memberInfo/pages/page-reset.css';
@import '@/common/style/memberInfo/pages/sub-page-base.css';
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-sub-nav.css';
.card-detail-page {
min-height: 100vh;
background: #F5F7FA;
}
.card-detail-page__body {
padding-bottom: 120px;
}
.cd-loading {
display: flex;
justify-content: center;
align-items: center;
height: 60vh;
font-size: 14px;
color: #718096;
}
.cd-hero {
background: linear-gradient(135deg, #1A365D 0%, #2C5282 100%);
padding: 32px 24px;
}
.cd-hero__card {
background: rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 24px;
backdrop-filter: blur(10px);
}
.cd-hero__card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.cd-hero__card-name {
font-size: 22px;
font-weight: 700;
color: #FFFFFF;
}
.cd-hero__card-tag {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
&--time { background: rgba(66, 153, 225, 0.3); color: #90CDF4; }
&--count { background: rgba(237, 137, 54, 0.3); color: #FBD38D; }
&--value { background: rgba(56, 161, 105, 0.3); color: #9AE6B4; }
&--default { background: rgba(160, 174, 192, 0.3); color: #CBD5E0; }
}
.cd-hero__card-body {
display: flex;
gap: 24px;
margin-bottom: 20px;
}
.cd-hero__card-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.cd-hero__card-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.6);
}
.cd-hero__card-value {
font-size: 18px;
font-weight: 600;
color: #FFFFFF;
}
.cd-hero__card-footer {
display: flex;
align-items: baseline;
gap: 8px;
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.cd-hero__card-price {
font-size: 32px;
font-weight: 700;
color: #F6E05E;
}
.cd-hero__card-original {
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
text-decoration: line-through;
}
.cd-section {
margin: 16px;
background: #FFFFFF;
border-radius: 16px;
padding: 20px;
}
.cd-section__title {
font-size: 16px;
font-weight: 600;
color: #1A202C;
margin-bottom: 16px;
display: block;
}
.cd-benefits {
display: flex;
flex-direction: column;
gap: 16px;
}
.cd-benefit {
display: flex;
align-items: flex-start;
gap: 12px;
}
.cd-benefit__icon {
font-size: 24px;
flex-shrink: 0;
}
.cd-benefit__content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.cd-benefit__title {
font-size: 14px;
font-weight: 600;
color: #2D3748;
}
.cd-benefit__desc {
font-size: 12px;
color: #718096;
}
.cd-desc__text {
font-size: 14px;
color: #4A5568;
line-height: 1.6;
}
.cd-notes {
display: flex;
flex-direction: column;
gap: 8px;
}
.cd-notes__item {
font-size: 13px;
color: #718096;
line-height: 1.5;
}
.cd-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
background: #FFFFFF;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.05);
z-index: 100;
}
.cd-bottom__total {
display: flex;
align-items: baseline;
gap: 8px;
}
.cd-bottom__total-label {
font-size: 14px;
color: #718096;
}
.cd-bottom__total-price {
font-size: 28px;
font-weight: 700;
color: #E53E3E;
}
.cd-bottom__btn {
background: linear-gradient(135deg, #1677FF 0%, #0958D9 100%);
border-radius: 12px;
padding: 16px 48px;
transition: all 0.2s;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
.cd-bottom__btn-text {
font-size: 16px;
font-weight: 600;
color: #FFFFFF;
}
</style>
File diff suppressed because it is too large Load Diff
+221 -113
View File
@@ -20,47 +20,41 @@
<text class="phone-display">验证码登录</text>
<text class="auth-tip">认证服务由中国移动提供</text>
<view class="form-group">
<view class="input-wrapper" :class="{ focused: isSmsPhoneFocused }">
<view class="form-group phone-form-group">
<input
v-model="smsPhone"
type="number"
placeholder="请输入手机号"
maxlength="11"
class="phone-input"
class="underline-input"
confirm-type="next"
@focus="isSmsPhoneFocused = true"
@blur="isSmsPhoneFocused = false"
/>
</view>
<view class="form-group code-form-group">
<VerifyCodeInput
ref="codeInputRef"
v-model="code"
:length="4"
:mask="false"
type="underline"
activeColor="#FF8C42"
@complete="handleCodeComplete"
@focus="onInputFocus"
@blur="onInputBlur"
/>
</view>
<view class="form-group">
<view class="input-wrapper code-input" :class="{ focused: isCodeFocused }">
<input
v-model="code"
type="number"
placeholder="请输入验证码"
maxlength="6"
class="phone-input"
confirm-type="done"
@focus="isCodeFocused = true"
@blur="isCodeFocused = false"
/>
<button
class="send-code-btn"
class="login-btn primary"
:class="{ disabled: countdown > 0 || isLoading }"
@tap="handleSendCode"
:disabled="countdown > 0 || isLoading"
>
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
</button>
</view>
</view>
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }" @tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
<text>{{ isLoading ? '登录中...' : '验证码登录' }}</text>
</button>
<button class="login-btn secondary" @tap="switchToPhone">
<text>其他登录方式</text>
@@ -84,22 +78,7 @@
<text class="phone-display">本机号码登录</text>
<text class="auth-tip">认证服务由中国移动提供</text>
<view class="form-group phone-login">
<view class="input-wrapper" :class="{ focused: isInputFocused }">
<input
v-model="phone"
type="number"
placeholder="请输入手机号"
maxlength="11"
class="phone-input"
confirm-type="done"
@focus="isInputFocused = true"
@blur="isInputFocused = false"
/>
</view>
</view>
<button class="login-btn primary" :class="{ disabled: !canSubmit, loading: isLoading }" @tap="handlePhoneLogin" :disabled="!canSubmit || isLoading">
<button class="login-btn primary" :class="{ disabled: isLoading, loading: isLoading }" @tap="handlePhoneLogin" :disabled="isLoading">
<text>{{ isLoading ? '登录中...' : '一键登录' }}</text>
</button>
@@ -123,17 +102,14 @@
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue'
import { setToken } from '@/utils/request.js'
import { ref, onUnmounted } from 'vue'
import { setToken, setRefreshToken } from '@/utils/request.js'
import { loginWithPhone, oneClickLogin, sendCode } from '@/api/main.js'
// 调用真实后端API,不再使用测试数据
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
const activeCard = ref('phone')
const phone = ref('')
const isLoading = ref(false)
const isInputFocused = ref(false)
const agreed = ref(false)
const agreed = ref(true)
const shakeAgreement = ref(false)
const smsPhone = ref('')
@@ -157,13 +133,27 @@ function showToast(msg) {
}, 2000)
}
const canSubmit = computed(() => {
return /^1[3-9]\d{9}$/.test(phone.value) && !isLoading.value
})
/**
* 保存登录信息到本地存储
*/
function saveLoginInfo(loginInfo) {
if (!loginInfo) return
const canSmsSubmit = computed(() => {
return /^1[3-9]\d{9}$/.test(smsPhone.value) && /^\d{6}$/.test(code.value) && !isLoading.value
})
// 保存 token (已经在 request.js 中通过 setToken 保存)
// 保存会员信息
if (loginInfo.memberId || loginInfo.id) {
const memberInfo = {
id: loginInfo.memberId || loginInfo.id,
memberId: loginInfo.memberId || loginInfo.id,
phone: loginInfo.phone || '',
nickname: loginInfo.nickname || '',
avatar: loginInfo.avatar || ''
}
uni.setStorageSync('loginMemberInfo', memberInfo)
console.log('[login] 登录信息已保存:', memberInfo)
}
}
function onAgreementChange(e) {
agreed.value = e.detail.value.includes('agreed')
@@ -175,8 +165,6 @@ function switchToSms() {
function switchToPhone() {
activeCard.value = 'phone'
phone.value = ''
phoneError.value = ''
}
function showAgreement() {
@@ -187,16 +175,20 @@ function showPrivacy() {
uni.showModal({ title: '隐私政策', content: '这里是隐私政策内容...', showCancel: false })
}
function validatePhone() {
if (!phone.value) {
showToast('请输入手机号')
return false
function handleCodeComplete(val) {
if (!isLoading.value) {
setTimeout(() => {
handleSmsLogin()
}, 100)
}
if (!/^1[3-9]\d{9}$/.test(phone.value)) {
showToast('请输入正确的手机号')
return false
}
return true
}
function onInputFocus() {
isCodeFocused.value = true
}
function onInputBlur() {
isCodeFocused.value = false
}
function validateSmsPhone() {
@@ -217,35 +209,147 @@ async function handlePhoneLogin() {
return
}
if (!validatePhone()) return
isLoading.value = true
try {
await new Promise((resolve, reject) => {
uni.preLogin({
provider: 'univerify',
success: resolve,
fail: (err) => {
console.warn('预登录失败,切换到验证码登录:', err)
reject(err)
}
})
})
uni.login({
provider: 'univerify',
loginBtnText: '本机号码一键登录',
univerifyStyle: {
fullScreen: true,
backgroundColor: '#ffffff',
icon: {
path: '/static/logo.png',
width: '60px',
height: '60px'
},
phoneNum: {
color: '#333333'
},
slogan: {
color: '#999999'
},
authButton: {
normalColor: '#FF8C42',
highlightColor: '#E07A38',
disabledColor: '#FFB88A',
textColor: '#ffffff',
title: '本机号码一键登录',
borderRadius: '24px'
},
otherLoginButton: {
visible: true,
normalColor: '#ffffff',
highlightColor: '#f5f5f5',
textColor: '#666666',
title: '其他登录方式',
borderColor: '#e5e5e5',
borderRadius: '24px'
},
privacyTerms: {
defaultCheckBoxState: true,
textColor: '#999999',
termsColor: '#FF8C42',
prefix: '我已阅读并同意',
suffix: '并使用本机号码登录',
privacyItems: [
{
url: 'https://example.com/agreement',
title: '用户服务协议'
},
{
url: 'https://example.com/privacy',
title: '隐私政策'
}
]
}
},
success: async (loginRes) => {
uni.closeAuthView()
try {
const authResult = loginRes.authResult
if (!authResult) {
console.error('authResult为空:', loginRes)
showToast('一键登录失败,请使用验证码登录')
switchToSms()
isLoading.value = false
return
}
const accessToken = authResult.accessToken || authResult.access_token || authResult.token
const openid = authResult.openid || authResult.openId
console.log('accessToken:', accessToken, 'openid:', openid)
if (!accessToken) {
console.error('accessToken为空,authResult:', authResult)
showToast('一键登录失败,请使用验证码登录')
switchToSms()
isLoading.value = false
return
}
const res = await oneClickLogin({
accessToken: phone.value,
openid: 'uniapp_phone_login',
accessToken: accessToken,
openid: openid,
nickname: '',
avatar: ''
})
if (res && res.accessToken) {
setToken(res.accessToken)
uni.setStorageSync('memberInfo', res)
uni.setStorageSync('isLogin', true)
if (res.refreshToken) {
setRefreshToken(res.refreshToken)
}
saveLoginInfo(res)
showToast('登录成功')
setTimeout(() => {
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
uni.navigateBack({ delta: 1 })
}, 1500)
} else {
showToast('登录失败,请重试')
showToast('一键登录失败,请使用验证码登录')
switchToSms()
}
} catch (err) {
console.error('一键登录失败:', err)
showToast(err?.message || '登录失败,请重试')
} catch (e) {
console.error('一键登录处理失败:', e)
showToast(e?.message || '一键登录失败,请使用验证码登录')
switchToSms()
} finally {
isLoading.value = false
}
},
fail: (err) => {
uni.closeAuthView()
console.error('一键登录失败:', err)
isLoading.value = false
if (err.code === 30008 || err.errMsg && err.errMsg.includes('其他登录方式')) {
switchToSms()
} else {
showToast('一键登录失败,请使用验证码登录')
switchToSms()
}
}
})
} catch (err) {
console.error('一键登录调用失败:', err)
showToast('一键登录失败,请使用验证码登录')
isLoading.value = false
switchToSms()
}
}
function triggerAgreementShake() {
@@ -302,12 +406,8 @@ async function handleSmsLogin() {
}
if (!validateSmsPhone()) return
if (!code.value) {
showToast('请输入验证码')
return
}
if (!/^\d{6}$/.test(code.value)) {
showToast('请输入6位验证码')
if (code.value.length !== 4) {
showToast('请输入4位验证码')
return
}
@@ -321,8 +421,10 @@ async function handleSmsLogin() {
if (res && res.accessToken) {
setToken(res.accessToken)
uni.setStorageSync('memberInfo', res)
uni.setStorageSync('isLogin', true)
if (res.refreshToken) {
setRefreshToken(res.refreshToken)
}
saveLoginInfo(res)
showToast('登录成功')
setTimeout(() => {
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
@@ -418,12 +520,12 @@ button::after {
.card-container {
position: relative;
width: 100%;
height: 620rpx;
height: 500rpx;
}
.login-card {
position: absolute;
height: 700rpx;
height: 620rpx;
background: $bg-white;
border-radius: $radius-lg;
padding: 48rpx 40rpx;
@@ -431,6 +533,12 @@ button::after {
will-change: transform, z-index, filter;
}
.card-container {
position: relative;
width: 100%;
height: 560rpx;
}
.sms-card {
z-index: 1;
top: -60rpx;
@@ -485,45 +593,45 @@ button::after {
.form-group {
width: 100%;
margin-bottom: 24rpx;
position: relative;
}
.phone-login{
margin-bottom: 150rpx;
}
.input-wrapper {
.phone-form-group {
height: 100rpx;
width: 360rpx;
display: flex;
align-items: center;
background: transparent;
padding: 0 32rpx;
height: 100rpx;
border-bottom: 2rpx solid $border-light;
&.code-input {
padding-right: 16rpx;
}
justify-content: center;
}
.phone-input {
flex: 1;
font-size: 34rpx;
.underline-input {
width: 100%;
height: 100%;
text-align: center;
font-size: 32rpx;
color: $text-dark;
letter-spacing: 2rpx;
background: transparent;
border: none;
border-bottom: 2rpx solid $border-light;
padding: 0 20rpx;
transition: border-color 0.3s ease;
&:focus {
border-bottom-color: $accent-orange;
outline: none;
}
&::placeholder {
color: $text-light;
font-size: 28rpx;
}
}
.send-code-btn {
width: 200rpx;
height: 72rpx;
background: $accent-orange;
color: $text-inverse;
font-size: 26rpx;
border-radius: $radius-sm;
border: none;
&.disabled {
background: $border-light;
color: $text-light;
}
.code-form-group {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
.login-btn {
File diff suppressed because it is too large Load Diff
@@ -89,6 +89,48 @@
<view class="member-page__body">
<view class="member-page__sections">
<!-- 快过期会员卡提醒 -->
<view class="expiring-cards-section" v-if="expiringCards.length > 0">
<view class="expiring-cards__inner">
<view class="expiring-cards__header">
<view class="expiring-cards__header-inner">
<view class="expiring-cards__title-row">
<image class="expiring-cards__warn-icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png" mode="aspectFit" />
<text class="expiring-cards__title">会员卡即将到期</text>
</view>
<view class="expiring-cards__link" hover-class="mi-tap--hover" :hover-stay-time="150" @tap="goMemberCard">
<text class="expiring-cards__link-text">查看全部</text>
<image class="expiring-cards__link-arrow" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/chevronright12.png" mode="aspectFit" />
</view>
</view>
</view>
<view class="expiring-cards__list">
<view
v-for="(card, index) in expiringCards"
:key="card.id || index"
class="expiring-card-item"
hover-class="mi-tap-row--hover"
:hover-stay-time="150"
@tap="goMemberCard"
>
<view class="expiring-card-item__inner">
<view class="expiring-card-item__icon-wrap">
<image class="expiring-card-item__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png" mode="aspectFit" />
</view>
<view class="expiring-card-item__info">
<text class="expiring-card-item__name">{{ card.name }}</text>
<text class="expiring-card-item__validity">{{ card.validity }}</text>
</view>
<view class="expiring-card-item__days">
<text class="expiring-card-item__days-num">{{ computeRemainingDays(card.validityEnd) }}</text>
<text class="expiring-card-item__days-unit">天后到期</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 会员卡区域 -->
<view class="member-card-section">
<view class="member-card-section__inner">
@@ -470,14 +512,18 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
import { loadMemberStore, getCenterPageData, renewMemberCard } from '@/common/memberInfo/store.js'
import { getCenterPageData, renewMemberCard, computeRemainingDays, saveMemberStore } from '@/common/memberInfo/store.js'
import { getMemberId } from '@/utils/request.js'
import { getMyMemberCards } from '@/api/main.js'
import TabBar from '@/components/TabBar.vue'
// 页面数据
const userInfo = ref({})
const stats = ref({})
const cardInfo = ref({})
const expiringCards = ref([])
const bookingPreview = ref([])
const checkIns = ref([])
const bodyReport = ref({})
@@ -579,6 +625,7 @@ function refreshFromStore() {
userInfo.value = pageData.userInfo
stats.value = pageData.stats
cardInfo.value = pageData.cardInfo
expiringCards.value = pageData.expiringCards || []
bookingPreview.value = pageData.bookingPreview
checkIns.value = pageData.checkIns
bodyReport.value = pageData.bodyReport
@@ -586,6 +633,64 @@ function refreshFromStore() {
referral.value = pageData.referral
}
async function refreshCardsFromServer() {
const memberId = getMemberId()
if (!memberId) return
try {
const res = await getMyMemberCards(memberId)
let cards = []
if (Array.isArray(res)) {
cards = res
} else if (res.code === 200 && res.data) {
cards = res.data
} else if (Array.isArray(res.data)) {
cards = res.data
}
if (cards.length > 0) {
const allCards = cards.map(myCard => {
const cardName = myCard.memberCardName || myCard.cardName || myCard.name || `会员卡#${myCard.memberCardId || myCard.id}`
const validityEnd = myCard.validityEnd || myCard.expireTime
const validityStart = myCard.validityStart || myCard.purchaseTime || myCard.createdAt
return {
id: myCard.id,
name: cardName,
status: myCard.status || 'active',
validity: validityEnd ? `有效期至 ${validityEnd}` : '永久有效',
validityEnd: validityEnd,
validityStart: validityStart
}
})
store.cards = allCards
const myCard = cards[0]
const cardName = myCard.memberCardName || myCard.cardName || myCard.name || `会员卡#${myCard.memberCardId || myCard.id}`
const validityEnd = myCard.validityEnd || myCard.expireTime
const validityStart = myCard.validityStart || myCard.purchaseTime || myCard.createdAt
store.card = {
id: myCard.id,
name: cardName,
status: myCard.status || 'active',
validity: validityEnd ? `有效期至 ${validityEnd}` : '永久有效',
validityEnd: validityEnd,
validityStart: validityStart
}
store.cardInfo = {
expireDate: validityEnd ? `有效期至 ${validityEnd}` : '永久有效',
remainingDays: computeRemainingDays(validityEnd)
}
saveMemberStore(store)
refreshFromStore()
}
} catch (e) {
console.error('[MemberCenter] 刷新会员卡数据失败:', e)
}
}
function goUserInfo() {
navigateToPage(PAGE.USER_INFO)
}
@@ -717,6 +822,10 @@ onMounted(() => {
syncNavSafeArea()
refreshFromStore()
})
onShow(() => {
refreshCardsFromServer()
})
</script>
<style lang="scss">
@@ -755,4 +864,126 @@ onMounted(() => {
font-size: 14px;
color: #8A99B4;
}
.expiring-cards-section {
margin: 0 16px 12px;
background: linear-gradient(135deg, #FFF7E6 0%, #FFE4CC 100%);
border-radius: 16px;
overflow: hidden;
}
.expiring-cards__inner {
padding: 16px;
}
.expiring-cards__header {
margin-bottom: 12px;
}
.expiring-cards__header-inner {
display: flex;
align-items: center;
justify-content: space-between;
}
.expiring-cards__title-row {
display: flex;
align-items: center;
gap: 8px;
}
.expiring-cards__warn-icon {
width: 20px;
height: 20px;
}
.expiring-cards__title {
font-size: 16px;
font-weight: 600;
color: #D46B08;
}
.expiring-cards__link {
display: flex;
align-items: center;
gap: 4px;
}
.expiring-cards__link-text {
font-size: 13px;
color: #D46B08;
}
.expiring-cards__link-arrow {
width: 12px;
height: 12px;
}
.expiring-cards__list {
background: rgba(255, 255, 255, 0.6);
border-radius: 12px;
padding: 4px 0;
}
.expiring-card-item {
padding: 12px;
}
.expiring-card-item__inner {
display: flex;
align-items: center;
gap: 12px;
}
.expiring-card-item__icon-wrap {
width: 40px;
height: 40px;
border-radius: 10px;
background: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.expiring-card-item__icon {
width: 24px;
height: 24px;
}
.expiring-card-item__info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.expiring-card-item__name {
font-size: 14px;
font-weight: 600;
color: #1A202C;
}
.expiring-card-item__validity {
font-size: 12px;
color: #718096;
}
.expiring-card-item__days {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
}
.expiring-card-item__days-num {
font-size: 20px;
font-weight: 700;
color: #E74C3C;
}
.expiring-card-item__days-unit {
font-size: 11px;
color: #E74C3C;
}
</style>
@@ -14,6 +14,7 @@
<view class="member-page__body">
<view class="member-page__sections">
<MemberInfoMemberCard
v-if="isLogin"
:card-info="cardInfo"
@view-all="goMemberCard"
@renew="onRenewCard"
@@ -59,12 +60,12 @@ import { ref, onMounted } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { PAGE, navigateToPage } from '@/common/constants/routes.js'
import {
loadMemberStore,
getCenterPageData,
renewMemberCard,
saveMemberStore,
getDefaultStore
} from '@/common/memberInfo/store.js'
import { getMemberId } from '@/utils/request.js'
import { login as miniappLogin, getUserInfo, getMemberDetail, getCheckInStats, updateUserInfo } from '@/api/main.js'
import { setToken, getToken, clearToken } from '@/utils/request.js'
import MemberInfoHeader from '@/components/memberInfo/MemberInfoHeader.vue'
@@ -87,117 +88,79 @@ const loading = ref(false)
const hasActiveCard = ref(false)
const isLogin = ref(false)
// 页面加载时检查token
onMounted(() => {
const token = getToken()
const isLoginStorage = uni.getStorageSync('isLogin')
if (token || isLoginStorage) {
const token = uni.getStorageSync('token')
const loginMember = uni.getStorageSync('loginMemberInfo')
if (token || loginMember) {
isLogin.value = true
}
})
// 处理访客登录点击(跳转到登录页面)
function handleGuestLogin() {
console.log('[memberInfo] 用户点击登录区域')
uni.navigateTo({
url: '/pages/memberInfo/login'
})
}
async function fetchMemberInfo() {
if (loading.value) return
loading.value = true
console.log('[memberInfo] fetchMemberInfo 开始执行')
try {
const res = await getUserInfo({ cache: false })
console.log('[memberInfo] API返回,res =', res)
const apiData = res.data || res
console.log('[memberInfo] apiData =', apiData)
// 先从 loginMemberInfo 获取用户信息
const loginMember = uni.getStorageSync('loginMemberInfo')
if (loginMember?.userInfo) {
console.log('[memberInfo] 从loginMemberInfo获取用户信息:', loginMember.userInfo)
const apiData = loginMember.userInfo
// 更新userInfo
userInfo.value = {
id: apiData.id,
name: apiData.nickname || apiData.name,
phone: apiData.phone,
avatar: apiData.avatar,
memberLevel: apiData.memberLevel || '普通会员'
}
}
if (apiData && (apiData.nickname !== undefined || apiData.phone !== undefined || apiData.id !== undefined)) {
console.log('[memberInfo] 收到有效数据,更新store')
const store = loadMemberStore()
// 更新memberProfile
store.memberProfile = {
...store.memberProfile,
id: apiData.id,
name: apiData.nickname || apiData.name,
phone: apiData.phone,
avatar: apiData.avatar,
memberLevel: apiData.memberLevel || '普通会员'
}
// 更新profile
store.profile = {
...store.profile,
id: apiData.id,
name: apiData.nickname || apiData.name,
phone: apiData.phone,
avatar: apiData.avatar,
gender: apiData.gender,
genderDesc: apiData.genderDesc,
birthday: apiData.birthday,
height: apiData.height,
weight: apiData.weight,
hasPhone: apiData.hasPhone,
isSubscribed: apiData.isSubscribed,
memberLevel: apiData.memberLevel || '普通会员'
}
// 获取会员详细信息(会员卡、积分)
// 获取积分信息
try {
const detailRes = await getMemberDetail()
console.log('[memberInfo] getMemberDetail 返回:', JSON.stringify(detailRes, null, 2))
const detailData = detailRes.data || {}
store.card = detailData.memberCards ? detailData.memberCards[0] || {} : {}
store.cardInfo = {
name: detailData.memberCards?.[0]?.memberCardName || '暂无会员卡',
type: detailData.memberCards?.[0]?.memberCardTypeDesc || '',
remainingTimes: detailData.memberCards?.[0]?.memberCardTotalTimes || 0,
status: detailData.memberCards?.[0]?.memberCardStatusDesc || ''
}
hasActiveCard.value = (detailData.activeCardCount || 0) > 0
store.stats = {
...store.stats,
stats.value = {
...stats.value,
pointsBalance: detailData.pointsBalance || 0
}
} catch (e) {
console.error('[memberInfo] 获取会员详情失败:', e)
console.error('[memberInfo] 获取积分信息失败:', e)
}
// 获取签到统计
try {
const statsRes = await getCheckInStats()
console.log('[memberInfo] getCheckInStats 返回:', JSON.stringify(statsRes, null, 2))
const statsData = statsRes.data || {}
store.stats.checkInCount = statsData.totalCount || 0
stats.value.checkInCount = statsData.totalCount || 0
} catch (e) {
console.error('[memberInfo] 获取签到统计失败:', e)
store.stats.checkInCount = 0
stats.value.checkInCount = 0
}
saveMemberStore(store)
// 刷新store中的业务数据
refreshFromStore()
console.log('[memberInfo] store已更新')
} else {
console.log('[memberInfo] 未登录或数据无效,使用默认空数据')
const store = loadMemberStore()
store.memberProfile = {}
store.profile = {}
store.stats.checkInCount = 0
hasActiveCard.value = false
saveMemberStore(store)
refreshFromStore()
}
console.log('[memberInfo] fetchMemberInfo 执行完成')
} catch (err) {
console.error('[memberInfo] 获取会员信息失败:', err)
// 失败时使用默认空数据
const store = loadMemberStore()
store.memberProfile = {}
store.profile = {}
store.stats.checkInCount = 0
hasActiveCard.value = false
saveMemberStore(store)
refreshFromStore()
} finally {
loading.value = false
console.log('[memberInfo] fetchMemberInfo 执行完成')
}
}
function refreshFromStore() {
const store = loadMemberStore()
const store = getDefaultStore()
const pageData = getCenterPageData(store)
console.log('[memberInfo] refreshFromStore - pageData.userInfo:', JSON.stringify(pageData.userInfo))
userInfo.value = pageData.userInfo
console.log('[memberInfo] refreshFromStore - pageData:', JSON.stringify(pageData))
stats.value = pageData.stats
cardInfo.value = pageData.cardInfo
bookingPreview.value = pageData.bookingPreview
@@ -210,13 +173,6 @@ function goUserInfo() {
navigateToPage(PAGE.USER_INFO)
}
function handleGuestLogin() {
console.log('[memberInfo] 用户点击登录区域')
uni.navigateTo({
url: '/pages/memberInfo/login'
})
}
function goMemberCard() {
navigateToPage(PAGE.MEMBER_CARD)
}
@@ -235,9 +191,6 @@ function onRenewCard() {
content: '确认续费 90 天?',
success: (res) => {
if (!res.confirm) return
const store = loadMemberStore()
renewMemberCard(store, 90)
refreshFromStore()
uni.showToast({ title: '续费成功', icon: 'success' })
}
})
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,405 @@
<template>
<view class="scroll-container theme-light">
<view class="recharge-page">
<MemberInfoSubNav title="储值卡充值" @back="goBack" />
<view class="recharge-page__body">
<!-- 当前余额 -->
<view class="rc-balance-card">
<view class="rc-balance-card__inner">
<text class="rc-balance-card__label">当前余额</text>
<view class="rc-balance-card__amount">
<text class="rc-balance-card__symbol">¥</text>
<text class="rc-balance-card__num">{{ currentBalance }}</text>
</view>
<text class="rc-balance-card__card-name">{{ currentCardName }}</text>
</view>
</view>
<!-- 充值档位 -->
<view class="rc-section">
<text class="rc-section__title">选择充值金额</text>
<view class="rc-grid">
<view
v-for="(item, index) in rechargeOptions"
:key="index"
class="rc-grid-item"
:class="{ 'rc-grid-item--selected': selectedIndex === index }"
@tap="selectRecharge(index)"
>
<view class="rc-grid-item__top">
<text class="rc-grid-item__amount">¥{{ item.amount }}</text>
</view>
<view class="rc-grid-item__bottom">
<text class="rc-grid-item__bonus">¥{{ item.bonus }}</text>
</view>
<view class="rc-grid-item__check" v-if="selectedIndex === index">
<text></text>
</view>
</view>
</view>
</view>
<!-- 充值说明 -->
<view class="rc-rules">
<text class="rc-rules__title">充值说明</text>
<view class="rc-rules__list">
<view class="rc-rules__item">
<text>1. 充值金额实时到账赠送金额同步到账</text>
</view>
<view class="rc-rules__item">
<text>2. 余额可用于购买会员卡消费等</text>
</view>
<view class="rc-rules__item">
<text>3. 充值金额一经到账不予退还</text>
</view>
</view>
</view>
<!-- 底部按钮 -->
<view class="rc-bottom">
<view class="rc-bottom__info">
<text class="rc-bottom__label">实付金额</text>
<view class="rc-bottom__amount">
<text class="rc-bottom__symbol">¥</text>
<text class="rc-bottom__num">{{ selectedAmount }}</text>
</view>
</view>
<view
class="rc-bottom__btn"
:class="{ 'rc-bottom__btn--disabled': loading }"
@tap="handleRecharge"
>
<text>{{ loading ? '处理中...' : '立即充值' }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
import { getMemberId } from '@/utils/request.js'
import { checkPayPasswordSet, getStoredCardInfo } from '@/api/main.js'
import { navigateToPage, PAGE } from '@/common/constants/routes.js'
const currentBalance = ref('0.00')
const selectedIndex = ref(0)
const loading = ref(false)
const rechargeOptions = ref([
{ amount: 100, bonus: 10, payAmount: 100 },
{ amount: 200, bonus: 20, payAmount: 200 },
{ amount: 300, bonus: 30, payAmount: 300 }
])
const selectedAmount = computed(() => {
return rechargeOptions.value[selectedIndex.value]?.payAmount || 0
})
onLoad(() => {
loadBalance()
})
async function loadBalance() {
const memberId = getMemberId()
if (!memberId) return
try {
const res = await getStoredCardInfo(memberId)
const data = res.data || res
currentBalance.value = ((data.balance || 0).toFixed(2))
} catch (e) {
console.error('获取储值卡余额失败:', e)
}
}
function selectRecharge(index) {
selectedIndex.value = index
}
async function handleRecharge() {
if (loading.value) return
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '用户未登录', icon: 'none' })
return
}
// 检查支付密码
uni.showLoading({ title: '检查中...' })
try {
const checkRes = await checkPayPasswordSet(memberId)
uni.hideLoading()
const isSet = checkRes.isSet || checkRes.data?.isSet || false
if (!isSet) {
uni.showModal({
title: '提示',
content: '您尚未设置支付密码,请先设置支付密码后再进行充值',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
navigateToPage(PAGE.SET_PAY_PASSWORD)
}
}
})
return
}
// 确认充值
const option = rechargeOptions.value[selectedIndex.value]
uni.showModal({
title: '确认充值',
content: `确认充值 ${option.amount} 元?\n到账金额:${option.amount + option.bonus}\n(含赠送 ${option.bonus} 元)`,
confirmText: '确认充值',
success: async (res) => {
if (res.confirm) {
await initiatePayment(option)
}
}
})
} catch (e) {
uni.hideLoading()
console.error('检查支付密码失败:', e)
uni.showToast({ title: '检查失败,请稍后重试', icon: 'none' })
}
}
async function initiatePayment(option) {
const amount = option.payAmount
const desc = `储值卡充值:充${option.amount}${option.amount + option.bonus}`
const remark = `rechargeAmount=${option.amount}&bonus=${option.bonus}`
uni.navigateTo({
url: `/pages/memberInfo/confirmPayment?orderType=STORED_CARD_RECHARGE&amount=${amount}&desc=${encodeURIComponent(desc)}&remark=${encodeURIComponent(remark)}&returnPage=memberCard&cardType=stored`,
fail: (err) => {
console.error('跳转支付页面失败:', err)
uni.showToast({ title: '跳转支付页面失败', icon: 'none' })
}
})
}
function goBack() {
uni.navigateBack()
}
</script>
<style lang="scss">
@import '@/common/style/base.css';
@import '@/common/style/memberInfo/pages/page-reset.css';
@import '@/common/style/memberInfo/pages/sub-page-base.css';
.recharge-page {
min-height: 100vh;
background: #F5F7FA;
padding-bottom: 100px;
}
.recharge-page__body {
padding: 16px;
}
.rc-balance-card {
margin-bottom: 20px;
}
.rc-balance-card__inner {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: #FFFFFF;
}
.rc-balance-card__label {
font-size: 13px;
opacity: 0.8;
margin-bottom: 8px;
display: block;
}
.rc-balance-card__amount {
display: flex;
align-items: baseline;
margin-bottom: 8px;
}
.rc-balance-card__symbol {
font-size: 18px;
font-weight: 600;
margin-right: 4px;
}
.rc-balance-card__num {
font-size: 36px;
font-weight: 700;
}
.rc-balance-card__card-name {
font-size: 13px;
opacity: 0.9;
}
.rc-section {
background: #FFFFFF;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
}
.rc-section__title {
font-size: 15px;
font-weight: 600;
color: #1A202C;
margin-bottom: 16px;
display: block;
}
.rc-grid {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.rc-grid-item {
width: calc(33.33% - 8px);
background: #F7FAFC;
border: 2px solid transparent;
border-radius: 12px;
padding: 16px 8px;
text-align: center;
position: relative;
transition: all 0.2s;
&--selected {
background: #F0F4FF;
border-color: #1677FF;
}
}
.rc-grid-item__top {
margin-bottom: 4px;
}
.rc-grid-item__amount {
font-size: 20px;
font-weight: 700;
color: #1A202C;
}
.rc-grid-item__bottom {
margin-bottom: 0;
}
.rc-grid-item__bonus {
font-size: 12px;
color: #E53E3E;
font-weight: 500;
}
.rc-grid-item__check {
position: absolute;
top: -1px;
right: -1px;
width: 20px;
height: 20px;
background: #1677FF;
border-radius: 0 10px 0 10px;
display: flex;
align-items: center;
justify-content: center;
text {
color: #FFFFFF;
font-size: 12px;
font-weight: 700;
}
}
.rc-rules {
background: #FFFFFF;
border-radius: 12px;
padding: 16px;
}
.rc-rules__title {
font-size: 14px;
font-weight: 600;
color: #1A202C;
margin-bottom: 12px;
display: block;
}
.rc-rules__list {
display: flex;
flex-direction: column;
gap: 8px;
}
.rc-rules__item {
font-size: 13px;
color: #718096;
line-height: 1.5;
}
.rc-bottom {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #FFFFFF;
padding: 12px 16px;
padding-bottom: calc(12px + env(safe-area-inset-bottom));
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
}
.rc-bottom__info {
display: flex;
align-items: baseline;
gap: 8px;
}
.rc-bottom__label {
font-size: 13px;
color: #718096;
}
.rc-bottom__amount {
display: flex;
align-items: baseline;
}
.rc-bottom__symbol {
font-size: 14px;
font-weight: 600;
color: #E53E3E;
margin-right: 2px;
}
.rc-bottom__num {
font-size: 24px;
font-weight: 700;
color: #E53E3E;
}
.rc-bottom__btn {
padding: 12px 32px;
background: #1677FF;
border-radius: 24px;
color: #FFFFFF;
font-size: 15px;
font-weight: 600;
&--disabled {
opacity: 0.6;
}
}
</style>
@@ -0,0 +1,217 @@
<template>
<view class="scroll-container theme-light">
<view class="set-pay-password-page">
<MemberInfoSubNav :title="isReset ? '修改支付密码' : '设置支付密码'" @back="goBack" />
<view class="set-pay-password-page__body">
<view class="spp-intro">
<image class="spp-intro__icon" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/lock.png" mode="aspectFit" />
</view>
<VerifyCodeInput
ref="verifyInputRef"
v-model="password"
:title="currentTitle"
:desc="currentDesc"
:length="6"
:mask="true"
type="box"
:errorMsg="errorMsg"
@complete="handleComplete"
/>
<view class="spp-forgot" v-if="isReset && step === 1" @tap="goForgotPassword">
<text>忘记密码</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
import VerifyCodeInput from '@/components/global/VerifyCodeInput.vue'
import { getMemberId } from '@/utils/request.js'
import { setPayPassword, verifyPayPassword, resetPayPassword } from '@/api/main.js'
const password = ref('')
const step = ref(1)
const firstPassword = ref('')
const isReset = ref(false)
const errorMsg = ref('')
const verifyInputRef = ref(null)
const currentTitle = computed(() => {
if (isReset.value && step.value === 1) {
return '请输入原6位支付密码'
}
if (step.value === 1) {
return isReset.value ? '请输入新的6位支付密码' : '请设置6位支付密码'
}
return '请再次输入6位支付密码'
})
const currentDesc = computed(() => {
if (isReset.value && step.value === 1) {
return '验证原密码后可设置新密码'
}
return '支付密码用于充值、消费等资金操作,请妥善保管'
})
onLoad((options) => {
if (options && options.reset === 'true') {
isReset.value = true
}
})
function handleComplete(pwd) {
errorMsg.value = ''
if (isReset.value && step.value === 1) {
verifyOldPassword(pwd)
return
}
if (step.value === 1) {
firstPassword.value = pwd
step.value = 2
password.value = ''
nextTick(() => {
verifyInputRef.value?.focusInput()
})
} else {
if (pwd !== firstPassword.value) {
errorMsg.value = '两次密码输入不一致,请重新输入'
password.value = ''
step.value = 1
firstPassword.value = ''
nextTick(() => {
verifyInputRef.value?.focusInput()
})
return
}
submitPassword(pwd)
}
}
async function verifyOldPassword(pwd) {
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '用户未登录', icon: 'none' })
return
}
uni.showLoading({ title: '验证中...' })
try {
const res = await verifyPayPassword(memberId, pwd)
uni.hideLoading()
if (res && (res.valid === true || res.data?.valid === true)) {
step.value = 2
password.value = ''
firstPassword.value = ''
nextTick(() => {
verifyInputRef.value?.focusInput()
})
} else {
errorMsg.value = '原密码错误,请重新输入'
password.value = ''
nextTick(() => {
verifyInputRef.value?.focusInput()
})
}
} catch (e) {
uni.hideLoading()
errorMsg.value = '验证失败,请稍后重试'
password.value = ''
}
}
async function submitPassword(pwd) {
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '用户未登录', icon: 'none' })
return
}
uni.showLoading({ title: '设置中...' })
try {
let res
if (isReset.value) {
res = await resetPayPassword(memberId, firstPassword.value, pwd)
} else {
res = await setPayPassword(memberId, pwd)
}
uni.hideLoading()
if (res && (res.code === 200 || res.code === 200 || res.message === '设置成功' || res.message === '重置成功')) {
uni.showToast({ title: isReset.value ? '修改成功' : '设置成功', icon: 'success' })
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
uni.showToast({ title: res.message || '设置失败', icon: 'none' })
password.value = ''
step.value = 1
firstPassword.value = ''
}
} catch (e) {
uni.hideLoading()
uni.showToast({ title: '设置失败,请稍后重试', icon: 'none' })
password.value = ''
step.value = 1
firstPassword.value = ''
}
}
function goBack() {
uni.navigateBack()
}
function goForgotPassword() {
uni.showToast({ title: '请联系客服重置密码', icon: 'none' })
}
</script>
<style lang="scss">
@import '@/common/style/base.css';
@import '@/common/style/memberInfo/pages/page-reset.css';
@import '@/common/style/memberInfo/pages/sub-page-base.css';
.set-pay-password-page {
min-height: 100vh;
background: #F5F7FA;
}
.set-pay-password-page__body {
padding: 40px 24px;
}
.spp-intro {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 24px;
}
.spp-intro__icon {
width: 56px;
height: 56px;
}
.spp-forgot {
text-align: right;
margin-top: 8px;
}
.spp-forgot text {
font-size: 13px;
color: #1677FF;
}
</style>
@@ -162,20 +162,6 @@
</view>
</view>
<view class="Pixso-frame-2_858"></view>
<view class="Pixso-frame-2_859">
<view class="frame-content-2_859">
<text class="Pixso-paragraph-2_860">微信</text>
<text class="Pixso-paragraph-2_861">已授权绑定</text>
<view class="stroke-wrapper-2_862">
<view class="Pixso-frame-2_862">
<text class="Pixso-paragraph-2_863">已绑定</text>
</view>
<view class="stroke-2_862"></view>
</view>
</view>
</view>
</view>
</view>
<view class="Pixso-frame-2_864">
<view class="frame-content-2_864">
<text class="Pixso-paragraph-2_865">健身目标</text>
@@ -197,6 +183,9 @@
</view>
</view>
</view>
</view>
</view>
</view>
<view class="user-info-save-bar">
<view
class="user-info-save-bar__btn"
@@ -207,7 +196,6 @@
<text class="user-info-save-bar__text">保存</text>
</view>
</view>
</view>
</template>
<script setup>
@@ -217,6 +205,7 @@ import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
import { getUserInfo, updateUserInfo } from '@/api/main.js'
import { previewImage, persistChosenImage } from '@/common/memberInfo/media.js'
import { maskPhone, normalizePhoneForStore } from '@/common/memberInfo/format.js'
import { getOssUrl, OSS_BASE_URL } from '@/utils/request.js'
import {
validateName,
validatePhoneForRebind,
@@ -229,7 +218,8 @@ import {
} from '@/common/memberInfo/validate.js'
import { backToMemberCenter } from '@/common/constants/routes.js'
const DEFAULT_AVATAR = 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
const DEFAULT_AVATAR = getOssUrl('/static/headerImg/default.png')
const DEFAULT_NAME = '活氧舱用户'
const name = ref('')
const phone = ref('')
@@ -278,7 +268,7 @@ function mapApiToProfile(apiData) {
}
}
return {
name: apiData.nickname || '',
name: apiData.nickname || DEFAULT_NAME,
phone: apiData.phone || '',
gender: genderMap[apiData.gender] || 'female',
birthday: birthdayVal,
@@ -0,0 +1,6 @@
{
"provider": "univerify",
"appid": "__UNI__52E2F0D",
"apiKey": "IKsoDGSM9l8pIws9gXtyp4",
"apiSecret": ""
}
@@ -0,0 +1,59 @@
exports.main = async (event, context) => {
let { access_token, openid, appid } = event
if (event.body) {
try {
const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body
access_token = body.access_token || body.accessToken || access_token
openid = body.openid || openid
appid = body.appid || appid
} catch (e) {
console.error('解析body失败:', e)
}
}
if (!access_token) {
return {
success: false,
message: 'access_token不能为空'
}
}
try {
const params = {
provider: 'univerify',
access_token: access_token,
openid: openid
}
if (appid) {
params.appid = appid
}
console.log('调用getPhoneNumber参数:', JSON.stringify(params))
const res = await uniCloud.getPhoneNumber(params)
console.log('getPhoneNumber返回:', JSON.stringify(res))
if (res && res.phoneNumber) {
return {
success: true,
phone: res.phoneNumber
}
} else {
return {
success: false,
message: '获取手机号失败',
error: res
}
}
} catch (error) {
console.error('获取手机号异常:', error)
return {
success: false,
message: '获取手机号异常',
error: error.message || error
}
}
}
@@ -0,0 +1,12 @@
{
"name": "getPhoneByUniverify",
"version": "1.0.0",
"description": "通过univerify获取手机号",
"main": "index.js",
"extensions": {
"uni-cloud-verify": {}
},
"cloudfunction-config": {
"path": "/getPhoneByUniverify"
}
}
@@ -0,0 +1,12 @@
// 本文件用于,使用JQL语法操作项目关联的uniCloud空间的数据库,方便开发调试和远程数据库管理
// 编写clientDB的js API(也支持常规js语法,比如var),可以对云数据库进行增删改查操作。不支持uniCloud-db组件写法
// 可以全部运行,也可以选中部分代码运行。点击工具栏上的运行按钮或者按下【F5】键运行代码
// 如果文档中存在多条JQL语句,只有最后一条语句生效
// 如果混写了普通js,最后一条语句需是数据库操作语句
// 此处代码运行不受DB Schema的权限控制,移植代码到实际业务中注意在schema中配好permission
// 不支持clientDB的action
// 数据库查询有最大返回条数限制,详见:https://uniapp.dcloud.net.cn/uniCloud/cf-database.html#limit
// 详细JQL语法,请参考:https://uniapp.dcloud.net.cn/uniCloud/jql.html
// 下面示例查询uni-id-users表的所有数据
db.collection('uni-id-users').get();
@@ -0,0 +1,6 @@
## 0.0.32022-11-11
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
## 0.0.22021-04-16
- 修改插件package信息
## 0.0.12021-03-15
- 初始化项目
@@ -0,0 +1,81 @@
{
"id": "uni-config-center",
"displayName": "uni-config-center",
"version": "0.0.3",
"description": "uniCloud 配置中心",
"keywords": [
"配置",
"配置中心"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function"
},
"directories": {
"example": "../../../scripts/dist"
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "u",
"Android Browser": "u",
"微信浏览器(Android)": "u",
"QQ浏览器(Android)": "u"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "u",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
},
"Vue": {
"vue2": "y",
"vue3": "u"
}
}
}
}
}
@@ -0,0 +1,93 @@
# 为什么使用uni-config-center
实际开发中很多插件需要配置文件才可以正常运行,如果每个插件都单独进行配置的话就会产生下面这样的目录结构
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ ├─index.js
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b // plugin-b对应的目录
├─index.js
└─config.json // plugin-b对应的配置文件
```
假设插件作者要发布一个项目模板,里面使用了很多需要配置的插件,无论是作者发布还是用户使用都是一个大麻烦。
uni-config-center就是用了统一管理这些配置文件的,使用uni-config-center后的目录结构如下
```bash
cloudfunctions
└─────common 公共模块
├─plugin-a // 插件A对应的目录
│ └─index.js
├─plugin-b // plugin-b对应的目录
│ └─index.js
└─uni-config-center
├─index.js // config-center入口文件
├─plugin-a
│ ├─config.json // plugin-a对应的配置文件
│ └─other-file.cert // plugin-a依赖的其他文件
└─plugin-b
└─config.json // plugin-b对应的配置文件
```
使用uni-config-center后的优势
- 配置文件统一管理,分离插件主体和配置信息,更新插件更方便
- 支持对config.json设置schema,插件使用者在HBuilderX内编写config.json文件时会有更好的提示(后续HBuilderX会提供支持)
# 用法
在要使用uni-config-center的公共模块或云函数内引入uni-config-center依赖,请参考:[使用公共模块](https://uniapp.dcloud.net.cn/uniCloud/cf-common)
```js
const createConfig = require('uni-config-center')
const uniIdConfig = createConfig({
pluginId: 'uni-id', // 插件id
defaultConfig: { // 默认配置
tokenExpiresIn: 7200,
tokenExpiresThreshold: 600,
},
customMerge: function(defaultConfig, userConfig) { // 自定义默认配置和用户配置的合并规则,不设置的情况侠会对默认配置和用户配置进行深度合并
// defaudltConfig 默认配置
// userConfig 用户配置
return Object.assign(defaultConfig, userConfig)
}
})
// 以如下配置为例
// {
// "tokenExpiresIn": 7200,
// "passwordErrorLimit": 6,
// "bindTokenToDevice": false,
// "passwordErrorRetryTime": 3600,
// "app-plus": {
// "tokenExpiresIn": 2592000
// },
// "service": {
// "sms": {
// "codeExpiresIn": 300
// }
// }
// }
// 获取配置
uniIdConfig.config() // 获取全部配置,注意:uni-config-center内不存在对应插件目录时会返回空对象
uniIdConfig.config('tokenExpiresIn') // 指定键值获取配置,返回:7200
uniIdConfig.config('service.sms.codeExpiresIn') // 指定键值获取配置,返回:300
uniIdConfig.config('tokenExpiresThreshold', 600) // 指定键值获取配置,如果不存在则取传入的默认值,返回:600
// 获取文件绝对路径
uniIdConfig.resolve('custom-token.js') // 获取uni-config-center/uni-id/custom-token.js文件的路径
// 引用文件(require
uniIDConfig.requireFile('custom-token.js') // 使用require方式引用uni-config-center/uni-id/custom-token.js文件。文件不存在时返回undefined,文件内有其他错误导致require失败时会抛出错误。
// 判断是否包含某文件
uniIDConfig.hasFile('custom-token.js') // 配置目录是否包含某文件,true: 文件存在,false: 文件不存在
```
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
{
"name": "uni-config-center",
"version": "0.0.3",
"description": "配置中心",
"main": "index.js",
"keywords": [],
"author": "DCloud",
"license": "Apache-2.0",
"origin-plugin-dev-name": "uni-config-center",
"origin-plugin-version": "0.0.3",
"plugin-dev-name": "uni-config-center",
"plugin-version": "0.0.3"
}
@@ -0,0 +1,38 @@
## 1.0.192025-12-16
- 增加配置参数缺失时的错误提示,指明配置文件路径
## 1.0.182024-07-08
- checkToken时如果传入的token为空则返回uni-id-check-token-failed错误码以便uniIdRouter能正常跳转
## 1.0.172024-04-26
- 兼容uni-app-x对客户端uniPlatform的调整(uni-app-x内uniPlatform区分app-android、app-ios
## 1.0.162023-04-25
- 新增maxTokenLength配置,用于限制数据库用户记录token数组的最大长度
## 1.0.152023-04-06
- 修复部分语言国际化出错的Bug
## 1.0.142023-03-07
- 修复 admin用户包含其他角色时未包含在token的Bug
## 1.0.132022-07-21
- 修复 创建token时未传角色权限信息生成的token不正确的bug
## 1.0.122022-07-15
- 提升与旧版本uni-id的兼容性(补充读取配置文件时回退平台app-plus、h5),但是仍推荐使用新平台名进行配置(app、web)
## 1.0.112022-07-14
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
## 1.0.102022-07-11
- 将token存储在用户表的token字段内,与旧版本uni-id保持一致
## 1.0.92022-07-01
- checkToken兼容token内未缓存角色权限的情况,此时将查库获取角色权限
## 1.0.82022-07-01
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
## 1.0.72022-06-30
- 修复config文件不合法时未抛出具体错误的Bug
## 1.0.62022-06-28
- 移除插件内的数据表schema
## 1.0.52022-06-27
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
## 1.0.42022-06-27
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
## 1.0.22022-06-23
- 对齐旧版本uni-id默认配置
## 1.0.12022-06-22
- 补充对uni-config-center的依赖
## 1.0.02022-06-21
- 提供uni-id token创建、校验、刷新接口,简化旧版uni-id公共模块
@@ -0,0 +1,101 @@
{
"id": "uni-id-common",
"displayName": "uni-id-common",
"version": "1.0.19",
"description": "包含uni-id token生成、校验、刷新功能的云函数公共模块",
"keywords": [
"uni-id-common",
"uniCloud",
"token",
"权限"
],
"repository": "https://gitcode.net/dcloud/uni-id-common",
"engines": {
"uni-app": "^3.1.0",
"uni-app-x": "^3.1.0"
},
"dcloudext": {
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "",
"type": "unicloud-template-function",
"darkmode": "-",
"i18n": "-",
"widescreen": "-"
},
"uni_modules": {
"dependencies": [
"uni-config-center"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "√",
"aliyun": "√",
"alipay": "√"
},
"client": {
"uni-app": {
"vue": {
"vue2": "-",
"vue3": "-"
},
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"vue": "-",
"nvue": "-",
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-",
"alipay": "-",
"toutiao": "-",
"baidu": "-",
"kuaishou": "-",
"jd": "-",
"harmony": "-",
"qq": "-",
"lark": "-"
},
"quickapp": {
"huawei": "-",
"union": "-"
}
},
"uni-app-x": {
"web": {
"safari": "-",
"chrome": "-"
},
"app": {
"android": "-",
"ios": "-",
"harmony": "-"
},
"mp": {
"weixin": "-"
}
}
}
}
}
}
@@ -0,0 +1,3 @@
# uni-id-common
文档请参考:[uni-id-common](https://uniapp.dcloud.net.cn/uniCloud/uni-id-common.html)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
{
"name": "uni-id-common",
"version": "1.0.19",
"description": "uni-id token生成、校验、刷新",
"main": "index.js",
"homepage": "https:\/\/uniapp.dcloud.io\/uniCloud\/uni-id-common.html",
"repository": {
"type": "git",
"url": "git+https:\/\/gitee.com\/dcloud\/uni-id-common.git"
},
"author": "DCloud",
"license": "Apache-2.0",
"dependencies": {
"uni-config-center": "file:..\/..\/..\/..\/..\/uni-config-center\/uniCloud\/cloudfunctions\/common\/uni-config-center"
},
"origin-plugin-dev-name": "uni-id-common",
"origin-plugin-version": "1.0.19",
"plugin-dev-name": "uni-id-common",
"plugin-version": "1.0.19"
}
+33 -6
View File
@@ -1,7 +1,32 @@
const BASE_URL = '/api'
// const BASE_URL = 'http://localhost:8084'
// const BASE_URL = '/api'
const BASE_URL = 'http://192.168.43.89:8084/api'
// 缓存相关常`量
// OSS 静态资源基础地址
export const OSS_BASE_URL = 'https://gymfuture.oss-cn-chengdu.aliyuncs.com'
/**
* 获取 OSS 资源完整地址
* @param {string} path - OSS 资源路径,如 '/static/images/camera.png'
* @returns {string} 完整 OSS 地址
*/
export const getOssUrl = (path) => {
if (!path) return ''
if (path.startsWith('http://') || path.startsWith('https://')) {
return path
}
return OSS_BASE_URL + path
}
/**
* 获取当前登录会员ID
* @returns {number|null} 会员ID,未登录返回null
*/
export const getMemberId = () => {
const loginMember = uni.getStorageSync('loginMemberInfo')
return loginMember?.id || loginMember?.memberId || null
}
// 缓存相关常量
const CACHE_PREFIX = 'API_CACHE_'
const CACHE_EXPIRE_TIME = 5 * 60 * 1000 // 默认缓存时间5分钟
@@ -221,11 +246,11 @@ export const request = (options) => {
console.log(`[API] 响应头:`, res.header)
console.log(`[API] 响应数据:`, JSON.stringify(res.data, null, 2))
if (res.statusCode === 200) {
if (res.statusCode === 200 || res.statusCode === 204) {
if (cache && cacheKey && res.data) {
setCache(cacheKey, res.data, cacheTime)
}
resolve(res.data)
resolve(res.data || {})
} else if (res.statusCode === 401) {
clearToken()
uni.showToast({
@@ -255,7 +280,9 @@ export const requestUtils = {
getCache,
setCache,
clearCache,
clearAllCache
clearAllCache,
OSS_BASE_URL,
getOssUrl
}
// 添加便捷方法