Files
jurong_circle_frontdesk/src/views/Transfers.vue

1081 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="transfers-page">
<!-- 导航栏 -->
<nav class="navbar">
<div class="nav-center">
<h1 class="nav-title">货款管理</h1>
</div>
</nav>
<!-- 待转账 -->
<div class="pending-transfers" v-if="pendingAllocations.length > 0 || pendingTransfers.length > 0">
<el-card>
<template #header>
<div class="card-header">
<span>待转账</span>
<el-badge :value="pendingAllocations.length + pendingTransfers.length" class="badge" />
</div>
</template>
<div class="transfer-list">
<!-- 我要转给别人的分配 -->
<div
v-for="allocation in pendingAllocations"
:key="'allocation-' + allocation.id"
class="transfer-item pending"
:class="{ 'timeout-item': !allocation.can_transfer }"
@click="router.push(`/mymatching`)"
>
<div class="transfer-info">
<div class="transfer-header">
<span class="from-user"></span>
<el-icon class="arrow"><Right /></el-icon>
<span class="to-user">{{ allocation.to_user_real_name || allocation.to_real_name }}</span>
<span class="amount">¥{{ allocation.amount }}</span>
<el-tag
:type="getOrderStatusType(allocation.order_status)"
size="small"
>
{{ getOrderStatusText(allocation.order_status) }}
</el-tag>
</div>
<div class="transfer-details">
<p class="description">匹配订单 {{ allocation.matching_order_id }} - {{ allocation.cycle_number }}</p>
<p class="time">创建时间{{ formatTime(allocation.created_at) }}</p>
<!-- 待出款状态 -->
<div v-if="allocation.status === 'pending'" class="deadline-info">
<span class="deadline-label">转账截止</span>
<span class="deadline-time" :class="allocation.time_status">
{{ formatTime(allocation.deadline) }}
</span>
<span class="time-left" :class="allocation.time_status">
({{ allocation.time_left }})
</span>
</div>
<!-- 已出款状态 -->
<div v-if="allocation.status === 'confirmed'" class="return-info">
<p class="confirmed-time">出款时间{{ formatTime(allocation.confirmed_at) }}</p>
<div class="return-deadline-info">
<span class="return-time" :class="getReturnTimeStatus(allocation)">
{{ formatTime(allocation.can_return_after) }}
</span>
</div>
</div>
<!-- 超时警告 -->
<div v-if="!allocation.can_transfer" class="timeout-warning">
<el-icon><Warning /></el-icon>
<span class="warning-text">{{ allocation.timeout_reason }}</span>
</div>
</div>
</div>
</div>
<!-- 别人转给我的进行中转账 -->
<div
v-for="transfer in pendingTransfers"
:key="'transfer-' + transfer.id"
class="transfer-item pending"
>
<div class="transfer-info">
<div class="transfer-header">
<span class="from-user">{{ transfer.from_real_name || transfer.from_username }}</span>
<el-icon class="arrow"><Right /></el-icon>
<span class="to-user"></span>
<span class="amount">¥{{ transfer.amount }}</span>
<el-tag type="warning" size="small">
进行中
</el-tag>
</div>
<div class="transfer-details">
<p class="description">{{ transfer.description || '转账' }}</p>
<p class="time">创建时间{{ formatTime(transfer.created_at) }}</p>
<p class="type">类型{{ getTransferTypeText(transfer.transfer_type) }}</p>
</div>
</div>
</div>
</div>
</el-card>
</div>
<!-- 转账记录 -->
<div class="transfer-history">
<el-card>
<template #header>
<div class="card-header">
<span>转账记录</span>
</div>
<!-- 状态筛选导航栏 -->
<div class="status-filter-nav">
<div
class="filter-item"
:class="{ active: statusFilter === '' }"
@click="statusFilter = ''"
>
<span class="filter-text">全部</span>
<!-- <span class="filter-count">{{ pagination.total }}</span> -->
</div>
<div
class="filter-item"
:class="{ active: statusFilter === 'pending' }"
@click="statusFilter = 'pending'"
>
<span class="filter-text">待确认</span>
<!-- <span class="filter-count">{{ pendingCount }}</span> -->
</div>
<div
class="filter-item"
:class="{ active: statusFilter === 'confirmed' }"
@click="statusFilter = 'confirmed'"
>
<span class="filter-text">已确认</span>
<!-- <span class="filter-count">{{ confirmedCount }}</span> -->
</div>
<!-- <div
class="filter-item"
:class="{ active: statusFilter === 'rejected' }"
@click="statusFilter = 'rejected'"
> -->
<div
class="filter-item"
:class="{ active: statusFilter === 'not_received' }"
@click="statusFilter = 'not_received'"
>
<span class="filter-text">已拒绝</span>
<!-- <span class="filter-count">{{ rejectedCount }}</span> -->
</div>
</div>
</template>
<div class="transfer-list" v-loading="loading">
<div
v-for="transfer in filteredTransferHistory"
:key="transfer.id"
class="transfer-item"
:class="transfer.status"
>
<div class="transfer-info">
<div class="transfer-header">
<span class="from-user">
{{ transfer.from_user_id === userStore.user.id ? '我' : (transfer.from_real_name || transfer.from_username) }}
</span>
<el-icon class="arrow"><Right /></el-icon>
<span class="to-user">
{{ transfer.to_user_id === userStore.user.id ? '我' : (transfer.to_real_name || transfer.to_username) }}
</span>
<span class="amount">¥{{ transfer.amount }}</span>
<el-tag
:type="getStatusType(transfer.status)"
size="small"
>
{{ getStatusText(transfer.status) }}
</el-tag>
</div>
<div class="transfer-details">
<p class="description">{{ transfer.description }}</p>
<p class="time">{{ formatTime(transfer.created_at) }}</p>
<p class="type">类型{{ getTransferTypeText(transfer.transfer_type) }}</p>
<div v-if="transfer.voucher_url" class="voucher">
<el-image
:src="formatImageUrl(transfer.voucher_url)"
:preview-src-list="[formatImageUrl(transfer.voucher_url)]"
class="voucher-image"
fit="cover"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</div>
<!-- 确认收款操作按钮 -->
<div
v-if="transfer.status === 'confirmed' && transfer.to_user_id === userStore.user.id"
class="transfer-actions"
>
<el-button
type="success"
size="small"
@click="showVoucherConfirmDialog(transfer)"
:loading="confirmLoading"
>
确认收款
</el-button>
<el-button
type="danger"
size="small"
@click="confirmNotReceived(transfer.id)"
:loading="confirmLoading"
>
拒绝收款
</el-button>
</div>
</div>
</div>
</div>
<div v-if="filteredTransferHistory.length === 0" class="empty-state">
<el-empty :description="'暂无转账记录'" />
</div>
</div>
<!-- 分页 -->
<div class="pagination" v-if="pagination.total > 0">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:page-sizes="[10, 20, 50]"
:total="pagination.total"
:layout="paginationLayout"
:small="isMobile"
@size-change="loadTransferHistory"
@current-change="loadTransferHistory"
/>
</div>
</el-card>
</div>
<!-- 转账凭证查看对话框 -->
<el-dialog
v-model="showVoucherDialog"
title="确认收款"
width="90%"
:style="{ maxWidth: '600px' }"
>
<div class="voucher-dialog-content">
<div class="transfer-info">
<h4>转账信息</h4>
<div class="info-row">
<span class="label">转账金额</span>
<span class="value amount">¥{{ currentTransfer.amount }}</span>
</div>
<div class="info-row">
<span class="label">转账说明</span>
<span class="value">{{ currentTransfer.description }}</span>
</div>
<div class="info-row">
<span class="label">转账时间</span>
<span class="value">{{ formatTime(currentTransfer.created_at) }}</span>
</div>
<div class="info-row">
<span class="label">转账类型</span>
<span class="value">{{ getTransferTypeText(currentTransfer.transfer_type) }}</span>
</div>
</div>
<div class="voucher-section" v-if="currentTransfer.voucher_url">
<h4>转账凭证</h4>
<div class="voucher-image-container">
<el-image
:src="formatImageUrl(currentTransfer.voucher_url)"
:preview-src-list="[formatImageUrl(currentTransfer.voucher_url)]"
class="voucher-preview"
fit="contain"
>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
<p>凭证加载失败</p>
</div>
</template>
</el-image>
</div>
</div>
<div v-else class="no-voucher">
<el-icon><Picture /></el-icon>
<p>暂无转账凭证</p>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="showVoucherDialog = false">取消</el-button>
<el-button
type="success"
@click="doConfirmReceived"
:loading="confirmLoading"
>
确认收款
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, watch, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Plus, Right, Picture, Warning } from '@element-plus/icons-vue'
import api, { transferAPI } from '@/utils/api'
import { uploadURL, getImageUrl, getUploadConfig } from '@/config'
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const loading = ref(false)
const submitLoading = ref(false)
const confirmLoading = ref(false)
const showVoucherDialog = ref(false)
const showReturnDialog = ref(false)
const statusFilter = ref('')
const currentTransfer = ref({})
const currentAllocation = ref({})
const pendingTransfers = ref([])
const pendingAllocations = ref([])
const allTransferHistory = ref([])
const filteredTransferHistory = ref([])
const userList = ref([])
const pagination = reactive({
page: 1,
limit: 10,
total: 0
})
const returnForm = reactive({
returnAmount: '',
description: ''
})
// 移动端检测和分页布局
const windowWidth = ref(window.innerWidth)
const isMobile = computed(() => {
return windowWidth.value <= 768
})
const paginationLayout = computed(() => {
if (windowWidth.value <= 480) {
return 'prev, pager, next'
} else if (windowWidth.value <= 768) {
return 'total, prev, pager, next'
} else {
return 'total, sizes, prev, pager, next, jumper'
}
})
// 计算属性
// const pendingCount = computed(() => {
// return allTransferHistory.value.filter(t =>
// t.status === 'pending' &&
// t.to_user_id === userStore.user.id
// ).length
// })
// const confirmedCount = computed(() => {
// return allTransferHistory.value.filter(t =>
// t.status === 'confirmed'
// ).length
// })
// const rejectedCount = computed(() => {
// return allTransferHistory.value.filter(t =>
// t.status === 'rejected'
// ).length
// })
// 窗口大小变化监听
const handleResize = () => {
windowWidth.value = window.innerWidth
}
onMounted(() => {
loadPendingTransfers()
loadPendingAllocations()
loadTransferHistory()
loadUserList()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// 监听状态筛选
watch(statusFilter, () => {
pagination.page = 1
loadTransferHistory()
})
// 方法
const loadPendingTransfers = async () => {
try {
const response = await api.get('/transfers/pending', {
params: {
user_id: userStore.user.id
}
})
if (response.data.success) {
pendingTransfers.value = response.data.data.filter(
transfer => transfer.to_user_id === userStore.user.id && transfer.status === 'pending'
)
}
} catch (error) {
console.error('加载待确认转账失败:', error)
ElMessage.error('加载待确认转账失败')
}
}
const loadPendingAllocations = async () => {
try {
const response = await api.get('/matching/pending-allocations')
if (response.data.success) {
pendingAllocations.value = response.data.data || []
}
} catch (error) {
console.error('加载待处理分配失败:', error)
ElMessage.error('加载待处理分配失败')
}
}
const loadTransferHistory = async () => {
try {
loading.value = true
const params = {
page: pagination.page,
limit: pagination.limit
}
if (statusFilter.value) {
params.status = statusFilter.value
}
const response = await api.get(`/transfers/user/${userStore.user.id}`, { params })
if (response.data.success) {
allTransferHistory.value = response.data.data.transfers || []
filteredTransferHistory.value = allTransferHistory.value
pagination.total = response.data.data.pagination?.total || 0
console.log(allTransferHistory.value)//打印结果
}
} catch (error) {
console.error('加载转账记录失败:', error)
ElMessage.error('加载转账记录失败')
} finally {
loading.value = false
}
}
const loadUserList = async () => {
try {
const response = await api.get('/users/for-transfer')
if (response.data.success) {
userList.value = response.data.data
}
} catch (error) {
console.error('加载用户列表失败:', error)
ElMessage.error('加载用户列表失败')
}
}
const getOrderStatusType = (status) => {
const statusMap = {
'matching': 'warning',
'completed': 'success',
'failed': 'danger',
'cancelled': 'info',
'rejected': 'warning'
}
return statusMap[status] || 'info'
}
const getOrderStatusText = (status) => {
const statusMap = {
'matching': '进行中',
'completed': '已完成',
'failed': '已失败',
'cancelled': '已取消',
'rejected': '已拒绝'
}
return statusMap[status] || '未知状态'
}
const getTransferTypeText = (type) => {
const typeMap = {
'user_to_user': '用户转账',
'initial': '初始转账',
'return': '返还转账',
'system_to_user': '系统转账',
'user_to_system': '系统回收'
}
return typeMap[type] || '未知类型'
}
const getStatusType = (status) => {
const types = {
pending: 'warning',
confirmed: 'success',
rejected: 'danger',
received: 'success',
not_received: 'danger'
}
return types[status] || 'info'
}
const getStatusText = (status) => {
const texts = {
pending: '待确认',
confirmed: '已确认',
rejected: '已拒绝',
received: '已收款',
//not_received: '未收到款',
not_received: '已拒绝',
cancelled: '已取消'
}
return texts[status] || '未知'
}
const formatTime = (time) => {
return new Date(time).toLocaleString('zh-CN')
}
const formatImageUrl = (url) => {
return getImageUrl(url)
}
const getReturnTimeStatus = (allocation) => {
if (!allocation.can_return_after) return 'normal'
const now = new Date()
const canReturnTime = new Date(allocation.can_return_after)
if (now >= canReturnTime) {
return 'can-return'
} else {
return 'waiting'
}
}
const showVoucherConfirmDialog = (transfer) => {
currentTransfer.value = transfer
showVoucherDialog.value = true
}
const doConfirmReceived = async () => {
try {
await ElMessageBox.confirm(
`确定已收到¥${currentTransfer.value.amount}款项吗?`,
'确认收款',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
}
)
confirmLoading.value = true
const response = await transferAPI.confirmReceived(currentTransfer.value.id)
if (response.data.success) {
ElMessage.success('收款确认成功')
showVoucherDialog.value = false
await loadTransferHistory()
await loadPendingTransfers()
}
} catch (error) {
if (error !== 'cancel') {
console.error('确认收款失败:', error)
const errorMsg = error.response?.data?.message ||
error.response?.data?.error?.message ||
'确认收款失败'
ElMessage.error(errorMsg)
}
} finally {
confirmLoading.value = false
}
}
const confirmNotReceived = async (transferId) => {
try {
await ElMessageBox.confirm(
'确定未收到款项吗?',
'确认未收款',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
confirmLoading.value = true
const response = await transferAPI.confirmNotReceived(transferId)
if (response.data.success) {
ElMessage.success(response.data.message || '操作成功')
await loadTransferHistory()
await loadPendingTransfers()
}
} catch (error) {
if (error !== 'cancel') {
console.error('确认未收款失败:', error)
const errorMsg = error.response?.data?.message ||
error.response?.data?.error?.message ||
'操作失败'
ElMessage.error(errorMsg)
}
} finally {
confirmLoading.value = false
}
}
</script>
<style lang="scss" scoped>
.transfers-page {
min-height: 100vh;
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 60px;
background: white;
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border-bottom: 1px solid #ebeef5;
}
.nav-title {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.pending-transfers,
.transfer-history {
margin: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.badge {
margin-left: 10px;
}
.status-filter-nav {
display: flex;
margin-top: 15px;
.filter-item {
flex: 1;
text-align: center;
padding: 8px 0;
cursor: pointer;
transition: all 0.3s;
border-bottom: 2px solid transparent;
display: flex;
flex-direction: column;
align-items: center;
&:hover {
color: #409eff;
}
&.active {
color: #409eff;
border-bottom-color: #409eff;
font-weight: 500;
}
.filter-text {
font-size: 14px;
margin-bottom: 4px;
}
.filter-count {
font-size: 12px;
background-color: #f0f2f5;
border-radius: 12px;
padding: 2px 6px;
min-width: 20px;
display: inline-block;
}
}
}
.transfer-list {
max-height: 400px;
overflow-y: auto;
}
.transfer-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border: 1px solid #ebeef5;
border-radius: 12px;
margin-bottom: 10px;
background: white;
overflow: hidden;
word-wrap: break-word;
}
.transfer-item.pending {
border-color: #e6a23c;
background: #fdf6ec;
}
.transfer-item.confirmed {
border-color: #67c23a;
background: #f0f9eb;
}
.transfer-item.rejected {
border-color: #f56c6c;
background: #fef0f0;
}
.transfer-info {
flex: 1;
}
.transfer-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.from-user,
.to-user {
font-weight: 500;
}
.arrow {
color: #909399;
}
.amount {
font-size: 18px;
font-weight: bold;
color: #409eff;
margin-left: auto;
}
.transfer-details {
color: #666;
font-size: 14px;
}
.transfer-details p {
margin: 4px 0;
}
.voucher {
margin-top: 10px;
}
.voucher-image {
width: 100px;
height: 100px;
border-radius: 12px;
}
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #f5f7fa;
color: #909399;
}
.transfer-actions {
display: flex;
gap: 10px;
}
.timeout-item {
border-left: 3px solid #f56c6c;
background-color: #fef0f0;
}
.deadline-info {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
font-size: 13px;
}
.deadline-label {
color: #606266;
font-weight: 500;
}
.deadline-time {
color: #303133;
font-weight: 500;
}
.deadline-time.urgent {
color: #e6a23c;
}
.deadline-time.expired {
color: #f56c6c;
}
.time-left {
font-size: 12px;
}
.time-left.normal {
color: #67c23a;
}
.time-left.urgent {
color: #e6a23c;
font-weight: 500;
}
.time-left.expired {
color: #f56c6c;
font-weight: 500;
}
.timeout-warning {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 8px 12px;
background-color: #fef0f0;
border: 1px solid #fbc4c4;
border-radius: 4px;
color: #f56c6c;
}
.timeout-warning .el-icon {
font-size: 16px;
color: #f56c6c;
}
.warning-text {
font-size: 12px;
font-weight: 500;
}
.return-info {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #ebeef5;
}
.return-time.can-return {
color: #67c23a;
font-weight: 500;
}
.return-time.waiting {
color: #e6a23c;
}
.return-status.can-return {
color: #67c23a;
font-weight: 500;
}
.return-status.waiting {
color: #e6a23c;
}
.return-action {
margin-top: 10px;
}
.empty-state {
text-align: center;
padding: 40px;
}
/* 凭证对话框样式 */
.voucher-dialog-content {
.transfer-info {
margin-bottom: 20px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
h4 {
margin: 0 0 12px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.info-row {
display: flex;
margin-bottom: 8px;
.label {
min-width: 80px;
color: #606266;
font-weight: 500;
}
.value {
color: #303133;
flex: 1;
&.amount {
color: #e6a23c;
font-weight: 600;
font-size: 16px;
}
}
}
}
.voucher-section {
h4 {
margin: 0 0 12px 0;
color: #303133;
font-size: 16px;
font-weight: 600;
}
.voucher-image-container {
display: flex;
justify-content: center;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 2px dashed #dcdfe6;
.voucher-preview {
max-width: 100%;
max-height: 400px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
}
}
.no-voucher {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 2px dashed #dcdfe6;
color: #909399;
.el-icon {
font-size: 48px;
margin-bottom: 12px;
}
p {
margin: 0;
font-size: 14px;
}
}
}
/* 移动端适配 */
@media (max-width: 768px) {
.status-filter-nav {
overflow-x: auto;
white-space: nowrap;
padding-bottom: 5px;
.filter-item {
min-width: 80px; // 在移动端保持最小宽度
flex: none;
padding: 6px 0;
.filter-text {
font-size: 13px;
}
.filter-count {
font-size: 11px;
}
}
}
}
@media (max-width: 480px) {
.deadline-info {
font-size: 12px;
}
.timeout-warning {
padding: 4px 8px;
}
.warning-text {
font-size: 10px;
}
.return-time-status {
font-size: 12px;
}
.navbar {
padding: 0 10px;
}
.pending-transfers,
.transfer-history {
margin: 10px;
}
.transfer-item {
padding: 10px;
}
.transfer-actions {
gap: 8px;
}
.transfer-actions .el-button {
font-size: 12px;
padding: 6px 12px;
}
.voucher-image {
width: 60px;
height: 60px;
}
}
.el-card {
border-radius: 12px !important;
overflow: hidden;
:deep(.el-card__header) {
border-radius: 12px 12px 0 0 !important;
}
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
</style>