This commit is contained in:
2025-09-16 22:11:19 +08:00
parent 562ec4abf9
commit 46c5682960
65 changed files with 1997 additions and 56 deletions

View File

@@ -0,0 +1,224 @@
<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>