You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
crmeb/app/pages/supply_chain/material_receipt/index.vue

1113 lines
27 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<z-paging ref="paging" v-model="receiptRecords" @query="queryList" :layout-only="activeTab === 'form'">
<template #top>
<!-- 顶部标签:我要领用 / 领用记录 -->
<view class="tabs">
<view
class="tab"
v-if="checkPermi('material_collection_audit')"
:class="activeTab === 'allList' ? 'active' : ''"
@click="switchToAllList"
>
领用审核
</view>
<view
class="tab"
:class="activeTab === 'form' ? 'active' : ''"
@click="activeTab = 'form'"
>
我要领用
</view>
<view
class="tab"
:class="activeTab === 'list' ? 'active' : ''"
@click="switchToList"
>
领用记录
</view>
</view>
</template>
<!-- 表单区域 -->
<view class="tab-content" v-show="activeTab === 'form'">
<view class="form-card">
<view class="form-item">
<text class="label">物资选择</text>
<view class="multi-select">
<view class="selected-items" v-if="selectedMaterials.length > 0">
<view class="selected-item" v-for="(item, index) in selectedMaterials" :key="item.id">
<text>{{ item.cargoName }} ({{ item.cargoSpec }}) x {{ item.quantity }}</text>
<text class="remove-btn" @click="removeMaterial(index)">x</text>
</view>
</view>
<view class="select-btn" @click="showMaterialSelector">
{{ selectedMaterials.length > 0 ? '已选择 ' + selectedMaterials.length + ' 项' : '请选择物资' }}
</view>
</view>
</view>
<view class="form-item">
<text class="label">领用用途</text>
<textarea
class="textarea"
v-model="receiptForm.purpose"
placeholder="请输入领用用途"
maxlength="200"
:auto-height="true"
/>
<text class="count">{{ receiptForm.purpose.length }}/200</text>
</view>
<!-- <view class="form-item">
<text class="label">领用人</text>
<input
class="input"
type="text"
v-model="receiptForm.applicant"
placeholder="请输入领用人姓名"
/>
</view> -->
</view>
<view class="submit-section">
<view class="submit-btn" @click="submitReceipt">提交领用申请</view>
</view>
</view>
<!-- 物资选择弹窗 -->
<uni-popup ref="materialPopup" type="bottom" :animation="false">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">选择物资</text>
<text class="popup-close" @click="closeMaterialSelector">×</text>
</view>
<view class="popup-body">
<view class="search-box">
<input type="text" v-model="searchKeyword" placeholder="搜索物资名称或规格" class="search-input" />
</view>
<view class="material-list">
<view
class="material-item"
v-for="material in filteredMaterials"
:key="material.id"
>
<view class="material-info">
<text class="material-name">{{ material.cargoName }}</text>
<text class="material-spec">{{ material.cargoSpec }}</text>
</view>
<view class="item-actions">
<view v-if="isMaterialSelected(material.id)" class="quantity-input">
<text class="quantity-btn" @click="decreaseQuantity(material.id)">-</text>
<input
type="number"
:value="getMaterialQuantity(material.id)"
class="quantity-input-box"
min="1"
@input="updateQuantity(material.id, $event)"
/>
<text class="quantity-btn" @click="increaseQuantity(material.id)">+</text>
</view>
<view class="checkbox" :class="{ checked: isMaterialSelected(material.id) }" @click="toggleMaterial(material)">
<text v-if="isMaterialSelected(material.id)">✓</text>
</view>
</view>
</view>
</view>
</view>
<view class="popup-footer">
<view class="footer-btn cancel" @click="closeMaterialSelector">取消</view>
<view class="footer-btn confirm" @click="confirmMaterialSelection">确定</view>
</view>
</view>
</uni-popup>
<!-- 列表区域 -->
<view class="tab-content" v-show="activeTab === 'allList' || activeTab === 'list'">
<view
class="record-card"
v-for="item in receiptRecords"
:key="item.id"
>
<view class="card-header">
<view class="left">
<text class="type">领用单</text>
<text
class="status"
:class="{
'status-pending': getStatusText(item) === '待审批',
'status-approved': getStatusText(item) === '已审批',
'status-rejected': getStatusText(item) === '已拒绝',
'status-canceled': getStatusText(item) === '已取消',
'status-verified': getStatusText(item) === '已核销'
}"
>
{{ getStatusText(item) }}
</text>
</view>
<view class="header-right">
<text class="time">
{{ item.billDate || '' }}
</text>
<text class="bill-number">
{{ item.billNumber || '' }}
</text>
</view>
</view>
<view class="card-body">
<view class="row">
<text class="label">领用用途</text>
<text class="value">
{{ item.remark || '—' }}
</text>
</view>
<view class="row" v-if="item.ckBillCargos && item.ckBillCargos.length > 0">
<text class="label">领用物资</text>
<text class="value">
<text v-for="(cargo, index) in item.ckBillCargos" :key="cargo.id">
{{ cargo.cargoName }}{{ cargo.cargoSpec ? ` (${cargo.cargoSpec})` : '' }}{{ cargo.cargoWt ? ` x ${parseFloat(cargo.cargoWt).toFixed(2).replace(/\.?0+$/, '')}` : '' }}{{ index < item.ckBillCargos.length - 1 ? '、' : '' }}
</text>
</text>
</view>
</view>
<view class="card-footer" v-if="activeTab === 'allList'">
<view class="action-buttons" v-if="item.cancelStatus === '0' && item.billStatus === '0'">
<view class="action-btn approve-btn" v-if="item.auditStatus === '0'" @click="approveBill(item.id)">审核通过</view>
<view class="action-btn reject-btn" v-if="item.auditStatus === '0'" @click="handleRejectBill(item.id)">驳回</view>
<view class="action-btn cancel-btn" v-if="item.auditStatus === '1'" @click="cancelAudit(item.id)">撤销审核</view>
</view>
</view>
<view class="card-footer" v-else>
<view class="action-buttons">
<view class="action-btn cancel-btn" v-if="item.cancelStatus === '0' && item.billStatus === '0'" @click="cancelBill(item.id)">取消单据</view>
<view class="action-btn voucher-btn" v-if="item.auditStatus === '1' && item.billStatus === '0'" @click="viewVoucher(item)">查看凭证</view>
</view>
</view>
</view>
</view>
<!-- 凭证弹窗 -->
<uni-popup ref="voucherPopup" type="center" :animation="false">
<view class="voucher-popup">
<view class="voucher-header">
<text class="voucher-title">领用凭证</text>
<text class="voucher-close" @click="closeVoucherPopup">×</text>
</view>
<view class="voucher-body">
<view class="voucher-info">
<text class="voucher-label">单据编号</text>
<text class="voucher-value">{{ currentVoucher ? currentVoucher.billNumber : '' }}</text>
</view>
<view class="qrcode-container">
<canvas canvas-id="qrcode" :style="{width: `${qrcodeSize}px`, height: `${qrcodeSize}px`}" />
</view>
</view>
<view class="voucher-footer">
<view class="footer-btn confirm" @click="closeVoucherPopup"></view>
</view>
</view>
</uni-popup>
</z-paging>
</template>
<script>
import uQRCode from '@/js_sdk/Sansnn-uQRCode/uqrcode.js';
import { listCkstock, quickOutBill, stockPageList, cancelBill, auditBill, rejectBill, cancelAuditBill } from '@/api/property.js';
import { checkPermi } from '@/utils/auth/permission.js';
export default {
data() {
return {
checkPermi,
activeTab: 'form',
receiptForm: {
purpose: '',
applicant: ''
},
statusClassMap: {
'待审批': 'status-pending',
'已审批': 'status-approved',
'已拒绝': 'status-rejected',
'已取消': 'status-canceled',
'已核销': 'status-verified',
'__default__': 'status-pending'
},
materials: [],
selectedMaterials: [],
searchKeyword: '',
tempSelectedMaterials: [],
receiptRecords: [],
currentVoucher: null,
qrcodeSize: 200
};
},
onLoad() {
if (checkPermi('material_collection_audit')) {
this.activeTab = 'allList';
} else if (this.activeTab === 'list') {
this.refreshList();
}
},
onShow() {
// 页面显示时获取物资列表
this.getMaterialsList();
},
computed: {
filteredMaterials() {
if (!this.searchKeyword) {
return this.materials;
}
return this.materials.filter(item =>
item.cargoName.includes(this.searchKeyword) ||
item.cargoSpec.includes(this.searchKeyword)
);
}
},
methods: {
switchToList() {
this.activeTab = 'list';
this.$refs.paging.reload();
},
switchToAllList() {
this.activeTab = 'allList';
this.$refs.paging.reload();
},
// 获取物资列表
getMaterialsList() {
listCkstock().then(res => {
if (res.code === 200) {
this.materials = res.data;
}
});
},
// 显示物资选择器
showMaterialSelector() {
// 重置临时选择列表
this.tempSelectedMaterials = [...this.selectedMaterials];
this.searchKeyword = '';
// 打开弹窗
this.$refs.materialPopup.open();
},
// 关闭物资选择器
closeMaterialSelector() {
this.$refs.materialPopup.close();
},
// 切换物资选择状态
toggleMaterial(material) {
const index = this.tempSelectedMaterials.findIndex(item => item.id === material.id);
if (index > -1) {
this.tempSelectedMaterials.splice(index, 1);
} else {
// 添加数量字段
this.tempSelectedMaterials.push({ ...material, quantity: 1 });
}
},
// 检查物资是否已选择
isMaterialSelected(id) {
return this.tempSelectedMaterials.some(item => item.id === id);
},
// 获取物资数量
getMaterialQuantity(id) {
const item = this.tempSelectedMaterials.find(item => item.id === id);
return item ? item.quantity : 1;
},
// 增加数量
increaseQuantity(id) {
const item = this.tempSelectedMaterials.find(item => item.id === id);
if (item) {
item.quantity += 1;
}
},
// 减少数量
decreaseQuantity(id) {
const item = this.tempSelectedMaterials.find(item => item.id === id);
if (item && item.quantity > 1) {
item.quantity -= 1;
}
},
// 更新数量
updateQuantity(id, event) {
const item = this.tempSelectedMaterials.find(item => item.id === id);
if (item) {
const value = parseInt(event.target.value) || 1;
item.quantity = Math.max(1, value);
}
},
// 确认物资选择
confirmMaterialSelection() {
this.selectedMaterials = [...this.tempSelectedMaterials];
this.closeMaterialSelector();
},
// 移除已选物资
removeMaterial(index) {
this.selectedMaterials.splice(index, 1);
},
submitReceipt() {
if (this.selectedMaterials.length === 0 || !this.receiptForm.purpose) {
uni.showToast({
title: '请填写完整信息',
icon: 'none'
});
return;
}
// 准备提交数据
const items = this.selectedMaterials.map(item => ({
cargoId: item.cargoId,
quantity: item.quantity,
billNumber: item.billNumber,
stockId: item.stockId,
stockCode: item.stockCode,
ckCargoStockId: item.id,
}));
const submitData = {
purpose: this.receiptForm.purpose,
items: items
};
uni.showLoading({
title: '提交中...'
});
// 调用接口提交领用申请
quickOutBill(submitData).then(res => {
uni.hideLoading();
if (res.code === 200) {
uni.showToast({
title: '领用申请提交成功',
icon: 'success'
});
// 重置表单
setTimeout(() => {
this.receiptForm = {
purpose: '',
applicant: ''
};
this.selectedMaterials = [];
},
1500);
// 如果当前在列表页,刷新列表
if (this.activeTab === 'list') {
this.$refs.paging.reload();
}
} else {
uni.showToast({
title: res.message || '提交失败',
icon: 'none'
});
}
}).catch(() => {
uni.hideLoading();
});
},
// 获取状态文本
getStatusText(item) {
if (item.billStatus === '1') {
return '已核销';
}
if (item.cancelStatus === '1') {
return '已取消';
}
switch (item.auditStatus) {
case '0':
return '待审批';
case '1':
return '已审批';
case '2':
return '已拒绝';
default:
return '待审批';
}
},
// 获取状态样式
getStatusClass(item) {
const statusText = this.getStatusText(item);
return this.statusClassMap[statusText] || this.statusClassMap['__default__'];
},
// 审核通过
approveBill(id) {
uni.showModal({
title: '审核确认',
content: '确定要审核通过此单据吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
auditBill(id).then(response => {
if (response.code === 200) {
uni.showToast({
title: '审核通过成功',
icon: 'success'
});
this.$refs.paging.reload();
} else {
uni.showToast({
title: response.message || '审核通过失败',
icon: 'none'
});
}
});
}
}
});
},
// 驳回单据
handleRejectBill(id) {
uni.showModal({
title: '驳回确认',
content: '确定要驳回此单据吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
rejectBill(id).then(response => {
if (response.code === 200) {
uni.showToast({
title: '单据已驳回',
icon: 'success'
});
this.$refs.paging.reload();
} else {
uni.showToast({
title: response.message || '驳回失败',
icon: 'none'
});
}
});
}
}
});
},
// 撤销审核
cancelAudit(id) {
uni.showModal({
title: '撤销确认',
content: '确定要撤销此单据的审核吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
cancelAuditBill(id).then(response => {
if (response.code === 200) {
uni.showToast({
title: '撤销审核成功',
icon: 'success'
});
this.$refs.paging.reload();
} else {
uni.showToast({
title: response.message || '撤销审核失败',
icon: 'none'
});
}
});
}
}
});
},
// 取消单据
cancelBill(id) {
uni.showModal({
title: '取消单据',
content: '确定要取消此单据吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
cancelBill(id).then(response => {
if (response.code === 200) {
uni.showToast({
title: '单据取消成功',
icon: 'success'
});
// 刷新列表
this.$refs.paging.reload();
} else {
uni.showToast({
title: response.message || '取消失败',
icon: 'none'
});
}
});
}
}
});
},
// 查看凭证
viewVoucher(item) {
this.currentVoucher = item;
// 打开弹窗
this.$refs.voucherPopup.open();
// 生成二维码
this.generateQRCode(item.billNumber);
},
// 关闭凭证弹窗
closeVoucherPopup() {
this.$refs.voucherPopup.close();
},
// 生成二维码
generateQRCode(billNumber) {
console.log('开始生成二维码');
uQRCode.make({
canvasId: 'qrcode',
text: billNumber,
size: this.qrcodeSize,
margin: 10,
success: res => {
console.log(res);
},
complete: () => {
},
fail: res => {
console.log(res);
uni.showToast({
title: '二维码生成失败',
icon: 'none'
});
}
});
},
// 获取领用记录列表
queryList(pageNo, pageSize) {
const params = { page: pageNo, limit: pageSize, billType: 2 };
if (this.activeTab === 'allList') {
params.uid = '';
}
stockPageList(params).then(res => {
if (res.code === 200) {
const list = res.data.list;
// 将请求结果通过complete传给z-paging处理
this.$refs.paging.complete(list);
} else {
this.$refs.paging.complete([]);
}
}).catch(e => {
this.$refs.paging.complete(false);
});
},
refreshList() {
if (this.$refs.paging) {
this.$refs.paging.reload();
}
}
}
};
</script>
<style lang="scss">
.tabs {
padding: 0 30rpx;
display: flex;
margin-bottom: 15rpx;
background-color: #fff;
border-radius: 10rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
.tab {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
color: #666;
position: relative;
&.active {
color: #409EFF;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #409EFF;
border-radius: 2rpx;
}
}
}
}
.tab-content {
padding: 0 30rpx;
.form-card {
background-color: #fff;
border-radius: 10rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.08);
.form-item {
margin-bottom: 30rpx;
.label {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 16rpx;
font-weight: 600;
}
.input {
width: 100%;
height: 72rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 0 20rpx;
font-size: 24rpx;
box-sizing: border-box;
}
.textarea {
width: 100%;
min-height: 160rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 20rpx;
font-size: 24rpx;
box-sizing: border-box;
line-height: 1.6;
}
.count {
display: block;
margin-top: 10rpx;
text-align: right;
font-size: 20rpx;
color: #999;
}
.multi-select {
.selected-items {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-bottom: 12rpx;
.selected-item {
display: flex;
align-items: center;
background-color: #f0f9ff;
border: 1rpx solid #e6f7ff;
border-radius: 16rpx;
padding: 8rpx 16rpx;
font-size: 22rpx;
color: #409EFF;
.remove-btn {
margin-left: 8rpx;
font-size: 24rpx;
font-weight: bold;
cursor: pointer;
}
}
}
.select-btn {
width: 100%;
height: 72rpx;
border-radius: 10rpx;
border: 1rpx solid #e5e5e5;
padding: 0 20rpx;
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
box-sizing: border-box;
cursor: pointer;
}
}
}
}
.submit-section {
margin-top: 40rpx;
margin-bottom: 30rpx;
.submit-btn {
height: 88rpx;
background-color: #409EFF;
color: #fff;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(64, 158, 255, 0.35);
}
}
.record-card {
background-color: #fff;
border-radius: 10rpx;
padding: 24rpx 26rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.06);
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
.left {
display: flex;
align-items: center;
.type {
font-size: 26rpx;
font-weight: 600;
color: #333;
margin-right: 16rpx;
}
.status {
padding: 6rpx 18rpx;
border-radius: 20rpx;
font-size: 20rpx;
}
.status-pending {
background-color: #E6F7FF;
color: #409EFF;
}
.status-approved {
background-color: #F6FFED;
color: #52C41A;
}
.status-rejected {
background-color: #FFF1F0;
color: #FF4D4F;
}
.status-canceled {
background-color: #F5F5F5;
color: #999;
}
.status-verified {
background-color: #E8F5E8;
color: #4CAF50;
}
}
.header-right {
display: flex;
align-items: center;
flex-direction: column;
align-items: flex-end;
.time {
font-size: 22rpx;
color: #999;
margin-bottom: 8rpx;
}
.bill-number {
font-size: 20rpx;
color: #666;
}
}
}
.card-body {
.row {
display: flex;
margin-bottom: 10rpx;
.label {
width: 140rpx;
font-size: 24rpx;
color: #666;
}
.value {
flex: 1;
font-size: 24rpx;
color: #333;
word-break: break-word;
}
}
}
.card-footer {
margin-top: 16rpx;
display: flex;
align-items: center;
justify-content: flex-end;
.action-buttons {
display: flex;
gap: 16rpx;
.action-btn {
padding: 8rpx 24rpx;
border-radius: 16rpx;
font-size: 22rpx;
cursor: pointer;
}
.cancel-btn {
background-color: #F5F5F5;
color: #666;
}
.voucher-btn {
background-color: #409EFF;
color: #fff;
}
.approve-btn {
background-color: #52C41A;
color: #fff;
}
.reject-btn {
background-color: #FF4D4F;
color: #fff;
}
}
}
}
}
/* 弹窗样式 */
.popup-content {
background-color: #fff;
border-top-left-radius: 20rpx;
border-top-right-radius: 20rpx;
padding-bottom: 30rpx;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.popup-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.popup-close {
font-size: 36rpx;
color: #999;
cursor: pointer;
}
}
.popup-body {
max-height: 60vh;
overflow-y: auto;
.search-box {
padding: 20rpx 30rpx;
.search-input {
width: 100%;
height: 64rpx;
border-radius: 32rpx;
border: 1rpx solid #e5e5e5;
padding: 0 24rpx;
font-size: 24rpx;
box-sizing: border-box;
background-color: #f9f9f9;
}
}
.material-list {
.material-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.material-info {
flex: 1;
.material-name {
display: block;
font-size: 26rpx;
color: #333;
margin-bottom: 8rpx;
}
.material-spec {
font-size: 22rpx;
color: #999;
}
}
.item-actions {
display: flex;
align-items: center;
gap: 20rpx;
.quantity-input {
display: flex;
align-items: center;
border: 1rpx solid #e5e5e5;
border-radius: 8rpx;
.quantity-btn {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #666;
background-color: #f5f5f5;
}
.quantity-input-box {
width: 80rpx;
height: 40rpx;
border: none;
text-align: center;
font-size: 22rpx;
color: #333;
}
}
.checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #e5e5e5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #fff;
cursor: pointer;
&.checked {
background-color: #409EFF;
border-color: #409EFF;
}
}
}
}
}
}
.popup-footer {
display: flex;
padding: 20rpx 30rpx 0;
gap: 20rpx;
.footer-btn {
flex: 1;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
cursor: pointer;
}
.cancel {
background-color: #f5f5f5;
color: #666;
}
.confirm {
background-color: #409EFF;
color: #fff;
}
}
}
/* 凭证弹窗样式 */
.voucher-popup {
width: 680rpx;
background-color: #fff;
border-radius: 20rpx;
overflow: hidden;
.voucher-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
.voucher-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.voucher-close {
font-size: 36rpx;
color: #999;
cursor: pointer;
}
}
.voucher-body {
padding: 40rpx 30rpx;
text-align: center;
.voucher-info {
margin-bottom: 30rpx;
.voucher-label {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 12rpx;
}
.voucher-value {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.qrcode-container {
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
}
}
.voucher-footer {
padding: 0 30rpx 30rpx;
.footer-btn {
width: 100%;
height: 72rpx;
border-radius: 36rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 600;
cursor: pointer;
}
.confirm {
background-color: #409EFF;
color: #fff;
}
}
}
</style>