初始化上传医疗项目到 medical-mall

This commit is contained in:
2026-04-10 09:03:21 +08:00
parent ca8794ea3a
commit ce124e7119
421 changed files with 15139 additions and 2363 deletions

View File

@@ -1,4 +1,4 @@
<!-- 商家端 - 商品管理列表页面 -->
<!-- 机构端 - 服务管理列表页面 -->
<template>
<view class="products-page">
<!-- #ifdef MP-WEIXIN -->
@@ -9,17 +9,7 @@
</view>
</view>
<!-- #endif -->
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="searchKeyword"
placeholder="搜索商品名称"
@confirm="handleSearch"
/>
<view class="search-btn" @click="handleSearch">搜索</view>
</view>
<!-- 筛选标签 -->
<view class="filter-tabs">
@@ -35,25 +25,37 @@
:class="{ active: currentFilter === 'onsale' }"
@click="switchFilter('onsale')"
>
上架
可预约
</view>
<view
class="filter-tab"
:class="{ active: currentFilter === 'offsale' }"
@click="switchFilter('offsale')"
>
下架
</view>
<view
class="filter-tab"
:class="{ active: currentFilter === 'low_stock' }"
@click="switchFilter('low_stock')"
>
库存预警
暂停服务
</view>
<view
class="filter-tab"
:class="{ active: currentFilter === 'low_stock' }"
@click="switchFilter('low_stock')"
>
器械预警
</view>
</view>
<!-- 商品列表 -->
<!-- 搜索栏 -->
<view class="search-bar">
<input
class="search-input"
type="text"
v-model="searchKeyword"
placeholder="搜索服务/商品名称"
@confirm="handleSearch"
/>
<view class="search-btn" @click="handleSearch">搜索</view>
</view>
<!-- 服务列表 -->
<scroll-view
class="products-list"
scroll-y
@@ -67,9 +69,9 @@
</view>
<view v-else-if="products.length === 0" class="empty-container">
<text class="empty-icon">📦</text>
<text class="empty-text">暂无商品</text>
<view class="add-first-btn" @click="addProduct">添加第一个商品</view>
<text class="empty-icon">🏥</text>
<text class="empty-text">暂无服务项目</text>
<view class="add-first-btn" @click="addProduct">添加第一个服务项目</view>
</view>
<view v-else>
@@ -79,46 +81,55 @@
class="product-card"
@click="viewProductDetail(product.id)"
>
<image
:src="product.main_image_url || '/static/images/default-product.png'"
class="product-image"
mode="aspectFill"
/>
<view class="product-info">
<view class="product-header">
<text class="product-name">{{ product.name }}</text>
<text class="product-status" :class="getStatusClass(product.status)">
{{ getStatusText(product.status) }}
</text>
</view>
<text class="product-subtitle">{{ product.subtitle || '暂无描述' }}</text>
<view class="product-tags">
<text v-if="product.is_hot" class="tag hot">热</text>
<text v-if="product.is_new" class="tag new">新</text>
<text v-if="product.is_featured" class="tag recommend">荐</text>
<text v-if="product.is_vip_discount" class="tag vip">VIP</text>
</view>
<view class="product-stats">
<view class="price-row">
<text class="current-price">¥{{ product.base_price }}</text>
<text v-if="product.market_price" class="original-price">¥{{ product.market_price }}</text>
<!-- 上半部分:图片 + 服务信息 -->
<view class="product-main">
<image
:src="product.main_image_url || '/static/images/default-product.png'"
class="product-thumb"
mode="aspectFill"
/>
<view class="product-content">
<!-- 第一层:服务名称 + 状态 badge -->
<view class="product-header">
<text class="product-name">{{ product.name }}</text>
<text class="product-status-badge" :class="getStatusClass(product.status)">
{{ getStatusText(product.status) }}
</text>
</view>
<view class="stock-row">
<text class="stock">库存: {{ product.total_stock || 0 }}</text>
<text class="sales">销量: {{ product.sale_count || 0 }}</text>
<!-- 第二层:描述 -->
<text class="product-desc">{{ product.subtitle || '暂无描述' }}</text>
<!-- 标签行(按需展示) -->
<view v-if="product.is_hot || product.is_new || product.is_featured || product.is_vip_discount" class="product-tags">
<text v-if="product.is_hot" class="tag tag-hot">热</text>
<text v-if="product.is_new" class="tag tag-new">新</text>
<text v-if="product.is_featured" class="tag tag-recommend">荐</text>
<text v-if="product.is_vip_discount" class="tag tag-vip">关怀</text>
</view>
<!-- 第三层:价格高亮 -->
<view class="product-price">
<text class="price-current">¥{{ product.base_price }}</text>
<text v-if="product.market_price" class="price-original">¥{{ product.market_price }}</text>
</view>
<!-- 第四层:名额、服务次数辅助信息 -->
<view class="product-meta">
<text class="meta-item">可约名额 {{ product.total_stock || 0 }}</text>
<text class="meta-sep">·</text>
<text class="meta-item">服务次数 {{ product.sale_count || 0 }}</text>
</view>
</view>
</view>
<!-- 分割线 -->
<view class="product-divider"></view>
<!-- 操作栏 -->
<view class="product-actions" @click.stop>
<view
class="action-btn"
:class="product.status === 1 ? 'warning' : 'success'"
class="action-btn action-secondary"
@click="toggleStatus(product)"
>
{{ product.status === 1 ? '下架' : '上架' }}
{{ product.status === 1 ? '暂停服务' : '开启服务' }}
</view>
<view class="action-btn default" @click="editProduct(product.id)">编辑</view>
<view class="action-btn danger" @click="deleteProduct(product)">删除</view>
<view class="action-btn action-edit" @click="editProduct(product.id)">编辑</view>
<view class="action-btn action-danger" @click="deleteProduct(product)">删除</view>
</view>
</view>
</view>
@@ -132,10 +143,10 @@
</view>
</scroll-view>
<!-- 添加商品按钮 -->
<!-- 添加服务按钮 -->
<view class="add-product-btn" @click="addProduct">
<text class="add-icon">+</text>
<text class="add-text">添加商品</text>
<text class="add-text">添加服务项目</text>
</view>
</view>
</template>
@@ -160,6 +171,7 @@
is_hot: boolean
is_new: boolean
is_featured: boolean
is_vip_discount: boolean
tags: string
created_at: string
updated_at: string
@@ -182,14 +194,14 @@
}
},
onLoad(options: any) {
const type = options.type as string
if (type === 'add') {
this.addProduct()
} else if (type === 'low_stock') {
this.currentFilter = 'low_stock'
}
this.initMerchantId()
async onLoad(options: any) {
const type = options.type as string
if (type === 'add') {
this.addProduct()
} else if (type === 'low_stock') {
this.currentFilter = 'low_stock'
}
await this.initMerchantId()
},
onShow() {
@@ -342,11 +354,11 @@
async toggleStatus(product: ProductType) {
const newStatus = product.status === 1 ? 2 : 1
const actionText = newStatus === 1 ? '上架' : '下架'
const actionText = newStatus === 1 ? '开启服务' : '暂停服务'
uni.showModal({
title: `确认${actionText}`,
content: `确定要${actionText}该商品吗?`,
content: `确定要${actionText}该服务项目吗?`,
success: async (res) => {
if (res.confirm) {
try {
@@ -377,7 +389,7 @@
async deleteProduct(product: ProductType) {
uni.showModal({
title: '确认删除',
content: '删除后将无法恢复,确定要删除该商品吗?',
content: '删除后将无法恢复,确定要删除该服务项目吗?',
success: async (res) => {
if (res.confirm) {
try {
@@ -410,318 +422,334 @@
getStatusText(status: number): string {
if (status === 1) return '在售'
if (status === 2 || status === 0) return '已下'
if (status === 2 || status === 0) return '已下线'
return '待审核'
}
}
}
</script>
<style>
.products-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 140rpx;
}
.products-page {
background-color: #f5f5f5;
min-height: 100vh;
padding-bottom: 140rpx;
}
.search-bar {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
}
.search-bar {
display: flex;
flex-direction: row;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
}
.search-input {
flex: 1;
height: 64rpx;
background-color: #f5f5f5;
border-radius: 32rpx;
padding: 0 30rpx;
font-size: 26rpx;
}
.search-input {
flex: 1;
height: 64rpx;
background-color: #f5f5f5;
border-radius: 32rpx;
padding: 0 30rpx;
font-size: 26rpx;
}
.search-btn {
margin-left: 20rpx;
padding: 16rpx 30rpx;
background-color: #007AFF;
color: #fff;
font-size: 26rpx;
border-radius: 32rpx;
}
.search-btn {
margin-left: 20rpx;
padding: 16rpx 30rpx;
background-color: rgb(66, 121, 240);
color: #fff;
font-size: 26rpx;
border-radius: 32rpx;
}
.filter-tabs {
display: flex;
background-color: #fff;
padding: 0 20rpx;
margin-bottom: 20rpx;
}
.filter-tabs {
display: flex;
flex-direction: row;
background-color: #fff;
padding: 0 20rpx;
margin-bottom: 20rpx;
}
.filter-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 26rpx;
color: #666;
position: relative;
}
.filter-tab {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 26rpx;
color: #666;
position: relative;
}
.filter-tab.active {
color: #007AFF;
font-weight: bold;
}
.filter-tab.active {
color: rgb(66, 121, 240);
font-weight: bold;
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
.filter-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: rgb(66, 121, 240);
border-radius: 2rpx;
}
.products-list {
padding: 0 20rpx;
height: calc(100vh - 260rpx);
}
.products-list {
padding: 0 20rpx;
height: calc(100vh - 260rpx);
}
.loading-container, .empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-container, .empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-icon {
font-size: 100rpx;
margin-bottom: 20rpx;
}
.empty-text, .loading-text {
font-size: 28rpx;
color: #999;
}
.empty-text, .loading-text {
font-size: 28rpx;
color: #999;
}
.add-first-btn {
margin-top: 30rpx;
padding: 20rpx 60rpx;
background-color: #007AFF;
color: #fff;
font-size: 28rpx;
border-radius: 40rpx;
}
.add-first-btn {
margin-top: 30rpx;
padding: 20rpx 60rpx;
background-color: rgb(66, 121, 240);
color: #fff;
font-size: 28rpx;
border-radius: 40rpx;
}
.product-card {
display: flex;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 24rpx;
flex-wrap: wrap;
}
.product-card {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.product-image {
width: 180rpx;
height: 180rpx;
border-radius: 12rpx;
margin-right: 20rpx;
background-color: #f5f5f5;
}
.product-main {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 24rpx;
}
.product-info {
flex: 1;
min-width: 0;
}
.product-thumb {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background-color: #f0f0f0;
flex-shrink: 0;
margin-right: 20rpx;
}
.product-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10rpx;
}
.product-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.product-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
.product-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
margin-left: 10rpx;
flex-shrink: 0;
}
.product-name {
font-size: 28rpx;
color: #222;
font-weight: 600;
flex: 1;
line-height: 1.45;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-right: 12rpx;
}
.status-onsale {
background-color: #E8F5E9;
color: #4CAF50;
}
.product-status-badge {
font-size: 20rpx;
padding: 4rpx 14rpx;
border-radius: 20rpx;
flex-shrink: 0;
font-weight: 500;
}
.status-offsale {
background-color: #FFEBEE;
color: #F44336;
}
.status-onsale {
background-color: #E8F5E9;
color: #4CAF50;
}
.status-pending {
background-color: #FFF3E0;
color: #FF9800;
}
.status-offsale {
background-color: #F5F5F5;
color: #999;
}
.product-subtitle {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 10rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-pending {
background-color: #E8F0FE;
color: rgb(66, 121, 240);
}
.product-tags {
display: flex;
gap: 10rpx;
margin-bottom: 10rpx;
}
.product-desc {
font-size: 24rpx;
color: #aaa;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.4;
}
.tag {
font-size: 20rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
}
.product-tags {
display: flex;
flex-direction: row;
gap: 8rpx;
}
.tag.hot {
background-color: #FF5722;
color: #fff;
}
.tag {
font-size: 18rpx;
padding: 3rpx 10rpx;
border-radius: 6rpx;
}
.tag.new {
background-color: #2196F3;
color: #fff;
}
.tag-hot {
background-color: #FF5722;
color: #fff;
}
.tag.recommend {
background-color: #9C27B0;
color: #fff;
}
.tag-new {
background-color: rgb(66, 121, 240);
color: #fff;
}
.tag.vip {
background-color: #FFC107;
color: #333;
font-weight: bold;
}
.tag-recommend {
background-color: #9C27B0;
color: #fff;
}
.product-stats {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.tag-vip {
background-color: #e8f0fe;
color: rgb(66, 121, 240);
font-weight: bold;
}
.price-row {
display: flex;
align-items: baseline;
}
.product-price {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 12rpx;
}
.current-price {
font-size: 32rpx;
color: #FF3B30;
font-weight: bold;
}
.price-current {
font-size: 34rpx;
color: rgb(225, 37, 27);
font-weight: bold;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
margin-left: 16rpx;
}
.price-original {
font-size: 22rpx;
color: #ccc;
text-decoration: line-through;
}
.stock-row {
display: flex;
justify-content: space-between;
font-size: 22rpx;
color: #999;
}
.product-meta {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.product-actions {
width: 100%;
display: flex;
justify-content: flex-end;
gap: 16rpx;
margin-top: 20rpx;
padding-top: 20rpx;
border-top: 1rpx solid #f5f5f5;
}
.meta-item {
font-size: 22rpx;
color: #bbb;
}
.action-btn {
padding: 12rpx 24rpx;
font-size: 24rpx;
border-radius: 24rpx;
}
.meta-sep {
font-size: 22rpx;
color: #ddd;
}
.action-btn.success {
background-color: #E8F5E9;
color: #4CAF50;
}
.product-divider {
height: 1rpx;
background-color: #f5f5f5;
margin: 0 24rpx;
}
.action-btn.warning {
background-color: #FFF3E0;
color: #FF9800;
}
.product-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
padding: 16rpx 24rpx;
gap: 16rpx;
}
.action-btn.default {
background-color: #F5F5F5;
color: #666;
}
.action-btn {
padding: 12rpx 28rpx;
font-size: 24rpx;
border-radius: 32rpx;
}
.action-btn.danger {
background-color: #FFEBEE;
color: #F44336;
}
.action-edit {
background-color: rgb(66, 121, 240);
color: #fff;
}
.load-more, .no-more {
padding: 30rpx 0;
text-align: center;
}
.action-secondary {
background-color: #f5f5f5;
color: #666;
}
.load-more-text, .no-more-text {
font-size: 24rpx;
color: #999;
}
.action-danger {
background-color: #FFEBEE;
color: #F44336;
}
.add-product-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 300rpx;
height: 88rpx;
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
border-radius: 44rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 122, 255, 0.3);
}
.load-more, .no-more {
padding: 30rpx 0;
text-align: center;
}
.add-icon {
font-size: 40rpx;
color: #fff;
margin-right: 10rpx;
}
.load-more-text, .no-more-text {
font-size: 24rpx;
color: #999;
}
.add-text {
font-size: 30rpx;
color: #fff;
font-weight: bold;
}
</style>
.add-product-btn {
position: fixed;
bottom: 30rpx;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 320rpx;
height: 88rpx;
background: linear-gradient(135deg, rgb(66, 121, 240) 0%, #5856D6 100%);
border-radius: 44rpx;
box-shadow: 0 8rpx 20rpx rgba(66, 121, 240, 0.35);
}
.add-icon {
font-size: 40rpx;
color: #fff;
margin-right: 10rpx;
}
.add-text {
font-size: 30rpx;
color: #fff;
font-weight: bold;
}
</style>