Files
PartsInquiry/frontend/pages/order/create.vue
2025-09-20 21:09:27 +08:00

381 lines
18 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' && saleType==='collect') || (biz==='purchase' && purchaseType==='pay')">
<!-- 客户 / 供应商 -->
<view class="field" v-if="biz==='sale'" @click="chooseCustomer">
<text class="label">客户</text>
<text class="value">{{ customerLabel }}</text>
</view>
<view class="field" v-else @click="chooseSupplier">
<text class="label">供应商</text>
<text class="value">{{ supplierLabel }}</text>
</view>
<!-- 三种收付款方式 -->
<view class="field pay-row">
<text class="label">现金</text>
<input class="pay-input" type="digit" v-model.number="payments.cash" placeholder="0.00" @input="recalcPay()"/>
</view>
<view class="field pay-row">
<text class="label">银行存款</text>
<input class="pay-input" type="digit" v-model.number="payments.bank" placeholder="0.00" @input="recalcPay()"/>
</view>
<view class="field pay-row">
<text class="label">微信</text>
<input class="pay-input" type="digit" v-model.number="payments.wechat" placeholder="0.00" @input="recalcPay()"/>
</view>
<view class="collapse-trigger" @click="showMore = !showMore">{{ showMore ? '收起' : '' }}</view>
<!-- 备注与日期 -->
<view class="textarea">
<view class="amount-badge">总金额{{ payTotal.toFixed(2) }}</view>
<textarea v-model="order.remark" maxlength="200" placeholder="备注最多输入200个字"></textarea>
<view class="date-mini">
<picker mode="date" :value="order.orderTime" @change="onDateChange">
<text>{{ order.orderTime }}</text>
</picker>
</view>
</view>
</view>
<!-- 已选商品与合计销售/进货 出入库 -->
<view v-else-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="subtabs">
<button class="subbtn" :class="{ active: counterpartyType==='customer' }" @click="setCounterparty('customer')">客户</button>
<button class="subbtn" :class="{ active: counterpartyType==='supplier' }" @click="setCounterparty('supplier')">供应商</button>
</view>
<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="onPriceInput(it)" />
</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 { get, 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: '',
customerPriceLevel: '零售价',
_lastCustomerId: null,
_priceCache: {},
supplierName: '',
items: [],
activeCategory: 'sale_income',
counterpartyType: 'customer',
trxAmount: 0,
selectedAccountId: null,
selectedAccountName: '',
// 收款/付款输入
payments: { cash: 0, bank: 0, wechat: 0 },
showMore: false
}
},
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 this._incomeCategories || INCOME_CATEGORIES },
expenseCategories() { return this._expenseCategories || EXPENSE_CATEGORIES },
accountLabel() { return this.selectedAccountName || '现金' },
counterpartyLabel() { return this.counterpartyType==='customer' ? (this.customerName || '—') : (this.supplierName || '—') },
// 收款/付款合计
payTotal() {
const p = this.payments || { cash:0, bank:0, wechat:0 }
return Number(p.cash||0) + Number(p.bank||0) + Number(p.wechat||0)
}
},
onLoad(query) {
try {
// 1) 优先读取本地预设参数(来自首页九宫格)
const preset = uni.getStorageSync('ORDER_DEFAULT_PARAMS') || {}
// 2) 再读取路由 query 作为兜底(兼容历史调用)
const biz = (query && query.biz) || preset.biz
const type = (query && query.type) || preset.type
if (biz === 'sale' || biz === 'purchase' || biz === 'income' || biz === 'expense') {
this.biz = biz
}
if (this.biz === 'sale' && (type === 'out' || type === 'return' || type === 'collect')) {
this.saleType = type
}
if (this.biz === 'purchase' && (type === 'in' || type === 'return' || type === 'pay')) {
this.purchaseType = type
}
// 一次性参数,读取后清空
try { uni.removeStorageSync('ORDER_DEFAULT_PARAMS') } catch(_) {}
} catch(e) {}
this.fetchCategories()
},
onShow() {
if (this.biz === 'sale') {
if (this.order.customerId && this.order.customerId !== this._lastCustomerId) {
this.loadCustomerLevel(this.order.customerId).then(() => {
this._lastCustomerId = this.order.customerId
for (const it of this.items) { if (it && (it._autoPrice || !it.unitPrice)) this.autoPriceItem(it) }
})
}
for (const it of this.items) { if (it && !it.unitPrice) this.autoPriceItem(it) }
}
},
methods: {
async fetchCategories() {
try {
const res = await get('/api/finance/categories')
if (res && Array.isArray(res.incomeCategories)) this._incomeCategories = res.incomeCategories
if (res && Array.isArray(res.expenseCategories)) this._expenseCategories = res.expenseCategories
this.ensureActiveCategory()
} catch (_) { this.ensureActiveCategory() }
},
ensureActiveCategory() {
const list = this.biz==='income' ? (this.incomeCategories||[]) : (this.expenseCategories||[])
if (!list.length) return
const exists = list.some(it => it && it.key === this.activeCategory)
if (!exists) this.activeCategory = list[0].key
},
async loadCustomerLevel(customerId) {
try {
const d = await get(`/api/customers/${customerId}`)
this.customerPriceLevel = d && d.priceLevel ? d.priceLevel : '零售价'
} catch(e) { this.customerPriceLevel = '零售价' }
},
priceFieldForLevel() {
const lvl = this.customerPriceLevel || '零售价'
if (lvl === '批发价') return 'wholesalePrice'
if (lvl === '大单报价') return 'bigClientPrice'
return 'retailPrice'
},
async autoPriceItem(it) {
if (this.biz !== 'sale') return
if (!it || !it.productId) return
const pid = it.productId
let detail = this._priceCache[pid]
if (!detail) {
try { detail = await get(`/api/products/${pid}`); this._priceCache[pid] = detail } catch(e) { return }
}
const field = this.priceFieldForLevel()
let price = Number(detail && detail[field] != null ? detail[field] : 0)
if (!price && field !== 'retailPrice') { price = Number(detail && detail.retailPrice != null ? detail.retailPrice : 0) }
it.unitPrice = price
it._autoPrice = true
this.recalc()
},
onPriceInput(it) { if (it) { it._autoPrice = false; this.recalc() } },
switchBiz(type) { this.biz = type; this.ensureActiveCategory() },
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?mode=pick' }) },
chooseCounterparty() {
if (!(this.biz==='income' || this.biz==='expense')) return
if (this.counterpartyType==='customer') { uni.navigateTo({ url: '/pages/customer/select' }) }
else { uni.navigateTo({ url: '/pages/supplier/select' }) }
},
setCounterparty(t) { this.counterpartyType = t; this.ensureActiveCategory() },
recalc() { this.$forceUpdate() },
recalcPay() { this.$forceUpdate() },
async submit() {
const isSaleOrPurchase = (this.biz==='sale' || this.biz==='purchase')
const isCollectOrPay = (this.biz==='sale' && this.saleType==='collect') || (this.biz==='purchase' && this.purchaseType==='pay')
const saleTypeValue = this.biz==='sale' ? ('sale.' + this.saleType) : ('purchase.' + this.purchaseType)
// 前置校验:销售/进货 出入库要求有明细
if (isSaleOrPurchase && !isCollectOrPay) {
if (!this.items.length) { uni.showToast({ title: '请先选择商品', icon: 'none' }); return }
const invalid = this.items.find(it => !it.productId || Number(it.quantity||0) <= 0)
if (invalid) { uni.showToast({ title: '数量需大于0', icon: 'none' }); return }
}
const payload = isSaleOrPurchase ? (isCollectOrPay ? [
{ method: 'cash', amount: Number(this.payments.cash||0) },
{ method: 'bank', amount: Number(this.payments.bank||0) },
{ method: 'wechat', amount: Number(this.payments.wechat||0) }
].filter(p => p.amount>0) : {
type: saleTypeValue,
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,
counterpartyType: this.counterpartyType,
counterpartyId: this.counterpartyType==='customer' ? (this.order.customerId || null) : (this.order.supplierId || null),
accountId: this.selectedAccountId || null,
amount: Number(this.trxAmount||0),
txTime: this.order.orderTime,
remark: this.order.remark
}
try {
const url = isSaleOrPurchase ? (isCollectOrPay ? (`/api/payments/${this.biz}`) : '/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 = ''
this.payments = { cash: 0, bank: 0, wechat: 0 }
}
}
}
</script>
<style lang="scss">
.order { padding-bottom: 140rpx; }
.tabs { display: flex; justify-content: space-around; padding: 16rpx 24rpx; }
.tabs text { color: $uni-text-color-grey; }
.tabs text.active { color: $uni-text-color; font-weight: 700; }
.subtabs { display: flex; gap: 16rpx; padding: 0 24rpx 16rpx; }
.subbtn { padding: 10rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color: $uni-text-color-grey; }
.subbtn.active { background: $uni-color-primary; color: #fff; }
.field { display:flex; justify-content: space-between; padding: 22rpx 24rpx; background: $uni-bg-color-grey; border-bottom: 1rpx solid $uni-border-color; }
.label { color:$uni-text-color-grey; }
.value { color:$uni-text-color; }
.summary { display:flex; justify-content: space-between; padding: 22rpx 24rpx; color:$uni-text-color; }
.add { margin: 24rpx auto; width: 120rpx; height: 120rpx; border-radius: 20rpx; background: $uni-color-primary; color:#fff; font-size: 72rpx; display:flex; align-items:center; justify-content:center; }
.empty { display:flex; flex-direction: column; align-items:center; padding: 60rpx 0; color:$uni-text-color-grey; }
.empty-img { width: 220rpx; margin-bottom: 20rpx; }
.empty-text { margin-bottom: 8rpx; }
.list { background:$uni-bg-color-grey; }
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 12rpx; padding: 16rpx 12rpx; align-items:center; border-bottom: 1rpx solid $uni-border-color; }
.col.name { padding-left: 12rpx; }
.col.amount { text-align:right; padding-right: 12rpx; color:$uni-text-color; }
.bottom { position: fixed; left:0; right:0; bottom:0; background:$uni-bg-color-grey; padding: 16rpx 24rpx calc(env(safe-area-inset-bottom) + 16rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.16); }
.primary { width: 100%; background: $uni-color-primary; color:#fff; border-radius: 999rpx; padding: 20rpx 0; font-weight:800; }
/* 收款/付款页样式 */
.pay-row .pay-input { text-align: right; color:$uni-text-color; }
.textarea { position: relative; padding: 16rpx 24rpx; background:$uni-bg-color-grey; border-top: 1rpx solid $uni-border-color; }
.amount-badge { position: absolute; right: 24rpx; top: -36rpx; background: $uni-color-primary; color:#fff; padding: 8rpx 16rpx; border-radius: 12rpx; font-size: 24rpx; }
.date-mini { position: absolute; right: 24rpx; bottom: 20rpx; color:$uni-text-color-grey; font-size: 24rpx; }
/* 分类chips样式选中后文字变红 */
.chips { display:flex; flex-wrap: wrap; gap: 12rpx; padding: 12rpx 24rpx; }
.chip { padding: 10rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color:$uni-text-color-grey; }
.chip.active { background: $uni-color-primary; color:#fff; }
</style>