wx-shop/pages/my/index.vue
@zuopngfei d13d026852 wewe
2025-11-27 09:38:39 +08:00

1657 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<view class="profile-card">
<view class="profile-header">
<view class="avatar-wrapper" @click="uploadAvatar">
<view class="avatar">
<image :src="avatarUrl" mode="aspectFill" @error="handleImageError"></image>
</view>
<view class="avatar-upload">📷</view>
</view>
<view class="profile-info">
<view class="title-row">
<text class="name">{{ userInfo.username }}</text>
</view>
<view class="meta-row">
<text class="meta-item">加入于 2025-01-15</text>
<view class="vip-badge">
<text>品鉴会员</text>
</view>
</view>
<view class="meta-row">
<text class="meta-item">上海市浦东新区张江高科技园区</text>
</view>
</view>
<view class="edit-btn" @click="toEditInfoPage()">编辑资料</view>
</view>
<view class="stats-grid">
<view class="stat-card" v-for="item in stats" :key="item.label"
@click="handleStatCardClick(item)">
<view class="stat-icon" :style="{ color: item.color }">{{ item.icon }}</view>
<text class="stat-value" :style="{ color: item.color }">{{ item.value }}</text>
<text class="stat-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="signin-card">
<view class="signin-header">
<view class="signin-headline">
<text class="signin-title">每日签到</text>
<text class="signin-desc">已连续签到 {{ signInfo.continuousDays }} 天</text>
</view>
<view class="signin-badge">+{{ signInfo.todayReward || 0 }} 积分</view>
</view>
<view class="signin-calendar">
<view class="signin-calendar-header">
<text class="signin-month">{{ signInfo.monthLabel }}</text>
<text class="signin-month-summary">本月已签 {{ signInfo.monthSignedDays || 0 }} 天</text>
</view>
<view class="signin-weekdays">
<text class="signin-weekday" v-for="label in weekdayLabels" :key="label">{{ label }}</text>
</view>
<view class="signin-calendar-grid">
<view class="signin-date" v-for="(day, index) in signCalendar" :key="index"
:class="[{ placeholder: day.isPlaceholder, signed: day.signed, today: day.isToday, missed: day.missed }]">
<text class="signin-date-number">{{ day.label }}</text>
</view>
</view>
</view>
<view class="signin-footer">
<view class="signin-stats">
<view class="signin-stat">
<text class="signin-stat-value">{{ signInfo.totalPoints }}</text>
<text class="signin-stat-label">累计积分</text>
</view>
<view class="signin-stat">
<text class="signin-stat-value">{{ signInfo.continuousDays }}</text>
<text class="signin-stat-label">连续天数</text>
</view>
</view>
<button class="signin-btn" :class="{ disabled: signInfo.signedToday || signLoading }"
@click="handleSignIn" :disabled="signInfo.signedToday || signLoading">
{{ signInfo.signedToday ? '今日已签到' : (signLoading ? '签到中...' : '立即签到') }}
</button>
<text class="signin-tip">{{ signInfo.nextRewardText || '坚持签到,更多奖励等你拿' }}</text>
</view>
</view>
<view class="tabs">
<view class="tab-item" v-for="(tab, index) in tabs" :key="tab" :class="{ active: index === currentTab }"
@click="handleTabClick(index)">
{{ tab }}
</view>
</view>
<view class="monthly-card" v-if="currentTab === 0">
<view class="monthly-title">本月数据</view>
<view class="monthly-list">
<view class="monthly-item" v-for="item in monthlyData" :key="item.label">
<text class="monthly-label">{{ item.label }}</text>
<text class="monthly-value" :style="{ color: item.color || '#1A1A1A' }">
{{ item.value }}
</text>
</view>
</view>
</view>
<view class="orders-card" v-if="currentTab === 1">
<view class="orders-header">
<text class="orders-title">我的订单</text>
<view class="orders-more" @click="navigateTo('/pages/order/index')">查看全部</view>
</view>
<view class="order-list">
<view class="order-item" v-for="order in orders" :key="order.no">
<view class="order-top">
<text class="order-no">{{ order.no }}</text>
<view class="order-tags">
<view class="order-tag type">{{ order.type }}</view>
<view class="order-tag status" :class="order.statusClass">{{ order.status }}</view>
</view>
<text class="order-date">{{ order.date }}</text>
</view>
<view class="order-body">
<view class="order-thumb">60x60</view>
<view class="order-info">
<text class="order-name">{{ order.product }}</text>
<text class="order-spec">{{ order.spec }}</text>
</view>
</view>
<view class="order-footer">
<view class="order-amount">
<text class="amount-label">订单总额</text>
<text class="amount-value">{{ order.amount }}</text>
</view>
<view class="order-actions">
<view class="order-action" v-for="action in order.actions" :key="action.label">
{{ action.label }}
</view>
</view>
</view>
</view>
<view class="order-empty" v-if="orders.length === 0">
<text class="order-empty-icon">📦</text>
<text class="order-empty-text">暂无订单</text>
</view>
</view>
</view>
<view class="favorites-card" v-if="currentTab === 2">
<view class="favorites-wrapper">
<view class="favorite-item" v-for="favorite in favorites" :key="favorite.title">
<view class="favorite-thumb">150x150</view>
<view class="favorite-info">
<view class="favorite-top">
<text class="favorite-title">{{ favorite.title }}</text>
<view class="favorite-heart"></view>
</view>
<view class="favorite-meta">
<text class="favorite-rating"> {{ favorite.rating }}</text>
<text class="favorite-count">({{ favorite.count }})</text>
</view>
<view class="favorite-price">
<text class="favorite-current">{{ favorite.price }}</text>
<text class="favorite-origin">{{ favorite.origin }}</text>
</view>
</view>
</view>
</view>
</view>
<view class="achievements-card" v-if="currentTab === 3">
<view class="achievement-item" v-for="achievement in achievements" :key="achievement.title"
:class="{ highlight: achievement.highlight }">
<view class="achievement-left">
<view class="achievement-icon" :style="{ backgroundColor: achievement.iconBg }">
<text class="achievement-icon-text">{{ achievement.icon }}</text>
</view>
<view class="achievement-info">
<text class="achievement-title">{{ achievement.title }}</text>
<text class="achievement-desc">{{ achievement.desc }}</text>
<view class="achievement-progress" v-if="achievement.progressText">
<view class="achievement-progress-bar">
<view class="achievement-progress-fill"
:style="{ width: achievement.progress + '%' }"></view>
</view>
<text class="achievement-progress-text">{{ achievement.progressText }}</text>
</view>
<text class="achievement-reward">奖励{{ achievement.reward }}</text>
</view>
</view>
<text class="achievement-trophy">{{ achievement.trophy }}</text>
</view>
</view>
<view class="settings-card" v-if="currentTab === 4">
<view class="settings-section">
<text class="settings-section-title">账户设置</text>
<view class="settings-list">
<view class="settings-item" v-for="item in accountSettings" :key="item.title">
<view class="settings-left">
<view class="settings-icon">{{ item.icon }}</view>
<view class="settings-text">
<text class="settings-title">{{ item.title }}</text>
<text class="settings-desc">{{ item.desc }}</text>
</view>
</view>
<text class="settings-arrow"></text>
</view>
</view>
</view>
<view class="settings-section">
<text class="settings-section-title">其他设置</text>
<view class="settings-list">
<view class="settings-item" v-for="item in otherSettings" :key="item.title">
<view class="settings-left">
<view class="settings-icon">{{ item.icon }}</view>
<view class="settings-text">
<text class="settings-title">{{ item.title }}</text>
<text class="settings-desc">{{ item.desc }}</text>
</view>
</view>
<text class="settings-arrow"></text>
</view>
</view>
</view>
<view class="logout-btn" @click="logout">退出登录</view>
</view>
<view class="quick-card">
<view class="quick-title">快捷功能</view>
<view class="quick-grid">
<view class="quick-item" v-for="action in quickActions" :key="action.label" @click="handleQuickAction(action)">
<view class="quick-icon">{{ action.icon }}</view>
<text class="quick-label">{{ action.label }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import request from '@/api/request.js';
import uploadFile from '@/api/upload.js';
export default {
data() {
return {
userInfo: {},
currentTab: 0,
signInfo: {
signedToday: false,
continuousDays: 0,
totalPoints: 0,
todayReward: 0,
nextRewardText: '',
monthLabel: '',
monthSignedDays: 0
},
signCalendar: [],
signLoading: false,
weekdayLabels: ['日', '一', '二', '三', '四', '五', '六'],
stats: [
{ icon: '🎁', value: '1280', label: '当前积分', color: '#7B43FF' },
{ icon: '🛍️', value: '23', label: '总订单数', color: '#2877FF' },
{ icon: '📈', value: '¥321', label: '累计省钱', color: '#26B95A' },
{ icon: '👥', value: '8', label: '团队人数', color: '#FF7A00' }
],
tabs: ['概览', '订单', '收藏', '成就', '设置'],
monthlyData: [
{ label: '本月订单', value: '5单' },
{ label: '本月消费', value: '¥450.00', color: '#26B95A' },
{ label: '分享次数', value: '18次', color: '#7B43FF' },
{ label: '分销佣金', value: '¥568.00', color: '#FF7A00' }
],
quickActions: [
{ icon: '💳', label: '我的钱包' },
{ icon: '📍', label: '地址管理', path: '/pages/address/index' },
{ icon: '🤝', label: '邀请用户', path: '/pages/inviteUsers/index' },
{ icon: '🔔', label: '消息通知' },
{ icon: '❓', label: '帮助中心' }
],
favorites: [
{
title: '经典玫瑰香水30ml',
rating: '4.8',
count: 1250,
price: '¥199',
origin: '¥299'
}
],
achievements: [
{
title: '拼团达人',
desc: '成功参与10次拼团',
progress: 80,
progressText: '8/10',
reward: '专属优惠券',
icon: '👥',
iconBg: '#f3f4f8',
trophy: '',
highlight: false
},
{
title: '分享达人',
desc: '分享商品20次',
progress: 90,
progressText: '18/20',
reward: '500积分',
icon: '🔗',
iconBg: '#f3f4f8',
trophy: '',
highlight: false
},
{
title: '消费达人',
desc: '累计消费满2000元',
progress: 100,
progressText: '',
reward: 'VIP会员体验',
icon: '🛍️',
iconBg: '#ffeeb7',
trophy: '🏆',
highlight: true
}
],
accountSettings: [
{ icon: '👤', title: '个人信息', desc: '姓名、手机、邮箱' },
{ icon: '🛡️', title: '账户安全', desc: '密码、支付设置' },
{ icon: '🔔', title: '通知设置', desc: '消息推送偏好' }
],
otherSettings: [
{ icon: '📄', title: '隐私政策', desc: '查看隐私条款' },
{ icon: '❓', title: '帮助中心', desc: '常见问题解答' }
],
orders: [
{
no: 'ORD20250402001',
type: '拼团订单',
status: '已完成',
statusClass: 'success',
date: '2025-04-02',
product: '经典玫瑰香水30ml',
spec: '¥199 × 1',
amount: '¥199.00',
actions: [{ label: '查看详情' }, { label: '再次购买' }]
},
{
no: 'ORD20250401001',
type: '秒杀订单',
status: '待收货',
statusClass: 'warning',
date: '2025-04-01',
product: '小样体验包',
spec: '¥1 × 1',
amount: '¥1.00',
actions: [{ label: '查看详情' }]
},
{
no: 'ORD20250328001',
type: '普通订单',
status: '待付款',
statusClass: 'pending',
date: '2025-03-28',
product: '薰衣草精油礼盒',
spec: '¥299 × 1',
amount: '¥299.00',
actions: [{ label: '查看详情' }]
}
]
};
},
computed: {
avatarUrl() {
// 如果有头像URL且不为空则使用头像URL否则使用默认头像
if (this.userInfo && this.userInfo.avatar_url && this.userInfo.avatar_url.trim() !== '') {
return this.userInfo.avatar_url;
}
// 默认头像使用base64编码的SVG头像占位符预编码兼容所有平台
// 这是一个圆形灰色背景,带有用户图标的默认头像
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYwIiBoZWlnaHQ9IjE2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSI4MCIgY3k9IjgwIiByPSI4MCIgZmlsbD0iI2U1ZTdlYSIvPjxjaXJjbGUgY3g9IjgwIiBjeT0iNjUiIHI9IjI1IiBmaWxsPSIjOWZhMGFmIi8+PHBhdGggZD0iTSA4MCAxMDAgUSA1MCAxMDAgNTAgMTMwIEwgNTAgMTYwIEwgMTEwIDE2MCBMIDExMCAxMzAgUSAxMTAgMTAwIDgwIDEwMCBaIiBmaWxsPSIjOWZhMGFmIi8+PC9zdmc+';
}
},
onShow: function () {
const token = wx.getStorageSync('access_token');
if (!token) {
uni.navigateTo({
url: '/pages/login/index'
});
return;
} else {
this.loadProfile();
this.loadSignInfo();
this.loadOrders();
}
},
methods: {
handleImageError(e) {
// 图片加载失败时,使用默认头像
console.log('头像加载失败,使用默认头像', e);
},
loadProfile() {
try {
request('xcx/get_user_info', 'GET').then(res => {
this.userInfo = res
});
} catch (error) {
console.warn('加载个人信息失败', error);
}
},
loadOrders() {
try {
request('xcx/orders', 'GET', {
page: 1,
page_size: 3
}).then(res => {
// 处理返回数据
let orderList = [];
if (Array.isArray(res)) {
orderList = res;
} else if (res.list && Array.isArray(res.list)) {
orderList = res.list;
} else if (res.data && Array.isArray(res.data)) {
orderList = res.data;
}
// 只取前三条
orderList = orderList.slice(0, 3);
// 转换为页面需要的格式
this.orders = orderList.map(order => {
// 获取第一个商品信息
const firstItem = (order.items && order.items.length > 0) ? order.items[0] : order;
// 格式化状态
const statusMap = {
'pending': { text: '待付款', class: 'pending' },
'paid': { text: '待发货', class: 'warning' },
'shipped': { text: '待收货', class: 'warning' },
'completed': { text: '已完成', class: 'success' },
'cancelled': { text: '已取消', class: 'cancelled' },
'refunded': { text: '已退款', class: 'cancelled' }
};
const statusInfo = statusMap[order.status] || { text: order.status || '未知', class: '' };
// 格式化价格
const formatPrice = (price) => {
if (!price && price !== 0) return '0.00';
if (price < 1000) return parseFloat(price).toFixed(2);
return (price / 100).toFixed(2);
};
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const year = date.getFullYear();
const month = this.padZero(date.getMonth() + 1);
const day = this.padZero(date.getDate());
return `${year}-${month}-${day}`;
};
// 获取操作按钮
const getActions = (status) => {
if (status === 'pending') {
return [{ label: '查看详情' }, { label: '立即付款' }];
} else if (status === 'shipped') {
return [{ label: '查看详情' }, { label: '确认收货' }];
} else if (status === 'completed') {
return [{ label: '查看详情' }, { label: '再次购买' }];
}
return [{ label: '查看详情' }];
};
return {
no: order.order_no || order.no || order.id,
type: order.order_type || '普通订单',
status: statusInfo.text,
statusClass: statusInfo.class,
date: formatDate(order.created_at || order.date),
product: firstItem.product_name || firstItem.name || order.product_name || '商品',
spec: firstItem.sku_name || firstItem.spec || `${formatPrice(firstItem.price || order.price)} × ${firstItem.quantity || order.quantity || 1}`,
amount: `¥${formatPrice(order.final_price || order.total_price || order.amount)}`,
actions: getActions(order.status)
};
});
}).catch(error => {
console.warn('加载订单列表失败', error);
// 失败时保持默认数据或清空
this.orders = [];
});
} catch (error) {
console.warn('获取订单列表异常', error);
this.orders = [];
}
},
loadSignInfo() {
try {
return request('xcx/user_checkins', 'GET', {
time_month: this.getMonthLabel()
}).then(res => {
const info = res || {};
// 处理签到列表,提取日期
const checkinList = info.list || [];
const monthLabel = this.getMonthLabel();
// 判断今天是否已签到
const today = new Date();
const todayStr = `${today.getFullYear()}-${this.padZero(today.getMonth() + 1)}-${this.padZero(today.getDate())}`;
const signedToday = checkinList.some(item => {
if (!item.checkin_time) return false;
// 从 "2025-11-23 14:51:27" 提取日期部分 "2025-11-23"
const dateStr = item.checkin_time.split(' ')[0];
return dateStr === todayStr;
});
const calendar = this.buildMonthlyCalendar(checkinList, monthLabel);
this.signCalendar = calendar;
const updatedSignInfo = {
...this.signInfo,
signedToday,
continuousDays: info.continuous_days || 0,
totalPoints: info.total_points || 0,
todayReward: info.today_reward || 0,
nextRewardText: info.next_reward_text || '',
monthLabel,
monthSignedDays: calendar.filter(day => day.signed && !day.isPlaceholder).length
};
this.signInfo = updatedSignInfo;
if (this.stats && this.stats.length) {
const newPoints = updatedSignInfo.totalPoints;
this.$set(this.stats, 0, {
...this.stats[0],
value: newPoints ? String(newPoints) : this.stats[0].value
});
}
}).catch(error => {
console.warn('加载签到信息失败', error);
this.signCalendar = this.buildMonthlyCalendar();
this.signInfo = {
...this.signInfo,
signedToday: false,
monthLabel: this.getMonthLabel(),
monthSignedDays: 0
};
});
} catch (error) {
console.warn('获取签到状态异常', error);
this.signCalendar = this.buildMonthlyCalendar();
this.signInfo = {
...this.signInfo,
signedToday: false,
monthLabel: this.getMonthLabel(),
monthSignedDays: 0
};
}
},
buildMonthlyCalendar(list = [], referenceMonth = '') {
const today = new Date();
const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate());
let year = today.getFullYear();
let month = today.getMonth() + 1;
if (referenceMonth && /^\d{4}-\d{2}$/.test(referenceMonth)) {
const parts = referenceMonth.split('-');
year = parseInt(parts[0], 10) || year;
month = parseInt(parts[1], 10) || month;
}
const firstDay = new Date(year, month - 1, 1);
const totalDays = new Date(year, month, 0).getDate();
const prefixDays = firstDay.getDay();
// 从签到列表中提取日期,构建日期映射表
// list 格式: [{ checkin_time: "2025-11-23 14:51:27" }, ...]
const signedDates = new Set();
if (Array.isArray(list)) {
list.forEach(item => {
if (item.checkin_time) {
// 从 "2025-11-23 14:51:27" 提取日期部分 "2025-11-23"
const dateStr = item.checkin_time.split(' ')[0];
if (dateStr && /^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
signedDates.add(dateStr);
}
}
});
}
const calendar = [];
for (let i = 0; i < prefixDays; i++) {
calendar.push({ label: '', isPlaceholder: true });
}
for (let day = 1; day <= totalDays; day++) {
const dateStr = `${year}-${this.padZero(month)}-${this.padZero(day)}`;
const targetDate = new Date(year, month - 1, day);
const isToday = this.isSameDate(targetDate, todayStart);
const isFuture = targetDate.getTime() > todayStart.getTime();
const signed = signedDates.has(dateStr);
calendar.push({
label: day,
date: dateStr,
signed,
isToday,
isFuture,
isPlaceholder: false,
missed: !signed && !isFuture && !isToday
});
}
while (calendar.length % 7 !== 0) {
calendar.push({ label: '', isPlaceholder: true });
}
return calendar;
},
getMonthLabel() {
const today = new Date();
return `${today.getFullYear()}-${this.padZero(today.getMonth() + 1)}`;
},
padZero(num) {
return num < 10 ? `0${num}` : `${num}`;
},
isSameDate(dateA, dateB) {
return (
dateA.getFullYear() === dateB.getFullYear() &&
dateA.getMonth() === dateB.getMonth() &&
dateA.getDate() === dateB.getDate()
);
},
async handleSignIn() {
if (this.signInfo.signedToday || this.signLoading) {
return;
}
this.signLoading = true;
try {
const res = await request('xcx/user_checkin', 'POST');
const rewardText = res?.message || '签到成功';
uni.showToast({
title: rewardText,
icon: 'success'
});
await this.loadSignInfo();
} catch (error) {
console.error('签到失败', error);
uni.showToast({
title: '签到失败,请稍后再试',
icon: 'none'
});
} finally {
this.signLoading = false;
}
},
uploadAvatar() {
console.log('uploadAvatar 被点击');
uni.chooseImage({
count: 1,
mediaType: ['image'],
sizeType: ['compressed', 'original'],
sourceType: ['album', 'camera'],
success: (res) => {
console.log('选择图片成功', res);
const tempFilePaths = res.tempFilePaths;
if (tempFilePaths && tempFilePaths.length > 0) {
uni.showLoading({
title: '上传中...'
});
uploadFile(tempFilePaths[0]).then((res) => {
console.log('上传成功', res);
this.userInfo.avatar_url = res;
request('xcx/set_user_info', 'POST', {
avatar_url: res,
username: this.userInfo.username,
sex: this.userInfo.sex,
mobile: this.userInfo.mobile,
}).then(res => {
uni.hideLoading({
title: '上传成功...'
});
console.log('设置头像成功', res);
}).catch((err) => {
uni.hideLoading({
title: '上传失败...'
});
console.error('设置头像失败', err);
})
});
}
},
fail: (err) => {
console.error('选择图片失败', err);
uni.showToast({
title: '选择图片失败',
icon: 'none'
});
}
});
},
handleTabClick(index) {
this.currentTab = index;
},
navigateTo(url) {
uni.navigateTo({
url: url
});
},
handleQuickAction(action) {
if (action.path) {
this.navigateTo(action.path);
return;
}
this.$nextTick(() => {
uni.showToast({
title: '功能开发中',
icon: 'none'
});
});
},
handleStatCardClick(item) {
// 点击"当前积分"卡片时跳转到积分流水记录页面
if (item.label === '当前积分') {
this.navigateTo('/pages/pointsLog/index');
}
if (item.label === '总订单数') {
this.navigateTo('/pages/order/index');
}
},
toEditInfoPage() {
uni.navigateTo({
url: '/pages/my/editInfo/index'
});
},
logout() {
wx.removeStorageSync('access_token');
uni.reLaunch({
url: '/pages/login/index'
});
}
}
};
</script>
<style scoped lang="scss">
.page {
min-height: 100vh;
/* padding: 24rpx; */
background-color: #fff;
display: flex;
flex-direction: column;
gap: 24rpx;
padding-bottom: 24rpx;
}
.profile-card {
background-color: #ffffff;
/* border-radius: 24rpx; */
padding-bottom: 24rpx;
}
.signin-card {
margin: 0 24rpx;
padding: 32rpx;
background: linear-gradient(120deg, #f6f1ff 0%, #ffeef5 100%);
border-radius: 28rpx;
box-shadow: 0 16rpx 32rpx rgba(139, 64, 255, 0.12);
display: flex;
flex-direction: column;
gap: 24rpx;
}
.signin-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.signin-headline {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.signin-title {
font-size: 36rpx;
font-weight: 700;
color: #33195d;
}
.signin-desc {
font-size: 26rpx;
color: #7b43ff;
}
.signin-badge {
padding: 12rpx 28rpx;
border-radius: 999rpx;
background: rgba(255, 60, 141, 0.12);
color: #ff3c8d;
font-size: 26rpx;
font-weight: 600;
}
.signin-calendar {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.signin-calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.signin-month {
font-size: 32rpx;
font-weight: 600;
color: #33195d;
}
.signin-month-summary {
font-size: 24rpx;
color: #7b43ff;
}
.signin-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
font-size: 24rpx;
color: #7b43ff;
text-align: center;
}
.signin-weekday {
opacity: 0.9;
}
.signin-calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 16rpx 12rpx;
}
.signin-date {
height: 80rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(255, 255, 255, 0.7);
border: 2rpx dashed rgba(139, 64, 255, 0.16);
color: #4a2d71;
font-size: 28rpx;
}
.signin-date.placeholder {
background: transparent;
border-color: transparent;
}
.signin-date.signed {
background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%);
color: #fff;
border-style: solid;
}
.signin-date.today {
border-color: #ff3c8d;
box-shadow: 0 0 0 3rpx rgba(255, 60, 141, 0.3);
}
.signin-date.missed {
border-style: solid;
border-color: rgba(255, 60, 141, 0.3);
color: rgba(51, 25, 93, 0.4);
}
.signin-date-number {
font-weight: 600;
}
.signin-footer {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.signin-stats {
display: flex;
gap: 32rpx;
}
.signin-stat {
flex: 1;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 24rpx;
padding: 16rpx;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.signin-stat-value {
font-size: 36rpx;
font-weight: 700;
color: #33195d;
}
.signin-stat-label {
font-size: 24rpx;
color: #7b43ff;
}
.signin-btn {
width: 100%;
// padding: 24rpx 0;
border-radius: 999rpx;
background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%);
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
text-align: center;
}
.signin-btn.disabled {
opacity: 0.6;
}
.signin-tip {
font-size: 24rpx;
color: #7b43ff;
text-align: center;
}
.profile-header {
background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%);
/* border-radius: 24rpx 24rpx 0 0; */
padding: 40rpx 32rpx;
display: flex;
align-items: flex-start;
gap: 24rpx;
color: #ffffff;
position: relative;
}
.avatar-wrapper {
position: relative;
}
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #ffffff;
border: 4rpx solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 0;
image {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.avatar-upload {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background-color: #ffffff;
color: #8b40ff;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
z-index: 10;
bottom: 0;
right: 0;
font-size: 32rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
pointer-events: auto;
cursor: pointer;
}
.profile-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.title-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.name {
font-size: 40rpx;
font-weight: 700;
}
.vip-badge {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 999rpx;
padding: 8rpx 20rpx;
font-size: 24rpx;
display: flex;
align-items: center;
}
.vip-badge text {
color: #ffe178;
font-weight: 600;
}
.meta-row {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 24rpx;
opacity: 0.88;
}
.meta-item {
color: rgba(255, 255, 255, 0.9);
}
.edit-btn {
position: absolute;
right: 32rpx;
top: 40rpx;
padding: 10rpx 24rpx;
border-radius: 999rpx;
background-color: rgba(255, 255, 255, 0.15);
border: 2rpx solid rgba(255, 255, 255, 0.5);
font-size: 24rpx;
}
.stats-grid {
padding: 32rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
}
.stat-card {
background-color: #f9f9ff;
border-radius: 24rpx;
padding: 32rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
box-shadow: inset 0 0 0 2rpx rgba(139, 64, 255, 0.05);
}
.stat-icon {
font-size: 44rpx;
}
.stat-value {
font-size: 44rpx;
font-weight: 700;
}
.stat-label {
font-size: 26rpx;
color: #5f5f5f;
}
.tabs {
display: flex;
background-color: #ffffff;
border-radius: 999rpx;
padding: 12rpx;
margin: 0 24rpx;
gap: 12rpx;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
}
.tab-item {
flex: 1;
text-align: center;
padding: 16rpx 0;
border-radius: 999rpx;
font-size: 28rpx;
color: #757c8e;
}
.tab-item.active {
background: linear-gradient(135deg, #8b40ff 0%, #ff3c8d 100%);
color: #ffffff;
font-weight: 600;
}
.monthly-card {
background-color: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
margin: 0 24rpx;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
display: flex;
flex-direction: column;
gap: 24rpx;
}
.monthly-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1a1a;
}
.monthly-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.monthly-item {
display: flex;
justify-content: space-between;
font-size: 28rpx;
color: #404040;
}
.monthly-label {
color: #757c8e;
}
.monthly-value {
font-weight: 600;
}
.quick-card {
background-color: #ffffff;
border-radius: 24rpx;
padding: 32rpx;
margin: 0 24rpx;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
display: flex;
flex-direction: column;
gap: 32rpx;
}
.quick-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1a1a;
}
.quick-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
}
.quick-item {
background-color: #f6f8ff;
border-radius: 24rpx;
padding: 32rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
border: 2rpx solid rgba(79, 116, 255, 0.1);
box-shadow: 0 6rpx 18rpx rgba(79, 116, 255, 0.08);
}
.quick-icon {
font-size: 48rpx;
color: #1a274c;
}
.quick-label {
font-size: 28rpx;
color: #1a274c;
font-weight: 500;
}
.favorites-card {
margin: 0 24rpx;
padding: 24rpx 24rpx 36rpx;
background-color: #ffffff;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
border-radius: 24rpx;
}
.favorites-wrapper {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.favorite-item {
background-color: #f6f8ff;
border-radius: 24rpx;
padding: 24rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
border: 2rpx solid rgba(79, 116, 255, 0.12);
box-shadow: inset 0 0 0 2rpx rgba(79, 116, 255, 0.04);
}
.favorite-thumb {
width: 100%;
height: 400rpx;
border-radius: 16rpx;
background-color: #eef2ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
color: #b3bac9;
}
.favorite-info {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.favorite-top {
display: flex;
justify-content: space-between;
align-items: center;
}
.favorite-title {
font-size: 30rpx;
color: #273248;
font-weight: 600;
}
.favorite-heart {
width: 48rpx;
height: 48rpx;
border-radius: 12rpx;
background-color: #ffffff;
box-shadow: 0 6rpx 18rpx rgba(79, 116, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #273248;
}
.favorite-meta {
display: flex;
align-items: center;
gap: 12rpx;
font-size: 24rpx;
color: #8c94a3;
}
.favorite-rating {
color: #ffb400;
}
.favorite-price {
display: flex;
align-items: baseline;
gap: 12rpx;
}
.favorite-current {
font-size: 32rpx;
color: #ff2e2e;
font-weight: 700;
}
.favorite-origin {
font-size: 26rpx;
color: #b3bac9;
text-decoration: line-through;
}
.settings-card {
margin: 0 24rpx;
padding: 24rpx;
background-color: #ffffff;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
border-radius: 24rpx;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.settings-section {
display: flex;
flex-direction: column;
gap: 24rpx;
background-color: #f8faff;
padding: 24rpx;
border-radius: 24rpx;
border: 2rpx solid rgba(79, 116, 255, 0.08);
box-shadow: inset 0 0 0 2rpx rgba(79, 116, 255, 0.04);
}
.settings-section-title {
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
}
.settings-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.settings-item {
background-color: #ffffff;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border: 2rpx solid rgba(79, 116, 255, 0.1);
box-shadow: 0 8rpx 18rpx rgba(79, 116, 255, 0.06);
}
.settings-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.settings-icon {
width: 72rpx;
height: 72rpx;
border-radius: 20rpx;
background-color: #f0f4ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 34rpx;
color: #4f74ff;
}
.settings-text {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.settings-title {
font-size: 28rpx;
color: #273248;
font-weight: 600;
}
.settings-desc {
font-size: 24rpx;
color: #8c94a3;
}
.settings-arrow {
font-size: 36rpx;
color: #c4cad6;
}
.logout-btn {
margin-top: 8rpx;
padding: 28rpx 0;
text-align: center;
border-radius: 20rpx;
border: 2rpx solid rgba(255, 99, 132, 0.2);
color: #ff4d6d;
font-size: 28rpx;
background-color: rgba(255, 99, 132, 0.08);
}
.achievements-card {
margin: 0 24rpx;
padding: 24rpx;
background-color: #ffffff;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
border-radius: 24rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
}
.achievement-item {
background-color: #f8faff;
border-radius: 24rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
border: 2rpx solid rgba(79, 116, 255, 0.08);
box-shadow: inset 0 0 0 2rpx rgba(79, 116, 255, 0.04);
}
.achievement-item.highlight {
background: linear-gradient(135deg, rgba(255, 238, 183, 0.55), rgba(255, 255, 255, 0.9));
border: 2rpx solid rgba(255, 200, 64, 0.5);
}
.achievement-left {
display: flex;
align-items: center;
gap: 24rpx;
flex: 1;
}
.achievement-icon {
width: 100rpx;
height: 100rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
}
.achievement-icon-text {
font-size: 40rpx;
}
.achievement-info {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.achievement-title {
font-size: 30rpx;
color: #273248;
font-weight: 600;
}
.achievement-desc {
font-size: 24rpx;
color: #8c94a3;
}
.achievement-progress {
display: flex;
align-items: center;
gap: 16rpx;
margin-top: 12rpx;
}
.achievement-progress-bar {
flex: 1;
height: 18rpx;
border-radius: 12rpx;
background-color: rgba(79, 116, 255, 0.15);
overflow: hidden;
}
.achievement-progress-fill {
height: 100%;
border-radius: 12rpx;
background: linear-gradient(135deg, #fccc75, #f5b34b);
}
.achievement-progress-text {
font-size: 24rpx;
color: #273248;
}
.achievement-reward {
margin-top: 16rpx;
font-size: 24rpx;
color: #f5b34b;
padding: 16rpx 20rpx;
background-color: rgba(255, 190, 60, 0.15);
border-radius: 16rpx;
}
.achievement-trophy {
font-size: 44rpx;
color: #f5b34b;
align-self: flex-start;
}
.orders-card {
background-color: #ffffff;
padding: 32rpx;
margin: 0 24rpx;
box-shadow: 0 12rpx 24rpx 0 rgba(123, 67, 255, 0.08);
display: flex;
flex-direction: column;
gap: 32rpx;
}
.orders-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.orders-title {
font-size: 32rpx;
font-weight: 600;
color: #1a1a1a;
}
.orders-more {
font-size: 24rpx;
color: #4f5f7a;
padding: 12rpx 28rpx;
border-radius: 999rpx;
background-color: #f2f7ff;
}
.order-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.order-item {
background-color: #ffffff;
border-radius: 24rpx;
border: 2rpx solid rgba(79, 116, 255, 0.12);
padding: 32rpx;
display: flex;
flex-direction: column;
gap: 24rpx;
box-shadow: 0 10rpx 26rpx rgba(79, 116, 255, 0.06);
}
.order-top {
display: flex;
align-items: center;
gap: 16rpx;
}
.order-no {
font-size: 26rpx;
color: #212a3a;
font-weight: 600;
flex-shrink: 0;
}
.order-tags {
display: flex;
align-items: center;
gap: 12rpx;
}
.order-tag {
font-size: 22rpx;
padding: 8rpx 20rpx;
border-radius: 999rpx;
}
.order-tag.type {
background-color: rgba(79, 116, 255, 0.12);
color: #4f74ff;
}
.order-tag.status {
color: #ffffff;
}
.order-tag.status.success {
background-color: #6cd198;
}
.order-tag.status.warning {
background-color: #c79bff;
}
.order-tag.status.pending {
background-color: #f7c96b;
color: #5a3b00;
}
.order-date {
margin-left: auto;
font-size: 24rpx;
color: #8c94a3;
}
.order-body {
display: flex;
gap: 24rpx;
}
.order-thumb {
width: 120rpx;
height: 120rpx;
border-radius: 16rpx;
background-color: #eef2ff;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #8c94a3;
}
.order-info {
display: flex;
flex-direction: column;
gap: 12rpx;
justify-content: center;
}
.order-name {
font-size: 30rpx;
color: #273248;
font-weight: 600;
}
.order-spec {
font-size: 24rpx;
color: #8c94a3;
}
.order-footer {
display: flex;
align-items: center;
gap: 24rpx;
}
.order-amount {
display: flex;
align-items: center;
gap: 12rpx;
}
.amount-label {
font-size: 24rpx;
color: #8c94a3;
}
.amount-value {
font-size: 32rpx;
color: #ff2e2e;
font-weight: 700;
}
.order-actions {
margin-left: auto;
display: flex;
gap: 20rpx;
}
.order-action {
padding: 12rpx 28rpx;
border-radius: 999rpx;
border: 2rpx solid rgba(79, 116, 255, 0.35);
font-size: 24rpx;
color: #4f5f7a;
}
.order-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 32rpx;
gap: 24rpx;
}
.order-empty-icon {
font-size: 80rpx;
opacity: 0.5;
}
.order-empty-text {
font-size: 28rpx;
color: #8c94a3;
}
</style>