Files
todo-frontend/src/pages/product/manage.vue
Agent 5dc15bb2a9
All checks were successful
continuous-integration/drone/push Build is passing
fix: 商品管理使用getAllProducts接口显示所有商品(包括下架)
2026-04-01 15:18:48 +00:00

687 lines
15 KiB
Vue
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="page">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar">
<input
class="search-input"
v-model="keyword"
placeholder="搜索商品名称"
@confirm="search"
/>
<button class="add-btn" @click="addProduct">+</button>
</view>
</view>
<!-- 分类侧栏 + 商品列表 -->
<view class="content-wrapper">
<!-- 左侧分类 -->
<scroll-view scroll-y class="category-sidebar">
<view class="category-item" :class="{ active: !categoryId }" @click="selectCategory('')">
全部
</view>
<view v-for="cat in categories" :key="cat.categoryId" class="category-item" :class="{ active: categoryId === cat.categoryId }" @click="selectCategory(cat.categoryId)">
{{ cat.name }}
</view>
</scroll-view>
<!-- 右侧商品列表 -->
<scroll-view scroll-y class="product-scroll">
<view class="product-list">
<view
v-for="item in productList"
:key="item.productId"
class="product-item"
>
<view class="product-info" @click="editProduct(item)">
<view class="product-header">
<text class="product-name">{{ item.name }}</text>
<text class="product-category" v-if="item.categoryName">{{ item.categoryName }}</text>
</view>
<text class="product-spec">{{ item.spec || '-' }}</text>
<view class="product-price">
<text class="price">¥{{ item.price }}</text>
<text class="unit">/{{ item.unit }}</text>
</view>
<view class="product-status">
<text :class="['status', item.status === 1 ? 'on' : 'off']">
{{ item.status === 1 ? '上架' : '下架' }}
</text>
</view>
</view>
<view class="product-actions">
<view class="action-btn" @click="toggleStatus(item)">
{{ item.status === 1 ? '下架' : '上架' }}
</view>
<view class="action-btn delete" @click="deleteProduct(item)">
删除
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="productList.length === 0" class="empty">
<text>暂无商品</text>
</view>
</view>
</scroll-view>
</view>
<!-- 商品表单弹窗 -->
<view class="modal" v-if="showModal">
<view class="modal-mask" @click="closeModal"></view>
<view class="modal-content">
<view class="modal-header">
<text>{{ isEdit ? '编辑商品' : '新增商品' }}</text>
<text class="close-btn" @click="closeModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="label"><text class="required">*</text>分类</text>
<picker :range="categories" range-key="name" @change="onCategoryChange">
<view class="picker">
{{ form.categoryId ? getCategoryName(form.categoryId) : '请选择分类' }}
</view>
</picker>
</view>
<view class="form-item">
<text class="label"><text class="required">*</text>商品名称</text>
<input class="input" v-model="form.name" placeholder="请输入商品名称" />
</view>
<view class="form-row size-row">
<view class="form-item half">
<text class="label">长度(mm)</text>
<input class="input" type="digit" v-model="form.length" maxlength="5" />
</view>
<view class="form-item half">
<text class="label">宽度(mm)</text>
<input class="input" type="digit" v-model="form.width" maxlength="5" />
</view>
<view class="form-item half">
<text class="label">面积()</text>
<input class="input" type="digit" v-model="form.area" disabled />
</view>
</view>
<view class="form-row">
<view class="form-item half">
<text class="label">颜色</text>
<input class="input" v-model="form.spec" />
</view>
<view class="form-item half">
<text class="label"><text class="required">*</text>单位</text>
<input class="input" v-model="form.unit" placeholder="如:个、箱、米" />
</view>
</view>
<view class="form-item">
<text class="label"><text class="required">*</text>价格</text>
<input class="input" type="digit" v-model="form.price" placeholder="请输入价格" />
</view>
<view class="form-item">
<text class="label">备注</text>
<textarea class="textarea" v-model="form.remark" />
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="closeModal">取消</button>
<button class="confirm-btn" @click="saveProduct">保存</button>
</view>
</view>
</view>
</view>
</template>
<script>
import productApi from '@/api/product'
import { canManageProduct } from '@/utils/auth'
export default {
data() {
return {
keyword: '',
categoryId: '',
productList: [],
categories: [],
showModal: false,
isEdit: false,
form: {
productId: '',
categoryId: '',
name: '',
spec: '',
unit: '',
price: '',
length: '',
width: '',
area: '',
remark: '',
status: 1
}
}
},
onLoad() {
if (!canManageProduct()) {
uni.showToast({ title: '无权限', icon: 'none' })
uni.navigateBack()
return
}
this.loadCategories()
this.loadProducts()
},
computed: {
computedArea() {
const l = parseFloat(this.form.length)
const w = parseFloat(this.form.width)
// 长度(mm) × 宽度(mm) ÷ 1000000 = 面积(m²)
if (!isNaN(l) && !isNaN(w) && l > 0 && w > 0) {
return (l * w / 1000000).toFixed(4)
}
return ''
}
},
watch: {
computedArea(val) {
this.form.area = val
}
},
methods: {
async loadCategories() {
try {
const categories = await productApi.getCategories()
this.categories = categories || []
} catch (e) {
console.error(e)
}
},
async loadProducts() {
try {
const res = await productApi.getAllProducts({
keyword: this.keyword,
categoryId: this.categoryId,
page: 1,
pageSize: 100
})
this.productList = res.records || []
} catch (e) {
console.error(e)
}
},
selectCategory(id) {
this.categoryId = id
this.loadProducts()
},
search() {
this.loadProducts()
},
addProduct() {
this.isEdit = false
this.form = {
productId: '',
name: '',
spec: '',
unit: '',
price: '',
categoryId: '',
length: '',
width: '',
area: '',
remark: '',
status: 1
}
this.showModal = true
},
editProduct(item) {
this.isEdit = true
this.form = { ...item }
this.showModal = true
},
closeModal() {
this.showModal = false
},
onCategoryChange(e) {
const index = e.detail.value
this.form.categoryId = this.categories[index].categoryId
},
getCategoryName(categoryId) {
const cat = this.categories.find(c => c.categoryId === categoryId)
return cat ? cat.name : ''
},
async saveProduct() {
if (!this.form.categoryId) {
uni.showToast({ title: '请选择分类', icon: 'none' })
return
}
if (!this.form.name) {
uni.showToast({ title: '请输入商品名称', icon: 'none' })
return
}
if (!this.form.unit) {
uni.showToast({ title: '请输入单位', icon: 'none' })
return
}
if (!this.form.price) {
uni.showToast({ title: '请输入价格', icon: 'none' })
return
}
try {
const formData = JSON.parse(JSON.stringify(this.form))
if (this.isEdit) {
await productApi.updateProduct(formData.productId, formData)
uni.showToast({ title: '更新成功', icon: 'success' })
} else {
await productApi.createProduct(formData)
uni.showToast({ title: '创建成功', icon: 'success' })
}
this.closeModal()
this.loadProducts()
} catch (e) {
console.error(e)
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
}
},
async toggleStatus(item) {
const newStatus = item.status === 1 ? 0 : 1
try {
await productApi.updateProduct(item.productId, {
status: newStatus
})
uni.showToast({ title: newStatus === 1 ? '已上架' : '已下架', icon: 'success' })
this.loadProducts()
} catch (e) {
console.error(e)
uni.showToast({ title: '操作失败', icon: 'none' })
}
},
async deleteProduct(item) {
uni.showModal({
title: '确认删除',
content: `确定要删除商品"${item.name}"吗?`,
success: async (res) => {
if (res.confirm) {
try {
await productApi.deleteProduct(item.productId)
uni.showToast({ title: '删除成功', icon: 'success' })
this.loadProducts()
} catch (e) {
console.error(e)
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
}
}
</script>
<style>
.page {
display: flex;
flex-direction: column;
height: 100vh;
}
/* 搜索区域 */
.search-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20rpx;
}
.search-bar {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.95);
border-radius: 50rpx;
padding: 0 20rpx;
height: 70rpx;
}
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.add-btn {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
background: #3cc51f;
color: #fff;
border: none;
border-radius: 50%;
font-size: 36rpx;
margin-left: 16rpx;
}
/* 内容区域:侧栏+列表 */
.content-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
/* 分类侧栏 */
.category-sidebar {
width: 160rpx;
background: #fff;
flex-shrink: 0;
border-right: 1rpx solid #eee;
}
.category-item {
padding: 28rpx 16rpx;
font-size: 26rpx;
color: #666;
text-align: center;
border-left: 6rpx solid transparent;
}
.category-item.active {
background: #f8f9fa;
color: #667eea;
font-weight: bold;
border-left-color: #667eea;
}
/* 商品列表 */
.product-scroll {
flex: 1;
background: #f8f9fa;
}
.product-list {
padding: 20rpx;
}
.search-bar {
display: flex;
margin-bottom: 20rpx;
}
.search-input {
flex: 1;
height: 70rpx;
padding: 0 20rpx;
background: #fff;
border-radius: 8rpx;
font-size: 28rpx;
}
.search-btn {
width: 120rpx;
height: 70rpx;
line-height: 70rpx;
background: #3cc51f;
color: #fff;
border: none;
border-radius: 8rpx;
margin-left: 20rpx;
font-size: 28rpx;
}
.add-btn {
width: 70rpx;
height: 70rpx;
line-height: 70rpx;
background: #1890ff;
color: #fff;
border: none;
border-radius: 8rpx;
margin-left: 20rpx;
font-size: 40rpx;
}
.product-list {
background: #fff;
border-radius: 16rpx;
}
.product-item {
display: flex;
justify-content: space-between;
padding: 24rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.product-info {
flex: 1;
}
.product-name {
font-size: 28rpx;
font-weight: bold;
display: block;
}
.product-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.product-category {
font-size: 22rpx;
color: #667eea;
background: #f0f0ff;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.product-spec {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.product-price {
margin-top: 12rpx;
}
.price {
color: #ff4d4f;
font-size: 28rpx;
font-weight: bold;
}
.unit {
color: #999;
font-size: 24rpx;
}
.product-status {
margin-top: 8rpx;
}
.status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.status.on {
background: #e6f7ff;
color: #1890ff;
}
.status.off {
background: #fff7e6;
color: #fa8c16;
}
.product-actions {
display: flex;
flex-direction: column;
justify-content: center;
}
.action-btn {
padding: 8rpx 20rpx;
background: #e6f7ff;
color: #1890ff;
border-radius: 4rpx;
font-size: 24rpx;
margin-bottom: 10rpx;
}
.action-btn.delete {
background: #fff1f0;
color: #ff4d4f;
}
.empty {
padding: 100rpx;
text-align: center;
color: #999;
}
/* 弹窗 */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
}
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
max-height: 80vh;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
font-size: 32rpx;
font-weight: bold;
}
.close-btn {
font-size: 48rpx;
color: #999;
}
.modal-body {
padding: 30rpx;
max-height: 60vh;
overflow-y: auto;
}
.form-item {
margin-bottom: 24rpx;
}
.form-row {
display: flex;
gap: 20rpx;
}
.form-row .half {
flex: 1;
}
.size-row {
display: flex;
gap: 16rpx;
}
.size-row .half {
flex: 1;
}
.form-row.three .half {
flex: 1;
}
.label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.required {
color: #ff4d4f;
margin-right: 4rpx;
}
.input[disabled] {
background: #f5f5f5;
color: #999;
}
.input {
width: 100%;
height: 70rpx;
padding: 0 20rpx;
background: #fff;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.picker {
width: 100%;
height: 70rpx;
padding: 0 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
line-height: 70rpx;
}
.textarea {
width: 100%;
height: 150rpx;
padding: 20rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
}
.modal-footer {
display: flex;
padding: 30rpx;
border-top: 1rpx solid #f5f5f5;
}
.cancel-btn, .confirm-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 28rpx;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
margin-right: 20rpx;
}
.confirm-btn {
background: #3cc51f;
color: #fff;
}
</style>