更新一键审批

master
chenhao 2025-12-10 18:14:28 +08:00
parent 3d3af1867b
commit 8c3cf68639
5 changed files with 305 additions and 72 deletions

View File

@ -1,18 +1,18 @@
import http from '@/utils/http' import http from '@/utils/http'
import type { ApiResponse, Order, OrderDetailResponse, ListParams, ApprovalParams, CompletedApprovalItem, CompletedListParams } from '@/types' import type { ApiResponse, Order, OrderDetailResponse, ListParams, CompletedApprovalItem, CompletedListParams, BatchApprovalParams } from '@/types'
import type { AxiosResponse } from 'axios' import type { AxiosResponse } from 'axios'
/** /**
* *
*/ */
export const getOrderList = (params: ListParams): Promise<AxiosResponse<ApiResponse<{ export const getOrderList = (params: ListParams): Promise<AxiosResponse<ApiResponse<{
total: number total: number
rows: Order[] rows: Order[]
}>>> => { }>>> => {
// 创建FormData对象 // 创建一个新的FormData对象
const formData = new FormData() const formData = new FormData()
// 添加参数到FormData // 动态地将params中的所有键值对添加到formData中
if (params.approve) formData.append('approve', params.approve) if (params.approve) formData.append('approve', params.approve)
formData.append('page', params.page.toString()) formData.append('page', params.page.toString())
formData.append('pageSize', params.pageSize.toString()) formData.append('pageSize', params.pageSize.toString())
@ -29,23 +29,23 @@ export const getOrderDetail = (id: string | number): Promise<AxiosResponse<ApiRe
} }
/** /**
* *
*/ */
export const submitApproval = (params: any): Promise<AxiosResponse<ApiResponse<any>>> => { export const submitApproval = (params: any): Promise<AxiosResponse<ApiResponse<any>>> => {
// 创建FormData对象 // 创建一个新的FormData对象
const formData = new FormData() const formData = new FormData()
// 将所有参数添加到FormData中 // 动态地将params中的所有键值对添加到formData中
Object.keys(params).forEach(key => { Object.keys(params).forEach(key => {
if (params[key] !== undefined && params[key] !== null) { if (params[key] !== undefined && params[key] !== null) {
// 特殊处理variables参数它应该是一个对象 // 如果字段是variables它是一个对象需要特殊处理
if (key === 'variables' && typeof params[key] === 'object' && params[key] !== null) { if (key === 'variables' && typeof params[key] === 'object' && params[key] !== null) {
// 将variables对象的每个属性单独添加到FormData中 // 将variables对象中的每个键值对添加到formData
Object.keys(params[key]).forEach(variableKey => { Object.keys(params[key]).forEach(variableKey => {
if (params[key][variableKey] !== undefined && params[key][variableKey] !== null) { if (params[key][variableKey] !== undefined && params[key][variableKey] !== null) {
formData.append(`variables[${variableKey}]`, params[key][variableKey].toString()) formData.append(`variables[${variableKey}]`, params[key][variableKey].toString());
} }
}) });
} else if (key === 'taxRateData' && Array.isArray(params[key])) { } else if (key === 'taxRateData' && Array.isArray(params[key])) {
// 特殊处理taxRateData数组 // 特殊处理taxRateData数组
params[key].forEach((item: any, index: number) => { params[key].forEach((item: any, index: number) => {
@ -71,14 +71,21 @@ export const getCompletedOrderList = (params: CompletedListParams): Promise<Axio
total: number total: number
rows: CompletedApprovalItem[] rows: CompletedApprovalItem[]
}>>> => { }>>> => {
// 创建FormData对象 // 创建一个新的FormData对象
const formData = new FormData() const formData = new FormData()
// 添加参数到FormData // 动态地将params中的所有键值对添加到formData中
formData.append('page', params.page.toString()) formData.append('page', params.page.toString())
formData.append('pageSize', params.pageSize.toString()) formData.append('pageSize', params.pageSize.toString())
if (params.businessName) formData.append('businessName', params.businessName) if (params.businessName) formData.append('businessName', params.businessName)
if (params.processKeyList) formData.append('processKeyList', params.processKeyList) if (params.processKeyList) formData.append('processKeyList', params.processKeyList.join(','))
return http.post('/flow/completed/list', formData) return http.post('/flow/completed/list', formData)
}
/**
*
*/
export const batchApproval = (params: BatchApprovalParams): Promise<AxiosResponse<ApiResponse<any>>> => {
return http.post('/project/order/order/approve/batch', params)
} }

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { Order, OrderDetailResponse, ListParams, CompletedApprovalItem, CompletedListParams } from '@/types' import type { Order, OrderDetailResponse, ListParams, CompletedApprovalItem, CompletedListParams, BatchApprovalParams } from '@/types'
import { getOrderList, getOrderDetail, getCompletedOrderList } from '@/api/order' import { getOrderList, getOrderDetail, getCompletedOrderList, batchApproval } from '@/api/order'
interface OrderState { interface OrderState {
// 待审批列表相关 // 待审批列表相关
@ -220,6 +220,18 @@ export const useOrderStore = defineStore('order', {
} }
}, },
/**
*
*/
async batchApproval(params: BatchApprovalParams) {
try {
const response = await batchApproval(params)
return response
} catch (error) {
throw error
}
},
/** /**
* *
*/ */

View File

@ -242,6 +242,14 @@ export interface CompletedListParams {
processKeyList: Array<string> processKeyList: Array<string>
} }
// 批量审批参数
export interface BatchApprovalParams {
variables: {
approveBtn: ApproveBtn
comment: string
}
}
// ============= 采购相关类型定义 ============= // ============= 采购相关类型定义 =============
// 采购订单信息类型 // 采购订单信息类型

View File

@ -77,7 +77,9 @@ class HttpClient {
message = data?.msg || `请求失败 (${status})` message = data?.msg || `请求失败 (${status})`
} }
} else if (error.request) { } else if (error.request) {
message = '网络连接失败' message = '会话已过期,请重新登录'
localStorage.removeItem('isAuthenticated')
window.location.href = '/login'
} }
showFailToast(message) showFailToast(message)

View File

@ -36,7 +36,7 @@
待审批 待审批
</div> </div>
</div> </div>
<div class="order-info"> <div class="order-info">
<div class="info-row"> <div class="info-row">
<span class="label">项目名称</span> <span class="label">项目名称</span>
@ -81,7 +81,7 @@
{{ getCompletedStatusText(item.approveStatus) }} {{ getCompletedStatusText(item.approveStatus) }}
</div> </div>
</div> </div>
<div class="order-info"> <div class="order-info">
<div class="info-row"> <div class="info-row">
<span class="label">合同名称</span> <span class="label">合同名称</span>
@ -114,16 +114,98 @@
</van-pull-refresh> </van-pull-refresh>
</van-tab> </van-tab>
</van-tabs> </van-tabs>
<!-- 一键审批操作悬浮框 -->
<div v-if="currentTab === 'pending' && orderList.length > 0" class="batch-actions-footer">
<div class="footer-content">
<van-button
type="primary"
size="large"
@click="showBatchApprovalDialog"
:loading="batchSubmitting"
class="batch-approve-btn"
>
一键审批
</van-button>
</div>
</div>
<!-- 批量审批意见弹窗 -->
<van-popup
v-model:show="batchApprovalDialogVisible"
position="bottom"
round
:style="{ height: '50%' }"
>
<div class="approval-dialog">
<div class="dialog-header">
<span>审批意见</span>
<van-icon name="cross" @click="batchApprovalDialogVisible = false"/>
</div>
<div class="dialog-body">
<!-- 默认意见标签 -->
<div class="opinion-tags">
<div class="tags-title">常用意见</div>
<div class="tags-container">
<van-tag
v-for="tag in getBatchOpinionTags()"
:key="tag"
:type="selectedBatchTag === tag ? 'primary' : 'default'"
size="medium"
@click="selectBatchTag(tag)"
class="opinion-tag"
>
{{ tag }}
</van-tag>
</div>
</div>
<div class="opinion-input">
<van-field
v-model="batchApprovalOpinion"
type="textarea"
placeholder="请输入审批意见"
rows="4"
autosize
maxlength="500"
show-word-limit
/>
</div>
</div>
<div class="dialog-footer">
<div class="footer-buttons">
<van-button
type="default"
block
@click="submitBatchApproval(0)"
:loading="batchSubmitting"
class="action-btn reject-btn"
>
驳回
</van-button>
<van-button
type="primary"
block
@click="submitBatchApproval(1)"
:loading="batchSubmitting"
class="action-btn approve-btn"
>
通过
</van-button>
</div>
</div>
</div>
</van-popup>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useOrderStore } from '@/store/order' import { useOrderStore } from '@/store/order'
import { formatOrderStatus, formatAmount, formatDate } from '@/utils' import { formatAmount, formatDate } from '@/utils'
import type { OrderStatus, ApprovalStatus } from '@/types' import type { ApprovalStatus, ApproveBtn, BatchApprovalParams } from '@/types'
import { showToast, showSuccessToast } from 'vant'
const router = useRouter() const router = useRouter()
const orderStore = useOrderStore() const orderStore = useOrderStore()
@ -142,15 +224,11 @@ const searchKeyword = ref('')
const refreshing = ref(false) const refreshing = ref(false)
const completedRefreshing = ref(false) const completedRefreshing = ref(false)
// //
const getStatusClass = (status: OrderStatus) => { const batchApprovalDialogVisible = ref(false)
const classMap = { const batchApprovalOpinion = ref('')
'0': 'pending', const batchSubmitting = ref(false)
'1': 'approved', const selectedBatchTag = ref('')
'2': 'rejected'
}
return classMap[status] || 'pending'
}
// //
const onLoad = async () => { const onLoad = async () => {
@ -178,27 +256,16 @@ const onTabChange = (name: string) => {
searchKeyword.value = '' searchKeyword.value = ''
if (name === 'completed' && completedList.value.length === 0) { if (name === 'completed' && completedList.value.length === 0) {
//
onCompletedLoad() onCompletedLoad()
} }
} }
// //
const onCompletedLoad = async () => { const onCompletedLoad = async () => {
console.log('=== van-list 触发 onCompletedLoad ===')
console.log('当前状态:', {
loading: completedLoading.value,
finished: completedFinished.value,
listLength: completedList.value.length,
currentPage: orderStore.completedCurrentPage
})
try { try {
await orderStore.loadCompletedOrderList() await orderStore.loadCompletedOrderList()
console.log('onCompletedLoad 执行完成')
} catch (error) { } catch (error) {
console.error('加载已审批列表失败:', error) console.error('加载已审批列表失败:', error)
// loading
orderStore.completedLoading = false orderStore.completedLoading = false
} }
} }
@ -244,19 +311,13 @@ const handleClear = async () => {
// //
const getCompletedStatusText = (status: ApprovalStatus) => { const getCompletedStatusText = (status: ApprovalStatus) => {
const statusMap = { const statusMap = { 2: '驳回', 3: '通过' }
2: '驳回',
3: '通过'
}
return statusMap[status] || '提交' return statusMap[status] || '提交'
} }
// //
const getCompletedStatusClass = (status: ApprovalStatus) => { const getCompletedStatusClass = (status: ApprovalStatus) => {
const classMap = { const classMap = { 2: 'rejected', 3: 'approved' }
2: 'rejected',
3: 'approved'
}
return classMap[status] || 'pending' return classMap[status] || 'pending'
} }
@ -265,16 +326,68 @@ const goToDetail = (id: number) => {
router.push({ path: `/detail/${id}`, query: { from: 'order' } }) router.push({ path: `/detail/${id}`, query: { from: 'order' } })
} }
// //
const goToCompletedDetail = (businessId: number) => { const goToCompletedDetail = (businessId: number) => {
router.push({ router.push({ path: `/detail/${businessId}`, query: { readonly: 'true', from: 'order' } })
path: `/detail/${businessId}`, }
query: { readonly: 'true', from: 'order' }
}) //
const showBatchApprovalDialog = () => {
batchApprovalOpinion.value = ''
selectedBatchTag.value = ''
batchApprovalDialogVisible.value = true
}
//
const getBatchOpinionTags = () => {
return [
'所有信息已阅,审核通过',
'经审查有问题,驳回'
]
}
//
const selectBatchTag = (tag: string) => {
if (selectedBatchTag.value === tag) {
selectedBatchTag.value = ''
batchApprovalOpinion.value = ''
} else {
selectedBatchTag.value = tag
batchApprovalOpinion.value = tag
}
}
//
const submitBatchApproval = async (approveStatus: ApproveBtn) => {
const opinion = batchApprovalOpinion.value.trim()
if (approveStatus === 0 && !opinion) {
showToast('请输入驳回原因')
return
}
batchSubmitting.value = true
try {
const params: BatchApprovalParams = {
variables: {
approveBtn: approveStatus+'',
comment: opinion || (approveStatus === 1 ? '同意' : '')
}
}
await orderStore.batchApproval(params)
showSuccessToast(approveStatus === 1 ? '一键审批通过成功' : '一键驳回成功')
batchApprovalDialogVisible.value = false
await onRefresh()
} catch (error) {
console.error('一键审批失败:', error)
showToast('一键审批失败,请稍后重试')
} finally {
batchSubmitting.value = false
}
} }
onMounted(() => { onMounted(() => {
//
orderStore.resetListState() orderStore.resetListState()
orderStore.resetCompletedListState() orderStore.resetCompletedListState()
onLoad() onLoad()
@ -285,6 +398,7 @@ onMounted(() => {
.order-list-page { .order-list-page {
min-height: 100vh; min-height: 100vh;
background-color: var(--van-background-color); background-color: var(--van-background-color);
padding-bottom: 80px; /* 为底部批量操作栏留出空间 */
} }
// Tab // Tab
@ -302,11 +416,11 @@ onMounted(() => {
background: #f5f5f5; background: #f5f5f5;
min-height: calc(100vh - 56px); min-height: calc(100vh - 56px);
} }
:deep(.van-tab) { :deep(.van-tab) {
font-weight: 500; font-weight: 500;
} }
:deep(.van-tab--active) { :deep(.van-tab--active) {
color: var(--van-primary-color); color: var(--van-primary-color);
font-weight: 600; font-weight: 600;
@ -322,7 +436,7 @@ onMounted(() => {
border-radius: 8px; border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
&:active { &:active {
transform: translateY(1px); transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
@ -349,19 +463,19 @@ onMounted(() => {
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-weight: 500; font-weight: 500;
&.pending { &.pending {
background: #FFF3E0; background: #FFF3E0;
color: #FF9800; color: #FF9800;
border: 1px solid #FFE0B2; border: 1px solid #FFE0B2;
} }
&.approved { &.approved {
background: #E8F5E8; background: #E8F5E8;
color: #4CAF50; color: #4CAF50;
border: 1px solid #C8E6C9; border: 1px solid #C8E6C9;
} }
&.rejected { &.rejected {
background: #FFEBEE; background: #FFEBEE;
color: #F44336; color: #F44336;
@ -374,11 +488,11 @@ onMounted(() => {
display: flex; display: flex;
margin-bottom: 8px; margin-bottom: 8px;
align-items: center; align-items: center;
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
.label { .label {
width: 80px; width: 80px;
color: #666666; color: #666666;
@ -386,7 +500,7 @@ onMounted(() => {
font-weight: 400; font-weight: 400;
flex-shrink: 0; flex-shrink: 0;
} }
.value { .value {
flex: 1; flex: 1;
color: #333333; color: #333333;
@ -394,7 +508,7 @@ onMounted(() => {
font-weight: 400; font-weight: 400;
word-break: break-all; word-break: break-all;
line-height: 1.4; line-height: 1.4;
&.amount { &.amount {
color: #1976D2; color: #1976D2;
font-weight: 600; font-weight: 600;
@ -407,7 +521,7 @@ onMounted(() => {
padding: 80px 20px; padding: 80px 20px;
text-align: center; text-align: center;
position: relative; position: relative;
:deep(.van-empty) { :deep(.van-empty) {
.van-empty__image { .van-empty__image {
width: 160px; width: 160px;
@ -415,7 +529,7 @@ onMounted(() => {
filter: drop-shadow(0 8px 32px rgba(102, 126, 234, 0.2)); filter: drop-shadow(0 8px 32px rgba(102, 126, 234, 0.2));
animation: float 3s ease-in-out infinite; animation: float 3s ease-in-out infinite;
} }
.van-empty__description { .van-empty__description {
font-size: 18px; font-size: 18px;
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.9);
@ -442,21 +556,21 @@ onMounted(() => {
background: rgba(255, 255, 255, 0.95); background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease; transition: all 0.3s ease;
&:focus-within { &:focus-within {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
} }
.van-search__content { .van-search__content {
background: transparent; background: transparent;
padding: 12px 16px; padding: 12px 16px;
.van-field__control { .van-field__control {
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
&::placeholder { &::placeholder {
color: #999; color: #999;
font-weight: normal; font-weight: normal;
@ -471,4 +585,94 @@ onMounted(() => {
color: #666; color: #666;
margin-right: 8px; margin-right: 8px;
} }
.batch-actions-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 10px 16px;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
.footer-content {
display: flex;
align-items: center;
justify-content: flex-end;
}
.batch-approve-btn {
width: 100%;
}
}
.approval-dialog {
height: 100%;
display: flex;
flex-direction: column;
.dialog-header {
padding: 16px;
border-bottom: 1px solid #ebedf0;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: 500;
}
.dialog-body {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.dialog-footer {
padding: 16px;
border-top: 1px solid #ebedf0;
.footer-buttons {
display: flex;
gap: 10px;
}
}
}
.opinion-tags {
margin-bottom: 16px;
.tags-title {
font-size: 14px;
color: #646566;
margin-bottom: 12px;
font-weight: 500;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
.opinion-tag {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
}
}
}
.opinion-input {
.van-field {
border: 1px solid #ebedf0;
border-radius: 8px;
background: #f7f8fa;
}
}
</style> </style>