765 lines
17 KiB
Plaintext
765 lines
17 KiB
Plaintext
<<<<<<< HEAD
|
|
<!-- 商家端 - 商品管理列表页面 -->
|
|
=======
|
|
<!-- 商家端 - 商品管理列表页面 -->
|
|
>>>>>>> local-backup-root-cyj
|
|
<template>
|
|
<view class="products-page">
|
|
<!-- 搜索栏 -->
|
|
<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">
|
|
<view
|
|
class="filter-tab"
|
|
:class="{ active: currentFilter === 'all' }"
|
|
@click="switchFilter('all')"
|
|
>
|
|
全部
|
|
</view>
|
|
<view
|
|
class="filter-tab"
|
|
: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>
|
|
|
|
<!-- 商品列表 -->
|
|
<scroll-view
|
|
class="products-list"
|
|
scroll-y
|
|
:refresher-enabled="true"
|
|
:refresher-triggered="refreshing"
|
|
@refresherrefresh="onRefresh"
|
|
@scrolltolower="loadMore"
|
|
>
|
|
<view v-if="loading && products.length === 0" class="loading-container">
|
|
<text class="loading-text">加载中...</text>
|
|
</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>
|
|
</view>
|
|
|
|
<view v-else>
|
|
<view
|
|
v-for="product in products"
|
|
:key="product.id"
|
|
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>
|
|
<<<<<<< HEAD
|
|
<view class="product-tags" v-if="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>
|
|
</view>
|
|
=======
|
|
<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>
|
|
>>>>>>> local-backup-root-cyj
|
|
<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>
|
|
<view class="stock-row">
|
|
<text class="stock">库存: {{ product.total_stock || 0 }}</text>
|
|
<text class="sales">销量: {{ product.sale_count || 0 }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="product-actions" @click.stop>
|
|
<view
|
|
class="action-btn"
|
|
:class="product.status === 1 ? 'warning' : 'success'"
|
|
@click="toggleStatus(product)"
|
|
>
|
|
{{ product.status === 1 ? '下架' : '上架' }}
|
|
</view>
|
|
<view class="action-btn default" @click="editProduct(product.id)">编辑</view>
|
|
<view class="action-btn danger" @click="deleteProduct(product)">删除</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="loadingMore" class="load-more">
|
|
<text class="load-more-text">加载中...</text>
|
|
</view>
|
|
|
|
<view v-if="!hasMore && products.length > 0" class="no-more">
|
|
<text class="no-more-text">没有更多了</text>
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- 添加商品按钮 -->
|
|
<view class="add-product-btn" @click="addProduct">
|
|
<text class="add-icon">+</text>
|
|
<text class="add-text">添加商品</text>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script lang="uts">
|
|
import supa from '@/components/supadb/aksupainstance.uts'
|
|
|
|
type ProductType = {
|
|
id: string
|
|
merchant_id: string
|
|
category_id: string
|
|
name: string
|
|
subtitle: string
|
|
description: string
|
|
main_image_url: string
|
|
image_urls: string
|
|
base_price: number
|
|
market_price: number
|
|
total_stock: number
|
|
sale_count: number
|
|
status: number
|
|
is_hot: boolean
|
|
is_new: boolean
|
|
is_featured: boolean
|
|
tags: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
searchKeyword: '',
|
|
currentFilter: 'all',
|
|
products: [] as ProductType[],
|
|
loading: false,
|
|
loadingMore: false,
|
|
refreshing: false,
|
|
page: 1,
|
|
limit: 20,
|
|
hasMore: true,
|
|
merchantId: '',
|
|
lowStockThreshold: 10
|
|
}
|
|
},
|
|
|
|
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()
|
|
},
|
|
|
|
onShow() {
|
|
this.loadProducts()
|
|
},
|
|
|
|
methods: {
|
|
async initMerchantId() {
|
|
try {
|
|
const session = supa.getSession()
|
|
if (session != null && session.user != null) {
|
|
this.merchantId = session.user.getString('id') || ''
|
|
}
|
|
if (!this.merchantId) {
|
|
this.merchantId = uni.getStorageSync('user_id') || ''
|
|
}
|
|
} catch (e) {
|
|
console.error('获取商户ID失败:', e)
|
|
}
|
|
},
|
|
|
|
async loadProducts() {
|
|
if (this.loading) return
|
|
this.loading = true
|
|
|
|
try {
|
|
let query = supa
|
|
.from('ml_products')
|
|
.select('*')
|
|
.eq('merchant_id', this.merchantId)
|
|
.order('created_at', { ascending: false })
|
|
.page(this.page)
|
|
.limit(this.limit)
|
|
|
|
if (this.searchKeyword) {
|
|
query = query.like('name', `%${this.searchKeyword}%`)
|
|
}
|
|
|
|
if (this.currentFilter === 'onsale') {
|
|
query = query.eq('status', 1)
|
|
} else if (this.currentFilter === 'offsale') {
|
|
<<<<<<< HEAD
|
|
query = query.eq('status', 0)
|
|
=======
|
|
query = query.eq('status', 2)
|
|
>>>>>>> local-backup-root-cyj
|
|
} else if (this.currentFilter === 'low_stock') {
|
|
query = query.lte('total_stock', this.lowStockThreshold).gte('total_stock', 0)
|
|
}
|
|
|
|
const response = await query.execute()
|
|
|
|
if (response.error != null) {
|
|
console.error('获取商品失败:', response.error)
|
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
const rawData = response.data as any[]
|
|
if (rawData == null || rawData.length === 0) {
|
|
this.products = []
|
|
this.hasMore = false
|
|
return
|
|
}
|
|
|
|
const productsData: ProductType[] = []
|
|
for (let i = 0; i < rawData.length; i++) {
|
|
const item = rawData[i]
|
|
const prodObj = item as UTSJSONObject
|
|
productsData.push({
|
|
id: prodObj.getString('id') || '',
|
|
merchant_id: prodObj.getString('merchant_id') || '',
|
|
category_id: prodObj.getString('category_id') || '',
|
|
name: prodObj.getString('name') || '',
|
|
subtitle: prodObj.getString('subtitle') || '',
|
|
description: prodObj.getString('description') || '',
|
|
main_image_url: prodObj.getString('main_image_url') || '',
|
|
image_urls: prodObj.getString('image_urls') || '',
|
|
base_price: prodObj.getNumber('base_price') || 0,
|
|
market_price: prodObj.getNumber('market_price') || 0,
|
|
total_stock: prodObj.getNumber('total_stock') || 0,
|
|
sale_count: prodObj.getNumber('sale_count') || 0,
|
|
<<<<<<< HEAD
|
|
status: prodObj.getNumber('status') || 0,
|
|
=======
|
|
status: prodObj.getNumber('status') || 1,
|
|
>>>>>>> local-backup-root-cyj
|
|
is_hot: prodObj.getBoolean('is_hot') || false,
|
|
is_new: prodObj.getBoolean('is_new') || false,
|
|
is_featured: prodObj.getBoolean('is_featured') || false,
|
|
tags: prodObj.getString('tags') || '',
|
|
created_at: prodObj.getString('created_at') || '',
|
|
updated_at: prodObj.getString('updated_at') || ''
|
|
} as ProductType)
|
|
}
|
|
|
|
if (this.page === 1) {
|
|
this.products = productsData
|
|
} else {
|
|
this.products = [...this.products, ...productsData]
|
|
}
|
|
|
|
this.hasMore = rawData.length >= this.limit
|
|
} catch (e) {
|
|
console.error('获取商品异常:', e)
|
|
} finally {
|
|
this.loading = false
|
|
this.refreshing = false
|
|
}
|
|
},
|
|
|
|
switchFilter(filter: string) {
|
|
this.currentFilter = filter
|
|
this.page = 1
|
|
this.hasMore = true
|
|
this.loadProducts()
|
|
},
|
|
|
|
handleSearch() {
|
|
this.page = 1
|
|
this.hasMore = true
|
|
this.loadProducts()
|
|
},
|
|
|
|
onRefresh() {
|
|
this.refreshing = true
|
|
this.page = 1
|
|
this.loadProducts()
|
|
},
|
|
|
|
loadMore() {
|
|
if (!this.loadingMore && this.hasMore) {
|
|
this.loadingMore = true
|
|
this.page++
|
|
this.loadProducts().then(() => {
|
|
this.loadingMore = false
|
|
})
|
|
}
|
|
},
|
|
|
|
viewProductDetail(productId: string) {
|
|
uni.navigateTo({
|
|
url: `/pages/mall/merchant/product-detail?productId=${productId}`
|
|
})
|
|
},
|
|
|
|
editProduct(productId: string) {
|
|
uni.navigateTo({
|
|
url: `/pages/mall/merchant/product-edit?productId=${productId}`
|
|
})
|
|
},
|
|
|
|
addProduct() {
|
|
uni.navigateTo({
|
|
url: '/pages/mall/merchant/product-edit'
|
|
})
|
|
},
|
|
|
|
async toggleStatus(product: ProductType) {
|
|
<<<<<<< HEAD
|
|
const newStatus = product.status === 1 ? 0 : 1
|
|
=======
|
|
const newStatus = product.status === 1 ? 2 : 1
|
|
>>>>>>> local-backup-root-cyj
|
|
const actionText = newStatus === 1 ? '上架' : '下架'
|
|
|
|
uni.showModal({
|
|
title: `确认${actionText}`,
|
|
content: `确定要${actionText}该商品吗?`,
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
try {
|
|
const response = await supa
|
|
.from('ml_products')
|
|
.update({
|
|
status: newStatus,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('id', product.id)
|
|
.execute()
|
|
|
|
if (response.error != null) {
|
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
product.status = newStatus
|
|
uni.showToast({ title: `${actionText}成功`, icon: 'success' })
|
|
} catch (e) {
|
|
uni.showToast({ title: '操作失败', icon: 'none' })
|
|
}
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
async deleteProduct(product: ProductType) {
|
|
uni.showModal({
|
|
title: '确认删除',
|
|
content: '删除后将无法恢复,确定要删除该商品吗?',
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
try {
|
|
const response = await supa
|
|
.from('ml_products')
|
|
.delete()
|
|
.eq('id', product.id)
|
|
.execute()
|
|
|
|
if (response.error != null) {
|
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
|
return
|
|
}
|
|
|
|
uni.showToast({ title: '删除成功', icon: 'success' })
|
|
this.loadProducts()
|
|
} catch (e) {
|
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
|
}
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
getStatusClass(status: number): string {
|
|
if (status === 1) return 'status-onsale'
|
|
<<<<<<< HEAD
|
|
if (status === 0) return 'status-offsale'
|
|
=======
|
|
if (status === 2 || status === 0) return 'status-offsale'
|
|
>>>>>>> local-backup-root-cyj
|
|
return 'status-pending'
|
|
},
|
|
|
|
getStatusText(status: number): string {
|
|
if (status === 1) return '在售'
|
|
<<<<<<< HEAD
|
|
if (status === 0) return '已下架'
|
|
=======
|
|
if (status === 2 || status === 0) return '已下架'
|
|
>>>>>>> local-backup-root-cyj
|
|
return '待审核'
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.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-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;
|
|
}
|
|
|
|
.filter-tabs {
|
|
display: flex;
|
|
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.active {
|
|
color: #007AFF;
|
|
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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.empty-icon {
|
|
font-size: 100rpx;
|
|
margin-bottom: 20rpx;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.product-card {
|
|
display: flex;
|
|
background-color: #fff;
|
|
border-radius: 16rpx;
|
|
margin-bottom: 20rpx;
|
|
padding: 24rpx;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.product-image {
|
|
width: 180rpx;
|
|
height: 180rpx;
|
|
border-radius: 12rpx;
|
|
margin-right: 20rpx;
|
|
background-color: #f5f5f5;
|
|
}
|
|
|
|
.product-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.product-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.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-status {
|
|
font-size: 22rpx;
|
|
padding: 4rpx 12rpx;
|
|
border-radius: 12rpx;
|
|
margin-left: 10rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-onsale {
|
|
background-color: #E8F5E9;
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.status-offsale {
|
|
background-color: #FFEBEE;
|
|
color: #F44336;
|
|
}
|
|
|
|
.status-pending {
|
|
background-color: #FFF3E0;
|
|
color: #FF9800;
|
|
}
|
|
|
|
.product-subtitle {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
display: block;
|
|
margin-bottom: 10rpx;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.product-tags {
|
|
display: flex;
|
|
gap: 10rpx;
|
|
margin-bottom: 10rpx;
|
|
}
|
|
|
|
.tag {
|
|
font-size: 20rpx;
|
|
padding: 4rpx 10rpx;
|
|
border-radius: 8rpx;
|
|
}
|
|
|
|
.tag.hot {
|
|
background-color: #FF5722;
|
|
color: #fff;
|
|
}
|
|
|
|
.tag.new {
|
|
background-color: #2196F3;
|
|
color: #fff;
|
|
}
|
|
|
|
<<<<<<< HEAD
|
|
.tag.recommend {
|
|
background-color: #9C27B0;
|
|
color: #fff;
|
|
}
|
|
=======
|
|
.tag.recommend {
|
|
background-color: #9C27B0;
|
|
color: #fff;
|
|
}
|
|
|
|
.tag.vip {
|
|
background-color: #FFC107;
|
|
color: #333;
|
|
font-weight: bold;
|
|
}
|
|
>>>>>>> local-backup-root-cyj
|
|
|
|
.product-stats {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.price-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
}
|
|
|
|
.current-price {
|
|
font-size: 32rpx;
|
|
color: #FF3B30;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.original-price {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
text-decoration: line-through;
|
|
margin-left: 16rpx;
|
|
}
|
|
|
|
.stock-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 22rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.product-actions {
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 16rpx;
|
|
margin-top: 20rpx;
|
|
padding-top: 20rpx;
|
|
border-top: 1rpx solid #f5f5f5;
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 12rpx 24rpx;
|
|
font-size: 24rpx;
|
|
border-radius: 24rpx;
|
|
}
|
|
|
|
.action-btn.success {
|
|
background-color: #E8F5E9;
|
|
color: #4CAF50;
|
|
}
|
|
|
|
.action-btn.warning {
|
|
background-color: #FFF3E0;
|
|
color: #FF9800;
|
|
}
|
|
|
|
.action-btn.default {
|
|
background-color: #F5F5F5;
|
|
color: #666;
|
|
}
|
|
|
|
.action-btn.danger {
|
|
background-color: #FFEBEE;
|
|
color: #F44336;
|
|
}
|
|
|
|
.load-more, .no-more {
|
|
padding: 30rpx 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.load-more-text, .no-more-text {
|
|
font-size: 24rpx;
|
|
color: #999;
|
|
}
|
|
|
|
.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);
|
|
}
|
|
|
|
.add-icon {
|
|
font-size: 40rpx;
|
|
color: #fff;
|
|
margin-right: 10rpx;
|
|
}
|
|
|
|
.add-text {
|
|
font-size: 30rpx;
|
|
color: #fff;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
<<<<<<< HEAD
|
|
=======
|
|
|
|
|
|
|
|
>>>>>>> local-backup-root-cyj
|