feat: 样式修改

This commit is contained in:
Blizzard
2026-02-10 17:22:53 +08:00
parent 6f88bc656b
commit e97fd30fa3
28 changed files with 2097 additions and 903 deletions
+102
View File
@@ -0,0 +1,102 @@
import request from '../../../utils/request';
Page({
data: {
records: [],
loading: true,
page: 1,
pageSize: 20,
total: 0,
hasMore: true,
expandedId: '', // Track which card is expanded
},
onLoad() {
this.loadRecords();
},
async loadRecords() {
this.setData({ loading: true });
try {
const res = await request.post('/classify/myClassifyLog', {
page: this.data.page,
pageSize: this.data.pageSize,
});
const list = (res.list || []).map(item => this._transformRecord(item));
this.setData({
records: this.data.page === 1 ? list : [...this.data.records, ...list],
total: res.total || 0,
hasMore: list.length >= this.data.pageSize,
loading: false,
});
} catch (err) {
console.error('Load identify history failed', err);
this.setData({ loading: false });
wx.showToast({ title: '加载失败', icon: 'none' });
}
},
_transformRecord(item) {
const allResults = item.allResults || [];
const topResult = allResults[0] || {};
const otherResults = allResults.slice(1);
return {
id: item.id,
time: this._formatTime(item.createdAt),
dateStr: item.createdAtStr || '',
topName: topResult.name || '未知植物',
topScore: topResult.score ? Math.round(topResult.score * 100) : 0,
topImage: topResult.baike_info?.image_url || '',
topDesc: topResult.baike_info?.description || '',
topBaikeUrl: topResult.baike_info?.baike_url || '',
otherResults: otherResults.map(r => ({
name: r.name || '未知',
score: r.score ? Math.round(r.score * 100) : 0,
hasInfo: !!r.baike_info,
})),
};
},
toggleExpand(e) {
const id = e.currentTarget.dataset.id;
this.setData({
expandedId: this.data.expandedId === id ? '' : id,
});
},
onReachBottom() {
if (!this.data.hasMore || this.data.loading) return;
this.setData({ page: this.data.page + 1 }, () => {
this.loadRecords();
});
},
onPullDownRefresh() {
this.setData({ page: 1, hasMore: true }, () => {
this.loadRecords().then(() => {
wx.stopPullDownRefresh();
});
});
},
_formatTime(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diffMs = now - d;
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return '刚刚';
if (diffMin < 60) return diffMin + '分钟前';
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) return diffHour + '小时前';
const diffDay = Math.floor(diffHour / 24);
if (diffDay < 7) return diffDay + '天前';
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
const hour = d.getHours().toString().padStart(2, '0');
const min = d.getMinutes().toString().padStart(2, '0');
return `${month}-${day} ${hour}:${min}`;
},
});
@@ -0,0 +1,9 @@
{
"navigationBarTitleText": "识别记录",
"usingComponents": {
"t-icon": "tdesign-miniprogram/icon/icon",
"t-image": "tdesign-miniprogram/image/image",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-loading": "tdesign-miniprogram/loading/loading"
}
}
+103
View File
@@ -0,0 +1,103 @@
<view class="history-page">
<!-- Loading -->
<view wx:if="{{loading && records.length === 0}}" class="loading-wrap">
<t-loading theme="circular" size="48rpx" text="加载中..." />
</view>
<!-- Empty -->
<view wx:elif="{{!loading && records.length === 0}}" class="empty-wrap">
<view class="empty-icon">
<t-icon name="scan" size="120rpx" color="#D1D5DB" />
</view>
<text class="empty-title">暂无识别记录</text>
<text class="empty-hint">去百科页面拍照识别植物吧</text>
</view>
<!-- Record List -->
<view wx:else class="record-list">
<view wx:for="{{records}}" wx:key="id" class="record-card" bindtap="toggleExpand" data-id="{{item.id}}">
<!-- Card Header -->
<view class="card-header">
<view class="card-thumb">
<t-image
wx:if="{{item.topImage}}"
src="{{item.topImage}}"
mode="aspectFill"
width="120rpx"
height="120rpx"
shape="round"
/>
<view wx:else class="thumb-placeholder">
<t-icon name="image" size="48rpx" color="#D1D5DB" />
</view>
</view>
<view class="card-info">
<view class="card-name-row">
<text class="card-name">{{item.topName}}</text>
<view class="score-badge" style="background: {{item.topScore >= 80 ? '#E8F5E9' : item.topScore >= 50 ? '#FFF8E1' : '#FBE9E7'}}; color: {{item.topScore >= 80 ? '#2E7D32' : item.topScore >= 50 ? '#F57F17' : '#D84315'}}">
{{item.topScore}}%
</view>
</view>
<text class="card-time">{{item.time}}</text>
<view wx:if="{{item.otherResults.length > 0}}" class="other-hint">
<text>还可能是: </text>
<text wx:for="{{item.otherResults}}" wx:for-item="other" wx:key="name" class="other-name">{{other.name}}{{index < item.otherResults.length - 1 ? '、' : ''}}</text>
</view>
</view>
<view class="expand-arrow {{expandedId === item.id ? 'expanded' : ''}}">
<t-icon name="chevron-down" size="32rpx" color="#C5C5C5" />
</view>
</view>
<!-- Expanded Detail -->
<view wx:if="{{expandedId === item.id}}" class="card-detail">
<!-- Description -->
<view wx:if="{{item.topDesc}}" class="detail-desc">
<text class="desc-text">{{item.topDesc}}</text>
</view>
<!-- All Results -->
<view class="detail-results">
<text class="detail-label">识别结果排名</text>
<view class="result-bars">
<!-- Top result -->
<view class="result-bar-item">
<text class="bar-name">{{item.topName}}</text>
<view class="bar-track">
<view class="bar-fill top" style="width: {{item.topScore}}%"></view>
</view>
<text class="bar-score">{{item.topScore}}%</text>
</view>
<!-- Other results -->
<view wx:for="{{item.otherResults}}" wx:for-item="other" wx:key="name" class="result-bar-item">
<text class="bar-name">{{other.name}}</text>
<view class="bar-track">
<view class="bar-fill" style="width: {{other.score}}%"></view>
</view>
<text class="bar-score">{{other.score}}%</text>
</view>
</view>
</view>
<view class="detail-meta">
<t-icon name="time" size="24rpx" color="#9CA3AF" />
<text class="meta-text">{{item.dateStr}}</text>
</view>
</view>
</view>
<!-- Load More -->
<view wx:if="{{loading && records.length > 0}}" class="load-more">
<t-loading theme="circular" size="36rpx" text="加载更多..." />
</view>
<view wx:elif="{{!hasMore && records.length > 0}}" class="no-more">
<text>— 没有更多了 —</text>
</view>
</view>
</view>
+278
View File
@@ -0,0 +1,278 @@
/* pages/profile/identify-history/index.wxss */
.history-page {
min-height: 100vh;
background: #F4F6F0;
padding: 24rpx;
box-sizing: border-box;
}
/* Loading & Empty */
.loading-wrap {
display: flex;
justify-content: center;
align-items: center;
padding-top: 200rpx;
}
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
}
.empty-icon {
width: 200rpx;
height: 200rpx;
background: #F3F4F6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.empty-title {
font-size: 32rpx;
font-weight: 700;
color: #6B7280;
margin-bottom: 12rpx;
}
.empty-hint {
font-size: 26rpx;
color: #9CA3AF;
}
/* Record List */
.record-list {
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 60rpx;
}
.record-card {
background: #fff;
border-radius: 28rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
transition: box-shadow 0.2s;
}
.record-card:active {
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
/* Card Header */
.card-header {
display: flex;
align-items: center;
gap: 24rpx;
}
.card-thumb {
flex-shrink: 0;
width: 120rpx;
height: 120rpx;
border-radius: 20rpx;
overflow: hidden;
}
.thumb-placeholder {
width: 100%;
height: 100%;
background: #F3F4F6;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.card-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.card-name-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.card-name {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.score-badge {
flex-shrink: 0;
font-size: 22rpx;
font-weight: 700;
padding: 4rpx 14rpx;
border-radius: 12rpx;
}
.card-time {
font-size: 24rpx;
color: #9CA3AF;
}
.other-hint {
font-size: 22rpx;
color: #9CA3AF;
display: flex;
flex-wrap: wrap;
gap: 2rpx;
margin-top: 4rpx;
}
.other-name {
color: #6B7280;
}
.expand-arrow {
flex-shrink: 0;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
padding: 8rpx;
}
.expand-arrow.expanded {
transform: rotate(180deg);
}
/* Card Detail (Expanded) */
.card-detail {
margin-top: 28rpx;
padding-top: 28rpx;
border-top: 2rpx solid #F3F4F6;
animation: fadeSlideDown 0.3s ease;
}
@keyframes fadeSlideDown {
from {
opacity: 0;
transform: translateY(-16rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Description */
.detail-desc {
background: #F4F6F0;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 28rpx;
}
.desc-text {
font-size: 26rpx;
line-height: 1.7;
color: #4B5563;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Result Bars */
.detail-results {
margin-bottom: 24rpx;
}
.detail-label {
font-size: 24rpx;
font-weight: 600;
color: #6B7280;
margin-bottom: 20rpx;
display: block;
}
.result-bars {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.result-bar-item {
display: flex;
align-items: center;
gap: 16rpx;
}
.bar-name {
width: 120rpx;
font-size: 26rpx;
font-weight: 600;
color: #374151;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.bar-track {
flex: 1;
height: 16rpx;
background: #F3F4F6;
border-radius: 8rpx;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 8rpx;
background: linear-gradient(90deg, #A5D6A7, #66BB6A);
transition: width 0.5s ease;
}
.bar-fill.top {
background: linear-gradient(90deg, #66BB6A, #388E3C);
}
.bar-score {
width: 80rpx;
font-size: 24rpx;
font-weight: 600;
color: #6B7280;
text-align: right;
flex-shrink: 0;
}
/* Meta */
.detail-meta {
display: flex;
align-items: center;
gap: 8rpx;
padding-top: 16rpx;
}
.meta-text {
font-size: 22rpx;
color: #9CA3AF;
}
/* Load More */
.load-more {
display: flex;
justify-content: center;
padding: 32rpx 0;
}
.no-more {
text-align: center;
padding: 32rpx 0;
font-size: 24rpx;
color: #D1D5DB;
}
+294 -78
View File
@@ -1,52 +1,163 @@
// pages/profile/index.js
import { MOCK_FAVORITES, MOCK_BADGES, MOCK_POSTS } from '../../utils/mockData';
import request from '../../utils/request';
const app = getApp();
Page({
data: {
view: 'profile', // profile, favorites, posts, badges
favTab: 'all', // all, plant, article
postsTab: 'published', // published, drafts
view: 'profile', // profile, favorites, posts, about
// User Info
userName: '植物爱好者',
userAvatar: '',
userLevel: '', // Reserved for future level system
userLevelTag: '', // e.g. 'Lv.4 资深植人'
// Stats
plantCount: 0,
taskDoneCount: 0,
postCount: 0,
// Favorites
favTab: 'all',
favorites: [],
filteredFavorites: [],
// Posts
postsTab: 'published',
myPublishedPosts: [],
myDrafts: [],
badges: []
// App version
appVersion: '1.0.0'
},
onLoad(options) {
this.setData({
favorites: MOCK_FAVORITES,
badges: MOCK_BADGES
});
this.filterFavorites();
onLoad() {
this.loadUserInfo();
},
onShow() {
if (typeof this.getTabBar === 'function' &&
this.getTabBar()) {
this.getTabBar().setData({
selected: 4 // Index 4 is Profile
})
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 4 });
}
// Refresh posts data
this.loadMyPosts();
this.loadDrafts();
},
// ======== User Info ========
loadUserInfo() {
// Try to get from globalData or storage
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo');
if (userInfo && userInfo.name) {
this.setData({
userName: userInfo.name || '植物爱好者',
userAvatar: userInfo.avatarUrl || userInfo.avatar || ''
});
return; // Use cached data, no API call
}
// Only fetch from backend if no cached info
request.get('/user/info').then(user => {
if (!user) return;
const avatarUrl = user.avatar ? user.avatar.url : '';
this.setData({
userName: user.name || '植物爱好者',
userAvatar: avatarUrl
});
const info = {
id: user.id,
name: user.name,
avatarUrl: avatarUrl,
account: user.account,
phone: user.phone,
avatarId: user.avatarId
};
app.globalData.userInfo = info;
wx.setStorageSync('userInfo', info);
}).catch(() => { });
},
// ======== Stats ========
loadStats() {
// Fetch plant count
request.post('/plant/page', { current: 1, pageSize: 1 }).then(res => {
this.setData({ plantCount: res.total || 0 });
}).catch(() => { });
// Fetch post count - user's own posts
request.post('/post/page', { current: 1, pageSize: 1, onlyMine: true }).then(res => {
this.setData({ postCount: res.total || 0 });
}).catch(() => { });
// Fetch completed tasks count
request.get('/plant/taskCount').then(res => {
this.setData({ taskDoneCount: res || 0 });
}).catch(() => { });
},
// ======== Navigation ========
setView(e) {
const view = e.currentTarget.dataset.view;
this.setData({ view });
if (view === 'favorites') {
this.loadFavorites();
} else if (view === 'posts') {
this.loadMyPosts();
this.loadDrafts();
}
},
goBack() {
this.setData({ view: 'profile' });
},
// ======== Favorites ========
loadFavorites() {
// TODO: Call favorites API when available
// request.get('/user/favorites').then(...)
this.filterFavorites();
},
onFavTabChange(e) {
this.setData({ favTab: e.detail.value }, () => {
this.filterFavorites();
});
},
filterFavorites() {
const { favorites, favTab } = this.data;
const filtered = favorites.filter(item => {
if (favTab === 'all') return true;
return item.type === favTab;
});
this.setData({ filteredFavorites: filtered });
},
// ======== Posts ========
loadMyPosts() {
// Get published posts by current user
const myPosts = MOCK_POSTS.filter(p => p.user === '我的花园');
this.setData({ myPublishedPosts: myPosts });
request.post('/post/page', { current: 1, pageSize: 50, onlyMine: true }).then(res => {
const records = res.records || res.list || [];
const posts = records.map(item => {
const publisher = item.publisher || {};
const imgList = item.imgList || [];
return {
id: item.id,
content: item.content || '',
time: this._formatTime(item.createdAt || item.createTime),
images: imgList.map(img => img.url),
likes: item.likeList || [],
comments: item.commentList || []
};
});
this.setData({ myPublishedPosts: posts });
}).catch(() => {
this.setData({ myPublishedPosts: [] });
});
},
loadDrafts() {
// Load drafts from storage
try {
const draft = wx.getStorageSync('post_draft');
if (draft && (draft.content || (draft.images && draft.images.length > 0))) {
// Convert single draft to array for consistency
this.setData({
myDrafts: [{
id: 'draft_1',
@@ -63,39 +174,10 @@ Page({
}
},
setView(e) {
const view = e.currentTarget.dataset.view;
this.setData({ view });
// Refresh data when entering posts view
if (view === 'posts') {
this.loadMyPosts();
this.loadDrafts();
}
},
onFavTabChange(e) {
const tab = e.detail.value;
this.setData({ favTab: tab }, () => {
this.filterFavorites();
});
},
onPostsTabChange(e) {
const tab = e.detail.value;
this.setData({ postsTab: tab });
this.setData({ postsTab: e.detail.value });
},
filterFavorites() {
const { favorites, favTab } = this.data;
const filtered = favorites.filter(item => {
if (favTab === 'all') return true;
return item.type === favTab;
});
this.setData({ filteredFavorites: filtered });
},
// Delete a published post
deletePost(e) {
const postId = e.currentTarget.dataset.id;
wx.showModal({
@@ -103,42 +185,176 @@ Page({
content: '确定要删除这条动态吗?',
confirmColor: '#EF5350',
success: (res) => {
if (res.confirm) {
// Remove from MOCK_POSTS
const idx = MOCK_POSTS.findIndex(p => p.id === postId);
if (idx > -1) {
MOCK_POSTS.splice(idx, 1);
}
if (!res.confirm) return;
wx.showLoading({ title: '删除中...' });
request.get('/post/delete', { id: postId }).then(() => {
wx.hideLoading();
this.loadMyPosts();
wx.showToast({ title: '已删除', icon: 'success' });
}
}).catch(() => {
wx.hideLoading();
wx.showToast({ title: '删除失败', icon: 'none' });
});
}
});
},
// Edit a draft
editDraft(e) {
// Navigate to create page, which will load the draft
wx.navigateTo({
url: '/pages/community/create/index'
});
editDraft() {
wx.navigateTo({ url: '/pages/community/create/index' });
},
// Delete a draft
deleteDraft(e) {
deleteDraft() {
wx.showModal({
title: '删除草稿',
content: '确定要删除这份草稿吗?',
confirmColor: '#EF5350',
success: (res) => {
if (res.confirm) {
try {
wx.removeStorageSync('post_draft');
} catch (e) { }
this.setData({ myDrafts: [] });
wx.showToast({ title: '已删除', icon: 'success' });
}
if (!res.confirm) return;
try { wx.removeStorageSync('post_draft'); } catch (e) { }
this.setData({ myDrafts: [] });
wx.showToast({ title: '已删除', icon: 'success' });
}
});
}
},
// ======== Menu Actions ========
goToIdentifyHistory() {
wx.navigateTo({ url: '/pages/profile/identify-history/index' });
},
goToNotificationSettings() {
// Open WeChat notification settings
wx.openSetting({
success: (res) => {
console.log('Settings opened', res);
}
});
},
goToAbout() {
this.setData({ view: 'about' });
},
goToAgreement() {
// TODO: Navigate to agreement page or show inline
wx.showToast({ title: '功能开发中', icon: 'none' });
},
goToPrivacy() {
// TODO: Navigate to privacy page or show inline
wx.showToast({ title: '功能开发中', icon: 'none' });
},
// ======== Profile Editor Popup ========
openProfileEditor() {
this.setData({
showProfileEditor: true,
tempAvatar: '',
tempNickname: this.data.userName === '植物爱好者' ? '' : this.data.userName
});
},
closeProfileEditor() {
this.setData({ showProfileEditor: false });
},
onProfilePopupChange(e) {
if (!e.detail.visible) {
this.setData({ showProfileEditor: false });
}
},
// WeChat native chooseAvatar callback
onChooseAvatar(e) {
const avatarUrl = e.detail.avatarUrl;
if (avatarUrl) {
this.setData({ tempAvatar: avatarUrl });
}
},
onNicknameInput(e) {
this.setData({ tempNickname: e.detail.value });
},
onNicknameBlur(e) {
// WeChat nickname type may return value on blur
if (e.detail.value) {
this.setData({ tempNickname: e.detail.value });
}
},
async saveProfile() {
const { tempAvatar, tempNickname } = this.data;
if (!tempAvatar && !tempNickname) {
wx.showToast({ title: '请选择头像或输入昵称', icon: 'none' });
return;
}
wx.showLoading({ title: '保存中...', mask: true });
try {
const updatePayload = {};
// 1. Upload avatar if changed
if (tempAvatar) {
const data = await request.upload(tempAvatar);
const fileData = data?.file || {};
if (fileData.id) {
updatePayload.avatar_id = fileData.id;
// Update local display
this.setData({ userAvatar: fileData.url || tempAvatar });
}
}
// 2. Set name if provided
if (tempNickname) {
updatePayload.name = tempNickname;
this.setData({ userName: tempNickname });
}
// 3. Call update API
if (Object.keys(updatePayload).length > 0) {
await request.post('/user/update', updatePayload);
}
wx.hideLoading();
this.setData({ showProfileEditor: false });
wx.showToast({ title: '资料已更新', icon: 'success' });
// Update globalData
const userInfo = app.globalData.userInfo || {};
if (updatePayload.name) userInfo.name = updatePayload.name;
if (updatePayload.avatar_id) userInfo.avatarId = updatePayload.avatar_id;
app.globalData.userInfo = userInfo;
} catch (err) {
wx.hideLoading();
console.error('Save profile failed', err);
wx.showToast({ title: '保存失败', icon: 'none' });
}
},
// ======== Utilities ========
_formatTime(dateStr) {
if (!dateStr) return '';
const d = new Date(dateStr);
const now = new Date();
const diffMs = now - d;
const diffMin = Math.floor(diffMs / 60000);
if (diffMin < 1) return '刚刚';
if (diffMin < 60) return diffMin + '分钟前';
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24) return diffHour + '小时前';
const diffDay = Math.floor(diffHour / 24);
if (diffDay < 7) return diffDay + '天前';
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const day = d.getDate().toString().padStart(2, '0');
return `${month}-${day}`;
},
// ======== Reserved: Future Level/Badge System ========
// These methods will be implemented when the backend supports level/badge APIs
// loadLevelInfo() { request.get('/user/level').then(...) },
// loadBadges() { request.get('/user/badges').then(...) },
})
+5 -4
View File
@@ -1,15 +1,16 @@
{
"navigationBarTitleText": "个人中心",
"disableScroll": true,
"usingComponents": {
"t-grid": "tdesign-miniprogram/grid/grid",
"t-grid-item": "tdesign-miniprogram/grid-item/grid-item",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-image": "tdesign-miniprogram/image/image",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-badge": "tdesign-miniprogram/badge/badge",
"t-progress": "tdesign-miniprogram/progress/progress"
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-button": "tdesign-miniprogram/button/button",
"t-popup": "tdesign-miniprogram/popup/popup"
}
}
+258 -178
View File
@@ -1,200 +1,280 @@
<wxs src="../../utils/tools.wxs" module="tools" />
<view class="profile-page">
<!-- Sub-views handled by conditional rendering to match prototype single-page feel -->
<!-- FAVORITES VIEW -->
<view wx:if="{{view === 'favorites'}}" class="favorites-page info-view-anim">
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">我的收藏</t-button>
</view>
<t-tabs value="{{favTab}}" bind:change="onFavTabChange" theme="card">
<t-tab-panel label="全部" value="all" />
<t-tab-panel label="植物" value="plant" />
<t-tab-panel label="文章" value="article" />
</t-tabs>
<view class="tab-content">
<view class="fav-grid">
<block wx:if="{{filteredFavorites.length > 0}}">
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
<t-image src="{{item.image}}" class="fav-img" mode="aspectFill" width="100%" height="240rpx" />
<view class="fav-info">
<text class="fav-name">{{item.name}}</text>
<view class="fav-meta-row">
<t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="32rpx" color="#90A4AE" />
<text class="fav-type">{{item.meta}}</text>
</view>
<!-- ======== FAVORITES VIEW ======== -->
<view wx:if="{{view === 'favorites'}}" class="sub-view info-view-anim">
<view class="sub-nav" bindtap="goBack">
<t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">我的收藏</text>
</view>
<view class="category-filter">
<view class="filter-chip {{favTab === 'all' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="all">全部</view>
<view class="filter-chip {{favTab === 'plant' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="plant">植物</view>
<view class="filter-chip {{favTab === 'article' ? 'active' : ''}}" bindtap="onFavTabChange" data-value="article">文章</view>
</view>
<scroll-view scroll-y class="sub-scroll">
<view wx:if="{{filteredFavorites.length > 0}}" class="fav-grid">
<view wx:for="{{filteredFavorites}}" wx:key="id" class="fav-card">
<t-image src="{{item.image}}" mode="aspectFill" width="100%" height="240rpx" class="fav-img" />
<view class="fav-info">
<text class="fav-name">{{item.name}}</text>
<view class="fav-meta-row">
<t-icon name="{{item.type === 'plant' ? 'heart' : 'book'}}" size="28rpx" color="#90A4AE" />
<text class="fav-type">{{item.meta}}</text>
</view>
</view>
</block>
<block wx:else>
<view class="empty-state">
<t-icon name="star" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无收藏内容</text>
</view>
</view>
<view wx:else class="empty-state">
<text class="empty-text">暂无收藏内容</text>
</view>
</scroll-view>
</view>
<!-- ======== POSTS VIEW ======== -->
<view wx:elif="{{view === 'posts'}}" class="sub-view info-view-anim">
<view class="sub-nav" bindtap="goBack">
<t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">我的发布</text>
</view>
<scroll-view scroll-y class="sub-scroll">
<view wx:for="{{myPublishedPosts}}" wx:key="id" class="my-post-card">
<view class="my-post-time">{{item.time}}</view>
<view class="my-post-content-wrap">
<text class="post-text">{{item.content}}</text>
<view wx:if="{{item.images.length > 0}}" class="my-post-images">
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this"
src="{{img}}" mode="aspectFill" width="140rpx" height="140rpx"
shape="round" />
</view>
<view class="my-post-footer">
<view class="footer-item"><t-icon name="heart" size="28rpx" /> <text>{{item.likes.length}}</text></view>
<view class="footer-item"><t-icon name="chat" size="28rpx" /> <text>{{item.comments.length}}</text></view>
<view class="footer-item" catchtap="deletePost" data-id="{{item.id}}" style="margin-left:auto; color: #EF5350;">
<t-icon name="delete" size="28rpx" />
</view>
</block>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- POSTS VIEW -->
<view wx:elif="{{view === 'posts'}}" class="posts-page-detail info-view-anim">
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">我的发布</t-button>
</view>
<!-- Tabs for Published / Drafts -->
<t-tabs value="{{postsTab}}" bind:change="onPostsTabChange" theme="card">
<t-tab-panel label="已发布" value="published" />
<t-tab-panel label="草稿箱" value="drafts" />
</t-tabs>
<!-- Published Posts -->
<view wx:if="{{postsTab === 'published'}}" class="my-posts-list">
<block wx:if="{{myPublishedPosts.length > 0}}">
<view wx:for="{{myPublishedPosts}}" wx:key="id" class="my-post-card">
<view class="my-post-time">{{item.time}}</view>
<view class="my-post-content-wrap">
<text class="post-text">{{item.content}}</text>
<view wx:if="{{item.images.length > 0}}" class="my-post-images">
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
</view>
<view class="my-post-footer">
<view class="footer-item"><t-icon name="heart" size="32rpx" /> <text>{{item.likes.length}}</text></view>
<view class="footer-item"><t-icon name="chat" size="32rpx" /> <text>{{item.comments.length}}</text></view>
<view class="footer-item delete-btn" bindtap="deletePost" data-id="{{item.id}}"><t-icon name="delete" size="32rpx" color="#EF5350" /></view>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<t-icon name="file-copy" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无已发布的动态</text>
</view>
<!-- ======== BADGES VIEW ======== -->
<view wx:elif="{{view === 'badges'}}" class="sub-view info-view-anim">
<view class="sub-nav" bindtap="goBack">
<t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">成就徽章</text>
</view>
<!-- Draft Posts -->
<view wx:if="{{postsTab === 'drafts'}}" class="my-posts-list">
<block wx:if="{{myDrafts.length > 0}}">
<view wx:for="{{myDrafts}}" wx:key="id" class="my-post-card draft-card">
<view class="draft-badge">草稿</view>
<view class="my-post-content-wrap">
<text class="post-text">{{item.content || '(无文字内容)'}}</text>
<view wx:if="{{item.images.length > 0}}" class="my-post-images">
<t-image wx:for="{{item.images}}" wx:for-item="img" wx:key="*this" src="{{img}}" mode="aspectFill" width="160rpx" height="160rpx" style="margin-right: 16rpx; display: inline-block; border-radius: 8rpx;" />
</view>
<view class="my-post-footer">
<view class="footer-item edit-btn" bindtap="editDraft" data-index="{{index}}">
<t-icon name="edit" size="32rpx" color="#558B2F" />
<text style="color: #558B2F;">编辑</text>
</view>
<view class="footer-item delete-btn" bindtap="deleteDraft" data-index="{{index}}">
<t-icon name="delete" size="32rpx" color="#EF5350" />
<text style="color: #EF5350;">删除</text>
</view>
</view>
</view>
</view>
</block>
<view wx:else class="empty-state">
<t-icon name="file-add" size="64rpx" color="#ccc" />
<text style="margin-top: 16rpx;">暂无草稿</text>
</view>
</view>
</view>
<!-- BADGES VIEW -->
<view wx:elif="{{view === 'badges'}}" class="badges-page info-view-anim">
<view class="back-nav sticky-nav">
<t-button variant="text" icon="arrow-left" bind:tap="setView" data-view="profile">成就徽章</t-button>
</view>
<scroll-view scroll-y class="badges-content">
<scroll-view scroll-y class="sub-scroll">
<!-- Level Card -->
<view class="level-card-large">
<view class="level-header">
<view class="level-info-large">
<text class="level-label">当前等级</text>
<text class="level-value">Lv.4 资深植人</text>
</view>
<t-icon name="trophy" size="80rpx" color="#FFD700" />
</view>
<view class="level-progress-section">
<view class="progress-text">
<text>经验值</text>
<text>350 / 500</text>
</view>
<t-progress percentage="70" theme="plump" color="#FFD700" track-color="rgba(255,255,255,0.3)" />
<text class="next-level-tip">距离 Lv.5 园艺大师 还需 150 经验</text>
</view>
<view class="level-card-bg"></view>
<view class="level-header">
<view class="level-info-large">
<text class="level-label">当前等级</text>
<text class="level-value">Lv.4 资深植人</text>
</view>
<t-icon name="trophy" size="80rpx" color="#FFD700" />
</view>
<view class="level-progress-section">
<view class="progress-text">
<text>经验值</text>
<text>350 / 500</text>
</view>
<view class="level-progress-bar-bg">
<view class="level-progress-bar-fill" style="width: 70%;"></view>
</view>
<text class="next-level-tip">距离 Lv.5 园艺大师 还需 150 经验</text>
</view>
</view>
<view class="section-title-badges">所有徽章 (3/6)</view>
<t-grid column="{{3}}" gutter="24rpx">
<t-grid-item wx:for="{{badges}}" wx:key="id" text="{{item.name}}" description="{{item.desc}}" image="{{item.unlocked ? '/assets/icons/'+item.icon+'.png' : '/assets/icons/lock.png'}}" />
<!-- TDesign grid item image might need full path or use slot for svg if needed, using png for now -->
</t-grid>
<view class="badges-grid">
<view wx:for="{{badges}}" wx:key="id" class="badge-item {{item.unlocked ? 'unlocked' : 'locked'}}">
<view class="badge-icon-circle" style="background: {{item.unlocked ? item.color + '20' : '#F5F5F5'}}">
<t-icon wx:if="{{item.unlocked}}" name="{{item.iconName}}" size="48rpx" color="{{item.color}}" />
<t-icon wx:else name="lock-on" size="40rpx" color="#BDBDBD" />
</view>
<text class="badge-name">{{item.name}}</text>
<text class="badge-desc">{{item.desc}}</text>
<text wx:if="{{item.progress}}" class="badge-progress">{{item.progress}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- MAIN PROFILE VIEW -->
<view wx:else class="main-profile-view">
<view class="profile-header">
<view class="user-main">
<view class="user-avatar">
<t-avatar image="https://api.dicebear.com/7.x/avataaars/svg?seed=Lucky" size="large" />
<!-- ======== ABOUT VIEW ======== -->
<view wx:elif="{{view === 'about'}}" class="sub-view info-view-anim">
<view class="sub-nav" bindtap="goBack">
<t-icon name="chevron-left" size="40rpx" />
<text class="sub-nav-title">关于我们</text>
</view>
<scroll-view scroll-y class="sub-scroll">
<view class="about-section">
<view class="about-logo-area">
<view class="about-logo">
<t-icon name="flower" size="72rpx" color="#558B2F" />
</view>
<text class="about-app-name">植物护理助手</text>
<text class="about-version">版本 {{appVersion}}</text>
</view>
<view class="user-text">
<text class="user-name">布偶猫园长</text>
<t-tag theme="warning" variant="light" size="small">Lv.4 资深植人</t-tag>
<view class="about-desc">
<text>一款专注于家庭植物养护的小程序。帮助你记录植物成长、制定养护计划、识别未知植物,与花友们分享养花心得。</text>
</view>
<view class="about-footer">
<text class="about-copyright">© 2026 Sundynix · All Rights Reserved</text>
</view>
</view>
<t-icon name="setting" size="48rpx" />
</view>
<scroll-view scroll-y class="profile-content">
<view class="stats-grid">
<view class="stat-col">
<text class="stat-num">12</text>
<text class="stat-label">植物</text>
</view>
<view class="stat-col">
<text class="stat-num">328</text>
<text class="stat-label">养护</text>
</view>
<view class="stat-col">
<text class="stat-num">15</text>
<text class="stat-label">关注</text>
</view>
</view>
<view class="profile-menu">
<t-cell-group title="常用功能" theme="card">
<t-cell title="我的收藏" hover arrow bind:tap="setView" data-view="favorites">
<t-icon slot="left-icon" name="star" color="#FFA000" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="我的发布" hover arrow bind:tap="setView" data-view="posts">
<t-icon slot="left-icon" name="file-copy" color="#1976D2" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="识别记录" hover arrow>
<t-icon slot="left-icon" name="scan" color="#388E3C" style="margin-right: 16rpx;" />
</t-cell>
<t-cell title="成就徽章" note="已获 3 个" hover arrow bind:tap="setView" data-view="badges">
<t-icon slot="left-icon" name="control-platform" color="#AB47BC" style="margin-right: 16rpx;" />
</t-cell>
</t-cell-group>
<t-cell-group title="更多服务" theme="card" style="margin-top: 24rpx;">
<t-cell title="帮助与反馈" hover arrow>
<t-icon slot="left-icon" name="help-circle" color="#757575" style="margin-right: 16rpx;" />
</t-cell>
</t-cell-group>
</view>
<!-- Spacer -->
<view style="height: 40rpx;"></view>
</scroll-view>
</view>
<!-- ======== MAIN PROFILE VIEW ======== -->
<view wx:else class="main-profile-view">
<!-- Header -->
<view class="profile-header">
<view class="user-main">
<view class="user-avatar" bindtap="openProfileEditor">
<t-avatar wx:if="{{userAvatar}}" image="{{userAvatar}}" size="120rpx" />
<t-avatar wx:else icon="user" size="120rpx" />
</view>
<view class="user-text" bindtap="openProfileEditor">
<view class="user-name">{{userName}}</view>
<view class="level-badge">Lv.4 资深植人</view>
</view>
</view>
<view class="settings-btn" bindtap="goToNotificationSettings">
<t-icon name="setting" size="40rpx" color="#666" />
</view>
</view>
<!-- Stats Card (Fixed) -->
<view class="stats-section">
<view class="stats-card">
<view class="stat-item">
<text class="stat-num">{{plantCount}}</text>
<text class="stat-label">植物</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{taskDoneCount}}</text>
<text class="stat-label">养护</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{postCount}}</text>
<text class="stat-label">动态</text>
</view>
</view>
</view>
<scroll-view scroll-y class="profile-content" enhanced show-scrollbar="{{false}}">
<!-- Menu -->
<view class="profile-menu">
<view class="menu-group-title">常用功能</view>
<view class="menu-item" bindtap="setView" data-view="favorites">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #FFF3E0">
<t-icon name="star" size="36rpx" color="#FF9800" />
</view>
<text class="menu-text">我的收藏</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
<view class="menu-item" bindtap="setView" data-view="posts">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #E3F2FD">
<t-icon name="file-copy" size="36rpx" color="#2196F3" />
</view>
<text class="menu-text">我的发布</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
<view class="menu-item" bindtap="goToIdentifyHistory">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #E8F5E9">
<t-icon name="scan" size="36rpx" color="#4CAF50" />
</view>
<text class="menu-text">识别记录</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
<view class="menu-item" bindtap="setView" data-view="badges">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #F3E5F5">
<t-icon name="award" size="36rpx" color="#9C27B0" />
</view>
<text class="menu-text">成就徽章</text>
</view>
<view class="menu-right-info">
<text class="menu-badge-text">已获 3 个</text>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>
<view class="menu-group-title" style="margin-top: 32rpx;">更多服务</view>
<view class="menu-item" bindtap="goToAbout">
<view class="menu-left">
<view class="menu-icon-bg" style="background: #F5F5F5">
<t-icon name="help-circle" size="36rpx" color="#616161" />
</view>
<text class="menu-text">帮助与关于</text>
</view>
<t-icon name="chevron-right" size="36rpx" color="#ccc" />
</view>
</view>
<view style="height: 100rpx;"></view>
</scroll-view>
</view>
<!-- Profile Edit Popup -->
<t-popup visible="{{showProfileEditor}}" placement="bottom" bind:visible-change="onProfilePopupChange">
<view class="profile-edit-popup">
<view class="popup-header">
<text class="popup-title">编辑资料</text>
<view class="popup-close" bindtap="closeProfileEditor">
<t-icon name="close" size="40rpx" color="#999" />
</view>
</view>
<view class="edit-row" bindtap="onChooseAvatar">
<text class="edit-row-label">头像</text>
<view class="edit-row-right">
<t-avatar
wx:if="{{tempAvatar || userAvatar}}"
image="{{tempAvatar || userAvatar}}"
size="96rpx"
/>
<t-avatar wx:else icon="user" size="96rpx" />
<t-icon name="chevron-right" size="32rpx" color="#C5C5C5" />
</view>
</view>
<view class="edit-row">
<text class="edit-row-label">昵称</text>
<input
class="nickname-input"
type="text"
placeholder="请输入昵称"
placeholder-style="color: #C5C5C5;"
value="{{tempNickname}}"
bindinput="onNicknameInput"
/>
</view>
<view class="edit-actions">
<t-button theme="primary" block shape="round" bind:tap="saveProfile">保存</t-button>
</view>
</view>
</t-popup>
</view>
+575 -101
View File
@@ -1,149 +1,623 @@
/** pages/profile/index.wxss **/
.profile-page {
background-color: #F4F6F0;
min-height: 100vh;
position: relative;
overflow: hidden;
display: flex; flex-direction: column;
background: #F4F6F0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ======== Sub-view Navigation ======== */
.sub-view {
background: #F4F6F0;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.sub-nav {
display: flex;
align-items: center;
gap: 12rpx;
padding: 30rpx 24rpx;
background: #fff;
font-size: 34rpx;
font-weight: 700;
color: #111827;
position: sticky;
top: 0;
z-index: 100;
}
.sub-nav-title {
margin-left: 8rpx;
}
.sub-scroll {
flex: 1;
padding: 24rpx;
box-sizing: border-box;
padding-bottom: 80rpx;
}
/* Animations */
.info-view-anim {
animation: slideInRight 0.3s cubic-bezier(0.25, 1, 0.5, 1);
animation: slideInRight 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.sticky-nav {
position: sticky; top: 0; z-index: 100; background: white;
border-bottom: 2rpx solid #f0f0f0;
padding: 20rpx;
/* ======== Category Filter (Custom Chips) ======== */
.category-filter {
display: flex;
gap: 16rpx;
padding: 0 24rpx 16rpx;
background: #fff;
margin-bottom: 16rpx;
}
.tab-content { padding: 32rpx; }
.filter-chip {
padding: 8rpx 24rpx;
background: #fff;
border: 2rpx solid #E5E7EB;
border-radius: 40rpx;
font-size: 26rpx;
color: #6B7280;
transition: all 0.2s;
}
/* Favorites Grid */
.filter-chip.active {
background: #333;
color: #fff;
border-color: #333;
}
/* ======== Favorites Grid ======== */
.fav-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
margin-top: 24rpx;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.fav-card {
background: white;
border-radius: 24rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
background: white;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
}
.fav-img { width: 100%; display: block; background: #f0f0f0; }
.fav-info { padding: 20rpx; }
.fav-name { font-size: 28rpx; font-weight: 700; color: #37474F; margin-bottom: 12rpx; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fav-meta-row { display: flex; align-items: center; gap: 8rpx; }
.fav-type { font-size: 20rpx; color: #90A4AE; }
.empty-state {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 80rpx 0; color: #B0BEC5; font-size: 28rpx;
.fav-img {
background: #f0f0f0;
}
/* Posts View */
.my-posts-list { padding: 40rpx; }
.my-post-card { display: flex; gap: 24rpx; margin-bottom: 48rpx; position: relative; }
.my-post-time { font-size: 24rpx; color: #B0BEC5; width: 140rpx; flex-shrink: 0; text-align: right; }
.my-post-content-wrap { flex: 1; border-left: 4rpx solid #ECEFF1; padding-left: 24rpx; padding-bottom: 24rpx; }
.my-post-images { margin: 16rpx 0; white-space: nowrap; overflow-x: auto; }
.my-post-footer { display: flex; gap: 32rpx; margin-top: 16rpx; }
.footer-item { display: flex; align-items: center; gap: 8rpx; font-size: 24rpx; color: #78909C; }
/* Draft Card */
.draft-card {
background: #FFFDE7;
border-radius: 16rpx;
padding: 20rpx;
margin-left: 0;
.fav-info {
padding: 16rpx 20rpx;
}
.draft-card .my-post-content-wrap {
border-left: 4rpx solid #FFC107;
.fav-name {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #1F2937;
margin-bottom: 8rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.draft-badge {
position: absolute;
top: 0;
left: 0;
background: #FFC107;
color: #fff;
font-size: 20rpx;
padding: 4rpx 16rpx;
border-radius: 8rpx 0 8rpx 0;
font-weight: 600;
.fav-meta-row {
display: flex;
align-items: center;
gap: 8rpx;
}
/* Action Buttons */
.edit-btn, .delete-btn {
cursor: pointer;
.fav-type {
font-size: 22rpx;
color: #9CA3AF;
}
.edit-btn:active, .delete-btn:active {
opacity: 0.7;
/* ======== Posts Styles (Refined) ======== */
.my-post-card {
display: flex;
gap: 24rpx;
margin-bottom: 32rpx;
}
/* Badges View */
.badges-content { padding: 40rpx; background: white; height: 100%; }
.my-post-time {
width: 100rpx;
font-size: 24rpx;
color: #9CA3AF;
font-weight: 500;
padding-top: 8rpx;
flex-shrink: 0;
text-align: right;
}
.my-post-content-wrap {
flex: 1;
background: #fff;
padding: 24rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
}
.post-text {
font-size: 28rpx;
line-height: 1.5;
color: #1F2937;
margin-bottom: 16rpx;
display: block;
}
.my-post-images {
display: flex;
gap: 12rpx;
margin-bottom: 16rpx;
flex-wrap: wrap;
}
.my-post-footer {
display: flex;
align-items: center;
gap: 32rpx;
border-top: 1rpx solid #F3F4F6;
padding-top: 16rpx;
}
.footer-item {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
color: #9CA3AF;
}
/* ======== Badges View ======== */
.level-card-large {
background: linear-gradient(135deg, #FFF3E0 0%, #FFE0B2 100%);
border-radius: 40rpx;
padding: 48rpx;
margin-bottom: 60rpx;
color: #E65100;
background: linear-gradient(135deg, #2c3e50 0%, #4ca1af 100%);
border-radius: 40rpx;
padding: 40rpx;
color: white;
margin-bottom: 48rpx;
box-shadow: 0 20rpx 40rpx rgba(44, 62, 80, 0.2);
position: relative;
overflow: hidden;
}
.level-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40rpx; }
.level-label { font-size: 26rpx; opacity: 0.8; display: block; }
.level-value { font-size: 48rpx; font-weight: 800; display: block; }
.level-card-bg {
position: absolute;
top: -100rpx;
right: -100rpx;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
border-radius: 50%;
}
.level-progress-section { }
.progress-text { display: flex; justify-content: space-between; font-size: 24rpx; font-weight: 600; margin-bottom: 12rpx; }
.next-level-tip { font-size: 22rpx; margin-top: 16rpx; display: block; opacity: 0.8; }
.level-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 40rpx;
position: relative;
z-index: 2;
}
.section-title-badges { font-size: 32rpx; font-weight: 700; color: #333; margin-bottom: 32rpx; }
.level-info-large {
display: flex;
flex-direction: column;
gap: 8rpx;
}
/* Basic TDesign Grid Item styling override if needed */
.t-grid-item__content { padding: 24rpx 0 !important; }
.level-label {
font-size: 24rpx;
opacity: 0.8;
letter-spacing: 2rpx;
text-transform: uppercase;
}
/* Main Profile */
.main-profile-view { display: flex; flex-direction: column; height: 100%; }
.level-value {
font-size: 48rpx;
font-weight: 800;
}
.level-progress-section {
position: relative;
z-index: 2;
}
.progress-text {
display: flex;
justify-content: space-between;
font-size: 26rpx;
margin-bottom: 16rpx;
font-weight: 600;
opacity: 0.9;
}
.level-progress-bar-bg {
height: 16rpx;
background: rgba(0,0,0,0.2);
border-radius: 8rpx;
margin-bottom: 24rpx;
border: 2rpx solid rgba(255,255,255,0.1);
}
.level-progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #FFD700, #FDB931);
border-radius: 8rpx;
box-shadow: 0 0 12rpx rgba(255, 215, 0, 0.4);
}
.next-level-tip {
font-size: 24rpx;
color: rgba(255,255,255,0.7);
display: block;
text-align: right;
}
.section-title-badges {
font-size: 32rpx;
font-weight: 700;
color: #1F2937;
margin-bottom: 32rpx;
margin-left: 12rpx;
}
.badges-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20rpx;
}
.badge-item {
background: #fff;
border-radius: 24rpx;
padding: 32rpx 16rpx;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
}
.badge-item.locked {
background: #F8F9FA;
border: 2rpx dashed #E5E7EB;
box-shadow: none;
}
.badge-icon-circle {
width: 88rpx;
height: 88rpx;
border-radius: 30rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.badge-name {
font-size: 26rpx;
font-weight: 700;
color: #374151;
margin-bottom: 6rpx;
}
.badge-desc {
font-size: 20rpx;
color: #9CA3AF;
}
.badge-progress {
margin-top: 12rpx;
font-size: 20rpx;
background: #F3F4F6;
padding: 4rpx 12rpx;
border-radius: 12rpx;
color: #6B7280;
}
/* ======== Main Profile View ======== */
.main-profile-view {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
height: 0;
}
.profile-header {
padding: 40rpx 48rpx;
background: white;
display: flex; justify-content: space-between; align-items: flex-start;
background: linear-gradient(180deg, #E8F5E9 0%, #FFFFFF 100%);
padding: 32rpx 40rpx;
/* Extra padding top handled by structure relative to status bar usually,
but standard padding is fine here */
display: flex;
justify-content: space-between;
align-items: center;
border-bottom-left-radius: 48rpx;
border-bottom-right-radius: 48rpx;
box-shadow: 0 8rpx 30rpx rgba(0,0,0,0.02);
margin-bottom: 24rpx;
flex-shrink: 0;
}
.user-main { display: flex; align-items: center; gap: 32rpx; }
.user-text { display: flex; flex-direction: column; gap: 12rpx; }
.user-name { font-size: 40rpx; font-weight: 800; color: var(--text-main); }
.stats-grid {
display: flex; justify-content: space-around;
padding: 40rpx;
margin: 24rpx 40rpx;
background: white;
border-radius: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.02);
.user-main {
display: flex;
align-items: center;
gap: 32rpx;
}
.stat-col { display: flex; flex-direction: column; align-items: center; gap: 4rpx; }
.stat-num { font-size: 36rpx; font-weight: 800; color: var(--text-main); }
.stat-label { font-size: 22rpx; color: #90A4AE; }
.user-avatar {
width: 128rpx;
height: 128rpx;
border-radius: 50%;
border: 6rpx solid #fff;
box-shadow: 0 8rpx 20rpx rgba(0,0,0,0.08);
overflow: hidden;
}
.user-text {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.user-name {
font-size: 40rpx;
font-weight: 800;
color: #1F2937;
}
.level-badge {
align-self: flex-start;
font-size: 22rpx;
background: #DCEDC8;
color: #33691E;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-weight: 600;
}
.settings-btn {
padding: 16rpx;
}
.profile-content {
flex: 1;
height: 0;
padding: 0 32rpx;
padding-bottom: 120rpx;
box-sizing: border-box;
}
/* Hide all scrollbars globally on this page */
.profile-page ::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.profile-page scroll-view {
scrollbar-width: none;
}
.stats-section {
padding: 0 32rpx;
flex-shrink: 0;
}
/* Stats Card */
.stats-card {
display: flex;
background: #fff;
padding: 40rpx 0;
border-radius: 32rpx;
box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.02);
margin-bottom: 32rpx;
}
.stat-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8rpx;
}
.stat-num {
font-size: 40rpx;
font-weight: 800;
color: #374151;
}
.stat-label {
font-size: 24rpx;
color: #9CA3AF;
}
.stat-divider {
width: 2rpx;
height: 60%;
background: #F3F4F6;
align-self: center;
}
/* Profile Menu */
.profile-menu {
padding: 0 32rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.menu-group-title {
font-size: 26rpx;
color: #9CA3AF;
font-weight: 600;
margin-left: 12rpx;
}
.menu-item {
background: #fff;
padding: 32rpx;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.015);
}
.menu-item:active {
background: #FAFAFA;
}
.menu-left {
display: flex;
align-items: center;
gap: 24rpx;
}
.menu-icon-bg {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.menu-text {
font-size: 30rpx;
font-weight: 600;
color: #374151;
}
.menu-right-info {
display: flex;
align-items: center;
gap: 8rpx;
}
.menu-badge-text {
font-size: 26rpx;
color: #6B7280;
}
/* Edit Popup Styles */
.profile-edit-popup {
background: #fff;
border-radius: 40rpx 40rpx 0 0;
padding: 0 48rpx;
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
}
.popup-header {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0 20rpx;
position: relative;
}
.popup-title {
font-size: 36rpx;
font-weight: 800;
color: #111827;
}
.popup-close {
position: absolute;
right: 0;
top: 40rpx;
}
/* Edit Form Rows */
.edit-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 0;
border-bottom: 2rpx solid #F3F4F6;
}
.edit-row-label {
font-size: 32rpx;
font-weight: 600;
color: #374151;
flex-shrink: 0;
}
.edit-row-right {
display: flex;
align-items: center;
gap: 16rpx;
}
.nickname-input {
flex: 1;
font-size: 32rpx;
font-weight: 500;
color: #111827;
text-align: right;
min-width: 0;
}
.edit-actions {
padding-top: 60rpx;
padding-bottom: 20rpx;
}
/* About Section */
.about-section {
padding: 24rpx;
}
.about-logo-area {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 60rpx;
margin-top: 40rpx;
}
.about-logo {
width: 160rpx;
height: 160rpx;
background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
border-radius: 48rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 12rpx 32rpx rgba(85, 139, 47, 0.1);
margin-bottom: 32rpx;
}
.about-app-name {
font-size: 40rpx;
font-weight: 700;
color: #1F2937;
letter-spacing: 2rpx;
}
.about-version {
font-size: 24rpx;
color: #9CA3AF;
margin-top: 8rpx;
}
.about-desc {
background: #fff;
padding: 40rpx;
border-radius: 24rpx;
font-size: 30rpx;
line-height: 1.7;
color: #4B5563;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
margin-bottom: 40rpx;
}