Files
jurong_circle_frontdesk/src/views/Matching.vue
2025-07-26 15:35:53 +08:00

1660 lines
38 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="matching-container">
<div class="header">
<h1>资金循环匹配</h1>
<p class="subtitle">智能匹配循环增值</p>
</div>
<!-- 统计卡片 -->
<div class="stats-cards">
<div class="stat-card">
<div class="stat-number">{{ stats.userStats?.initiated_orders || 0 }}</div>
<div class="stat-label">发起订单</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ stats.userStats?.participated_allocations || 0 }}</div>
<div class="stat-label">参与分配</div>
</div>
<div class="stat-card">
<div class="stat-number">¥{{ formatAmount(stats.userStats?.total_initiated_amount) }}</div>
<div class="stat-label">发起总额</div>
</div>
<div class="stat-card">
<div class="stat-number">¥{{ formatAmount(stats.userStats?.total_participated_amount) }}</div>
<div class="stat-label">参与总额</div>
</div>
</div>
<!-- 操作区域 -->
<div class="action-section">
<div class="create-order-card">
<h3>资金匹配</h3>
<!-- 匹配类型选择 -->
<div class="matching-type-selector">
<div class="type-tabs">
<button
:class="['type-tab', { active: matchingType === 'small' }]"
@click="matchingType = 'small'"
>
小额匹配
</button>
<button
:class="['type-tab', { active: matchingType === 'large' }]"
@click="matchingType = 'large'"
>
大额匹配
</button>
</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>
<span class="value">¥1,000 - ¥5,000</span>
</div>
</div>
<!-- 大额匹配信息 -->
<div v-if="matchingType === 'large'" class="matching-info">
<div class="info-item">
<span class="label">自定义金额:</span>
<div class="custom-amount-input">
<el-input
v-model="customAmount"
type="number"
:min="5000"
:max="50000"
step="100"
placeholder="请输入5000-50000之间的金额"
>
<template #prepend>¥</template>
</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"
>
{{ creating ? '匹配中...' : '开始匹配' }}
</button>
<!-- 小额匹配提示 -->
<div v-if="matchingType === 'small'" class="tips">
<p> 系统将为您匹配3笔转账总金额5000元</p>
<p> 优先匹配已完成出款的用户</p>
<p> 每笔金额随机分配确保资金循环</p>
</div>
<!-- 大额匹配提示 -->
<div v-if="matchingType === 'large'" class="tips">
<p> 金额范围5000-50000</p>
<p> 15000元以下分成3笔随机金额</p>
<p> 15000元以上随机分拆每笔1000-8000</p>
<p> 优先匹配已完成出款的用户</p>
</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>
<span class="cycle">{{ allocation.cycle_number }}</span>
</div>
<div class="allocation-details">
<p>转账给: <strong>{{ allocation.to_user_name }}</strong></p>
<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>
<button
@click="rejectAllocation(allocation.id)"
class="reject-btn"
:disabled="processing"
>
拒绝
</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>
<p>发起人: {{ order.initiator_name }}</p>
<p v-if="!order.is_system_reverse">轮次: {{ order.cycle_count + 1 }}/{{ order.max_cycles }}</p>
<p v-if="order.is_system_reverse" class="system-note">系统自动发起向负余额用户补充资金</p>
<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>
<p><strong>发起人:</strong> {{ selectedOrder.order.initiator_name }}</p>
<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">
<span class="cycle">{{ allocation.cycle_number }}</span>
<span :class="['status', allocation.status]">{{ getStatusText(allocation.status) }}</span>
</div>
<p>{{ allocation.from_user_name }} {{ allocation.to_user_name }}</p>
<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">
<span class="action">{{ getActionText(record.action) }}</span>
<span class="user">{{ record.username }}</span>
<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>
<p><strong>收款人:</strong> {{ transferDialog.toUser.name }}</p>
<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" />
</div>
<div v-if="transferDialog.toUser.alipayQr" class="payment-item">
<h5>支付宝收款码</h5>
<img :src="getImageUrl(transferDialog.toUser.alipayQr)" alt="支付宝收款码" class="qr-code" />
</div>
<div v-if="transferDialog.toUser.unionpayQr" class="payment-item">
<h5>云闪付收款码</h5>
<img :src="getImageUrl(transferDialog.toUser.unionpayQr)" alt="云闪付收款码" class="qr-code" />
</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"
:show-file-list="false"
accept="image/*"
>
<el-button size="small" type="primary">上传凭证</el-button>
</el-upload>
<div v-if="transferDialog.voucher" class="upload-preview">
<img :src="getImageUrl(transferDialog.voucher)" alt="转账凭证" />
</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>
</div>
</template>
<script>
import api from '../utils/api'
import { uploadURL, getImageUrl, getUploadConfig } from '@/config'
export default {
name: 'Matching',
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: '',
bankCard: ''
},
amount: 0,
actualAmount: 0,
voucher: '',
description: ''
}
}
},
async mounted() {
await this.loadData()
},
methods: {
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() {
this.creating = true
try {
// 构建请求参数
const requestData = {
matchingType: this.matchingType
}
// 如果是大额匹配,添加自定义金额
if (this.matchingType === 'large') {
if (!this.isValidCustomAmount) {
this.$message.error('请输入有效的匹配金额5000-50000元')
return
}
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) {
console.error('创建匹配订单失败:', error)
const errorMessage = error.response?.data?.message || '匹配失败,请稍后重试'
// 检查是否是审核相关的错误
if (errorMessage.includes('审核') || errorMessage.includes('上传') || errorMessage.includes('完善')) {
this.$confirm(errorMessage + ',是否前往个人中心完善资料?', '提示', {
confirmButtonText: '前往完善',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$router.push('/profile')
}).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,
bankCard: userPaymentInfo.bank_card
},
amount: expectedAmount,
actualAmount: expectedAmount
}
} catch (error) {
console.error('获取转账信息失败:', error)
this.$message.error('获取转账信息失败')
}
},
/**
* 拒绝分配
* @param {string} allocationId - 分配ID
*/
async rejectAllocation(allocationId) {
const reason = prompt('请输入拒绝原因(可选):')
// 用户点击取消时prompt返回null此时不执行后续操作
if (reason === null) {
return
}
this.processing = true
try {
await api.post(`/matching/reject-allocation/${allocationId}`, {
reason
})
this.$message.success('已拒绝分配')
await this.loadData()
} catch (error) {
console.error('拒绝分配失败:', error)
this.$message.error('拒绝分配失败')
} finally {
this.processing = false
}
},
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: '已确认',
rejected: '已拒绝',
failed: '匹配失败'
}
return statusMap[status] || status
},
getActionText(action) {
const actionMap = {
join: '加入',
confirm: '确认',
reject: '拒绝',
complete: '完成'
}
return actionMap[action] || action
},
formatDate(dateString) {
return new Date(dateString).toLocaleString('zh-CN')
},
/**
* 格式化截止时间显示
* @param {string} dateString - 日期字符串
* @returns {string} 格式化后的时间字符串
*/
formatDeadline(dateString) {
if (!dateString) return ''
const date = new Date(dateString)
const now = new Date()
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const targetDate = new Date(date.getFullYear(), date.getMonth(), date.getDate())
const timeStr = date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
if (targetDate.getTime() === today.getTime()) {
return `今天${timeStr}`
} else if (targetDate.getTime() === today.getTime() + 24 * 60 * 60 * 1000) {
return `明天${timeStr}`
} else {
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
},
/**
* 格式化金额显示,确保数字安全
* @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) {
return '分成3笔随机金额'
} else {
return '随机分拆每笔1000-8000元'
}
},
/**
* 获取大额匹配的预计笔数
* @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以上随机分拆估算笔数范围
const minCount = Math.ceil(amount / 8000) // 按最大单笔8000计算最少笔数
const maxCount = Math.floor(amount / 1000) // 按最小单笔1000计算最多笔数
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 = ''
},
/**
* 获取图片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('只能上传图片文件!')
}
if (!isLt5M) {
this.$message.error('图片大小不能超过 5MB!')
}
return isImage && isLt5M
},
/**
* 上传成功处理
* @param {Object} response - 上传响应
*/
handleUploadSuccess(response) {
if (response.success) {
this.transferDialog.voucher = response.url
this.$message.success('凭证上传成功')
} else {
this.$message.error(response.message || '上传失败')
}
},
/**
* 上传失败处理
* @param {Error} error - 错误信息
*/
handleUploadError(error) {
console.error('上传失败:', error)
this.$message.error('上传失败')
},
/**
* 提交转账
*/
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;
}
.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;
}
.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 {
width: 100%;
padding: 12px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
.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;
}
.confirm-btn,
.reject-btn {
padding: 8px 16px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.confirm-btn {
background: #27ae60;
color: white;
}
.confirm-btn:hover {
background: #229954;
}
.reject-btn {
background: #e74c3c;
color: white;
}
.reject-btn:hover {
background: #c0392b;
}
.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%;
}
/* 转账弹窗样式 */
.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;
}
/* 超时警告样式 */
.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>