327 lines
9.0 KiB
Vue
327 lines
9.0 KiB
Vue
<template>
|
||
<view class="bx-page bx-page--form">
|
||
<view class="topbar" :style="safeAreaStyle">
|
||
<view class="topbar-left">
|
||
<text class="topbar-back" @click="goBack">←</text>
|
||
<text class="topbar-title">备用金申请</text>
|
||
</view>
|
||
<view class="topbar-right" />
|
||
</view>
|
||
|
||
<view class="page-padded">
|
||
<view class="form-card">
|
||
<view class="form-header">
|
||
<text class="form-header-title">备用金信息</text>
|
||
<text class="form-badge">{{ isEdit ? '编辑' : '新建' }}</text>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">申请原因<text class="req">*</text></text>
|
||
<textarea
|
||
class="form-textarea"
|
||
:class="{ error: fieldErr.reason }"
|
||
:value="form.reason"
|
||
placeholder="请输入申请原因"
|
||
:maxlength="500"
|
||
@input="onReasonInput"
|
||
/>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">申请金额<text class="req">*</text></text>
|
||
<view class="form-input-prefix">
|
||
<input
|
||
class="form-input"
|
||
:class="{ error: fieldErr.amount }"
|
||
type="digit"
|
||
:value="form.amount"
|
||
placeholder="请输入金额"
|
||
@input="onAmountInput"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="field">
|
||
<text class="field-label">备注</text>
|
||
<textarea
|
||
class="form-textarea"
|
||
:value="form.remark"
|
||
placeholder="选填"
|
||
:maxlength="500"
|
||
@input="onRemarkInput"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="form-card">
|
||
<view class="form-header">
|
||
<text class="form-header-title">附件</text>
|
||
</view>
|
||
<button
|
||
class="btn btn-primary btn-sm"
|
||
:disabled="uploading"
|
||
@click="chooseAttachments"
|
||
>
|
||
{{ uploading ? '上传中…' : '上传图片' }}
|
||
</button>
|
||
<view v-if="files.length" class="att-grid">
|
||
<view v-for="(file, idx) in files" :key="file.id" class="att-img">
|
||
<image :src="file.previewUrl || file.localPath" mode="aspectFill" />
|
||
<view v-if="!file._pending" class="del" @click.stop="removeFile(idx)">×</view>
|
||
</view>
|
||
</view>
|
||
<view v-else class="detail-section-empty" style="margin-top: 8px">暂无附件(可选)</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="bottom-action-bar">
|
||
<button class="btn btn-primary btn-block" :disabled="submitting" @click="onSubmit">
|
||
{{ submitting ? '保存中…' : '保存' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
import { PAGE_PATHS } from '@/config/api'
|
||
import { advanceApi, attachmentApi } from '@/api/bx'
|
||
import { getSafeAreaStyle } from '@/utils/safeArea'
|
||
const safeAreaStyle = getSafeAreaStyle()
|
||
|
||
const MAX_ATTACHMENTS = 10
|
||
const editId = ref('')
|
||
const savedId = ref('')
|
||
const submitting = ref(false)
|
||
const uploading = ref(false)
|
||
const files = ref([])
|
||
|
||
const form = reactive({
|
||
reason: '',
|
||
amount: '',
|
||
remark: ''
|
||
})
|
||
const fieldErr = reactive({ reason: false, amount: false })
|
||
|
||
const isEdit = computed(() => !!editId.value || !!savedId.value)
|
||
|
||
function onReasonInput(e) {
|
||
form.reason = e.detail.value
|
||
fieldErr.reason = false
|
||
}
|
||
function onAmountInput(e) {
|
||
form.amount = e.detail.value
|
||
fieldErr.amount = false
|
||
}
|
||
function onRemarkInput(e) {
|
||
form.remark = e.detail.value
|
||
}
|
||
|
||
function goBack() {
|
||
uni.navigateBack({ fail: () => uni.redirectTo({ url: PAGE_PATHS.bxAdvanceApply }) })
|
||
}
|
||
|
||
function validate() {
|
||
fieldErr.reason = false
|
||
fieldErr.amount = false
|
||
if (!form.reason?.trim()) {
|
||
fieldErr.reason = true
|
||
uni.showToast({ title: '请输入申请原因', icon: 'none' })
|
||
return false
|
||
}
|
||
const raw = String(form.amount ?? '').trim()
|
||
if (!raw) {
|
||
fieldErr.amount = true
|
||
uni.showToast({ title: '请输入申请金额', icon: 'none' })
|
||
return false
|
||
}
|
||
const amt = Number(raw)
|
||
if (Number.isNaN(amt) || amt <= 0) {
|
||
fieldErr.amount = true
|
||
uni.showToast({ title: '请输入大于 0 的申请金额', icon: 'none' })
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function chooseAttachments() {
|
||
const remain = MAX_ATTACHMENTS - files.value.length
|
||
if (remain <= 0) {
|
||
uni.showToast({ title: `最多上传 ${MAX_ATTACHMENTS} 个附件`, icon: 'none' })
|
||
return
|
||
}
|
||
uni.chooseImage({
|
||
count: remain,
|
||
sizeType: ['compressed'],
|
||
sourceType: ['album', 'camera'],
|
||
success: async (res) => {
|
||
const paths = res.tempFilePaths || []
|
||
for (const path of paths) {
|
||
await uploadOne(path)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
async function uploadOne(filePath) {
|
||
const pendingId = `pending_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`
|
||
files.value.push({
|
||
id: pendingId,
|
||
previewUrl: filePath,
|
||
localPath: filePath,
|
||
_pending: true,
|
||
_existing: false
|
||
})
|
||
uploading.value = true
|
||
try {
|
||
const res = await attachmentApi.upload(filePath, '', 0)
|
||
const data = res?.data || {}
|
||
const id = String(data.id || '')
|
||
let previewUrl = filePath
|
||
if (id) {
|
||
try {
|
||
const pRes = await attachmentApi.preview(id)
|
||
if (pRes?.data?.url) previewUrl = pRes.data.url
|
||
} catch (e) {
|
||
// keep local
|
||
}
|
||
}
|
||
const idx = files.value.findIndex((f) => f.id === pendingId)
|
||
if (idx >= 0) {
|
||
files.value[idx] = {
|
||
id: id || pendingId,
|
||
previewUrl,
|
||
localPath: filePath,
|
||
_pending: false,
|
||
_existing: false
|
||
}
|
||
}
|
||
} catch (e) {
|
||
files.value = files.value.filter((f) => f.id !== pendingId)
|
||
} finally {
|
||
uploading.value = files.value.some((f) => f._pending)
|
||
}
|
||
}
|
||
|
||
function removeFile(idx) {
|
||
const file = files.value[idx]
|
||
if (!file) return
|
||
if (file._existing && file.id && !String(file.id).startsWith('pending_')) {
|
||
attachmentApi.delete(file.id).catch(() => {})
|
||
}
|
||
files.value.splice(idx, 1)
|
||
}
|
||
|
||
async function bindNewAttachments(bizId) {
|
||
const newIds = files.value
|
||
.filter((f) => f.id && !f._existing && !f._pending && !String(f.id).startsWith('pending_'))
|
||
.map((f) => f.id)
|
||
if (!newIds.length) return
|
||
await attachmentApi.bindBiz(newIds, 'advance', bizId)
|
||
files.value.forEach((f) => {
|
||
if (newIds.includes(f.id)) f._existing = true
|
||
})
|
||
}
|
||
|
||
async function loadEditData(id) {
|
||
try {
|
||
const res = await advanceApi.getById(id)
|
||
const data = res?.data || {}
|
||
form.reason = data.reason || ''
|
||
form.amount = data.amount != null ? String(data.amount) : ''
|
||
form.remark = data.remark != null ? String(data.remark) : ''
|
||
try {
|
||
const attRes = await attachmentApi.listByBiz('advance', id)
|
||
const list = attRes?.data || []
|
||
const mapped = []
|
||
for (const a of list) {
|
||
let previewUrl = ''
|
||
if ((a.fileType || '').startsWith('image/')) {
|
||
try {
|
||
const pRes = await attachmentApi.preview(a.id)
|
||
previewUrl = pRes?.data?.url || ''
|
||
} catch (e) {
|
||
// skip
|
||
}
|
||
}
|
||
mapped.push({
|
||
id: String(a.id),
|
||
previewUrl,
|
||
_pending: false,
|
||
_existing: true
|
||
})
|
||
}
|
||
files.value = mapped
|
||
} catch (e) {
|
||
files.value = []
|
||
}
|
||
} catch (e) {
|
||
uni.showToast({ title: '加载数据失败', icon: 'none' })
|
||
}
|
||
}
|
||
|
||
async function onSubmit() {
|
||
if (!validate()) return
|
||
if (uploading.value) {
|
||
uni.showToast({ title: '附件上传中,请稍候', icon: 'none' })
|
||
return
|
||
}
|
||
submitting.value = true
|
||
const isEditingAction = !!editId.value || !!savedId.value
|
||
try {
|
||
const data = {
|
||
reason: form.reason.trim(),
|
||
amount: Number(form.amount),
|
||
remark: form.remark
|
||
}
|
||
let id
|
||
if (editId.value || savedId.value) {
|
||
id = savedId.value || editId.value
|
||
await advanceApi.update(id, data)
|
||
await bindNewAttachments(id)
|
||
} else {
|
||
const res = await advanceApi.create(data)
|
||
id = String(res?.data?.id ?? res?.id ?? '')
|
||
if (!id) throw new Error('保存成功但未返回申请单ID')
|
||
await bindNewAttachments(id)
|
||
savedId.value = id
|
||
editId.value = id
|
||
}
|
||
uni.showModal({
|
||
title: isEditingAction ? '保存成功' : '创建成功',
|
||
content: '备用金申请已保存为草稿',
|
||
confirmText: '提交审批',
|
||
confirmColor: '#00a874',
|
||
cancelText: '继续编辑',
|
||
success: async (modalRes) => {
|
||
if (!modalRes.confirm) return
|
||
try {
|
||
await advanceApi.submit(id)
|
||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||
setTimeout(() => goBack(), 500)
|
||
} catch (e) {
|
||
// toasted
|
||
}
|
||
}
|
||
})
|
||
} catch (e) {
|
||
// toasted by bxRequest when BizError
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
onLoad((query) => {
|
||
const id = query?.id ? String(query.id) : ''
|
||
if (id) {
|
||
editId.value = id
|
||
loadEditData(id)
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss">
|
||
@import '@/styles/bx-theme.scss';
|
||
</style>
|