Files
medical-mall/pages/mall/merchant/product-detail.uvue

716 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 商家端 - 商品管理详情页 -->
<template>
<view class="product-manage-detail">
<!-- #ifdef MP-WEIXIN -->
<view style="padding-top: var(--status-bar-height); background-color: #ffffff; display: flex; flex-direction: row; align-items: flex-end; border-bottom: 1rpx solid #eeeeee; box-sizing: border-box; height: calc(88rpx + var(--status-bar-height));">
<view style="display: flex; flex-direction: row; align-items: center; padding: 0 30rpx; height: 88rpx;" @click="uni.navigateBack()">
<text style="font-size: 44rpx; color: #333333; line-height: 1; margin-right: 6rpx;"></text>
<text style="font-size: 28rpx; color: #333333;">返回</text>
</view>
</view>
<!-- #endif -->
<!-- 商品基本信息 -->
<view class="product-section">
<view class="section-header">
<text class="section-title">商品信息</text>
<text class="edit-btn" @click="editProduct">编辑</text>
</view>
<view class="product-images">
<view class="image-list">
<view v-for="(image, index) in product.images" :key="index" class="image-item">
<image :src="image" class="product-image" mode="aspectFit" />
</view>
<view class="add-image" @click="addImage">+</view>
</view>
</view>
<view class="product-info">
<view class="info-item">
<text class="info-label">商品名称</text>
<text class="info-value">{{ product.name }}</text>
</view>
<view class="info-item">
<text class="info-label">商品描述</text>
<text class="info-value">{{ product.description || '暂无描述' }}</text>
</view>
<view class="info-item">
<text class="info-label">商品价格</text>
<text class="info-value price">¥{{ product.price }}</text>
</view>
<view class="info-item">
<text class="info-label">原价</text>
<text class="info-value">¥{{ product.original_price || '未设置' }}</text>
</view>
<view class="info-item">
<text class="info-label">库存数量</text>
<text class="info-value">{{ product.stock }}件</text>
</view>
<view class="info-item">
<text class="info-label">销量</text>
<text class="info-value">{{ product.sales }}件</text>
</view>
<view class="info-item">
<text class="info-label">商品状态</text>
<text class="info-value" :class="{ 'status-on': product.status === 1, 'status-off': product.status === 2 || product.status === 0 }">
{{ product.status === 1 ? '上架' : (product.status === 2 || product.status === 0 ? '下架' : '其他') }}
</text>
</view>
</view>
</view>
<!-- SKU规格管理 -->
<view class="sku-section">
<view class="section-header">
<text class="section-title">规格管理</text>
<text class="add-btn" @click="addSku">添加规格</text>
</view>
<view v-if="productSkus.length === 0" class="empty-sku">
<text class="empty-text">暂无规格,点击添加规格</text>
</view>
<view v-for="sku in productSkus" :key="sku.id" class="sku-item">
<view class="sku-info">
<text class="sku-code">{{ sku.sku_code }}</text>
<text class="sku-spec">{{ getSkuSpecText(sku) }}</text>
</view>
<view class="sku-details">
<text class="sku-price">¥{{ sku.price }}</text>
<text class="sku-stock">库存: {{ sku.stock }}</text>
<text class="sku-status" :class="{ 'status-on': sku.status === 1, 'status-off': sku.status === 2 || sku.status === 0 }">
{{ sku.status === 1 ? '启用' : '禁用' }}
</text>
</view>
<view class="sku-actions">
<text class="action-btn edit" @click="editSku(sku)">编辑</text>
<text class="action-btn delete" @click="deleteSku(sku)">删除</text>
</view>
</view>
</view>
<!-- 销售数据 -->
<view class="sales-section">
<view class="section-header">
<text class="section-title">销售数据</text>
<text class="view-detail" @click="viewSalesDetail">查看详情</text>
</view>
<view class="sales-stats">
<view class="stat-item">
<text class="stat-value">{{ salesData.today_sales }}</text>
<text class="stat-label">今日销量</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ salesData.week_sales }}</text>
<text class="stat-label">本周销量</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ salesData.month_sales }}</text>
<text class="stat-label">本月销量</text>
</view>
<view class="stat-item">
<text class="stat-value">¥{{ salesData.total_revenue }}</text>
<text class="stat-label">总收入</text>
</view>
</view>
</view>
<!-- 评价管理 -->
<view class="review-section">
<view class="section-header">
<text class="section-title">商品评价</text>
<text class="view-all" @click="viewAllReviews">查看全部</text>
</view>
<view class="review-summary">
<view class="rating-info">
<text class="rating-score">{{ reviewData.average_rating.toFixed(1) }}</text>
<view class="rating-stars">
<text v-for="i in 5" :key="i" class="star" :class="{ filled: i <= Math.floor(reviewData.average_rating) }">★</text>
</view>
<text class="review-count">{{ reviewData.total_reviews }}条评价</text>
</view>
</view>
<view v-if="recentReviews.length > 0" class="recent-reviews">
<view v-for="review in recentReviews" :key="review.id" class="review-item">
<view class="review-header">
<text class="reviewer-name">{{ review.user_name }}</text>
<view class="review-rating">
<text v-for="i in review.rating" :key="i" class="star filled">★</text>
</view>
</view>
<text class="review-content">{{ review.content }}</text>
<text class="review-time">{{ formatTime(review.created_at) }}</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="action-btn primary" @click="toggleProductStatus">
{{ product.status === 1 ? '下架商品' : '上架商品' }}
</button>
<button class="action-btn secondary" @click="editProduct">编辑商品</button>
<button class="action-btn danger" @click="deleteProduct">删除商品</button>
</view>
</view>
</template>
<script>
import { ProductType, ProductSkuType } from '@/types/mall-types.uts'
type SalesDataType = {
today_sales: number
week_sales: number
month_sales: number
total_revenue: number
}
type ReviewDataType = {
average_rating: number
total_reviews: number
}
type ReviewType = {
id: string
user_name: string
rating: number
content: string
created_at: string
}
export default {
data() {
return {
product: {
id: '',
merchant_id: '',
category_id: '',
name: '',
description: '',
images: [] as Array<string>,
price: 0,
original_price: 0,
stock: 0,
sales: 0,
status: 0,
created_at: ''
} as ProductType,
productSkus: [] as Array<ProductSkuType>,
salesData: {
today_sales: 0,
week_sales: 0,
month_sales: 0,
total_revenue: 0
} as SalesDataType,
reviewData: {
average_rating: 0,
total_reviews: 0
} as ReviewDataType,
recentReviews: [] as Array<ReviewType>
}
},
onLoad(options: any) {
const productId = options.productId as string
if (productId) {
this.loadProductDetail(productId)
}
},
methods: {
loadProductDetail(productId: string) {
// 模拟加载商品详情数据
this.product = {
id: productId,
merchant_id: 'merchant_001',
category_id: 'cat_001',
name: '精选好物商品',
description: '这是一个高品质的商品,具有优秀的性能和优美的外观设计。',
images: [
'/static/product1.jpg',
'/static/product2.jpg',
'/static/product3.jpg'
],
price: 199.99,
original_price: 299.99,
stock: 100,
sales: 1256,
status: 1,
created_at: '2024-01-15'
}
this.productSkus = [
{
id: 'sku_001',
product_id: productId,
sku_code: 'SKU001',
specifications: { color: '红色', size: 'M' },
price: 199.99,
stock: 50,
image_url: '/static/sku1.jpg',
status: 1
},
{
id: 'sku_002',
product_id: productId,
sku_code: 'SKU002',
specifications: { color: '蓝色', size: 'L' },
price: 219.99,
stock: 30,
image_url: '/static/sku2.jpg',
status: 1
}
]
this.salesData = {
today_sales: 5,
week_sales: 28,
month_sales: 156,
total_revenue: 25680.50
}
this.reviewData = {
average_rating: 4.6,
total_reviews: 89
}
this.recentReviews = [
{
id: 'review_001',
user_name: '用户***123',
rating: 5,
content: '商品质量很好,物流也很快,满意!',
created_at: '2024-01-14 15:30:00'
},
{
id: 'review_002',
user_name: '用户***456',
rating: 4,
content: '整体不错,就是包装有点简单。',
created_at: '2024-01-13 09:20:00'
}
]
},
getSkuSpecText(sku: ProductSkuType): string {
if (sku.specifications) {
const specs: any = sku.specifications
return Object.keys(specs).map(key => `${key}: ${specs[key]}`).join(', ')
}
return '无规格'
},
formatTime(timeStr: string): string {
return timeStr.replace('T', ' ').split('.')[0]
},
editProduct() {
uni.navigateTo({
url: `/pages/mall/merchant/product-edit?productId=${this.product.id}`
})
},
addImage() {
uni.chooseImage({
count: 1,
success: (res) => {
this.product.images.push(res.tempFilePaths[0])
uni.showToast({
title: '图片添加成功',
icon: 'success'
})
}
})
},
addSku() {
uni.navigateTo({
url: `/pages/mall/merchant/sku-add?productId=${this.product.id}`
})
},
editSku(sku: ProductSkuType) {
uni.navigateTo({
url: `/pages/mall/merchant/sku-edit?skuId=${sku.id}`
})
},
deleteSku(sku: ProductSkuType) {
uni.showModal({
title: '确认删除',
content: `确定要删除规格 ${sku.sku_code} 吗?`,
success: (res) => {
if (res.confirm) {
this.productSkus = this.productSkus.filter(item => item.id !== sku.id)
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
})
},
viewSalesDetail() {
uni.navigateTo({
url: `/pages/mall/merchant/sales-detail?productId=${this.product.id}`
})
},
viewAllReviews() {
uni.navigateTo({
url: `/pages/mall/merchant/product-reviews?productId=${this.product.id}`
})
},
toggleProductStatus() {
const newStatus = this.product.status === 1 ? 0 : 1
const actionText = newStatus === 1 ? '上架' : '下架'
uni.showModal({
title: `确认${actionText}`,
content: `确定要${actionText}这个商品吗?`,
success: (res) => {
if (res.confirm) {
this.product.status = newStatus
uni.showToast({
title: `${actionText}成功`,
icon: 'success'
})
}
}
})
},
deleteProduct() {
uni.showModal({
title: '确认删除',
content: '删除后将无法恢复,确定要删除这个商品吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
})
}
}
}
</script>
<style>
.product-manage-detail {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 200rpx;
}
.product-section, .sku-section, .sales-section, .review-section {
background-color: #fff;
margin-bottom: 20rpx;
padding: 30rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.edit-btn, .add-btn, .view-detail, .view-all {
font-size: 26rpx;
color: #007aff;
}
.product-images {
margin-bottom: 30rpx;
}
.image-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.image-item, .add-image {
width: 150rpx;
height: 150rpx;
border-radius: 10rpx;
overflow: hidden;
}
.product-image {
width: 100%;
height: 100%;
}
.add-image {
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: #999;
border: 2rpx dashed #ddd;
}
.product-info {
margin-top: 30rpx;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.info-label {
font-size: 28rpx;
color: #666;
width: 150rpx;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
text-align: right;
}
.info-value.price {
color: #ff4444;
font-weight: bold;
}
.status-on {
color: #4caf50 !important;
}
.status-off {
color: #ff4444 !important;
}
.empty-sku {
text-align: center;
padding: 60rpx 0;
}
.empty-text {
font-size: 26rpx;
color: #999;
}
.sku-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.sku-info {
margin-bottom: 15rpx;
}
.sku-code {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-right: 20rpx;
}
.sku-spec {
font-size: 26rpx;
color: #666;
}
.sku-details {
display: flex;
align-items: center;
margin-bottom: 15rpx;
}
.sku-price {
font-size: 26rpx;
color: #ff4444;
font-weight: bold;
margin-right: 30rpx;
}
.sku-stock {
font-size: 24rpx;
color: #666;
margin-right: 30rpx;
}
.sku-status {
font-size: 24rpx;
}
.sku-actions {
display: flex;
gap: 30rpx;
}
.action-btn {
font-size: 24rpx;
padding: 10rpx 20rpx;
border-radius: 15rpx;
}
.action-btn.edit {
background-color: #e3f2fd;
color: #007aff;
}
.action-btn.delete {
background-color: #ffebee;
color: #ff4444;
}
.sales-stats {
display: flex;
gap: 30rpx;
}
.stat-item {
flex: 1;
text-align: center;
padding: 30rpx 0;
background-color: #f8f9fa;
border-radius: 10rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.review-summary {
margin-bottom: 30rpx;
}
.rating-info {
display: flex;
align-items: center;
gap: 20rpx;
}
.rating-score {
font-size: 48rpx;
font-weight: bold;
color: #ffa726;
}
.rating-stars {
display: flex;
}
.star {
font-size: 24rpx;
color: #ddd;
}
.star.filled {
color: #ffa726;
}
.review-count {
font-size: 24rpx;
color: #666;
}
.recent-reviews {
margin-top: 30rpx;
}
.review-item {
padding: 25rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.review-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.reviewer-name {
font-size: 26rpx;
color: #333;
}
.review-rating {
display: flex;
}
.review-content {
font-size: 26rpx;
color: #666;
line-height: 1.4;
margin-bottom: 10rpx;
}
.review-time {
font-size: 22rpx;
color: #999;
}
.action-buttons {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
gap: 20rpx;
}
.action-buttons .action-btn {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
border: none;
}
.action-buttons .action-btn.primary {
background-color: #4caf50;
color: #fff;
}
.action-buttons .action-btn.secondary {
background-color: #ffa726;
color: #fff;
}
.action-buttons .action-btn.danger {
background-color: #ff4444;
color: #fff;
}
</style>