Files
todo-frontend/src/pages/order/create.vue
Agent f90152f010
All checks were successful
continuous-integration/drone/push Build is passing
feat: 创建订单时先选客户种类再选客户,默认顾客
2026-03-29 05:35:01 +00:00

700 lines
17 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="section card-section">
<view class="section-header">
<text class="section-icon">👤</text>
<text class="section-title">客户信息</text>
</view>
<view class="form-item">
<text class="label">客户种类</text>
<picker
mode="selector"
:range="typeOptions"
range-key="label"
@change="selectType"
>
<view class="picker-value">
{{ selectedTypeLabel || '请选择客户种类' }}
<text class="arrow"></text>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">客户</text>
<picker
mode="selector"
:range="filteredCustomers"
range-key="name"
@change="selectCustomer"
>
<view class="picker-value">
{{ selectedCustomer ? selectedCustomer.name : '请选择客户' }}
<text class="arrow"></text>
</view>
</picker>
</view>
</view>
<!-- 商品选择 -->
<view class="section card-section">
<view class="section-header">
<text class="section-icon">📦</text>
<text class="section-title">商品明细</text>
<text class="add-btn" @click="goToSelectProduct">+ 添加商品</text>
</view>
<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>
</view>
<view class="item-edit">
<view class="quantity-edit">
<text class="qty-label">数量</text>
<input
class="qty-input"
type="number"
v-model="item.quantity"
@change="calcAmount"
/>
</view>
<view class="price-edit">
<text class="qty-label">单价</text>
<input
class="price-input"
type="digit"
v-model="item.price"
@change="calcAmount"
/>
</view>
<text class="item-subtotal">¥{{ (item.price * item.quantity).toFixed(2) }}</text>
<text class="delete-btn" @click="removeItem(index)">×</text>
</view>
</view>
<view v-if="orderItems.length === 0" class="empty-tip">
<text class="empty-icon">🛒</text>
<text>请添加商品</text>
</view>
</view>
<!-- 优惠设置 -->
<view class="section card-section">
<view class="section-header">
<text class="section-icon">🎫</text>
<text class="section-title">优惠设置</text>
</view>
<view class="form-item">
<text class="label">优惠金额</text>
<input
class="discount-input"
type="digit"
v-model="discountMoney"
@input="calcAmount"
/>
<text class="unit"></text>
</view>
</view>
<!-- 订单金额 -->
<view class="section amount-section">
<view class="amount-header">
<text class="amount-icon">💰</text>
<text class="amount-title">订单金额</text>
</view>
<view class="amount-row">
<text class="amount-label">原价合计</text>
<text class="amount-value">¥{{ totalAmount.toFixed(2) }}</text>
</view>
<view class="amount-row">
<text class="amount-label">优惠金额</text>
<text class="amount-value discount">-¥{{ discountAmount.toFixed(2) }}</text>
</view>
<view class="amount-row actual">
<text class="amount-label">实付金额</text>
<text class="amount-value">¥{{ actualAmount.toFixed(2) }}</text>
</view>
</view>
<!-- 支付方式 -->
<view class="section card-section">
<view class="section-header">
<text class="section-icon">💳</text>
<text class="section-title">支付方式</text>
</view>
<view class="payment-methods">
<view
class="method-item"
:class="{ active: paymentMethod === 'cash' }"
@click="paymentMethod = 'cash'"
>
💵 现金
</view>
<view
class="method-item"
:class="{ active: paymentMethod === 'wechat' }"
@click="paymentMethod = 'wechat'"
>
💬 微信
</view>
<view
class="method-item"
:class="{ active: paymentMethod === 'alipay' }"
@click="paymentMethod = 'alipay'"
>
💰 支付宝
</view>
</view>
</view>
<!-- 备注 -->
<view class="section card-section">
<view class="section-header">
<text class="section-icon">📝</text>
<text class="section-title">备注</text>
</view>
<textarea
class="remark-input"
v-model="remark"
placeholder="请输入备注信息"
/>
</view>
<!-- 提交按钮 -->
<view class="submit-bar">
<view class="submit-info">
<text>实付: </text>
<text class="submit-amount">¥{{ actualAmount.toFixed(2) }}</text>
</view>
<button class="submit-btn" @click="createOrder">{{ editingOrderId ? '保存订单' : '创建订单' }}</button>
</view>
</view>
</template>
<script>
import orderApi from '@/api/order'
import productApi from '@/api/product'
import customerApi from '@/api/customer'
export default {
data() {
return {
// 客户种类
typeOptions: [
{ label: '顾客', value: 'customer' },
{ label: '木匠', value: 'carpenter' },
{ label: '装修公司', value: 'company' }
],
selectedType: 'customer',
selectedTypeLabel: '顾客',
// 客户相关
customers: [],
selectedCustomer: null,
// 商品相关
orderItems: [],
productList: [],
searchKeyword: '',
// 金额相关
discountRate: 100, // 折扣率保留逻辑默认100%不打折)
discountMoney: 0, // 优惠金额
totalAmount: 0, // 原价
discountAmount: 0, // 优惠金额(计算结果)
actualAmount: 0, // 实付金额
// 其他
paymentMethod: 'cash',
remark: '',
// 编辑模式
editingOrderId: null,
tempCustomerId: null, // 临时保存编辑时的客户ID
tempCustomerType: null // 临时保存编辑时的客户种类
}
},
onLoad(options) {
this.loadCustomers()
this.loadProducts()
if (options.orderId) {
this.editingOrderId = options.orderId
// 编辑模式
uni.setNavigationBarTitle({ title: '编辑订单' })
// 等客户列表加载完成后再加载订单
setTimeout(() => {
this.loadOrder(options.orderId)
}, 500)
}
},
computed: {
filteredCustomers() {
return this.customers.filter(c => c.type === this.selectedType)
}
},
methods: {
async loadOrder(orderId) {
try {
const detail = await orderApi.getOrderDetail(orderId)
const order = detail.order
// 先保存 customerId等客户列表加载完成后再匹配
this.tempCustomerId = order.customerId
this.tempCustomerType = order.customerType
this.orderItems = (detail.items || []).map(item => ({
productId: item.productId,
productName: item.productName,
spec: item.productSpec,
unit: item.unit,
price: item.price,
quantity: item.quantity
}))
this.discountRate = order.discountRate
this.discountMoney = order.discountMoney || 0
this.remark = order.remark || ''
this.paymentMethod = order.paymentMethod || 'cash'
this.calcAmount()
// 尝试匹配客户
this.matchCustomer()
} catch (e) {
console.error(e)
}
},
matchCustomer() {
if (this.tempCustomerId && this.customers.length > 0) {
this.selectedCustomer = this.customers.find(c => c.customerId === this.tempCustomerId)
if (this.tempCustomerType) {
this.selectedType = this.tempCustomerType
const typeOption = this.typeOptions.find(t => t.value === this.tempCustomerType)
if (typeOption) {
this.selectedTypeLabel = typeOption.label
}
}
}
},
async loadCustomers() {
try {
const res = await customerApi.getCustomers({ page: 1, pageSize: 100 })
this.customers = res.records || []
// 客户列表加载完成后,尝试匹配编辑订单的客户
this.matchCustomer()
} catch (e) {
console.error(e)
}
},
async loadProducts() {
try {
const res = await productApi.getProducts({ page: 1, pageSize: 100 })
this.productList = res.records || []
} catch (e) {
console.error(e)
}
},
selectCustomer(e) {
this.selectedCustomer = this.filteredCustomers[e.detail.value]
},
selectType(e) {
const idx = e.detail.value
this.selectedType = this.typeOptions[idx].value
this.selectedTypeLabel = this.typeOptions[idx].label
// 切换种类后清除已选客户
this.selectedCustomer = null
},
goToSelectProduct() {
uni.navigateTo({ url: '/pages/product/select' })
},
addProduct(product) {
// 检查是否已添加
const exists = this.orderItems.find(item => item.productId === product.productId)
if (exists) {
uni.showToast({ title: '商品已添加', icon: 'none' })
return
}
this.orderItems.push({
productId: product.productId,
productName: product.name,
spec: product.spec,
unit: product.unit,
price: product.price,
quantity: 1
})
this.calcAmount()
},
removeItem(index) {
this.orderItems.splice(index, 1)
this.calcAmount()
},
// 核心金额计算逻辑
calcAmount() {
// 1. 计算原价 = Σ(单价 × 数量)
this.totalAmount = this.orderItems.reduce((sum, item) => {
return sum + (item.price * item.quantity)
}, 0)
// 2. 优惠金额直接使用用户输入的值,不能为负
let discountVal = parseFloat(this.discountMoney)
if (isNaN(discountVal) || discountVal < 0) {
discountVal = 0
}
this.discountAmount = discountVal
// 3. 计算实付金额 = 原价 - 优惠金额
this.actualAmount = this.totalAmount - this.discountAmount
},
async createOrder() {
if (this.orderItems.length === 0) {
uni.showToast({ title: '请添加商品', icon: 'none' })
return
}
// 校验:客户必须选择
if (!this.selectedCustomer) {
uni.showToast({ title: '请选择客户', icon: 'none' })
return
}
// 校验实付金额必须大于0
if (this.actualAmount <= 0) {
uni.showToast({ title: '实付金额必须大于0', icon: 'none' })
return
}
// 校验:优惠金额不能为负
const discountVal = parseFloat(this.discountMoney) || 0
if (discountVal < 0) {
uni.showToast({ title: '优惠金额不能为负', icon: 'none' })
return
}
const data = {
customerId: this.selectedCustomer ? this.selectedCustomer.customerId : null,
items: this.orderItems.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.price
})),
discountRate: 100, // 保留折扣率逻辑默认100%
discountMoney: parseFloat(this.discountMoney) || 0,
remark: this.remark,
paymentMethod: this.paymentMethod
}
try {
let order
if (this.editingOrderId) {
order = await orderApi.updateOrder(this.editingOrderId, data)
uni.showToast({ title: '订单更新成功', icon: 'success' })
} else {
order = await orderApi.createOrder(data)
uni.showToast({ title: '订单创建成功', icon: 'success' })
}
// 跳转到订单列表
setTimeout(() => {
uni.switchTab({
url: '/pages/order/list'
})
}, 1500)
} catch (e) {
console.error(e)
}
}
}
}
</script>
<style>
/* 全局 */
.page {
padding: 20rpx;
padding-bottom: 180rpx;
background: #f8f9fa;
}
/* 卡片区块 */
.card-section {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.section-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.section-title {
font-size: 28rpx;
font-weight: bold;
flex: 1;
}
.add-btn {
color: #667eea;
font-size: 26rpx;
font-weight: 500;
}
.form-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
}
.label {
width: 140rpx;
font-size: 28rpx;
color: #666;
}
.picker-value {
flex: 1;
font-size: 28rpx;
color: #333;
display: flex;
justify-content: space-between;
align-items: center;
}
.arrow {
font-size: 32rpx;
color: #ccc;
}
.discount-input {
width: 120rpx;
text-align: right;
font-size: 28rpx;
padding: 10rpx;
background: #f5f5f5;
border-radius: 8rpx;
}
.unit {
font-size: 28rpx;
color: #666;
margin-left: 10rpx;
}
/* 订单商品项 */
.order-item {
padding: 20rpx;
background: #f9f9f9;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
.item-info {
margin-bottom: 16rpx;
}
.item-name {
font-size: 28rpx;
font-weight: bold;
display: block;
color: #333;
}
.item-spec {
font-size: 24rpx;
color: #999;
}
.item-edit {
display: flex;
align-items: center;
}
.quantity-edit, .price-edit {
display: flex;
align-items: center;
margin-right: 20rpx;
}
.qty-label {
font-size: 24rpx;
color: #666;
margin-right: 8rpx;
}
.qty-input, .price-input {
width: 100rpx;
height: 56rpx;
background: #fff;
border: 1rpx solid #ddd;
border-radius: 8rpx;
text-align: center;
font-size: 24rpx;
}
.item-subtotal {
flex: 1;
text-align: right;
font-size: 28rpx;
font-weight: bold;
color: #ff4d4f;
}
.delete-btn {
margin-left: 20rpx;
font-size: 40rpx;
color: #999;
padding: 10rpx;
}
.empty-tip {
padding: 40rpx;
text-align: center;
color: #999;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-icon {
font-size: 60rpx;
margin-bottom: 10rpx;
}
/* 金额区域 */
.amount-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.amount-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.amount-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.amount-title {
font-size: 28rpx;
font-weight: bold;
color: #fff;
}
.amount-row {
display: flex;
justify-content: space-between;
padding: 12rpx 0;
}
.amount-label {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.8);
}
.amount-value {
font-size: 26rpx;
color: #fff;
}
.amount-value.discount {
color: #7dff7d;
}
.amount-row.actual {
border-top: 1rpx solid rgba(255, 255, 255, 0.2);
padding-top: 20rpx;
margin-top: 12rpx;
}
.amount-row.actual .amount-value {
font-size: 36rpx;
font-weight: bold;
}
/* 支付方式 */
.payment-methods {
display: flex;
gap: 20rpx;
}
.method-item {
flex: 1;
padding: 24rpx;
text-align: center;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 26rpx;
transition: all 0.3s;
}
.method-item.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
}
/* 备注 */
.remark-input {
width: 100%;
height: 120rpx;
font-size: 28rpx;
background: #f9f9f9;
border-radius: 12rpx;
padding: 20rpx;
box-sizing: border-box;
}
/* 底部提交 */
.submit-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: #fff;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.submit-info {
flex: 1;
font-size: 28rpx;
}
.submit-amount {
font-size: 40rpx;
font-weight: bold;
color: #ff4d4f;
}
.submit-btn {
width: 240rpx;
height: 88rpx;
line-height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 44rpx;
font-size: 30rpx;
font-weight: bold;
box-shadow: 0 8rpx 30rpx rgba(102, 126, 234, 0.4);
}
</style>