修复bug
This commit is contained in:
1 parent
1850ac640e
commit
892ba5886d
20 files changed
+1078
-178
No files matched your search
@@ -20,10 +20,19 @@
|
||||
<el-button size="small" :loading="amountComparing" @click="handleCompareAmounts">
|
||||
核对发票金额
|
||||
</el-button>
|
||||
<el-button v-if="isDraft" type="primary" size="small" @click="openAddItem">添加费用</el-button>
|
||||
<el-button type="primary" size="small" :disabled="!isEditable" @click="openAddItem">添加费用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="!isEditable"
|
||||
class="edit-lock-tip"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="editLockTip"
|
||||
/>
|
||||
|
||||
<div v-if="amountCompare" class="amount-compare-card">
|
||||
<div class="amount-compare-summary">
|
||||
<div class="compare-item">
|
||||
@@ -75,7 +84,7 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="compare-actions">
|
||||
<el-button v-if="isDraft" type="primary" size="small" :loading="amountSaving" @click="handleSaveReceiptAmounts">
|
||||
<el-button v-if="isEditable" type="primary" size="small" :loading="amountSaving" @click="handleSaveReceiptAmounts">
|
||||
保存发票金额
|
||||
</el-button>
|
||||
<el-button size="small" @click="closeAmountCompare">取消</el-button>
|
||||
@@ -105,7 +114,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" :width="isDraft ? 176 : 70" fixed="right">
|
||||
<el-table-column label="操作" width="176" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div class="item-actions">
|
||||
<el-button
|
||||
@@ -118,26 +127,38 @@
|
||||
>
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</el-button>
|
||||
<template v-if="isDraft">
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
title="上传发票(可多选,追加到当前费用项)"
|
||||
:title="isEditable ? '上传发票(可多选,追加到当前费用项)' : '当前状态不可编辑费用明细'"
|
||||
aria-label="上传发票"
|
||||
:disabled="batchUploading"
|
||||
:disabled="!isEditable || batchUploading"
|
||||
@click="openInvoiceFilePicker(row)"
|
||||
>
|
||||
<i
|
||||
:class="batchUploading ? 'fa-solid fa-spinner fa-spin' : 'fa-solid fa-file-arrow-up'"
|
||||
></i>
|
||||
</el-button>
|
||||
<el-button link type="primary" title="编辑" aria-label="编辑" @click="openEditItem(row)">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:title="isEditable ? '编辑' : '当前状态不可编辑费用明细'"
|
||||
aria-label="编辑"
|
||||
:disabled="!isEditable"
|
||||
@click="openEditItem(row)"
|
||||
>
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</el-button>
|
||||
<el-button link type="danger" title="删除" aria-label="删除" @click="removeItem(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:title="isEditable ? '删除' : '当前状态不可编辑费用明细'"
|
||||
aria-label="删除"
|
||||
:disabled="!isEditable"
|
||||
@click="removeItem(row)"
|
||||
>
|
||||
<i class="fa-regular fa-trash-can"></i>
|
||||
</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -191,7 +212,7 @@
|
||||
<i v-else class="fa-regular fa-file-pdf receipt-thumb-pdf"></i>
|
||||
<span class="receipt-thumb-name" :title="receipt.name">{{ receipt.name }}</span>
|
||||
<el-button
|
||||
v-if="isDraft"
|
||||
v-if="isEditable"
|
||||
link
|
||||
type="danger"
|
||||
class="receipt-delete-btn"
|
||||
@@ -227,8 +248,11 @@
|
||||
:timestamp="formatDateTime(record.create_time)"
|
||||
placement="top"
|
||||
>
|
||||
<strong>{{ actionLabel(record.action) }} · {{ record.approver_name || '-' }}</strong>
|
||||
<div v-if="record.comment" class="record-comment">{{ record.comment }}</div>
|
||||
<div class="record-head">
|
||||
<strong>{{ actionLabel(record.action) }}</strong>
|
||||
<span class="record-operator">操作人:{{ record.approver_name || '-' }}</span>
|
||||
</div>
|
||||
<div class="record-comment">操作理由:{{ record.comment || defaultRecordReason(record) }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无审批记录" :image-size="60" />
|
||||
@@ -244,6 +268,14 @@
|
||||
|
||||
<template #footer>
|
||||
<div class="detail-toolbar">
|
||||
<el-button
|
||||
v-if="isApproving"
|
||||
type="warning"
|
||||
:loading="withdrawing"
|
||||
@click="handleWithdraw"
|
||||
>
|
||||
撤回并编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isApproved"
|
||||
type="success"
|
||||
@@ -254,12 +286,12 @@
|
||||
</el-button>
|
||||
<div class="toolbar-right">
|
||||
<el-button
|
||||
v-if="isDraft"
|
||||
v-if="isEditable"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交审核
|
||||
{{ isDraft ? "提交审核" : "重新提交" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +303,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { uploadFile } from '@/api/file'
|
||||
import { deleteReimbursementItem, submitReimbursement, updateReimbursementItem, updateReimbursementStatus } from '@/api/reimburse'
|
||||
import { deleteReimbursementItem, submitReimbursement, updateReimbursementItem, updateReimbursementStatus, withdrawReimbursement } from '@/api/reimburse'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import {
|
||||
compareReimbursementAmounts,
|
||||
@@ -302,16 +334,34 @@ const statusOptions = [
|
||||
{ value: 4, label: '已撤回' },
|
||||
{ value: 5, label: '已打款' }
|
||||
]
|
||||
const statusLabel = value => statusOptions.find(item => item.value === value)?.label || '未知'
|
||||
const statusType = value => ({ 0: 'info', 1: 'warning', 2: 'success', 3: 'danger', 4: 'info', 5: 'success' })[value] || 'info'
|
||||
const statusLabel = value => statusOptions.find(item => item.value === Number(value))?.label || '未知'
|
||||
const statusType = value => ({ 0: 'info', 1: 'warning', 2: 'success', 3: 'danger', 4: 'info', 5: 'success' })[Number(value)] || 'info'
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => emit('update:modelValue', value)
|
||||
})
|
||||
const isDraft = computed(() => props.detail?.status === 0)
|
||||
const isDraft = computed(() => Number(props.detail?.status) === 0)
|
||||
// 可编辑状态:草稿、已驳回、已撤回;审批中需先撤回
|
||||
const isEditable = computed(() => [0, 3, 4].includes(Number(props.detail?.status)))
|
||||
// 审批中:可撤回到草稿后继续编辑
|
||||
const isApproving = computed(() => Number(props.detail?.status) === 1)
|
||||
// 审批通过(等待报销人确认款项到账)
|
||||
const isApproved = computed(() => props.detail?.status === 2)
|
||||
const isApproved = computed(() => Number(props.detail?.status) === 2)
|
||||
|
||||
// 非可编辑状态时说明原因,避免按钮置灰后无从判断
|
||||
const editLockTip = computed(() => {
|
||||
switch (Number(props.detail?.status)) {
|
||||
case 1:
|
||||
return '审批中不可编辑费用明细;如需修改,请点击底部「撤回并编辑」退回草稿。'
|
||||
case 2:
|
||||
return '审批已通过,费用明细不可编辑。'
|
||||
case 5:
|
||||
return '款项已打款,费用明细不可编辑。'
|
||||
default:
|
||||
return '当前状态不可编辑费用明细。'
|
||||
}
|
||||
})
|
||||
|
||||
const detailApplicantName = computed(() => {
|
||||
const name = props.detail?.user_name
|
||||
@@ -326,6 +376,7 @@ const detailApplicantName = computed(() => {
|
||||
return '-'
|
||||
})
|
||||
const submitting = ref(false)
|
||||
const withdrawing = ref(false)
|
||||
const manualSaving = ref(false)
|
||||
const itemVisible = ref(false)
|
||||
const batchUploading = ref(false)
|
||||
@@ -681,9 +732,19 @@ const handleItemsSaved = reimbursementId => {
|
||||
const handleSubmit = async () => {
|
||||
if (!props.detail?.id) return
|
||||
try {
|
||||
await ElMessageBox.confirm('提交后将进入审批流程,确认提交吗?', '提示', { type: 'warning' })
|
||||
const { value: comment } = await ElMessageBox.prompt(
|
||||
'提交后将进入审批流程,可填写提交说明,将记录在审批时间线中。',
|
||||
'提交审核',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '提交',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder: '请输入提交说明(选填)'
|
||||
}
|
||||
)
|
||||
submitting.value = true
|
||||
await submitReimbursement(props.detail.id)
|
||||
await submitReimbursement(props.detail.id, comment || '')
|
||||
ElMessage.success('提交成功')
|
||||
emit('changed', props.detail.id)
|
||||
} catch (error) {
|
||||
@@ -695,6 +756,35 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 撤回审批中的单据:回到草稿后可继续编辑费用条目,再重新提交
|
||||
const handleWithdraw = async () => {
|
||||
if (!props.detail?.id) return
|
||||
try {
|
||||
const { value: comment } = await ElMessageBox.prompt(
|
||||
'撤回后单据将回到草稿,可继续编辑费用条目并重新提交。',
|
||||
'撤回确认',
|
||||
{
|
||||
type: 'warning',
|
||||
confirmButtonText: '确认撤回',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder: '请输入撤回原因',
|
||||
inputValidator: value => (String(value || '').trim() ? true : '请输入撤回原因')
|
||||
}
|
||||
)
|
||||
withdrawing.value = true
|
||||
await withdrawReimbursement(props.detail.id, String(comment || '').trim())
|
||||
ElMessage.success('已撤回,可继续编辑')
|
||||
emit('changed', props.detail.id)
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error?.message || '撤回失败')
|
||||
}
|
||||
} finally {
|
||||
withdrawing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 审批通过后由报销人本人确认款项到账,单据置为「已打款」
|
||||
const confirmPaid = async () => {
|
||||
if (!props.detail?.id) return
|
||||
@@ -899,6 +989,23 @@ const removeReceipt = async receipt => {
|
||||
}
|
||||
|
||||
const actionLabel = value => ({ 1: '提交申请', 2: '审批通过', 3: '审批驳回', 4: '转审', 5: '撤回' })[value] || '审批操作'
|
||||
// 老记录没有审批意见时,用动作补一条可读的说明,避免时间线只显示 “-”
|
||||
const defaultRecordReason = record => {
|
||||
switch (Number(record?.action)) {
|
||||
case 1:
|
||||
return '提交报销申请'
|
||||
case 2:
|
||||
return '审批通过'
|
||||
case 3:
|
||||
return '审批驳回'
|
||||
case 4:
|
||||
return '转交他人审批'
|
||||
case 5:
|
||||
return '报销人主动撤回'
|
||||
default:
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
const money = value => Number(value || 0).toFixed(2)
|
||||
</script>
|
||||
|
||||
@@ -916,14 +1023,32 @@ const money = value => Number(value || 0).toFixed(2)
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-lock-tip {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
margin: 0 0 12px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.record-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.record-operator {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.record-comment {
|
||||
margin-top: 6px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.receipt-preview {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-alert
|
||||
title="保存报销单后,可在详情中逐条添加报销事项。"
|
||||
title="保存后为草稿,不会提交审批;可在详情中逐条添加报销事项,确认无误后再到列表操作栏点「提交审核」。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
@@ -56,7 +56,7 @@ const cloneForm = data => ({
|
||||
|
||||
const form = reactive(cloneForm(props.formData))
|
||||
const formRef = ref()
|
||||
const submitLabel = computed(() => (props.isEdit ? '保存' : '创建'))
|
||||
const submitLabel = computed(() => (props.isEdit ? '保存' : '保存草稿'))
|
||||
const formRules = {
|
||||
apply_date: [{ required: true, message: '请选择申请日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,14 @@
|
||||
>详情</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.status === 2"
|
||||
v-if="isStatus(row, 1)"
|
||||
link
|
||||
type="warning"
|
||||
@click="handleWithdraw(row)"
|
||||
>撤回</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="isStatus(row, 2)"
|
||||
link
|
||||
type="success"
|
||||
@click="handleConfirmPaid(row)"
|
||||
@@ -107,21 +114,21 @@
|
||||
>导出</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.status === 0"
|
||||
v-if="isEditable(row)"
|
||||
link
|
||||
type="primary"
|
||||
@click="openEdit(row.id)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.status === 0"
|
||||
v-if="isEditable(row)"
|
||||
link
|
||||
type="success"
|
||||
@click="handleSubmit(row)"
|
||||
>提交</el-button
|
||||
>{{ isStatus(row, 0) ? "提交审核" : "重新提交" }}</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="row.status === 0"
|
||||
v-if="isStatus(row, 0)"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
@@ -189,6 +196,7 @@ import {
|
||||
submitReimbursement,
|
||||
updateReimbursement,
|
||||
updateReimbursementStatus,
|
||||
withdrawReimbursement,
|
||||
} from "@/api/reimburse";
|
||||
|
||||
const statusOptions = [
|
||||
@@ -286,11 +294,22 @@ const openEdit = async (id) => {
|
||||
const saveForm = async (payload) => {
|
||||
saving.value = true;
|
||||
try {
|
||||
if (editingId.value) await updateReimbursement(editingId.value, payload);
|
||||
else await createReimbursement(payload);
|
||||
ElMessage.success("保存成功");
|
||||
let createdId = 0;
|
||||
if (editingId.value) {
|
||||
await updateReimbursement(editingId.value, payload);
|
||||
} else {
|
||||
// 新建只保存为草稿,不提交审批;审核需在列表操作栏点「提交审核」
|
||||
const res = await createReimbursement(payload);
|
||||
createdId = Number(res?.data?.id || res?.id || 0);
|
||||
}
|
||||
ElMessage.success(editingId.value ? "保存成功" : "已保存为草稿,可继续添加费用明细");
|
||||
formVisible.value = false;
|
||||
loadList();
|
||||
if (createdId) {
|
||||
// 直接打开详情继续编辑,草稿状态下可反复修改
|
||||
await openDetail(createdId);
|
||||
} else {
|
||||
loadList();
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.message || "保存失败");
|
||||
} finally {
|
||||
@@ -300,10 +319,18 @@ const saveForm = async (payload) => {
|
||||
|
||||
const handleSubmit = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm("提交后将进入审批流程,确认提交吗?", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
await submitReimbursement(row.id);
|
||||
const { value: comment } = await ElMessageBox.prompt(
|
||||
"提交后将进入审批流程,可填写提交说明,将记录在审批时间线中。",
|
||||
"提交审核",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "提交",
|
||||
cancelButtonText: "取消",
|
||||
inputType: "textarea",
|
||||
inputPlaceholder: "请输入提交说明(选填)",
|
||||
}
|
||||
);
|
||||
await submitReimbursement(row.id, comment || "");
|
||||
ElMessage.success("提交成功");
|
||||
loadList();
|
||||
} catch (error) {
|
||||
@@ -312,6 +339,31 @@ const handleSubmit = async (row) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 撤回审批中的单据:回到草稿后可继续编辑费用条目,再重新提交
|
||||
const handleWithdraw = async (row) => {
|
||||
try {
|
||||
const { value: comment } = await ElMessageBox.prompt(
|
||||
"撤回后单据将回到草稿,可继续编辑费用条目并重新提交。",
|
||||
"撤回确认",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确认撤回",
|
||||
cancelButtonText: "取消",
|
||||
inputType: "textarea",
|
||||
inputPlaceholder: "请输入撤回原因",
|
||||
inputValidator: (value) =>
|
||||
String(value || "").trim() ? true : "请输入撤回原因",
|
||||
}
|
||||
);
|
||||
await withdrawReimbursement(row.id, String(comment || "").trim());
|
||||
ElMessage.success("已撤回,可继续编辑");
|
||||
loadList();
|
||||
} catch (error) {
|
||||
if (error !== "cancel" && error !== "close")
|
||||
ElMessage.error(error?.message || "撤回失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 审批通过后由报销人本人确认款项到账,确认后状态变为「已打款」
|
||||
const handleConfirmPaid = async (row) => {
|
||||
try {
|
||||
@@ -384,7 +436,11 @@ const formatDate = (value) => {
|
||||
return formatDateUtil(value);
|
||||
};
|
||||
const statusLabel = (value) =>
|
||||
statusOptions.find((item) => item.value === value)?.label || "未知";
|
||||
statusOptions.find((item) => item.value === Number(value))?.label || "未知";
|
||||
// 兼容后端把状态序列化为字符串的情况,避免编辑/提交按钮被误隐藏
|
||||
const isStatus = (row, status) => Number(row?.status) === status;
|
||||
// 可编辑状态:草稿、已驳回、已撤回;审批中需先撤回
|
||||
const isEditable = (row) => [0, 3, 4].includes(Number(row?.status));
|
||||
const statusType = (value) =>
|
||||
({
|
||||
0: "info",
|
||||
@@ -393,7 +449,7 @@ const statusType = (value) =>
|
||||
3: "danger",
|
||||
4: "info",
|
||||
5: "success",
|
||||
})[value] || "info";
|
||||
})[Number(value)] || "info";
|
||||
onMounted(() => {
|
||||
checkMobile();
|
||||
window.addEventListener("resize", checkMobile);
|
||||
|
||||
Reference in new issue
Block a user