Files
crm_uiapp/src/pages-erp/purchase-in/index.vue

801 lines
24 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="采购入库"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<SearchForm @search="handleQuery" @reset="handleReset" />
<!-- 状态快速筛选 -->
<view class="mx-24rpx mb-16rpx flex items-center overflow-hidden rounded-12rpx bg-white shadow-sm">
<view
v-for="tab in statusTabs"
:key="tab.value"
class="relative flex-1 py-20rpx text-center text-26rpx"
:class="activeStatus === tab.value ? 'text-[#1890ff] font-semibold' : 'text-[#666]'"
@click="handleQuickFilter(tab.value)"
>
{{ tab.label }}
<view
v-if="activeStatus === tab.value"
class="absolute bottom-0 left-1/4 h-4rpx w-1/2 rounded-2rpx bg-[#1890ff]"
/>
</view>
</view>
<!-- 采购入库列表 -->
<view class="px-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-20rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleCardClick(item)"
>
<view class="p-24rpx">
<!-- 头部单号 + 状态 -->
<view class="mb-16rpx flex items-center justify-between">
<view class="text-30rpx text-[#333] font-semibold">
{{ item.no }}
</view>
<view
class="rounded-8rpx px-16rpx py-4rpx text-24rpx"
:class="getStatusClass(item.status, item.isQualified)"
>
{{ getStatusText(item.status, item.isQualified) }}
</view>
</view>
<!-- 供应商 -->
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">供应商</text>
<text>{{ item.supplierName || '-' }}</text>
</view>
<!-- 产品 -->
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">产品</text>
<text class="line-clamp-1 ml-16rpx text-right" style="max-width: 400rpx;">{{ item.productNames || '-' }}</text>
</view>
<!-- 入库时间 -->
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">入库时间</text>
<text>{{ formatDate(item.inTime) }}</text>
</view>
<!-- 创建人 -->
<view class="mb-8rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">创建人</text>
<text>{{ item.creatorName || '-' }}</text>
</view>
<!-- 批次号 -->
<view class="mb-12rpx flex items-center justify-between text-26rpx text-[#666]">
<text class="text-[#999]">批次号</text>
<text class="line-clamp-1 ml-16rpx text-right" style="max-width: 400rpx;">{{ formatItemBatchNos(item) || '-' }}</text>
</view>
<!-- 数量金额区域 -->
<view class="mt-12rpx flex items-center justify-around border-t border-[#f0f0f0] pt-16rpx">
<view class="text-center">
<view class="text-30rpx text-[#333] font-semibold">
{{ item.totalCount || 0 }}
</view>
<view class="mt-4rpx text-22rpx text-[#999]">
总数量
</view>
</view>
<view class="text-center">
<view class="text-30rpx text-[#e6a23c] font-semibold">
¥{{ item.totalPrice || 0 }}
</view>
<view class="mt-4rpx text-22rpx text-[#999]">
应付
</view>
</view>
<view class="text-center">
<view class="text-30rpx text-[#52c41a] font-semibold">
¥{{ item.paymentPrice || 0 }}
</view>
<view class="mt-4rpx text-22rpx text-[#999]">
已付
</view>
</view>
<view class="text-center">
<view
class="text-30rpx font-semibold"
:class="(item.totalPrice || 0) - (item.paymentPrice || 0) > 0 ? 'text-[#f5222d]' : 'text-[#999]'"
>
¥{{ (item.totalPrice || 0) - (item.paymentPrice || 0) }}
</view>
<view class="mt-4rpx text-22rpx text-[#999]">
未付
</view>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="flex flex-wrap gap-12rpx px-24rpx pb-20rpx" @click.stop>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:query'])"
size="small" plain @click="handleDetail(item)"
>
详情
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:update']) && item.status === 10"
size="small" type="primary" plain @click="handleEdit(item)"
>
修改
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:query'])"
size="small" type="warning" plain @click="handleScanIn(item)"
>
扫码入库
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:submit-approval']) && item.status === 10 && !item.hasApprovalRecords"
size="small" type="success" plain @click="handleSubmitApproval(item)"
>
提交审批
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:query-approval']) && item.hasApprovalRecords"
size="small" type="info" plain @click="handleViewApproval(item)"
>
审批记录
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:approval-record:process']) && item.hasApprovalRecords && item.status !== 20 && item.status !== 30"
size="small" type="primary" plain @click="handleProcessApproval(item)"
>
处理审批
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:update-status']) && item.status === 10"
size="small" type="success" plain @click="handleAudit(item)"
>
审核
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:update-status']) && (item.status === 20 || item.status === 30)"
size="small" type="warning" plain @click="handleReverseAudit(item.id!)"
>
反审核
</wd-button>
<wd-button
v-if="hasAccessByCodes(['erp:purchase-in:delete']) && item.status === 10 && !item.hasApprovalRecords"
size="small" type="error" plain @click="handleDelete(item.id!)"
>
删除
</wd-button>
</view>
</view>
<!-- 加载更多 -->
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
<wd-status-tip image="content" tip="暂无采购入库数据" />
</view>
<wd-loadmore
v-if="list.length > 0"
:state="loadMoreState"
@reload="loadMore"
/>
</view>
<!-- 新增按钮 -->
<wd-fab
v-if="hasAccessByCodes(['erp:purchase-in:create'])"
position="right-bottom"
type="primary"
:expandable="false"
@click="handleAdd"
/>
<!-- 审核弹窗 -->
<wd-popup v-model="auditVisible" position="bottom" closable @close="auditVisible = false">
<view class="p-32rpx">
<view class="mb-32rpx text-center text-32rpx text-[#333] font-semibold">
采购入库审核
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
是否合格
</view>
<wd-switch v-model="auditForm.isQualified" @change="handleQualifiedChange" />
</view>
<view v-if="!auditForm.isQualified" class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
返回方式
</view>
<wd-radio-group v-model="auditForm.returnType" shape="button">
<wd-radio
v-for="opt in RETURN_TYPE_OPTIONS"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</wd-radio>
</wd-radio-group>
</view>
<view v-if="!auditForm.isQualified" class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
返回备注
</view>
<wd-textarea
v-model="auditForm.returnRemark"
placeholder="请输入返回方式备注"
:maxlength="200"
/>
</view>
<view class="mt-32rpx flex gap-24rpx">
<wd-button class="flex-1" plain @click="auditVisible = false">
取消
</wd-button>
<wd-button class="flex-1" type="primary" :loading="auditLoading" @click="submitAudit">
确定
</wd-button>
</view>
</view>
</wd-popup>
<!-- 提交审批弹窗 -->
<wd-popup v-model="submitApprovalVisible" position="bottom" closable @close="submitApprovalVisible = false">
<view class="p-32rpx">
<view class="mb-32rpx text-center text-32rpx text-[#333] font-semibold">
提交审批
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
审批人
</view>
<wd-input
v-model="submitApprovalForm.nextApprover"
placeholder="请输入审批人用户名"
clearable
/>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
备注
</view>
<wd-textarea
v-model="submitApprovalForm.remark"
placeholder="请输入备注"
:maxlength="200"
/>
</view>
<view class="mt-32rpx flex gap-24rpx">
<wd-button class="flex-1" plain @click="submitApprovalVisible = false">
取消
</wd-button>
<wd-button class="flex-1" type="primary" :loading="submitApprovalLoading" @click="confirmSubmitApproval">
提交
</wd-button>
</view>
</view>
</wd-popup>
<!-- 处理审批弹窗 -->
<wd-popup v-model="processApprovalVisible" position="bottom" closable @close="processApprovalVisible = false">
<view class="p-32rpx">
<view class="mb-32rpx text-center text-32rpx text-[#333] font-semibold">
处理审批
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
审批结果
</view>
<wd-radio-group v-model="processApprovalForm.approvalResult" shape="button">
<wd-radio
v-for="opt in APPROVAL_RESULT_OPTIONS"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</wd-radio>
</wd-radio-group>
</view>
<view class="mb-24rpx">
<view class="mb-12rpx text-26rpx text-[#666]">
审批意见
</view>
<wd-textarea
v-model="processApprovalForm.comment"
placeholder="请输入审批意见"
:maxlength="200"
/>
</view>
<view class="mt-32rpx flex gap-24rpx">
<wd-button class="flex-1" plain @click="processApprovalVisible = false">
取消
</wd-button>
<wd-button class="flex-1" type="primary" :loading="processApprovalLoading" @click="confirmProcessApproval">
确定
</wd-button>
</view>
</view>
</wd-popup>
<!-- 审批记录弹窗 -->
<wd-popup v-model="approvalRecordsVisible" position="bottom" closable @close="approvalRecordsVisible = false">
<view class="p-32rpx">
<view class="mb-32rpx text-center text-32rpx text-[#333] font-semibold">
审批记录
</view>
<view v-if="approvalRecords.length === 0" class="py-60rpx text-center text-28rpx text-[#999]">
暂无审批记录
</view>
<view v-else class="max-h-600rpx overflow-y-auto">
<view
v-for="(record, index) in approvalRecords"
:key="record.id || index"
class="mb-16rpx rounded-12rpx bg-[#f8f8f8] p-20rpx"
>
<view class="mb-8rpx flex items-center justify-between">
<text class="text-28rpx text-[#333] font-semibold">{{ record.approverName || record.approver }}</text>
<view
class="rounded-8rpx px-12rpx py-4rpx text-22rpx"
:class="getApprovalResultClass(record.approvalResult)"
>
{{ getApprovalResultText(record.approvalResult) }}
</view>
</view>
<view v-if="record.comment" class="mb-8rpx text-26rpx text-[#666]">
意见{{ record.comment }}
</view>
<view class="text-24rpx text-[#999]">
{{ record.approvalTime ? new Date(record.approvalTime).toLocaleString('zh-CN') : '-' }}
</view>
</view>
</view>
<view class="mt-24rpx">
<wd-button type="primary" block @click="approvalRecordsVisible = false">
关闭
</wd-button>
</view>
</view>
</wd-popup>
</view>
</template>
<script lang="ts" setup>
import type { PurchaseIn } from '@/api/erp/purchase-in'
import type { LoadMoreState } from '@/http/types'
import { onReachBottom } from '@dcloudio/uni-app'
import { onMounted, reactive, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { APPROVAL_RESULT, APPROVAL_RESULT_OPTIONS, getApprovalRecordListByBiz, processApproval, submitApproval } from '@/api/erp/approval'
import { deletePurchaseIn, getPurchaseInPage, RETURN_TYPE_OPTIONS, updatePurchaseInStatus } from '@/api/erp/purchase-in'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import SearchForm from './components/search-form.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const { hasAccessByCodes } = useAccess()
const toast = useToast()
const total = ref(0)
const list = ref<PurchaseIn[]>([])
const loadMoreState = ref<LoadMoreState>('loading')
const activeStatus = ref<number | undefined>(undefined)
const queryParams = ref<Record<string, any>>({
pageNo: 1,
pageSize: 10,
})
const statusTabs = [
{ label: '全部', value: undefined },
{ label: '未审核', value: 10 },
{ label: '已审核', value: 20 },
{ label: '不合格', value: 30 },
]
// 审核相关
const auditVisible = ref(false)
const auditLoading = ref(false)
const auditForm = reactive({
id: undefined as number | undefined,
isQualified: true,
returnType: undefined as string | undefined,
returnRemark: undefined as string | undefined,
})
// 提交审批相关
const submitApprovalVisible = ref(false)
const submitApprovalLoading = ref(false)
const submitApprovalForm = reactive({
bizId: '' as string,
nextApprover: '' as string,
remark: '' as string,
})
// 处理审批相关
const processApprovalVisible = ref(false)
const processApprovalLoading = ref(false)
const processApprovalForm = reactive({
id: undefined as number | undefined,
approvalResult: APPROVAL_RESULT.APPROVED,
comment: '' as string,
})
// 审批记录相关
const approvalRecordsVisible = ref(false)
const approvalRecords = ref<any[]>([])
/** 格式化日期 */
function formatDate(date?: string) {
if (!date)
return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
/** 格式化批次号列表 */
function formatItemBatchNos(row: PurchaseIn): string {
const items = row?.items || []
const batchCountMap = new Map<string, number>()
items.forEach((it: any) => {
const batchNo = (it?.batchNo || '').trim()
if (!batchNo)
return
const count = Number(it?.count || 0)
batchCountMap.set(batchNo, (batchCountMap.get(batchNo) || 0) + count)
})
if (!batchCountMap.size)
return ''
return Array.from(batchCountMap.entries())
.map(([batchNo, count]) => `${batchNo}[${count}]`)
.join('')
}
/** 获取状态文本 */
function getStatusText(status?: number, isQualified?: boolean) {
if (status === 10)
return '未审核'
if (status === 20)
return isQualified ? '已审核' : '不合格'
if (status === 30)
return '不合格'
return '未知'
}
/** 获取状态样式 */
function getStatusClass(status?: number, isQualified?: boolean) {
if (status === 10)
return 'bg-[#fff7e6] text-[#fa8c16]'
if (status === 20)
return isQualified ? 'bg-[#f6ffed] text-[#52c41a]' : 'bg-[#fff1f0] text-[#f5222d]'
if (status === 30)
return 'bg-[#fff1f0] text-[#f5222d]'
return 'bg-[#f5f5f5] text-[#999]'
}
/** 获取审批结果文本 */
function getApprovalResultText(result?: number) {
if (result === APPROVAL_RESULT.PENDING)
return '待审批'
if (result === APPROVAL_RESULT.APPROVED)
return '已通过'
if (result === APPROVAL_RESULT.REJECTED)
return '已拒绝'
if (result === APPROVAL_RESULT.TRANSFERRED)
return '已转办'
return '未知'
}
/** 获取审批结果样式 */
function getApprovalResultClass(result?: number) {
if (result === APPROVAL_RESULT.PENDING)
return 'bg-[#fff7e6] text-[#fa8c16]'
if (result === APPROVAL_RESULT.APPROVED)
return 'bg-[#f6ffed] text-[#52c41a]'
if (result === APPROVAL_RESULT.REJECTED)
return 'bg-[#fff1f0] text-[#f5222d]'
return 'bg-[#f5f5f5] text-[#999]'
}
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
/** 查询采购入库列表 */
async function getList() {
loadMoreState.value = 'loading'
try {
const params = { ...queryParams.value }
const data = await getPurchaseInPage(params)
// 为每条记录检查是否有审批记录
const listWithApprovalStatus = await Promise.all(
data.list.map(async (item) => {
try {
const records = await getApprovalRecordListByBiz(String(item.id), 'erp_purchase_in')
return {
...item,
hasApprovalRecords: records && records.length > 0,
}
} catch {
return {
...item,
hasApprovalRecords: false,
}
}
}),
)
list.value = [...list.value, ...listWithApprovalStatus]
total.value = data.total
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
} catch {
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
loadMoreState.value = 'error'
}
}
/** 状态快速筛选 */
function handleQuickFilter(status?: number) {
activeStatus.value = status
queryParams.value.status = status
queryParams.value.pageNo = 1
list.value = []
getList()
}
/** 搜索按钮操作 */
function handleQuery(data?: Record<string, any>) {
queryParams.value = {
...data,
status: activeStatus.value,
pageNo: 1,
pageSize: queryParams.value.pageSize,
}
list.value = []
getList()
}
/** 重置按钮操作 */
function handleReset() {
activeStatus.value = undefined
handleQuery()
}
/** 加载更多 */
function loadMore() {
if (loadMoreState.value === 'finished') {
return
}
queryParams.value.pageNo++
getList()
}
/** 卡片点击 — 已审核打开详情,未审核打开编辑 */
function handleCardClick(item: PurchaseIn) {
if (item.status === 20 || item.status === 30) {
handleDetail(item)
} else if (item.status === 10) {
handleEdit(item)
}
}
/** 新增采购入库 */
function handleAdd() {
uni.navigateTo({
url: '/pages-erp/purchase-in/form/index',
})
}
/** 查看详情 */
function handleDetail(item: PurchaseIn) {
uni.navigateTo({
url: `/pages-erp/purchase-in/detail/index?id=${item.id}`,
})
}
/** 编辑 */
function handleEdit(item: PurchaseIn) {
uni.navigateTo({
url: `/pages-erp/purchase-in/form/index?id=${item.id}`,
})
}
/** 扫码入库 */
function handleScanIn(item: PurchaseIn) {
uni.navigateTo({
url: `/pages-erp/purchase-in/scan-in/index?id=${item.id}&no=${encodeURIComponent(item.no || '')}`,
})
}
/** 打开审核弹窗 */
function handleAudit(item: PurchaseIn) {
auditForm.id = item.id
auditForm.isQualified = true
auditForm.returnType = undefined
auditForm.returnRemark = undefined
auditVisible.value = true
}
/** 合格状态变化 */
function handleQualifiedChange(val: boolean) {
if (val) {
auditForm.returnType = undefined
auditForm.returnRemark = undefined
}
}
/** 提交审核 */
async function submitAudit() {
if (!auditForm.id)
return
if (!auditForm.isQualified && !auditForm.returnType) {
toast.warning('请选择返回方式')
return
}
auditLoading.value = true
try {
await updatePurchaseInStatus({
id: auditForm.id,
isQualified: auditForm.isQualified,
returnType: auditForm.returnType,
returnRemark: auditForm.returnRemark,
status: auditForm.isQualified ? 20 : 30,
})
toast.success('审核成功')
auditVisible.value = false
list.value = []
queryParams.value.pageNo = 1
getList()
} finally {
auditLoading.value = false
}
}
/** 反审核 */
function handleReverseAudit(id: number) {
uni.showModal({
title: '提示',
content: '确定反审核该入库单吗?',
success: async (res) => {
if (!res.confirm)
return
try {
await updatePurchaseInStatus({
id,
isQualified: false,
status: 10,
})
toast.success('反审核成功')
list.value = []
queryParams.value.pageNo = 1
getList()
} catch {
// error handled by http
}
},
})
}
/** 删除 */
function handleDelete(id: number) {
uni.showModal({
title: '提示',
content: '确定要删除该采购入库单吗?',
success: async (res) => {
if (!res.confirm)
return
try {
await deletePurchaseIn([id])
toast.success('删除成功')
list.value = []
queryParams.value.pageNo = 1
getList()
} catch {
// error handled by http
}
},
})
}
/** 提交审批 */
function handleSubmitApproval(item: PurchaseIn) {
submitApprovalForm.bizId = String(item.id)
submitApprovalForm.nextApprover = ''
submitApprovalForm.remark = ''
submitApprovalVisible.value = true
}
/** 确认提交审批 */
async function confirmSubmitApproval() {
if (!submitApprovalForm.nextApprover) {
toast.warning('请输入审批人')
return
}
submitApprovalLoading.value = true
try {
await submitApproval({
bizId: submitApprovalForm.bizId,
bizTableName: 'erp_purchase_in',
nextApprover: submitApprovalForm.nextApprover,
remark: submitApprovalForm.remark,
})
toast.success('提交审批成功')
submitApprovalVisible.value = false
list.value = []
queryParams.value.pageNo = 1
getList()
} finally {
submitApprovalLoading.value = false
}
}
/** 查看审批记录 */
async function handleViewApproval(item: PurchaseIn) {
try {
toast.loading('加载中...')
const records = await getApprovalRecordListByBiz(String(item.id), 'erp_purchase_in')
approvalRecords.value = records || []
approvalRecordsVisible.value = true
} finally {
toast.close()
}
}
/** 处理审批 */
async function handleProcessApproval(item: PurchaseIn) {
try {
toast.loading('加载中...')
const records = await getApprovalRecordListByBiz(String(item.id), 'erp_purchase_in')
// 找到待处理的审批记录
const pendingRecord = records?.find((r: any) => r.approvalResult === APPROVAL_RESULT.PENDING)
if (!pendingRecord) {
toast.warning('没有待处理的审批记录')
return
}
processApprovalForm.id = pendingRecord.id
processApprovalForm.approvalResult = APPROVAL_RESULT.APPROVED
processApprovalForm.comment = ''
processApprovalVisible.value = true
} finally {
toast.close()
}
}
/** 确认处理审批 */
async function confirmProcessApproval() {
if (!processApprovalForm.id)
return
processApprovalLoading.value = true
try {
await processApproval({
id: processApprovalForm.id,
approvalResult: processApprovalForm.approvalResult,
comment: processApprovalForm.comment,
})
toast.success('审批处理成功')
processApprovalVisible.value = false
list.value = []
queryParams.value.pageNo = 1
getList()
} finally {
processApprovalLoading.value = false
}
}
/** 触底加载更多 */
onReachBottom(() => {
loadMore()
})
/** 初始化 */
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
</style>