2026-04-17 12:09:43 +08:00
|
|
|
<template>
|
|
|
|
|
<view class="page">
|
2026-04-20 15:56:29 +08:00
|
|
|
<view class="scroll">
|
|
|
|
|
<view
|
|
|
|
|
v-for="section in sections"
|
|
|
|
|
:key="section.key"
|
|
|
|
|
class="card"
|
|
|
|
|
>
|
2026-04-17 12:09:43 +08:00
|
|
|
<view class="card-title">{{ section.title }}</view>
|
2026-04-20 15:56:29 +08:00
|
|
|
|
|
|
|
|
<view
|
|
|
|
|
v-for="field in section.fields"
|
|
|
|
|
:key="field.key"
|
|
|
|
|
class="field"
|
|
|
|
|
>
|
|
|
|
|
<text class="label">
|
|
|
|
|
{{ field.label }}
|
|
|
|
|
<text v-if="field.required" class="req">*</text>
|
|
|
|
|
</text>
|
|
|
|
|
|
|
|
|
|
<template v-if="field.type === 'select'">
|
|
|
|
|
<picker
|
|
|
|
|
mode="selector"
|
|
|
|
|
:range="field.options"
|
|
|
|
|
@change="onSelect(field, $event)"
|
|
|
|
|
>
|
|
|
|
|
<view class="input-like clickable">
|
|
|
|
|
{{ getFieldDisplayValue(field) }}
|
|
|
|
|
</view>
|
|
|
|
|
</picker>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="field.type === 'datetime'">
|
|
|
|
|
<view class="input-like clickable" @tap="openDateTime(field.key)">
|
|
|
|
|
{{ getFieldDisplayValue(field) }}
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="field.type === 'city'">
|
|
|
|
|
<view class="input-like clickable" @tap="openCity(field.key)">
|
|
|
|
|
{{ getFieldDisplayValue(field) || field.placeholder || '选择城市' }}
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="field.type === 'computed'">
|
|
|
|
|
<view class="computed-box">
|
|
|
|
|
{{ getFieldDisplayValue(field) }}
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
2026-04-17 12:09:43 +08:00
|
|
|
<template v-else-if="field.type === 'file'">
|
|
|
|
|
<view class="file-picker-wrap">
|
2026-04-20 15:56:29 +08:00
|
|
|
<view class="upload-box clickable" :class="{ disabled: uploadingFieldKey === field.key }" @tap="openFilePicker(field.key)">
|
2026-04-17 12:09:43 +08:00
|
|
|
<view class="upload-hint">{{ field.placeholder || '点击上传文件' }}</view>
|
2026-04-20 15:56:29 +08:00
|
|
|
<view class="upload-sub">{{ uploadingFieldKey === field.key ? '上传中,请稍候...' : (field.uploadTip || '支持文件上传,点击后选择文件') }}</view>
|
2026-04-17 12:09:43 +08:00
|
|
|
</view>
|
2026-04-20 15:56:29 +08:00
|
|
|
|
|
|
|
|
<view v-if="field.accept" class="upload-rule">
|
|
|
|
|
仅支持 {{ field.accept }} 格式文件
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view v-if="uploadingFieldKey === field.key" class="upload-loading">
|
|
|
|
|
<text class="loading-dot"></text>
|
|
|
|
|
<text class="loading-text">文件正在上传中...</text>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view
|
|
|
|
|
v-if="getFileList(field.key).length"
|
|
|
|
|
class="file-list"
|
|
|
|
|
>
|
|
|
|
|
<view
|
|
|
|
|
v-for="(file, idx) in getFileList(field.key)"
|
|
|
|
|
:key="file.ossId || file.url || idx"
|
|
|
|
|
class="file-item"
|
|
|
|
|
>
|
2026-04-17 12:09:43 +08:00
|
|
|
<text class="file-name">{{ file.originalName || file.fileName || file.url }}</text>
|
|
|
|
|
<text class="remove" @tap="removeFile(field.key, idx)">删除</text>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
2026-04-20 15:56:29 +08:00
|
|
|
|
|
|
|
|
<template v-else-if="field.type === 'textarea'">
|
|
|
|
|
<textarea
|
|
|
|
|
v-model="form[field.key]"
|
|
|
|
|
class="textarea"
|
|
|
|
|
:maxlength="field.maxlength || 200"
|
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="field.type === 'input' || field.type === 'number' || field.type === 'text' || !field.type">
|
|
|
|
|
<view class="text-input-wrap">
|
|
|
|
|
<input
|
|
|
|
|
v-model="form[field.key]"
|
|
|
|
|
class="input"
|
|
|
|
|
:type="normalizeInputType(field.inputType)"
|
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
:confirm-type="field.confirmType || 'done'"
|
|
|
|
|
:adjust-position="true"
|
|
|
|
|
@focus="onInputFocus(field.key)"
|
|
|
|
|
/>
|
|
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
<template v-else>
|
|
|
|
|
<input
|
|
|
|
|
:value="form[field.key]"
|
|
|
|
|
class="input"
|
|
|
|
|
type="text"
|
|
|
|
|
:placeholder="field.placeholder"
|
|
|
|
|
:confirm-type="field.confirmType || 'done'"
|
|
|
|
|
:adjust-position="true"
|
|
|
|
|
@input="onInput(field.key, $event)"
|
|
|
|
|
@focus="onInputFocus(field.key)"
|
|
|
|
|
/>
|
|
|
|
|
</template>
|
|
|
|
|
</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')">
|
|
|
|
|
{{ assigneeLabel || '点击选择审批人' }}
|
|
|
|
|
</view>
|
2026-04-17 12:09:43 +08:00
|
|
|
</view>
|
|
|
|
|
</view>
|
2026-04-20 15:56:29 +08:00
|
|
|
|
|
|
|
|
<view class="card">
|
|
|
|
|
<view class="card-title">抄送</view>
|
|
|
|
|
<view class="field">
|
|
|
|
|
<text class="label">抄送人</text>
|
|
|
|
|
<view class="input-like clickable" @tap="openEmployeePicker('cc')">
|
|
|
|
|
{{ ccLabel }}
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
<view class="card">
|
|
|
|
|
<view class="card-title">提交</view>
|
|
|
|
|
<button class="btn primary" :loading="submitting" @tap="submit">提交申请</button>
|
|
|
|
|
</view>
|
|
|
|
|
</view>
|
|
|
|
|
|
2026-04-17 12:09:43 +08:00
|
|
|
<x-file-picker ref="globalFilePicker" @select="onGlobalFileSelect" />
|
2026-04-20 15:56:29 +08:00
|
|
|
<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="form[datetimeFieldKey]"
|
|
|
|
|
@change="onDateTimeChange"
|
|
|
|
|
/>
|
|
|
|
|
<city-picker
|
|
|
|
|
:visible.sync="citySheetVisible"
|
|
|
|
|
:value="form[cityFieldKey]"
|
|
|
|
|
@change="onCityChange"
|
|
|
|
|
/>
|
2026-04-17 12:09:43 +08:00
|
|
|
</view>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
import { ccFlowTask } from '@/api/hrm/flow'
|
|
|
|
|
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: { EmployeePicker, DateTimePicker, CityPicker, XFilePicker },
|
2026-04-20 15:56:29 +08:00
|
|
|
props: {
|
|
|
|
|
title: String,
|
|
|
|
|
subtitle: String,
|
|
|
|
|
bizType: String,
|
|
|
|
|
requestApi: Function,
|
|
|
|
|
initialForm: Object,
|
|
|
|
|
sections: Array,
|
|
|
|
|
flowFields: { type: Array, default: () => [] }
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
submitting: false,
|
|
|
|
|
form: JSON.parse(JSON.stringify(this.initialForm || {})),
|
|
|
|
|
employees: [],
|
|
|
|
|
employeeSheetVisible: false,
|
|
|
|
|
employeeMode: 'assignee',
|
|
|
|
|
assignee: null,
|
|
|
|
|
ccUsers: [],
|
|
|
|
|
cachedAssigneeKey: '',
|
|
|
|
|
dateTimeSheetVisible: false,
|
|
|
|
|
datetimeFieldKey: '',
|
|
|
|
|
citySheetVisible: false,
|
|
|
|
|
cityFieldKey: '',
|
|
|
|
|
fileFields: {},
|
|
|
|
|
currentFileFieldKey: '',
|
|
|
|
|
currentFileFieldConfig: null,
|
|
|
|
|
uploadingFieldKey: '',
|
|
|
|
|
currentInputFieldKey: '',
|
|
|
|
|
currentEmpId: null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
assigneeLabel() {
|
|
|
|
|
return this.assignee ? this.employeeName(this.assignee) : ''
|
|
|
|
|
},
|
|
|
|
|
ccLabel() {
|
|
|
|
|
return this.ccUsers.length ? '已选择 ' + this.ccUsers.length + ' 人' : '点击选择抄送人'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async created() {
|
|
|
|
|
this.cachedAssigneeKey = 'hrm_manual_assignee_' + (this.bizType || 'default')
|
|
|
|
|
await this.loadEmployees()
|
2026-04-17 12:09:43 +08:00
|
|
|
await this.loadCurrentEmpId()
|
2026-04-20 15:56:29 +08:00
|
|
|
this.restoreAssignee()
|
2026-04-17 12:09:43 +08:00
|
|
|
},
|
2026-04-20 15:56:29 +08:00
|
|
|
watch: {
|
|
|
|
|
'form.startTime'() {
|
|
|
|
|
this.autoFillHours()
|
|
|
|
|
},
|
|
|
|
|
'form.endTime'() {
|
|
|
|
|
this.autoFillHours()
|
|
|
|
|
}
|
2026-04-17 12:09:43 +08:00
|
|
|
},
|
|
|
|
|
methods: {
|
2026-04-20 15:56:29 +08:00
|
|
|
getFieldValue(fieldKey) {
|
|
|
|
|
return this.form[fieldKey]
|
|
|
|
|
},
|
|
|
|
|
getFieldDisplayValue(field) {
|
|
|
|
|
if (field.type === 'datetime') return this.formatDateTime(field.key) || field.placeholder || ''
|
|
|
|
|
if (field.type === 'computed') return this.computedFieldText(field.key) || field.placeholder || '—'
|
|
|
|
|
var value = this.getFieldValue(field.key)
|
|
|
|
|
return value !== undefined && value !== null && value !== '' ? value : (field.placeholder || '')
|
|
|
|
|
},
|
|
|
|
|
getFileList(fieldKey) {
|
|
|
|
|
return this.fileFields[fieldKey] || []
|
|
|
|
|
},
|
|
|
|
|
normalizeInputType(inputType) {
|
|
|
|
|
return inputType || 'text'
|
|
|
|
|
},
|
|
|
|
|
onInputFocus(fieldKey) {
|
|
|
|
|
this.currentInputFieldKey = fieldKey
|
|
|
|
|
},
|
|
|
|
|
onInput(fieldKey, e) {
|
|
|
|
|
var value = e && e.detail ? e.detail.value : e
|
|
|
|
|
this.$set(this.form, fieldKey, value)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onSelect(field, e) {
|
|
|
|
|
var idx = Number(e.detail.value)
|
|
|
|
|
var options = field.options || []
|
|
|
|
|
this.$set(this.form, field.key, options[idx])
|
|
|
|
|
},
|
|
|
|
|
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() {
|
|
|
|
|
try {
|
|
|
|
|
var res = await listUser({ pageNum: 1, pageSize: 500 })
|
|
|
|
|
this.employees = res.rows || res.data || []
|
|
|
|
|
} catch (e) {
|
|
|
|
|
this.employees = []
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
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) {
|
|
|
|
|
console.log('[loadCurrentEmpId] error', 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) {
|
|
|
|
|
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 || []
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-04-17 12:09:43 +08:00
|
|
|
openFilePicker(fieldKey) {
|
|
|
|
|
this.currentFileFieldKey = fieldKey
|
2026-04-20 15:56:29 +08:00
|
|
|
this.currentFileFieldConfig = this.flowFields.find(function (f) { return f.key === fieldKey }) || null
|
|
|
|
|
var picker = this.$refs.globalFilePicker
|
2026-04-17 12:09:43 +08:00
|
|
|
if (picker && picker.open) {
|
|
|
|
|
picker.open()
|
|
|
|
|
} else {
|
2026-04-20 15:56:29 +08:00
|
|
|
console.warn('[RequestForm] file picker ref missing or open() unavailable', fieldKey)
|
2026-04-17 12:09:43 +08:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async onGlobalFileSelect(payload) {
|
2026-04-20 15:56:29 +08:00
|
|
|
var fieldKey = this.currentFileFieldKey
|
2026-04-17 12:09:43 +08:00
|
|
|
if (!fieldKey) return
|
2026-04-20 15:56:29 +08:00
|
|
|
var files = Array.isArray(payload) ? payload : [payload]
|
2026-04-17 12:09:43 +08:00
|
|
|
if (!this.fileFields[fieldKey]) this.$set(this.fileFields, fieldKey, [])
|
2026-04-20 15:56:29 +08:00
|
|
|
var cfg = this.currentFileFieldConfig || {}
|
|
|
|
|
var allowedExt = (cfg.accept || '').toLowerCase().split(',').map(function (x) { return x.trim() }).filter(Boolean)
|
|
|
|
|
if (cfg.key === 'applyFileIds' && this.bizType === 'seal' && allowedExt.indexOf('.pdf') === -1) {
|
|
|
|
|
allowedExt = ['.pdf']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.uploadingFieldKey = fieldKey
|
|
|
|
|
uni.showLoading({ title: '上传中...' })
|
|
|
|
|
try {
|
|
|
|
|
for (var i = 0; i < files.length; i++) {
|
|
|
|
|
var f = files[i]
|
|
|
|
|
if (!f) continue
|
|
|
|
|
var name = String(f.name || f.fileName || '').toLowerCase()
|
|
|
|
|
var ext = name.indexOf('.') >= 0 ? '.' + name.split('.').pop() : ''
|
|
|
|
|
if (allowedExt.length && allowedExt.indexOf(ext) === -1) {
|
|
|
|
|
uni.showToast({ title: '仅支持上传 PDF 文件', icon: 'none' })
|
|
|
|
|
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.fileFields[fieldKey].push({
|
|
|
|
|
name: uploaded.fileName || f.name || f.fileName || 'file',
|
|
|
|
|
extname: ext.replace('.', '') || (f.name || f.fileName || '').split('.').pop() || '',
|
|
|
|
|
url: uploaded.url || f.path || f.url || '',
|
|
|
|
|
ossId: uploaded.ossId || uploaded.url || f.path || f.url || '',
|
|
|
|
|
fileName: uploaded.fileName || f.fileName || f.name || '',
|
|
|
|
|
originalName: f.name || f.fileName || uploaded.fileName || ''
|
|
|
|
|
})
|
|
|
|
|
} catch (e) {
|
|
|
|
|
uni.showToast({ title: e.message || '上传失败', icon: 'none' })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.syncFileIds(fieldKey)
|
|
|
|
|
} finally {
|
|
|
|
|
uni.hideLoading()
|
|
|
|
|
this.uploadingFieldKey = ''
|
|
|
|
|
this.currentFileFieldKey = ''
|
|
|
|
|
this.currentFileFieldConfig = null
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
removeFile(fieldKey, idx) {
|
|
|
|
|
var arr = (this.fileFields[fieldKey] || []).slice()
|
|
|
|
|
arr.splice(idx, 1)
|
|
|
|
|
this.$set(this.fileFields, fieldKey, arr)
|
|
|
|
|
this.syncFileIds(fieldKey)
|
|
|
|
|
},
|
|
|
|
|
syncFileIds(fieldKey) {
|
|
|
|
|
var arr = this.fileFields[fieldKey] || []
|
|
|
|
|
this.$set(this.form, fieldKey, arr.map(function (x) { return x.ossId }).join(','))
|
|
|
|
|
},
|
|
|
|
|
openDateTime(fieldKey) {
|
|
|
|
|
this.datetimeFieldKey = fieldKey
|
|
|
|
|
this.dateTimeSheetVisible = true
|
|
|
|
|
},
|
|
|
|
|
formatDateTime(key) {
|
|
|
|
|
var v = this.form[key]
|
|
|
|
|
if (!v) return ''
|
|
|
|
|
return String(v).replace('T', ' ').substring(0, 16)
|
|
|
|
|
},
|
|
|
|
|
onDateTimeChange(val) {
|
|
|
|
|
if (!this.datetimeFieldKey) return
|
|
|
|
|
this.$set(this.form, this.datetimeFieldKey, val)
|
|
|
|
|
this.autoFillHours()
|
|
|
|
|
},
|
|
|
|
|
computedFieldText(key) {
|
|
|
|
|
if (key === 'hours') {
|
|
|
|
|
var val = this.form[key]
|
|
|
|
|
return val ? val + ' 小时' : ''
|
|
|
|
|
}
|
|
|
|
|
var value = this.form[key]
|
|
|
|
|
return value !== undefined && value !== null ? value : ''
|
|
|
|
|
},
|
|
|
|
|
openCity(fieldKey) {
|
|
|
|
|
this.cityFieldKey = fieldKey
|
|
|
|
|
this.citySheetVisible = true
|
|
|
|
|
},
|
|
|
|
|
onCityChange(val) {
|
|
|
|
|
if (!this.cityFieldKey) return
|
|
|
|
|
this.$set(this.form, this.cityFieldKey, val)
|
|
|
|
|
},
|
|
|
|
|
autoFillHours() {
|
|
|
|
|
var s = this.parseDT(this.form.startTime)
|
|
|
|
|
var e = this.parseDT(this.form.endTime)
|
|
|
|
|
if (!s || !e) return
|
|
|
|
|
var ms = e.getTime() - s.getTime()
|
|
|
|
|
if (ms <= 0) return
|
|
|
|
|
var hours = (Math.round((ms / 3600000) * 2) / 2).toFixed(1).replace(/\.0$/, '')
|
|
|
|
|
this.$set(this.form, 'hours', hours)
|
|
|
|
|
},
|
|
|
|
|
parseDT(v) {
|
|
|
|
|
if (!v) return null
|
|
|
|
|
var d = new Date(String(v).replace('T', ' ').replace(/-/g, '/'))
|
|
|
|
|
return Number.isNaN(d.getTime()) ? null : d
|
|
|
|
|
},
|
|
|
|
|
validateFlowFields() {
|
|
|
|
|
for (var i = 0; i < this.flowFields.length; i++) {
|
|
|
|
|
var f = this.flowFields[i]
|
|
|
|
|
if (f.required && !this.form[f.key]) {
|
|
|
|
|
uni.showToast({ title: '请填写' + f.label, icon: 'none' })
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!this.assignee) {
|
|
|
|
|
uni.showToast({ title: '请选择审批人', icon: 'none' })
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
},
|
|
|
|
|
submit() {
|
|
|
|
|
this.autoFillHours()
|
|
|
|
|
if (!this.validateFlowFields()) return
|
|
|
|
|
this.doSubmit()
|
|
|
|
|
},
|
|
|
|
|
buildSubmitPayload() {
|
|
|
|
|
return {
|
|
|
|
|
...this.form,
|
|
|
|
|
status: 'pending',
|
|
|
|
|
bizType: this.bizType,
|
|
|
|
|
manualAssigneeUserId: this.assignee.userId || this.assignee.id,
|
|
|
|
|
tplId: this.form.tplId || null,
|
|
|
|
|
empId: this.currentEmpId
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async doSubmit() {
|
|
|
|
|
this.submitting = true
|
|
|
|
|
try {
|
|
|
|
|
var payload = this.buildSubmitPayload()
|
|
|
|
|
console.log('[RequestForm] submit payload', payload)
|
|
|
|
|
var res = await this.requestApi(payload)
|
|
|
|
|
var inst = res && (res.data || res)
|
|
|
|
|
|
|
|
|
|
if (!inst || (!inst.instId && !inst.bizId)) {
|
|
|
|
|
console.warn('[RequestForm] submit success but no flow instance returned', res)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.ccUsers.length && inst && inst.instId) {
|
|
|
|
|
await ccFlowTask({
|
|
|
|
|
instId: inst.instId,
|
|
|
|
|
bizId: inst.bizId,
|
|
|
|
|
bizType: this.bizType,
|
|
|
|
|
ccUserIds: this.ccUsers.map(function (u) { return u.userId || u.id }),
|
|
|
|
|
remark: '手机端抄送',
|
|
|
|
|
fromUserId: (this.$store && this.$store.state && this.$store.state.user && this.$store.state.user.id) || '',
|
|
|
|
|
nodeId: 0,
|
|
|
|
|
nodeName: '节点#0',
|
|
|
|
|
readFlag: 0
|
2026-04-17 12:09:43 +08:00
|
|
|
})
|
|
|
|
|
}
|
2026-04-20 15:56:29 +08:00
|
|
|
|
|
|
|
|
uni.showToast({ title: '提交成功', icon: 'success' })
|
|
|
|
|
setTimeout(function () {
|
|
|
|
|
uni.navigateBack({ delta: 1 })
|
|
|
|
|
}, 500)
|
|
|
|
|
} catch (e) {
|
|
|
|
|
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
|
|
|
|
} finally {
|
|
|
|
|
this.submitting = false
|
2026-04-17 12:09:43 +08:00
|
|
|
}
|
2026-04-20 15:56:29 +08:00
|
|
|
}
|
2026-04-17 12:09:43 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-04-20 15:56:29 +08:00
|
|
|
.page{min-height:100vh;background:#f5f7fb}.scroll{height:100vh;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}.text-input-wrap{position:relative;z-index:20;background:transparent;pointer-events:auto}.text-input-wrap .input{position:relative;z-index:21;pointer-events:auto;touch-action:manipulation}.clickable{color:#111827}.textarea{min-height:160rpx}.computed-box{width:100%;box-sizing:border-box;padding:18rpx 20rpx;border-radius:16rpx;background:#eef6ff;color:#6b8fd6;font-size:26rpx;font-weight:500;line-height:1.5}.upload-box{position:relative;z-index:1;border:1rpx dashed #cbd5e1;border-radius:18rpx;background:linear-gradient(180deg,#fbfdff 0%,#f4f8ff 100%);padding:24rpx 22rpx;box-shadow:0 8rpx 24rpx rgba(22,119,255,.06);pointer-events:auto}.upload-hint{font-size:28rpx;color:#111827;font-weight:700}.upload-sub{margin-top:8rpx;font-size:22rpx;color:#94a3b8;line-height:1.5}.upload-rule{margin-top:8rpx;font-size:22rpx;color:#f59e0b;line-height:1.5}.upload-loading{margin-top:12rpx;display:flex;align-items:center;gap:12rpx;padding:16rpx 18rpx;border-radius:16rpx;background:#f0f7ff;color:#1677ff;font-size:24rpx;font-weight:500}.loading-dot{width:16rpx;height:16rpx;border-radius:50%;background:#1677ff;box-shadow:0 0 0 0 rgba(22,119,255,.45);animation:uploadPulse 1.2s infinite}.loading-text{flex:1}.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}@keyframes uploadPulse{0%{transform:scale(.85);opacity:.7}50%{transform:scale(1);opacity:1}100%{transform:scale(.85);opacity:.7}}
|
|
|
|
|
</style>
|