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

730 lines
17 KiB
Plaintext
Raw Permalink 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;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
padding-left: 16rpx;
border-left: 6rpx solid rgb(66, 121, 240);
}
.edit-btn, .add-btn, .view-detail, .view-all {
font-size: 26rpx;
color: rgb(66, 121, 240);
}
.product-images {
margin-bottom: 30rpx;
}
.image-list {
display: flex;
flex-direction: row;
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;
flex-direction: row;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: #999;
border: 2rpx dashed #ddd;
}
.product-info {
margin-top: 30rpx;
}
.info-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.info-label {
font-size: 28rpx;
color: #666;
width: 180rpx;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: #333;
text-align: right;
}
.info-value.price {
color: rgb(225, 37, 27);
font-weight: bold;
}
.status-on {
color: #4caf50 !important;
}
.status-off {
color: #999 !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;
flex-direction: row;
align-items: center;
margin-bottom: 15rpx;
}
.sku-price {
font-size: 26rpx;
color: rgb(225, 37, 27);
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;
flex-direction: row;
gap: 30rpx;
}
.action-btn {
font-size: 24rpx;
padding: 10rpx 20rpx;
border-radius: 15rpx;
}
.action-btn.edit {
background-color: #e8f0fe;
color: rgb(66, 121, 240);
}
.action-btn.delete {
background-color: #ffebee;
color: #f44336;
}
.sales-stats {
display: flex;
flex-direction: row;
gap: 20rpx;
}
.stat-item {
flex: 1;
text-align: center;
padding: 30rpx 0;
background: linear-gradient(135deg, #eef2fe, #ece9fa);
border-radius: 12rpx;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: rgb(66, 121, 240);
margin-bottom: 10rpx;
}
.stat-label {
font-size: 22rpx;
color: #666;
}
.review-summary {
margin-bottom: 30rpx;
}
.rating-info {
display: flex;
flex-direction: row;
align-items: center;
gap: 20rpx;
}
.rating-score {
font-size: 48rpx;
font-weight: bold;
color: #ffa726;
}
.rating-stars {
display: flex;
flex-direction: row;
}
.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;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 15rpx;
}
.reviewer-name {
font-size: 26rpx;
color: #333;
}
.review-rating {
display: flex;
flex-direction: row;
}
.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.08);
display: flex;
flex-direction: row;
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: rgb(66, 121, 240);
color: #fff;
}
.action-buttons .action-btn.secondary {
background-color: #E8F0FE;
color: rgb(66, 121, 240);
}
.action-buttons .action-btn.danger {
background-color: #ffebee;
color: #f44336;
}
</style>