381 lines
17 KiB
Vue
381 lines
17 KiB
Vue
<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>
|
||
.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; }
|
||
/* 收款/付款页样式 */
|
||
.pay-row .pay-input { text-align: right; color:#333; }
|
||
.textarea { position: relative; padding: 16rpx 24rpx; background:#fff; border-top: 1rpx solid #eee; }
|
||
.amount-badge { position: absolute; right: 24rpx; top: -36rpx; background: #d1f0ff; color:#107e9b; padding: 8rpx 16rpx; border-radius: 12rpx; font-size: 24rpx; }
|
||
.date-mini { position: absolute; right: 24rpx; bottom: 20rpx; color:#666; font-size: 24rpx; }
|
||
/* 分类chips样式:选中后文字变红 */
|
||
.chips { display:flex; flex-wrap: wrap; gap: 12rpx; padding: 12rpx 24rpx; }
|
||
.chip { padding: 10rpx 20rpx; border-radius: 999rpx; background: #f4f4f4; color:#666; }
|
||
.chip.active { color: #e54d42; }
|
||
</style>
|
||
|
||
|