Files
PartsInquiry/frontend/pages/order/create.vue
2025-09-27 22:57:59 +08:00

462 lines
22 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="seg3" v-if="biz==='sale'">
<view :class="['seg3-item', saleType==='out' && 'active']" @click="saleType='out'">
<image :src="SEG_ICONS.sale.out" class="seg3-icon" mode="aspectFit" />
<text>出货</text>
</view>
<view :class="['seg3-item', saleType==='return' && 'active']" @click="saleType='return'">
<image :src="SEG_ICONS.sale.return" class="seg3-icon" mode="aspectFit" />
<text>退货</text>
</view>
<view :class="['seg3-item', saleType==='collect' && 'active']" @click="saleType='collect'">
<image :src="SEG_ICONS.sale.collect" class="seg3-icon" mode="aspectFit" />
<text>收款</text>
</view>
</view>
<view class="seg3" v-else-if="biz==='purchase'">
<view :class="['seg3-item', purchaseType==='in' && 'active']" @click="purchaseType='in'">
<image :src="SEG_ICONS.purchase.in" class="seg3-icon" mode="aspectFit" />
<text>进货</text>
</view>
<view :class="['seg3-item', purchaseType==='return' && 'active']" @click="purchaseType='return'">
<image :src="SEG_ICONS.purchase.return" class="seg3-icon" mode="aspectFit" />
<text>退货</text>
</view>
<view :class="['seg3-item', purchaseType==='pay' && 'active']" @click="purchaseType='pay'">
<image :src="SEG_ICONS.purchase.pay" class="seg3-icon" mode="aspectFit" />
<text>付款</text>
</view>
</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="info-card">
<view class="info-field party-field" @click="biz==='sale' ? chooseCustomer() : chooseSupplier()">
<text class="info-label">{{ biz==='sale' ? '客户' : '供应商' }}</text>
<text class="info-value">{{ biz==='sale' ? customerLabel : supplierLabel }}</text>
</view>
<picker class="info-field time-field" mode="date" :value="order.orderTime" @change="onDateChange">
<view>
<text class="info-label">时间</text>
<text class="info-value">{{ order.orderTime }}</text>
</view>
</picker>
<button class="info-action" @click="chooseProduct">
<image src="/static/icons/icons8-shopping-cart-100.png" class="info-icon" mode="aspectFit" />
<text>加商品</text>
</button>
</view>
<view class="summary">
<text class="sel">选中货品{{ totalQuantity }}</text>
<view class="total-pill">合计 ¥ {{ totalAmount.toFixed(2) }}</view>
</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">
<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,
SEG_ICONS: {
sale: {
out: '/static/icons/icons8-shopping-cart-100.png',
return: '/static/icons/icons8-return-purchase-50.png',
collect: '/static/icons/icons8-profit-50.png'
},
purchase: {
in: '/static/icons/icons8-purchase-order-100.png',
return: '/static/icons/icons8-return-purchase-50.png',
pay: '/static/icons/icons8-dollar-ethereum-exchange-50.png'
}
}
}
},
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: {
fixMojibake(s) {
try {
if (!s) return s
const bad = /[ÂÃæåé¼½¢]/.test(s)
if (!bad) return s
return decodeURIComponent(escape(s))
} catch(_) { return s }
},
normalizeCats(list) {
if (!Array.isArray(list)) return []
return list.map(it => ({
key: it && it.key || '',
label: this.fixMojibake((it && it.label) || '')
})).filter(it => it.key && it.label)
},
async fetchCategories() {
try {
const res = await get('/api/finance/categories')
if (res && Array.isArray(res.incomeCategories)) this._incomeCategories = this.normalizeCats(res.incomeCategories)
if (res && Array.isArray(res.expenseCategories)) this._expenseCategories = this.normalizeCats(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; }
/* 三段式胶囊切换 */
.seg3 { display:flex; gap: 0; margin: 12rpx 16rpx; padding: 6rpx; background:#fff; border-radius: 999rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.04); }
.seg3-item { flex:1; display:flex; align-items:center; justify-content:center; gap: 8rpx; padding: 12rpx 0; color:$uni-text-color; border-radius: 999rpx; transition: box-shadow .2s ease, background .2s ease; }
/* 间隔通过内边距处理,避免空选择器 */
.seg3-item.active { background:#fff; color:#4C8DFF; box-shadow: 0 3rpx 10rpx rgba(76,141,255,0.16); }
.seg3-icon { width: 28rpx; height: 28rpx; opacity: .9; }
.field { display:flex; align-items:center; justify-content: flex-start; padding: 22rpx 24rpx; background: #f8faff; gap: 16rpx; }
.label { width: 160rpx; color:$uni-text-color-grey; }
.value { flex:1; color:$uni-text-color; text-align:right; }
/* 汇总卡片:白底卡片+主色按钮 */
.summary { display:flex; justify-content: space-between; align-items:center; padding: 18rpx 20rpx; margin: 16rpx 18rpx 10rpx; background: none; border-radius: 18rpx; color:$uni-text-color; }
/* 加号改为图标按钮 */
.add { margin: 24rpx auto 18rpx; width: 120rpx; height: 120rpx; border-radius: 28rpx; background: none; border:0; color:$uni-color-primary; font-size: 72rpx; display:flex; align-items:center; justify-content:center; box-shadow: none; }
.empty { display:flex; flex-direction: column; align-items:center; padding: 60rpx 0; color:$uni-text-color-grey; }
.empty-img { width: 160rpx; margin-bottom: 16rpx; }
.empty-text { margin-bottom: 8rpx; }
.list { background:#fff; margin: 0 18rpx 20rpx; border-radius: 18rpx; overflow: hidden; box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.06); }
.row { display:grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 12rpx; padding: 18rpx 16rpx; align-items:center; }
.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: 6rpx 18rpx calc(env(safe-area-inset-bottom) + 2rpx); box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.16); }
.order .bottom button { margin: 0; }
/* 仅限开单页底部按钮样式(缩小高度) */
.order .bottom .primary { width: 100%; background: $uni-color-primary; color:#fff; border-radius: 999rpx; padding: 14rpx 0; font-weight:700; font-size: 28rpx; }
.order .bottom .ghost { background: transparent; color: $uni-color-primary; border: 0; border-radius: 999rpx; padding: 12rpx 0; font-size: 28rpx; }
/* 收款/付款页样式 */
.pay-row .pay-input { text-align: right; color:$uni-text-color; }
.textarea { position: relative; padding: 16rpx 24rpx; background:#f8faff; }
.amount-badge { position: absolute; right: 24rpx; top: -32rpx; background: $uni-color-primary; color:#fff; padding: 10rpx 20rpx; border-radius: 14rpx; font-size: 24rpx; }
.date-mini { position: absolute; right: 24rpx; bottom: 20rpx; color:$uni-text-color-grey; font-size: 24rpx; }
/* 分类chips样式选中后文字变红 */
.chips { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12rpx 16rpx; padding: 12rpx 24rpx; }
.chip { padding: 12rpx 20rpx; border-radius: 999rpx; background: $uni-bg-color-hover; color:$uni-text-color-grey; text-align:center; }
.chip.active { background: rgba(76,141,255,0.15); color:$uni-color-primary; }
/* 顶部业务 Tabs 显示 */
/* 快捷操作宫格 */
/* 信息卡片式表达(更稳重) */
.info-card { display:grid; grid-template-columns: 1fr 1fr auto; gap: 10rpx; margin: 10rpx 12rpx 0; background:transparent; padding: 0; align-items:center; }
.info-field { background:#fff; border:0; border-radius: 14rpx; padding: 12rpx 14rpx; box-shadow: 0 6rpx 16rpx rgba(0,0,0,0.06); }
.info-label { color:$uni-text-color-grey; font-size: 24rpx; margin-right: 8rpx; }
.info-value { color:$uni-text-color; font-weight: 700; }
/* 缩小“加商品”按钮尺寸,仅在本页卡片内 */
.order .info-card .info-action { display:flex; align-items:center; gap: 6rpx; background:$uni-color-primary; color:#fff; border-radius: 12rpx; padding: 8rpx 12rpx; box-shadow: 0 5rpx 12rpx rgba(76,141,255,0.18); font-size: 26rpx; }
.order .info-card .info-icon { width: 24rpx; height: 24rpx; }
</style>