OMS_H5/src/views/List/index.vue

655 lines
17 KiB
Vue

<template>
<div class="order-list-page">
<!-- 搜索栏 -->
<div class="search-container">
<van-search v-model="searchKeyword" :placeholder="currentTab === 'pending' ? '搜索订单编号、客户名称' : '搜索合同名称'"
@search="handleSearch" @clear="handleClear" class="custom-search">
<template #left-icon>
<svg class="search-icon" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" />
<path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" />
</svg>
</template>
</van-search>
</div>
<!-- Tab切换 -->
<van-tabs v-model:active="currentTab" @change="onTabChange" class="approval-tabs">
<!-- 待审批Tab -->
<van-tab name="pending" title="待审批">
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<!-- <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了">
<div v-for="order in orderList" :key="order.id" class="order-item" @click="goToDetail(order.id)">
<div class="order-header">
<div class="order-code">{{ order.orderCode }}</div>
<div class="status-tag pending">
待审批
</div>
</div>
<div class="order-info">
<div class="info-row">
<span class="label">项目名称:</span>
<span class="value">{{ order.projectName }}</span>
</div>
<div class="info-row">
<span class="label">客户名称:</span>
<span class="value">{{ order.customerName }}</span>
</div>
<div class="info-row">
<span class="label">订单金额:</span>
<span class="value amount">{{ formatAmount(order.shipmentAmount) }}</span>
</div>
<div class="info-row">
<span class="label">创建时间:</span>
<span class="value">{{ formatDate(order.createTime, 'YYYY-MM-DD HH:mm') }}</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!loading && orderList.length === 0" class="empty-state">
<van-empty description="暂无待审批数据" />
</div>
</van-list>
</van-pull-refresh>
</van-tab>
<!-- 已审批Tab -->
<van-tab name="completed" title="已审批">
<van-pull-refresh v-model="completedRefreshing" @refresh="onCompletedRefresh">
<van-list v-model:loading="completedLoading" :finished="completedFinished" finished-text="没有更多了"
@load="onCompletedLoad">
<div v-for="item in completedList" :key="item.todoId" class="order-item"
@click="goToCompletedDetail(item.businessId)">
<div class="order-header">
<div class="order-code">{{ item.businessKey }}</div>
<div class="status-tag" :class="getCompletedStatusClass(item.approveStatus)">
{{ getCompletedStatusText(item.approveStatus) }}
</div>
</div>
<div class="order-info">
<div class="info-row">
<span class="label">合同名称:</span>
<span class="value">{{ item.businessName }}</span>
</div>
<div class="info-row">
<span class="label">流程名称:</span>
<span class="value">{{ item.processName }}</span>
</div>
<div class="info-row">
<span class="label">发起人:</span>
<span class="value">{{ item.applyUserName }}</span>
</div>
<div class="info-row">
<span class="label">审批时间:</span>
<span class="value">{{ item.approveTime }}</span>
</div>
<div v-if="item.approveOpinion" class="info-row">
<span class="label">审批意见:</span>
<span class="value">{{ item.approveOpinion }}</span>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="!completedLoading && completedList.length === 0" class="empty-state">
<van-empty description="暂无已审批数据" />
</div>
</van-list>
</van-pull-refresh>
</van-tab>
</van-tabs>
<!-- 一键审批操作悬浮框 -->
<div v-if="canBatchApprove && 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>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useOrderStore } from '@/store/order'
import { useAuthStore } from '@/store/auth'
import { formatAmount, formatDate } from '@/utils'
import type { ApprovalStatus, ApproveBtn, BatchApprovalParams } from '@/types'
import { showToast, showSuccessToast } from 'vant'
const router = useRouter()
const orderStore = useOrderStore()
const authStore = useAuthStore()
//
const { userInfo } = storeToRefs(authStore)
//
const { orderList, loading, finished } = storeToRefs(orderStore)
//
const { completedList, completedLoading, completedFinished } = storeToRefs(orderStore)
// Tab
const currentTab = ref('pending')
//
const searchKeyword = ref('')
const refreshing = ref(false)
const completedRefreshing = ref(false)
//
const batchApprovalDialogVisible = ref(false)
const batchApprovalOpinion = ref('')
const batchSubmitting = ref(false)
const selectedBatchTag = ref('')
//
const canBatchApprove = computed(() => {
if (!userInfo.value || !Array.isArray(userInfo.value.roles)) {
return true // 如果没有用户信息或角色信息,默认显示
}
// 如果角色名包含“商务”,则不显示
return !userInfo.value.roles.some(role => role.roleName && role.roleName.includes('商务'))
})
// 加载数据
const onLoad = async () => {
try {
await orderStore.loadOrderList(false, "onLoad")
} catch (error) {
console.error('加载订单列表失败:', error)
}
}
// 下拉刷新
const onRefresh = async () => {
try {
await orderStore.loadOrderList(true)
refreshing.value = false
} catch (error) {
refreshing.value = false
console.error('刷新失败:', error)
}
}
// Tab切换处理
const onTabChange = (name: string) => {
currentTab.value = name
searchKeyword.value = ''
if (name === 'completed' && completedList.value.length === 0) {
onCompletedLoad()
}
}
// 已审批列表加载
const onCompletedLoad = async () => {
try {
await orderStore.loadCompletedOrderList()
} catch (error) {
console.error('加载已审批列表失败:', error)
orderStore.completedLoading = false
}
}
// 已审批列表刷新
const onCompletedRefresh = async () => {
try {
await orderStore.loadCompletedOrderList(true)
completedRefreshing.value = false
} catch (error) {
completedRefreshing.value = false
console.error('刷新已审批列表失败:', error)
}
}
// 搜索处理
const handleSearch = async () => {
try {
const keyword = searchKeyword.value.trim()
if (currentTab.value === 'pending') {
await orderStore.searchOrders(keyword)
} else {
await orderStore.searchCompletedOrders(keyword)
}
} catch (error) {
console.error('搜索失败:', error)
}
}
// 清空搜索
const handleClear = async () => {
searchKeyword.value = ''
try {
if (currentTab.value === 'pending') {
await orderStore.searchOrders('')
} else {
await orderStore.searchCompletedOrders('')
}
} catch (error) {
console.error('清空搜索失败:', error)
}
}
// 已审批状态文本转换
const getCompletedStatusText = (status: ApprovalStatus) => {
const statusMap = { 2: '驳回', 3: '通过' }
return statusMap[status] || '提交'
}
// 已审批状态样式类
const getCompletedStatusClass = (status: ApprovalStatus) => {
const classMap = { 2: 'rejected', 3: 'approved' }
return classMap[status] || 'pending'
}
// 跳转到详情页
const goToDetail = (id: number) => {
router.push({ path: `/detail/${id}`, query: { from: 'order' } })
}
// 跳转到已审批详情页
const goToCompletedDetail = (businessId: number) => {
router.push({ 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(async () => {
console.log("1231231231")
orderStore.resetListState()
orderStore.resetCompletedListState()
// 如果已经登录,则确保获取了用户信息
if (authStore.isAuthenticated && !authStore.userInfo) {
await authStore.getInfo()
}
onLoad()
})
</script>
<style lang="scss" scoped>
.order-list-page {
min-height: 100vh;
background-color: var(--van-background-color);
padding-bottom: 80px;
/* 为底部批量操作栏留出空间 */
}
// Tab相关样式
.approval-tabs {
:deep(.van-tabs__wrap) {
position: sticky;
top: 0;
z-index: 10;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
:deep(.van-tabs__content) {
padding: 16px;
background: #f5f5f5;
min-height: calc(100vh - 56px);
}
:deep(.van-tab) {
font-weight: 500;
}
:deep(.van-tab--active) {
color: var(--van-primary-color);
font-weight: 600;
}
}
.order-item {
background: #ffffff;
margin-bottom: 16px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
border: 1px solid #f0f0f0;
&:active {
transform: translateY(1px);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.order-code {
font-size: 16px;
font-weight: 600;
color: #333333;
}
.status-tag {
font-size: 12px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 500;
&.pending {
background: #FFF3E0;
color: #FF9800;
border: 1px solid #FFE0B2;
}
&.approved {
background: #E8F5E8;
color: #4CAF50;
border: 1px solid #C8E6C9;
}
&.rejected {
background: #FFEBEE;
color: #F44336;
border: 1px solid #FFCDD2;
}
}
.order-info {
.info-row {
display: flex;
margin-bottom: 8px;
align-items: center;
&:last-child {
margin-bottom: 0;
}
.label {
width: 80px;
color: #666666;
font-size: 14px;
font-weight: 400;
flex-shrink: 0;
}
.value {
flex: 1;
color: #333333;
font-size: 14px;
font-weight: 400;
word-break: break-all;
line-height: 1.4;
&.amount {
color: #1976D2;
font-weight: 600;
}
}
}
}
.empty-state {
padding: 80px 20px;
text-align: center;
position: relative;
:deep(.van-empty) {
.van-empty__image {
width: 160px;
height: 160px;
filter: drop-shadow(0 8px 32px rgba(102, 126, 234, 0.2));
animation: float 3s ease-in-out infinite;
}
.van-empty__description {
font-size: 18px;
color: rgba(255, 255, 255, 0.9);
font-weight: 500;
margin-top: 24px;
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
}
}
// 搜索栏容器
.search-container {
position: relative;
z-index: 10;
margin: 16px 20px 20px 20px;
}
// 搜索栏样式优化
:deep(.custom-search) {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
&:focus-within {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
}
.van-search__content {
background: transparent;
padding: 12px 16px;
.van-field__control {
font-size: 15px;
font-weight: 500;
color: #333;
&::placeholder {
color: #999;
font-weight: normal;
}
}
}
}
.search-icon {
width: 20px;
height: 20px;
color: #666;
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>