增强办公,新增审批,缺少电子签章功能
This commit is contained in:
@@ -1,48 +1,301 @@
|
||||
<template>
|
||||
<request-form
|
||||
title="出差申请"
|
||||
subtitle="填写出差信息,支持手机端快速提交"
|
||||
biz-type="travel"
|
||||
:request-api="submitTravel"
|
||||
: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="travelTypes" @change="onTravelTypeChange">
|
||||
<view class="input-like clickable">{{ travelType || '请选择出差类型' }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">开始时间<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openDateTime('startTime')">{{ formatDateTime(startTime) || '请选择开始时间' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">结束时间<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openDateTime('endTime')">{{ formatDateTime(endTime) || '请选择结束时间' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">目的地<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openCityPicker">{{ destination || '请选择城市' }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">预估费用</text>
|
||||
<input class="input" type="text" :value="estimatedCost" placeholder="请输入预估费用" @input="onEstimatedCostInput" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card">
|
||||
<view class="card-title">审批信息</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">审批人<text class="req">*</text></text>
|
||||
<view class="input-like clickable" @tap="openEmployeePicker('assignee')">{{ employeeLoading ? '审批人加载中...' : (assigneeLabel || '请选择审批人') }}</view>
|
||||
</view>
|
||||
|
||||
<view class="field">
|
||||
<text class="label">抄送人</text>
|
||||
<view class="input-like clickable" @tap="openEmployeePicker('cc')">{{ employeeLoading ? '抄送人加载中...' : ccLabel }}</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>
|
||||
<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 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" />
|
||||
<employee-picker
|
||||
:visible.sync="employeeSheetVisible"
|
||||
:title="employeeMode === 'assignee' ? '选择审批人' : '选择抄送人'"
|
||||
:list="employees"
|
||||
:multiple="employeeMode === 'cc'"
|
||||
:value="employeeMode === 'assignee' ? assignee : ccUsers"
|
||||
@change="onEmployeeChange"
|
||||
/>
|
||||
<date-time-picker :visible.sync="dateTimeSheetVisible" :value="dateValue" @change="onDateChange" />
|
||||
<city-picker :visible.sync="citySheetVisible" :value="destination" @change="onCityChange" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RequestForm from '@/components/hrm/RequestForm.vue'
|
||||
import { addTravelReq } from '@/api/hrm/travel'
|
||||
import { listUser } from '@/api/oa/user'
|
||||
import { getEmployeeByUserId } from '@/api/hrm/employee'
|
||||
import { uploadFile } from '@/api/common/upload'
|
||||
import EmployeePicker from '@/components/hrm/EmployeePicker.vue'
|
||||
import DateTimePicker from '@/components/hrm/DateTimePicker.vue'
|
||||
import CityPicker from '@/components/hrm/CityPicker.vue'
|
||||
import XFilePicker from '@/components/x-native-uploader/x-file-picker.vue'
|
||||
|
||||
export default {
|
||||
components: { RequestForm },
|
||||
components: { EmployeePicker, DateTimePicker, CityPicker, XFilePicker },
|
||||
data() {
|
||||
return {
|
||||
initialForm: { travelType: '', startTime: '', endTime: '', destination: '', reason: '', applyFileIds: '', remark: '' },
|
||||
sections: [
|
||||
{ key: 'basic', title: '基础信息', fields: [
|
||||
{ key: 'travelType', label: '出差类型', type: 'select', required: true, placeholder: '请选择出差类型', options: ['客户拜访', '项目支持', '培训学习', '会议会展', '验收交付', '其他'] },
|
||||
{ key: 'startTime', label: '开始时间', type: 'datetime', required: true, placeholder: '请选择开始时间' },
|
||||
{ key: 'endTime', label: '结束时间', type: 'datetime', required: true, placeholder: '请选择结束时间' },
|
||||
{ key: 'destination', label: '目的地', type: 'city', required: true, placeholder: '请选择全球城市' }
|
||||
]},
|
||||
{ key: 'desc', title: '说明', fields: [
|
||||
{ key: 'reason', label: '事由', type: 'textarea', required: true, placeholder: '请说明出差目的与任务' },
|
||||
{ key: 'applyFileIds', label: '附件', type: 'file', required: false, placeholder: '上传附件文件' },
|
||||
{ key: 'remark', label: '备注', type: 'textarea', required: false, placeholder: '可选' }
|
||||
]}
|
||||
],
|
||||
flowFields: [
|
||||
{ key: 'travelType', label: '出差类型', required: true },
|
||||
{ key: 'startTime', label: '开始时间', required: true },
|
||||
{ key: 'endTime', label: '结束时间', required: true },
|
||||
{ key: 'destination', label: '目的地', required: true },
|
||||
{ key: 'reason', label: '事由', required: true }
|
||||
]
|
||||
travelTypes: ['客户拜访', '项目支持', '培训学习', '会议会展', '验收交付', '其他'],
|
||||
travelType: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
destination: '',
|
||||
estimatedCost: '',
|
||||
reason: '',
|
||||
remark: '',
|
||||
accessoryApplyIds: '',
|
||||
uploadedFiles: [],
|
||||
submitting: false,
|
||||
employees: [],
|
||||
employeeLoading: true,
|
||||
employeeSheetVisible: false,
|
||||
employeeMode: 'assignee',
|
||||
assignee: null,
|
||||
ccUsers: [],
|
||||
dateTimeSheetVisible: false,
|
||||
dateFieldKey: '',
|
||||
dateValue: '',
|
||||
citySheetVisible: false,
|
||||
cachedAssigneeKey: '',
|
||||
currentEmpId: null
|
||||
}
|
||||
},
|
||||
methods: { submitTravel(payload) { return addTravelReq(payload) } }
|
||||
computed: {
|
||||
assigneeLabel() {
|
||||
return this.assignee ? this.employeeName(this.assignee) : ''
|
||||
},
|
||||
ccLabel() {
|
||||
return this.ccUsers.length ? '已选择 ' + this.ccUsers.length + ' 人' : '点击选择抄送人'
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.cachedAssigneeKey = 'hrm_manual_assignee_' + 'travel'
|
||||
await this.loadEmployees()
|
||||
await this.loadCurrentEmpId()
|
||||
this.restoreAssignee()
|
||||
},
|
||||
methods: {
|
||||
employeeName(emp) {
|
||||
return emp && (emp.empName || emp.nickName || emp.userName || emp.realName) || ('员工' + ((emp && (emp.userId || emp.id)) || ''))
|
||||
},
|
||||
employeeDept(emp) {
|
||||
return (emp && emp.dept && emp.dept.deptName) || (emp && emp.deptName) || '未分配部门'
|
||||
},
|
||||
async loadEmployees() {
|
||||
this.employeeLoading = true
|
||||
try {
|
||||
var res = await listUser({ pageNum: 1, pageSize: 500 })
|
||||
this.employees = res.rows || res.data || []
|
||||
} catch (e) {
|
||||
this.employees = []
|
||||
} finally {
|
||||
this.employeeLoading = false
|
||||
}
|
||||
},
|
||||
async loadCurrentEmpId() {
|
||||
try {
|
||||
var oaId = uni.getStorageSync('oaId')
|
||||
if (!oaId) return
|
||||
var emp = await getEmployeeByUserId(oaId)
|
||||
if (emp && emp.data && emp.data.empId) {
|
||||
this.currentEmpId = emp.data.empId
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
restoreAssignee() {
|
||||
try {
|
||||
var raw = uni.getStorageSync(this.cachedAssigneeKey)
|
||||
if (!raw) return
|
||||
var cached = typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
var id = String(cached.userId || cached.empId || '')
|
||||
var hit = this.employees.find(function (e) { return String(e.userId || e.id) === id })
|
||||
if (hit) this.assignee = hit
|
||||
} catch (e) {}
|
||||
},
|
||||
openEmployeePicker(mode) {
|
||||
if (this.employeeLoading) {
|
||||
uni.showToast({ title: '人员加载中,请稍候', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.employeeMode = mode
|
||||
this.employeeSheetVisible = true
|
||||
},
|
||||
onEmployeeChange(val) {
|
||||
if (this.employeeMode === 'assignee') {
|
||||
this.assignee = val
|
||||
try {
|
||||
uni.setStorageSync(this.cachedAssigneeKey, JSON.stringify({
|
||||
userId: val && (val.userId || val.id),
|
||||
empId: val && val.empId || '',
|
||||
empName: this.employeeName(val),
|
||||
deptName: this.employeeDept(val)
|
||||
}))
|
||||
} catch (e) {}
|
||||
} else {
|
||||
this.ccUsers = val || []
|
||||
}
|
||||
},
|
||||
onTravelTypeChange(e) {
|
||||
var idx = Number(e.detail.value)
|
||||
this.travelType = this.travelTypes[idx] || ''
|
||||
},
|
||||
openDateTime(fieldKey) {
|
||||
this.dateFieldKey = fieldKey
|
||||
this.dateValue = this[fieldKey] || ''
|
||||
this.dateTimeSheetVisible = true
|
||||
},
|
||||
onDateChange(val) {
|
||||
if (!this.dateFieldKey) return
|
||||
this[this.dateFieldKey] = val
|
||||
},
|
||||
formatDateTime(v) {
|
||||
if (!v) return ''
|
||||
return String(v).replace('T', ' ').substring(0, 16)
|
||||
},
|
||||
openCityPicker() {
|
||||
this.citySheetVisible = true
|
||||
},
|
||||
onCityChange(val) {
|
||||
this.destination = val || ''
|
||||
},
|
||||
onEstimatedCostInput(e) {
|
||||
this.estimatedCost = 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.accessoryApplyIds = this.uploadedFiles.map(function (f) { return f.ossId }).join(',')
|
||||
},
|
||||
validate() {
|
||||
if (!this.assignee) return uni.showToast({ title: '请选择审批人', icon: 'none' }), false
|
||||
if (!this.travelType) return uni.showToast({ title: '请选择出差类型', icon: 'none' }), false
|
||||
if (!this.startTime) return uni.showToast({ title: '请选择开始时间', icon: 'none' }), false
|
||||
if (!this.endTime) return uni.showToast({ title: '请选择结束时间', icon: 'none' }), false
|
||||
if (!this.destination) return uni.showToast({ title: '请选择目的地', icon: 'none' }), false
|
||||
if (!this.reason) return uni.showToast({ title: '请填写事由', icon: 'none' }), false
|
||||
return true
|
||||
},
|
||||
async submit() {
|
||||
if (!this.validate()) return
|
||||
this.submitting = true
|
||||
try {
|
||||
await addTravelReq({
|
||||
empId: this.currentEmpId,
|
||||
travelType: this.travelType,
|
||||
startTime: this.startTime,
|
||||
endTime: this.endTime,
|
||||
destination: this.destination,
|
||||
estimatedCost: this.estimatedCost,
|
||||
reason: this.reason,
|
||||
accessoryApplyIds: this.accessoryApplyIds,
|
||||
remark: this.remark,
|
||||
status: 'pending',
|
||||
bizType: 'travel',
|
||||
manualAssigneeUserId: this.assignee && (this.assignee.userId || this.assignee.id),
|
||||
ccUserIds: this.ccUsers.map(function (u) { return u.userId || u.id }).join(',')
|
||||
})
|
||||
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}.card-title{font-size:30rpx;font-weight:700;color:#111827;margin-bottom:16rpx}.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>
|
||||
|
||||
Reference in New Issue
Block a user