feat: 信用IEO系统+贷款模块前端

This commit is contained in:
testadmin
2026-04-28 18:52:00 +08:00
parent 406715b41d
commit f3610f801d
3 changed files with 1384 additions and 0 deletions
+483
View File
@@ -0,0 +1,483 @@
<template>
<div>
<indexHeader></indexHeader>
<secondaryHead :text="$t('credit.credit_ieo') || '信用IEO专区'"></secondaryHead>
<div class="cieo-page">
<div class="cieo-wrap">
<!-- Tabs -->
<div class="tab-bar">
<div
v-for="(tab, idx) in tabs"
:key="idx"
class="tab-item"
:class="{ active: currentTab === idx }"
@click="currentTab = idx"
>
{{ tab }}
</div>
</div>
<!-- 项目列表 -->
<div v-if="currentTab === 0 || currentTab === 1" class="project-list">
<div
v-for="item in filteredList"
:key="item.id"
class="ieo-card"
>
<div class="card-header">
<img v-if="item.thumb_image" :src="item.thumb_image" class="card-icon" />
<div class="card-info">
<div class="card-title">
{{ item.name }}
<span class="card-symbol">{{ item.symbol }}</span>
</div>
<div class="card-type">
{{ item.type === 1 ? ($t('ieo.sub_type') || 'Subscription') : ($t('ieo.allo_type') || 'Allocation') }}
</div>
</div>
<span class="status-badge" :class="'s-' + item.status">{{ statusText(item.status) }}</span>
</div>
<div class="card-body">
<div class="progress-row">
<span>{{ $t('ieo.price') || 'Price' }}: {{ item.token_price }} USDC</span>
<span class="progress-pct">{{ item.progress || 0 }}%</span>
</div>
<div class="progress-bg">
<div
class="progress-bar"
:style="{ width: Math.min(item.progress || 0, 100) + '%' }"
></div>
</div>
<div class="card-time">{{ item.start_time }} ~ {{ item.end_time }}</div>
</div>
<div class="card-foot">
<button
class="btn-subscribe"
:disabled="item.status != 1"
@click="openSubscribe(item)"
>
{{ $t('credit.subscribe_with_credit') || '使用信用额度参与' }}
</button>
</div>
</div>
<div v-if="filteredList.length === 0" class="empty-text">
{{ $t('ieo.no_projects') || 'No IEO projects' }}
</div>
</div>
<!-- 我的订单 -->
<div v-if="currentTab === 2" class="order-list">
<div v-for="o in myOrders" :key="o.id" class="order-card">
<div class="order-top">
<span class="order-name">{{ o.project_name || o.name }}</span>
<span class="order-status" :class="'os-' + o.status">{{ orderStatusText(o.status) }}</span>
</div>
<div class="order-row">
<span>{{ $t('ieo.order_amount') || 'Amount' }}</span>
<span>{{ o.amount }} USDC</span>
</div>
<div class="order-row">
<span>{{ $t('credit.frozen_amount') || '冻结额度' }}</span>
<span>{{ o.frozen_amount || o.amount }} USDC</span>
</div>
<div class="order-row">
<span>{{ $t('ieo.order_time') || 'Time' }}</span>
<span>{{ o.created_at || o.create_time }}</span>
</div>
<div v-if="o.status == 0 || o.status == 3" class="order-foot">
<button class="btn-repay" @click="repayOrder(o)">
{{ $t('credit.repay_btn') || '还款' }}
</button>
</div>
</div>
<div v-if="myOrders.length === 0" class="empty-text">
{{ $t('credit.no_orders') || '暂无订单' }}
</div>
</div>
</div>
</div>
<!-- 参与弹窗 -->
<el-dialog
:title="$t('credit.subscribe_with_credit') || '使用信用额度参与'"
:visible.sync="subscribeVisible"
width="90%"
custom-class="credit-dialog"
:close-on-click-modal="false"
>
<div v-if="currentProject" class="dialog-body">
<div class="dialog-row">
<span class="d-label">{{ $t('ieo.project') || '项目' }}</span>
<span class="d-value">{{ currentProject.name }} ({{ currentProject.symbol }})</span>
</div>
<div class="dialog-row">
<span class="d-label">{{ $t('ieo.price') || 'Price' }}</span>
<span class="d-value">{{ currentProject.token_price }} USDC</span>
</div>
<div class="dialog-row">
<span class="d-label">{{ $t('credit.available') || '可用额度' }}</span>
<span class="d-value green">{{ formatNum(availableCredit) }} USDC</span>
</div>
<div class="dialog-input">
<label>{{ $t('credit.enter_amount') || '请输入金额' }}</label>
<div class="input-wrap">
<input
type="number"
v-model="subscribeAmount"
:placeholder="$t('credit.enter_amount') || '请输入金额'"
/>
<span class="input-unit">USDC</span>
</div>
<div class="input-hint" v-if="subscribeAmount && currentProject.token_price">
{{ estimateTokens }} {{ currentProject.symbol }}
</div>
</div>
</div>
<div slot="footer" class="dialog-foot">
<button class="btn-cancel" @click="subscribeVisible = false">
{{ $t('cancel') || '取消' }}
</button>
<button class="btn-confirm" :disabled="submitting" @click="submitSubscribe">
{{ submitting ? '...' : ($t('credit.confirm_subscribe') || '确认参与') }}
</button>
</div>
</el-dialog>
</div>
</template>
<script>
import indexHeader from '@/view/indexHeader'
import secondaryHead from '@/components/secondaryHead'
export default {
name: 'CreditIeoList',
components: { indexHeader, secondaryHead },
data() {
return {
currentTab: 0,
list: [],
myOrders: [],
availableCredit: 0,
subscribeVisible: false,
currentProject: null,
subscribeAmount: '',
submitting: false,
}
},
computed: {
tabs() {
return [
this.$t('ieo.status_active') || 'Active',
this.$t('ieo.status_upcoming') || 'Coming Soon',
this.$t('credit.my_orders') || '我的订单',
]
},
filteredList() {
const map = [1, 0]
if (this.currentTab > 1) return []
return this.list.filter((i) => i.status == map[this.currentTab])
},
estimateTokens() {
const price = Number(this.currentProject && this.currentProject.token_price) || 0
const amt = Number(this.subscribeAmount) || 0
if (!price) return '0'
return (amt / price).toFixed(4)
},
},
watch: {
currentTab(val) {
if (val === 2) this.loadOrders()
},
},
mounted() {
this.loadList()
this.loadAvailable()
},
methods: {
formatNum(n) {
if (!n && n !== 0) return '0'
return Number(n).toLocaleString('en-US', { maximumFractionDigits: 2 })
},
statusText(s) {
const map = [
this.$t('ieo.coming_soon') || 'Coming Soon',
this.$t('ieo.active') || 'Active',
this.$t('ieo.ended') || 'Ended',
]
return map[s] || ''
},
orderStatusText(s) {
const map = {
0: this.$t('credit.frozen') || '冻结中',
1: this.$t('credit.repaid') || '已还款',
2: this.$t('credit.unlocked') || '已解锁',
3: this.$t('credit.overdue') || '逾期',
}
return map[s] || '-'
},
loadList() {
this.$http({ url: '/api/ieo/list', method: 'get' }).then((res) => {
if (res.data && res.data.type === 'ok') {
this.list = res.data.message || []
}
})
},
loadAvailable() {
const token = localStorage.getItem('token')
if (!token) return
this.$http({ url: '/api/credit/dashboard', method: 'get', headers: { Authorization: localStorage.getItem('token') } }).then((res) => {
if (res.data && res.data.type === 'ok') {
const d = res.data.message || {}
this.availableCredit = Number(d.available_amount || d.max_credit || 0) || (Number(d.total_amount || d.max_credit || 0) - Number(d.used_amount || d.used_credit || 0))
}
})
},
loadOrders() {
const token = localStorage.getItem('token')
if (!token) {
this.$message.error(this.$t('ieo.login_first') || '请先登录')
return
}
this.$http({ url: '/api/credit/my_orders', method: 'get', headers: { Authorization: localStorage.getItem('token') } }).then((res) => {
if (res.data && res.data.type === 'ok') {
this.myOrders = res.data.message || []
}
})
},
openSubscribe(item) {
const token = localStorage.getItem('token')
if (!token) {
this.$message.error(this.$t('ieo.login_first') || '请先登录')
return
}
this.currentProject = item
this.subscribeAmount = ''
this.subscribeVisible = true
},
submitSubscribe() {
const amt = Number(this.subscribeAmount)
if (!amt || amt <= 0) {
this.$message.error(this.$t('credit.enter_amount') || '请输入金额')
return
}
if (amt > this.availableCredit) {
this.$message.error(this.$t('credit.exceed_available') || '超出可用额度')
return
}
this.submitting = true
this.$http({
url: '/api/credit/subscribe',
method: 'post',
data: {
project_id: this.currentProject.id,
amount: amt,
},
headers: { Authorization: localStorage.getItem('token') }
})
.then((res) => {
this.submitting = false
if (res.data && res.data.type === 'ok') {
this.$message.success((res.data && res.data.message) || 'OK')
this.subscribeVisible = false
this.loadAvailable()
if (this.currentTab === 2) this.loadOrders()
} else {
this.$message.error((res.data && res.data.message) || 'Error')
}
})
.catch(() => {
this.submitting = false
})
},
repayOrder(o) {
this.$confirm(
this.$t('credit.repay_confirm') || '确认还款?',
this.$t('credit.repay_btn') || '还款',
{ type: 'warning' }
)
.then(() => {
this.$http({
url: '/api/credit/repay',
method: 'post',
data: { order_id: o.id },
headers: { Authorization: localStorage.getItem('token') }
}).then((res) => {
if (res.data && res.data.type === 'ok') {
this.$message.success(res.data.message || 'OK')
this.loadOrders()
this.loadAvailable()
} else {
this.$message.error((res.data && res.data.message) || 'Error')
}
})
})
.catch(() => {})
},
},
}
</script>
<style scoped>
.cieo-page {
min-height: 100vh;
background: #0e1726;
padding: 20px 16px 60px;
color: #fff;
}
.cieo-wrap { max-width: 960px; margin: 0 auto; }
.tab-bar {
display: flex;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
margin-bottom: 16px;
}
.tab-item {
flex: 1;
text-align: center;
padding: 12px 0;
font-size: 14px;
color: #8c99a4;
cursor: pointer;
position: relative;
}
.tab-item.active { color: #00c48c; font-weight: bold; }
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 2px;
background: #00c48c;
border-radius: 1px;
}
.ieo-card {
background: rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.card-header { display: flex; align-items: center; }
.card-icon { width: 40px; height: 40px; border-radius: 50%; margin-right: 12px; }
.card-info { flex: 1; }
.card-title { font-size: 16px; font-weight: bold; }
.card-symbol { font-size: 12px; color: #8c99a4; margin-left: 6px; }
.card-type { font-size: 12px; color: #8c99a4; }
.status-badge { padding: 3px 10px; border-radius: 6px; font-size: 12px; }
.s-0 { background: rgba(245, 166, 35, 0.15); color: #f5a623; }
.s-1 { background: rgba(0, 196, 140, 0.15); color: #00c48c; }
.s-2 { background: rgba(150, 150, 150, 0.15); color: #999; }
.progress-row { display: flex; justify-content: space-between; margin: 12px 0 6px; font-size: 13px; }
.progress-pct { color: #00c48c; font-weight: bold; }
.progress-bg { height: 6px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; overflow: hidden; }
.progress-bar { height: 100%; background: linear-gradient(90deg, #00c48c, #52f3c9); border-radius: 3px; }
.card-time { font-size: 11px; color: #666; margin-top: 8px; }
.card-foot { margin-top: 14px; }
.btn-subscribe {
width: 100%;
background: #00c48c;
color: #fff;
border: none;
padding: 10px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.btn-subscribe:disabled { background: #2b3548; color: #666; cursor: not-allowed; }
.empty-text { text-align: center; padding: 40px 0; color: #666; }
/* 订单 */
.order-card {
background: rgba(255, 255, 255, 0.04);
border-radius: 12px;
padding: 16px;
margin-bottom: 12px;
}
.order-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.order-name { font-size: 15px; font-weight: bold; }
.order-status { padding: 3px 10px; border-radius: 6px; font-size: 12px; }
.os-0 { background: rgba(0, 136, 255, 0.15); color: #0088ff; }
.os-1 { background: rgba(0, 196, 140, 0.15); color: #00c48c; }
.os-2 { background: rgba(150, 150, 150, 0.15); color: #999; }
.os-3 { background: rgba(245, 108, 108, 0.15); color: #f56c6c; }
.order-row {
display: flex;
justify-content: space-between;
font-size: 13px;
color: #8c99a4;
margin-bottom: 6px;
}
.order-row span:last-child { color: #fff; }
.order-foot { margin-top: 12px; }
.btn-repay {
width: 100%;
background: transparent;
color: #00c48c;
border: 1px solid #00c48c;
padding: 8px;
border-radius: 8px;
font-size: 13px;
cursor: pointer;
}
.btn-repay:hover { background: rgba(0, 196, 140, 0.12); }
/* 弹窗 */
.dialog-body { padding: 4px 0; }
.dialog-row {
display: flex;
justify-content: space-between;
font-size: 13px;
margin-bottom: 10px;
}
.d-label { color: #8c99a4; }
.d-value { color: #333; }
.d-value.green { color: #00c48c; font-weight: bold; }
.dialog-input { margin-top: 14px; }
.dialog-input label {
display: block;
font-size: 13px;
color: #606266;
margin-bottom: 6px;
}
.input-wrap {
display: flex;
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 6px;
padding: 8px 12px;
}
.input-wrap input {
flex: 1;
border: none;
outline: none;
font-size: 14px;
background: transparent;
}
.input-unit { color: #8c99a4; font-size: 13px; }
.input-hint { font-size: 12px; color: #8c99a4; margin-top: 6px; }
.dialog-foot { text-align: right; }
.btn-cancel, .btn-confirm {
padding: 8px 20px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
border: none;
margin-left: 8px;
}
.btn-cancel { background: #f4f4f5; color: #606266; }
.btn-confirm { background: #00c48c; color: #fff; font-weight: 600; }
.btn-confirm:disabled { opacity: 0.6; cursor: not-allowed; }
@media (max-width: 768px) {
.cieo-page { padding: 14px 12px 40px; }
}
</style>
+457
View File
@@ -0,0 +1,457 @@
<template>
<div>
<indexHeader></indexHeader>
<secondaryHead :text="$t('credit.title') || '信用额度'"></secondaryHead>
<div class="credit-page">
<div class="credit-wrap">
<!-- 未开通 -->
<div v-if="!hasCredit && !loading" class="no-credit-box">
<i class="el-icon-lock no-credit-icon"></i>
<div class="no-credit-title">{{ $t('credit.contact_open') || '联系客服开通额度' }}</div>
<div class="no-credit-desc">{{ $t('credit.contact_desc') || '请联系在线客服,提交资料后开通您的信用额度' }}</div>
<button class="btn-contact" @click="goContact">{{ $t('credit.contact_btn') || '联系客服' }}</button>
</div>
<!-- 已开通 -->
<template v-else-if="hasCredit">
<!-- 信用蓝卡 -->
<div class="credit-card">
<div class="credit-card-inner">
<div class="cc-label">{{ $t('credit.max_credit') || '最大信用额度' }}</div>
<div class="cc-amount">
<span class="cc-num">{{ formatNum(dashboard.max_credit) }}</span>
<span class="cc-unit">USDC</span>
</div>
<div class="cc-stats">
<div class="cc-stat">
<div class="cc-stat-label">{{ $t('credit.used') || '已使用额度' }}</div>
<div class="cc-stat-value">{{ formatNum(dashboard.used_credit) }} USDC</div>
</div>
<div class="cc-stat">
<div class="cc-stat-label">{{ $t('credit.score') || '信用点数' }}</div>
<div class="cc-stat-value">{{ dashboard.credit_score || 0 }}</div>
</div>
</div>
<div class="cc-actions">
<button class="cc-btn cc-btn-light" @click="goIncrease">
<i class="el-icon-plus"></i> {{ $t('credit.increase') || '增加额度' }}
</button>
<button class="cc-btn cc-btn-ghost" @click="goRecords">
<i class="el-icon-document"></i> {{ $t('credit.records') || '记录' }}
</button>
</div>
</div>
</div>
<!-- 信息条 -->
<div class="info-row">
<div class="info-cell">
<div class="info-label">{{ $t('credit.total_credit') || '总信用额度' }}</div>
<div class="info-value">{{ formatNum(dashboard.max_credit) }} USDC</div>
</div>
<div class="info-cell">
<div class="info-label">{{ $t('credit.daily_rate') || '日利率' }}</div>
<div class="info-value">{{ (Number(dashboard.daily_rate || 0) * 100).toFixed(2) }}%</div>
</div>
<div class="info-cell">
<div class="info-label">{{ $t('credit.cycle') || '信用周期' }}</div>
<div class="info-value">{{ dashboard.cycle_days || 0 }} {{ $t('credit.days') || '天' }}</div>
</div>
</div>
<!-- 入口按钮 -->
<div class="entry-row">
<button class="entry-btn ieo-btn" @click="goCreditIeo">
<i class="el-icon-s-promotion"></i>
<span>{{ $t('credit.credit_ieo') || '信用IEO专区' }}</span>
<i class="el-icon-arrow-right"></i>
</button>
</div>
<div class="repay-row">
<button class="btn-repay-all" @click="repayAll">
{{ $t('credit.repay_all') || '全部还款' }}
</button>
</div>
<!-- 如何使用 -->
<div class="howto-card">
<div class="howto-title">{{ $t('credit.how_to_use') || '如何使用信用额度' }}</div>
<div class="howto-steps">
<div class="howto-step">
<div class="step-num">1</div>
<div class="step-content">
<div class="step-name">{{ $t('credit.step1') || '提交审核资料' }}</div>
<div class="step-desc">{{ $t('credit.step1_desc') || '上传身份信息,完成实名认证' }}</div>
</div>
</div>
<div class="howto-step">
<div class="step-num">2</div>
<div class="step-content">
<div class="step-name">{{ $t('credit.step2') || '客服审核' }}</div>
<div class="step-desc">{{ $t('credit.step2_desc') || '专员评估信用资质,核定额度' }}</div>
</div>
</div>
<div class="howto-step">
<div class="step-num">3</div>
<div class="step-content">
<div class="step-name">{{ $t('credit.step3') || '额度使用说明' }}</div>
<div class="step-desc">{{ $t('credit.step3_desc') || '可用于参与IEO,按周期还款解冻' }}</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script>
import indexHeader from '@/view/indexHeader'
import secondaryHead from '@/components/secondaryHead'
export default {
name: 'CreditCenter',
components: { indexHeader, secondaryHead },
data() {
return {
loading: true,
hasCredit: false,
dashboard: {
max_credit: 0,
used_credit: 0,
credit_score: 0,
daily_rate: '0.00',
cycle_days: 0,
},
}
},
mounted() {
this.loadDashboard()
},
methods: {
loadDashboard() {
const token = localStorage.getItem('token')
if (!token) {
this.loading = false
this.hasCredit = false
return
}
this.$http({
url: '/api/credit/dashboard',
method: 'get',
headers: { Authorization: token }
})
.then((res) => {
this.loading = false
if (res.data && res.data.type === 'ok') {
const data = res.data.message || {}
this.hasCredit = !!(data.has_credit || (data.total_amount && data.total_amount > 0) || (data.max_credit && data.max_credit > 0))
this.dashboard = {
max_credit: data.total_amount || data.max_credit || 0,
used_credit: data.used_amount || data.used_credit || 0,
credit_score: data.credit_score || 0,
daily_rate: data.daily_rate || '0.00',
cycle_days: data.cycle_days || 0,
}
} else {
this.hasCredit = false
}
})
.catch(() => {
this.loading = false
this.hasCredit = false
})
},
formatNum(n) {
if (!n && n !== 0) return '0'
return Number(n).toLocaleString('en-US', { maximumFractionDigits: 2 })
},
goCreditIeo() {
this.$router.push('/creditIeo')
},
goIncrease() {
this.goContact()
},
goRecords() {
this.$router.push('/creditIeo')
},
repayAll() {
this.$confirm(
this.$t('credit.repay_all_confirm') || '确认全部还款?',
this.$t('credit.repay_all') || '全部还款',
{ type: 'warning' }
)
.then(() => {
this.$http({
url: '/api/credit/repay_all',
method: 'post',
headers: { Authorization: localStorage.getItem('token') }
}).then((res) => {
if (res.data && res.data.type === 'ok') {
this.$message.success(res.data.message || 'OK')
this.loadDashboard()
} else {
this.$message.error((res.data && res.data.message) || 'Error')
}
})
})
.catch(() => {})
},
goContact() {
const url = (window.localStorage.getItem('service_url')) || '/help'
if (url.indexOf('http') === 0) {
window.open(url)
} else {
this.$router.push(url)
}
},
},
}
</script>
<style scoped>
.credit-page {
min-height: 100vh;
background: #0e1726;
padding: 20px 16px 60px;
color: #fff;
}
.credit-wrap {
max-width: 960px;
margin: 0 auto;
}
/* 未开通 */
.no-credit-box {
background: rgba(255, 255, 255, 0.04);
border-radius: 16px;
padding: 60px 24px;
text-align: center;
}
.no-credit-icon {
font-size: 48px;
color: #00c48c;
margin-bottom: 16px;
}
.no-credit-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.no-credit-desc {
font-size: 13px;
color: #8c99a4;
margin-bottom: 24px;
}
.btn-contact {
background: #00c48c;
color: #fff;
border: none;
padding: 12px 36px;
border-radius: 8px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
}
.btn-contact:hover {
opacity: 0.9;
}
/* 蓝色信用卡 */
.credit-card {
background: linear-gradient(135deg, #0066cc 0%, #00aaff 60%, #00c4ff 100%);
border-radius: 18px;
padding: 24px 22px;
box-shadow: 0 10px 30px rgba(0, 136, 255, 0.25);
position: relative;
overflow: hidden;
}
.credit-card::before {
content: '';
position: absolute;
top: -80px;
right: -80px;
width: 220px;
height: 220px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
}
.credit-card-inner {
position: relative;
z-index: 1;
}
.cc-label {
color: rgba(255, 255, 255, 0.85);
font-size: 13px;
margin-bottom: 6px;
}
.cc-amount {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 22px;
}
.cc-num {
font-size: 38px;
font-weight: bold;
color: #fff;
letter-spacing: 0.5px;
}
.cc-unit {
font-size: 16px;
color: rgba(255, 255, 255, 0.9);
}
.cc-stats {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.cc-stat {
flex: 1;
}
.cc-stat-label {
color: rgba(255, 255, 255, 0.75);
font-size: 12px;
margin-bottom: 4px;
}
.cc-stat-value {
color: #fff;
font-size: 16px;
font-weight: 600;
}
.cc-actions {
display: flex;
gap: 10px;
}
.cc-btn {
flex: 1;
padding: 10px 12px;
border-radius: 8px;
font-size: 13px;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
font-weight: 500;
}
.cc-btn-light {
background: #fff;
color: #0066cc;
}
.cc-btn-ghost {
background: rgba(255, 255, 255, 0.15);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.cc-btn:hover { opacity: 0.9; }
/* 信息条 */
.info-row {
display: flex;
background: rgba(255, 255, 255, 0.04);
border-radius: 12px;
margin-top: 16px;
padding: 16px 8px;
}
.info-cell {
flex: 1;
text-align: center;
border-right: 1px solid rgba(255, 255, 255, 0.06);
}
.info-cell:last-child { border-right: none; }
.info-label {
color: #8c99a4;
font-size: 12px;
margin-bottom: 6px;
}
.info-value {
color: #fff;
font-size: 14px;
font-weight: 600;
}
/* 入口按钮 */
.entry-row {
margin-top: 16px;
display: flex;
gap: 12px;
}
.entry-btn {
flex: 1;
background: rgba(0, 196, 140, 0.12);
border: 1px solid rgba(0, 196, 140, 0.35);
color: #00c48c;
padding: 14px 18px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
}
.entry-btn span { flex: 1; text-align: left; }
.entry-btn:hover { background: rgba(0, 196, 140, 0.2); }
.repay-row { margin-top: 16px; }
.btn-repay-all {
width: 100%;
background: #1a2332;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.12);
padding: 14px 18px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
}
.btn-repay-all:hover { background: #223048; }
/* 如何使用 */
.howto-card {
margin-top: 24px;
background: rgba(255, 255, 255, 0.04);
border-radius: 14px;
padding: 20px 18px;
}
.howto-title {
font-size: 15px;
font-weight: bold;
margin-bottom: 16px;
color: #fff;
}
.howto-steps { display: flex; flex-direction: column; gap: 16px; }
.howto-step { display: flex; gap: 14px; align-items: flex-start; }
.step-num {
flex: 0 0 28px;
width: 28px;
height: 28px;
background: rgba(0, 196, 140, 0.18);
color: #00c48c;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
}
.step-name { color: #fff; font-size: 14px; font-weight: 600; margin-bottom: 2px; }
.step-desc { color: #8c99a4; font-size: 12px; line-height: 1.5; }
/* H5 */
@media (max-width: 768px) {
.credit-page { padding: 14px 12px 40px; }
.credit-card { padding: 20px 18px; border-radius: 14px; }
.cc-num { font-size: 30px; }
.cc-actions { flex-direction: row; }
.info-row { padding: 12px 4px; }
.info-value { font-size: 13px; }
.entry-row { flex-direction: column; }
}
</style>
+444
View File
@@ -0,0 +1,444 @@
<template>
<div>
<indexHeader></indexHeader>
<secondaryHead :text="$t('loan.title') || '贷款申请'"></secondaryHead>
<div class="loan-page">
<div class="loan-layout">
<!-- 左栏 -->
<div class="loan-col loan-col-left">
<!-- 贷款概览 -->
<div class="card">
<div class="card-title">{{ $t('loan.overview') || 'Loan Overview' }}</div>
<div class="overview">
<div class="ov-row">
<span class="ov-label">{{ $t('loan.currency') || 'Loan Currency' }}</span>
<span class="ov-value green">{{ loanCurrency }}</span>
</div>
<div class="ov-row">
<span class="ov-label">{{ $t('loan.amount') || 'Loan Amount' }}</span>
<span class="ov-value">{{ amount || '--' }} {{ loanCurrency }}</span>
</div>
<div class="ov-row">
<span class="ov-label">{{ $t('loan.repayment') || 'Repayment' }}</span>
<span class="ov-value">{{ $t('loan.lump_sum') || 'Lump sum at maturity' }}</span>
</div>
<div class="ov-row">
<span class="ov-label">{{ $t('loan.institution') || 'Lending Institution' }}</span>
<span class="ov-value">{{ institution || 'Global Finance' }}</span>
</div>
</div>
</div>
<!-- 贷款流程 -->
<div class="card">
<div class="card-title">{{ $t('loan.process') || 'Loan Process' }}</div>
<div class="steps">
<div class="step" v-for="(s, i) in steps" :key="i">
<div class="step-circle">{{ i + 1 }}</div>
<div class="step-text">
<div class="step-name">{{ s.name }}</div>
<div class="step-desc">{{ s.desc }}</div>
</div>
</div>
</div>
</div>
<!-- 底部说明 -->
<div class="tip-box">
<i class="el-icon-lock"></i>
<span>{{ $t('loan.confidential') || 'Your information is strictly confidential and used only for loan review.' }}</span>
</div>
</div>
<!-- 右栏 -->
<div class="loan-col loan-col-right">
<!-- 安全标签 -->
<div class="secure-tag">
<i class="el-icon-lock"></i>
<span>{{ $t('loan.secure_tag') || 'Data Encryption — Safe & Secure' }}</span>
</div>
<!-- 身份信息 -->
<div class="card">
<div class="card-title">{{ $t('loan.identity') || 'Identity Information' }}</div>
<div class="upload-grid">
<div class="upload-section" v-for="item in uploadFields" :key="item.key">
<div class="upload-box" @click="chooseImage(item.key)">
<img v-if="previews[item.key]" :src="previews[item.key]" class="preview-img" />
<div v-else class="upload-placeholder">
<i class="el-icon-plus"></i>
</div>
</div>
<div class="upload-label">{{ item.label }}</div>
</div>
</div>
<div class="upload-hint">{{ $t('loan.upload_hint') || 'JPG / PNG, up to 5MB' }}</div>
</div>
<!-- 贷款信息 -->
<div class="card">
<div class="card-title">{{ $t('loan.info') || 'Loan Information' }}</div>
<div class="form-row">
<label>{{ $t('loan.amount') || 'Loan Amount' }}</label>
<div class="input-group">
<input type="number" v-model="amount" :placeholder="$t('loan.amount_placeholder') || 'Enter amount'" />
<span class="unit">{{ loanCurrency }}</span>
</div>
</div>
<div class="form-row">
<label>{{ $t('loan.repay_currency') || 'Repayment Currency' }}</label>
<div class="select-group">
<select v-model="repayCurrency">
<option v-for="c in currencyList" :key="c" :value="c">{{ c }}</option>
</select>
</div>
</div>
<div class="form-row">
<label>{{ $t('loan.purpose') || 'Purpose of Funds' }}</label>
<div class="textarea-group">
<textarea
v-model="purpose"
maxlength="200"
rows="3"
:placeholder="$t('loan.purpose_placeholder') || 'Optional, briefly describe the purpose'"
></textarea>
<div class="char-count">{{ (purpose || '').length }}/200</div>
</div>
</div>
</div>
<!-- 推荐放款机构 -->
<div class="card inst-card">
<div class="inst-head">
<div class="inst-name">{{ institution || 'Global Finance' }}</div>
<div class="inst-badge">{{ $t('loan.recommended') || 'Recommended' }}</div>
</div>
<div class="inst-desc">{{ $t('loan.inst_desc') || 'Professional digital asset financial institution' }}</div>
</div>
<!-- 按钮 -->
<button class="submit-btn" :disabled="submitting" @click="submitLoan">
{{ submitting ? ($t('loan.submitting') || 'Submitting...') : ($t('loan.submit') || 'Submit Application') }}
</button>
<button class="contact-btn" @click="contactService">
<i class="el-icon-service"></i>
{{ $t('loan.contact') || 'Contact Support' }}
</button>
<!-- 历史记录 -->
<div class="card" v-if="loanList.length > 0">
<div class="card-title">{{ $t('loan.history') || 'My Applications' }}</div>
<div class="history-item" v-for="(item, idx) in loanList" :key="idx">
<div class="history-row">
<span class="history-amount">{{ item.amount }} {{ loanCurrency }}</span>
<span class="history-status" :class="'status-' + item.status">{{ item.status_text }}</span>
</div>
<div class="history-time">{{ item.create_time }}</div>
<div class="history-remark" v-if="item.remark">{{ item.remark }}</div>
</div>
</div>
</div>
</div>
<input type="file" ref="fileInput" accept="image/jpeg,image/png" style="display:none" @change="onFileSelected" />
</div>
</div>
</template>
<script>
import indexHeader from '@/view/indexHeader'
import secondaryHead from '@/components/secondaryHead'
export default {
name: 'LoanPage',
components: { indexHeader, secondaryHead },
data() {
return {
loanCurrency: 'USDC',
repayCurrency: 'USDC',
currencyList: ['USDC', 'BTC', 'ETH'],
cardImage: '', cardbmImage: '', handImage: '',
previews: { card: '', cardbm: '', hand: '' },
amount: '',
purpose: '',
institution: '',
loanList: [],
currentField: '',
submitting: false,
}
},
computed: {
uploadFields() {
return [
{ key: 'card', label: this.$t('loan.card_front') || 'ID Card (Front)' },
{ key: 'cardbm', label: this.$t('loan.card_back') || 'ID Card (Back)' },
{ key: 'hand', label: this.$t('loan.card_hand') || 'Handheld ID Photo' },
]
},
steps() {
return [
{ name: this.$t('loan.step1') || 'Submit Application', desc: this.$t('loan.step1_desc') || 'Fill in info and upload ID' },
{ name: this.$t('loan.step2') || 'Institution Review', desc: this.$t('loan.step2_desc') || 'Lender reviews materials' },
{ name: this.$t('loan.step3') || 'Sign Agreement', desc: this.$t('loan.step3_desc') || 'Confirm loan terms' },
{ name: this.$t('loan.step4') || 'Disbursement', desc: this.$t('loan.step4_desc') || 'Funds credited to account' },
]
},
},
mounted() {
this.getInstitution()
this.getMyLoans()
},
methods: {
getInstitution() {
this.$http({ url: '/api/loan/institution', method: 'get' }).then(res => {
if (res.data.type === 'ok') {
this.institution = res.data.message.institution
}
})
},
getMyLoans() {
this.$http({
url: '/api/loan/my_loans',
method: 'get',
headers: { Authorization: localStorage.getItem('token') },
}).then(res => {
if (res.data.type === 'ok') this.loanList = res.data.message || []
})
},
chooseImage(field) {
this.currentField = field
this.$refs.fileInput.click()
},
onFileSelected(e) {
const file = e.target.files[0]
if (!file) return
if (file.size > 5 * 1024 * 1024) {
this.$message.error(this.$t('loan.size_limit') || 'File must be less than 5MB')
this.$refs.fileInput.value = ''
return
}
const reader = new FileReader()
reader.onload = (ev) => {
this.$set(this.previews, this.currentField, ev.target.result)
}
reader.readAsDataURL(file)
const formData = new FormData()
formData.append('file', file)
const field = this.currentField
this.$http({
url: '/api/upload_new',
method: 'post',
data: formData,
headers: {
Authorization: localStorage.getItem('token'),
},
}).then(res => {
if (res.data.type === 'ok') {
const url = res.data.message
if (field === 'card') this.cardImage = url
if (field === 'cardbm') this.cardbmImage = url
if (field === 'hand') this.handImage = url
} else {
this.$message.error(res.data.message)
}
})
this.$refs.fileInput.value = ''
},
submitLoan() {
if (this.submitting) return
if (!this.cardImage) return this.$message.error(this.$t('loan.need_front') || 'Please upload ID card front')
if (!this.cardbmImage) return this.$message.error(this.$t('loan.need_back') || 'Please upload ID card back')
if (!this.handImage) return this.$message.error(this.$t('loan.need_hand') || 'Please upload handheld ID photo')
if (!this.amount || Number(this.amount) <= 0) {
return this.$message.error(this.$t('loan.need_amount') || 'Please enter a valid amount')
}
this.submitting = true
this.$http({
url: '/api/loan/submit',
method: 'post',
data: {
amount: this.amount,
card_image: this.cardImage,
cardbm_image: this.cardbmImage,
hand_image: this.handImage,
purpose: this.purpose,
repay_currency: this.repayCurrency,
},
headers: { Authorization: localStorage.getItem('token') },
}).then(res => {
this.submitting = false
if (res.data.type === 'ok') {
this.$message.success(res.data.message)
this.amount = ''
this.purpose = ''
this.cardImage = ''; this.cardbmImage = ''; this.handImage = ''
this.previews = { card: '', cardbm: '', hand: '' }
this.getMyLoans()
} else {
this.$message.error(res.data.message)
}
}).catch(() => {
this.submitting = false
})
},
contactService() {
this.$router.push('/service')
},
},
}
</script>
<style scoped>
.loan-page { padding: 0 16px 30px; background: #0e1726; min-height: 100vh; color: #fff; box-sizing: border-box; }
.page-header { display: flex; align-items: center; height: 50px; }
.page-header h3 { margin: 0; flex: 1; text-align: center; font-size: 18px; font-weight: 600; }
.back-btn { cursor: pointer; font-size: 22px; color: #fff; width: 28px; }
.loan-layout { display: flex; gap: 20px; max-width: 1200px; margin: 10px auto 0; align-items: flex-start; }
.loan-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 14px; }
.card { background: #1a2332; border: 1px solid #2a3542; border-radius: 12px; padding: 16px; }
.card-title { font-size: 15px; font-weight: 600; margin-bottom: 14px; color: #fff; }
/* 概览 */
.overview { display: flex; flex-direction: column; gap: 12px; }
.ov-row { display: flex; justify-content: space-between; align-items: center; font-size: 14px; }
.ov-label { color: #8c99a4; }
.ov-value { color: #fff; font-weight: 500; }
.ov-value.green { color: #00c48c; }
/* 流程 */
.steps { display: flex; flex-direction: column; gap: 14px; }
.step { display: flex; align-items: flex-start; gap: 12px; position: relative; }
.step:not(:last-child)::after {
content: ''; position: absolute; left: 13px; top: 28px; width: 2px; height: calc(100% - 14px);
background: linear-gradient(to bottom, #00c48c 0%, rgba(0,196,140,0.2) 100%);
}
.step-circle {
width: 28px; height: 28px; border-radius: 50%; background: #00c48c;
color: #fff; font-size: 13px; font-weight: 600;
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
box-shadow: 0 0 0 3px rgba(0,196,140,0.15);
}
.step-text { padding-top: 2px; }
.step-name { font-size: 14px; color: #fff; font-weight: 500; }
.step-desc { font-size: 12px; color: #8c99a4; margin-top: 2px; }
/* 提示框 */
.tip-box {
background: rgba(0,196,140,0.08); border: 1px solid rgba(0,196,140,0.25);
border-radius: 10px; padding: 12px 14px; font-size: 12px; color: #8c99a4;
display: flex; align-items: center; gap: 8px;
}
.tip-box i { color: #00c48c; font-size: 14px; }
/* 安全标签 */
.secure-tag {
display: inline-flex; align-items: center; gap: 6px;
background: rgba(0,196,140,0.1); border: 1px solid rgba(0,196,140,0.3);
color: #00c48c; padding: 6px 12px; border-radius: 20px;
font-size: 12px; align-self: flex-start;
}
/* 上传 */
.upload-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.upload-section { display: flex; flex-direction: column; align-items: center; }
.upload-box {
width: 100%; aspect-ratio: 3 / 2; background: #0e1726;
border: 1px dashed #2a3542; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; overflow: hidden; transition: border-color .2s;
}
.upload-box:hover { border-color: #00c48c; }
.preview-img { width: 100%; height: 100%; object-fit: cover; }
.upload-placeholder { color: #8c99a4; font-size: 28px; }
.upload-label { margin-top: 8px; font-size: 12px; color: #8c99a4; text-align: center; }
.upload-hint { font-size: 12px; color: #8c99a4; margin-top: 10px; }
/* 表单 */
.form-row { margin-bottom: 14px; }
.form-row:last-child { margin-bottom: 0; }
.form-row label { display: block; margin-bottom: 8px; font-size: 13px; color: #8c99a4; }
.input-group, .select-group {
display: flex; align-items: center; background: #0e1726;
border: 1px solid #2a3542; border-radius: 8px; padding: 0 12px;
transition: border-color .2s;
}
.input-group:focus-within, .select-group:focus-within { border-color: #00c48c; }
.input-group input, .select-group select {
flex: 1; background: transparent; border: none; outline: none;
color: #fff; height: 42px; font-size: 14px;
}
.select-group select { cursor: pointer; appearance: none; }
.input-group .unit { color: #00c48c; font-size: 13px; font-weight: 500; }
.textarea-group {
background: #0e1726; border: 1px solid #2a3542; border-radius: 8px;
padding: 10px 12px; position: relative;
}
.textarea-group:focus-within { border-color: #00c48c; }
.textarea-group textarea {
width: 100%; background: transparent; border: none; outline: none;
color: #fff; font-size: 14px; resize: none; font-family: inherit;
}
.char-count { text-align: right; font-size: 12px; color: #8c99a4; margin-top: 4px; }
/* 机构卡 */
.inst-card { display: flex; flex-direction: column; gap: 8px; }
.inst-head { display: flex; align-items: center; gap: 10px; }
.inst-name { font-size: 15px; font-weight: 600; color: #fff; }
.inst-badge {
background: rgba(0,196,140,0.15); color: #00c48c;
padding: 2px 10px; border-radius: 4px; font-size: 12px;
}
.inst-desc { color: #8c99a4; font-size: 13px; }
/* 按钮 */
.submit-btn {
width: 100%; height: 48px; border: none; border-radius: 10px;
background: linear-gradient(135deg, #00c48c 0%, #00a876 100%);
color: #fff; font-size: 16px; font-weight: 600; cursor: pointer;
box-shadow: 0 4px 12px rgba(0,196,140,0.3); transition: opacity .2s;
}
.submit-btn:hover { opacity: 0.9; }
.submit-btn:disabled { opacity: 0.6; cursor: not-allowed; }
.contact-btn {
width: 100%; height: 46px; background: transparent;
border: 1px solid #2a3542; border-radius: 10px;
color: #8c99a4; font-size: 15px; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 6px;
transition: all .2s;
}
.contact-btn:hover { border-color: #00c48c; color: #00c48c; }
/* 历史 */
.history-item { background: #0e1726; border: 1px solid #2a3542; border-radius: 8px; padding: 12px; margin-bottom: 8px; }
.history-item:last-child { margin-bottom: 0; }
.history-row { display: flex; justify-content: space-between; align-items: center; }
.history-amount { font-weight: 600; font-size: 14px; }
.history-status { font-size: 12px; padding: 2px 8px; border-radius: 4px; }
.status-0 { color: #f5a623; background: rgba(245,166,35,0.1); }
.status-1 { color: #00c48c; background: rgba(0,196,140,0.1); }
.status-2 { color: #ff4040; background: rgba(255,64,64,0.1); }
.history-time { font-size: 12px; color: #8c99a4; margin-top: 6px; }
.history-remark { font-size: 12px; color: #8c99a4; margin-top: 4px; }
/* 响应式 */
@media (max-width: 768px) {
.loan-page { padding: 0 12px 20px; }
.loan-layout { flex-direction: column; gap: 14px; }
.loan-col { gap: 12px; }
.card { padding: 14px; }
.upload-grid { grid-template-columns: repeat(3, 1fr); gap: 8px; }
.submit-btn { height: 46px; font-size: 15px; }
}
@media (max-width: 480px) {
.card-title { font-size: 14px; }
.ov-row { font-size: 13px; }
.upload-box { aspect-ratio: 1 / 1; }
.upload-label { font-size: 11px; }
}
</style>