feat(approval): 员工端审批中心入口与业务完成确认单页面 (#19)

This commit is contained in:
2026-10-30 20:20:34 +08:00
parent ae8bcb1e57
commit 370091ed6e
9 changed files with 1794 additions and 2 deletions
+441
View File
@@ -0,0 +1,441 @@
<template>
<view class="detail-page">
<view v-if="detail" class="content">
<uni-card :border="false" :padding="'16px'">
<view class="head">
<text class="title">{{ detail.title || detail.name || '单据详情' }}</text>
<text class="status" :class="'status-' + detail.status">
{{ detail.statusName }}
</text>
</view>
<view class="rows">
<view class="row">
<text class="label">单号</text>
<text class="value">{{ detail.bizNo || '—' }}</text>
</view>
<view class="row">
<text class="label">单据类型</text>
<text class="value">{{ detail.docTypeName }}</text>
</view>
<view class="row">
<text class="label">报送人</text>
<text class="value">{{ detail.reporterName || '—' }}</text>
</view>
<view class="row">
<text class="label">报送部门</text>
<text class="value">{{ detail.reportOrgName || '—' }}</text>
</view>
<view class="row">
<text class="label">当前节点</text>
<text class="value">{{ detail.currentNodeName || '—' }}</text>
</view>
<view class="row">
<text class="label">提交时间</text>
<text class="value">{{ formatTime(detail.submitTime) }}</text>
</view>
</view>
</uni-card>
<uni-card :border="false" :padding="'16px'" title="单据信息">
<view v-if="detail.docType === 'completion'" class="rows">
<view class="row">
<text class="label">名称</text>
<text class="value">{{ detail.name }}</text>
</view>
<view class="row">
<text class="label">实施单位</text>
<text class="value">{{ detail.implementOrg }}</text>
</view>
<view class="row">
<text class="label">费用(元)</text>
<text class="value">{{ formatFee(detail.fee) }}</text>
</view>
<view class="row">
<text class="label">服务内容</text>
<text class="value multiline">{{ detail.serviceContent }}</text>
</view>
</view>
<view v-else class="rows">
<view class="row">
<text class="label">受理部门</text>
<text class="value">{{ detail.acceptOrgName || '—' }}</text>
</view>
<view class="row">
<text class="label">标题</text>
<text class="value">{{ detail.title }}</text>
</view>
<view class="row">
<text class="label">内容</text>
<text class="value multiline">{{ detail.content }}</text>
</view>
<view class="row">
<text class="label">备注</text>
<text class="value multiline">{{ detail.remark || '—' }}</text>
</view>
</view>
</uni-card>
<uni-card :border="false" :padding="'16px'" title="附件">
<view
v-for="item in detail.attachments"
:key="item.id"
class="attachment"
@click="openAttachment(item.url)"
>
<view class="attachment-main">
<text class="attachment-name">{{ item.name }}</text>
<text class="attachment-meta">
{{ item.categoryName }} · {{ formatSize(item.length) }}
</text>
</view>
<text class="link">查看</text>
</view>
<view v-if="detail.attachments.length === 0" class="empty">暂无附件</view>
</uni-card>
<uni-card :border="false" :padding="'16px'" title="处理记录">
<view v-for="item in detail.records" :key="item.id" class="timeline-item">
<view class="timeline-dot"></view>
<view class="timeline-body">
<view class="timeline-head">
<text class="timeline-operator">{{ item.operatorName || '—' }}</text>
<text class="timeline-action">{{ item.actionName }}</text>
</view>
<text class="timeline-node">{{ item.nodeName || '提交' }}</text>
<text v-if="item.opinion" class="timeline-opinion">意见:{{ item.opinion }}</text>
<text class="timeline-time">{{ formatTime(item.createTime) }}</text>
</view>
</view>
<view v-if="detail.records.length === 0" class="empty">暂无记录</view>
</uni-card>
<view v-if="detail.permissions.canEdit || detail.permissions.canSubmit" class="actions">
<button
v-if="detail.permissions.canEdit"
class="action-button ghost"
@click="editDraft"
>
编辑
</button>
<button
v-if="detail.permissions.canSubmit"
class="action-button primary"
:loading="submitting"
@click="submitDraft"
>
提交
</button>
</view>
</view>
<view v-else class="loading">
<text>{{ loading ? '加载中...' : '单据不存在' }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { getInstance, submitInstance } from '@/api/approval/instance';
import type { ApprovalInstance } from '@/api/approval/model';
import { showError } from '@/utils/app';
const detail = ref<ApprovalInstance | null>(null);
const loading = ref(false);
const submitting = ref(false);
const instanceId = ref(0);
onLoad(options => {
instanceId.value = Number(options?.id) || 0;
load();
});
async function load() {
if (!instanceId.value) {
return;
}
loading.value = true;
try {
detail.value = await getInstance(instanceId.value);
} catch (error: any) {
showError(error?.message || '加载失败', null);
} finally {
loading.value = false;
}
}
function editDraft() {
uni.navigateTo({
url:
'/packageApproval/pages/form-completion?mode=edit&id=' + instanceId.value
});
}
function submitDraft() {
uni.showModal({
title: '提交确认',
content: '提交后字段与附件将锁定,确认提交?',
success: async result => {
if (!result.confirm) {
return;
}
submitting.value = true;
try {
const response = await submitInstance(instanceId.value);
uni.showToast({
title: response.message || '提交成功',
icon: 'none'
});
await load();
} catch (error: any) {
showError(error?.message || '提交失败', null);
} finally {
submitting.value = false;
}
}
});
}
function openAttachment(url?: string) {
if (!url) {
return;
}
// #ifdef H5
window.open(url, '_blank');
// #endif
// #ifndef H5
uni.downloadFile({
url,
success: result => {
if (result.statusCode === 200) {
uni.openDocument({
filePath: result.tempFilePath,
showMenu: true
});
}
},
fail: () => uni.showToast({ title: '附件打开失败', icon: 'none' })
});
// #endif
}
function formatTime(value?: string) {
if (!value) {
return '—';
}
return value.replace('T', ' ').substring(0, 19);
}
function formatFee(value?: number) {
if (value === undefined || value === null) {
return '—';
}
return Number(value).toFixed(2);
}
function formatSize(value?: number) {
if (!value) {
return '0 B';
}
if (value < 1024) {
return value + ' B';
}
if (value < 1024 * 1024) {
return (value / 1024).toFixed(1) + ' KB';
}
return (value / 1024 / 1024).toFixed(1) + ' MB';
}
</script>
<style lang="scss">
.detail-page {
min-height: 100vh;
background-color: #f5f6f8;
padding-bottom: 180rpx;
}
.head {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16rpx;
}
.title {
flex: 1;
font-size: 32rpx;
font-weight: 600;
color: #303133;
}
.status {
font-size: 24rpx;
padding: 4rpx 14rpx;
border-radius: 999rpx;
background: #f2f3f5;
color: #606266;
}
.status-PENDING {
background: #fff7e6;
color: #d46b08;
}
.status-REJECTED {
background: #fff1f0;
color: #cf1322;
}
.status-APPROVED {
background: #f6ffed;
color: #389e0d;
}
.status-ARCHIVED {
background: #e6f4ff;
color: #0958d9;
}
.rows {
display: flex;
flex-direction: column;
}
.row {
display: flex;
align-items: flex-start;
margin-top: 12rpx;
font-size: 26rpx;
}
.label {
width: 160rpx;
color: #909399;
flex-shrink: 0;
}
.value {
flex: 1;
color: #303133;
word-break: break-all;
}
.multiline {
white-space: pre-wrap;
line-height: 1.6;
}
.attachment {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.attachment-main {
display: flex;
flex-direction: column;
flex: 1;
}
.attachment-name {
font-size: 26rpx;
color: #303133;
word-break: break-all;
}
.attachment-meta {
margin-top: 6rpx;
font-size: 24rpx;
color: #909399;
}
.link {
font-size: 26rpx;
color: #d81e06;
margin-left: 16rpx;
}
.timeline-item {
display: flex;
padding: 12rpx 0;
}
.timeline-dot {
width: 16rpx;
height: 16rpx;
margin: 10rpx 16rpx 0 0;
border-radius: 50%;
background: #d81e06;
flex-shrink: 0;
}
.timeline-body {
flex: 1;
display: flex;
flex-direction: column;
}
.timeline-head {
display: flex;
align-items: center;
gap: 12rpx;
}
.timeline-operator {
font-size: 28rpx;
color: #303133;
font-weight: 600;
}
.timeline-action {
font-size: 24rpx;
color: #d81e06;
}
.timeline-node,
.timeline-opinion,
.timeline-time {
margin-top: 6rpx;
font-size: 24rpx;
color: #909399;
}
.empty {
padding: 24rpx 0;
text-align: center;
font-size: 26rpx;
color: #909399;
}
.actions {
display: flex;
gap: 24rpx;
padding: 24rpx 32rpx 48rpx;
}
.action-button {
flex: 1;
font-size: 30rpx;
border-radius: 12rpx;
}
.action-button.primary {
background: #d81e06;
color: #ffffff;
}
.action-button.ghost {
background: #ffffff;
color: #303133;
border: 1rpx solid #dcdfe6;
}
.loading {
padding: 160rpx 0;
text-align: center;
color: #909399;
font-size: 28rpx;
}
</style>