完成一键登录和支付功能
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -12,7 +12,10 @@ export {
|
||||
cancelOngoingBooking,
|
||||
renewMemberCard,
|
||||
parseLocalDate,
|
||||
saveUserProfile
|
||||
saveUserProfile,
|
||||
getCurrentMemberId,
|
||||
getLoginMemberInfo,
|
||||
getToken
|
||||
} from './store.js'
|
||||
export {
|
||||
getLatestBodyTestRecord,
|
||||
|
||||
@@ -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] 余额不使用缓存,无需清除')
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -1,138 +1,271 @@
|
||||
<template>
|
||||
<view class="member-card-section">
|
||||
<view class="member-card-section__inner">
|
||||
<view class="member-card-section__head">
|
||||
<view class="member-card-section__head-inner">
|
||||
<text class="member-card-section__title">
|
||||
我的会员卡
|
||||
</text>
|
||||
<view
|
||||
class="member-card-section__link"
|
||||
hover-class="mi-tap--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="$emit('view-all')"
|
||||
>
|
||||
<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" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="member-card-section__inner">
|
||||
<view class="member-card-section__head">
|
||||
<view class="member-card-section__head-inner">
|
||||
<text class="member-card-section__title">
|
||||
我的会员卡
|
||||
</text>
|
||||
<view
|
||||
class="member-card-preview"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="$emit('view-all')"
|
||||
class="member-card-section__count"
|
||||
>
|
||||
<view class="member-card-preview__inner">
|
||||
<view class="member-card-preview__head">
|
||||
<view class="member-card-preview__head-inner">
|
||||
<view
|
||||
class="member-card-preview__type-row"
|
||||
>
|
||||
<view
|
||||
class="member-card-preview__icon-wrap"
|
||||
>
|
||||
<view
|
||||
class="member-card-preview__icon-border"
|
||||
>
|
||||
<view
|
||||
class="member-card-preview__icon-bg"
|
||||
></view>
|
||||
<view
|
||||
class="member-card-preview__icon-stroke"
|
||||
></view>
|
||||
</view>
|
||||
<image class="member-card-preview__icon-line" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Line_2_468.png" mode="aspectFill" />
|
||||
</view>
|
||||
<text
|
||||
class="member-card-preview__name"
|
||||
>
|
||||
{{ cardInfo.name }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="member-card-preview__tag"
|
||||
>
|
||||
<text class="member-card-preview__tag-text">
|
||||
{{ cardInfo.detailTag || '详情' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="member-card-preview__expire">
|
||||
{{ cardInfo.expireDate }}
|
||||
</text>
|
||||
<view class="member-card-preview__footer">
|
||||
<view class="member-card-preview__footer-inner">
|
||||
<view
|
||||
class="member-card-preview__days"
|
||||
>
|
||||
<text
|
||||
class="member-card-preview__days-num"
|
||||
>
|
||||
{{ cardInfo.remainingDays }}
|
||||
</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"
|
||||
>
|
||||
续费
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="member-card-preview__purchase"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="$emit('purchase')"
|
||||
>
|
||||
<text
|
||||
class="member-card-preview__purchase-text"
|
||||
>
|
||||
购买新卡
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</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>
|
||||
<text class="member-card-section__count-num">{{ cardInfo.activeCount || 0 }}</text>
|
||||
<text class="member-card-section__count-text">张有效</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="member-card-preview"
|
||||
hover-class="mi-tap-card--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="$emit('view-all')"
|
||||
>
|
||||
<view class="member-card-preview__inner">
|
||||
<view class="member-card-preview__head">
|
||||
<view class="member-card-preview__head-inner">
|
||||
<view
|
||||
class="member-card-preview__type-row"
|
||||
>
|
||||
<view
|
||||
class="member-card-preview__icon-wrap"
|
||||
>
|
||||
<view
|
||||
class="member-card-preview__icon-border"
|
||||
>
|
||||
<view
|
||||
class="member-card-preview__icon-bg"
|
||||
></view>
|
||||
<view
|
||||
class="member-card-preview__icon-stroke"
|
||||
></view>
|
||||
</view>
|
||||
<image class="member-card-preview__icon-line" src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/Line_2_468.png" mode="aspectFill" />
|
||||
</view>
|
||||
<text
|
||||
class="member-card-preview__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">
|
||||
{{ getTagText() }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<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()"
|
||||
>
|
||||
{{ getDisplayDays() }}
|
||||
</text>
|
||||
<text
|
||||
class="member-card-preview__days-unit"
|
||||
>
|
||||
{{ getDaysUnit() }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="member-card-preview__purchase"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap.stop="$emit('purchase')"
|
||||
>
|
||||
<text
|
||||
class="member-card-preview__purchase-text"
|
||||
>
|
||||
购买新卡
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</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">
|
||||
|
||||
@@ -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打包配置 */
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 950 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 573 B |
|
Before Width: | Height: | Size: 950 B |
|
Before Width: | Height: | Size: 2.0 KiB |
|
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 view,height不能小于20,width不能小于父视图宽度的一半
|
||||
*/
|
||||
@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的frame,view布局或布局发生变化时调用,不实现则按默认处理 */
|
||||
@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 view,height不能小于50,width不能小于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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
@@ -20,48 +20,42 @@
|
||||
<text class="phone-display">验证码登录</text>
|
||||
<text class="auth-tip">认证服务由中国移动提供</text>
|
||||
|
||||
<view class="form-group">
|
||||
<view class="input-wrapper" :class="{ focused: isSmsPhoneFocused }">
|
||||
<input
|
||||
v-model="smsPhone"
|
||||
type="number"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
class="phone-input"
|
||||
confirm-type="next"
|
||||
@focus="isSmsPhoneFocused = true"
|
||||
@blur="isSmsPhoneFocused = false"
|
||||
/>
|
||||
</view>
|
||||
<view class="form-group phone-form-group">
|
||||
<input
|
||||
v-model="smsPhone"
|
||||
type="number"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
class="underline-input"
|
||||
confirm-type="next"
|
||||
@focus="isSmsPhoneFocused = true"
|
||||
@blur="isSmsPhoneFocused = false"
|
||||
/>
|
||||
</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="{ disabled: countdown > 0 || isLoading }"
|
||||
@tap="handleSendCode"
|
||||
:disabled="countdown > 0 || isLoading"
|
||||
>
|
||||
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<button class="login-btn primary" :class="{ disabled: !canSmsSubmit, loading: isLoading }" @tap="handleSmsLogin" :disabled="!canSmsSubmit || isLoading">
|
||||
<text>{{ isLoading ? '登录中...' : '验证码登录' }}</text>
|
||||
|
||||
<button
|
||||
class="login-btn primary"
|
||||
:class="{ disabled: countdown > 0 || isLoading }"
|
||||
@tap="handleSendCode"
|
||||
:disabled="countdown > 0 || isLoading"
|
||||
>
|
||||
<text>{{ countdown > 0 ? `${countdown}秒后重发` : '发送验证码' }}</text>
|
||||
</button>
|
||||
|
||||
|
||||
<button class="login-btn secondary" @tap="switchToPhone">
|
||||
<text>其他登录方式</text>
|
||||
</button>
|
||||
@@ -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
|
||||
})
|
||||
|
||||
const canSmsSubmit = computed(() => {
|
||||
return /^1[3-9]\d{9}$/.test(smsPhone.value) && /^\d{6}$/.test(code.value) && !isLoading.value
|
||||
})
|
||||
/**
|
||||
* 保存登录信息到本地存储
|
||||
*/
|
||||
function saveLoginInfo(loginInfo) {
|
||||
if (!loginInfo) return
|
||||
|
||||
// 保存 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,34 +209,146 @@ async function handlePhoneLogin() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!validatePhone()) return
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const res = await oneClickLogin({
|
||||
accessToken: phone.value,
|
||||
openid: 'uniapp_phone_login',
|
||||
nickname: '',
|
||||
avatar: ''
|
||||
await new Promise((resolve, reject) => {
|
||||
uni.preLogin({
|
||||
provider: 'univerify',
|
||||
success: resolve,
|
||||
fail: (err) => {
|
||||
console.warn('预登录失败,切换到验证码登录:', err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (res && res.accessToken) {
|
||||
setToken(res.accessToken)
|
||||
uni.setStorageSync('memberInfo', res)
|
||||
uni.setStorageSync('isLogin', true)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.switchTab({ url: '/pages/memberInfo/memberInfo' })
|
||||
}, 1500)
|
||||
} else {
|
||||
showToast('登录失败,请重试')
|
||||
}
|
||||
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: accessToken,
|
||||
openid: openid,
|
||||
nickname: '',
|
||||
avatar: ''
|
||||
})
|
||||
|
||||
if (res && res.accessToken) {
|
||||
setToken(res.accessToken)
|
||||
if (res.refreshToken) {
|
||||
setRefreshToken(res.refreshToken)
|
||||
}
|
||||
saveLoginInfo(res)
|
||||
showToast('登录成功')
|
||||
setTimeout(() => {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
}, 1500)
|
||||
} else {
|
||||
showToast('一键登录失败,请使用验证码登录')
|
||||
switchToSms()
|
||||
}
|
||||
} 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(err?.message || '登录失败,请重试')
|
||||
} finally {
|
||||
console.error('一键登录调用失败:', err)
|
||||
showToast('一键登录失败,请使用验证码登录')
|
||||
isLoading.value = false
|
||||
switchToSms()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,17 +404,13 @@ async function handleSmsLogin() {
|
||||
triggerAgreementShake()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!validateSmsPhone()) return
|
||||
if (!code.value) {
|
||||
showToast('请输入验证码')
|
||||
if (code.value.length !== 4) {
|
||||
showToast('请输入4位验证码')
|
||||
return
|
||||
}
|
||||
if (!/^\d{6}$/.test(code.value)) {
|
||||
showToast('请输入6位验证码')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
@@ -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,13 +533,19 @@ button::after {
|
||||
will-change: transform, z-index, filter;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 560rpx;
|
||||
}
|
||||
|
||||
.sms-card {
|
||||
z-index: 1;
|
||||
top: -60rpx;
|
||||
left: -100rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.12);
|
||||
filter: blur(10rpx);
|
||||
|
||||
|
||||
&.active {
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
@@ -453,7 +561,7 @@ button::after {
|
||||
left: 0;
|
||||
box-shadow: 0 24rpx 60rpx rgba(0, 0, 0, 0.18), 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
filter: blur(0);
|
||||
|
||||
|
||||
&:not(.active) {
|
||||
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 {
|
||||
|
||||
@@ -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"
|
||||
@@ -46,7 +47,7 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
|
||||
<!-- 固定 TabBar -->
|
||||
<view class="tabbar-fixed">
|
||||
<TabBar />
|
||||
@@ -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)
|
||||
|
||||
if (apiData && (apiData.nickname !== undefined || apiData.phone !== undefined || apiData.id !== undefined)) {
|
||||
console.log('[memberInfo] 收到有效数据,更新store')
|
||||
const store = loadMemberStore()
|
||||
// 更新memberProfile
|
||||
store.memberProfile = {
|
||||
...store.memberProfile,
|
||||
// 先从 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 || '普通会员'
|
||||
}
|
||||
// 更新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,
|
||||
pointsBalance: detailData.pointsBalance || 0
|
||||
}
|
||||
} catch (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
|
||||
} catch (e) {
|
||||
console.error('[memberInfo] 获取签到统计失败:', e)
|
||||
store.stats.checkInCount = 0
|
||||
}
|
||||
saveMemberStore(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()
|
||||
}
|
||||
|
||||
// 获取积分信息
|
||||
try {
|
||||
const detailRes = await getMemberDetail()
|
||||
const detailData = detailRes.data || {}
|
||||
stats.value = {
|
||||
...stats.value,
|
||||
pointsBalance: detailData.pointsBalance || 0
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[memberInfo] 获取积分信息失败:', e)
|
||||
}
|
||||
|
||||
// 获取签到统计
|
||||
try {
|
||||
const statsRes = await getCheckInStats()
|
||||
console.log('[memberInfo] getCheckInStats 返回:', JSON.stringify(statsRes, null, 2))
|
||||
const statsData = statsRes.data || {}
|
||||
stats.value.checkInCount = statsData.totalCount || 0
|
||||
} catch (e) {
|
||||
console.error('[memberInfo] 获取签到统计失败:', e)
|
||||
stats.value.checkInCount = 0
|
||||
}
|
||||
|
||||
// 刷新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' })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,50 +162,38 @@
|
||||
</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 class="Pixso-frame-2_864">
|
||||
<view class="frame-content-2_864">
|
||||
<text class="Pixso-paragraph-2_865">健身目标</text>
|
||||
<view class="goal-tags">
|
||||
<view
|
||||
v-for="goal in fitnessGoalOptions"
|
||||
:key="goal"
|
||||
class="goal-tag"
|
||||
:class="{ 'goal-tag--selected': isGoalSelected(goal) }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="toggleGoal(goal)"
|
||||
>
|
||||
<text class="goal-tag__text">{{ goal }}</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>
|
||||
<view class="goal-tags">
|
||||
<view
|
||||
v-for="goal in fitnessGoalOptions"
|
||||
:key="goal"
|
||||
class="goal-tag"
|
||||
:class="{ 'goal-tag--selected': isGoalSelected(goal) }"
|
||||
hover-class="mi-tap-btn--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="toggleGoal(goal)"
|
||||
>
|
||||
<text class="goal-tag__text">{{ goal }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info-save-bar">
|
||||
<view
|
||||
class="user-info-save-bar__btn"
|
||||
hover-class="mi-tap-save--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="handleSave"
|
||||
>
|
||||
<text class="user-info-save-bar__text">保存</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info-save-bar">
|
||||
<view
|
||||
class="user-info-save-bar__btn"
|
||||
hover-class="mi-tap-save--hover"
|
||||
:hover-stay-time="150"
|
||||
@tap="handleSave"
|
||||
>
|
||||
<text class="user-info-save-bar__text">保存</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -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.3(2022-11-11)
|
||||
- 修复 config 方法获取根节点为数组格式配置时错误的转化为了对象的Bug
|
||||
## 0.0.2(2021-04-16)
|
||||
- 修改插件package信息
|
||||
## 0.0.1(2021-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: 文件不存在
|
||||
```
|
||||
@@ -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.19(2025-12-16)
|
||||
- 增加配置参数缺失时的错误提示,指明配置文件路径
|
||||
## 1.0.18(2024-07-08)
|
||||
- checkToken时如果传入的token为空则返回uni-id-check-token-failed错误码以便uniIdRouter能正常跳转
|
||||
## 1.0.17(2024-04-26)
|
||||
- 兼容uni-app-x对客户端uniPlatform的调整(uni-app-x内uniPlatform区分app-android、app-ios)
|
||||
## 1.0.16(2023-04-25)
|
||||
- 新增maxTokenLength配置,用于限制数据库用户记录token数组的最大长度
|
||||
## 1.0.15(2023-04-06)
|
||||
- 修复部分语言国际化出错的Bug
|
||||
## 1.0.14(2023-03-07)
|
||||
- 修复 admin用户包含其他角色时未包含在token的Bug
|
||||
## 1.0.13(2022-07-21)
|
||||
- 修复 创建token时未传角色权限信息生成的token不正确的bug
|
||||
## 1.0.12(2022-07-15)
|
||||
- 提升与旧版本uni-id的兼容性(补充读取配置文件时回退平台app-plus、h5),但是仍推荐使用新平台名进行配置(app、web)
|
||||
## 1.0.11(2022-07-14)
|
||||
- 修复 部分情况下报`read property 'reduce' of undefined`的错误
|
||||
## 1.0.10(2022-07-11)
|
||||
- 将token存储在用户表的token字段内,与旧版本uni-id保持一致
|
||||
## 1.0.9(2022-07-01)
|
||||
- checkToken兼容token内未缓存角色权限的情况,此时将查库获取角色权限
|
||||
## 1.0.8(2022-07-01)
|
||||
- 修复clientDB默认依赖时部分情况下获取不到uni-id配置的Bug
|
||||
## 1.0.7(2022-06-30)
|
||||
- 修复config文件不合法时未抛出具体错误的Bug
|
||||
## 1.0.6(2022-06-28)
|
||||
- 移除插件内的数据表schema
|
||||
## 1.0.5(2022-06-27)
|
||||
- 修复使用多应用配置时报`Cannot read property 'appId' of undefined`的Bug
|
||||
## 1.0.4(2022-06-27)
|
||||
- 修复使用自定义token内容功能报错的Bug [详情](https://ask.dcloud.net.cn/question/147945)
|
||||
## 1.0.2(2022-06-23)
|
||||
- 对齐旧版本uni-id默认配置
|
||||
## 1.0.1(2022-06-22)
|
||||
- 补充对uni-config-center的依赖
|
||||
## 1.0.0(2022-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)
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 添加便捷方法
|
||||
|
||||