Files
PartsInquiry/frontend/pages/order/create.vue
2025-09-16 22:11:19 +08:00

225 lines
8.9 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="order">
<!-- 顶部 Tab -->
<view class="tabs">
<text :class="{ active: biz==='sale' }" @click="switchBiz('sale')">销售</text>
<text :class="{ active: biz==='purchase' }" @click="switchBiz('purchase')">进货</text>
<text :class="{ active: biz==='income' }" @click="switchBiz('income')">其他收入</text>
<text :class="{ active: biz==='expense' }" @click="switchBiz('expense')">其他支出</text>
</view>
<!-- 子类目按钮 -->
<view class="subtabs" v-if="biz==='sale'">
<button class="subbtn" :class="{ active: saleType==='out' }" @click="saleType='out'">出货</button>
<button class="subbtn" :class="{ active: saleType==='return' }" @click="saleType='return'">退货</button>
<button class="subbtn" :class="{ active: saleType==='collect' }" @click="saleType='collect'">收款</button>
</view>
<view class="subtabs" v-else-if="biz==='purchase'">
<button class="subbtn" :class="{ active: purchaseType==='in' }" @click="purchaseType='in'">进货</button>
<button class="subbtn" :class="{ active: purchaseType==='return' }" @click="purchaseType='return'">退货</button>
<button class="subbtn" :class="{ active: purchaseType==='pay' }" @click="purchaseType='pay'">付款</button>
</view>
<!-- 日期与客户 -->
<picker mode="date" :value="order.orderTime" @change="onDateChange">
<view class="field">
<text class="label">时间</text>
<text class="value">{{ order.orderTime }}</text>
</view>
</picker>
<view class="field" v-if="biz==='sale'" @click="chooseCustomer">
<text class="label">客户</text>
<text class="value">{{ customerLabel }}</text>
</view>
<view class="field" v-else-if="biz==='purchase'" @click="chooseSupplier">
<text class="label">供应商</text>
<text class="value">{{ supplierLabel }}</text>
</view>
<!-- 已选商品与合计销售/进货 -->
<view v-if="biz==='sale' || biz==='purchase'">
<view class="summary">
<text>选中货品{{ totalQuantity }}</text>
<text>合计金额¥ {{ totalAmount.toFixed(2) }}</text>
</view>
<!-- 加号添加商品 -->
<view class="add" @click="chooseProduct">+</view>
</view>
<!-- 其它收入/支出 表单 -->
<view v-else>
<view class="chips">
<view v-for="c in (biz==='income' ? incomeCategories : expenseCategories)" :key="c.key" class="chip" :class="{ active: activeCategory===c.key }" @click="activeCategory=c.key">{{ c.label }}</view>
</view>
<view class="field" @click="chooseCounterparty">
<text class="label">往来单位</text>
<text class="value">{{ counterpartyLabel }}</text>
</view>
<view class="field" @click="chooseAccount">
<text class="label">结算账户</text>
<text class="value">{{ accountLabel }}</text>
</view>
<view class="field">
<text class="label">金额</text>
<input class="value" type="digit" v-model.number="trxAmount" placeholder="0.00" />
</view>
<view class="textarea">
<textarea v-model="order.remark" maxlength="200" placeholder="备注最多输入200个字"></textarea>
</view>
</view>
<!-- 购物车空态 -->
<view class="empty" v-if="!items.length">
<image src="/static/logo.png" mode="widthFix" class="empty-img"></image>
<text class="empty-text">购物车里空空如也</text>
<text class="empty-sub">扫描或点击 + 选择商品吧</text>
</view>
<!-- 商品列表 -->
<view v-else class="list">
<view class="row" v-for="(it, idx) in items" :key="idx">
<view class="col name">{{ it.productName }}</view>
<view class="col qty">
<input type="number" v-model.number="it.quantity" @input="recalc()" />
</view>
<view class="col price">
<input type="number" v-model.number="it.unitPrice" @input="recalc()" />
</view>
<view class="col amount">¥ {{ (Number(it.quantity)*Number(it.unitPrice)).toFixed(2) }}</view>
</view>
</view>
<!-- 底部提交栏 -->
<view class="bottom">
<button class="ghost" @click="saveAndReset">再记一笔</button>
<button class="primary" @click="submit">保存</button>
</view>
</view>
</template>
<script>
import { post } from '../../common/http.js'
import { INCOME_CATEGORIES, EXPENSE_CATEGORIES } from '../../common/constants.js'
function todayString() {
const d = new Date()
const m = (d.getMonth()+1).toString().padStart(2,'0')
const day = d.getDate().toString().padStart(2,'0')
return `${d.getFullYear()}-${m}-${day}`
}
export default {
data() {
return {
biz: 'sale',
saleType: 'out',
purchaseType: 'in',
order: {
orderTime: todayString(),
customerId: null,
supplierId: null,
remark: ''
},
customerName: '',
supplierName: '',
items: [],
activeCategory: 'sale_income',
trxAmount: 0,
selectedAccountId: null,
selectedAccountName: ''
}
},
computed: {
totalQuantity() {
return this.items.reduce((s, it) => s + Number(it.quantity || 0), 0)
},
totalAmount() {
return this.items.reduce((s, it) => s + Number(it.quantity || 0) * Number(it.unitPrice || 0), 0)
},
customerLabel() { return this.customerName || '零售客户' },
supplierLabel() { return this.supplierName || '零散供应商' },
incomeCategories() { return INCOME_CATEGORIES },
expenseCategories() { return EXPENSE_CATEGORIES },
accountLabel() { return this.selectedAccountName || '现金' },
counterpartyLabel() { return this.customerName || this.supplierName || '—' }
},
methods: {
switchBiz(type) { this.biz = type },
onDateChange(e) { this.order.orderTime = e.detail.value },
chooseCustomer() {
uni.navigateTo({ url: '/pages/customer/select' })
},
chooseSupplier() { uni.navigateTo({ url: '/pages/supplier/select' }) },
chooseProduct() {
uni.navigateTo({ url: '/pages/product/select' })
},
chooseAccount() { uni.navigateTo({ url: '/pages/account/select' }) },
chooseCounterparty() {
if (this.biz==='income' || this.biz==='expense') {
uni.navigateTo({ url: '/pages/customer/select' })
}
},
recalc() { this.$forceUpdate() },
async submit() {
const isSaleOrPurchase = (this.biz==='sale' || this.biz==='purchase')
const payload = isSaleOrPurchase ? {
type: this.biz === 'sale' ? (this.saleType) : ('purchase.' + this.purchaseType),
orderTime: this.order.orderTime,
customerId: this.order.customerId,
supplierId: this.order.supplierId,
items: this.items.map(it => ({ productId: it.productId, quantity: Number(it.quantity||0), unitPrice: Number(it.unitPrice||0) })),
amount: this.totalAmount
} : {
type: this.biz,
category: this.activeCategory,
counterpartyId: this.order.customerId || null,
accountId: this.selectedAccountId || null,
amount: Number(this.trxAmount||0),
txTime: this.order.orderTime,
remark: this.order.remark
}
try {
const url = isSaleOrPurchase ? '/api/orders' : '/api/other-transactions'
await post(url, payload)
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => { uni.navigateBack() }, 600)
} catch (e) {
uni.showToast({ title: e && e.message || '保存失败', icon: 'none' })
}
},
saveAndReset() {
this.items = []
this.trxAmount = 0
this.order.remark = ''
}
}
}
</script>
<style>
.order { padding-bottom: 140rpx; }
.tabs { display: flex; justify-content: space-around; padding: 16rpx 24rpx; }
.tabs text { color: #666; }
.tabs text.active { color: #333; font-weight: 700; }
.subtabs { display: flex; gap: 16rpx; padding: 0 24rpx 16rpx; }
.subbtn { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f4f4f4; color: #666; }
.subbtn.active { background: #ffe69a; color: #3f320f; }
.field { display:flex; justify-content: space-between; padding: 22rpx 24rpx; background: #fff; border-bottom: 1rpx solid #eee; }
.label { color:#666; }
.value { color:#333; }
.summary { display:flex; justify-content: space-between; padding: 22rpx 24rpx; color:#333; }
.add { margin: 24rpx auto; width: 120rpx; height: 120rpx; border-radius: 20rpx; background: #c7eef7; color:#16a1c4; font-size: 72rpx; display:flex; align-items:center; justify-content:center; }
.empty { display:flex; flex-direction: column; align-items:center; padding: 60rpx 0; color:#888; }
.empty-img { width: 220rpx; margin-bottom: 20rpx; }
.empty-text { margin-bottom: 8rpx; }
.list { background:#fff; }
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 12rpx; padding: 16rpx 12rpx; align-items:center; border-bottom: 1rpx solid #f3f3f3; }
.col.name { padding-left: 12rpx; }
.col.amount { text-align:right; padding-right: 12rpx; color:#333; }
.bottom { position: fixed; left:0; right:0; bottom:0; background:#fff; padding: 16rpx 24rpx calc(env(safe-area-inset-bottom) + 16rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.06); }
.primary { width: 100%; background: linear-gradient(135deg, #FFE69A 0%, #F4CF62 45%, #D7A72E 100%); color:#493c1b; border-radius: 999rpx; padding: 20rpx 0; font-weight:800; }
</style>