diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/RbacAuthorizationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/RbacAuthorizationFilter.java index 0c87d9a..b1c326f 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/RbacAuthorizationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/RbacAuthorizationFilter.java @@ -64,7 +64,13 @@ public class RbacAuthorizationFilter extends AbstractGatewayFilterFactory diff --git a/gym-manage-uniapp/pages/index/index.vue b/gym-manage-uniapp/pages/index/index.vue index 490e6b2..6a854c3 100644 --- a/gym-manage-uniapp/pages/index/index.vue +++ b/gym-manage-uniapp/pages/index/index.vue @@ -11,10 +11,9 @@ - + - 你好,{{ memberInfo.nickname }} - {{ memberInfo.memberNo }} + {{ memberInfo.nickname ? '你好,' + memberInfo.nickname : '欢迎您!点击前往更新您的信息。' }} @@ -52,9 +51,7 @@ - - {{ course.recommendTitle }} - + {{ course.courseName }} {{ course.recommendReason }} @@ -74,39 +71,35 @@ diff --git a/gym-manage-uniapp/pages/login/login.vue b/gym-manage-uniapp/pages/login/login.vue index ced1e1a..130eec6 100644 --- a/gym-manage-uniapp/pages/login/login.vue +++ b/gym-manage-uniapp/pages/login/login.vue @@ -48,20 +48,69 @@ diff --git a/gym-manage-uniapp/pages/profile/profile.vue b/gym-manage-uniapp/pages/profile/profile.vue index 5566267..24398ef 100644 --- a/gym-manage-uniapp/pages/profile/profile.vue +++ b/gym-manage-uniapp/pages/profile/profile.vue @@ -11,82 +11,44 @@ - J + {{ avatarText }} - - 会员编号 - {{ userInfo.memberNo }} - 手机号 {{ userInfo.phone }} 性别 - {{ userInfo.gender === 1 ? '男' : userInfo.gender === 2 ? '女' : '未设置' }} + {{ genderLabel }} 生日 {{ userInfo.birthday || '未设置' }} + + 上次登录 + {{ userInfo.lastLoginAt || '暂无' }} + - {{ userInfo.totalCourses }} - 总课时 + {{ userInfo.totalSignInDays }} + 累计签到(天) - {{ userInfo.streakDays }} - 连续打卡 - - - - - {{ userInfo.rating }} - - - 评分 - - - - - - - 我的会员卡 - - - - {{ card.cardName }} - {{ card.typeLabel }} - - 剩余 {{ card.remainingTimes }} - 余额 {{ card.remainingAmount }} - 有效期至 {{ card.expireTime }} - - {{ card.statusLabel }} - - - - - - - - {{ menu.symbol }} - {{ menu.label }} - + {{ userInfo.monthSignInCount }} + 本月签到 @@ -105,6 +67,7 @@ {{ record.typeLabel }} + {{ record.sourceLabel }} {{ record.statusLabel }} @@ -116,16 +79,18 @@ @@ -158,8 +178,7 @@ .big-avatar { width: 56px; height: 56px; background: linear-gradient(135deg, #00E676, #00BFA5); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 24px; color: #1A1A1A; flex-shrink: 0; margin-right: 14px; } .user-info { flex: 1; display: flex; flex-direction: column; } .user-name { font-size: 18px; font-weight: 700; color: #1E1E1E; } - .user-tag { font-size: 13px; color: #00C853; font-weight: 500; display: flex; align-items: center; } - .tag-symbol { font-size: 12px; margin-right: 4px; } + .user-tag { font-size: 13px; color: #00C853; font-weight: 500; } .edit-symbol { font-size: 20px; color: #7A7E84; } .user-details { background: #F5F7FA; border-radius: 12px; padding: 14px 16px; display: flex; flex-direction: column; } .detail-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } @@ -170,31 +189,9 @@ .stat-number { font-size: 24px; font-weight: 700; color: #00E676; } .stat-label { font-size: 12px; color: rgba(255,255,255,0.6); margin-top: 4px; } .stat-divider { width: 1px; background: rgba(255,255,255,0.15); } - .stat-rating-row { display: flex; align-items: center; } - .star-icon { font-size: 14px; color: #00E676; } - .card-section { margin-bottom: 16px; } .section-title-row { display: flex; align-items: center; margin-bottom: 12px; } .section-symbol { font-size: 16px; color: #00E676; margin-right: 8px; } .section-title { font-size: 16px; font-weight: 700; color: #1E1E1E; } - .card-scroll { display: flex; white-space: nowrap; } - .member-card-item { display: inline-block; width: 200px; background: #FFFFFF; border-radius: 20px; padding: 16px; margin-right: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); position: relative; overflow: hidden; } - .member-card-item::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; } - .card-type-time::before { background: #00E676; } - .card-type-stored::before { background: linear-gradient(135deg, #FFA502, #FF6B4A); } - .card-type-count::before { background: #4A90D9; } - .card-name { font-size: 15px; font-weight: 700; color: #1E1E1E; display: block; margin-top: 4px; } - .card-type-label { font-size: 11px; color: #7A7E84; background: #F5F7FA; padding: 2px 8px; border-radius: 10px; margin-top: 4px; display: inline-block; } - .card-details { margin-top: 12px; display: flex; flex-direction: column; } - .card-remain { font-size: 13px; color: #7A7E84; } - .card-remain-num { font-size: 16px; font-weight: 700; color: #00C853; margin: 0 2px; } - .card-expire { font-size: 11px; color: #BDBDBD; } - .card-status { position: absolute; top: 12px; right: 12px; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 20px; } - .card-status.active { background: rgba(0,230,118,0.15); color: #00C853; } - .menu-section { margin-bottom: 16px; } - .menu-grid { display: flex; flex-wrap: wrap; } - .menu-item { width: calc(33.33% - 8px); background: #FFFFFF; border-radius: 12px; padding: 16px 0; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.06); display: flex; flex-direction: column; align-items: center; margin-right: 12px; margin-bottom: 12px; } - .menu-symbol { font-size: 22px; color: #7A7E84; } - .menu-label { font-size: 13px; font-weight: 500; color: #7A7E84; } .sign-section { margin-bottom: 16px; } .sign-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; } .sign-header .section-title-row { margin-bottom: 0; } @@ -204,8 +201,10 @@ .sign-date { font-size: 14px; font-weight: 600; color: #1E1E1E; } .sign-time { font-size: 12px; color: #7A7E84; margin-top: 2px; } .sign-right { display: flex; align-items: center; } - .sign-type { font-size: 12px; color: #7A7E84; margin-right: 10px; } + .sign-type { font-size: 12px; color: #7A7E84; margin-right: 6px; } + .sign-source { font-size: 11px; color: #BDBDBD; background: #F5F7FA; padding: 1px 6px; border-radius: 8px; margin-right: 6px; } .sign-status { font-size: 12px; font-weight: 600; padding: 3px 10px; border-radius: 20px; } .sign-status.success { background: rgba(0,230,118,0.15); color: #00C853; } + .sign-status.fail { background: rgba(255,82,82,0.15); color: #FF5252; } .bottom-safe { height: 50px; } diff --git a/gym-manage-uniapp/pages/search/search.vue b/gym-manage-uniapp/pages/search/search.vue index 63e53c4..0bf0eb2 100644 --- a/gym-manage-uniapp/pages/search/search.vue +++ b/gym-manage-uniapp/pages/search/search.vue @@ -24,15 +24,11 @@ - - 时段 - - - {{ period.symbol }} - {{ period.label }} - - + + + {{ period.label }} + @@ -58,34 +54,48 @@ - - - {{ course.typeChar }} - - - {{ course.courseName }} - - 教练 {{ course.coachName }} - {{ course.difficultyLabel }} - - - 日期 {{ course.startTime }} - - - {{ course.location }} - {{ course.currentMembers }}/{{ course.maxMembers }}人 - - - - {{ course.storedValueAmount > 0 ? course.storedValueAmount + '元/次' : '免费' }} - - + + + + + + {{ course.typeName.slice(0, 2) }} + + + + {{ course.courseName }} + + {{ course.category }} + {{ course.difficultyLabel }} + + + + {{ course.shortDate }} + {{ course.shortTime }} - {{ course.endShortTime }} + {{ course.duration }} + + + {{ course.location }} + + + 已预约 {{ course.currentMembers }}/{{ course.maxMembers }}人 + + + {{ label.labelName }} + + + {{ course.storedValueAmount > 0 ? '¥' + course.storedValueAmount + '/次' : '免费' }} + + + + + - @@ -99,40 +109,35 @@ @@ -184,7 +269,7 @@ .nav-bar { background: #1A1A1A; padding: 0 20px; display: flex; align-items: center; } .nav-title { font-size: 18px; font-weight: 700; color: #FFFFFF; } - .search-area { background: #1A1A1A; padding: 0 16px 20px; } + .search-area { background: #1A1A1A; padding: 18px 16px 20px; } .search-box { background: rgba(255,255,255,0.12); border-radius: 40px; padding: 0 16px; display: flex; align-items: center; height: 42px; margin-bottom: 14px; } .search-symbol { font-size: 15px; color: rgba(255,255,255,0.5); flex-shrink: 0; margin-right: 8px; } .search-input { flex: 1; font-size: 14px; color: #FFFFFF; height: 100%; } @@ -197,12 +282,9 @@ .range-value.placeholder { color: rgba(255,255,255,0.35); font-weight: 400; } .range-sep { color: rgba(255,255,255,0.5); font-size: 14px; flex-shrink: 0; margin-right: 10px; } - .period-selector { display: flex; align-items: center; } - .period-label { font-size: 13px; color: rgba(255,255,255,0.6); flex-shrink: 0; margin-right: 10px; } - .period-scroll { flex: 1; display: flex; white-space: nowrap; } - .period-item { display: inline-flex; align-items: center; padding: 6px 14px; border-radius: 40px; font-size: 13px; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.08); margin-right: 8px; } + .period-row { display: flex; } + .period-item { flex: 1; text-align: center; padding: 7px 0; border-radius: 40px; font-size: 13px; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.08); margin: 0 3px; } .period-item.active { background: #00E676; color: #1A1A1A; font-weight: 600; } - .period-symbol { font-size: 13px; margin-right: 4px; } .filter-tabs { background: #FFFFFF; display: flex; white-space: nowrap; border-bottom: 1px solid #EEEEEE; } .filter-tabs-inner { padding: 14px 16px; display: flex; white-space: nowrap; } @@ -216,21 +298,35 @@ .sort-item.active { color: #00C853; font-weight: 600; } .course-list-inner { padding: 12px 16px 0; } - .course-card { background: #FFFFFF; border-radius: 20px; padding: 14px; margin-bottom: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); display: flex; } - .course-cover { width: 72px; height: 72px; border-radius: 12px; background: rgba(0,230,118,0.15); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 14px; } - .cover-text { font-size: 24px; font-weight: 700; color: #00C853; } - .course-body { flex: 1; display: flex; flex-direction: column; } - .course-name { font-size: 16px; font-weight: 700; color: #1E1E1E; } - .course-row { display: flex; align-items: center; font-size: 12px; color: #7A7E84; } - .course-row > * { margin-right: 12px; } + .course-item { margin-bottom: 12px; border-radius: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); overflow: hidden; } + .course-card { background: #FFFFFF; border-radius: 20px; padding: 14px; display: flex; } + .course-cover { width: 72px; height: 72px; border-radius: 12px; background: rgba(0,230,118,0.12); display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 14px; overflow: hidden; position: relative; } + .cover-skeleton { position: absolute; inset: 0; background: linear-gradient(90deg, #E8E8E8 25%, #F0F0F0 50%, #E8E8E8 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; z-index: 1; } + @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } + .cover-img { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; transition: opacity 0.3s; z-index: 0; } + .cover-img.loaded { opacity: 1; z-index: 2; } + .cover-text { font-size: 22px; font-weight: 700; color: #00C853; } + .course-body { flex: 1; display: flex; flex-direction: column; min-width: 0; } + .course-top { display: flex; justify-content: space-between; align-items: flex-start; } + .course-name { font-size: 16px; font-weight: 700; color: #1E1E1E; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .course-tags { display: flex; flex-shrink: 0; margin-left: 6px; } + .course-category { font-size: 11px; color: #00C853; background: rgba(0,230,118,0.12); padding: 1px 8px; border-radius: 20px; font-weight: 500; margin-right: 4px; } .course-difficulty { padding: 1px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; } .diff-low { background: rgba(0,230,118,0.15); color: #00C853; } .diff-mid { background: rgba(255,165,2,0.15); color: #FFA502; } .diff-high { background: rgba(255,82,82,0.15); color: #FF5252; } - .course-bottom { display: flex; justify-content: space-between; align-items: center; margin-top: 4px; } - .course-price { font-size: 13px; font-weight: 600; color: #00C853; } + .course-meta { display: flex; align-items: center; font-size: 12px; color: #7A7E84; margin-top: 4px; } + .course-date { flex-shrink: 0; } + .course-time { margin-left: 8px; } + .course-duration { margin-left: 8px; color: #00C853; font-weight: 500; } + .course-location { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .course-capacity { color: #7A7E84; } + .course-labels { display: flex; flex-wrap: wrap; margin-top: 6px; } + .course-label { font-size: 10px; padding: 1px 6px; border-radius: 10px; margin-right: 4px; margin-bottom: 2px; } + .course-bottom { text-align: right; margin-top: 6px; } + .course-price { font-size: 18px; font-weight: 700; color: #00C853; display: block; margin-top: 6px; } .course-price.free { color: #00C853; } - .btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 6px 16px; border-radius: 40px; font-weight: 700; font-size: 13px; line-height: 1.4; box-shadow: 0 2px 6px rgba(0,230,118,0.3); } + .btn-book { background: #00E676; color: #1A1A1A; border: none; padding: 6px 16px; border-radius: 40px; font-weight: 700; font-size: 13px; line-height: 1.4; box-shadow: 0 2px 8px rgba(0,230,118,0.3); } .btn-book::after { border: none; } .btn-book.disabled { background: #E0E0E0; color: #BDBDBD; box-shadow: none; } diff --git a/gym-manage-uniapp/store/index.js b/gym-manage-uniapp/store/index.js new file mode 100644 index 0000000..d27b3a0 --- /dev/null +++ b/gym-manage-uniapp/store/index.js @@ -0,0 +1,65 @@ +/** + * 全局状态管理 — 以 uni.Storage 为唯一数据源,确保跨页面/跨模块共享 + */ +const TOKEN_KEY = 'gym_token' +const MEMBER_KEY = 'gym_member' + +const store = { + /** 是否已登录 */ + get isLoggedIn() { + try { + return !!uni.getStorageSync(TOKEN_KEY) + } catch (e) { + return false + } + }, + + /** 获取 token */ + getToken() { + try { + return uni.getStorageSync(TOKEN_KEY) || null + } catch (e) { + return null + } + }, + + /** 获取会员信息 */ + getMemberInfo() { + try { + return uni.getStorageSync(MEMBER_KEY) || null + } catch (e) { + return null + } + }, + + /** 设置登录态 */ + setLogin(token, memberInfo) { + try { + uni.setStorageSync(TOKEN_KEY, token) + if (memberInfo) uni.setStorageSync(MEMBER_KEY, memberInfo) + } catch (e) { + console.error('存储登录态失败:', e) + } + }, + + /** 更新会员信息 */ + updateMemberInfo(memberInfo) { + try { + uni.setStorageSync(MEMBER_KEY, memberInfo) + } catch (e) { + console.error('更新会员信息失败:', e) + } + }, + + /** 清除登录态 */ + clearLogin() { + try { + uni.removeStorageSync(TOKEN_KEY) + uni.removeStorageSync(MEMBER_KEY) + } catch (e) { + console.error('清除登录态失败:', e) + } + } +} + +module.exports = store diff --git a/gym-manage-uniapp/utils/request.js b/gym-manage-uniapp/utils/request.js new file mode 100644 index 0000000..bf26275 --- /dev/null +++ b/gym-manage-uniapp/utils/request.js @@ -0,0 +1,63 @@ +const luchRequest = require('luch-request') +const Request = luchRequest.default || luchRequest +const { generateSignatureHeaders } = require('./signature') +const store = require('../store/index') + +const BASE_URL = 'http://localhost:8084/api' + +const http = new Request({ + baseURL: BASE_URL, + timeout: 15000, + header: { + 'Content-Type': 'application/json' + } +}) + +// 请求拦截器 +http.interceptors.request.use( + (config) => { + // 注入 JWT token + const token = store.getToken() + if (token) { + config.header = config.header || {} + config.header.Authorization = 'Bearer ' + token + } + + // 注入签名头 + const method = (config.method || 'GET').toUpperCase() + const url = config.url || '' + const body = config.data + + const signatureHeaders = generateSignatureHeaders(method, url, body) + config.header = config.header || {} + Object.assign(config.header, signatureHeaders) + + return config + }, + (error) => Promise.reject(error) +) + +// 响应拦截器 +http.interceptors.response.use( + (response) => { + // 直接返回 data,统一解包 + if (response.statusCode === 200) { + return response.data + } + return response + }, + (error) => { + // 401 处理:清除登录态,跳转登录页 + if (error.statusCode === 401) { + store.clearLogin() + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + if (currentPage && currentPage.route !== 'pages/login/login') { + uni.reLaunch({ url: '/pages/login/login' }) + } + } + return Promise.reject(error) + } +) + +module.exports = http diff --git a/gym-manage-uniapp/utils/signature.js b/gym-manage-uniapp/utils/signature.js new file mode 100644 index 0000000..f7d4e2d --- /dev/null +++ b/gym-manage-uniapp/utils/signature.js @@ -0,0 +1,60 @@ +const CryptoJS = require('crypto-js') + +const SIGNATURE_SECRET = 'NovalonManageSystemSecretKey2026' + +/** + * 生成 HMAC-SHA256 签名 + */ +function generateSignature(method, path, query, body, timestamp, nonce) { + const stringToSign = [method, path, query || '', body || '', String(timestamp), nonce].join('\n') + const signature = CryptoJS.HmacSHA256(stringToSign, SIGNATURE_SECRET) + return CryptoJS.enc.Base64.stringify(signature) +} + +/** + * 生成随机 nonce + */ +function generateNonce() { + const timestamp = Date.now().toString(36) + const randomPart = Math.random().toString(36).substring(2, 15) + return timestamp + '-' + randomPart +} + +/** + * 从 URL 解析 path 和 query + */ +function parseUrl(url) { + if (url.startsWith('http://') || url.startsWith('https://')) { + // 小程序环境无 URL 构造函数,手动解析 + const withoutProtocol = url.substring(url.indexOf('://') + 3) + const pathStart = withoutProtocol.indexOf('/') + if (pathStart === -1) return { path: '/', query: '' } + const pathAndQuery = withoutProtocol.substring(pathStart) + const queryIndex = pathAndQuery.indexOf('?') + if (queryIndex === -1) return { path: pathAndQuery, query: '' } + return { path: pathAndQuery.substring(0, queryIndex), query: pathAndQuery.substring(queryIndex + 1) } + } + const queryIndex = url.indexOf('?') + if (queryIndex === -1) return { path: url, query: '' } + return { path: url.substring(0, queryIndex), query: url.substring(queryIndex + 1) } +} + +/** + * 生成签名请求头 + */ +function generateSignatureHeaders(method, url, body) { + const timestamp = Date.now() + const nonce = generateNonce() + const { path, query } = parseUrl(url) + const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '' + + const signature = generateSignature(method.toUpperCase(), path, query, bodyStr, timestamp, nonce) + + return { + 'X-Signature': signature, + 'X-Timestamp': String(timestamp), + 'X-Nonce': nonce + } +} + +module.exports = { generateSignatureHeaders }