wx-shop/pages/my/index.vue
@zuopngfei 26563409c9 wewe
2025-11-27 18:18:37 +08:00

1755 lines
42 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.order_id || order.id">
<view class="order-top">
<text class="order-no">{{ order.order_id || '-' }}</text>
<view class="order-tags">
<!-- <view class="order-tag type">{{ order.typeLabel }}</view> -->
<view class="order-tag status" :class="order.statusClass">{{ order.statusText }}</view>
</view>
<text class="order-date">{{ order.displayDate }}</text>
</view>
<view class="order-body">
<view class="order-thumb">
<image v-if="order.productImage" :src="order.productImage" mode="aspectFill"></image>
<text v-else>60x60</text>
</view>
<view class="order-info">
<text class="order-name">{{ order.productName }}</text>
<text class="order-spec">{{ order.productSpec }}</text>
</view>
</view>
<view class="order-footer">
<view class="order-amount">
<text class="amount-label">订单总额</text>
<text class="amount-value">¥{{ order.amountText }}</text>
</view>
<view class="order-actions">
<view class="order-action" v-for="action in order.actions" :key="action.label" @click="handleOrderAction(action, order)">
查看详情
</view>
<!-- <view class="order-action" v-for="action in order.actions" :key="action.label" @click="handleOrderAction(action, order)">
{{ 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: []
};
},
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: {
async handleOrderAction(action, order) {
if (!order) return;
if (action.label === '查看详情') {
const orderId = order.order_id || order.id || order.order_no || order.no;
if (!orderId) {
console.warn('缺少订单ID无法跳转详情', order);
return;
}
uni.navigateTo({
url: `/pages/order/detail?id=${orderId}`
});
} else if (action.label === '加入购物车') {
const skuList = order.product_list || order.productList || [];
if (!Array.isArray(skuList) || !skuList.length) {
console.warn('订单中没有商品列表,无法加入购物车', order);
return;
}
for (const sku of skuList) {
try {
await request('xcx/cart', 'POST', {
product_id: sku.product_id || order.product_id,
sku_id: sku.sku_id,
quantity: sku.quantity || 1
});
} catch (error) {
console.error('加入购物车失败', error);
}
}
uni.showToast({
title: '已加入购物车',
icon: 'success'
});
}
},
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);
// 订单状态映射后端1待支付 2支付失败 3待发货 4待收货 5已完成 6已取消 7已退款
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' },
// 数字状态(你提供的规范)
'1': { text: '待支付', class: 'pending' },
'2': { text: '支付失败', class: 'cancelled' },
'3': { text: '待发货', class: 'warning' },
'4': { text: '待收货', class: 'warning' },
'5': { text: '已完成', class: 'success' },
'6': { text: '已取消', class: 'cancelled' },
'7': { text: '已退款', class: 'cancelled' }
};
const formatPrice = (price) => {
if (price === null || price === undefined || price === '') return '0.00';
const numericPrice = Number(price);
if (Number.isNaN(numericPrice)) return '0.00';
// if (numericPrice >= 1000 && numericPrice % 100 === 0) {
// return (numericPrice / 100).toFixed(2);
// }
return (numericPrice / 100).toFixed(2);
};
const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr.replace(/-/g, '/'));
if (Number.isNaN(date.getTime())) {
return dateStr.split(' ')[0] || 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) => {
const statusNumber = typeof status === 'number' ? status : parseInt(status, 10);
// 1: 待支付
if (status === 'pending' || statusNumber === 1) {
return [{ label: '查看详情' }, { label: '立即付款' }];
}
// 4: 待收货
if (status === 'shipped' || statusNumber === 4) {
return [{ label: '查看详情' }, { label: '确认收货' }];
}
// 5: 已完成
if (status === 'completed' || statusNumber === 5) {
return [{ label: '查看详情' }, { label: '加入购物车' }];
}
// 其他状态统一只有「查看详情」
return [{ label: '查看详情' }];
};
const getFirstProduct = (order) => {
if (Array.isArray(order.product_list) && order.product_list.length) {
return order.product_list[0];
}
if (Array.isArray(order.productList) && order.productList.length) {
return order.productList[0];
}
if (Array.isArray(order.items) && order.items.length) {
return order.items[0];
}
return order || {};
};
const getFirstValidNumber = (...values) => {
for (let i = 0; i < values.length; i++) {
const value = values[i];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return 0;
};
// 转换为页面需要的格式
this.orders = orderList.map(order => {
const firstItem = getFirstProduct(order);
const orderId = order.order_id || order.orderId || order.order_no || order.no || order.id || '';
// 兼容多种后端状态字段status / order_status / orderStatus / state / order_state
const rawStatus =
order.status !== undefined && order.status !== null
? order.status
: (order.order_status !== undefined && order.order_status !== null
? order.order_status
: (order.orderStatus !== undefined && order.orderStatus !== null
? order.orderStatus
: (order.state !== undefined && order.state !== null
? order.state
: (order.order_state !== undefined && order.order_state !== null
? order.order_state
: ''))));
const statusKey = rawStatus !== '' && rawStatus !== undefined && rawStatus !== null ? String(rawStatus) : '';
const statusInfo = statusMap[statusKey] || statusMap[rawStatus] || {
text: rawStatus === '' || rawStatus === undefined || rawStatus === null ? '未知状态' : String(rawStatus),
class: ''
};
const quantity = firstItem && firstItem.quantity ? firstItem.quantity : (order.quantity || 1);
const skuLabel = firstItem && (firstItem.sku_name || firstItem.spec) ? (firstItem.sku_name || firstItem.spec) : '';
const specParts = [];
if (skuLabel) {
specParts.push(skuLabel);
}
specParts.push(`x${quantity}`);
const amountSource = getFirstValidNumber(
order.payable_amount,
order.total_amount,
order.final_price,
order.total_price,
order.amount
);
return {
...order,
order_id: orderId,
typeLabel: (firstItem && firstItem.category_name) || order.order_type || '普通订单',
statusText: statusInfo.text,
statusClass: statusInfo.class,
displayDate: formatDate(order.created_at || order.date),
productName: (firstItem && (firstItem.product_name || firstItem.name)) || order.product_name || '商品',
productSpec: specParts.join(' · '),
productImage: (firstItem && (firstItem.product_main_image_url || firstItem.image_url)) || '',
amountText: formatPrice(amountSource),
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-tag.status.cancelled {
background-color: #c9ced8;
color: #ffffff;
}
.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-thumb image {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.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>