Files
shanxia2.0/miniprogram/pages/bx/advance/apply.vue
T

327 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>