Files
crm_uiapp/src/pages-erp/purchase-order/form/index.vue
2026-05-15 15:00:41 +08:00

525 lines
17 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="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
:title="getTitle"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 表单区域 -->
<view>
<wd-form ref="formRef" :model="formData" :rules="formRules">
<wd-cell-group title="基本信息" border>
<wd-cell title="关联请购单" title-width="180rpx" is-link center @click="openRequisitionPicker">
<text v-if="formData.purchaseRequisitionNo" class="text-[#1890ff]">{{ formData.purchaseRequisitionNo }}</text>
<text v-else class="text-[#999]">请选择可选</text>
</wd-cell>
<wd-cell title="供应商" title-width="180rpx" prop="supplierId" center>
<wd-picker
v-model="formData.supplierId"
:columns="supplierColumns"
label=""
placeholder="请选择供应商"
@confirm="onSupplierConfirm"
/>
</wd-cell>
<wd-cell title="采购时间" title-width="180rpx" prop="orderTime" center>
<wd-datetime-picker
v-model="formData.orderTime"
type="datetime"
label=""
placeholder="请选择采购时间"
/>
</wd-cell>
</wd-cell-group>
<wd-cell-group title="合同信息" border>
<wd-cell title="发票类别" title-width="180rpx" center>
<wd-radio-group v-model="formData.invoiceType" shape="button">
<wd-radio :value="1">
增值税普通发票
</wd-radio>
<wd-radio :value="2">
增值税专用发票
</wd-radio>
</wd-radio-group>
</wd-cell>
<wd-cell title="运费承担方" title-width="180rpx" center>
<wd-radio-group v-model="formData.freightPayer" shape="button">
<wd-radio :value="1">
卖方承担
</wd-radio>
<wd-radio :value="2">
买方承担
</wd-radio>
</wd-radio-group>
</wd-cell>
<wd-cell title="结算方式" title-width="180rpx" center>
<wd-radio-group v-model="formData.settlementMethod" shape="button">
<wd-radio :value="1">
款到发货
</wd-radio>
<wd-radio :value="2">
货到票到付款
</wd-radio>
<wd-radio :value="3">
预付部分货到付清
</wd-radio>
</wd-radio-group>
</wd-cell>
</wd-cell-group>
<wd-cell-group title="其他信息" border>
<wd-input
v-model="formData.discountPercent"
label="优惠率(%)"
label-width="180rpx"
type="number"
placeholder="请输入优惠率"
clearable
/>
<wd-input
v-model="formData.depositPrice"
label="定金金额"
label-width="180rpx"
type="number"
placeholder="请输入定金金额"
clearable
/>
<wd-textarea
v-model="formData.remark"
label="备注"
label-width="180rpx"
placeholder="请输入备注"
:maxlength="200"
show-word-limit
clearable
/>
</wd-cell-group>
</wd-form>
</view>
<!-- 订单明细 -->
<view class="mx-24rpx mt-24rpx pb-180rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
订单明细{{ formData.items?.length || 0 }}
</view>
<wd-button size="small" type="primary" @click="handleAddItem">
+ 添加产品
</wd-button>
</view>
<view
v-for="(item, index) in formData.items"
:key="index"
class="mb-16rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
>
<view class="flex items-center justify-between bg-[#f8f8f8] px-24rpx py-12rpx">
<text class="text-28rpx text-[#333] font-semibold">产品 #{{ index + 1 }} {{ item.productName || '' }}</text>
<wd-button size="small" type="error" plain @click="handleRemoveItem(index)">
删除
</wd-button>
</view>
<view class="p-24rpx">
<view class="mb-16rpx">
<text class="mb-8rpx block text-24rpx text-[#999]">选择产品</text>
<wd-picker
v-model="item.productId"
:columns="productColumns"
placeholder="请选择产品"
@confirm="(e: any) => onProductConfirm(e, index)"
/>
</view>
<!-- 自动填充的产品信息只读 -->
<view v-if="item.productId" class="mb-16rpx rounded-8rpx bg-[#f9f9f9] p-16rpx">
<view class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">规格</text>
<text class="text-[#333]">{{ item.productSpec || '-' }}</text>
</view>
<view class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">单位</text>
<text class="text-[#333]">{{ item.productUnitName || '-' }}</text>
</view>
<view class="mb-8rpx flex justify-between text-24rpx">
<text class="text-[#999]">条码</text>
<text class="text-[#333]">{{ item.productBarCode || '-' }}</text>
</view>
<view class="flex justify-between text-24rpx">
<text class="text-[#999]">供应商</text>
<text class="text-[#333]">{{ item.supplierName || '-' }}</text>
</view>
</view>
<view class="mb-16rpx flex gap-16rpx">
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">数量</text>
<wd-input
v-model="item.count"
type="number"
placeholder="数量"
clearable
/>
</view>
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">单价</text>
<wd-input
v-model="item.productPrice"
type="number"
placeholder="单价"
clearable
/>
</view>
</view>
<view class="mb-16rpx flex gap-16rpx">
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">税率(%)</text>
<wd-input
v-model="item.taxPercent"
type="number"
placeholder="税率"
clearable
/>
</view>
<view class="flex-1">
<text class="mb-8rpx block text-24rpx text-[#999]">金额</text>
<text class="block pt-16rpx text-28rpx text-[#333]">{{ calcTotalPrice(item) }}</text>
</view>
</view>
<view>
<text class="mb-8rpx block text-24rpx text-[#999]">备注</text>
<wd-input
v-model="item.remark"
placeholder="请输入备注"
clearable
/>
</view>
</view>
</view>
<view v-if="!formData.items?.length" class="py-60rpx text-center text-28rpx text-[#999]">
暂无产品请点击添加产品按钮
</view>
</view>
<!-- 底部保存按钮 -->
<view class="yd-detail-footer">
<wd-button
type="primary"
block
:loading="formLoading"
@click="handleSubmit"
>
保存
</wd-button>
</view>
<!-- 请购单选择弹窗 -->
<wd-popup v-model="requisitionPickerVisible" position="bottom" :safe-area-inset-bottom="true">
<view class="bg-white">
<view class="flex items-center justify-between border-b border-[#eee] px-24rpx py-24rpx">
<text class="text-32rpx font-semibold">选择请购单</text>
<wd-icon name="close" size="40rpx" @click="requisitionPickerVisible = false" />
</view>
<view class="max-h-[60vh] overflow-y-auto">
<view v-if="requisitionLoading" class="py-60rpx text-center text-[#999]">
加载中...
</view>
<view v-else-if="requisitionList.length === 0" class="py-60rpx text-center text-[#999]">
暂无已审核的请购单
</view>
<view v-else>
<view
v-for="req in requisitionList"
:key="req.id"
class="border-b border-[#f5f5f5] px-24rpx py-20rpx"
@click="onRequisitionSelect(req)"
>
<view class="mb-8rpx flex items-center justify-between">
<text class="text-28rpx text-[#333] font-semibold">{{ req.no }}</text>
<text class="text-24rpx text-[#1890ff]">¥{{ req.totalPrice || 0 }}</text>
</view>
<view class="flex items-center justify-between text-24rpx text-[#999]">
<text>请购人{{ req.requesterNickname || '-' }}</text>
<text>{{ formatDate(req.requestTime) }}</text>
</view>
</view>
</view>
</view>
</view>
</wd-popup>
</view>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
import type { PurchaseOrder, PurchaseOrderItem } from '@/api/erp/purchase-order'
import type { ProductSimple } from '@/api/erp/product'
import type { SupplierSimple } from '@/api/erp/supplier'
import type { PurchaseRequisition } from '@/api/erp/purchase-requisition'
import { computed, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { createPurchaseOrder, getPurchaseOrder, updatePurchaseOrder } from '@/api/erp/purchase-order'
import { getProductSimpleList } from '@/api/erp/product'
import { getSupplierSimpleList } from '@/api/erp/supplier'
import { getPurchaseRequisition, getPurchaseRequisitionPage } from '@/api/erp/purchase-requisition'
import { formatDate as formatDateValue } from '@/utils/date'
import { navigateBackPlus } from '@/utils'
const props = defineProps<{
id?: number | any
}>()
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const toast = useToast()
const getTitle = computed(() => props.id ? '编辑采购订单' : '新增采购订单')
const formLoading = ref(false)
const formData = ref<PurchaseOrder>({
id: undefined,
purchaseRequisitionId: undefined,
purchaseRequisitionNo: undefined,
supplierId: undefined,
orderTime: undefined,
discountPercent: undefined,
depositPrice: undefined,
invoiceType: undefined,
freightPayer: undefined,
settlementMethod: undefined,
remark: '',
items: [],
})
const formRules = {
supplierId: [{ required: true, message: '供应商不能为空' }],
orderTime: [{ required: true, message: '采购时间不能为空' }],
}
const formRef = ref<FormInstance>()
const supplierList = ref<SupplierSimple[]>([])
const productList = ref<ProductSimple[]>([])
const requisitionList = ref<PurchaseRequisition[]>([])
const requisitionPickerVisible = ref(false)
const requisitionLoading = ref(false)
/** 供应商下拉列 */
const supplierColumns = computed(() => [
supplierList.value.map(s => ({ value: s.id, label: s.name })),
])
/** 产品下拉列 */
const productColumns = computed(() => [
productList.value.map(p => ({ value: p.id, label: p.name })),
])
/** 供应商选择回调 */
function onSupplierConfirm({ value }: any) {
formData.value.supplierId = value?.[0]
}
/** 格式化日期 */
function formatDate(dateVal?: string | number | Date) {
return formatDateValue(dateVal) || '-'
}
/** 打开请购单选择弹窗 */
async function openRequisitionPicker() {
requisitionPickerVisible.value = true
requisitionLoading.value = true
try {
const res = await getPurchaseRequisitionPage({ pageNo: 1, pageSize: 50, status: 2 })
requisitionList.value = res.list || []
} finally {
requisitionLoading.value = false
}
}
/** 请购单选择回调:带出税率税额等信息 */
async function onRequisitionSelect(requisition: PurchaseRequisition) {
requisitionPickerVisible.value = false
formData.value.purchaseRequisitionId = requisition.id
formData.value.purchaseRequisitionNo = requisition.no
// 自动填充供应商
if (requisition.supplierId) {
formData.value.supplierId = requisition.supplierId
}
try {
toast.loading('加载请购单明细...')
// 获取请购单详情(包含明细项)
const detail = await getPurchaseRequisition(requisition.id!)
// 转换请购单明细为采购订单明细(带上税率和税额)
const orderItems: PurchaseOrderItem[] = (detail.items || []).map(item => ({
productId: item.productId,
productName: item.productName,
productSpec: item.productSpec,
productUnitId: item.productUnitId,
productUnitName: item.productUnitName,
productBarCode: item.productBarCode,
productPrice: item.productPrice,
count: item.count,
totalProductPrice: item.totalProductPrice || item.totalPrice,
taxPercent: item.taxPercent || 0,
taxPrice: item.taxPrice || 0,
totalPrice: item.totalPrice,
supplierId: item.supplierId,
supplierName: item.supplierName,
remark: item.remark,
}))
formData.value.items = orderItems
// 带上备注
if (detail.remark) {
formData.value.remark = `基于请购单 ${detail.no} 生成\n${detail.remark}`
}
toast.success('已导入请购单明细')
} catch {
toast.error('获取请购单详情失败')
}
}
/** 产品选择回调:自动填充规格/单位/条码/单价/供应商 */
function onProductConfirm({ value }: any, index: number) {
const productId = value?.[0]
if (formData.value.items && formData.value.items[index]) {
const item = formData.value.items[index]
item.productId = productId
const product = productList.value.find(p => p.id === productId)
if (product) {
item.productName = product.name
item.productSpec = product.standard || ''
item.productUnitName = product.unitName || ''
item.productUnitId = product.unitId as any
item.productBarCode = product.barCode || ''
item.productPrice = product.purchasePrice
item.supplierId = product.supplierId
item.supplierName = product.supplierName || ''
}
}
}
/** 计算行项金额 */
function calcTotalPrice(item: PurchaseOrderItem) {
if (item.productPrice && item.count) {
const total = Number(item.productPrice) * Number(item.count)
return `¥${total.toFixed(2)}`
}
return '-'
}
/** 加载下拉列表数据 */
async function loadDropdownData() {
try {
const [suppliers, products] = await Promise.all([
getSupplierSimpleList(),
getProductSimpleList(),
])
supplierList.value = suppliers || []
productList.value = products || []
} catch {
// error handled by http
}
}
/** 返回上一页 */
function handleBack() {
navigateBackPlus('/pages-erp/purchase-order/index')
}
/** 添加订单明细行 */
function handleAddItem() {
if (!formData.value.items) {
formData.value.items = []
}
formData.value.items.push({
productId: undefined as any,
productName: undefined as any,
productUnitId: undefined as any,
productUnitName: undefined as any,
productPrice: undefined,
productSpec: undefined as any,
productBarCode: undefined as any,
count: 1 as any,
taxPercent: undefined,
supplierId: undefined,
supplierName: undefined as any,
remark: '',
})
}
/** 删除订单明细行 */
function handleRemoveItem(index: number) {
uni.showModal({
title: '提示',
content: `确定要删除第 ${index + 1} 项产品吗?`,
success: (res) => {
if (res.confirm) {
formData.value.items?.splice(index, 1)
}
},
})
}
/** 加载详情 */
async function getDetail() {
if (!props.id) {
return
}
formData.value = await getPurchaseOrder(props.id)
}
/** 提交表单 */
async function handleSubmit() {
const { valid } = await formRef.value!.validate()
if (!valid) {
return
}
// 校验订单明细
if (!formData.value.items || formData.value.items.length === 0) {
toast.warning('请至少添加一项产品明细')
return
}
for (let i = 0; i < formData.value.items.length; i++) {
const item = formData.value.items[i]
if (!item.productId) {
toast.warning(`${i + 1} 项产品ID不能为空`)
return
}
if (!item.count) {
toast.warning(`${i + 1} 项产品数量不能为空`)
return
}
}
formLoading.value = true
try {
if (props.id) {
await updatePurchaseOrder(formData.value)
toast.success('修改成功')
} else {
await createPurchaseOrder(formData.value)
toast.success('新增成功')
}
setTimeout(() => {
handleBack()
}, 500)
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
await loadDropdownData()
getDetail()
})
</script>
<style lang="scss" scoped>
</style>