增强办公,新增审批,缺少电子签章功能

This commit is contained in:
2026-04-20 15:56:29 +08:00
parent a026ef7a54
commit cd71a3a457
18 changed files with 1791 additions and 537 deletions

View File

@@ -1,44 +1,142 @@
<template>
<request-form
title="报销申请"
subtitle="填写报销信息,支持手机端快速提交"
biz-type="reimburse"
:request-api="submitReimburse"
:initial-form="initialForm"
:sections="sections"
:flow-fields="flowFields"
/>
<view class="page">
<view class="card">
<view class="card-title">基础信息</view>
<view class="field">
<text class="label">报销类型<text class="req">*</text></text>
<picker mode="selector" :range="reimburseTypes" @change="onReimburseTypeChange">
<view class="input-like clickable">{{ reimburseType || '请选择报销类型' }}</view>
</picker>
</view>
<view class="field">
<text class="label">报销金额<text class="req">*</text></text>
<input class="input" type="text" :value="totalAmount" placeholder="请输入金额" @input="onAmountInput" />
</view>
<view class="field">
<text class="label">附件</text>
<view class="input-like clickable" @tap="openFilePicker">点击上传附件</view>
<view v-if="uploadedFiles.length" class="file-list">
<view v-for="(file, idx) in uploadedFiles" :key="file.ossId || file.url || idx" class="file-item">
<text class="file-name">{{ file.originalName || file.fileName || file.name || file.url }}</text>
<text class="remove" @tap.stop="removeFile(idx)">删除</text>
</view>
</view>
</view>
</view>
<view class="card">
<view class="card-title">说明</view>
<view class="field">
<text class="label">事由<text class="req">*</text></text>
<textarea v-model="reason" class="textarea" placeholder="请说明报销用途" />
</view>
<view class="field">
<text class="label">备注</text>
<textarea v-model="remark" class="textarea" placeholder="可选" />
</view>
</view>
<view class="card">
<button class="btn primary" :loading="submitting" @tap="submit">提交申请</button>
</view>
<x-file-picker ref="globalFilePicker" @select="onGlobalFileSelect" />
</view>
</template>
<script>
import RequestForm from '@/components/hrm/RequestForm.vue'
import { addReimburseReq } from '@/api/hrm/reimburse'
import { uploadFile } from '@/api/common/upload'
import XFilePicker from '@/components/x-native-uploader/x-file-picker.vue'
export default {
components: { RequestForm },
components: { XFilePicker },
data() {
return {
initialForm: { reimburseType: '', totalAmount: '', applyFileIds: '', reason: '', remark: '' },
sections: [
{ key: 'basic', title: '基础信息', fields: [
{ key: 'reimburseType', label: '报销类型', type: 'select', required: true, placeholder: '请选择报销类型', options: ['差旅报销', '招待报销', '采购报销', '办公报销', '其他'] },
{ key: 'totalAmount', label: '报销金额', type: 'input', inputType: 'digit', required: true, placeholder: '请输入金额' },
{ key: 'applyFileIds', label: '附件', type: 'file', required: false, placeholder: '上传附件文件' }
]},
{ key: 'desc', title: '说明', fields: [
{ key: 'reason', label: '事由', type: 'textarea', required: true, placeholder: '请说明报销用途' },
{ key: 'remark', label: '备注', type: 'textarea', required: false, placeholder: '可选' }
]}
],
flowFields: [
{ key: 'reimburseType', label: '报销类型', required: true },
{ key: 'totalAmount', label: '报销金额', required: true },
{ key: 'reason', label: '事由', required: true }
]
reimburseTypes: ['差旅报销', '招待报销', '采购报销', '办公报销', '其他'],
reimburseType: '',
totalAmount: '',
reason: '',
remark: '',
applyFileIds: '',
uploadedFiles: [],
submitting: false
}
},
methods: { submitReimburse(payload) { return addReimburseReq(payload) } }
methods: {
onReimburseTypeChange(e) {
var idx = Number(e.detail.value)
this.reimburseType = this.reimburseTypes[idx] || ''
},
onAmountInput(e) {
this.totalAmount = e.detail.value
},
openFilePicker() {
var picker = this.$refs.globalFilePicker
if (picker && picker.open) picker.open()
},
async onGlobalFileSelect(payload) {
var files = Array.isArray(payload) ? payload : [payload]
for (var i = 0; i < files.length; i++) {
var f = files[i]
if (!f) continue
try {
var uploaded = await uploadFile({
path: f.path || f.url || '',
name: f.name || f.fileName || 'file',
size: f.size || 0,
type: f.type || ''
})
this.uploadedFiles.push({
name: uploaded.fileName || f.name || f.fileName || 'file',
fileName: uploaded.fileName || f.fileName || f.name || '',
originalName: f.name || f.fileName || uploaded.fileName || '',
url: uploaded.url || f.path || f.url || '',
ossId: uploaded.ossId || uploaded.url || f.path || f.url || ''
})
} catch (e) {
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
}
}
this.syncFileIds()
},
removeFile(idx) {
this.uploadedFiles.splice(idx, 1)
this.syncFileIds()
},
syncFileIds() {
this.applyFileIds = this.uploadedFiles.map(function (f) { return f.ossId }).join(',')
},
async submit() {
if (!this.reimburseType) return uni.showToast({ title: '请选择报销类型', icon: 'none' })
if (!this.totalAmount) return uni.showToast({ title: '请输入报销金额', icon: 'none' })
if (!this.reason) return uni.showToast({ title: '请填写事由', icon: 'none' })
this.submitting = true
try {
await addReimburseReq({
reimburseType: this.reimburseType,
totalAmount: this.totalAmount,
applyFileIds: this.applyFileIds,
reason: this.reason,
remark: this.remark,
status: 'pending',
bizType: 'reimburse'
})
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => uni.navigateBack({ delta: 1 }), 500)
} catch (e) {
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
} finally {
this.submitting = false
}
}
}
}
</script>
<style scoped></style>
<style scoped>
.page{min-height:100vh;background:#f5f7fb;padding:24rpx;box-sizing:border-box}.card{background:#fff;border-radius:20rpx;padding:24rpx;margin-bottom:20rpx}.field{margin-bottom:18rpx}.label{display:block;margin-bottom:8rpx;font-size:24rpx;color:#6b7280}.req{color:#ef4444;margin-left:4rpx}.input-like,.input,.textarea{width:100%;box-sizing:border-box;border:1rpx solid #e5e7eb;border-radius:16rpx;background:#f9fafb;padding:20rpx;font-size:28rpx;color:#111827}.input{min-height:88rpx}.clickable{color:#111827}.textarea{min-height:160rpx}.file-list{margin-top:16rpx;display:flex;flex-direction:column;gap:12rpx}.file-item{display:flex;align-items:center;justify-content:space-between;padding:18rpx 18rpx;border-radius:16rpx;background:#f8fafc;border:1rpx solid #e5eefc}.file-name{flex:1;font-size:26rpx;color:#334155;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:16rpx}.remove{font-size:24rpx;color:#ef4444;flex-shrink:0}.btn.primary{width:100%;background:#1677ff;color:#fff;border-radius:16rpx}
</style>