Files
art-design-uniapp/pages/approval/borrow_approval_detils.vue
T

581 lines
14 KiB
Vue
Raw Normal View History

2026-02-06 14:39:37 +08:00
<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>
2026-02-12 09:39:41 +08:00
<!-- 审批附件 -->
<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>
2026-02-06 14:39:37 +08:00
<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: [],
2026-02-12 09:39:41 +08:00
auditRemark: '',
maxMedia: 20,
// 审批附件列表
auditAttachment: []
2026-02-06 14:39:37 +08:00
}
},
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
})
},
2026-02-12 09:39:41 +08:00
// 上传图片
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)
},
2026-02-06 14:39:37 +08:00
buildPayload(action) {
return {
requisition_id: this.detail.id || this.id,
action, // 'approve' | 'reject'
2026-02-12 09:39:41 +08:00
approve_id: this.detail.approve_id,
comment: this.auditRemark || '',
attachment: this.auditAttachment
2026-02-06 14:39:37 +08:00
// 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
}
2026-02-12 09:39:41 +08:00
// 检查附件是否上传完成
if (Array.isArray(this.auditAttachment)) {
const uploading = this.auditAttachment.some(a => !a || !a.file_path)
if (uploading) {
showToast({ title: '附件上传中,请稍后再提交' })
return
}
}
2026-02-06 14:39:37 +08:00
const payload = this.buildPayload(action)
request({
url: AUDIT_API,
method: 'POST',
data: payload
}).then((res) => {
2026-02-12 09:39:41 +08:00
const msg = (res.data && (res.data.data.message || res.data.data.msg)) || '操作完成'
2026-02-06 14:39:37 +08:00
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;
}
2026-02-12 09:39:41 +08:00
.media-upload-wrapper {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.addimg {
width: 100rpx;
height: 100rpx;
}
::v-deep .uni-file-picker {
width: 100%;
}
2026-02-06 14:39:37 +08:00
</style>