Files
oa_app/pages/todo/detail.vue
T
2026-09-23 16:52:48 +08:00

175 lines
7.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">
<text class="who">{{ log.who || log.actor || log.userName }} · {{ 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 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">
<view class="btn no" @click="audit('reject')">驳回</view>
<view class="btn-primary yes" @click="audit('approve')">通过</view>
</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; padding-bottom: 80rpx; }
.card { padding: 28rpx; margin-bottom: 16rpx; }
.block { padding: 24rpx 28rpx; }
.h { display: block; font-size: 26rpx; font-weight: 700; margin-bottom: 12rpx; }
.t { display: block; font-size: 34rpx; font-weight: 700; }
.s { display: block; font-size: 24rpx; color: #737686; margin-top: 8rpx; }
.amt { display: block; margin-top: 12rpx; color: #004ac6; font-weight: 700; font-size: 32rpx; }
.kv { display: flex; padding: 10rpx 0; border-bottom: 1rpx solid #eaedff; }
.k { width: 180rpx; color: #737686; font-size: 24rpx; }
.v { flex: 1; font-size: 26rpx; }
.inv { padding: 12rpx 0; border-bottom: 1rpx solid #eaedff; }
.tn { display: block; font-weight: 600; font-size: 26rpx; }
.log { padding: 12rpx 0; border-bottom: 1rpx solid #eaedff; }
.who { display: block; font-size: 26rpx; font-weight: 600; }
.field { margin: 24rpx 0; }
.lab { font-size: 24rpx; color: #434655; }
.box { background: #fff; border-radius: 16rpx; padding: 20rpx; min-height: 140rpx; }
.btns { display: flex; gap: 16rpx; }
.btn { flex: 1; height: 88rpx; border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
.no { background: #ffdad6; color: #ba1a1a; }
.yes { flex: 1; }
.empty { padding: 80rpx; text-align: center; color: #737686; }
</style>