Files
medical-mall/pages/mall/merchant/inventory.uvue

289 lines
11 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="inventory-page">
<view class="stats-bar">
<view class="stat-item">
<text class="stat-value">{{ stats.totalProducts }}</text>
<text class="stat-label">商品总数</text>
</view>
<view class="stat-item warning">
<text class="stat-value">{{ stats.lowStock }}</text>
<text class="stat-label">库存预警</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ stats.outOfStock }}</text>
<text class="stat-label">已售罄</text>
</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 === 'low' }" @click="switchFilter('low')">库存预警</view>
<view class="filter-tab" :class="{ active: currentFilter === 'out' }" @click="switchFilter('out')">已售罄</view>
</view>
<scroll-view class="inventory-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>
<view v-else>
<view v-for="product in products" :key="product.id" class="product-card">
<image :src="product.main_image_url || '/static/images/default-product.png'" class="product-image" mode="aspectFill"/>
<view class="product-info">
<text class="product-name">{{ product.name }}</text>
<view class="stock-info">
<text class="stock-label">库存:</text>
<text class="stock-value" :class="getStockClass(product.total_stock)">{{ product.total_stock }}</text>
</view>
<view class="warning-info" v-if="product.total_stock <= (product.warning_stock || 10)">
<text class="warning-text">库存不足</text>
</view>
</view>
<view class="product-actions">
<view class="action-btn" @click="editStock(product)">调整库存</view>
</view>
</view>
</view>
</scroll-view>
<view v-if="showStockModal" class="modal-mask" @click="closeStockModal">
<view class="modal-content" @click.stop>
<view class="modal-header"><text class="modal-title">调整库存</text><text class="modal-close" @click="closeStockModal">×</text></view>
<view class="modal-body">
<view class="form-item">
<text class="label">商品</text>
<text class="value">{{ currentProduct?.name }}</text>
</view>
<view class="form-item">
<text class="label">当前库存</text>
<text class="value">{{ currentProduct?.total_stock }}</text>
</view>
<view class="form-item">
<text class="label">新库存</text>
<input class="input" type="number" v-model="newStock" placeholder="请输入新库存"/>
</view>
</view>
<view class="modal-footer">
<view class="modal-btn cancel" @click="closeStockModal">取消</view>
<view class="modal-btn confirm" @click="saveStock">保存</view>
</view>
</view>
</view>
</view>
</template>
<script lang="uts">
import supa from '@/components/supadb/aksupainstance.uts'
type ProductType = {
id: string
name: string
main_image_url: string
total_stock: number
warning_stock: number
}
export default {
data() {
return {
currentFilter: 'all',
products: [] as ProductType[],
loading: false,
refreshing: false,
page: 1,
limit: 20,
hasMore: true,
merchantId: '',
stats: { totalProducts: 0, lowStock: 0, outOfStock: 0 },
showStockModal: false,
currentProduct: null as ProductType | null,
newStock: ''
}
},
onLoad() {
this.initMerchantId()
},
onShow() {
this.loadProducts()
this.loadStats()
},
methods: {
async initMerchantId() {
try {
const session = supa.getSession()
this.merchantId = session?.user?.getString('id') || uni.getStorageSync('user_id') || ''
} catch (e) {}
},
async loadProducts() {
this.loading = true
try {
let query = supa.from('ml_products').select('id, name, main_image_url, total_stock, warning_stock').eq('merchant_id', this.merchantId).order('total_stock', { ascending: true }).page(this.page).limit(this.limit)
const response = await query.execute()
if (response.error != null || !response.data) {
this.products = []
return
}
const rawData = response.data as any[]
let productsData: ProductType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
const stock = item.getNumber('total_stock') || 0
const warning = item.getNumber('warning_stock') || 10
if (this.currentFilter === 'low' && stock > warning) continue
if (this.currentFilter === 'out' && stock > 0) continue
productsData.push({
id: item.getString('id') || '',
name: item.getString('name') || '',
main_image_url: item.getString('main_image_url') || '',
total_stock: stock,
warning_stock: warning
})
}
this.products = productsData
} catch (e) {
console.error('加载失败:', e)
} finally {
this.loading = false
this.refreshing = false
}
},
async loadStats() {
try {
const response = await supa.from('ml_products').select('id, total_stock, warning_stock', { count: 'exact' }).eq('merchant_id', this.merchantId).execute()
if (response.error != null || !response.data) return
let total = 0, low = 0, out = 0
const rawData = response.data as any[]
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
total++
const stock = item.getNumber('total_stock') || 0
const warning = item.getNumber('warning_stock') || 10
if (stock === 0) out++
else if (stock <= warning) low++
}
this.stats = { totalProducts: total, lowStock: low, outOfStock: out }
} catch (e) {}
},
switchFilter(filter: string) {
this.currentFilter = filter
this.loadProducts()
},
onRefresh() {
this.refreshing = true
this.loadProducts()
this.loadStats()
},
loadMore() {
if (this.hasMore && !this.loading) {
this.page++
this.loadProducts()
}
},
editStock(product: ProductType) {
this.currentProduct = product
this.newStock = String(product.total_stock)
this.showStockModal = true
},
closeStockModal() {
this.showStockModal = false
this.currentProduct = null
this.newStock = ''
},
async saveStock() {
if (!this.newStock || isNaN(parseInt(this.newStock))) {
uni.showToast({ title: '请输入有效库存', icon: 'none' })
return
}
try {
const response = await supa.from('ml_products').update({ total_stock: parseInt(this.newStock), updated_at: new Date().toISOString() }).eq('id', this.currentProduct!.id).execute()
if (response.error != null) {
uni.showToast({ title: '保存失败', icon: 'none' })
return
}
uni.showToast({ title: '保存成功', icon: 'success' })
this.closeStockModal()
this.loadProducts()
this.loadStats()
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
}
},
getStockClass(stock: number): string {
if (stock === 0) return 'out'
if (stock <= 10) return 'low'
return 'normal'
}
}
}
</script>
<style>
.inventory-page { background-color: #f5f5f5; min-height: 100vh; }
.stats-bar { display: flex; background-color: #fff; padding: 30rpx 20rpx; margin-bottom: 20rpx; }
.stat-item { flex: 1; text-align: center; }
.stat-value { font-size: 40rpx; font-weight: bold; color: #333; display: block; }
.stat-item.warning .stat-value { color: #FF9800; }
.stat-label { font-size: 24rpx; color: #999; }
.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; }
.inventory-list { padding: 0 20rpx; height: calc(100vh - 280rpx); }
.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; }
.product-card { display: flex; align-items: center; background-color: #fff; border-radius: 16rpx; padding: 20rpx; margin-bottom: 16rpx; }
.product-image { width: 120rpx; height: 120rpx; border-radius: 8rpx; margin-right: 20rpx; background-color: #f5f5f5; }
.product-info { flex: 1; }
.product-name { font-size: 28rpx; color: #333; font-weight: 500; display: block; margin-bottom: 10rpx; }
.stock-info { display: flex; align-items: center; }
.stock-label { font-size: 24rpx; color: #999; margin-right: 10rpx; }
.stock-value { font-size: 28rpx; font-weight: bold; }
.stock-value.normal { color: #4CAF50; }
.stock-value.low { color: #FF9800; }
.stock-value.out { color: #F44336; }
.warning-info { margin-top: 8rpx; }
.warning-text { font-size: 22rpx; color: #F44336; }
.product-actions { margin-left: 20rpx; }
.action-btn { padding: 12rpx 24rpx; font-size: 24rpx; background-color: #E3F2FD; color: #1976D2; border-radius: 24rpx; }
.modal-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-content { width: 80%; background-color: #fff; border-radius: 16rpx; }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 30rpx; border-bottom: 1rpx solid #f5f5f5; }
.modal-title { font-size: 32rpx; font-weight: bold; color: #333; }
.modal-close { font-size: 44rpx; color: #999; }
.modal-body { padding: 30rpx; }
.form-item { margin-bottom: 20rpx; }
.form-item .label { font-size: 26rpx; color: #999; display: block; margin-bottom: 10rpx; }
.form-item .value { font-size: 28rpx; color: #333; }
.input { height: 72rpx; border: 1rpx solid #e5e5e5; border-radius: 8rpx; padding: 0 20rpx; font-size: 28rpx; }
.modal-footer { display: flex; border-top: 1rpx solid #f5f5f5; }
.modal-btn { flex: 1; height: 88rpx; line-height: 88rpx; text-align: center; font-size: 28rpx; }
.modal-btn.cancel { color: #666; border-right: 1rpx solid #f5f5f5; }
.modal-btn.confirm { color: #007AFF; font-weight: bold; }
</style>