Files
oa_app/pages/apply/form.vue
T
2026-09-23 17:47:57 +08:00

178 lines
7.7 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">
<fy-header :title="spec ? spec.title : '申请'" back />
<view v-if="spec" class="body">
<view class="field">
<text class="lab">标题</text>
<view class="box"><input v-model="title" /></view>
</view>
<view v-if="spec.multiInvoice">
<view v-for="(inv, i) in invoices" :key="i" class="card inv">
<text class="lab">发票 {{ i + 1 }}</text>
<picker :range="ticketTypes" @change="(e) => inv.ticketType = ticketTypes[e.detail.value]">
<view class="box">{{ inv.ticketType || '选择票种' }}</view>
</picker>
<view class="box"><input v-model="inv.amount" type="digit" placeholder="金额" /></view>
<view class="box"><input v-model="inv.invoiceNo" placeholder="发票号(选填)" /></view>
<picker mode="date" @change="(e) => inv.date = e.detail.value">
<view class="box">{{ inv.date || '发票日期(选填)' }}</view>
</picker>
<view class="box"><input v-model="inv.note" placeholder="备注" /></view>
<fy-files :files="inv.files" label="发票附件" editable kind="expense" @update:files="(rows) => inv.files = rows" @remove="(idx) => inv.files.splice(idx, 1)" />
</view>
<text class="link" @click="invoices.push({ ticketType: '', amount: '', invoiceNo: '', date: '', note: '', files: [], proxyPay: false })"> 添加发票</text>
<view class="field">
<text class="lab">报销说明</text>
<view class="box tall"><textarea v-model="remark" /></view>
</view>
</view>
<view v-for="f in spec.fields || []" :key="f.key" class="field">
<text class="lab">{{ f.label }}</text>
<picker v-if="f.type === 'select'" :range="optionsOf(f)" @change="(e) => values[f.key] = optionsOf(f)[e.detail.value]">
<view class="box">{{ values[f.key] || '请选择' }}</view>
</picker>
<picker v-else-if="f.type === 'date'" mode="date" @change="(e) => values[f.key] = e.detail.value">
<view class="box">{{ values[f.key] || '请选择日期' }}</view>
</picker>
<picker v-else-if="f.type === 'datetime'" mode="date" @change="(e) => values[f.key] = e.detail.value">
<view class="box">{{ values[f.key] || '请选择日期' }}</view>
</picker>
<view v-else-if="f.type === 'textarea'" class="box tall"><textarea v-model="values[f.key]" :placeholder="f.placeholder" /></view>
<view v-else class="box"><input v-model="values[f.key]" :placeholder="f.placeholder" :type="f.type === 'money' || f.type === 'number' ? 'digit' : 'text'" /></view>
</view>
<fy-files v-if="!spec.multiInvoice" :files="attachments" label="附件" editable :kind="kind" @update:files="(rows) => attachments = rows" @remove="(idx) => attachments.splice(idx, 1)" />
<view v-if="!fixed" class="field">
<text class="lab">审批人</text>
<picker :range="approverNames" @change="pickApprover">
<view class="box">{{ approverLabel || '请选择审批人' }}</view>
</picker>
</view>
<view v-else class="field">
<text class="lab">审批链</text>
<text class="hint">{{ suggestedLabel || '将按固定审批链流转' }}</text>
</view>
<wd-button type="primary" block size="large" @click="submit">提交申请</wd-button>
</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 { applyKindById, buildApplyPayload, isFixedChain } from '../../utils/apply.js'
import { asList, toast } from '../../utils/format.js'
import { oaGet } from '../../utils/api.js'
export default {
components: { FyHeader, FyFiles },
data() {
return {
kind: 'leave',
title: '',
values: {},
invoices: [{ ticketType: '', amount: '', invoiceNo: '', date: '', note: '', files: [], proxyPay: false }],
attachments: [],
remark: '',
approvers: [],
suggested: '',
suggestedName: '',
approver: '',
ticketTypes: ['机票', '高铁/火车', '出租车', '住宿', '餐饮招待', '办公用品', '业务招待', '过路费/油费', '其他'],
peopleOpts: []
}
},
computed: {
spec() { return applyKindById(this.kind) },
fixed() { return isFixedChain(this.kind) },
approverNames() { return this.approvers.map((a) => a.realName || a.name || a.username) },
approverLabel() {
const a = this.approvers.find((x) => x.username === this.approver)
return a ? (a.realName || a.name || a.username) : ''
},
suggestedLabel() { return this.suggestedName }
},
onLoad(q) {
this.kind = q.kind || 'leave'
const spec = this.spec
const me = store.me
if (me && spec) this.title = me.name + ' ' + spec.title
this.boot()
},
methods: {
optionsOf(f) {
if (f.key === 'targetUsername') return this.peopleOpts.map((p) => p.name)
return f.options || []
},
async boot() {
try {
const preview = await oaGet('/workflow/chain-preview?type=' + encodeURIComponent(this.kind), store.token)
this.approvers = asList(preview && preview.approvers)
const chain = asList(preview && preview.suggested)
if (chain[0]) {
this.suggested = chain[0].username || ''
this.suggestedName = chain[0].realName || chain[0].name || this.suggested
}
if (!this.fixed && !this.approver) this.approver = this.suggested || (this.approvers[0] && this.approvers[0].username) || ''
} catch (_) {}
if (this.kind === 'layoff') {
this.peopleOpts = Object.values(store.people)
}
},
pickApprover(e) {
const a = this.approvers[e.detail.value]
this.approver = a ? a.username : ''
},
async submit() {
if (!this.spec) return
if (!store.can('personal', 'apply')) return toast('没有发起申请权限')
if (!this.title.trim()) return toast('请填写标题')
if (this.spec.multiInvoice) {
const missing = this.invoices.some((r) => Number(r.amount) > 0 && !(r.files && r.files.length))
if (missing) return toast('请为有金额的发票上传附件')
}
let layoffName = ''
if (this.kind === 'layoff' && this.values.targetUsername) {
const p = this.peopleOpts.find((x) => x.name === this.values.targetUsername || x.username === this.values.targetUsername)
if (p) {
this.values.targetUsername = p.username
layoffName = p.name
}
}
const body = buildApplyPayload({
spec: this.spec,
title: this.title,
values: this.values,
invoices: this.invoices,
expenseRemark: this.remark,
approverUsername: this.approver,
suggestedUsername: this.suggested,
me: store.me,
layoffTargetName: layoffName,
attachments: this.attachments
})
try {
await store.submitApply(body)
toast('已提交', 'success')
setTimeout(() => uni.navigateBack(), 600)
} catch (e) {
toast(e.message || '提交失败')
}
}
}
}
</script>
<style scoped>
.body { padding: 24rpx 28rpx 48rpx; }
.field { margin-bottom: 24rpx; }
.lab { font-size: 26rpx; color: #374151; display: block; margin-bottom: 10rpx; }
.box { min-height: 92rpx; background: #fff; border-radius: 16rpx; padding: 22rpx 24rpx; font-size: 30rpx; border: 1rpx solid rgba(17,24,39,0.06); }
.box.tall { min-height: 180rpx; }
.box textarea, .box input { width: 100%; font-size: 30rpx; }
.inv { padding: 24rpx; margin-bottom: 16rpx; }
.inv .box { margin-top: 12rpx; }
.link { color: #1677FF; font-size: 28rpx; display: block; margin-bottom: 20rpx; }
.hint { font-size: 24rpx; color: #6B7280; line-height: 1.5; }
</style>