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

359 lines
13 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">
<!-- #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="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="adjust-type">
<view class="type-btn" :class="{ active: adjustType === 'set' }" @click="adjustType = 'set'">直接设为</view>
<view class="type-btn" :class="{ active: adjustType === 'add' }" @click="adjustType = 'add'">增加</view>
<view class="type-btn" :class="{ active: adjustType === 'sub' }" @click="adjustType = 'sub'">减少</view>
</view>
<view class="form-item">
<text class="label">{{ adjustType === 'set' ? '新库存数量' : '调整数值' }}</text>
<input class="input" type="number" v-model="newStock" :placeholder="adjustType === 'set' ? '请输入新库存' : '请输入数值'"/>
</view>
<view class="form-item">
<text class="label">备注 (可选)</text>
<input class="input" v-model="stockRemark" 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: '',
adjustType: 'set', // 'set', 'add', 'sub'
stockRemark: ''
}
},
onLoad() {
this.initMerchantId()
},
onShow() {
this.page = 1
this.loadProducts()
this.loadStats()
},
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) {}
},
async loadProducts() {
if (this.loading && this.page === 1) return
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)
if (this.currentFilter === 'low') {
query = query.lte('total_stock', 10) // 简化处理,实际应关联 warning_stock
} else if (this.currentFilter === 'out') {
query = query.eq('total_stock', 0)
}
const response = await query.execute()
if (response.error != null) {
console.error('加载商品失败:', response.error)
return
}
const rawData = response.data as any[]
if (!rawData) return
const productsData: ProductType[] = []
for (let i = 0; i < rawData.length; i++) {
const item = rawData[i] as UTSJSONObject
productsData.push({
id: item.getString('id') || '',
name: item.getString('name') || '',
main_image_url: item.getString('main_image_url') || '',
total_stock: item.getNumber('total_stock') || 0,
warning_stock: item.getNumber('warning_stock') || 10
} 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
}
},
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 = ''
this.adjustType = 'set'
this.stockRemark = ''
this.showStockModal = true
},
closeStockModal() {
this.showStockModal = false
this.currentProduct = null
this.newStock = ''
},
async saveStock() {
const val = parseInt(this.newStock)
if (isNaN(val)) {
uni.showToast({ title: '请输入有效数值', icon: 'none' })
return
}
let finalStock = 0
if (this.adjustType === 'set') {
finalStock = val
} else if (this.adjustType === 'add') {
finalStock = (this.currentProduct?.total_stock || 0) + val
} else if (this.adjustType === 'sub') {
finalStock = (this.currentProduct?.total_stock || 0) - val
}
if (finalStock < 0) {
uni.showToast({ title: '最终库存不能小于0', icon: 'none' })
return
}
uni.showLoading({ title: '更新中...' })
try {
const response = await supa.from('ml_products')
.update({
total_stock: finalStock,
available_stock: finalStock,
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.page = 1
this.loadProducts()
this.loadStats()
} catch (e) {
uni.showToast({ title: '操作异常', icon: 'none' })
} finally {
uni.hideLoading()
}
},
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-body { padding: 30rpx; }
.adjust-type { display: flex; justify-content: space-between; margin-bottom: 30rpx; }
.type-btn { flex: 1; height: 64rpx; line-height: 64rpx; text-align: center; font-size: 24rpx; background-color: #f5f5f5; color: #666; margin: 0 10rpx; border-radius: 32rpx; border: 1rpx solid #eee; }
.type-btn.active { background-color: #E3F2FD; color: #007AFF; border-color: #007AFF; }
.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>