oa_app仓库初始化

This commit is contained in:
2026-09-23 16:52:48 +08:00
commit 65c5e9f66f
82 changed files with 6490 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
<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>
<view class="btn-primary" @click="submit">提交申请</view>
</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; padding-bottom: 80rpx; }
.field { margin-bottom: 20rpx; }
.lab { font-size: 24rpx; color: #434655; display: block; margin-bottom: 8rpx; }
.box { min-height: 88rpx; background: #fff; border-radius: 16rpx; padding: 20rpx 24rpx; font-size: 28rpx; }
.box.tall { min-height: 160rpx; }
.box textarea, .box input { width: 100%; }
.inv { padding: 20rpx; margin-bottom: 16rpx; }
.inv .box { margin-top: 12rpx; }
.link { color: #2563eb; font-size: 26rpx; display: block; margin-bottom: 20rpx; }
.hint { font-size: 24rpx; color: #737686; }
</style>