qweew
This commit is contained in:
parent
c03d3149d2
commit
0dbfaea026
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name" : "香水有毒",
|
"name" : "香水有毒",
|
||||||
"appid" : "",
|
"appid" : "__UNI__E8ED88D",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode" : "100",
|
"versionCode" : "100",
|
||||||
|
|||||||
36
pages.json
36
pages.json
@ -48,6 +48,42 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "用户登录"
|
"navigationBarTitleText": "用户登录"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/shoppingCart/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "购物车"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/shopDetail/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商品详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/create",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "确认订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/pointsLog/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "积分流水"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="shop-list" v-if="searchType == 1">
|
<view class="shop-list" v-if="searchType == 1">
|
||||||
<view class="shop-item" v-for="(item, index) in shopList">
|
<view class="shop-item" v-for="(item, index) in shopList" :key="item.id" @click="goShopDetail(item)">
|
||||||
<view class="shop-image">
|
<view class="shop-image">
|
||||||
<image :src="item.main_image_url"></image>
|
<image :src="item.main_image_url"></image>
|
||||||
<view class="shop-tag flex">
|
<view class="shop-tag flex">
|
||||||
@ -76,25 +76,26 @@
|
|||||||
</view>
|
</view>
|
||||||
</view> -->
|
</view> -->
|
||||||
<view class="sku-title">
|
<view class="sku-title">
|
||||||
{{item.name}}
|
{{ item.name }}
|
||||||
</view>
|
</view>
|
||||||
<view class="sku-description">
|
<view class="sku-description">
|
||||||
{{item.description}}
|
{{ item.description }}
|
||||||
</view>
|
</view>
|
||||||
<view class="sku-rating">
|
<view class="sku-rating">
|
||||||
<text class="iconfont icon-xingxing"></text>
|
<text class="iconfont icon-xingxing"></text>
|
||||||
<text class="sku-rating-num">{{item.rating}}</text>
|
<text class="sku-rating-num">{{ item.rating }}</text>
|
||||||
<text class="sku-rating-text">(156条评价) </text>
|
<text class="sku-rating-text">(156条评价) </text>
|
||||||
<text class="sku-rating-text"> {{item.like_count}} 人喜欢</text>
|
<text class="sku-rating-text"> {{ item.like_count }} 人喜欢</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="sku-price" v-if=" item.skus && item.skus.length > 0">
|
<view class="sku-price" v-if="item.skus && item.skus.length > 0">
|
||||||
<text>¥{{ item.skus[0].price }}</text>
|
<text>¥{{ formatPrice(item.skus[0].price) }}</text>
|
||||||
<text class="original-price">¥{{ item.skus[0].original_price }}</text>
|
<text class="original-price">¥{{ formatPrice(item.skus[0].original_price) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="sku-operation flex">
|
<view class="sku-operation flex">
|
||||||
<view class="sku-num" @click="handelLike(item)">
|
<view class="sku-num" @click.stop="handelLike(item)">
|
||||||
<text v-if="item.is_liked" class="iconfont icon-xin heart-filled"></text>
|
<text v-if="item.is_liked" class="iconfont icon-xin heart-filled"></text>
|
||||||
<text v-else class="iconfont icon-xin1 heart-outline"></text> {{item.like_count}}
|
<text v-else class="iconfont icon-xin1 heart-outline"></text>
|
||||||
|
{{ item.like_count }}
|
||||||
</view>
|
</view>
|
||||||
<view class="">
|
<view class="">
|
||||||
<text class="iconfont icon-zhifeiji1"></text>
|
<text class="iconfont icon-zhifeiji1"></text>
|
||||||
@ -230,8 +231,17 @@ export default {
|
|||||||
this.getShopList()
|
this.getShopList()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 格式化价格
|
||||||
getShopList(){
|
formatPrice(value) {
|
||||||
|
if (!value) {
|
||||||
|
return 0.00;
|
||||||
|
}
|
||||||
|
// 分转换为元
|
||||||
|
value = value / 100;
|
||||||
|
// 保留两位小数
|
||||||
|
return value.toFixed(2);
|
||||||
|
},
|
||||||
|
getShopList() {
|
||||||
request('xcx/products', 'get', {
|
request('xcx/products', 'get', {
|
||||||
page: this.page,
|
page: this.page,
|
||||||
page_size: this.page_size
|
page_size: this.page_size
|
||||||
@ -239,9 +249,9 @@ export default {
|
|||||||
this.shopList = res.list
|
this.shopList = res.list
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async userIsLogin(){
|
async userIsLogin() {
|
||||||
const token = await uni.getStorageSync('access_token')
|
const token = await uni.getStorageSync('access_token')
|
||||||
if(token){
|
if (token) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@ -270,7 +280,13 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
async goShopDetail(item) {
|
||||||
|
await uni.setStorageSync('product_info', item)
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/shopDetail/index?id=${item.id}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,24 +54,27 @@ export default {
|
|||||||
if (!loginRes.code) {
|
if (!loginRes.code) {
|
||||||
throw new Error('获取微信登录code失败');
|
throw new Error('获取微信登录code失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const open_id = (await request('xcx/basic_login', 'post', {code: loginRes.code})).openid
|
||||||
|
|
||||||
// 调用登录接口,传递code和加密数据
|
// 调用登录接口,传递code和加密数据
|
||||||
const loginData = {
|
const loginData = {
|
||||||
code: e.detail.code,
|
code: e.detail.code,
|
||||||
invitation_code: e.detail.encryptedData,
|
invitation_code: '',
|
||||||
|
openid: open_id
|
||||||
// iv: e.detail.iv
|
// iv: e.detail.iv
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await request('xcx/quick_login', 'POST', loginData);
|
const result = await request('xcx/quick_login', 'POST', loginData);
|
||||||
await wx.setStorageSync('access_token', result.token);
|
await wx.setStorageSync('access_token', result.token);
|
||||||
await wx.setStorageSync('is_personal_information_complete', result.is_personal_information_complete);
|
await wx.setStorageSync('is_personal_information_complete', result.is_personal_information_complete);
|
||||||
if (result.is_personal_information_complete) {
|
// if (result.is_personal_information_complete) {
|
||||||
wx.navigateBack();
|
wx.navigateBack();
|
||||||
} else {
|
// } else {
|
||||||
wx.navigateTo({
|
// wx.navigateTo({
|
||||||
url: `/pages/my/editInfo/index`,
|
// url: `/pages/my/editInfo/index`,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
// 保存token
|
// 保存token
|
||||||
// if (result.token) {
|
// if (result.token) {
|
||||||
// wx.setStorageSync('access_token', result.token);
|
// wx.setStorageSync('access_token', result.token);
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
<view class="edit-btn" @click="navigateTo('/pages/my/editInfo/index')">编辑资料</view>
|
<view class="edit-btn" @click="navigateTo('/pages/my/editInfo/index')">编辑资料</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stats-grid">
|
<view class="stats-grid">
|
||||||
<view class="stat-card" v-for="item in stats" :key="item.label">
|
<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>
|
<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-value" :style="{ color: item.color }">{{ item.value }}</text>
|
||||||
<text class="stat-label">{{ item.label }}</text>
|
<text class="stat-label">{{ item.label }}</text>
|
||||||
@ -96,7 +97,7 @@
|
|||||||
<view class="orders-card" v-if="currentTab === 1">
|
<view class="orders-card" v-if="currentTab === 1">
|
||||||
<view class="orders-header">
|
<view class="orders-header">
|
||||||
<text class="orders-title">我的订单</text>
|
<text class="orders-title">我的订单</text>
|
||||||
<view class="orders-more">查看全部</view>
|
<view class="orders-more" @click="navigateTo('/pages/order/index')">查看全部</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="order-list">
|
<view class="order-list">
|
||||||
<view class="order-item" v-for="order in orders" :key="order.no">
|
<view class="order-item" v-for="order in orders" :key="order.no">
|
||||||
@ -127,6 +128,10 @@
|
|||||||
</view>
|
</view>
|
||||||
</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>
|
</view>
|
||||||
<view class="favorites-card" v-if="currentTab === 2">
|
<view class="favorites-card" v-if="currentTab === 2">
|
||||||
@ -370,6 +375,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.loadProfile();
|
this.loadProfile();
|
||||||
this.loadSignInfo();
|
this.loadSignInfo();
|
||||||
|
this.loadOrders();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -386,16 +392,117 @@ export default {
|
|||||||
console.warn('加载个人信息失败', 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() {
|
loadSignInfo() {
|
||||||
try {
|
try {
|
||||||
return request('xcx/sign_in', 'GET').then(res => {
|
return request('xcx/user_checkins', 'GET', {
|
||||||
|
time_month: this.getMonthLabel()
|
||||||
|
}).then(res => {
|
||||||
const info = res || {};
|
const info = res || {};
|
||||||
const monthLabel = info.current_month || this.getMonthLabel();
|
// 处理签到列表,提取日期
|
||||||
const calendar = this.buildMonthlyCalendar(info.month_records || info.recent_records, monthLabel);
|
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;
|
this.signCalendar = calendar;
|
||||||
const updatedSignInfo = {
|
const updatedSignInfo = {
|
||||||
...this.signInfo,
|
...this.signInfo,
|
||||||
signedToday: !!info.signed_today,
|
signedToday,
|
||||||
continuousDays: info.continuous_days || 0,
|
continuousDays: info.continuous_days || 0,
|
||||||
totalPoints: info.total_points || 0,
|
totalPoints: info.total_points || 0,
|
||||||
todayReward: info.today_reward || 0,
|
todayReward: info.today_reward || 0,
|
||||||
@ -416,6 +523,7 @@ export default {
|
|||||||
this.signCalendar = this.buildMonthlyCalendar();
|
this.signCalendar = this.buildMonthlyCalendar();
|
||||||
this.signInfo = {
|
this.signInfo = {
|
||||||
...this.signInfo,
|
...this.signInfo,
|
||||||
|
signedToday: false,
|
||||||
monthLabel: this.getMonthLabel(),
|
monthLabel: this.getMonthLabel(),
|
||||||
monthSignedDays: 0
|
monthSignedDays: 0
|
||||||
};
|
};
|
||||||
@ -425,6 +533,7 @@ export default {
|
|||||||
this.signCalendar = this.buildMonthlyCalendar();
|
this.signCalendar = this.buildMonthlyCalendar();
|
||||||
this.signInfo = {
|
this.signInfo = {
|
||||||
...this.signInfo,
|
...this.signInfo,
|
||||||
|
signedToday: false,
|
||||||
monthLabel: this.getMonthLabel(),
|
monthLabel: this.getMonthLabel(),
|
||||||
monthSignedDays: 0
|
monthSignedDays: 0
|
||||||
};
|
};
|
||||||
@ -443,21 +552,22 @@ export default {
|
|||||||
const firstDay = new Date(year, month - 1, 1);
|
const firstDay = new Date(year, month - 1, 1);
|
||||||
const totalDays = new Date(year, month, 0).getDate();
|
const totalDays = new Date(year, month, 0).getDate();
|
||||||
const prefixDays = firstDay.getDay();
|
const prefixDays = firstDay.getDay();
|
||||||
const recordMap = {};
|
|
||||||
|
// 从签到列表中提取日期,构建日期映射表
|
||||||
|
// list 格式: [{ checkin_time: "2025-11-23 14:51:27" }, ...]
|
||||||
|
const signedDates = new Set();
|
||||||
if (Array.isArray(list)) {
|
if (Array.isArray(list)) {
|
||||||
list.forEach(item => {
|
list.forEach(item => {
|
||||||
const dateStr = item?.date || item?.day || item?.label;
|
if (item.checkin_time) {
|
||||||
if (dateStr) {
|
// 从 "2025-11-23 14:51:27" 提取日期部分 "2025-11-23"
|
||||||
const status =
|
const dateStr = item.checkin_time.split(' ')[0];
|
||||||
item?.checked ??
|
if (dateStr && /^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
||||||
item?.signed ??
|
signedDates.add(dateStr);
|
||||||
(typeof item?.status !== 'undefined' ? item.status === 1 : undefined) ??
|
}
|
||||||
item?.is_signed ??
|
|
||||||
false;
|
|
||||||
recordMap[dateStr] = !!status;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendar = [];
|
const calendar = [];
|
||||||
for (let i = 0; i < prefixDays; i++) {
|
for (let i = 0; i < prefixDays; i++) {
|
||||||
calendar.push({ label: '', isPlaceholder: true });
|
calendar.push({ label: '', isPlaceholder: true });
|
||||||
@ -467,7 +577,7 @@ export default {
|
|||||||
const targetDate = new Date(year, month - 1, day);
|
const targetDate = new Date(year, month - 1, day);
|
||||||
const isToday = this.isSameDate(targetDate, todayStart);
|
const isToday = this.isSameDate(targetDate, todayStart);
|
||||||
const isFuture = targetDate.getTime() > todayStart.getTime();
|
const isFuture = targetDate.getTime() > todayStart.getTime();
|
||||||
const signed = !!recordMap[dateStr];
|
const signed = signedDates.has(dateStr);
|
||||||
calendar.push({
|
calendar.push({
|
||||||
label: day,
|
label: day,
|
||||||
date: dateStr,
|
date: dateStr,
|
||||||
@ -503,7 +613,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.signLoading = true;
|
this.signLoading = true;
|
||||||
try {
|
try {
|
||||||
const res = await request('xcx/sign_in', 'POST');
|
const res = await request('xcx/user_checkin', 'POST');
|
||||||
const rewardText = res?.message || '签到成功';
|
const rewardText = res?.message || '签到成功';
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: rewardText,
|
title: rewardText,
|
||||||
@ -584,6 +694,12 @@ export default {
|
|||||||
icon: 'none'
|
icon: 'none'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
handleStatCardClick(item) {
|
||||||
|
// 点击"当前积分"卡片时跳转到积分流水记录页面
|
||||||
|
if (item.label === '当前积分') {
|
||||||
|
this.navigateTo('/pages/pointsLog/index');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1503,4 +1619,23 @@ export default {
|
|||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #4f5f7a;
|
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>
|
</style>
|
||||||
769
pages/order/create.vue
Normal file
769
pages/order/create.vue
Normal file
@ -0,0 +1,769 @@
|
|||||||
|
<template>
|
||||||
|
<view class="order-page">
|
||||||
|
<!-- 收货地址 -->
|
||||||
|
<view class="address-section" @click="showAddressPicker">
|
||||||
|
<view v-if="selectedAddress" class="address-card">
|
||||||
|
<view class="address-header">
|
||||||
|
<text class="receiver-name">{{ selectedAddress.receiver_name }}</text>
|
||||||
|
<text class="receiver-phone">{{ selectedAddress.phone }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="address-detail">
|
||||||
|
<text class="address-text">{{ formatAddress(selectedAddress) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="address-arrow">
|
||||||
|
<text class="iconfont icon-arrow-right">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="address-empty">
|
||||||
|
<text class="iconfont icon-location">📍</text>
|
||||||
|
<text class="empty-text">请选择收货地址</text>
|
||||||
|
<text class="iconfont icon-arrow-right">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 地址选择弹窗 -->
|
||||||
|
<view class="address-picker-mask" v-if="showAddressList" @click="hideAddressPicker">
|
||||||
|
<view class="address-picker" @click.stop>
|
||||||
|
<view class="picker-header">
|
||||||
|
<text class="picker-title">选择收货地址</text>
|
||||||
|
<text class="picker-close" @click="hideAddressPicker">×</text>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="picker-content">
|
||||||
|
<view
|
||||||
|
class="picker-item"
|
||||||
|
v-for="(item, index) in addressList"
|
||||||
|
:key="item.id || index"
|
||||||
|
@click="chooseAddress(item)"
|
||||||
|
>
|
||||||
|
<view class="picker-item-header">
|
||||||
|
<text class="picker-receiver">{{ item.receiver_name }}</text>
|
||||||
|
<text class="picker-phone">{{ item.phone }}</text>
|
||||||
|
<view class="picker-tag" v-if="Number(item.is_default) === 1">默认</view>
|
||||||
|
</view>
|
||||||
|
<view class="picker-item-detail">
|
||||||
|
<text class="picker-address">{{ formatAddress(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="picker-item-check" v-if="selectedAddress && selectedAddress.id === item.id">
|
||||||
|
<text class="iconfont icon-gou">✓</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-if="addressList.length === 0" class="picker-empty">
|
||||||
|
<text class="empty-text">暂无地址,请先添加</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="picker-footer">
|
||||||
|
<view class="picker-btn" @click="goToAddressManage">管理地址</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品列表 -->
|
||||||
|
<view class="goods-section">
|
||||||
|
<view class="section-title">商品信息</view>
|
||||||
|
<view class="goods-list">
|
||||||
|
<view class="goods-item" v-for="(item, index) in orderItems" :key="index">
|
||||||
|
<view class="goods-image">
|
||||||
|
<image :src="item.product_image || item.main_image_url" mode="aspectFill"></image>
|
||||||
|
</view>
|
||||||
|
<view class="goods-info">
|
||||||
|
<view class="goods-name">{{ item.product_name || item.name }}</view>
|
||||||
|
<view class="goods-spec" v-if="item.sku_name">{{ item.sku_name }}</view>
|
||||||
|
<view class="goods-price-row">
|
||||||
|
<view class="goods-price">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ formatPrice(item.price) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="goods-quantity">x{{ item.quantity }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单备注 -->
|
||||||
|
<view class="remark-section">
|
||||||
|
<view class="section-title">订单备注</view>
|
||||||
|
<textarea
|
||||||
|
class="remark-input"
|
||||||
|
v-model="orderRemark"
|
||||||
|
placeholder="选填,对本次购买的说明(如:请尽快发货)"
|
||||||
|
maxlength="200"
|
||||||
|
auto-height
|
||||||
|
></textarea>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 价格明细 -->
|
||||||
|
<view class="price-section">
|
||||||
|
<view class="price-item">
|
||||||
|
<text class="price-label">商品总价</text>
|
||||||
|
<text class="price-value">¥{{ formatPrice(totalPrice) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="price-item">
|
||||||
|
<text class="price-label">运费</text>
|
||||||
|
<text class="price-value">¥{{ formatPrice(shippingFee) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="price-item total" v-if="discountAmount > 0">
|
||||||
|
<text class="price-label">优惠</text>
|
||||||
|
<text class="price-value discount">-¥{{ formatPrice(discountAmount) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="price-item total">
|
||||||
|
<text class="price-label">实付金额</text>
|
||||||
|
<text class="price-value total-price">¥{{ formatPrice(finalPrice) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部提交栏 -->
|
||||||
|
<view class="order-footer">
|
||||||
|
<view class="footer-total">
|
||||||
|
<text class="total-label">合计:</text>
|
||||||
|
<text class="total-price">¥{{ formatPrice(finalPrice) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="footer-btn" :class="{ disabled: !canSubmit }" @click="submitOrder">
|
||||||
|
{{ submitting ? '提交中...' : '提交订单' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '@/api/request.js';
|
||||||
|
import { fetchAddressList } from '@/api/address.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
orderItems: [], // 订单商品列表
|
||||||
|
selectedAddress: null, // 选中的收货地址
|
||||||
|
addressList: [], // 地址列表
|
||||||
|
showAddressList: false, // 是否显示地址选择弹窗
|
||||||
|
orderRemark: '', // 订单备注
|
||||||
|
shippingFee: 0, // 运费(单位:分)
|
||||||
|
discountAmount: 0, // 优惠金额(单位:分)
|
||||||
|
submitting: false, // 是否正在提交
|
||||||
|
app_id: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 商品总价(单位:分)
|
||||||
|
totalPrice() {
|
||||||
|
let total = 0;
|
||||||
|
this.orderItems.forEach(item => {
|
||||||
|
const price = parseFloat(item.price || 0);
|
||||||
|
const quantity = parseInt(item.quantity || 1);
|
||||||
|
total += price * quantity;
|
||||||
|
});
|
||||||
|
return total;
|
||||||
|
},
|
||||||
|
// 最终支付金额(单位:分)
|
||||||
|
finalPrice() {
|
||||||
|
return Math.max(0, this.totalPrice + this.shippingFee - this.discountAmount);
|
||||||
|
},
|
||||||
|
// 是否可以提交订单
|
||||||
|
canSubmit() {
|
||||||
|
return this.selectedAddress && this.orderItems.length > 0 && !this.submitting;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
// 从购物车传递过来的商品数据
|
||||||
|
if (options.items) {
|
||||||
|
try {
|
||||||
|
this.orderItems = JSON.parse(decodeURIComponent(options.items));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析商品数据失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: '商品数据错误',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载地址列表
|
||||||
|
this.loadAddressList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 格式化价格(分转元)
|
||||||
|
formatPrice(value) {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
// 分转换为元
|
||||||
|
value = value / 100;
|
||||||
|
// 保留两位小数
|
||||||
|
return value.toFixed(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化地址
|
||||||
|
formatAddress(address) {
|
||||||
|
if (!address) return '';
|
||||||
|
const region = [
|
||||||
|
address.province || '',
|
||||||
|
address.city || '',
|
||||||
|
address.district || ''
|
||||||
|
].filter(Boolean).join('');
|
||||||
|
return region + (address.detail_address || '');
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载地址列表
|
||||||
|
async loadAddressList() {
|
||||||
|
try {
|
||||||
|
this.addressList = await fetchAddressList();
|
||||||
|
// 自动选择默认地址
|
||||||
|
const defaultAddress = this.addressList.find(addr => Number(addr.is_default) === 1);
|
||||||
|
if (defaultAddress) {
|
||||||
|
this.selectedAddress = defaultAddress;
|
||||||
|
} else if (this.addressList.length > 0) {
|
||||||
|
// 如果没有默认地址,选择第一个
|
||||||
|
this.selectedAddress = this.addressList[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载地址列表失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 显示地址选择弹窗
|
||||||
|
showAddressPicker() {
|
||||||
|
if (this.addressList.length === 0) {
|
||||||
|
// 如果没有地址,直接跳转到地址管理页面
|
||||||
|
this.goToAddressManage();
|
||||||
|
} else {
|
||||||
|
this.showAddressList = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 隐藏地址选择弹窗
|
||||||
|
hideAddressPicker() {
|
||||||
|
this.showAddressList = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择地址
|
||||||
|
chooseAddress(address) {
|
||||||
|
this.selectedAddress = address;
|
||||||
|
this.hideAddressPicker();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 跳转到地址管理页面
|
||||||
|
goToAddressManage() {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/address/index'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 提交订单
|
||||||
|
async submitOrder() {
|
||||||
|
if (!this.canSubmit) {
|
||||||
|
if (!this.selectedAddress) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请选择收货地址',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitting = true;
|
||||||
|
// 获取小程序的appid
|
||||||
|
if (!this.app_id) {
|
||||||
|
const accountInfo = wx.getAccountInfoSync && wx.getAccountInfoSync();
|
||||||
|
this.app_id = accountInfo && accountInfo.miniProgram && accountInfo.miniProgram.appId
|
||||||
|
? accountInfo.miniProgram.appId
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 构建订单数据
|
||||||
|
const orderData = {
|
||||||
|
app_id: this.app_id,
|
||||||
|
address_id: this.selectedAddress.id,
|
||||||
|
items: this.orderItems.map(item => ({
|
||||||
|
cart_id: item.cart_id,
|
||||||
|
product_id: item.product_id,
|
||||||
|
sku_id: item.sku_id,
|
||||||
|
quantity: item.quantity,
|
||||||
|
price: item.price
|
||||||
|
})),
|
||||||
|
remark: this.orderRemark,
|
||||||
|
total_price: this.totalPrice,
|
||||||
|
shipping_fee: this.shippingFee,
|
||||||
|
discount_amount: this.discountAmount,
|
||||||
|
final_price: this.finalPrice
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调用创建订单接口
|
||||||
|
const result = await request('xcx/order', 'POST', orderData);
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '订单提交成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 跳转到订单详情或订单列表页面
|
||||||
|
setTimeout(() => {
|
||||||
|
// 根据实际路由调整
|
||||||
|
uni.redirectTo({
|
||||||
|
url: `/pages/order/detail?id=${result.order_id || result.id}`
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交订单失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '提交订单失败,请重试',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.submitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
// 页面显示时,重新加载地址列表(从地址管理页面返回时)
|
||||||
|
this.loadAddressList();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 收货地址区域 */
|
||||||
|
.address-section {
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-name {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-phone {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-detail {
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-arrow .iconfont {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-empty .iconfont {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 商品区域 */
|
||||||
|
.goods-section {
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-image {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-image image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-spec {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
color: #e7000b;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-quantity {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 备注区域 */
|
||||||
|
.remark-section {
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remark-input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 120rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 20rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 价格明细 */
|
||||||
|
.price-section {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item.total {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 2rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-item.total .price-label {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e7000b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount {
|
||||||
|
color: #e7000b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部提交栏 */
|
||||||
|
.order-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-total {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #e7000b;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn {
|
||||||
|
min-width: 200rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 0 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn.disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 地址选择弹窗 */
|
||||||
|
.address-picker-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-picker {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 80vh;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 32rpx 32rpx 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
border-bottom: 2rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-close {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #999;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-content {
|
||||||
|
flex: 1;
|
||||||
|
max-height: 60vh;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item {
|
||||||
|
position: relative;
|
||||||
|
padding: 24rpx;
|
||||||
|
border-bottom: 2rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item:active {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-receiver {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-phone {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
background-color: #9810fa;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item-detail {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-address {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item-check {
|
||||||
|
position: absolute;
|
||||||
|
right: 24rpx;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-item-check .iconfont {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-empty {
|
||||||
|
padding: 80rpx 24rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-empty .empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-footer {
|
||||||
|
padding: 24rpx;
|
||||||
|
border-top: 2rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
35
pages/order/detail.vue
Normal file
35
pages/order/detail.vue
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<view class="order-detail-page">
|
||||||
|
<view class="detail-content">
|
||||||
|
<text class="placeholder-text">订单详情页面开发中...</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
onLoad(options) {
|
||||||
|
console.log('订单ID:', options.id);
|
||||||
|
// TODO: 加载订单详情数据
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.order-detail-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-content {
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
702
pages/order/index.vue
Normal file
702
pages/order/index.vue
Normal file
@ -0,0 +1,702 @@
|
|||||||
|
<template>
|
||||||
|
<view class="order-list-page">
|
||||||
|
<!-- 状态筛选标签 -->
|
||||||
|
<view class="status-tabs">
|
||||||
|
<view
|
||||||
|
class="status-tab"
|
||||||
|
v-for="tab in statusTabs"
|
||||||
|
:key="tab.value"
|
||||||
|
:class="{ active: currentStatus === tab.value }"
|
||||||
|
@click="switchStatus(tab.value)"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="order-scroll"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
:refresher-enabled="true"
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="refresh"
|
||||||
|
>
|
||||||
|
<view class="order-list">
|
||||||
|
<view
|
||||||
|
class="order-item"
|
||||||
|
v-for="order in orderList"
|
||||||
|
:key="order.id || order.order_no"
|
||||||
|
@click="goToDetail(order)"
|
||||||
|
>
|
||||||
|
<!-- 订单头部 -->
|
||||||
|
<view class="order-header">
|
||||||
|
<view class="order-info">
|
||||||
|
<text class="order-no">订单号:{{ order.order_no || order.no }}</text>
|
||||||
|
<text class="order-date">{{ formatDate(order.created_at || order.date) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-status-tag" :class="getStatusClass(order.status)">
|
||||||
|
{{ getStatusText(order.status) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品列表 -->
|
||||||
|
<view class="order-goods">
|
||||||
|
<view
|
||||||
|
class="goods-item"
|
||||||
|
v-for="(item, index) in order.items || [order]"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<view class="goods-image">
|
||||||
|
<image
|
||||||
|
:src="item.product_image || item.main_image_url || item.image"
|
||||||
|
mode="aspectFill"
|
||||||
|
@error="handleImageError"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
<view class="goods-info">
|
||||||
|
<view class="goods-name">{{ item.product_name || item.name || order.product_name }}</view>
|
||||||
|
<view class="goods-spec" v-if="item.sku_name || item.spec">
|
||||||
|
{{ item.sku_name || item.spec }}
|
||||||
|
</view>
|
||||||
|
<view class="goods-price-row">
|
||||||
|
<view class="goods-price">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ formatPrice(item.price || order.price) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="goods-quantity">x{{ item.quantity || order.quantity || 1 }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单底部 -->
|
||||||
|
<view class="order-footer">
|
||||||
|
<view class="order-total">
|
||||||
|
<text class="total-label">共{{ getTotalQuantity(order) }}件商品 合计:</text>
|
||||||
|
<text class="total-price">¥{{ formatPrice(order.final_price || order.total_price || order.amount) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="order-actions">
|
||||||
|
<view
|
||||||
|
class="action-btn"
|
||||||
|
v-for="action in getOrderActions(order)"
|
||||||
|
:key="action.label"
|
||||||
|
:class="action.class"
|
||||||
|
@click.stop="handleAction(action, order)"
|
||||||
|
>
|
||||||
|
{{ action.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-if="!loading && orderList.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">📦</text>
|
||||||
|
<text class="empty-text">暂无订单</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="loading && orderList.length > 0" class="loading-more">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 没有更多 -->
|
||||||
|
<view v-if="!hasMore && orderList.length > 0" class="no-more">
|
||||||
|
<text class="no-more-text">没有更多订单了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '@/api/request.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentStatus: 'all', // 当前选中的状态
|
||||||
|
statusTabs: [
|
||||||
|
{ label: '全部', value: 'all' },
|
||||||
|
{ label: '待付款', value: 'pending' },
|
||||||
|
{ label: '待发货', value: 'paid' },
|
||||||
|
{ label: '待收货', value: 'shipped' },
|
||||||
|
{ label: '已完成', value: 'completed' }
|
||||||
|
],
|
||||||
|
orderList: [],
|
||||||
|
loading: false,
|
||||||
|
refreshing: false,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onLoad(options) {
|
||||||
|
// 如果从其他页面传入状态参数,切换到对应状态
|
||||||
|
if (options.status) {
|
||||||
|
this.currentStatus = options.status;
|
||||||
|
}
|
||||||
|
this.loadOrderList();
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
// 页面显示时刷新列表(从订单详情返回时)
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 格式化价格(分转元)
|
||||||
|
formatPrice(value) {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return '0.00';
|
||||||
|
}
|
||||||
|
// 如果已经是元为单位,直接返回
|
||||||
|
if (value < 1000) {
|
||||||
|
return parseFloat(value).toFixed(2);
|
||||||
|
}
|
||||||
|
// 分转换为元
|
||||||
|
value = value / 100;
|
||||||
|
return value.toFixed(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
getStatusText(status) {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': '待付款',
|
||||||
|
'paid': '待发货',
|
||||||
|
'shipped': '待收货',
|
||||||
|
'completed': '已完成',
|
||||||
|
'cancelled': '已取消',
|
||||||
|
'refunded': '已退款'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status || '未知';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取状态样式类
|
||||||
|
getStatusClass(status) {
|
||||||
|
const classMap = {
|
||||||
|
'pending': 'pending',
|
||||||
|
'paid': 'warning',
|
||||||
|
'shipped': 'warning',
|
||||||
|
'completed': 'success',
|
||||||
|
'cancelled': 'cancelled',
|
||||||
|
'refunded': 'cancelled'
|
||||||
|
};
|
||||||
|
return classMap[status] || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取订单操作按钮
|
||||||
|
getOrderActions(order) {
|
||||||
|
const status = order.status;
|
||||||
|
const actions = [];
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
actions.push({ label: '取消订单', action: 'cancel', class: 'cancel-btn' });
|
||||||
|
actions.push({ label: '立即付款', action: 'pay', class: 'primary-btn' });
|
||||||
|
} else if (status === 'paid') {
|
||||||
|
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||||||
|
} else if (status === 'shipped') {
|
||||||
|
actions.push({ label: '查看物流', action: 'logistics', class: 'default-btn' });
|
||||||
|
actions.push({ label: '确认收货', action: 'confirm', class: 'primary-btn' });
|
||||||
|
} else if (status === 'completed') {
|
||||||
|
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||||||
|
actions.push({ label: '再次购买', action: 'rebuy', class: 'primary-btn' });
|
||||||
|
} else {
|
||||||
|
actions.push({ label: '查看详情', action: 'detail', class: 'default-btn' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取订单商品总数量
|
||||||
|
getTotalQuantity(order) {
|
||||||
|
if (order.items && Array.isArray(order.items)) {
|
||||||
|
return order.items.reduce((sum, item) => sum + (parseInt(item.quantity) || 1), 0);
|
||||||
|
}
|
||||||
|
return parseInt(order.quantity) || 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换状态
|
||||||
|
switchStatus(status) {
|
||||||
|
if (this.currentStatus === status) return;
|
||||||
|
this.currentStatus = status;
|
||||||
|
this.page = 1;
|
||||||
|
this.hasMore = true;
|
||||||
|
this.orderList = [];
|
||||||
|
this.loadOrderList();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载订单列表
|
||||||
|
async loadOrderList() {
|
||||||
|
if (this.loading || !this.hasMore) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: this.page,
|
||||||
|
page_size: this.pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
// 如果不是全部,添加状态筛选
|
||||||
|
if (this.currentStatus !== 'all') {
|
||||||
|
params.status = this.currentStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await request('xcx/orders', 'GET', params);
|
||||||
|
|
||||||
|
// 处理返回数据
|
||||||
|
let orders = [];
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
orders = response;
|
||||||
|
} else if (response.list && Array.isArray(response.list)) {
|
||||||
|
orders = response.list;
|
||||||
|
} else if (response.data && Array.isArray(response.data)) {
|
||||||
|
orders = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orders.length < this.pageSize) {
|
||||||
|
this.hasMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.page === 1) {
|
||||||
|
this.orderList = orders;
|
||||||
|
} else {
|
||||||
|
this.orderList = [...this.orderList, ...orders];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载订单列表失败:', error);
|
||||||
|
// 如果是第一页且没有数据,显示空状态
|
||||||
|
if (this.page === 1) {
|
||||||
|
this.orderList = [];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
this.refreshing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
refresh() {
|
||||||
|
this.refreshing = true;
|
||||||
|
this.page = 1;
|
||||||
|
this.hasMore = true;
|
||||||
|
this.orderList = [];
|
||||||
|
this.loadOrderList();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
loadMore() {
|
||||||
|
if (!this.loading && this.hasMore) {
|
||||||
|
this.loadOrderList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 处理订单操作
|
||||||
|
handleAction(action, order) {
|
||||||
|
switch (action.action) {
|
||||||
|
case 'pay':
|
||||||
|
this.payOrder(order);
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
this.cancelOrder(order);
|
||||||
|
break;
|
||||||
|
case 'confirm':
|
||||||
|
this.confirmOrder(order);
|
||||||
|
break;
|
||||||
|
case 'logistics':
|
||||||
|
this.viewLogistics(order);
|
||||||
|
break;
|
||||||
|
case 'rebuy':
|
||||||
|
this.rebuyOrder(order);
|
||||||
|
break;
|
||||||
|
case 'detail':
|
||||||
|
default:
|
||||||
|
this.goToDetail(order);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 跳转到订单详情
|
||||||
|
goToDetail(order) {
|
||||||
|
const orderId = order.id || order.order_id;
|
||||||
|
if (orderId) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/order/detail?id=${orderId}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 支付订单
|
||||||
|
async payOrder(order) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '支付功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
// TODO: 实现支付逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
// 取消订单
|
||||||
|
async cancelOrder(order) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要取消这个订单吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
await request(`xcx/order/${order.id || order.order_id}/cancel`, 'POST');
|
||||||
|
uni.showToast({
|
||||||
|
title: '订单已取消',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
this.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消订单失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '取消订单失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 确认收货
|
||||||
|
async confirmOrder(order) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定已收到商品吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
await request(`xcx/order/${order.id || order.order_id}/confirm`, 'POST');
|
||||||
|
uni.showToast({
|
||||||
|
title: '确认收货成功',
|
||||||
|
icon: 'success'
|
||||||
|
});
|
||||||
|
this.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('确认收货失败:', error);
|
||||||
|
uni.showToast({
|
||||||
|
title: error.message || '确认收货失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查看物流
|
||||||
|
viewLogistics(order) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '物流功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
// TODO: 实现物流查看逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
// 再次购买
|
||||||
|
rebuyOrder(order) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '再次购买功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
// TODO: 实现再次购买逻辑
|
||||||
|
},
|
||||||
|
|
||||||
|
// 图片加载错误处理
|
||||||
|
handleImageError(e) {
|
||||||
|
console.log('图片加载失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.order-list-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tabs {
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tab {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tab.active {
|
||||||
|
color: #9810fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 60rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
border-radius: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh - 100rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 2rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-no {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-date {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status-tag {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status-tag.pending {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status-tag.warning {
|
||||||
|
background-color: #e1bee7;
|
||||||
|
color: #7b1fa2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status-tag.success {
|
||||||
|
background-color: #c8e6c9;
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status-tag.cancelled {
|
||||||
|
background-color: #ffcdd2;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-goods {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-image {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-image image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-spec {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
color: #e7000b;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-quantity {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 24rpx;
|
||||||
|
border-top: 2rpx solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-total {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #e7000b;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-btn {
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
color: #666;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-btn {
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
color: #999;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text,
|
||||||
|
.no-more-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
345
pages/pointsLog/index.vue
Normal file
345
pages/pointsLog/index.vue
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
<template>
|
||||||
|
<view class="points-log-page">
|
||||||
|
<!-- 积分流水列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="log-scroll"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
:refresher-enabled="true"
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="refresh"
|
||||||
|
>
|
||||||
|
<view class="log-list">
|
||||||
|
<view
|
||||||
|
class="log-item"
|
||||||
|
v-for="log in logList"
|
||||||
|
:key="log.id"
|
||||||
|
>
|
||||||
|
<view class="log-left">
|
||||||
|
<view class="log-icon" :class="getSourceTypeClass(log.source_type)">
|
||||||
|
{{ getSourceTypeIcon(log.source_type) }}
|
||||||
|
</view>
|
||||||
|
<view class="log-info">
|
||||||
|
<text class="log-type">{{ getSourceTypeText(log.source_type) }}</text>
|
||||||
|
<text class="log-time">{{ formatDate(log.created_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="log-right">
|
||||||
|
<text class="log-points" :class="getPointsClass(log.points)">
|
||||||
|
{{ formatPoints(log.points) }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view v-if="!loading && logList.length === 0" class="empty-state">
|
||||||
|
<text class="empty-icon">📋</text>
|
||||||
|
<text class="empty-text">暂无积分流水记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view v-if="loading && logList.length > 0" class="loading-more">
|
||||||
|
<text class="loading-text">加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 没有更多 -->
|
||||||
|
<view v-if="!hasMore && logList.length > 0" class="no-more">
|
||||||
|
<text class="no-more-text">没有更多记录了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '@/api/request.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logList: [],
|
||||||
|
loading: false,
|
||||||
|
refreshing: false,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
hasMore: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadLogList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 格式化日期时间
|
||||||
|
formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 格式化积分显示
|
||||||
|
formatPoints(points) {
|
||||||
|
if (!points && points !== 0) return '0';
|
||||||
|
const num = parseFloat(points);
|
||||||
|
if (num > 0) {
|
||||||
|
return `+${num}`;
|
||||||
|
}
|
||||||
|
return String(num);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取积分样式类(正数绿色,负数红色)
|
||||||
|
getPointsClass(points) {
|
||||||
|
const num = parseFloat(points);
|
||||||
|
return num > 0 ? 'positive' : 'negative';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取积分来源类型文本
|
||||||
|
getSourceTypeText(sourceType) {
|
||||||
|
const typeMap = {
|
||||||
|
1: '签到',
|
||||||
|
2: '邀请',
|
||||||
|
3: '消费',
|
||||||
|
4: '兑换扣除',
|
||||||
|
5: '系统发放',
|
||||||
|
6: '消费',
|
||||||
|
7: '回退'
|
||||||
|
};
|
||||||
|
return typeMap[sourceType] || '未知';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取积分来源类型图标
|
||||||
|
getSourceTypeIcon(sourceType) {
|
||||||
|
const iconMap = {
|
||||||
|
1: '📅',
|
||||||
|
2: '👥',
|
||||||
|
3: '🛍️',
|
||||||
|
4: '💸',
|
||||||
|
5: '🎁',
|
||||||
|
6: '🛒',
|
||||||
|
7: '↩️'
|
||||||
|
};
|
||||||
|
return iconMap[sourceType] || '📝';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取积分来源类型样式类
|
||||||
|
getSourceTypeClass(sourceType) {
|
||||||
|
const classMap = {
|
||||||
|
1: 'type-sign',
|
||||||
|
2: 'type-invite',
|
||||||
|
3: 'type-consume',
|
||||||
|
4: 'type-exchange',
|
||||||
|
5: 'type-system',
|
||||||
|
6: 'type-consume',
|
||||||
|
7: 'type-refund'
|
||||||
|
};
|
||||||
|
return classMap[sourceType] || '';
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载积分流水列表
|
||||||
|
async loadLogList() {
|
||||||
|
if (this.loading || !this.hasMore) return;
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: this.page,
|
||||||
|
page_size: this.pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request('xcx/user_points_logs', 'GET', params);
|
||||||
|
|
||||||
|
// 处理返回数据
|
||||||
|
let logs = [];
|
||||||
|
if (Array.isArray(response)) {
|
||||||
|
logs = response;
|
||||||
|
} else if (response.list && Array.isArray(response.list)) {
|
||||||
|
logs = response.list;
|
||||||
|
} else if (response.data && Array.isArray(response.data)) {
|
||||||
|
logs = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logs.length < this.pageSize) {
|
||||||
|
this.hasMore = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.page === 1) {
|
||||||
|
this.logList = logs;
|
||||||
|
} else {
|
||||||
|
this.logList = [...this.logList, ...logs];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page++;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载积分流水失败:', error);
|
||||||
|
// 如果是第一页且没有数据,显示空状态
|
||||||
|
if (this.page === 1) {
|
||||||
|
this.logList = [];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
this.refreshing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 刷新列表
|
||||||
|
refresh() {
|
||||||
|
this.refreshing = true;
|
||||||
|
this.page = 1;
|
||||||
|
this.hasMore = true;
|
||||||
|
this.logList = [];
|
||||||
|
this.loadLogList();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载更多
|
||||||
|
loadMore() {
|
||||||
|
if (!this.loading && this.hasMore) {
|
||||||
|
this.loadLogList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.points-log-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(100vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-list {
|
||||||
|
padding: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-item {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon.type-sign {
|
||||||
|
background-color: #fff3e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon.type-invite {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon.type-consume {
|
||||||
|
background-color: #fce4ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon.type-exchange {
|
||||||
|
background-color: #ffebee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon.type-system {
|
||||||
|
background-color: #e8f5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-icon.type-refund {
|
||||||
|
background-color: #f3e5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-type {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-points {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-points.positive {
|
||||||
|
color: #26b95a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-points.negative {
|
||||||
|
color: #e7000b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text,
|
||||||
|
.no-more-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
1257
pages/shopDetail/index.vue
Normal file
1257
pages/shopDetail/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
559
pages/shoppingCart/index.vue
Normal file
559
pages/shoppingCart/index.vue
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
<template>
|
||||||
|
<view class="cart-page">
|
||||||
|
<!-- 购物车列表 -->
|
||||||
|
<view class="cart-list" v-if="cartList.length > 0">
|
||||||
|
<view class="cart-item" v-for="(item, index) in cartList" :key="item.id">
|
||||||
|
<view class="item-checkbox" @click="toggleSelect(index)">
|
||||||
|
<view class="checkbox" :class="{ checked: item.selected === 1 }">
|
||||||
|
<text class="iconfont icon-gou" v-if="item.selected === 1"></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-image">
|
||||||
|
<image :src="item.product_image || item.main_image_url" mode="aspectFill"></image>
|
||||||
|
</view>
|
||||||
|
<view class="item-info">
|
||||||
|
<view class="item-title">{{ item.product_name || item.name }}</view>
|
||||||
|
<view class="item-spec" v-if="item.sku_name">
|
||||||
|
{{ item.sku_name }}
|
||||||
|
</view>
|
||||||
|
<view class="item-price-row">
|
||||||
|
<view class="item-price">
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-value">{{ formatPrice(item.price) || formatPrice(item.sku_price) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-count">
|
||||||
|
<view class="count-btn" @click="decreaseCount(index)">-</view>
|
||||||
|
<view class="count-num">{{ item.quantity || item.count }}</view>
|
||||||
|
<view class="count-btn" @click="increaseCount(index)">+</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="item-delete" @click="deleteItem(index)">
|
||||||
|
<text class="iconfont icon-cangpeitubiao_shanchu"></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空购物车 -->
|
||||||
|
<view class="empty-cart" v-else>
|
||||||
|
<view class="empty-icon">
|
||||||
|
<text class="iconfont icon-gouwuche"></text>
|
||||||
|
</view>
|
||||||
|
<view class="empty-text">购物车是空的</view>
|
||||||
|
<view class="empty-tip">快去挑选心仪的商品吧~</view>
|
||||||
|
<view class="empty-btn" @click="goShopping">去逛逛</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部结算栏 -->
|
||||||
|
<view class="cart-footer" v-if="cartList.length > 0">
|
||||||
|
<view class="footer-left">
|
||||||
|
<view class="footer-checkbox" @click="toggleSelectAll">
|
||||||
|
<view class="checkbox" :class="{ checked: isAllSelected }">
|
||||||
|
<text class="iconfont icon-gou" v-if="isAllSelected"></text>
|
||||||
|
</view>
|
||||||
|
<text class="footer-text">全选</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="footer-right">
|
||||||
|
<view class="footer-total">
|
||||||
|
<text class="total-label">合计:</text>
|
||||||
|
<text class="total-price">¥{{ formatPrice(totalPrice) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="footer-btn" :class="{ disabled: selectedCount === 0 }" @click="checkout">
|
||||||
|
结算({{ selectedCount }})
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import request from '@/api/request.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
cartList: [],
|
||||||
|
loading: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 是否全选
|
||||||
|
isAllSelected() {
|
||||||
|
if (this.cartList.length === 0) return false;
|
||||||
|
return this.cartList.every(item => item.selected === 1);
|
||||||
|
},
|
||||||
|
// 已选商品数量
|
||||||
|
selectedCount() {
|
||||||
|
return this.cartList.filter(item => item.selected === 1).length;
|
||||||
|
},
|
||||||
|
// 总价
|
||||||
|
totalPrice() {
|
||||||
|
let total = 0;
|
||||||
|
this.cartList.forEach(item => {
|
||||||
|
if (item.selected === 1) {
|
||||||
|
const price = parseFloat(item.price || item.sku_price || 0);
|
||||||
|
const quantity = parseInt(item.quantity || item.count || 0);
|
||||||
|
total += price * quantity;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return total.toFixed(2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoad() {
|
||||||
|
this.loadCartList();
|
||||||
|
},
|
||||||
|
onShow() {
|
||||||
|
// 页面显示时刷新购物车
|
||||||
|
this.loadCartList();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatPrice(value) {
|
||||||
|
if(!value) {
|
||||||
|
return 0.00;
|
||||||
|
}
|
||||||
|
// 分转换为元
|
||||||
|
value = value / 100;
|
||||||
|
// 保留两位小数
|
||||||
|
return value.toFixed(2);
|
||||||
|
},
|
||||||
|
// 加载购物车列表
|
||||||
|
async loadCartList() {
|
||||||
|
try {
|
||||||
|
this.loading = true;
|
||||||
|
const res = await request('xcx/carts', 'GET', {page: 1, page_size: 99});
|
||||||
|
// 根据实际返回数据结构调整
|
||||||
|
const list = res.list || res.data || res || [];
|
||||||
|
// 为每个商品添加selected属性,1表示选中,2表示不选中
|
||||||
|
this.cartList = list.map(item => ({
|
||||||
|
...item,
|
||||||
|
selected: item.selected !== undefined ? item.selected : 1
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载购物车失败:', error);
|
||||||
|
// 如果接口不存在,使用模拟数据
|
||||||
|
if (error.message && error.message.includes('404')) {
|
||||||
|
this.cartList = [];
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: '加载购物车失败',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 切换商品选择状态
|
||||||
|
toggleSelect(index) {
|
||||||
|
// 1表示选中,2表示不选中
|
||||||
|
this.cartList[index].selected = this.cartList[index].selected === 1 ? 2 : 1;
|
||||||
|
this.updateCartItem(index);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 全选/取消全选
|
||||||
|
toggleSelectAll() {
|
||||||
|
// 1表示选中,2表示不选中
|
||||||
|
const selectAll = this.isAllSelected ? 2 : 1;
|
||||||
|
this.cartList.forEach((item, index) => {
|
||||||
|
item.selected = selectAll;
|
||||||
|
this.updateCartItem(index);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 减少数量
|
||||||
|
async decreaseCount(index) {
|
||||||
|
const item = this.cartList[index];
|
||||||
|
const currentCount = parseInt(item.quantity || item.count || 1);
|
||||||
|
if (currentCount <= 1) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '商品数量不能少于1',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.quantity = currentCount - 1;
|
||||||
|
item.count = currentCount - 1;
|
||||||
|
await this.updateCartItem(index);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 增加数量
|
||||||
|
async increaseCount(index) {
|
||||||
|
const item = this.cartList[index];
|
||||||
|
const currentCount = parseInt(item.quantity || item.count || 1);
|
||||||
|
item.quantity = currentCount + 1;
|
||||||
|
item.count = currentCount + 1;
|
||||||
|
await this.updateCartItem(index);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新购物车商品
|
||||||
|
async updateCartItem(index) {
|
||||||
|
const item = this.cartList[index];
|
||||||
|
try {
|
||||||
|
// 根据实际接口调整,selected: 1表示选中,2表示不选中
|
||||||
|
await request('xcx/cart/' + item.id, 'PUT', {
|
||||||
|
quantity: item.quantity || item.count,
|
||||||
|
selected: item.selected
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新购物车失败:', error);
|
||||||
|
// 如果接口不存在,只更新本地数据
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除商品
|
||||||
|
deleteItem(index) {
|
||||||
|
const item = this.cartList[index];
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要删除这个商品吗?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
// 使用 DELETE 方法删除,与更新接口风格保持一致
|
||||||
|
await request('xcx/cart/' + item.id, 'DELETE');
|
||||||
|
// 删除成功后从列表中移除
|
||||||
|
this.cartList.splice(index, 1);
|
||||||
|
uni.showToast({
|
||||||
|
title: '删除成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
// 如果接口调用失败,仍然删除本地数据(容错处理)
|
||||||
|
this.cartList.splice(index, 1);
|
||||||
|
uni.showToast({
|
||||||
|
title: '删除成功',
|
||||||
|
icon: 'success',
|
||||||
|
duration: 1500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 结算
|
||||||
|
checkout() {
|
||||||
|
if (this.selectedCount === 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '请选择要结算的商品',
|
||||||
|
icon: 'none'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedItems = this.cartList.filter(item => item.selected === 1);
|
||||||
|
// 将选中的商品信息传递给订单页面
|
||||||
|
const orderData = selectedItems.map(item => ({
|
||||||
|
cart_id: item.id,
|
||||||
|
product_id: item.product_id,
|
||||||
|
sku_id: item.sku_id,
|
||||||
|
quantity: item.quantity || item.count,
|
||||||
|
price: item.price || item.sku_price
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 跳转到订单页面,根据实际路由调整
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/order/create?items=${encodeURIComponent(JSON.stringify(orderData))}`
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 去逛逛
|
||||||
|
goShopping() {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.cart-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 购物车列表 */
|
||||||
|
.cart-list {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #fff;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding: 30rpx 24rpx;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-checkbox {
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox.checked {
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
border-color: #9810fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox.checked .iconfont {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-image image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 160rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-spec {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-price {
|
||||||
|
color: #e7000b;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-value {
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 2rpx solid #e5e5e5;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-btn {
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-btn:active {
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.count-num {
|
||||||
|
min-width: 60rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
border-left: 2rpx solid #e5e5e5;
|
||||||
|
border-right: 2rpx solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-delete {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
padding: 10rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
top: 20rpx;
|
||||||
|
right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-delete:active {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-delete .iconfont {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #e7000b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空购物车 */
|
||||||
|
.empty-cart {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 200rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon .iconfont {
|
||||||
|
font-size: 120rpx;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-btn {
|
||||||
|
width: 240rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底部结算栏 */
|
||||||
|
.cart-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100rpx;
|
||||||
|
background-color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-total {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #e7000b;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn {
|
||||||
|
min-width: 180rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
line-height: 72rpx;
|
||||||
|
text-align: center;
|
||||||
|
background: linear-gradient(135deg, #9810fa 0%, #7a0bc7 100%);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
padding: 0 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-btn.disabled {
|
||||||
|
background: #ccc;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 5057334 */
|
font-family: "iconfont"; /* Project id 5057334 */
|
||||||
src: url('//at.alicdn.com/t/c/font_5057334_7ak9qra77f.woff2?t=1763287992869') format('woff2'),
|
src: url('//at.alicdn.com/t/c/font_5057334_65098frmpc4.woff2?t=1763879647176') format('woff2'),
|
||||||
url('//at.alicdn.com/t/c/font_5057334_7ak9qra77f.woff?t=1763287992869') format('woff'),
|
url('//at.alicdn.com/t/c/font_5057334_65098frmpc4.woff?t=1763879647176') format('woff'),
|
||||||
url('//at.alicdn.com/t/c/font_5057334_7ak9qra77f.ttf?t=1763287992869') format('truetype');
|
url('//at.alicdn.com/t/c/font_5057334_65098frmpc4.ttf?t=1763879647176') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@ -13,6 +13,22 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-gou:before {
|
||||||
|
content: "\e786";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-zhengpinbaozhang:before {
|
||||||
|
content: "\e602";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wuliu:before {
|
||||||
|
content: "\e70d";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-cangpeitubiao_shanchu:before {
|
||||||
|
content: "\e618";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-xin:before {
|
.icon-xin:before {
|
||||||
content: "\e641";
|
content: "\e641";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user