优化报销功能

This commit is contained in:
hero920103 committed 2026-10-16 23:46:49 +08:00
1 parent 395c7f717e
commit 4306cbd74b
19 files changed
+2059 -534

No files matched your search

+27
View File
@@ -57,3 +57,30 @@ export function getApprovalFlowPick(params) {
export function getAvailableFlows(params) {
return request({ url: '/backend/approval/flow/available', method: 'get', params });
}
// ==================== 审批中心(运行时实例) ====================
// 我发起的审批实例列表
export function getMyApprovalInstances(params) {
return request({ url: '/backend/approval/instance/mine', method: 'get', params });
}
// 我作为审批人的任务列表(handled=1 返回我已处理)
export function getApprovalTodo(params) {
return request({ url: '/backend/approval/instance/todo', method: 'get', params });
}
// 审批实例详情(含流程时间线)
export function getApprovalInstanceDetail(id) {
return request({ url: `/backend/approval/instance/detail/${id}`, method: 'get' });
}
// 审批人通过 / 驳回
export function approveApprovalInstance(data) {
return request({ url: '/backend/approval/instance/approve', method: 'post', data });
}
// 发起人撤回
export function withdrawApprovalInstance(id) {
return request({ url: `/backend/approval/instance/withdraw/${id}`, method: 'post' });
}
@@ -0,0 +1,383 @@
<template>
<el-drawer v-model="visible" title="审批详情" size="560px" destroy-on-close>
<div v-loading="loading">
<template v-if="item">
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="单据标题">{{
item.biz_title || "-"
}}</el-descriptions-item>
<el-descriptions-item label="单据类型">{{
item.biz_type || "-"
}}</el-descriptions-item>
<el-descriptions-item label="审批流程">{{
flowName || "-"
}}</el-descriptions-item>
<el-descriptions-item label="发起人">{{
item.submit_user_name || "-"
}}</el-descriptions-item>
<el-descriptions-item label="提交时间">{{
item.submit_time || item.create_time || "-"
}}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{
item.finish_time || "-"
}}</el-descriptions-item>
<el-descriptions-item label="当前状态">
<el-tag :type="tagType(item.status_tag)">{{
item.status_label || "-"
}}</el-tag>
</el-descriptions-item>
</el-descriptions>
<div class="section-title">审批流程</div>
<div class="flow">
<div
v-for="(node, idx) in timeline"
:key="`${node.node_key}-${idx}`"
class="flow-item"
>
<div class="flow-rail">
<div class="avatar" :class="{ end: node.node_type === 3 }">
{{ avatarText(node) }}
</div>
<span class="badge" :class="badgeClass(node)">{{
badgeIcon(node)
}}</span>
<span v-if="idx !== timeline.length - 1" class="line" />
</div>
<div class="flow-main">
<div class="flow-head">
<div class="flow-title">
<div class="flow-name">{{ nodeTitle(node) }}</div>
<div class="flow-sub">{{ nodeSub(node) }}</div>
</div>
<el-tag size="small" :type="tagType(node.status_tag)">{{
node.status_label
}}</el-tag>
</div>
<div v-if="nodeTime(node)" class="flow-time">
{{ nodeTime(node) }}
</div>
<div v-if="node.comment" class="flow-comment">
{{ node.comment }}
</div>
</div>
</div>
<el-empty v-if="!timeline.length" description="暂无流程节点" />
</div>
</template>
</div>
<template #footer>
<div class="drawer-footer">
<el-button v-if="canWithdraw" @click="handleWithdraw">撤回审批</el-button>
<template v-if="canApprove">
<el-button type="danger" :loading="submitting" @click="handleApprove(false)"
>驳回</el-button
>
<el-button type="primary" :loading="submitting" @click="handleApprove(true)"
>通过</el-button
>
</template>
<el-button @click="visible = false">关闭</el-button>
</div>
</template>
</el-drawer>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
approveApprovalInstance,
getApprovalInstanceDetail,
withdrawApprovalInstance,
} from "@/api/approval";
const props = defineProps({
modelValue: { type: Boolean, default: false },
instanceId: { type: [Number, String], default: "" },
});
const emit = defineEmits(["update:modelValue", "changed"]);
const visible = computed({
get: () => props.modelValue,
set: (val) => emit("update:modelValue", val),
});
const loading = ref(false);
const submitting = ref(false);
const item = ref(null);
const timeline = ref([]);
const flowName = ref("");
const canApprove = ref(false);
const canWithdraw = ref(false);
const currentTaskId = ref(0);
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
watch(
() => [props.modelValue, props.instanceId],
([open, id]) => {
if (open && id) loadDetail(id);
},
);
async function loadDetail(id) {
loading.value = true;
try {
const data = responseData(await getApprovalInstanceDetail(id));
item.value = data.item || null;
timeline.value = data.timeline || [];
flowName.value = data.flow_name || "";
canApprove.value = !!data.can_approve;
canWithdraw.value = !!data.can_withdraw;
currentTaskId.value = data.current_task_id || 0;
} catch (error) {
ElMessage.error(error?.message || "加载审批详情失败");
} finally {
loading.value = false;
}
}
async function handleApprove(pass) {
let comment = "";
try {
const { value } = await ElMessageBox.prompt(
pass ? "审批意见(可留空)" : "请填写驳回原因",
pass ? "审批通过" : "审批驳回",
{ inputType: "textarea", inputPlaceholder: "请输入", confirmButtonText: "确定", cancelButtonText: "取消" },
);
comment = value || "";
} catch (error) {
return;
}
submitting.value = true;
try {
await approveApprovalInstance({
task_id: currentTaskId.value,
pass,
comment,
});
ElMessage.success(pass ? "已通过" : "已驳回");
emit("changed");
await loadDetail(props.instanceId);
} catch (error) {
ElMessage.error(error?.message || "操作失败");
} finally {
submitting.value = false;
}
}
async function handleWithdraw() {
try {
await ElMessageBox.confirm("撤回后本次审批立即结束,确认撤回吗?", "撤回确认", {
type: "warning",
});
} catch (error) {
return;
}
submitting.value = true;
try {
await withdrawApprovalInstance(props.instanceId);
ElMessage.success("已撤回");
emit("changed");
await loadDetail(props.instanceId);
} catch (error) {
ElMessage.error(error?.message || "撤回失败");
} finally {
submitting.value = false;
}
}
/* ===== 时间线展示辅助(与移动端保持一致的语义) ===== */
function avatarText(node) {
const name = String(node.approver_name || "").trim();
return name ? name.slice(0, 1) : "系";
}
function badgeClass(node) {
if (node.is_current) return "badge-running";
if (node.status === 1) return "badge-pass";
if (node.status === 2) return "badge-reject";
return "badge-skip";
}
function badgeIcon(node) {
if (node.is_current) return "·";
if (node.status === 1) return "✓";
if (node.status === 2) return "✕";
return "—";
}
function nodeTitle(node) {
if (node.node_type === 1) return `${node.approver_name || "发起人"}(申请人)`;
if (node.node_type === 3) return "流程结束";
return node.approver_name || node.node_name || "审批节点";
}
function nodeSub(node) {
if (node.node_type === 1) return "提交申请";
if (node.node_type === 3) return node.status_label || "";
return node.approver_sub || node.node_name || "审批";
}
function nodeTime(node) {
const value = node.finish_time || node.create_time || "";
return value ? String(value).slice(0, 16) : "";
}
/** uview 的 tag 类型映射为 element-plus 的 tag 类型 */
function tagType(type) {
if (type === "error") return "danger";
return type || "info";
}
</script>
<style lang="less" scoped>
.section-title {
position: relative;
margin: 18px 0 12px;
padding-left: 10px;
font-size: 15px;
font-weight: 600;
color: #303133;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 14px;
border-radius: 2px;
background: #2563eb;
}
}
.flow {
display: flex;
flex-direction: column;
}
.flow-item {
display: flex;
gap: 12px;
}
.flow-rail {
width: 40px;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
flex-shrink: 0;
}
.avatar {
position: relative;
width: 36px;
height: 36px;
border-radius: 8px;
background: linear-gradient(135deg, #60a5fa, #2563eb);
color: #fff;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
&.end {
background: linear-gradient(135deg, #cbd5e1, #94a3b8);
}
}
.badge {
position: absolute;
top: 26px;
left: 26px;
width: 16px;
height: 16px;
border-radius: 8px;
border: 2px solid #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
line-height: 1;
color: #fff;
z-index: 3;
&.badge-pass {
background: #16a34a;
}
&.badge-running {
background: #f59e0b;
}
&.badge-reject {
background: #ef4444;
}
&.badge-skip {
background: #94a3b8;
}
}
.line {
flex: 1;
width: 0;
margin-top: 6px;
border-left: 1px dashed #cbd5e1;
}
.flow-main {
flex: 1;
min-width: 0;
padding-bottom: 20px;
}
.flow-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.flow-title {
min-width: 0;
}
.flow-name {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.flow-sub {
margin-top: 2px;
font-size: 12px;
color: #909399;
}
.flow-time {
margin-top: 4px;
font-size: 12px;
color: #909399;
}
.flow-comment {
margin-top: 8px;
padding: 8px 10px;
background: #f1f5f9;
border-radius: 6px;
font-size: 13px;
color: #606266;
line-height: 1.6;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -0,0 +1,228 @@
<template>
<div class="approval-page">
<div class="page-header">
<div>
<h2>审批列表</h2>
<p>处理待我审批的单据,并查看已处理记录</p>
</div>
<div class="header-actions">
<el-radio-group v-model="filters.handled" @change="handleScopeChange">
<el-radio-button :value="0">待我审批</el-radio-button>
<el-radio-button :value="1">我已处理</el-radio-button>
</el-radio-group>
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
</div>
</div>
<div class="filter-card">
<el-form :inline="true" :model="filters" @submit.prevent>
<el-form-item label="单据类型">
<el-select
v-model="filters.biz_type"
clearable
placeholder="全部类型"
style="width: 160px"
@change="loadList"
>
<el-option
v-for="item in bizTypes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="loadList">查询</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-card">
<div class="table-scroll">
<el-table v-loading="loading" :data="list" stripe>
<el-table-column label="单据标题" min-width="200" show-overflow-tooltip>
<template #default="{ row }">{{ row.instance?.biz_title || "-" }}</template>
</el-table-column>
<el-table-column label="单据类型" width="120">
<template #default="{ row }">{{ row.instance?.biz_type || "-" }}</template>
</el-table-column>
<el-table-column label="发起人" width="120">
<template #default="{ row }">{{ row.instance?.submit_user_name || "-" }}</template>
</el-table-column>
<el-table-column prop="node_name" label="审批节点" width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.node_name || row.node_key || "-" }}</template>
</el-table-column>
<el-table-column label="单据状态" width="110">
<template #default="{ row }">
<el-tag :type="tagType(row.instance?.status_tag)">{{
row.instance?.status_label || "-"
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="任务状态" width="110">
<template #default="{ row }">
<el-tag :type="tagType(row.status_tag)">{{ row.status_label || "-" }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="到达时间" width="170">
<template #default="{ row }">{{ row.create_time || "-" }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="180">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row.instance_id)">详情</el-button>
<el-button
v-if="row.status === 0"
link
type="primary"
@click="openDetail(row.instance_id)"
>去处理</el-button
>
</template>
</el-table-column>
<template #empty><el-empty :description="emptyText" /></template>
</el-table>
</div>
<div class="pagination">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@update:current-page="pagination.page = $event"
@update:page-size="pagination.pageSize = $event"
@current-change="loadList"
@size-change="loadList"
/>
</div>
</div>
<DetailDrawer v-model="detailVisible" :instance-id="detailId" @changed="loadList" />
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { ElMessage } from "element-plus";
import { Refresh, Search } from "@element-plus/icons-vue";
import DetailDrawer from "./components/DetailDrawer.vue";
import { getApprovalTodo } from "@/api/approval";
const bizTypes = [
{ value: "reimburse", label: "报销单" },
{ value: "oa", label: "OA 通用审批" },
];
const filters = reactive({ handled: 0, biz_type: "" });
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const list = ref([]);
const loading = ref(false);
const detailVisible = ref(false);
const detailId = ref("");
const emptyText = computed(() =>
filters.handled === 1 ? "暂无已处理的审批" : "暂无待我审批的单据",
);
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
const loadList = async () => {
loading.value = true;
try {
const data = responseData(
await getApprovalTodo({
handled: filters.handled,
biz_type: filters.biz_type || "",
page: pagination.page,
pageSize: pagination.pageSize,
}),
);
// 前端二次过滤:类型下拉切换后即时生效
const rows = data.list || [];
list.value = filters.biz_type
? rows.filter((row) => row.instance?.biz_type === filters.biz_type)
: rows;
pagination.total = Number(data.total || 0);
} catch (error) {
ElMessage.error(error?.message || "加载审批列表失败");
} finally {
loading.value = false;
}
};
const handleScopeChange = () => {
pagination.page = 1;
loadList();
};
const openDetail = (id) => {
detailId.value = id;
detailVisible.value = true;
};
function tagType(type) {
if (type === "error") return "danger";
return type || "info";
}
onMounted(loadList);
</script>
<style lang="less" scoped>
.approval-page {
background: #f5f7fa;
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h2 {
margin: 0 0 8px;
color: #303133;
font-size: 22px;
}
p {
margin: 0;
color: #909399;
font-size: 13px;
}
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.filter-card,
.table-card {
background: #fff;
border-radius: 6px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.filter-card {
margin-bottom: 16px;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.table-scroll {
width: 100%;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>
@@ -0,0 +1,236 @@
<template>
<div class="approval-page">
<div class="page-header">
<div>
<h2>我的发起</h2>
<p>查看自己发起的审批单据及当前进度</p>
</div>
<div class="header-actions">
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
</div>
</div>
<div class="filter-card">
<el-form :inline="true" :model="filters" @submit.prevent>
<el-form-item label="关键词">
<el-input
v-model="filters.keyword"
clearable
placeholder="搜索单据标题"
@keyup.enter="loadList"
/>
</el-form-item>
<el-form-item label="状态">
<el-select
v-model="filters.status"
clearable
placeholder="全部状态"
class="status-select"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" @click="loadList">查询</el-button>
<el-button :icon="Refresh" @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="table-card">
<div class="table-scroll">
<el-table v-loading="loading" :data="list" stripe>
<el-table-column prop="biz_title" label="单据标题" min-width="200" show-overflow-tooltip>
<template #default="{ row }">{{ row.biz_title || "-" }}</template>
</el-table-column>
<el-table-column prop="biz_type" label="单据类型" width="120">
<template #default="{ row }">{{ row.biz_type || "-" }}</template>
</el-table-column>
<el-table-column prop="flow_name" label="审批流程" width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.flow_name || "-" }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="tagType(row.status_tag)">{{ row.status_label || "-" }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="current_node" label="当前节点" width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.current_node || "-" }}</template>
</el-table-column>
<el-table-column prop="submit_time" label="提交时间" width="170">
<template #default="{ row }">{{ row.submit_time || row.create_time || "-" }}</template>
</el-table-column>
<el-table-column prop="finish_time" label="完成时间" width="170">
<template #default="{ row }">{{ row.finish_time || "-" }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="140">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row.id)">详情</el-button>
<el-button
v-if="row.status === 1"
link
type="danger"
@click="handleWithdraw(row)"
>撤回</el-button
>
</template>
</el-table-column>
<template #empty><el-empty description="暂无发起的审批" /></template>
</el-table>
</div>
<div class="pagination">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@update:current-page="pagination.page = $event"
@update:page-size="pagination.pageSize = $event"
@current-change="loadList"
@size-change="loadList"
/>
</div>
</div>
<DetailDrawer v-model="detailVisible" :instance-id="detailId" @changed="loadList" />
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Refresh, Search } from "@element-plus/icons-vue";
import DetailDrawer from "./components/DetailDrawer.vue";
import { getMyApprovalInstances, withdrawApprovalInstance } from "@/api/approval";
const statusOptions = [
{ value: 1, label: "审批中" },
{ value: 2, label: "已通过" },
{ value: 3, label: "已驳回" },
{ value: 4, label: "已撤回" },
{ value: 5, label: "已取消" },
];
const filters = reactive({ keyword: "", status: undefined });
const pagination = reactive({ page: 1, pageSize: 10, total: 0 });
const list = ref([]);
const loading = ref(false);
const detailVisible = ref(false);
const detailId = ref("");
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
const loadList = async () => {
loading.value = true;
try {
const data = responseData(
await getMyApprovalInstances({
...filters,
status: filters.status ?? "",
page: pagination.page,
pageSize: pagination.pageSize,
}),
);
list.value = data.list || [];
pagination.total = Number(data.total || 0);
} catch (error) {
ElMessage.error(error?.message || "加载审批列表失败");
} finally {
loading.value = false;
}
};
const resetFilters = () => {
filters.keyword = "";
filters.status = undefined;
pagination.page = 1;
loadList();
};
const openDetail = (id) => {
detailId.value = id;
detailVisible.value = true;
};
const handleWithdraw = async (row) => {
try {
await ElMessageBox.confirm(`确认撤回「${row.biz_title || "该单据"}」的审批吗?`, "撤回确认", {
type: "warning",
});
await withdrawApprovalInstance(row.id);
ElMessage.success("已撤回");
loadList();
} catch (error) {
if (error !== "cancel" && error !== "close")
ElMessage.error(error?.message || "撤回失败");
}
};
function tagType(type) {
if (type === "error") return "danger";
return type || "info";
}
onMounted(loadList);
</script>
<style lang="less" scoped>
.approval-page {
background: #f5f7fa;
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
h2 {
margin: 0 0 8px;
color: #303133;
font-size: 22px;
}
p {
margin: 0;
color: #909399;
font-size: 13px;
}
}
.filter-card,
.table-card {
background: #fff;
border-radius: 6px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.filter-card {
margin-bottom: 16px;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.status-select {
width: 150px;
}
.table-scroll {
width: 100%;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
</style>
@@ -0,0 +1,117 @@
<template>
<div class="approval-settings-page">
<div class="page-header">
<div>
<h2>审批流设置</h2>
<p>为本租户各功能模块编排审批流程:选择分类 → 新建流程 → 拖拽节点并保存</p>
</div>
</div>
<!-- 复用「系统管理 → 审批流配置」的编排界面,避免同一能力两份实现 -->
<ApprovalFlowConfig />
<div class="reference-card">
<div class="section-title">流程编号引用</div>
<p class="hint">
保存后系统会为每个流程自动生成唯一的「审批流编号」,业务程序按编号调用:
</p>
<el-table v-loading="loading" :data="flows" size="small" stripe>
<el-table-column prop="module_code" label="功能模块" width="120" />
<el-table-column prop="flow_name" label="流程名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="flow_code" label="审批流编号" width="160" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">{{
row.status === 1 ? "启用" : "停用"
}}</el-tag>
</template>
</el-table-column>
<template #empty><el-empty description="暂无审批流,请先在上方新建" /></template>
</el-table>
</div>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { ElMessage } from "element-plus";
import ApprovalFlowConfig from "@/views/system/approval/index.vue";
import { getApprovalFlowList } from "@/api/approval";
const flows = ref([]);
const loading = ref(false);
const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
const loadFlows = async () => {
loading.value = true;
try {
const data = responseData(await getApprovalFlowList());
flows.value = Array.isArray(data) ? data : data.list || [];
} catch (error) {
ElMessage.error(error?.message || "加载审批流失败");
} finally {
loading.value = false;
}
};
onMounted(loadFlows);
</script>
<style lang="less" scoped>
.approval-settings-page {
background: #f5f7fa;
padding: 20px;
}
.page-header {
margin-bottom: 16px;
h2 {
margin: 0 0 8px;
color: #303133;
font-size: 22px;
}
p {
margin: 0;
color: #909399;
font-size: 13px;
}
}
.reference-card {
margin-top: 16px;
background: #fff;
border-radius: 6px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}
.section-title {
position: relative;
margin-bottom: 12px;
padding-left: 10px;
font-size: 15px;
font-weight: 600;
color: #303133;
&::before {
content: "";
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 14px;
border-radius: 2px;
background: #2563eb;
}
}
.hint {
margin: 0 0 12px;
font-size: 13px;
color: #909399;
}
</style>
@@ -242,33 +242,16 @@
@saved="handleItemsSaved"
/>
<el-dialog
v-model="manualConfirmVisible"
title="手动确认"
width="420px"
class="manual-confirm-dialog"
append-to-body
destroy-on-close
>
<p class="manual-tip">选择要变更的单据状态,保存后立即生效。</p>
<el-radio-group v-model="manualStatus" class="manual-status-group">
<el-radio
v-for="item in statusOptions"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
<template #footer>
<el-button @click="manualConfirmVisible = false">取消</el-button>
<el-button type="primary" :loading="manualSaving" @click="saveManualStatus">确认</el-button>
</template>
</el-dialog>
<template #footer>
<div class="detail-toolbar">
<el-button @click="openManualConfirm">手动确认</el-button>
<el-button
v-if="isApproved"
type="success"
:loading="manualSaving"
@click="confirmPaid"
>
确认到账
</el-button>
<div class="toolbar-right">
<el-button
v-if="isDraft"
@@ -327,6 +310,8 @@ const visible = computed({
set: value => emit('update:modelValue', value)
})
const isDraft = computed(() => props.detail?.status === 0)
// 审批通过(等待报销人确认款项到账)
const isApproved = computed(() => props.detail?.status === 2)
const detailApplicantName = computed(() => {
const name = props.detail?.user_name
@@ -341,9 +326,7 @@ const detailApplicantName = computed(() => {
return '-'
})
const submitting = ref(false)
const manualConfirmVisible = ref(false)
const manualSaving = ref(false)
const manualStatus = ref(0)
const itemVisible = ref(false)
const batchUploading = ref(false)
const invoiceFileInputRef = ref(null)
@@ -712,23 +695,25 @@ const handleSubmit = async () => {
}
}
const resolveManualStatus = status => (status >= 0 && status <= 5 ? status : 0)
const openManualConfirm = () => {
manualStatus.value = resolveManualStatus(props.detail?.status ?? 0)
manualConfirmVisible.value = true
}
const saveManualStatus = async () => {
// 审批通过后由报销人本人确认款项到账,单据置为「已打款」
const confirmPaid = async () => {
if (!props.detail?.id) return
try {
await ElMessageBox.confirm(
`确认已收到 ¥${Number(props.detail.total_amount || 0).toFixed(2)} 的报销款吗?确认后单据状态变为「已打款」。`,
'确认到账',
{ type: 'warning', confirmButtonText: '确认已到账', cancelButtonText: '暂未到账' }
)
} catch (error) {
return
}
manualSaving.value = true
try {
await updateReimbursementStatus(props.detail.id, { status: manualStatus.value })
ElMessage.success('状态已更新')
manualConfirmVisible.value = false
await updateReimbursementStatus(props.detail.id, { status: 5 })
ElMessage.success('已确认到账')
emit('changed', props.detail.id)
} catch (error) {
ElMessage.error(error?.message || '更新状态失败')
ElMessage.error(error?.message || '确认到账失败')
} finally {
manualSaving.value = false
}
@@ -1066,19 +1051,6 @@ const money = value => Number(value || 0).toFixed(2)
gap: 10px;
}
.manual-tip {
margin: 0 0 16px;
color: #909399;
font-size: 13px;
}
.manual-status-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.amount-compare-card {
margin-bottom: 16px;
padding: 14px 16px;
@@ -92,6 +92,13 @@
<el-button link type="primary" @click="openDetail(row.id)"
>详情</el-button
>
<el-button
v-if="row.status === 2"
link
type="success"
@click="handleConfirmPaid(row)"
>确认到账</el-button
>
<el-button
link
type="primary"
@@ -181,6 +188,7 @@ import {
getReimbursementRecords,
submitReimbursement,
updateReimbursement,
updateReimbursementStatus,
} from "@/api/reimburse";
const statusOptions = [
@@ -304,6 +312,23 @@ const handleSubmit = async (row) => {
}
};
// 审批通过后由报销人本人确认款项到账,确认后状态变为「已打款」
const handleConfirmPaid = async (row) => {
try {
await ElMessageBox.confirm(
`确认已收到 ¥${money(row.total_amount)} 的报销款吗?确认后单据状态变为「已打款」。`,
"确认到账",
{ type: "warning", confirmButtonText: "确认已到账", cancelButtonText: "暂未到账" }
);
await updateReimbursementStatus(row.id, { status: 5 });
ElMessage.success("已确认到账");
loadList();
} catch (error) {
if (error !== "cancel" && error !== "close")
ElMessage.error(error?.message || "确认到账失败");
}
};
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm("确认删除这条报销记录吗?", "删除确认", {