diff --git a/me/me.js b/me/me.js index 26cd27f..565ea0c 100644 --- a/me/me.js +++ b/me/me.js @@ -1,5 +1,18 @@ const app = getApp() +function normalizeAvatarUrl(avatarUrl, baseUrl) { + if (!avatarUrl) return '' + return avatarUrl.startsWith('http') ? avatarUrl : `${baseUrl}${avatarUrl}` +} + +function requestFailHint(errMsg) { + const m = String(errMsg || '').toLowerCase() + if (m.includes('domain') || m.includes('合法域名')) return '域名未在公众平台配置,或未开启 HTTPS' + if (m.includes('ssl') || m.includes('certificate')) return 'HTTPS 证书异常,请检查服务器证书' + if (m.includes('timeout') || m.includes('timed out')) return '连接超时,请稍后重试' + return '网络异常,请检查网络后重试' +} + const DEFAULT_AVATAR = 'data:image/svg+xml;utf8,' + encodeURIComponent(` @@ -33,22 +46,52 @@ Page({ this.loadProfile() }, async loadProfile() { - this.setData({ loading: true, error: '' }) - await app.ensureLogin() - if (!app.globalData.openid) { - this.setData({ loading: false, error: '登录失败,请稍后重试' }) + const openid = app.globalData.openid || wx.getStorageSync('openid') || '' + if (!openid) { + this.setData({ + loading: false, + openid: '', + name: '未登录', + nickname: '未登录', + avatarUrl: '', + nicknameDraft: '', + badges: 0, + days: 0, + error: '', + }) return } + this.setData({ loading: true, error: '' }) + app.globalData.openid = openid + const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl + let profileErrMsg = '' wx.request({ - url: `${app.globalData.baseUrl}/api/v1/auth/profile?openid=${encodeURIComponent(app.globalData.openid)}`, + url: `${baseUrl}/api/v1/auth/profile?openid=${encodeURIComponent(openid)}`, method: 'GET', success: (res) => { - const profile = res.data?.data || {} - const avatarUrl = profile.avatar_url || wx.getStorageSync(`avatarUrl:${app.globalData.openid}`) || '' + const body = res.data || {} + const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && body.data + if (!ok) { + profileErrMsg = '个人信息加载失败,请稍后重试' + const storedAvatarUrl = wx.getStorageSync(`avatarUrl:${openid}`) || '' + const normalizedAvatar = normalizeAvatarUrl(storedAvatarUrl, baseUrl) + this.setData({ + openid, + name: '考研战士', + nickname: '考研战士', + avatarUrl: normalizedAvatar, + nicknameDraft: '考研战士', + error: profileErrMsg, + }) + return + } + const profile = body.data + const avatarUrl = wx.getStorageSync(`avatarUrl:${openid}`) || profile.avatar_url || '' const nickname = profile.nickname || '考研战士' - const normalizedAvatar = avatarUrl.startsWith('http') ? avatarUrl : avatarUrl ? `${app.globalData.baseUrl}${avatarUrl}` : '' + const normalizedAvatar = normalizeAvatarUrl(avatarUrl, baseUrl) + profileErrMsg = '' this.setData({ - openid: app.globalData.openid, + openid, name: nickname, nickname, avatarUrl: normalizedAvatar, @@ -58,19 +101,20 @@ Page({ }) }, fail: () => { - const storedAvatarUrl = wx.getStorageSync(`avatarUrl:${app.globalData.openid}`) || '' - const normalizedAvatar = storedAvatarUrl.startsWith('http') ? storedAvatarUrl : storedAvatarUrl ? `${app.globalData.baseUrl}${storedAvatarUrl}` : '' + profileErrMsg = '个人信息加载失败,请稍后重试' + const storedAvatarUrl = wx.getStorageSync(`avatarUrl:${openid}`) || '' + const normalizedAvatar = normalizeAvatarUrl(storedAvatarUrl, baseUrl) this.setData({ - openid: app.globalData.openid, + openid, name: '考研战士', nickname: '考研战士', avatarUrl: normalizedAvatar, nicknameDraft: '考研战士', - error: '个人信息加载失败,请稍后重试', + error: profileErrMsg, }) }, complete: () => { - this.loadAchievements() + this.loadAchievements(profileErrMsg) this.setData({ loading: false }) }, }) @@ -79,7 +123,7 @@ Page({ this.setData({ showNicknameModal: true, nicknameDraft: this.data.nickname || this.data.name || '考研战士' }) }, onAvatarError() { - this.setData({ avatarUrl: '' }) + this.setData({ avatarUrl: this.data.defaultAvatarUrl }) }, closeNicknameModal() { this.setData({ showNicknameModal: false, nicknameDraft: this.data.nickname || this.data.name || '考研战士' }) @@ -95,7 +139,7 @@ Page({ return } wx.request({ - url: `${app.globalData.baseUrl}/api/v1/auth/profile`, + url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/profile`, method: 'PUT', header: { 'Content-Type': 'application/json' }, data: { openid: app.globalData.openid, nickname }, @@ -108,94 +152,111 @@ Page({ fail: () => wx.showToast({ title: '保存失败', icon: 'none' }), }) }, - chooseAvatar() { + onChooseAvatar(e) { + const tempPath = e.detail?.avatarUrl || '' if (!app.globalData.openid) { wx.showToast({ title: '请先登录', icon: 'none' }) return } - wx.chooseImage({ - count: 1, - sizeType: ['compressed'], - sourceType: ['album', 'camera'], - success: (res) => { - const localPath = res.tempFilePaths?.[0] || '' - if (!localPath) return - wx.getImageInfo({ - src: localPath, - success: (info) => { - const size = 320 - const side = Math.min(info.width, info.height) - const x = Math.max(0, Math.floor((info.width - side) / 2)) - const y = Math.max(0, Math.floor((info.height - side) / 2)) - const ctx = wx.createCanvasContext('avatar-canvas', this) - ctx.clearRect(0, 0, size, size) - ctx.save() - ctx.beginPath() - ctx.arc(size / 2, size / 2, size / 2, 0, 2 * Math.PI) - ctx.clip() - ctx.drawImage(localPath, x, y, side, side, 0, 0, size, size) - ctx.draw(false, () => { - wx.canvasToTempFilePath({ - canvasId: 'avatar-canvas', - width: size, - height: size, - destWidth: size, - destHeight: size, - quality: 0.85, - success: (canvasRes) => { - const uploadPath = canvasRes.tempFilePath || localPath - wx.uploadFile({ - url: `${app.globalData.baseUrl}/api/v1/auth/avatar?openid=${encodeURIComponent(app.globalData.openid)}`, - filePath: uploadPath, - name: 'file', - success: (uploadRes) => { - try { - const payload = JSON.parse(uploadRes.data) - const avatarUrl = payload?.data?.avatar_url || '' - if (!avatarUrl) throw new Error('missing avatar_url') - const fullUrl = avatarUrl.startsWith('http') ? avatarUrl : `${app.globalData.baseUrl}${avatarUrl}` - wx.setStorageSync(`avatarUrl:${app.globalData.openid}`, fullUrl) - this.setData({ avatarUrl: fullUrl }) - wx.request({ - url: `${app.globalData.baseUrl}/api/v1/auth/profile?openid=${encodeURIComponent(app.globalData.openid)}`, - method: 'GET', - success: (profileRes) => { - const profile = profileRes.data?.data || {} - const nextNickname = profile.nickname || '考研战士' - this.setData({ name: nextNickname, nickname: nextNickname, avatarUrl: fullUrl }) - }, - }) - wx.showToast({ title: '头像已更新', icon: 'success' }) - } catch (err) { - wx.showToast({ title: '头像更新失败', icon: 'none' }) - } - }, - fail: () => wx.showToast({ title: '头像上传失败', icon: 'none' }), - }) - }, - fail: () => wx.showToast({ title: '头像处理失败', icon: 'none' }), - }, this) - }) - }, - fail: () => wx.showToast({ title: '读取图片失败', icon: 'none' }), - }) + if (!tempPath) { + wx.showToast({ title: '未选择头像', icon: 'none' }) + return + } + const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl + wx.uploadFile({ + url: `${baseUrl}/api/v1/auth/avatar?openid=${encodeURIComponent(app.globalData.openid)}`, + filePath: tempPath, + name: 'file', + success: (uploadRes) => { + try { + const payload = JSON.parse(uploadRes.data || '{}') + const ok = + uploadRes.statusCode >= 200 && + uploadRes.statusCode < 300 && + payload.code === 0 && + payload.data + if (!ok) { + wx.showToast({ title: payload.message || '头像上传失败', icon: 'none' }) + return + } + const rel = payload.data.avatar_url || '' + const full = normalizeAvatarUrl(rel, baseUrl) + wx.setStorageSync(`avatarUrl:${app.globalData.openid}`, full) + this.setData({ avatarUrl: full }) + wx.showToast({ title: '头像已更新', icon: 'success' }) + } catch { + wx.showToast({ title: '头像上传结果异常', icon: 'none' }) + } }, + fail: (err) => wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' }), }) }, - loadAchievements() { + loadAchievements(profileErrMsg) { if (!app.globalData.openid) { this.setData({ error: '登录状态已失效,请重新进入页面' }) return } + const baseUrl = app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl + const achFail = '成就数据加载失败,请稍后重试' wx.request({ - url: `${app.globalData.baseUrl}/api/v1/achievements`, + url: `${baseUrl}/api/v1/achievements`, header: { 'X-OpenID': app.globalData.openid }, success: (res) => { - const list = res.data?.data || [] - const badges = Array.isArray(list) ? list.filter((i) => i.is_unlocked).length : 0 - this.setData({ badges }) + const body = res.data || {} + const ok = res.statusCode >= 200 && res.statusCode < 300 && body.code === 0 && Array.isArray(body.data) + if (!ok) { + this.setData({ error: profileErrMsg ? `${profileErrMsg};${achFail}` : achFail }) + return + } + const list = body.data + const badges = list.filter((i) => i.is_unlocked).length + this.setData({ badges, error: profileErrMsg || '' }) + }, + fail: (err) => { + const netHint = requestFailHint(err.errMsg) + const line = profileErrMsg ? `${profileErrMsg};${achFail}` : achFail + this.setData({ error: `${line}(${netHint})` }) + }, + }) + }, + manualLogin() { + wx.login({ + success: (res) => { + const code = res.code || '' + console.log('[login] wx.login success', res) + console.log('[login] code', code) + if (!code) { + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) + return + } + wx.request({ + url: `${app.getApiBaseUrl ? app.getApiBaseUrl() : app.globalData.baseUrl}/api/v1/auth/wx-login`, + method: 'POST', + header: { 'Content-Type': 'application/json' }, + data: { code }, + success: (loginRes) => { + console.log('[login] wx-login success', loginRes) + const payload = loginRes.data?.data || {} + if (loginRes.statusCode >= 200 && loginRes.statusCode < 300 && loginRes.data?.code === 0 && payload.openid) { + app.globalData.openid = payload.openid + wx.setStorageSync('openid', payload.openid) + wx.showToast({ title: '登录成功', icon: 'success' }) + this.loadProfile() + return + } + console.log('[login] wx-login invalid response', loginRes.data) + wx.showToast({ title: '登录接口异常,请稍后重试', icon: 'none' }) + }, + fail: (err) => { + console.log('[login] wx-login fail', err) + wx.showToast({ title: requestFailHint(err.errMsg), icon: 'none' }) + }, + }) + }, + fail: (err) => { + console.log('[login] wx.login fail', err) + wx.showToast({ title: '登录失败,请重试', icon: 'none' }) }, - fail: () => this.setData({ error: '成就数据加载失败,请稍后重试' }), }) }, goTo(e) { diff --git a/me/me.wxml b/me/me.wxml index ecef783..54209fc 100644 --- a/me/me.wxml +++ b/me/me.wxml @@ -6,20 +6,26 @@ 正在获取个人信息 + + 还未登录 + 请点击下方按钮手动登录后继续使用 + + + {{error}} - + + {{name}} @@ -87,7 +93,7 @@ 修改昵称 × - + diff --git a/me/me.wxss b/me/me.wxss index b596cdf..ed2acd7 100644 --- a/me/me.wxss +++ b/me/me.wxss @@ -1 +1,545 @@ -page{background:#0c0e12;color:#f6f6fc}.page{min-height:100vh;background:#0c0e12;box-sizing:border-box;overflow-x:hidden}.content{height:calc(100vh - 120rpx);padding:96rpx 40rpx 180rpx;box-sizing:border-box}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:80rpx 32rpx 64rpx;text-align:center}.empty-state.small{padding-top:24rpx;padding-bottom:28rpx}.empty-title{display:block;font-size:36rpx;font-weight:800;line-height:1.2;margin-bottom:8rpx;color:#f6f6fc}.empty-subtitle{display:block;font-size:24rpx;line-height:1.6;color:#aaabb0}.error-banner{display:flex;flex-direction:column;align-items:center;gap:20rpx;padding:32rpx 28rpx;border:1rpx solid rgba(255,255,255,.08);border-radius:32rpx;background:rgba(23,26,31,.6);backdrop-filter:blur(24rpx);margin-bottom:32rpx}.error-text{color:#ff6e84;font-size:24rpx;text-align:center}.secondary-btn{border:none;background:#23262c;color:#f6f6fc;border-radius:9999rpx;padding:0 28rpx;line-height:72rpx;font-size:24rpx}.error-btn{min-width:200rpx}.hero-section{display:flex;align-items:center;gap:28rpx;margin-bottom:48rpx}.hero-avatar-wrap{padding:2rpx;border-radius:9999rpx;background:linear-gradient(135deg,#c19cff,#9146ff,#00e3fd);box-shadow:0 10rpx 30rpx rgba(193,156,255,.2)}.hero-avatar-border{position:relative;width:176rpx;height:176rpx;border-radius:9999rpx;overflow:hidden;border:6rpx solid #0c0e12;background:#1d2025}.hero-avatar{width:100%;height:100%}.avatar-mask{position:absolute;left:0;right:0;bottom:0;padding:14rpx 12rpx;background:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,0,0,.72));display:flex;align-items:flex-end;justify-content:center}.avatar-mask-text{font-size:20rpx;color:#fff;font-weight:600;letter-spacing:.04em}.avatar-canvas{position:absolute;left:-9999rpx;top:-9999rpx;width:320rpx;height:320rpx;opacity:0}.hero-info{display:flex;flex-direction:column;justify-content:center}.nickname-input{width:100%;height:72rpx;margin-bottom:12rpx;padding:0 20rpx;border-radius:18rpx;border:1rpx solid rgba(255,255,255,.08);background:#171a1f;color:#f6f6fc;font-size:26rpx;box-sizing:border-box}.hero-name{display:block;font-size:56rpx;font-weight:800;line-height:1.1;margin-bottom:12rpx}.hero-badge{display:inline-flex;align-items:center;gap:12rpx;padding:12rpx 18rpx;border-radius:9999rpx;background:#1d2025;border:1rpx solid rgba(255,255,255,.1);width:fit-content}.pulse-dot{width:16rpx;height:16rpx;border-radius:9999rpx;background:#00e3fd;box-shadow:0 0 16rpx rgba(0,227,253,.8)}.badge-text{font-size:22rpx;color:#aaabb0}.grid-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24rpx;margin-bottom:40rpx}.glass-card{position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:214rpx;padding:28rpx;border:1rpx solid rgba(255,255,255,.08);border-radius:24rpx;background:rgba(29,32,37,.6);backdrop-filter:blur(24rpx);box-shadow:0 20rpx 40rpx rgba(0,0,0,.08)}.card-glow{position:absolute;right:-48rpx;top:-48rpx;width:180rpx;height:180rpx;border-radius:50%;filter:blur(48rpx);opacity:.8}.accent-tertiary .card-glow{background:rgba(255,108,149,.18)}.accent-secondary .card-glow{background:rgba(0,227,253,.18)}.card-icon{position:relative;z-index:1;width:96rpx;height:96rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;margin-bottom:20rpx;background:rgba(255,255,255,.06);border:1rpx solid rgba(255,255,255,.05);font-size:40rpx}.icon-tertiary{color:#ff6c95;box-shadow:0 0 24rpx rgba(255,108,149,.35)}.icon-secondary{color:#00e3fd;box-shadow:0 0 24rpx rgba(0,227,253,.35)}.card-title{position:relative;z-index:1;display:block;font-size:34rpx;line-height:1.2;font-weight:800;color:#f6f6fc;margin-bottom:8rpx}.card-subtitle{position:relative;z-index:1;display:block;font-size:22rpx;line-height:1.5;color:#aaabb0}.divider-row{display:flex;align-items:center;gap:24rpx;margin:8rpx 0 24rpx}.divider{height:2rpx;flex:1;background:linear-gradient(90deg,transparent,rgba(255,255,255,.1),transparent)}.section-kicker{font-size:20rpx;font-weight:700;letter-spacing:.2em;text-transform:uppercase;color:rgba(170,171,176,.6)}.management-grid{margin-bottom:40rpx}.small-action{display:flex;align-items:center;gap:24rpx;padding:24rpx;border:1rpx solid rgba(255,255,255,.05);border-radius:24rpx;background:rgba(17,19,24,.8);backdrop-filter:blur(16rpx)}.small-icon{width:80rpx;height:80rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1rpx solid rgba(255,255,255,.05);font-size:34rpx}.small-icon.primary{background:rgba(193,156,255,.1);color:#c19cff}.small-icon.secondary{background:rgba(0,227,253,.1);color:#00e3fd}.small-icon.tertiary{background:rgba(255,108,149,.1);color:#ff6c95}.small-icon.primary-fixed{background:rgba(182,138,255,.1);color:#b68aff}.small-text{font-size:26rpx;font-weight:600;color:#f6f6fc}.settings-card{display:flex;align-items:center;justify-content:space-between;padding:30rpx 32rpx;border-radius:24rpx;border:1rpx solid rgba(255,255,255,.08);background:rgba(35,38,44,.4);backdrop-filter:blur(24rpx);box-shadow:0 10rpx 30rpx rgba(0,0,0,.1)}.settings-icon{width:96rpx;height:96rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.06);border:1rpx solid rgba(255,255,255,.05);font-size:38rpx;color:#f6f6fc}.settings-text{flex:1;padding-left:20rpx;font-size:28rpx;font-weight:600;letter-spacing:.04em;color:#f6f6fc}.chevron{width:64rpx;height:64rpx;border-radius:9999rpx;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.04);color:#aaabb0;font-size:44rpx}.modal-mask{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;padding:40rpx;background:rgba(0,0,0,.55);backdrop-filter:blur(16rpx)}.modal-card{width:100%;max-width:620rpx;padding:32rpx;border-radius:32rpx;background:#171a1f;border:1rpx solid rgba(255,255,255,.08);box-shadow:0 24rpx 80rpx rgba(0,0,0,.45)}.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:24rpx}.modal-title{font-size:32rpx;font-weight:800;color:#f6f6fc}.modal-close{width:56rpx;height:56rpx;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:40rpx;color:#aaabb0;background:rgba(255,255,255,.04)}.modal-input{width:100%;height:84rpx;box-sizing:border-box;padding:0 24rpx;border-radius:20rpx;border:1rpx solid rgba(255,255,255,.08);background:#0f1216;color:#f6f6fc;font-size:28rpx;margin-bottom:28rpx}.modal-actions{display:flex;gap:20rpx}.modal-btn{flex:1;border:none;border-radius:20rpx;height:80rpx;line-height:80rpx;font-size:26rpx}.modal-btn.ghost{background:#23262c;color:#f6f6fc}.modal-btn.primary{background:linear-gradient(135deg,#c19cff,#9146ff);color:#fff}.bottom-nav{position:fixed;left:0;right:0;bottom:0;z-index:50;display:flex;justify-content:space-around;align-items:center;padding:20rpx 24rpx 40rpx;background:rgba(12,14,18,.7);backdrop-filter:blur(32rpx);border-top:1rpx solid rgba(255,255,255,.08);border-radius:32rpx 32rpx 0 0;box-shadow:0 -10rpx 40rpx rgba(0,0,0,.4)}.nav-item{display:flex;flex-direction:column;align-items:center;gap:6rpx;color:#aaabb0}.nav-item.active{color:#00e3fd;transform:translateY(-6rpx)}.nav-icon{width:56rpx;height:56rpx;border-radius:9999rpx;background:rgba(255,255,255,.05);display:flex;align-items:center;justify-content:center;font-size:34rpx}.nav-label{font-size:20rpx} +page { + background: #0c0e12; + color: #f6f6fc +} + +.page { + min-height: 100vh; + background: #0c0e12; + box-sizing: border-box; + overflow-x: hidden +} + +.content { + height: calc(100vh - 120rpx); + padding: 96rpx 40rpx 180rpx; + box-sizing: border-box +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 80rpx 32rpx 64rpx; + text-align: center +} + +.empty-state.small { + padding-top: 24rpx; + padding-bottom: 28rpx +} + +.empty-title { + display: block; + font-size: 36rpx; + font-weight: 800; + line-height: 1.2; + margin-bottom: 8rpx; + color: #f6f6fc +} + +.empty-subtitle { + display: block; + font-size: 24rpx; + line-height: 1.6; + color: #aaabb0 +} + +.error-banner { + display: flex; + flex-direction: column; + align-items: center; + gap: 20rpx; + padding: 32rpx 28rpx; + border: 1rpx solid rgba(255, 255, 255, .08); + border-radius: 32rpx; + background: rgba(23, 26, 31, .6); + backdrop-filter: blur(24rpx); + margin-bottom: 32rpx +} + +.error-text { + color: #ff6e84; + font-size: 24rpx; + text-align: center +} + +.secondary-btn { + border: none; + background: #23262c; + color: #f6f6fc; + border-radius: 9999rpx; + padding: 0 28rpx; + line-height: 72rpx; + font-size: 24rpx +} + +.error-btn { + min-width: 200rpx +} + +.hero-section { + display: flex; + align-items: center; + gap: 28rpx; + margin-bottom: 48rpx +} + +.hero-avatar-wrap { + padding: 2rpx; + border-radius: 9999rpx; + background: linear-gradient(135deg, #c19cff, #9146ff, #00e3fd); + box-shadow: 0 10rpx 10rpx rgba(193, 156, 255, .2) +} + +.hero-avatar-wrap.avatar-choose-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: auto; + flex-shrink: 0; + box-sizing: border-box +} + +.avatar-choose-btn { + margin: 0; + padding: 2rpx; + line-height: normal; + height: auto; + background: transparent; + border: none; + font-size: inherit +} + +.avatar-choose-btn::after { + border: none +} + +.hero-avatar-border { + position: relative; + width: 176rpx; + height: 176rpx; + border-radius: 9999rpx; + overflow: hidden; + border: 6rpx solid #0c0e12; + background: #1d2025 +} + +.hero-avatar { + width: 100%; + height: 100% +} + +.avatar-mask { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 14rpx 12rpx; + background: linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, .72)); + display: flex; + align-items: flex-end; + justify-content: center +} + +.avatar-mask-text { + font-size: 20rpx; + color: #fff; + font-weight: 600; + letter-spacing: .04em +} + +.avatar-canvas { + position: absolute; + left: -9999rpx; + top: -9999rpx; + width: 320rpx; + height: 320rpx; + opacity: 0 +} + +.hero-info { + display: flex; + flex-direction: column; + justify-content: center +} + +.nickname-input { + width: 100%; + height: 72rpx; + margin-bottom: 12rpx; + padding: 0 20rpx; + border-radius: 18rpx; + border: 1rpx solid rgba(255, 255, 255, .08); + background: #171a1f; + color: #f6f6fc; + font-size: 26rpx; + box-sizing: border-box +} + +.hero-name { + display: block; + font-size: 56rpx; + font-weight: 800; + line-height: 1.1; + margin-bottom: 12rpx +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 12rpx; + padding: 12rpx 18rpx; + border-radius: 9999rpx; + background: #1d2025; + border: 1rpx solid rgba(255, 255, 255, .1); + width: fit-content +} + +.pulse-dot { + width: 16rpx; + height: 16rpx; + border-radius: 9999rpx; + background: #00e3fd; + box-shadow: 0 0 16rpx rgba(0, 227, 253, .8) +} + +.badge-text { + font-size: 22rpx; + color: #aaabb0 +} + +.grid-2 { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 24rpx; + margin-bottom: 40rpx +} + +.glass-card { + position: relative; + overflow: hidden; + display: flex; + flex-direction: column; + min-height: 214rpx; + padding: 28rpx; + border: 1rpx solid rgba(255, 255, 255, .08); + border-radius: 24rpx; + background: rgba(29, 32, 37, .6); + backdrop-filter: blur(24rpx); + box-shadow: 0 20rpx 40rpx rgba(0, 0, 0, .08) +} + +.card-glow { + position: absolute; + right: -48rpx; + top: -48rpx; + width: 180rpx; + height: 180rpx; + border-radius: 50%; + filter: blur(48rpx); + opacity: .8 +} + +.accent-tertiary .card-glow { + background: rgba(255, 108, 149, .18) +} + +.accent-secondary .card-glow { + background: rgba(0, 227, 253, .18) +} + +.card-icon { + position: relative; + z-index: 1; + width: 96rpx; + height: 96rpx; + border-radius: 9999rpx; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20rpx; + background: rgba(255, 255, 255, .06); + border: 1rpx solid rgba(255, 255, 255, .05); + font-size: 40rpx +} + +.icon-tertiary { + color: #ff6c95; + box-shadow: 0 0 24rpx rgba(255, 108, 149, .35) +} + +.icon-secondary { + color: #00e3fd; + box-shadow: 0 0 24rpx rgba(0, 227, 253, .35) +} + +.card-title { + position: relative; + z-index: 1; + display: block; + font-size: 34rpx; + line-height: 1.2; + font-weight: 800; + color: #f6f6fc; + margin-bottom: 8rpx +} + +.card-subtitle { + position: relative; + z-index: 1; + display: block; + font-size: 22rpx; + line-height: 1.5; + color: #aaabb0 +} + +.divider-row { + display: flex; + align-items: center; + gap: 24rpx; + margin: 8rpx 0 24rpx +} + +.divider { + height: 2rpx; + flex: 1; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .1), transparent) +} + +.section-kicker { + font-size: 20rpx; + font-weight: 700; + letter-spacing: .2em; + text-transform: uppercase; + color: rgba(170, 171, 176, .6) +} + +.management-grid { + margin-bottom: 40rpx +} + +.small-action { + display: flex; + align-items: center; + gap: 24rpx; + padding: 24rpx; + border: 1rpx solid rgba(255, 255, 255, .05); + border-radius: 24rpx; + background: rgba(17, 19, 24, .8); + backdrop-filter: blur(16rpx) +} + +.small-icon { + width: 80rpx; + height: 80rpx; + border-radius: 9999rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 1rpx solid rgba(255, 255, 255, .05); + font-size: 34rpx +} + +.small-icon.primary { + background: rgba(193, 156, 255, .1); + color: #c19cff +} + +.small-icon.secondary { + background: rgba(0, 227, 253, .1); + color: #00e3fd +} + +.small-icon.tertiary { + background: rgba(255, 108, 149, .1); + color: #ff6c95 +} + +.small-icon.primary-fixed { + background: rgba(182, 138, 255, .1); + color: #b68aff +} + +.small-text { + font-size: 26rpx; + font-weight: 600; + color: #f6f6fc +} + +.settings-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 30rpx 32rpx; + border-radius: 24rpx; + border: 1rpx solid rgba(255, 255, 255, .08); + background: rgba(35, 38, 44, .4); + backdrop-filter: blur(24rpx); + box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, .1) +} + +.settings-icon { + width: 96rpx; + height: 96rpx; + border-radius: 9999rpx; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, .06); + border: 1rpx solid rgba(255, 255, 255, .05); + font-size: 38rpx; + color: #f6f6fc +} + +.settings-text { + flex: 1; + padding-left: 20rpx; + font-size: 28rpx; + font-weight: 600; + letter-spacing: .04em; + color: #f6f6fc +} + +.chevron { + width: 64rpx; + height: 64rpx; + border-radius: 9999rpx; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, .04); + color: #aaabb0; + font-size: 44rpx +} + +.modal-mask { + position: fixed; + inset: 0; + z-index: 80; + display: flex; + align-items: center; + justify-content: center; + padding: 40rpx; + background: rgba(0, 0, 0, .55); + backdrop-filter: blur(16rpx) +} + +.modal-card { + width: 100%; + max-width: 620rpx; + padding: 32rpx; + border-radius: 32rpx; + background: #171a1f; + border: 1rpx solid rgba(255, 255, 255, .08); + box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, .45) +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24rpx +} + +.modal-title { + font-size: 32rpx; + font-weight: 800; + color: #f6f6fc +} + +.modal-close { + width: 56rpx; + height: 56rpx; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 40rpx; + color: #aaabb0; + background: rgba(255, 255, 255, .04) +} + +.modal-input { + width: 100%; + height: 84rpx; + box-sizing: border-box; + padding: 0 24rpx; + border-radius: 20rpx; + border: 1rpx solid rgba(255, 255, 255, .08); + background: #0f1216; + color: #f6f6fc; + font-size: 28rpx; + margin-bottom: 28rpx +} + +.modal-actions { + display: flex; + gap: 20rpx +} + +.modal-btn { + flex: 1; + border: none; + border-radius: 20rpx; + height: 80rpx; + line-height: 80rpx; + font-size: 26rpx +} + +.modal-btn.ghost { + background: #23262c; + color: #f6f6fc +} + +.modal-btn.primary { + background: linear-gradient(135deg, #c19cff, #9146ff); + color: #fff +} + +.bottom-nav { + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 50; + display: flex; + justify-content: space-around; + align-items: center; + padding: 20rpx 24rpx 40rpx; + background: rgba(12, 14, 18, .7); + backdrop-filter: blur(32rpx); + border-top: 1rpx solid rgba(255, 255, 255, .08); + border-radius: 32rpx 32rpx 0 0; + box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, .4) +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 6rpx; + color: #aaabb0 +} + +.nav-item.active { + color: #00e3fd; + transform: translateY(-6rpx) +} + +.nav-icon { + width: 56rpx; + height: 56rpx; + border-radius: 9999rpx; + background: rgba(255, 255, 255, .05); + display: flex; + align-items: center; + justify-content: center; + font-size: 34rpx +} + +.nav-label { + font-size: 20rpx +} \ No newline at end of file