Files
art-design-uniapp/pages/approval/borrow_approval_detils.vue
T
2026-02-12 09:39:41 +08:00

581 lines
14 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="page">
<uni-nav-bar
leftText=" "
:fixed="true"
leftIcon="back"
background-color="rgb(48, 116, 247)"
color="#fff"
:statusBar="true"
title="借用审核"
@clickLeft="onBack"
/>
<view class="content">
<!-- 基本信息 -->
<view class="card">
<view class="card-head">
<text class="card-title">借用信息</text>
<view class="status-tag" :class="'s' + (detail.status || 0)">
<text class="status-text">{{ statusText }}</text>
</view>
</view>
<view class="row"><text class="label">单号</text><text class="value">{{ detail.order_no || '-' }}</text></view>
<view class="row"><text class="label">申请人</text><text class="value">{{ detail.applicant_name || '-' }}</text></view>
<view class="row">
<text class="label">借用时间</text>
<text class="value">{{ detail.use_start_date || '-' }} ~ {{ detail.use_end_date || '-' }}</text>
</view>
<view class="row"><text class="label">借用事由</text><text class="value">{{ detail.borrow_reason || '-' }}</text></view>
<view class="row" v-if="detail.remark !== undefined">
<text class="label">备注</text>
<text class="value">{{ detail.remark || '-' }}</text>
</view>
</view>
<!-- 物品明细 -->
<view class="card">
<view class="card-head">
<text class="card-title">物品明细</text>
</view>
<view v-if="approveItems.length === 0" class="empty">暂无物品</view>
<view v-for="(it, idx) in approveItems" :key="it.requisition_item_id" class="item">
<view class="item-left">
<text class="item-name">{{ it.requisition_item_name }}</text>
<text class="item-meta" v-if="it.spec_model_snap">规格:{{ it.spec_model_snap }}</text>
</view>
<view class="item-right">
<text class="qty-label">申请数量</text>
<text class="qty-val">{{ it.request_qty }}</text>
</view>
</view>
</view>
<!-- 申请附件 -->
<view class="card" v-if="(detail.attachment || []).length">
<view class="card-head">
<text class="card-title">申请附件</text>
</view>
<view class="img-grid">
<image
v-for="(a, i) in detail.attachment"
:key="a.id || i"
class="img"
:src="a.file_path_pull || ''"
mode="aspectFill"
@click="previewAttachment(i)"
/>
</view>
</view>
<!-- 审核意见 -->
<view class="card">
<view class="card-head">
<text class="card-title">审核意见</text>
</view>
<textarea
class="textarea"
v-model="auditRemark"
placeholder="请输入审核备注(可选)"
:auto-height="true"
maxlength="500"
/>
</view>
<!-- 审批附件 -->
<view class="card">
<view class="card-head">
<text class="card-title">审批附件</text>
</view>
<view class="media-upload-wrapper">
<uni-file-picker
ref="mediaPicker"
:limit="maxMedia"
fileMediatype="image"
:auto-upload="false"
@select="selectMedia"
@delete="delfn"
>
<view class="upload-icon-wrapper">
<image src="/static/image/upload.png" mode="" class="addimg"></image>
</view>
</uni-file-picker>
</view>
</view>
<view style="height: 120rpx;"></view>
</view>
<!-- 底部操作 -->
<view class="footer">
<button class="btn btn-ghost" @click="onReject">驳回</button>
<button class="btn btn-primary" @click="onApprove">同意</button>
</view>
</view>
</template>
<script>
import { request, showToast } from '@/api/apiT.js'
// ⚠️ 你需要把这里改成真实的“审核提交接口”
// 如果你后端有具体字段/接口名,发我,我可以帮你对齐成完全可用。
const AUDIT_API = 'admin/inv/requisition/approve'
export default {
data() {
return {
id: '',
detail: {},
approveItems: [],
auditRemark: '',
maxMedia: 20,
// 审批附件列表
auditAttachment: []
}
},
computed: {
statusText() {
// 你返回里 status=10;这里做个兼容显示
const s = Number(this.detail.status || 0)
if (this.detail.is_returned === 1) return '已归还'
if (s === 10) return '待审核'
if (s === 11) return '审批中'
if (s === 12) return '待归还'
if (s === 13) return '借用拒绝'
if (s === 14) return '归还待审核'
if (s === 15) return '归还审批中'
if (s === 16) return '部分归还'
if (s === 17) return '已归还'
if (s === 18) return '归还拒绝'
return '处理中'
}
},
onLoad(option) {
this.id = option.id || option.requisition_id || option.requisitionId || ''
if (!this.id) {
showToast({ title: '缺少id参数' })
return
}
this.getDetail()
},
methods: {
onBack() {
uni.navigateBack()
},
getDetail() {
request({
url: 'admin/inv/requisition/show',
method: 'GET',
data: { id: this.id }
}).then((res) => {
this.detail = (res.data && res.data.data) || {}
this.initApproveItems()
})
},
initApproveItems() {
const list = Array.isArray(this.detail.item) ? this.detail.item : []
this.approveItems = list.map((it) => {
// approved_qty 可能后端已经给了;没有就默认=申请数量
const req = Number(it.request_qty || 0)
const approved = it.approved_qty === undefined || it.approved_qty === null ? req : Number(it.approved_qty || 0)
return {
requisition_item_id: it.id,
requisition_item_name: it.item_name_snap || '',
spec_model_snap: it.spec_model_snap || '',
unit_snap: it.unit_snap || '',
request_qty: req,
returned_qty: Number(it.returned_qty || 0),
approved_qty: this.clamp(approved, 0, req)
}
})
},
clamp(v, min, max) {
const n = Number(v || 0)
if (n < min) return min
if (n > max) return max
return n
},
onApprovedQtyChange(idx, val) {
const it = this.approveItems[idx]
if (!it) return
it.approved_qty = this.clamp(val, 0, it.request_qty)
// 触发视图更新
this.$set(this.approveItems, idx, it)
},
previewAttachment(startIndex) {
const urls = (this.detail.attachment || [])
.map((a) => a.file_path_pull)
.filter(Boolean)
if (!urls.length) return
uni.previewImage({
current: urls[startIndex] || urls[0],
urls
})
},
// 上传图片
uploadfn(imgUrl) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: 'https://fdesk.hnxinyouhe.com/upload?scene=itemsimg',
filePath: imgUrl,
name: 'file',
header: {
'Fdesk-Corp-Abbr': uni.getStorageSync('corp_abbr'),
'Fdesk-Auth-Token': 'Bearer ' + uni.getStorageSync('token'),
'Fdesk-Language': uni.getStorageSync('i18locale') ? uni.getStorageSync('i18locale') : 'zh',
'Fdesk-Login-Type': 'feishu',
'Fdesk-System-Code': 'fdesk'
},
success: (uploadFileRes) => {
const res = JSON.parse(uploadFileRes.data || '{}')
if (res && res.data && res.data.file_name) {
resolve(res.data)
} else {
reject('上传失败')
}
},
fail: (err) => reject(err)
})
})
},
// 选择图片
selectMedia(e) {
this.auditAttachment = Array.isArray(this.auditAttachment) ? this.auditAttachment : []
const allowCount = this.maxMedia - this.auditAttachment.length
const tempFilePaths = e.tempFilePaths || (e.tempFiles || []).map(item => item.path || item.url)
const paths = tempFilePaths.slice(0, allowCount)
if (paths.length === 0) return
const startIndex = this.auditAttachment.length
// 先占位
paths.forEach(p => {
this.auditAttachment.push({
__local_path: p,
original_name: '',
file_path: ''
})
})
Promise.all(
paths.map((p, i) =>
this.uploadfn(p)
.then(data => ({ i, ok: true, data }))
.catch(err => ({ i, ok: false, err }))
)
).then(results => {
// 写入成功结果
results.forEach(r => {
if (!r.ok) return
const idx = startIndex + r.i
const row = this.auditAttachment[idx]
if (!row) return
row.original_name = r.data.original_name
row.file_path = r.data.file_name
delete row.__local_path
})
// 移除失败项
const failed = results.filter(r => !r.ok).map(r => r.i).sort((a, b) => b - a)
if (failed.length) {
failed.forEach(i => {
const idx = startIndex + i
this.auditAttachment.splice(idx, 1)
this.$nextTick(() => {
if (this.$refs.mediaPicker && this.$refs.mediaPicker.clearFiles) {
this.$refs.mediaPicker.clearFiles(idx)
}
})
})
uni.showToast({
title: '部分图片上传失败,已自动移除,请重试',
icon: 'none'
})
}
})
},
// 删除图片
delfn(e) {
this.auditAttachment.splice(e.index, 1)
},
buildPayload(action) {
return {
requisition_id: this.detail.id || this.id,
action, // 'approve' | 'reject'
approve_id: this.detail.approve_id,
comment: this.auditRemark || '',
attachment: this.auditAttachment
// items: this.approveItems.map((it) => ({
// requisition_item_id: it.requisition_item_id,
// requisition_item_name: it.requisition_item_name,
// approved_qty: it.approved_qty
// }))
}
},
doSubmit(action) {
if (!this.approveItems.length) {
showToast({ title: '没有可审核的物品' })
return
}
// 检查附件是否上传完成
if (Array.isArray(this.auditAttachment)) {
const uploading = this.auditAttachment.some(a => !a || !a.file_path)
if (uploading) {
showToast({ title: '附件上传中,请稍后再提交' })
return
}
}
const payload = this.buildPayload(action)
request({
url: AUDIT_API,
method: 'POST',
data: payload
}).then((res) => {
const msg = (res.data && (res.data.data.message || res.data.data.msg)) || '操作完成'
showToast({ title: msg })
if (res.data && Number(res.data.code) === 200) {
setTimeout(() => {
// 回到上一页并传递刷新参数
uni.navigateBack({
delta: 1,
success: function() {
// 触发上一页的刷新
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage && prevPage.$refs.paging) {
prevPage.$refs.paging.reload()
}
}
})
}, 600)
}
})
},
onApprove() {
uni.showModal({
title: '确认通过',
content: '确认同意该借用申请?',
success: (r) => {
if (r.confirm) this.doSubmit('1')
}
})
},
onReject() {
uni.showModal({
title: '确认驳回',
content: '确认驳回该借用申请?',
success: (r) => {
if (r.confirm) this.doSubmit('2')
}
})
}
}
}
</script>
<style lang="scss">
.page {
min-height: 100vh;
background: rgb(244, 246, 248);
}
.content {
padding: 20rpx;
}
.card {
background: #fff;
border-radius: 20rpx;
padding: 22rpx;
margin-bottom: 18rpx;
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.04);
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 700;
color: #1f2d3d;
}
.card-sub {
font-size: 24rpx;
color: #98a2b3;
}
.row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 10rpx 0;
border-bottom: 1rpx solid #f1f3f5;
}
.row:last-child {
border-bottom: none;
}
.label {
width: 160rpx;
font-size: 26rpx;
color: #667085;
}
.value {
flex: 1;
text-align: right;
font-size: 26rpx;
color: #101828;
}
.status-tag {
padding: 6rpx 14rpx;
border-radius: 999rpx;
background: rgba(48, 116, 247, 0.12);
}
.status-text {
font-size: 24rpx;
color: rgb(48, 116, 247);
}
.empty {
padding: 24rpx 0;
text-align: center;
color: #98a2b3;
font-size: 26rpx;
}
.item {
padding: 16rpx 0;
border-top: 1rpx dashed #eef2f6;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.item-left {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6rpx;
}
.item-right {
display: flex;
align-items: baseline;
gap: 10rpx;
flex-shrink: 0;
}
.qty-label {
font-size: 22rpx;
color: #667085;
}
.qty-val {
font-size: 30rpx;
font-weight: 700;
color: #101828;
}
.item:first-child {
border-top: none;
padding-top: 0;
}
.item-top {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 12rpx;
}
.item-name {
font-size: 28rpx;
font-weight: 700;
color: #101828;
}
.item-meta {
font-size: 24rpx;
color: #98a2b3;
}
.grid {
display: flex;
gap: 16rpx;
}
.cell {
flex: 1;
background: #f8fafc;
border-radius: 16rpx;
padding: 14rpx;
}
.k {
display: block;
font-size: 22rpx;
color: #667085;
margin-bottom: 8rpx;
}
.v {
font-size: 26rpx;
color: #101828;
}
.hint {
margin-top: 10rpx;
font-size: 24rpx;
color: #667085;
}
.img-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.img {
width: 210rpx;
height: 210rpx;
border-radius: 16rpx;
background: #f2f4f7;
}
.textarea {
width: 100%;
min-height: 140rpx;
font-size: 26rpx;
line-height: 1.6;
background: #f8fafc;
border-radius: 16rpx;
padding: 16rpx;
box-sizing: border-box;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 16rpx 20rpx calc(16rpx + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.98);
border-top: 1rpx solid #eef2f6;
display: flex;
gap: 16rpx;
}
.btn {
flex: 1;
height: 84rpx;
line-height: 84rpx;
border-radius: 18rpx;
font-size: 30rpx;
font-weight: 700;
}
.btn-ghost {
background: #fff;
border: 2rpx solid #d0d5dd;
color: #344054;
}
.btn-primary {
background: rgb(48, 116, 247);
color: #fff;
}
.media-upload-wrapper {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.addimg {
width: 100rpx;
height: 100rpx;
}
::v-deep .uni-file-picker {
width: 100%;
}
</style>