195 lines
8.0 KiB
Vue
195 lines
8.0 KiB
Vue
<template>
|
|
<view class="page">
|
|
<fy-header title="审批详情" back />
|
|
<view v-if="row" class="body">
|
|
<view class="card">
|
|
<text class="t">{{ row.title || row.no }}</text>
|
|
<text class="s">{{ row.typeLabel || row.type }} · {{ row.submitter || '' }}</text>
|
|
<text class="s">单号 {{ row.no }}</text>
|
|
<text v-if="row.node" class="s">当前节点 {{ row.node }}</text>
|
|
<text v-if="row.amount != null && row.amount !== ''" class="amt">¥ {{ row.amount }}</text>
|
|
</view>
|
|
|
|
<view v-if="fields.length" class="card block">
|
|
<text class="h">申请内容</text>
|
|
<view v-for="f in fields" :key="f.label" class="kv">
|
|
<text class="k">{{ f.label }}</text>
|
|
<text class="v">{{ f.value }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="invoices.length" class="card block">
|
|
<text class="h">发票明细 · {{ invoices.length }} 张</text>
|
|
<view v-for="(inv, i) in invoices" :key="inv.seq || i" class="inv">
|
|
<text class="tn">发票 {{ inv.seq || i + 1 }} · {{ inv.ticketType || '未选票种' }}</text>
|
|
<text class="s">金额 ¥{{ inv.amount || 0 }} {{ inv.invoiceNo ? '· 票号 ' + inv.invoiceNo : '' }} {{ inv.date || '' }}</text>
|
|
<text v-if="inv.note" class="s">{{ inv.note }}</text>
|
|
<fy-files :files="inv.files || []" label="" />
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="files.length" class="card block">
|
|
<fy-files :files="files" label="附件(点击预览)" />
|
|
</view>
|
|
|
|
<view v-if="logs.length" class="card block">
|
|
<text class="h">审批流程</text>
|
|
<view v-for="(log, i) in logs" :key="i" class="log">
|
|
<view class="dot" :class="{ last: i === logs.length - 1 }" />
|
|
<view class="logb">
|
|
<text class="who">{{ log.who || log.actor || log.userName }}</text>
|
|
<text class="act">{{ log.action }}</text>
|
|
<text v-if="log.comment" class="s">{{ log.comment }}</text>
|
|
<text class="s">{{ log.label || log.time || log.at || '' }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="canAct" class="acts">
|
|
<view class="field">
|
|
<text class="lab">审批意见</text>
|
|
<view class="box"><textarea v-model="comment" placeholder="选填" /></view>
|
|
</view>
|
|
<view class="btns">
|
|
<wd-button plain type="error" @click="audit('reject')">驳回</wd-button>
|
|
<wd-button type="primary" custom-class="yes" @click="audit('approve')">通过</wd-button>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-else class="empty">{{ err || '加载中…' }}</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import FyHeader from '../../components/fy-header.vue'
|
|
import FyFiles from '../../components/fy-files.vue'
|
|
import { store } from '../../utils/store.js'
|
|
import { oaGet } from '../../utils/api.js'
|
|
import { toast } from '../../utils/format.js'
|
|
import { collectFiles, fileId } from '../../utils/files.js'
|
|
|
|
const LABELS = {
|
|
reason: '事由', remark: '说明', project: '关联项目', expenseType: '费用类型',
|
|
receipts: '票据张数', city: '出差地点', leaveType: '假期类型', days: '时长',
|
|
targetName: '对象', lastDay: '最后工作日', proxyPayee: '代付收款人',
|
|
offsetLoanNo: '冲账借支', goods: '采购/领用内容', supplier: '意向供应商',
|
|
needDate: '期望到货', sealCompany: '用章主体', sealType: '申请印章',
|
|
amount: '金额', costType: '费用归属', start: '开始', end: '结束',
|
|
hours: '加班时长', date: '日期', place: '地点', punchDate: '补卡日期',
|
|
punchKind: '补卡类型', fromDept: '原部门', toDept: '拟调入部门',
|
|
newTitle: '拟任岗位', currentStatus: '当前身份', hiredAt: '入职日期',
|
|
summary: '工作总结', nextPlan: '转正后计划', qty: '数量'
|
|
}
|
|
|
|
export default {
|
|
components: { FyHeader, FyFiles },
|
|
data() {
|
|
return { no: '', row: null, comment: '', err: '' }
|
|
},
|
|
computed: {
|
|
form() {
|
|
const r = this.row || {}
|
|
return r.form && typeof r.form === 'object' ? { ...r, ...r.form } : r
|
|
},
|
|
fields() {
|
|
const f = this.form
|
|
const skip = new Set(['invoices', 'files', 'attachments', 'invoiceFiles', 'receiptFiles', 'title', 'type', 'typeLabel', 'no', 'status', 'submitter', 'node', 'logs', 'approverUsername', 'id'])
|
|
const out = []
|
|
Object.keys(f).forEach((key) => {
|
|
if (skip.has(key)) return
|
|
const v = f[key]
|
|
if (v == null || v === '' || typeof v === 'object') return
|
|
out.push({ label: LABELS[key] || key, value: String(v) })
|
|
})
|
|
return out
|
|
},
|
|
invoices() {
|
|
const f = this.form
|
|
return Array.isArray(f.invoices) ? f.invoices : []
|
|
},
|
|
files() {
|
|
const all = collectFiles(this.row)
|
|
const used = new Set()
|
|
this.invoices.forEach((inv) => (inv.files || []).forEach((f) => {
|
|
const id = fileId(f)
|
|
if (id) used.add(id)
|
|
}))
|
|
return all.filter((f) => !used.has(fileId(f)))
|
|
},
|
|
logs() {
|
|
const logs = (this.row && this.row.logs) || []
|
|
return Array.isArray(logs) ? [...logs].reverse() : []
|
|
},
|
|
canAct() {
|
|
if (!this.row) return false
|
|
const st = String(this.row.status || '')
|
|
const pending = !st || st === 'pending' || st === '待审批' || this.row.canAudit === true
|
|
return store.can('personal', 'approve') && pending
|
|
}
|
|
},
|
|
onLoad(q) {
|
|
this.no = decodeURIComponent(q.no || '')
|
|
this.load()
|
|
},
|
|
methods: {
|
|
async load() {
|
|
try {
|
|
this.row = await oaGet('/workflow/requests/' + encodeURIComponent(this.no), store.token)
|
|
} catch (e) {
|
|
this.err = e.message || '加载失败'
|
|
}
|
|
},
|
|
async audit(action) {
|
|
try {
|
|
await store.audit(this.no, action, this.comment)
|
|
toast(action === 'approve' ? '已通过' : '已驳回', 'success')
|
|
setTimeout(() => uni.navigateBack(), 500)
|
|
} catch (e) {
|
|
toast(e.message || '操作失败')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.body { padding: 24rpx 28rpx 200rpx; }
|
|
.card { padding: 28rpx; margin-bottom: 16rpx; }
|
|
.block { padding: 24rpx 28rpx; }
|
|
.h { display: block; font-size: 30rpx; font-weight: 650; margin-bottom: 16rpx; }
|
|
.t { display: block; font-size: 34rpx; font-weight: 650; line-height: 1.4; }
|
|
.s { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
|
.amt { display: block; margin-top: 16rpx; color: #1677FF; font-weight: 650; font-size: 36rpx; }
|
|
.kv { display: flex; padding: 16rpx 0; }
|
|
.kv + .kv { border-top: 1rpx solid #F0F2F5; }
|
|
.k { width: 180rpx; color: #6B7280; font-size: 26rpx; }
|
|
.v { flex: 1; font-size: 28rpx; text-align: right; }
|
|
.inv { padding: 16rpx 0; border-bottom: 1rpx solid #F0F2F5; }
|
|
.tn { display: block; font-weight: 600; font-size: 26rpx; }
|
|
.log { display: flex; position: relative; padding: 0 0 28rpx 8rpx; }
|
|
.dot {
|
|
width: 16rpx; height: 16rpx; border-radius: 50%; background: #1677FF; margin-top: 10rpx; flex-shrink: 0;
|
|
box-shadow: 0 0 0 6rpx #E8F1FF;
|
|
}
|
|
.dot::after {
|
|
content: ''; position: absolute; left: 14rpx; top: 36rpx; bottom: 0; width: 2rpx; background: #E8ECF2;
|
|
}
|
|
.log:last-child .dot::after { display: none; }
|
|
.logb { margin-left: 20rpx; flex: 1; }
|
|
.who { display: block; font-size: 28rpx; font-weight: 600; }
|
|
.act { display: block; font-size: 24rpx; color: #1677FF; margin-top: 4rpx; }
|
|
.field { margin: 8rpx 0 20rpx; }
|
|
.lab { font-size: 26rpx; color: #4B5563; }
|
|
.box { background: #F5F7FA; border-radius: 16rpx; padding: 20rpx; min-height: 140rpx; margin-top: 10rpx; }
|
|
.acts {
|
|
position: fixed; left: 0; right: 0; bottom: 0; background: #fff;
|
|
border-top: 1rpx solid #E8ECF2; padding: 16rpx 28rpx calc(16rpx + env(safe-area-inset-bottom));
|
|
}
|
|
.btns { display: flex; gap: 16rpx; }
|
|
.btns :deep(.wd-button) { flex: 1; }
|
|
.btn { flex: 1; height: 88rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; font-size: 30rpx; font-weight: 600; }
|
|
.no { background: #fff; color: #DC2626; border: 1rpx solid #FECACA; }
|
|
.yes { flex: 1.4; }
|
|
.empty { padding: 80rpx; text-align: center; color: #6B7280; }
|
|
</style>
|