Files
jurong_circle_frontdesk/src/views/Matching.vue

1934 lines
45 KiB
Vue
Raw Normal View History

2025-07-26 15:35:53 +08:00
<template>
<div class="matching-container">
<div class="header">
2025-08-24 08:32:23 +08:00
<h1>融豆匹配</h1>
2025-08-04 12:32:53 +08:00
<!-- <p class="subtitle">智能匹配循环增值</p> -->
2025-07-26 15:35:53 +08:00
</div>
<!-- 操作区域 -->
<div class="action-section">
<div class="create-order-card">
2025-08-07 09:35:48 +08:00
<div class="card-header">
2025-08-24 08:32:23 +08:00
<h3>融豆匹配</h3>
2025-08-07 09:35:48 +08:00
<div class="toggle-container">
<span class="toggle-label">开启大额匹配</span>
<label class="apple-switch">
<input
type="checkbox"
v-model="tempMatchingType"
true-value="large"
false-value="small"
@change="handleMatchingTypeChange"
>
<span class="apple-switch-slider"></span>
</label>
2025-07-26 15:35:53 +08:00
</div>
</div>
<!-- 小额匹配信息 -->
<div v-if="matchingType === 'small'" class="matching-info">
<div class="info-item">
<span class="label">匹配总额:</span>
<span class="value">¥5,000.00</span>
</div>
<div class="info-item">
<span class="label">分配笔数:</span>
<span class="value">3</span>
</div>
<div class="info-item">
<span class="label">单笔范围:</span>
2025-08-25 13:55:56 +08:00
<span class="value">¥100 - ¥5,000</span>
2025-07-26 15:35:53 +08:00
</div>
</div>
<!-- 大额匹配信息 -->
<div v-if="matchingType === 'large'" class="matching-info">
<div class="info-item">
2025-08-24 08:32:23 +08:00
<span class="label">购买数量:</span>
2025-07-26 15:35:53 +08:00
<div class="custom-amount-input">
<el-input
v-model="customAmount"
type="number"
:min="5000"
:max="50000"
step="100"
placeholder="请输入5000-50000之间的金额"
>
2025-09-03 11:00:08 +08:00
<template #prepend><img src='/imgs/profile/rongdou.png' alt="融豆" class="bean-image"></template>
2025-07-26 15:35:53 +08:00
</el-input>
</div>
</div>
<div class="info-item">
<span class="label">分配规则:</span>
<span class="value">{{ getLargeMatchingRule() }}</span>
</div>
<div class="info-item">
<span class="label">预计笔数:</span>
<span class="value">{{ getLargeMatchingCount() }}</span>
</div>
</div>
<button
@click="createOrder"
:disabled="creating || (matchingType === 'large' && !isValidCustomAmount)"
class="create-btn"
>
2025-08-24 08:32:23 +08:00
{{ creating ? '匹配中...' : '获得融豆' }}
2025-07-26 15:35:53 +08:00
</button>
<!-- 小额匹配提示 -->
<div v-if="matchingType === 'small'" class="tips">
2025-08-24 08:32:23 +08:00
<p> 系统将为您匹配3笔订单总获取5000融豆</p>
<p> 优先匹配拥有融豆并自愿出售的用户</p>
2025-08-04 12:32:53 +08:00
<!-- <p> 每笔金额随机分配确保货款循环</p> -->
2025-07-26 15:35:53 +08:00
</div>
<!-- 大额匹配提示 -->
<div v-if="matchingType === 'large'" class="tips">
2025-08-24 08:32:23 +08:00
<p> 融豆数量5000-50000</p>
<p> 15000以下分成多笔随机融豆</p>
<p> 15000以上随机分拆每笔100-10000</p>
<p> 优先匹配拥有融豆并自愿出售的用户</p>
2025-07-26 15:35:53 +08:00
</div>
</div>
</div>
<!-- 待处理分配 -->
<div class="pending-section" v-if="pendingAllocations.length > 0">
<h3>待处理分配</h3>
<div class="allocation-list">
<div
v-for="allocation in pendingAllocations"
:key="allocation.id"
class="allocation-card"
>
<div class="allocation-info">
<div class="allocation-header">
<span class="order-id">订单 #{{ allocation.matching_order_id }}</span>
2025-08-25 10:47:18 +08:00
<!-- <span class="cycle">{{ allocation.cycle_number }}</span> -->
2025-07-26 15:35:53 +08:00
</div>
<div class="allocation-details">
2025-07-31 13:54:37 +08:00
<p>转账给: <strong>{{ allocation.to_user_real_name }}</strong></p>
2025-07-26 15:35:53 +08:00
<p>金额: <strong class="amount">¥{{ allocation.amount }}</strong></p>
<p>总金额: ¥{{ allocation.total_amount }}</p>
<p class="deadline-info">
转账时效:
<span :class="['time-left', allocation.time_status]">
{{ allocation.time_left }}
</span>
<span class="deadline-time">({{ formatDeadline(allocation.deadline) }})</span>
</p>
<div v-if="!allocation.can_transfer" class="timeout-warning">
<i class="el-icon-warning"></i>
<span class="warning-text">{{ allocation.timeout_reason }}</span>
</div>
</div>
</div>
<div class="allocation-actions">
<button
@click="confirmAllocation(allocation.id, allocation.amount)"
class="confirm-btn"
:disabled="processing || !allocation.can_transfer"
:title="!allocation.can_transfer ? allocation.timeout_reason : ''"
>
{{ allocation.can_transfer ? '确认转账' : '无法转账' }}
</button>
</div>
</div>
</div>
</div>
<!-- 我的匹配订单 -->
<div class="orders-section">
<h3>我的匹配订单</h3>
<div class="orders-list">
<div
v-for="order in matchingOrders"
:key="order.id"
class="order-card"
@click="viewOrderDetail(order.id)"
>
<div class="order-header">
<span class="order-id">#{{ order.id }}</span>
<span v-if="order.is_system_reverse" class="system-reverse-tag">系统反向匹配</span>
<span :class="['status', order.status]">{{ getStatusText(order.status) }}</span>
</div>
<div class="order-info">
<p>金额: ¥{{ order.amount }}</p>
2025-08-04 12:32:53 +08:00
<p>发起人: {{ order.initiator_real_name }}</p>
2025-07-26 15:35:53 +08:00
<p v-if="!order.is_system_reverse">轮次: {{ order.cycle_count + 1 }}/{{ order.max_cycles }}</p>
2025-08-04 12:32:53 +08:00
<p v-if="order.is_system_reverse" class="system-note">系统自动发起向负余额用户补充货款</p>
2025-07-26 15:35:53 +08:00
<p>创建时间: {{ formatDate(order.created_at) }}</p>
</div>
</div>
</div>
<div v-if="matchingOrders.length === 0" class="empty-state">
<p>暂无匹配订单</p>
</div>
</div>
<!-- 订单详情弹窗 -->
<div v-if="showOrderDetail" class="modal-overlay" @click="closeOrderDetail">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>订单详情 #{{ selectedOrder?.order?.id }}</h3>
<button @click="closeOrderDetail" class="close-btn">×</button>
</div>
<div class="modal-body" v-if="selectedOrder">
<div class="order-summary">
<p><strong>状态:</strong> {{ getStatusText(selectedOrder.order.status) }}</p>
<p><strong>金额:</strong> ¥{{ selectedOrder.order.amount }}</p>
2025-08-04 12:32:53 +08:00
<p><strong>发起人:</strong> {{ selectedOrder.order.initiator_real_name }}</p>
2025-07-26 15:35:53 +08:00
<p><strong>轮次:</strong> {{ selectedOrder.order.cycle_count + 1 }}/{{ selectedOrder.order.max_cycles }}</p>
</div>
<div class="allocations-section">
<h4>分配详情</h4>
<div class="allocation-timeline">
<div
v-for="allocation in selectedOrder.allocations"
:key="allocation.id"
class="timeline-item"
>
<div class="timeline-content">
<div class="timeline-header">
2025-08-25 10:47:18 +08:00
<!-- <span class="cycle">{{ allocation.cycle_number }}</span> -->
2025-07-26 15:35:53 +08:00
<span :class="['status', allocation.status]">{{ getStatusText(allocation.status) }}</span>
</div>
2025-08-04 12:32:53 +08:00
<p>{{ allocation.from_user_real_name }} {{ allocation.to_user_real_name }}</p>
2025-07-26 15:35:53 +08:00
<p class="amount">¥{{ allocation.amount }}</p>
</div>
</div>
</div>
</div>
<div class="records-section">
<h4>操作记录</h4>
<div class="records-list">
<div
v-for="record in selectedOrder.records"
:key="record.id"
class="record-item"
>
<div class="record-info">
2025-08-25 13:55:56 +08:00
<span class="action" @click="$router.push('/transfers')">{{ getActionText(record.action) }}</span>
2025-08-04 12:32:53 +08:00
<span class="user">{{ record.real_name }}</span>
2025-07-26 15:35:53 +08:00
<span class="time">{{ formatDate(record.created_at) }}</span>
</div>
<div v-if="record.amount" class="record-amount">¥{{ record.amount }}</div>
<div v-if="record.note" class="record-note">{{ record.note }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 转账弹窗 -->
<el-dialog
v-model="transferDialog.visible"
title="确认转账"
width="90%"
:style="{ maxWidth: '600px' }"
@close="closeTransferDialog"
>
<div class="transfer-dialog-content">
<!-- 转账信息 -->
<div class="transfer-info">
<h4>转账信息</h4>
2025-08-01 09:33:46 +08:00
<p><strong>收款人:</strong> {{ transferDialog.toUser.to_user_real_name }}</p>
2025-07-26 15:35:53 +08:00
<p><strong>转账金额:</strong> ¥{{ transferDialog.amount }}</p>
</div>
<!-- 收款码展示 -->
<div class="payment-codes">
<h4>收款方式</h4>
<div class="payment-grid">
<div v-if="transferDialog.toUser.wechatQr" class="payment-item">
<h5>微信收款码</h5>
<!-- <img :src="getImageUrl(transferDialog.toUser.wechatQr)" alt="微信收款码" class="qr-code" /> -->
<el-image
:src="getImageUrl(transferDialog.toUser.wechatQr)"
:preview-src-list="[getImageUrl(transferDialog.toUser.wechatQr)]"
class="qr-code"
fit="cover"
>
<template #error>
<div>
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
2025-07-26 15:35:53 +08:00
</div>
<div v-if="transferDialog.toUser.alipayQr" class="payment-item">
<h5>支付宝收款码</h5>
<!-- <img :src="getImageUrl(transferDialog.toUser.alipayQr)" alt="支付宝收款码" class="qr-code" /> -->
<el-image
:src="getImageUrl(transferDialog.toUser.alipayQr)"
:preview-src-list="[getImageUrl(transferDialog.toUser.alipayQr)]"
class="qr-code"
fit="cover"
>
<template #error>
<div>
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
2025-07-26 15:35:53 +08:00
</div>
<div v-if="transferDialog.toUser.unionpayQr" class="payment-item">
<h5>云闪付收款码</h5>
<!-- <img :src="getImageUrl(transferDialog.toUser.unionpayQr)" alt="云闪付收款码" class="qr-code" /> -->
<el-image
:src="getImageUrl(transferDialog.toUser.unionpayQr)"
:preview-src-list="[getImageUrl(transferDialog.toUser.unionpayQr)]"
class="qr-code"
fit="cover"
>
<template #error>
<div>
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
2025-07-26 15:35:53 +08:00
</div>
<div v-if="transferDialog.toUser.bankCard" class="payment-item">
<h5>银行账号</h5>
<p class="bank-card">{{ transferDialog.toUser.bankCard }}</p>
</div>
</div>
</div>
<!-- 转账表单 -->
<div class="transfer-form">
<h4>转账确认</h4>
<el-form label-width="100px">
<el-form-item label="转账金额">
<el-input
v-model="transferDialog.actualAmount"
readonly
disabled
>
<template #prepend>¥</template>
</el-input>
</el-form-item>
<el-form-item label="转账说明">
<el-input
v-model="transferDialog.description"
type="textarea"
placeholder="请输入转账说明(可选)"
:rows="3"
/>
</el-form-item>
<el-form-item label="转账凭证">
<el-upload
:action="uploadUrl"
:headers="uploadHeaders"
:before-upload="beforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
2025-08-30 15:09:43 +08:00
:on-progress="handleUploadProgress"
2025-07-26 15:35:53 +08:00
:show-file-list="false"
2025-08-30 15:09:43 +08:00
:disabled="transferDialog.uploading"
2025-07-26 15:35:53 +08:00
accept="image/*"
>
2025-08-30 15:09:43 +08:00
<el-button
size="small"
type="primary"
:loading="transferDialog.uploading"
:disabled="transferDialog.uploading"
>
{{ transferDialog.uploading ? '上传中...' : '上传凭证' }}
</el-button>
2025-07-26 15:35:53 +08:00
</el-upload>
<div v-if="transferDialog.voucher" class="upload-preview">
<el-image
:src="getImageUrl(transferDialog.voucher)"
:preview-src-list="[getImageUrl(transferDialog.voucher)]"
alt="转账凭证"
2025-09-02 17:05:49 +08:00
fit="contain"
2025-08-30 15:09:43 +08:00
:lazy="true"
>
2025-08-30 15:09:43 +08:00
<template #placeholder>
<div class="image-slot">
<el-icon class="is-loading"><Loading /></el-icon>
<span>加载中...</span>
</div>
</template>
<template #error>
2025-08-30 15:09:43 +08:00
<div class="image-slot error">
<el-icon><Picture /></el-icon>
2025-08-30 15:09:43 +08:00
<span>图片加载失败</span>
<el-button size="small" type="text" @click="reloadImage">重新加载</el-button>
</div>
</template>
</el-image>
2025-08-30 15:09:43 +08:00
<div class="upload-actions">
<el-button size="small" type="danger" @click="removeVoucher">删除</el-button>
</div>
2025-07-26 15:35:53 +08:00
</div>
<div v-else class="upload-tip">
<span class="tip-text">* 必须上传转账凭证才能确认转账</span>
</div>
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="closeTransferDialog">取消</el-button>
<el-button
type="primary"
@click="submitTransfer"
:loading="processing"
:disabled="!transferDialog.voucher"
>
确认转账
</el-button>
</span>
</template>
</el-dialog>
2025-08-07 09:35:48 +08:00
<!-- 大额匹配确认弹窗 -->
<el-dialog
v-model="showLargeMatchingConfirm"
title="开启大额匹配"
width="90%"
:style="{ maxWidth: '500px' }"
>
<div class="confirm-dialog-content">
<p>确认要开启大额匹配吗</p>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelLargeMatching">取消</el-button>
<el-button type="primary" @click="confirmLargeMatching">
确认开启
</el-button>
</span>
</template>
</el-dialog>
2025-07-26 15:35:53 +08:00
</div>
</template>
<script>
import api from '../utils/api'
import { uploadURL, getImageUrl, getUploadConfig } from '@/config'
2025-08-30 15:09:43 +08:00
import { Picture, Loading } from '@element-plus/icons-vue'
2025-07-26 15:35:53 +08:00
export default {
name: 'Matching',
components: {
2025-08-30 15:09:43 +08:00
Picture,
Loading
},
2025-07-26 15:35:53 +08:00
data() {
return {
stats: {
userStats: null
},
creating: false,
processing: false,
pendingAllocations: [],
matchingOrders: [],
showOrderDetail: false,
selectedOrder: null,
matchingType: 'small', // 匹配类型small(小额) 或 large(大额)
customAmount: '', // 大额匹配自定义金额
transferDialog: {
visible: false,
allocationId: null,
toUser: {
id: null,
name: '',
wechatQr: '',
alipayQr: '',
unionpayQr: '',
2025-08-01 09:33:46 +08:00
bankCard: '',
to_user_real_name:'',
2025-07-26 15:35:53 +08:00
},
amount: 0,
actualAmount: 0,
voucher: '',
2025-08-30 15:09:43 +08:00
description: '',
uploading: false // 添加上传状态
2025-08-07 09:35:48 +08:00
},
showLargeMatchingConfirm: false,
tempMatchingType: 'small' // 临时存储切换前的类型
2025-07-26 15:35:53 +08:00
}
},
async mounted() {
await this.loadData()
},
methods: {
2025-08-07 09:35:48 +08:00
handleMatchingTypeChange() {
if (this.tempMatchingType === 'large') {
// 如果要切换到大额匹配,显示确认对话框
this.showLargeMatchingConfirm = true
} else {
// 直接切换到小额匹配
this.matchingType = this.tempMatchingType
}
},
confirmLargeMatching() {
this.matchingType = 'large'
this.showLargeMatchingConfirm = false
this.$message.success('已开启大额匹配模式')
},
cancelLargeMatching() {
this.tempMatchingType = 'small' // 重置开关状态
this.showLargeMatchingConfirm = false
},
2025-07-26 15:35:53 +08:00
async loadData() {
try {
await Promise.all([
this.loadStats(),
this.loadPendingAllocations(),
this.loadMatchingOrders()
])
} catch (error) {
console.error('加载数据失败:', error)
this.$message.error('加载数据失败')
}
},
async loadStats() {
try {
const response = await api.get('/matching/stats')
this.stats = response.data.data || {}
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
async loadPendingAllocations() {
try {
const response = await api.get('/matching/pending-allocations')
this.pendingAllocations = response.data.data || []
} catch (error) {
console.error('加载待处理分配失败:', error)
}
},
async loadMatchingOrders() {
try {
const response = await api.get('/matching/my-orders')
this.matchingOrders = response.data.data || []
} catch (error) {
console.error('加载匹配订单失败:', error)
}
},
/**
* 创建匹配订单
*/
async createOrder() {
try {
2025-07-31 13:54:37 +08:00
// 构建确认信息
let confirmMessage = ''
if (this.matchingType === 'small') {
confirmMessage = '确定要开始小额匹配吗?\n\n匹配成功后将生成3笔转账分配。'
} else {
if (!this.isValidCustomAmount) {
this.$message.error('请输入有效的匹配金额5000-50000元')
return
}
const amount = parseFloat(this.customAmount)
const count = this.getLargeMatchingCount()
confirmMessage = `确定要开始大额匹配吗?\n\n匹配金额${amount}\n将生成${count}笔转账分配`
}
// 二次确认对话框
await this.$confirm(confirmMessage, '确认匹配', {
confirmButtonText: '确认匹配',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: false
})
this.creating = true
2025-07-26 15:35:53 +08:00
// 构建请求参数
const requestData = {
matchingType: this.matchingType
}
// 如果是大额匹配,添加自定义金额
if (this.matchingType === 'large') {
requestData.customAmount = parseFloat(this.customAmount)
}
await api.post('/matching/create', requestData)
const successMessage = this.matchingType === 'small'
? '小额匹配成功已为您生成3笔转账分配'
: `大额匹配成功!已为您生成${this.getLargeMatchingCount()}笔转账分配`
this.$message.success(successMessage)
await this.loadData()
} catch (error) {
2025-07-31 13:54:37 +08:00
// 如果用户取消确认,不显示错误信息
if (error === 'cancel') {
return
}
2025-07-26 15:35:53 +08:00
console.error('创建匹配订单失败:', error)
const errorMessage = error.response?.data?.message || '匹配失败,请稍后重试'
// 检查是否是审核相关的错误
if (errorMessage.includes('审核') || errorMessage.includes('上传') || errorMessage.includes('完善')) {
this.$confirm(errorMessage + ',是否前往个人中心完善资料?', '提示', {
confirmButtonText: '前往完善',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
2025-08-08 09:41:19 +08:00
this.$router.push('/myprofile')
2025-07-26 15:35:53 +08:00
}).catch(() => {})
} else {
this.$message.error(errorMessage)
}
} finally {
this.creating = false
}
},
/**
* 确认分配并创建转账记录
* @param {number} allocationId - 分配ID
* @param {number} expectedAmount - 预期转账金额
*/
async confirmAllocation(allocationId, expectedAmount) {
try {
// 获取分配详情和收款用户信息
const allocationResponse = await api.get(`/matching/allocation/${allocationId}`)
const allocation = allocationResponse.data.data
// 获取收款用户的收款码信息
const userResponse = await api.get(`/users/payment-info/${allocation.to_user_id}`)
const userPaymentInfo = userResponse.data.data
// 设置转账弹窗数据
this.transferDialog = {
visible: true,
allocationId: allocationId,
toUser: {
id: allocation.to_user_id,
name: allocation.to_user_name,
wechatQr: userPaymentInfo.wechat_qr,
alipayQr: userPaymentInfo.alipay_qr,
unionpayQr: userPaymentInfo.unionpay_qr,
2025-08-01 09:33:46 +08:00
bankCard: userPaymentInfo.bank_card,
to_user_real_name: allocation.to_user_real_name,
2025-07-26 15:35:53 +08:00
},
amount: expectedAmount,
actualAmount: expectedAmount
}
} catch (error) {
console.error('获取转账信息失败:', error)
this.$message.error('获取转账信息失败')
}
},
2025-07-31 13:54:37 +08:00
2025-07-26 15:35:53 +08:00
async viewOrderDetail(orderId) {
try {
const response = await api.get(`/matching/order/${orderId}`)
this.selectedOrder = response.data.data
this.showOrderDetail = true
} catch (error) {
console.error('获取订单详情失败:', error)
this.$message.error('获取订单详情失败')
}
},
closeOrderDetail() {
this.showOrderDetail = false
this.selectedOrder = null
},
getStatusText(status) {
const statusMap = {
pending: '待处理',
matching: '匹配中',
completed: '已完成',
cancelled: '已取消',
confirmed: '已确认',
2025-08-25 10:47:18 +08:00
failed: '匹配失败',
received: '已收款',
2025-07-26 15:35:53 +08:00
}
return statusMap[status] || status
},
getActionText(action) {
const actionMap = {
join: '加入',
confirm: '确认',
2025-08-25 10:47:18 +08:00
complete: '完成',
confirm: '确认收款',
2025-07-26 15:35:53 +08:00
}
return actionMap[action] || action
},
formatDate(dateString) {
2025-08-24 10:41:24 +08:00
return this.$dayjs(dateString).format('YYYY-MM-DD HH:mm:ss')
2025-07-26 15:35:53 +08:00
},
/**
* 格式化截止时间显示
* @param {string} dateString - 日期字符串
* @returns {string} 格式化后的时间字符串
*/
formatDeadline(dateString) {
if (!dateString) return ''
2025-08-24 10:41:24 +08:00
const date = this.$dayjs(dateString)
const now = this.$dayjs()
2025-07-26 15:35:53 +08:00
2025-08-24 10:41:24 +08:00
const timeStr = date.format('HH:mm')
2025-07-26 15:35:53 +08:00
2025-08-24 10:41:24 +08:00
if (date.isSame(now, 'day')) {
2025-07-26 15:35:53 +08:00
return `今天${timeStr}`
2025-08-24 10:41:24 +08:00
} else if (date.isSame(now.add(1, 'day'), 'day')) {
2025-07-26 15:35:53 +08:00
return `明天${timeStr}`
} else {
2025-08-24 10:41:24 +08:00
return date.format('MM-DD HH:mm')
2025-07-26 15:35:53 +08:00
}
},
/**
* 格式化金额显示确保数字安全
* @param {number|string|null|undefined} amount - 金额值
* @returns {string} 格式化后的金额字符串
*/
formatAmount(amount) {
const num = parseFloat(amount)
return isNaN(num) ? '0.00' : num.toFixed(2)
},
/**
* 获取大额匹配的分配规则描述
* @returns {string} 规则描述
*/
getLargeMatchingRule() {
const amount = parseFloat(this.customAmount) || 0
if (amount <= 0) {
return '请输入金额'
} else if (amount < 5000) {
return '金额不能少于5000元'
} else if (amount > 50000) {
return '金额不能超过50000元'
} else if (amount <= 15000) {
2025-08-22 10:23:43 +08:00
return '分成多笔随机金额'
2025-07-26 15:35:53 +08:00
} else {
2025-08-07 09:35:48 +08:00
return '随机分拆每笔100-10000元'
2025-07-26 15:35:53 +08:00
}
},
/**
* 获取大额匹配的预计笔数
* @returns {string} 预计笔数描述
*/
getLargeMatchingCount() {
const amount = parseFloat(this.customAmount) || 0
if (amount <= 0 || amount < 5000 || amount > 50000) {
return '0'
} else if (amount <= 15000) {
return '3'
} else {
// 15000以上随机分拆估算笔数范围
2025-08-07 09:35:48 +08:00
const minCount = Math.ceil(amount / 10000) // 按最大单笔10000计算最少笔数
const maxCount = Math.floor(amount / 100) // 按最小单笔100计算最多笔数
2025-07-26 15:35:53 +08:00
return `${minCount}-${Math.min(maxCount, 10)}` // 限制最大显示笔数为10
}
},
/**
* 关闭转账弹窗
*/
closeTransferDialog() {
this.transferDialog.visible = false
this.transferDialog.allocationId = null
this.transferDialog.toUser = {
id: null,
name: '',
wechatQr: '',
alipayQr: '',
unionpayQr: '',
bankCard: ''
}
this.transferDialog.amount = 0
this.transferDialog.actualAmount = 0
this.transferDialog.voucher = ''
this.transferDialog.description = ''
2025-08-30 15:09:43 +08:00
this.transferDialog.uploading = false // 重置上传状态
2025-07-26 15:35:53 +08:00
},
/**
* 获取图片URL
* @param {string} imagePath - 图片路径
* @returns {string} 完整的图片URL
*/
getImageUrl(imagePath) {
return getImageUrl(imagePath)
},
/**
* 上传前验证
* @param {File} file - 上传的文件
* @returns {boolean} 是否通过验证
*/
beforeUpload(file) {
const isImage = file.type.startsWith('image/')
const isLt5M = file.size / 1024 / 1024 < 5
if (!isImage) {
this.$message.error('只能上传图片文件!')
2025-08-30 15:09:43 +08:00
return false
2025-07-26 15:35:53 +08:00
}
if (!isLt5M) {
this.$message.error('图片大小不能超过 5MB!')
2025-08-30 15:09:43 +08:00
return false
2025-07-26 15:35:53 +08:00
}
2025-08-30 15:09:43 +08:00
// 开始上传,设置上传状态
this.transferDialog.uploading = true
return true
},
/**
* 上传进度处理
* @param {Object} event - 进度事件
*/
handleUploadProgress(event) {
// 可以在这里添加进度条显示
console.log('上传进度:', Math.round(event.percent) + '%')
2025-07-26 15:35:53 +08:00
},
/**
* 上传成功处理
* @param {Object} response - 上传响应
*/
handleUploadSuccess(response) {
2025-08-30 15:09:43 +08:00
console.log('上传响应:', response) // 添加调试信息
// 重置上传状态
this.transferDialog.uploading = false
// 兼容不同的响应格式
let success = false
let imageUrl = ''
let message = ''
if (response) {
// 检查多种可能的成功标识
success = response.success === true || response.code === 200 || response.status === 'success'
// 检查多种可能的图片URL字段
imageUrl = response.url || response.data?.url || response.file_url || response.path || response.data?.path
// 检查错误信息
message = response.message || response.msg || response.data?.message
}
if (success && imageUrl) {
this.transferDialog.voucher = imageUrl
2025-07-26 15:35:53 +08:00
this.$message.success('凭证上传成功')
2025-08-30 15:09:43 +08:00
console.log('图片URL已设置:', imageUrl) // 调试信息
2025-07-26 15:35:53 +08:00
} else {
2025-08-30 15:09:43 +08:00
console.error('上传失败,响应数据:', response) // 调试信息
this.$message.error(message || '上传失败,请检查网络连接或联系管理员')
2025-07-26 15:35:53 +08:00
}
},
/**
* 上传失败处理
* @param {Error} error - 错误信息
*/
handleUploadError(error) {
2025-08-30 15:09:43 +08:00
// 重置上传状态
this.transferDialog.uploading = false
2025-07-26 15:35:53 +08:00
console.error('上传失败:', error)
2025-08-30 15:09:43 +08:00
this.$message.error('上传失败,请检查网络连接后重试')
},
/**
* 删除已上传的凭证
*/
removeVoucher() {
this.$confirm('确定要删除已上传的凭证吗?', '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.transferDialog.voucher = ''
this.$message.success('凭证已删除')
}).catch(() => {})
},
/**
* 重新加载图片
*/
reloadImage() {
// 强制重新加载图片,通过添加时间戳
const currentVoucher = this.transferDialog.voucher
this.transferDialog.voucher = ''
this.$nextTick(() => {
this.transferDialog.voucher = currentVoucher
})
2025-07-26 15:35:53 +08:00
},
/**
* 提交转账
*/
async submitTransfer() {
// 校验是否上传了转账凭证
if (!this.transferDialog.voucher) {
this.$message.error('请先上传转账凭证')
return
}
const actualAmount = parseFloat(this.transferDialog.actualAmount)
this.processing = true
try {
await api.post(`/matching/confirm-allocation/${this.transferDialog.allocationId}`, {
transferAmount: actualAmount,
description: this.transferDialog.description,
voucher: this.transferDialog.voucher
})
this.$message.success('转账凭证已提交,转账记录已创建')
this.closeTransferDialog()
// 直接跳转到转账记录页面
this.$router.push('/transfers')
} catch (error) {
console.error('确认分配失败:', error)
this.$message.error(error.response?.data?.message || '确认分配失败')
} finally {
this.processing = false
}
}
},
computed: {
/**
* 验证自定义金额是否有效
* @returns {boolean} 金额是否有效
*/
isValidCustomAmount() {
const amount = parseFloat(this.customAmount)
return !isNaN(amount) && amount >= 5000 && amount <= 50000
},
/**
* 上传URL
*/
uploadUrl() {
return uploadURL
},
/**
* 上传请求头
*/
uploadHeaders() {
return getUploadConfig().headers
}
}
}
</script>
<style scoped>
.matching-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
2025-08-07 09:35:48 +08:00
background: linear-gradient(to bottom, #72c9ffae, #f3f3f3);
2025-07-26 15:35:53 +08:00
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
margin-bottom: 10px;
}
.subtitle {
color: #7f8c8d;
font-size: 16px;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #3498db;
margin-bottom: 5px;
}
.stat-label {
color: #7f8c8d;
font-size: 14px;
}
.action-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
2025-08-07 09:35:48 +08:00
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.toggle-container {
display: flex;
align-items: center;
gap: 8px;
}
.toggle-label {
font-size: 12px;
color: #666;
}
.apple-switch {
position: relative;
display: inline-block;
width: 34px;
height: 16px;
}
.apple-switch input {
opacity: 0;
width: 0;
height: 0;
}
.apple-switch-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #e0e0e0;
transition: .4s;
border-radius: 16px;
}
.apple-switch-slider:before {
position: absolute;
content: "";
height: 12px;
width: 12px;
left: 2px;
bottom: 2px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
.apple-switch input:checked + .apple-switch-slider {
background-color: #4CD964;
}
.apple-switch input:checked + .apple-switch-slider:before {
transform: translateX(18px);
}
/* 移除原有的匹配类型选择器样式 */
.matching-type-selector,
.type-tabs,
.type-tab {
display: none;
}
2025-07-26 15:35:53 +08:00
.create-order-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.create-order-card h3 {
margin-bottom: 15px;
color: #2c3e50;
}
.matching-info {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-item .label {
color: #6c757d;
font-weight: 500;
}
.info-item .value {
color: #2c3e50;
font-weight: bold;
font-size: 16px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #34495e;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
}
.create-btn {
2025-08-07 09:35:48 +08:00
width: 50%;
2025-07-26 15:35:53 +08:00
padding: 12px;
2025-08-07 09:35:48 +08:00
background: #0099ff;
2025-07-26 15:35:53 +08:00
color: white;
border: none;
2025-08-07 09:35:48 +08:00
border-radius: 1000px; /* 保持胶囊形状 */
2025-07-26 15:35:53 +08:00
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
2025-08-07 09:35:48 +08:00
display: block; /* 改为块级元素 */
margin: 0 auto; /* 水平居中 */
text-align: center; /* 文字居中 */
2025-07-26 15:35:53 +08:00
}
.create-btn:hover {
background: #2980b9;
}
.create-btn:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.tips {
margin-top: 15px;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
font-size: 12px;
color: #6c757d;
}
.tips p {
margin: 5px 0;
}
.amount {
font-weight: bold;
color: #27ae60;
}
.status.active {
color: #27ae60;
font-weight: bold;
}
.status.inactive {
color: #e74c3c;
font-weight: bold;
}
.pending-section,
.orders-section {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.pending-section h3,
.orders-section h3 {
margin-bottom: 15px;
color: #2c3e50;
}
.allocation-list,
.orders-list {
display: grid;
gap: 15px;
}
.allocation-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border: 1px solid #e1e8ed;
border-radius: 8px;
background: #f8f9fa;
}
.allocation-header {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.order-id,
.cycle {
background: #3498db;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
.allocation-details p {
margin: 5px 0;
font-size: 14px;
}
.deadline-info {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.time-left {
font-weight: bold;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
}
.time-left.normal {
background: #d4edda;
color: #155724;
}
.time-left.urgent {
background: #fff3cd;
color: #856404;
}
.time-left.expired {
background: #f8d7da;
color: #721c24;
animation: blink 1s infinite;
}
.deadline-time {
color: #6c757d;
font-size: 12px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.5; }
}
.allocation-actions {
display: flex;
gap: 10px;
}
2025-07-31 13:54:37 +08:00
.confirm-btn {
2025-07-26 15:35:53 +08:00
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.confirm-btn {
background: #27ae60;
color: white;
}
.confirm-btn:hover {
background: #229954;
}
.order-card {
padding: 15px;
border: 1px solid #e1e8ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.order-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 8px;
}
.system-reverse-tag {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.system-note {
color: #667eea;
font-style: italic;
font-size: 13px;
}
.status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status.pending {
background: #f39c12;
color: white;
}
.status.matching {
background: #3498db;
color: white;
}
.status.completed {
background: #27ae60;
color: white;
}
.status.cancelled {
background: #e74c3c;
color: white;
}
.status.failed {
background: #e74c3c;
color: white;
}
.order-info p {
margin: 5px 0;
font-size: 14px;
color: #34495e;
}
.empty-state {
text-align: center;
padding: 40px;
color: #7f8c8d;
}
/* 匹配类型选择器样式 */
.matching-type-selector {
margin-bottom: 20px;
}
.type-tabs {
display: flex;
background: #f8f9fa;
border-radius: 12px;
padding: 4px;
gap: 4px;
border: 1px solid #e9ecef;
}
.type-tab {
flex: 1;
padding: 12px 20px;
border: none;
background: transparent;
color: #6c757d;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.type-tab:hover {
color: #495057;
background: rgba(52, 144, 220, 0.1);
}
.type-tab.active {
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
font-weight: 600;
box-shadow: 0 2px 8px rgba(52, 144, 220, 0.3);
transform: translateY(-1px);
}
.type-tab.active::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255,255,255,0.2), transparent);
pointer-events: none;
}
.type-tab:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(52, 144, 220, 0.2);
}
.type-tab:active {
transform: translateY(0);
}
/* 自定义金额输入框样式 */
.custom-amount-input {
flex: 1;
margin-left: 10px;
}
.custom-amount-input .el-input {
width: 100%;
}
2025-08-24 08:32:23 +08:00
.bean-image {
width: 20px;
height: 20px;
flex-shrink: 0; /* 防止图片被压缩 */
}
2025-07-26 15:35:53 +08:00
/* 转账弹窗样式 */
.transfer-dialog-content {
max-height: 70vh;
overflow-y: auto;
}
.transfer-info {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.transfer-info h4 {
margin: 0 0 10px 0;
color: #2c3e50;
font-size: 16px;
}
.transfer-info p {
margin: 5px 0;
font-size: 14px;
}
.payment-codes {
margin-bottom: 20px;
}
.payment-codes h4 {
margin: 0 0 15px 0;
color: #2c3e50;
font-size: 16px;
}
.payment-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.payment-item {
text-align: center;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fff;
}
.payment-item h5 {
margin: 0 0 10px 0;
color: #34495e;
font-size: 14px;
}
.qr-code {
width: 150px;
height: 150px;
object-fit: contain;
border: 1px solid #ddd;
border-radius: 4px;
}
.bank-card {
font-family: 'Courier New', monospace;
font-size: 16px;
font-weight: bold;
color: #2c3e50;
background: #f0f0f0;
padding: 10px;
border-radius: 4px;
word-break: break-all;
}
.transfer-form {
border-top: 1px solid #e0e0e0;
padding-top: 20px;
}
.transfer-form h4 {
margin: 0 0 15px 0;
color: #2c3e50;
font-size: 16px;
}
.upload-preview {
margin-top: 10px;
}
.upload-preview img {
max-width: 200px;
max-height: 200px;
border: 1px solid #ddd;
border-radius: 4px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 10px;
max-width: 800px;
max-height: 80vh;
overflow-y: auto;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #e1e8ed;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #7f8c8d;
}
.modal-body {
padding: 20px;
}
.order-summary {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.order-summary p {
margin: 8px 0;
}
.allocations-section,
.records-section {
margin-bottom: 20px;
}
.allocations-section h4,
.records-section h4 {
margin-bottom: 15px;
color: #2c3e50;
}
.timeline-item {
border-left: 3px solid #3498db;
padding-left: 15px;
margin-bottom: 15px;
}
.timeline-content {
background: #f8f9fa;
padding: 10px;
border-radius: 5px;
}
.timeline-header {
display: flex;
gap: 10px;
margin-bottom: 5px;
}
.record-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #e1e8ed;
}
.record-info {
display: flex;
gap: 15px;
align-items: center;
}
.action {
font-weight: bold;
color: #3498db;
}
.user {
color: #2c3e50;
}
.time {
color: #7f8c8d;
font-size: 12px;
}
.record-amount {
font-weight: bold;
color: #27ae60;
}
.record-note {
font-style: italic;
color: #7f8c8d;
font-size: 12px;
}
@media (max-width: 768px) {
.action-section {
grid-template-columns: 1fr;
}
.stats-cards {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.allocation-card {
flex-direction: column;
align-items: flex-start;
gap: 15px;
padding: 15px;
}
.allocation-info {
width: 100%;
}
.allocation-info h4 {
font-size: 14px;
margin-bottom: 8px;
}
.allocation-info p {
font-size: 13px;
margin: 4px 0;
}
.deadline-info {
width: 100%;
margin-top: 10px;
}
.time-left {
font-size: 12px;
}
.deadline-time {
font-size: 11px;
}
.allocation-actions {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.allocation-actions .el-button {
flex: 1;
min-width: 80px;
font-size: 12px;
padding: 8px 12px;
}
.modal-content {
width: 95%;
margin: 10px;
max-height: 90vh;
}
.modal-header {
padding: 15px;
}
.modal-body {
padding: 15px;
}
.order-summary {
padding: 12px;
}
.qr-code {
width: 120px;
height: 120px;
}
.bank-card {
font-size: 14px;
padding: 8px;
}
.record-item {
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 12px 8px;
}
.record-info {
width: 100%;
flex-direction: column;
gap: 4px;
}
.record-amount {
align-self: flex-end;
}
}
/* 上传提示样式 */
.upload-tip {
margin-top: 8px;
}
.tip-text {
color: #f56c6c;
font-size: 12px;
font-style: italic;
}
2025-08-30 15:09:43 +08:00
.upload-preview {
margin-top: 15px;
border: 1px solid #dcdfe6;
border-radius: 8px;
padding: 10px;
background: #fafafa;
}
.upload-preview .el-image {
width: 200px;
height: 150px;
border-radius: 6px;
overflow: hidden;
}
.image-slot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 12px;
}
.image-slot.error {
color: #f56c6c;
}
.image-slot .el-icon {
font-size: 24px;
margin-bottom: 8px;
}
.upload-actions {
margin-top: 10px;
text-align: center;
}
.is-loading {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
2025-07-26 15:35:53 +08:00
/* 超时警告样式 */
.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 i {
font-size: 16px;
color: #f56c6c;
}
.warning-text {
font-size: 12px;
font-weight: 500;
}
/* 禁用状态的确认按钮样式 */
.confirm-btn:disabled {
background-color: #f5f7fa !important;
border-color: #e4e7ed !important;
color: #c0c4cc !important;
cursor: not-allowed !important;
}
.confirm-btn:disabled:hover {
background-color: #f5f7fa !important;
border-color: #e4e7ed !important;
color: #c0c4cc !important;
}
@media (max-width: 480px) {
.stats-cards {
grid-template-columns: 1fr;
}
.allocation-card {
padding: 12px;
}
.allocation-info h4 {
font-size: 13px;
}
.allocation-info p {
font-size: 12px;
}
.time-left {
font-size: 11px;
}
.deadline-time {
font-size: 10px;
}
.allocation-actions .el-button {
font-size: 11px;
padding: 6px 10px;
}
.qr-code {
width: 100px;
height: 100px;
}
.bank-card {
font-size: 12px;
padding: 6px;
}
}
</style>