Compare commits

...

52 Commits

Author SHA1 Message Date
Agent
646f69bb56 fix: 修复createCategory参数顺序
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-04 08:09:25 +00:00
Agent
756444ef2b fix: 退货页面调用后端退款接口恢复库存
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-03 06:58:31 +00:00
Agent
6dfc201c66 fix: 增加数量和单价输入框宽度
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-03 02:18:23 +00:00
Agent
870e1c612d fix: 商品名称固定宽度,库存移到最后
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-03 01:45:14 +00:00
Agent
fbcd930887 fix: 订单创建页面商品名称规格长宽放同一行
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-03 01:38:46 +00:00
Agent
a1bcc2e478 feat: 订单创建页面商品数量支持+号-号,且不超过库存
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-03 01:32:24 +00:00
Agent
a84cd57fad feat: 选择商品页面显示库存,库存为0不可选中
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-03 01:23:47 +00:00
Agent
c49a6d8288 fix: 入库页面商品卡片参照商品管理页面显示
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:31:17 +00:00
Agent
796d083823 fix: 移除库存管理入口,保留入库和库存流水
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:28:01 +00:00
Agent
817f6e3436 fix: 首页商品管理入口改为switchTab跳转tabbar页面
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:24:04 +00:00
Agent
3e32aa6313 debug: 添加success fail回调
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:21:38 +00:00
Agent
255a0be7f2 debug: 添加alert测试
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:19:12 +00:00
Agent
1c422f5436 fix: 移除商品管理页面权限限制
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:13:06 +00:00
Agent
46894e04fe fix: 底部导航商品入口改为商品管理页面
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 13:01:08 +00:00
Agent
1ea0fc04bf feat: 商品列表查看详情跳转到商品管理进行编辑
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 12:56:32 +00:00
Agent
7a9c31ca27 fix: 增加总面积列宽度
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 12:07:02 +00:00
Agent
7b24ea86f5 fix: 商品名称规格长宽显示在同一行
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 11:58:16 +00:00
Agent
b789a4af92 fix: 分享订单页商品明细与订单详情保持一致
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 11:53:32 +00:00
Agent
c6f93aaa44 fix: 面积数值不显示单位
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 11:07:10 +00:00
Agent
3fe8c80696 fix: 商品名称和规格同一行显示,不同颜色
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 10:04:01 +00:00
Agent
3860c9583b fix: 合并商品名称规格长宽为一列
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 09:59:10 +00:00
Agent
82bebc0dd8 fix: 订单详情小计显示整数
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 09:51:31 +00:00
Agent
2c68fe898a fix: 编辑订单时加载length、width、area字段
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 09:12:26 +00:00
Agent
e8259fbf91 fix: 创建订单时传递length、width、area参数
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 09:04:53 +00:00
Agent
84b0d15a0c feat: 订单详情页显示商品规格、长度宽度和总面积
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-02 04:23:43 +00:00
Agent
d932d26830 fix: 订单列表直接使用返回的items,不再单独请求
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 16:39:54 +00:00
Agent
b62c62c6e0 feat: 订单商品明细显示颜色长度宽高度数量总面面积
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 16:25:41 +00:00
Agent
10b421d682 fix: 商品种类放左侧,分类侧栏
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 16:18:59 +00:00
Agent
1899ebf226 fix: 点击商品直接选中,显示长宽面积与颜色同一行
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 16:14:42 +00:00
Agent
02f151307d fix: 恢复选择商品页面正确版本,修复模板标签平衡
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 16:12:58 +00:00
Agent
c0adc172c1 fix: 点击商品卡片直接选中,不再弹窗编辑长度
Some checks failed
continuous-integration/drone/push Build is failing
2026-04-01 16:08:23 +00:00
Agent
b90847a0f1 fix: 修复选择商品页面模板结构,分类侧栏
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:59:54 +00:00
Agent
e25d288fae fix: 选择商品页面分类改回左侧侧栏
Some checks failed
continuous-integration/drone/push Build is failing
2026-04-01 15:53:17 +00:00
Agent
0500d3e688 feat: 选择商品页面卡片显示长宽面积与颜色同一行
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:48:40 +00:00
Agent
45d6cc53ca feat: 商品卡片颜色单位价格显示一行
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:37:28 +00:00
Agent
d9881a50c1 feat: 商品卡片颜色单位显示一行,有面积时另起一行显示规格
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:30:50 +00:00
Agent
6df829cf90 feat: 商品卡片增加编辑按钮,仅上架商品可编辑,状态改为已上架/已下架
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:24:27 +00:00
Agent
5dc15bb2a9 fix: 商品管理使用getAllProducts接口显示所有商品(包括下架)
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:18:48 +00:00
Agent
08f440f7c8 fix: 修复下架按钮API参数位置
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:11:02 +00:00
Agent
826973f42a feat: 商品管理增加分类侧栏,商品卡片显示分类
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 15:01:29 +00:00
Agent
e040f6d93b fix: 修复API参数顺序确保data传值
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:53:09 +00:00
Agent
a38039f4aa fix: 修复API参数位置
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:48:42 +00:00
Agent
5af9b7c0ff fix: 修复商品保存时数据为空的问题
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:45:39 +00:00
Agent
10ebbd6b6f feat: 规格改为颜色
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:41:03 +00:00
Agent
0772a91c26 feat: 分类改为必填项
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:36:48 +00:00
Agent
3d827c0033 style: 输入框增加边框,更明显
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:33:23 +00:00
Agent
f388a0c5b1 fix: 增加长度宽度面积输入框间距
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:29:44 +00:00
Agent
324bd1166f feat: 商品表单调整顺序,必填项*改为红色
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:24:14 +00:00
Agent
64767e9ca0 fix: 修复长度宽度面积三列在一行的样式
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:15:26 +00:00
Agent
a4f548b847 feat: 商品管理长度宽度单位改为mm,面积改为m²,最多5位数字
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:11:48 +00:00
Agent
82efb8c251 fix: 商品管理面积计算改为平方米
All checks were successful
continuous-integration/drone/push Build is passing
2026-04-01 14:06:46 +00:00
Agent
d7995d8cc5 feat: 选择商品弹窗增加长度、宽度、面积字段,自动计算面积 2026-04-01 14:01:50 +00:00
12 changed files with 886 additions and 152 deletions

View File

@@ -15,7 +15,7 @@ export default {
* 新增分类
*/
createCategory(data) {
return api.request('/products/categories', 'POST', data)
return api.request('/products/categories', 'POST', {}, data)
},
/**
@@ -33,12 +33,19 @@ export default {
},
/**
* 获取商品列表
* 获取商品列表(只显示上架)
*/
getProducts(params) {
return api.request('/products', 'GET', params)
},
/**
* 获取所有商品(包括下架,用于管理)
*/
getAllProducts(params) {
return api.request('/products/all', 'GET', params)
},
/**
* 获取商品详情
*/
@@ -50,14 +57,14 @@ export default {
* 新增商品
*/
createProduct(data) {
return api.request('/products', 'POST', data)
return api.request('/products', 'POST', {}, data)
},
/**
* 修改商品
*/
updateProduct(id, data) {
return api.request(`/products/${id}`, 'PUT', data)
return api.request(`/products/${id}`, 'PUT', {}, data)
},
/**

View File

@@ -127,7 +127,7 @@
"text": "首页"
},
{
"pagePath": "pages/product/list",
"pagePath": "pages/product/manage",
"text": "商品"
},
{

View File

@@ -74,10 +74,6 @@
<view class="menu-icon-box pink"><text class="icon-text"></text></view>
<text class="menu-text">种类管理</text>
</view>
<view class="menu-item" @click="goStock()">
<view class="menu-icon-box cyan"><text class="icon-text"></text></view>
<text class="menu-text">库存管理</text>
</view>
<view class="menu-item" @click="goTo('/pages/stock/in')">
<view class="menu-icon-box orange"><text class="icon-text"></text></view>
<text class="menu-text">入库</text>
@@ -145,14 +141,16 @@ export default {
else this.greeting = '晚上好'
},
goTo(url) {
// tabBar 页面需要用 switchTab
if (url.startsWith('/pages/product/manage') || url.startsWith('/pages/order/list') || url.startsWith('/pages/index/index')) {
uni.switchTab({ url })
} else {
uni.navigateTo({ url })
}
},
goToTab(url) {
uni.switchTab({ url })
},
goStock() {
uni.navigateTo({ url: '/pages/stock/list' })
},
logout() {
uni.showModal({
title: '退出登录',

View File

@@ -46,18 +46,24 @@
<view v-for="(item, index) in orderItems" :key="index" class="order-item">
<view class="item-info">
<text class="item-name">{{ item.productName }}</text>
<text class="item-spec">{{ item.spec || '-' }}</text>
<text class="item-name-box">{{ item.productName }}</text>
<text class="item-spec">{{ item.spec ? ' ' + item.spec : '' }}</text>
<text class="item-dims" v-if="item.length && item.width">{{ item.length }}x{{ item.width }}</text>
<text class="item-stock">库存: {{ stocks[item.productId] || 0 }}</text>
</view>
<view class="item-edit">
<view class="quantity-edit">
<text class="qty-label">数量</text>
<view class="qty-wrapper">
<text class="qty-btn" @click="qtyMinus(index)">-</text>
<input
class="qty-input"
type="number"
v-model="item.quantity"
@change="calcAmount"
@change="onQuantityChange(index)"
/>
<text class="qty-btn" @click="qtyPlus(index)">+</text>
</view>
</view>
<view class="price-edit">
<text class="qty-label">单价</text>
@@ -174,6 +180,7 @@
<script>
import orderApi from '@/api/order'
import stockApi from '@/api/stock'
import productApi from '@/api/product'
import customerApi from '@/api/customer'
@@ -197,6 +204,7 @@ export default {
orderItems: [],
productList: [],
searchKeyword: '',
stocks: {}, // 商品库存
// 金额相关
discountRate: 100, // 折扣率保留逻辑默认100%不打折)
@@ -218,6 +226,7 @@ export default {
onLoad(options) {
this.loadCustomersByType()
this.loadProducts()
this.loadStocks()
if (options.orderId) {
this.editingOrderId = options.orderId
// 编辑模式
@@ -249,7 +258,10 @@ export default {
spec: item.productSpec,
unit: item.unit,
price: item.price,
quantity: item.quantity
quantity: item.quantity,
length: item.length || '',
width: item.width || '',
area: item.area || ''
}))
this.discountRate = order.discountRate
@@ -295,6 +307,20 @@ export default {
console.error(e)
}
},
async loadStocks() {
try {
const res = await stockApi.getStockList({ page: 1, pageSize: 500 })
const stockMap = {}
if (res.records) {
res.records.forEach(s => {
stockMap[s.productId] = s.quantity || 0
})
}
this.stocks = stockMap
} catch (e) {
console.error(e)
}
},
selectCustomer(e) {
this.selectedCustomer = this.filteredCustomers[e.detail.value]
},
@@ -335,6 +361,9 @@ export default {
spec: product.spec,
unit: product.unit,
price: product.price,
length: product.length || '',
width: product.width || '',
area: product.area || '',
quantity: 1
})
this.calcAmount()
@@ -343,6 +372,38 @@ export default {
this.orderItems.splice(index, 1)
this.calcAmount()
},
qtyMinus(index) {
const item = this.orderItems[index]
const stock = this.stocks[item.productId] || 0
if (item.quantity > 1) {
item.quantity--
this.calcAmount()
}
},
qtyPlus(index) {
const item = this.orderItems[index]
const stock = this.stocks[item.productId] || 0
if (item.quantity < stock) {
item.quantity++
this.calcAmount()
} else {
uni.showToast({ title: '库存不足', icon: 'none' })
}
},
onQuantityChange(index) {
const item = this.orderItems[index]
const stock = this.stocks[item.productId] || 0
let qty = parseInt(item.quantity)
if (isNaN(qty) || qty < 1) {
qty = 1
}
if (qty > stock) {
qty = stock
uni.showToast({ title: '库存不足,已调整为最大库存', icon: 'none' })
}
item.quantity = qty
this.calcAmount()
},
// 核心金额计算逻辑
calcAmount() {
// 1. 计算原价 = Σ(单价 × 数量)
@@ -390,7 +451,10 @@ export default {
items: this.orderItems.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.price
price: item.price,
length: item.length || null,
width: item.width || null,
area: item.area || null
})),
discountRate: 100, // 保留折扣率逻辑默认100%
discountMoney: parseFloat(this.discountMoney) || 0,
@@ -514,13 +578,19 @@ export default {
.item-info {
margin-bottom: 16rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
}
.item-name {
.item-name-box {
font-size: 28rpx;
font-weight: bold;
display: block;
color: #333;
width: 170rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-spec {
@@ -528,6 +598,21 @@ export default {
color: #999;
}
.item-dims {
font-size: 22rpx;
color: #667eea;
background: #f0f0ff;
padding: 2rpx 8rpx;
border-radius: 4rpx;
margin-left: 8rpx;
}
.item-stock {
font-size: 22rpx;
color: #999;
margin-left: auto;
}
.item-edit {
display: flex;
align-items: center;
@@ -536,23 +621,69 @@ export default {
.quantity-edit, .price-edit {
display: flex;
align-items: center;
margin-right: 20rpx;
margin-right: 24rpx;
}
.qty-label {
font-size: 24rpx;
color: #666;
margin-right: 8rpx;
white-space: nowrap;
}
.qty-input, .price-input {
.qty-wrapper {
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 8rpx;
}
.qty-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #667eea;
background: #fff;
border: 1rpx solid #ddd;
}
.qty-btn:first-child {
border-radius: 8rpx 0 0 8rpx;
}
.qty-btn:last-child {
border-radius: 0 8rpx 8rpx 0;
}
.qty-input {
width: 100rpx;
height: 56rpx;
height: 48rpx;
background: #fff;
border: 1rpx solid #ddd;
border-left: none;
border-right: none;
text-align: center;
font-size: 24rpx;
}
.price-input {
width: 120rpx;
height: 48rpx;
background: #fff;
border: 1rpx solid #ddd;
border-radius: 8rpx;
text-align: center;
font-size: 24rpx;
margin-left: 8rpx;
}
.stock-info {
font-size: 20rpx;
color: #999;
margin-left: 12rpx;
}
.item-subtotal {

View File

@@ -36,7 +36,8 @@
<view class="section-title">商品明细</view>
<view class="items-list">
<view class="item-row header">
<text class="item-name">商品名称</text>
<text class="item-info">商品信息</text>
<text class="item-area">总面积()</text>
<text class="item-qty">数量</text>
<text class="item-price">单价</text>
<text class="item-subtotal">小计</text>
@@ -46,10 +47,11 @@
:key="index"
class="item-row"
>
<text class="item-name">{{ item.productName }}</text>
<text class="item-info"><text class="item-name-text">{{ item.productName }}</text><text class="item-spec-text">{{ item.productSpec ? ' ' + item.productSpec : '' }}</text><text class="item-dims-text">{{ item.length || '-' }}x{{ item.width || '-' }}</text></text>
<text class="item-area">{{ calcArea(item) }}</text>
<text class="item-qty">{{ item.quantity }}</text>
<text class="item-price">¥{{ item.price }}</text>
<text class="item-subtotal">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
<text class="item-subtotal">¥{{ (item.price * item.quantity).toFixed(0) }}</text>
</view>
</view>
</view>
@@ -170,6 +172,12 @@ export default {
if (!time) return ''
return time.substring(0, 16).replace('T', ' ')
},
calcArea(item) {
if (item.length && item.width && item.quantity) {
return (item.length * item.width * item.quantity / 1000000).toFixed(4)
}
return '-'
},
getPaymentMethod(method) {
const map = {
'cash': '现金',
@@ -345,7 +353,8 @@ export default {
display: flex;
padding: 20rpx 0;
border-bottom: 1rpx solid #f8f8f8;
font-size: 26rpx;
font-size: 24rpx;
align-items: center;
}
.item-row.header {
@@ -355,12 +364,36 @@ export default {
padding: 16rpx 0;
}
.item-name {
flex: 2;
.item-info {
flex: 3;
font-size: 24rpx;
color: #333;
line-height: 1.6;
}
.item-name-text {
color: #333;
font-weight: 500;
}
.item-spec-text {
color: #999;
}
.item-dims-text {
color: #666;
margin-left: 8rpx;
}
.item-area {
flex: 1.5;
text-align: center;
color: #667eea;
font-weight: 500;
}
.item-qty {
flex: 1;
flex: 0.8;
text-align: center;
}

View File

@@ -158,14 +158,9 @@ export default {
const list = res.records || []
// 加载每个订单的明细
// 直接使用订单中的items不再单独请求
for (const order of list) {
try {
const detail = await orderApi.getOrderDetail(order.orderId)
this.$set(this.orderItemsMap, order.orderId, detail.items || [])
} catch (e) {
console.error(e)
}
this.$set(this.orderItemsMap, order.orderId, order.items || [])
}
if (this.page === 1) {

View File

@@ -219,15 +219,21 @@ export default {
this.returnQty++
}
},
confirmReturn() {
async confirmReturn() {
if (this.returnQty <= 0 || this.returnQty > this.currentItem.returnableQty) {
uni.showToast({ title: '退货数量无效', icon: 'none' })
return
}
try {
await orderApi.refundOrder(this.currentItem.orderId)
uni.showToast({ title: '退货成功', icon: 'success' })
// TODO: 调用后端创建退货订单
this.closePopup()
this.loadReturnableGoods()
} catch (e) {
console.error(e)
uni.showToast({ title: e.message || '退货失败', icon: 'none' })
}
}
}
}

View File

@@ -159,8 +159,9 @@ export default {
this.loadProducts()
},
viewDetail(item) {
// 跳转到商品管理页面进行编辑
uni.navigateTo({
url: `/pages/product/detail?productId=${item.productId}`
url: `/pages/product/manage?productId=${item.productId}`
})
},
getStock(productId) {

View File

@@ -1,6 +1,7 @@
<template>
<view class="page">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-bar">
<input
class="search-input"
@@ -8,31 +9,53 @@
placeholder="搜索商品名称"
@confirm="search"
/>
<button class="search-btn" @click="search">搜索</button>
<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 products"
v-for="item in productList"
:key="item.productId"
class="product-item"
>
<view class="product-info" @click="editProduct(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>
<view class="product-price">
<text class="price">¥{{ item.price }}</text>
<text class="unit">/{{ item.unit }}</text>
<text class="product-unit">/{{ item.unit }}</text>
<text class="product-price-text">¥{{ item.price }}</text>
</view>
<view class="product-spec-row" v-if="item.length && item.width">
<text class="spec-text">规格{{ item.length }} x {{ item.width }} = {{ item.area }} </text>
</view>
<view class="product-status">
<text :class="['status', item.status === 1 ? 'on' : 'off']">
{{ item.status === 1 ? '上架' : '下架' }}
{{ 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>
@@ -43,10 +66,12 @@
</view>
<!-- 空状态 -->
<view v-if="products.length === 0" class="empty">
<view v-if="productList.length === 0" class="empty">
<text>暂无商品</text>
</view>
</view>
</scroll-view>
</view>
<!-- 商品表单弹窗 -->
<view class="modal" v-if="showModal">
@@ -58,32 +83,48 @@
</view>
<view class="modal-body">
<view class="form-item">
<text class="label">商品名称*</text>
<input class="input" v-model="form.name" placeholder="请输入商品名称" />
</view>
<view class="form-item">
<text class="label">规格</text>
<input class="input" v-model="form.spec" placeholder="请输入规格" />
</view>
<view class="form-item">
<text class="label">单位*</text>
<input class="input" v-model="form.unit" placeholder="如:个、箱、米" />
</view>
<view class="form-item">
<text class="label">价格*</text>
<input class="input" type="digit" v-model="form.price" placeholder="请输入价格" />
</view>
<view class="form-item">
<text class="label">分类</text>
<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" placeholder="请输入备注" />
<textarea class="textarea" v-model="form.remark" />
</view>
</view>
<view class="modal-footer">
@@ -103,30 +144,55 @@ export default {
data() {
return {
keyword: '',
products: [],
categoryId: '',
productList: [],
categories: [],
showModal: false,
isEdit: false,
form: {
productId: '',
categoryId: '',
name: '',
spec: '',
unit: '',
price: '',
categoryId: '',
length: '',
width: '',
area: '',
remark: '',
status: 1
}
}
},
onLoad() {
if (!canManageProduct()) {
uni.showToast({ title: '无权限', icon: 'none' })
uni.navigateBack()
return
}
onLoad(options) {
// 移除权限检查,允许所有用户访问
this.loadCategories()
this.loadProducts()
// 如果传入了 productId则打开编辑弹窗
if (options.productId) {
this.$nextTick(() => {
const item = this.productList.find(p => p.productId === options.productId)
if (item) {
this.editProduct(item)
}
})
}
},
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() {
@@ -139,16 +205,21 @@ export default {
},
async loadProducts() {
try {
const res = await productApi.getProducts({
const res = await productApi.getAllProducts({
keyword: this.keyword,
categoryId: this.categoryId,
page: 1,
pageSize: 100
})
this.products = res.records || []
this.productList = res.records || []
} catch (e) {
console.error(e)
}
},
selectCategory(id) {
this.categoryId = id
this.loadProducts()
},
search() {
this.loadProducts()
},
@@ -161,6 +232,9 @@ export default {
unit: '',
price: '',
categoryId: '',
length: '',
width: '',
area: '',
remark: '',
status: 1
}
@@ -183,6 +257,10 @@ export default {
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
@@ -197,11 +275,12 @@ export default {
}
try {
const formData = JSON.parse(JSON.stringify(this.form))
if (this.isEdit) {
await productApi.updateProduct(this.form)
await productApi.updateProduct(formData.productId, formData)
uni.showToast({ title: '更新成功', icon: 'success' })
} else {
await productApi.createProduct(this.form)
await productApi.createProduct(formData)
uni.showToast({ title: '创建成功', icon: 'success' })
}
this.closeModal()
@@ -214,8 +293,7 @@ export default {
async toggleStatus(item) {
const newStatus = item.status === 1 ? 0 : 1
try {
await productApi.updateProduct({
productId: item.productId,
await productApi.updateProduct(item.productId, {
status: newStatus
})
uni.showToast({ title: newStatus === 1 ? '已上架' : '已下架', icon: 'success' })
@@ -249,6 +327,81 @@ export default {
<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;
}
@@ -312,13 +465,57 @@ export default {
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-row {
display: flex;
align-items: center;
margin-top: 8rpx;
}
.product-unit {
color: #999;
font-size: 24rpx;
margin-left: 4rpx;
}
.product-price-text {
color: #ff4d4f;
font-size: 24rpx;
font-weight: bold;
margin-left: 16rpx;
}
.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;
}
@@ -369,6 +566,11 @@ export default {
margin-bottom: 10rpx;
}
.action-btn.edit {
background: #f0f5ff;
color: #667eea;
}
.action-btn.delete {
background: #fff1f0;
color: #ff4d4f;
@@ -434,6 +636,28 @@ export default {
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;
@@ -441,13 +665,25 @@ export default {
margin-bottom: 10rpx;
}
.required {
color: #ff4d4f;
margin-right: 4rpx;
}
.input[disabled] {
background: #f5f5f5;
color: #999;
}
.input {
width: 100%;
height: 70rpx;
padding: 0 20rpx;
background: #f5f5f5;
background: #fff;
border: 2rpx solid #e5e5e5;
border-radius: 8rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.picker {

View File

@@ -14,7 +14,7 @@
</view>
</view>
<!-- 分类 + 商品列表 -->
<!-- 分类侧栏 + 商品列表 -->
<view class="content-wrapper">
<!-- 左侧分类 -->
<scroll-view scroll-y class="category-sidebar">
@@ -29,10 +29,17 @@
<!-- 右侧商品列表 -->
<scroll-view scroll-y class="product-scroll">
<view class="product-list">
<view v-for="item in productList" :key="item.productId" class="product-item" @click="selectProduct(item)">
<view v-for="item in productList" :key="item.productId" class="product-item" :class="{ disabled: stocks[item.productId] === 0 }" @click="selectProduct(item)">
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<view class="product-row">
<text class="product-spec">{{ item.spec || '-' }}</text>
<text class="product-size" v-if="item.length && item.width">{{ item.length }} x {{ item.width }} = {{ item.area }} m²</text>
</view>
<view class="product-stock">
<text class="stock-label">库存:</text>
<text class="stock-value" :class="{ 'stock-zero': stocks[item.productId] === 0 }">{{ stocks[item.productId] || 0 }}</text>
</view>
</view>
<view class="product-price">
<text class="price">¥{{ item.price }}</text>
@@ -47,11 +54,42 @@
</view>
</scroll-view>
</view>
<!-- 商品详情/选择弹窗 -->
<view class="modal-mask" v-if="showModal" @click="closeModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ selectedProduct.name }}</text>
<text class="modal-close" @click="closeModal">×</text>
</view>
<view class="modal-body">
<view class="size-row">
<view class="size-item">
<text class="size-label">长度(cm)</text>
<input class="size-input" type="digit" v-model="selectedProduct.length" placeholder="非必填" />
</view>
<view class="size-item">
<text class="size-label">宽度(cm)</text>
<input class="size-input" type="digit" v-model="selectedProduct.width" placeholder="非必填" />
</view>
<view class="size-item">
<text class="size-label">面积()</text>
<input class="size-input" type="digit" v-model="selectedProduct.area" disabled />
</view>
</view>
</view>
<view class="modal-footer">
<button class="cancel-btn" @click="closeModal">取消</button>
<button class="confirm-btn" @click="confirmProduct">确认</button>
</view>
</view>
</view>
</view>
</template>
<script>
import productApi from '@/api/product'
import stockApi from '@/api/stock'
export default {
data() {
@@ -60,9 +98,29 @@ export default {
categoryId: '',
categories: [],
productList: [],
stocks: {}, // 商品库存
page: 1,
pageSize: 50,
loading: false
loading: false,
showModal: false,
selectedProduct: {
productId: '',
name: '',
spec: '',
unit: '',
price: 0,
length: '',
width: '',
area: ''
}
}
},
watch: {
'selectedProduct.length'(val) {
this.calcArea()
},
'selectedProduct.width'(val) {
this.calcArea()
}
},
onLoad() {
@@ -70,6 +128,23 @@ export default {
this.getProducts()
},
methods: {
selectProduct(item) {
// 库存为0不可选中
if (this.stocks[item.productId] === 0) {
uni.showToast({ title: '库存不足', icon: 'none' })
return
}
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
prevPage.$vm.addProduct(item)
uni.navigateBack()
},
calcArea() {
const length = parseFloat(this.selectedProduct.length) || 0
const width = parseFloat(this.selectedProduct.width) || 0
// 长度(cm) * 宽度(cm) / 10000 = 面积(m²)
this.selectedProduct.area = length && width ? (length * width / 10000).toFixed(2) : ''
},
async loadCategories() {
try {
const categories = await productApi.getCategories()
@@ -88,6 +163,16 @@ export default {
pageSize: this.pageSize
})
this.productList = res.records || []
// 获取每个商品的库存
const stockRes = await stockApi.getStockList({ page: 1, pageSize: 500 })
const stockMap = {}
if (stockRes.records) {
stockRes.records.forEach(s => {
stockMap[s.productId] = s.quantity || 0
})
}
this.stocks = stockMap
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
@@ -103,10 +188,31 @@ export default {
this.page = 1
this.getProducts()
},
selectProduct(item) {
openProductDetail(item) {
this.selectedProduct = {
productId: item.productId,
name: item.name,
spec: item.spec,
unit: item.unit,
price: item.price,
length: '',
width: '',
area: ''
}
this.showModal = true
},
closeModal() {
this.showModal = false
},
confirmProduct() {
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
prevPage.$vm.addProduct(item)
prevPage.$vm.addProduct({
...this.selectedProduct,
length: this.selectedProduct.length,
width: this.selectedProduct.width,
area: this.selectedProduct.area
})
uni.navigateBack()
}
}
@@ -144,26 +250,29 @@ export default {
color: #999;
}
/* 内容区域:侧栏+列表 */
.content-wrapper {
display: flex;
height: calc(100vh - 130rpx);
}
/* 分类侧栏 */
.category-sidebar {
width: 140rpx;
width: 160rpx;
background: #fff;
flex-shrink: 0;
border-right: 1rpx solid #eee;
}
.category-item {
padding: 28rpx 20rpx;
.category-sidebar .category-item {
padding: 28rpx 16rpx;
font-size: 26rpx;
color: #666;
text-align: center;
border-left: 6rpx solid transparent;
}
.category-item.active {
.category-sidebar .category-item.active {
background: #f8f9fa;
color: #667eea;
font-weight: bold;
@@ -190,6 +299,11 @@ export default {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
.product-item.disabled {
opacity: 0.5;
background: #f5f5f5;
}
.product-info {
flex: 1;
}
@@ -207,6 +321,42 @@ export default {
color: #999;
}
.product-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8rpx;
}
.product-size {
font-size: 22rpx;
color: #667eea;
background: #f0f0ff;
padding: 2rpx 8rpx;
border-radius: 4rpx;
}
.product-stock {
display: flex;
align-items: center;
margin-top: 8rpx;
}
.stock-label {
font-size: 22rpx;
color: #999;
}
.stock-value {
font-size: 22rpx;
color: #333;
margin-left: 4rpx;
}
.stock-value.stock-zero {
color: #ff4d4f;
}
.product-price {
text-align: right;
}
@@ -235,4 +385,97 @@ export default {
color: #999;
margin-top: 20rpx;
}
/* 弹窗 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
.modal-content {
width: 80%;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #eee;
}
.modal-title {
font-size: 32rpx;
font-weight: bold;
}
.modal-close {
font-size: 48rpx;
color: #999;
}
.modal-body {
padding: 30rpx;
}
.size-row {
display: flex;
gap: 20rpx;
}
.size-item {
flex: 1;
}
.size-label {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
}
.size-input {
width: 100%;
height: 70rpx;
background: #f5f5f5;
border-radius: 10rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.modal-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
}
.cancel-btn, .confirm-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
border-radius: 40rpx;
font-size: 30rpx;
}
.cancel-btn {
background: #f5f5f5;
color: #666;
}
.confirm-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
}
</style>

View File

@@ -50,7 +50,8 @@
<view class="section-title">商品明细</view>
<view class="items-list">
<view class="item-row header">
<text class="item-name">商品</text>
<text class="item-info">商品信息</text>
<text class="item-area">总面积()</text>
<text class="item-qty">数量</text>
<text class="item-price">单价</text>
<text class="item-subtotal">小计</text>
@@ -60,10 +61,11 @@
:key="index"
class="item-row"
>
<text class="item-name">{{ item.productName }}</text>
<text class="item-info"><text class="item-name-text">{{ item.productName }}</text><text class="item-spec-text">{{ item.productSpec ? ' ' + item.productSpec : '' }}</text><text class="item-dims-text">{{ item.length || '-' }}x{{ item.width || '-' }}</text></text>
<text class="item-area">{{ calcArea(item) }}</text>
<text class="item-qty">{{ item.quantity }}</text>
<text class="item-price">¥{{ item.price }}</text>
<text class="item-subtotal">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
<text class="item-subtotal">¥{{ (item.price * item.quantity).toFixed(0) }}</text>
</view>
</view>
</view>
@@ -153,6 +155,12 @@ export default {
formatTime(time) {
if (!time) return ''
return time.substring(0, 16).replace('T', ' ')
},
calcArea(item) {
if (item.length && item.width && item.quantity) {
return (item.length * item.width * item.quantity / 1000000).toFixed(4)
}
return '-'
}
}
}
@@ -284,9 +292,10 @@ export default {
.item-row {
display: flex;
padding: 20rpx 0;
padding: 16rpx 0;
border-bottom: 1rpx solid #f8f8f8;
font-size: 26rpx;
font-size: 24rpx;
align-items: center;
}
.item-row.header {
@@ -295,12 +304,36 @@ export default {
color: #666;
}
.item-name {
flex: 2;
.item-info {
flex: 3;
font-size: 24rpx;
color: #333;
line-height: 1.6;
}
.item-name-text {
color: #333;
font-weight: 500;
}
.item-spec-text {
color: #999;
}
.item-dims-text {
color: #666;
margin-left: 8rpx;
}
.item-area {
flex: 1.5;
text-align: center;
color: #667eea;
font-weight: 500;
}
.item-qty {
flex: 1;
flex: 0.8;
text-align: center;
}

View File

@@ -32,8 +32,18 @@
@click="openQuantityPopup(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>
<text class="product-price-text">¥{{ item.price }}</text>
</view>
<view class="product-spec-row" v-if="item.length && item.width">
<text class="spec-text">{{ item.length }} x {{ item.width }} = {{ item.area }} </text>
</view>
</view>
<view class="product-add">
<text class="add-icon">+</text>
@@ -320,11 +330,30 @@ export default {
flex: 1;
}
.product-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.product-name {
display: block;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.product-category {
font-size: 22rpx;
color: #667eea;
background: #f0f4ff;
padding: 4rpx 12rpx;
border-radius: 8rpx;
margin-left: 12rpx;
}
.product-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
@@ -333,6 +362,28 @@ export default {
color: #999;
}
.product-unit {
font-size: 22rpx;
color: #999;
}
.product-price-text {
font-size: 26rpx;
color: #ff4d4f;
font-weight: 500;
margin-left: 16rpx;
}
.product-spec-row {
display: flex;
align-items: center;
}
.spec-text {
font-size: 22rpx;
color: #666;
}
.product-add {
width: 60rpx;
height: 60rpx;