723 lines
16 KiB
Vue
723 lines
16 KiB
Vue
<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">
|
||
<view class="product-header">
|
||
<text class="product-name">{{ item.name }}</text>
|
||
<text class="product-category" v-if="item.categoryName">{{ item.categoryName }}</text>
|
||
</view>
|
||
<view class="product-row">
|
||
<text class="product-spec">{{ item.spec || '-' }}</text>
|
||
<text class="product-unit">/{{ item.unit }}</text>
|
||
</view>
|
||
<view class="product-spec-row" v-if="item.length && item.width">
|
||
<text class="spec-text">规格:{{ item.length }} x {{ item.width }} = {{ item.area }} m²</text>
|
||
</view>
|
||
<view class="product-price">
|
||
<text class="price">¥{{ item.price }}</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 edit" @click="editProduct(item)" v-if="item.status === 1">
|
||
编辑
|
||
</view>
|
||
<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">面积(m²)</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;
|
||
display: block;
|
||
}
|
||
|
||
.product-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.product-unit {
|
||
color: #999;
|
||
font-size: 24rpx;
|
||
margin-left: 4rpx;
|
||
}
|
||
|
||
.product-spec-row {
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.spec-text {
|
||
font-size: 24rpx;
|
||
color: #666;
|
||
background: #f5f5f5;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.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.edit {
|
||
background: #f0f5ff;
|
||
color: #667eea;
|
||
}
|
||
|
||
.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>
|