This commit is contained in:
左哥 2025-11-23 15:21:50 +08:00
parent c03d3149d2
commit 0dbfaea026
12 changed files with 3917 additions and 44 deletions

View File

@ -1,6 +1,6 @@
{ {
"name" : "香水有毒", "name" : "香水有毒",
"appid" : "", "appid" : "__UNI__E8ED88D",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.0",
"versionCode" : "100", "versionCode" : "100",

View File

@ -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": {

View File

@ -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}`
})
} }
} }
} }

View File

@ -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);

View File

@ -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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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 || [];
// selected12
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) {
// 12
this.cartList[index].selected = this.cartList[index].selected === 1 ? 2 : 1;
this.updateCartItem(index);
},
// /
toggleSelectAll() {
// 12
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: 12
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>

View File

@ -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";
} }