foreign-admin-ui/src/views/dashboard/components/view/index.vue

1191 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="personal-center">
<!-- 如何开始 -->
<section class="section getting-started">
<h2 class="section-title">
<el-icon><MagicStick /></el-icon>
如何开始
</h2>
<div class="progress-steps">
<div
v-for="(step, index) in startSteps"
:key="index"
class="step-item"
:class="{ completed: step.completed, active: index === currentStep }"
>
<div class="step-icon">
<el-icon v-if="step.completed"><Check /></el-icon>
<span v-else>{{ index + 1 }}</span>
</div>
<div class="step-info">
<router-link :to="step.path" class="step-title">{{ step.title }}</router-link>
<span class="step-desc">{{ step.desc }}</span>
</div>
<div v-if="index < startSteps.length - 1" class="step-connector">
<div class="connector-line" :class="{ filled: step.completed }"></div>
</div>
</div>
</div>
</section>
<!-- 常用功能 -->
<section class="section quick-actions">
<h2 class="section-title">
<el-icon><Grid /></el-icon>
常用功能
</h2>
<div class="action-cards">
<router-link
v-for="action in quickActions"
:key="action.id"
:to="action.path"
class="action-card"
>
<div class="card-icon" :style="{ background: action.bgColor }">
<el-icon :size="32" :color="action.iconColor">
<component :is="action.icon" />
</el-icon>
</div>
<div class="card-content">
<h3>{{ action.title }}</h3>
<p>{{ action.desc }}</p>
</div>
<div class="card-arrow">
<el-icon><ArrowRight /></el-icon>
</div>
</router-link>
</div>
</section>
<!-- 我的钱包 -->
<section class="section my-wallet">
<h2 class="section-title">
<el-icon><Wallet /></el-icon>
我的钱包
<span class="wallet-badge">USD 主钱包</span>
</h2>
<div class="wallet-card" @click="handleWalletClick">
<div class="wallet-main">
<div class="balance-section">
<div class="balance-label">总金额</div>
<div class="balance-value">
<AnimatedNumber :value="walletCapital.amount_total || 0" :decimals="2" />
</div>
</div>
<div class="balance-detail">
<div class="detail-item">
<span class="label">可用资金</span>
<span class="value">
<AnimatedNumber :value="walletCapital.amount || 0" :decimals="2" />
</span>
</div>
<template v-if="isAgent">
<div class="detail-item">
<span class="label">点差</span>
<span class="value">{{ walletCapital.commission_point_diff || '0.00' }}</span>
</div>
<div class="detail-item">
<span class="label">手续费</span>
<span class="value">{{ walletCapital.commission_fee_handling || '0.00' }}</span>
</div>
</template>
</div>
</div>
<div class="wallet-actions">
<router-link to="/capital/transfer" class="action-btn">
<el-icon><RefreshLeft /></el-icon>
转账
</router-link>
<router-link to="/capital/withdraw" class="action-btn">
<el-icon><Money /></el-icon>
取款
</router-link>
<router-link to="/capital/fundFlow" class="action-btn">
<el-icon><List /></el-icon>
资金明细
</router-link>
</div>
</div>
</section>
<!-- 个人账户 -->
<section class="section my-accounts">
<h2 class="section-title">
<el-icon><User /></el-icon>
个人账户
<el-button type="primary" @click="handleAddSubAccount" v-auth="'user_createAccount'">
<el-icon><Plus /></el-icon>
添加子账户
</el-button>
</h2>
<div class="account-grid">
<div
v-for="item in accountList"
:key="item.id"
class="account-card"
@click="handleAccountClick(item)"
>
<div class="account-header">
<div class="account-name">
<span v-if="isEdit !== item.account_id">{{ item.account_name }}</span>
<el-input
v-else
v-model="editName"
size="small"
@blur="handleSave"
@keyup.enter="handleSave"
ref="inputRef"
/>
</div>
<div class="account-actions">
<el-icon size="20" @click.stop="handleEdit(item.account_name, item.account_id)">
<EditPen />
</el-icon>
<el-icon size="20" @click.stop="handleSetting(item.account_id)">
<Setting />
</el-icon>
</div>
</div>
<div class="account-body">
<div class="account-number">{{ item.account_id || '--' }}</div>
<div class="account-stats">
<div class="stat-item">
<span class="stat-label">净资产</span>
<span class="stat-value">
<AnimatedNumber :value="item?.wallets_trade?.amount || 0" :decimals="2" />
</span>
</div>
<div class="stat-item">
<span class="stat-label">余额</span>
<span class="stat-value">
<AnimatedNumber :value="item?.wallets_trade?.amount || 0" :decimals="2" />
</span>
</div>
<div class="stat-item">
<span class="stat-label">杠杆</span>
<span class="stat-value">1:{{ item?.wallets_trade?.lever || '--' }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 推广板块 -->
<section class="section promotion" v-if="userInfo.type * 1 === 2 && userInfo.role_id * 1 === 5">
<h2 class="section-title">
<el-icon><Share /></el-icon>
推广中心
<span class="promotion-time">统计起始时间: {{ promotionStats.startTime }}</span>
</h2>
<div class="promotion-content">
<div class="promotion-link">
<h4>推广链接</h4>
<div class="link-box">
<input type="text" :value="copyText" readonly class="link-input" />
<el-button type="primary" @click="handleCopyUrl" class="copy-btn">
<el-icon><DocumentCopy /></el-icon>
复制链接
</el-button>
</div>
</div>
<div class="promotion-qrcode">
<h4>推广二维码</h4>
<div class="qrcode-card">
<canvas ref="qrCanvas"></canvas>
<p>扫码注册</p>
</div>
</div>
</div>
</section>
<!-- 添加/编辑子账户弹窗 -->
<AgentDialog
:visible="dialogVisible"
:title="dialogTitle"
:type="dialogType"
@confirm="handleSubmit"
@cancel="handleCancel"
@close="handleClose"
>
<div class="tab" v-if="dialogType !== 'add'"
style="margin-bottom: 20px;">
<el-button
:type="dialogType === 'setting' ? 'primary' : 'default'"
@click="dialogType = 'setting'"
>
账户设置
</el-button>
<el-button
:type="dialogType === 'leverage' ? 'primary' : 'default'"
@click="handleChangeToLeverage"
>
修改杠杆
</el-button>
</div>
<AgentForm
ref="subAccountFormRef"
:formData="subAccountForm"
:formItems="subAccountFormItem"
:formRules="subAccountRules"
:optionsMap="subAccountFormOptionsMap"
:rowGutter="rowGutter"
:colSpan="colSpan"
/>
</AgentDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
Check,
Grid,
Wallet,
User,
Share,
ArrowRight,
RefreshLeft,
Money,
List,
Plus,
EditPen,
Setting,
DocumentCopy,
MagicStick,
CircleCheckFilled,
TrendCharts
} from '@element-plus/icons-vue'
import AgentDialog from '@/components/common/Dialog.vue'
import AgentForm from '@/components/common/Form.vue'
import { formItem, resetPasswordFormItem, leverageFormItem } from './options'
import { rules } from './rules'
import {
getLeverOptionsApi,
createAgentApi,
editAgentNameApi,
leverageLeverApi,
} from '@/api/modules/agent'
import { getAllAccount, getWalletCapital,editAccountPass } from '@/api/modules/dashboard.ts'
import { getStorage } from '@/utils/storage'
import { useUserStore } from '@/stores/modules/user.ts'
import QRCode from 'qrcode'
import AnimatedNumber from '@/views/agent/user/AnimatedNumber.vue'
const userStore = useUserStore()
// 判断是否为代理role_id === 5
const isAgent = computed(() => userStore.userInfo?.role_id === 5)
// 如何开始步骤
const startSteps = ref([
{
title: '实名认证',
desc: '完成身份验证',
path: '/profile/kyc',
completed: true
},
{
title: '存款',
desc: '注入资金',
path: '/capital/rechange',
completed: true
},
{
title: '交易',
desc: '开始投资',
path: '/trade/exe',
completed: false
}
])
const currentStep = computed(() => {
const completedCount = startSteps.value.filter(s => s.completed).length
return Math.min(completedCount, startSteps.value.length - 1)
})
// 常用功能
const quickActions = ref([
{
id: 1,
title: '存款',
desc: '安全便捷的充值服务',
path: '/capital/rechange',
icon: CircleCheckFilled,
bgColor: 'linear-gradient(135deg, #e6f4ff 0%, #bae0ff 100%)',
iconColor: '#165DFF'
},
{
id: 2,
title: '取款',
desc: '快速到账资金提取',
path: '/capital/withdraw',
icon: Money,
bgColor: 'linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%)',
iconColor: '#52c41a'
},
{
id: 3,
title: '交易',
desc: '全球金融产品交易',
path: '/trade/exe',
icon: TrendCharts,
bgColor: 'linear-gradient(135deg, #fff7e6 0%, #ffd591 100%)',
iconColor: '#fa8c16'
}
])
// 钱包数据
const walletCapital = ref<any>({})
const getWalletCapitaInfo = async () => {
try {
const res = await getWalletCapital()
console.log('getWalletCapitaInfo', res)
walletCapital.value = res || {}
} catch (error) {
console.error('获取钱包信息失败', error)
}
}
// 账户列表
const accountList = ref<any[]>([])
const getAccoundList = async () => {
try {
const res = await getAllAccount({ username: '' })
console.log(res)
accountList.value = res || []
} catch (error) {
console.error('获取账户列表失败', error)
}
}
// 钱包点击
const handleWalletClick = () => {
console.log('钱包点击')
}
// 添加子账户
const handleAddSubAccount = () => {
dialogVisible.value = true
dialogTitle.value = '添加子账户'
dialogType.value = 'add'
}
// 账户点击
const handleAccountClick = (item: any) => {
console.log('账户点击', item)
}
// 编辑账户
let account_id = ''
const editName = ref('')
const isEdit = ref('')
const inputRef = ref()
const handleEdit = async (name: string, id: string) => {
if (isEdit.value) return (isEdit.value = '')
isEdit.value = id
editName.value = name
account_id = id
setTimeout(() => {
inputRef.value?.focus()
}, 100)
}
// 保存编辑
const handleSave = async () => {
if (account_id === '') return ElMessage.error('请先选择账户')
try {
await editAgentNameApi({
account_id,
account_name: editName.value
})
ElMessage.success('修改成功')
isEdit.value = ''
getAccoundList()
} catch (error) {
ElMessage.error('修改失败')
}
}
// 账户设置
const handleSetting = (id: string) => {
dialogVisible.value = true
dialogTitle.value = '账户设置'
dialogType.value = 'setting'
account_id = id
}
// 弹窗相关
const dialogVisible = ref(false)
const dialogTitle = ref('')
const dialogType = ref('add')
const subAccountForm = ref({
password: '',
leverage: null,
resetPassword: '',
confirmPassword: ''
})
const subAccountFormRef = ref()
const subAccountRules = ref(rules(subAccountForm.value))
const subAccountFormItem = computed(() => {
if (dialogType.value === 'setting') {
return resetPasswordFormItem
} else if (dialogType.value === 'leverage') {
return leverageFormItem
}
return formItem
})
const subAccountFormOptionsMap = ref({
leverage: []
})
const rowGutter = ref(20)
const colSpan = ref(12)
// 获取杠杆选项
const getLeverOptions = async () => {
try {
const data: any = await getLeverOptionsApi()
console.log(data)
subAccountFormOptionsMap.value.leverage = data.map((item: any) => ({
label: item.label,
value: item.value,
}))
} catch (error) {
console.error('获取杠杆选项失败', error)
}
}
// 切换到杠杆表单
const handleChangeToLeverage = () => {
const currentAccount = accountList.value.find((item: any) => item.account_id === account_id)
if (currentAccount?.wallets_trade?.lever) {
subAccountForm.value.leverage = currentAccount.wallets_trade.lever
}
dialogType.value = 'leverage'
}
// 提交表单
const handleSubmit = async () => {
console.log(dialogType.value)
await subAccountFormRef.value.validate()
if (dialogType.value === 'add') {
const params = {
password: subAccountForm.value.password,
lever: subAccountForm.value.leverage
}
await createAgentApi(params)
ElMessage.success('子账户创建成功')
} else if (dialogType.value === 'setting') {
const params = {
user_password: subAccountForm.value.resetPassword,
account_password: subAccountForm.value.confirmPassword,
account_id: account_id || ''
}
await editAccountPass(params)
ElMessage.success('修改成功')
subAccountForm.value.resetPassword = ''
subAccountForm.value.confirmPassword = ''
} else if (dialogType.value === 'leverage') {
const params = {
account_id: account_id || '',
lever: subAccountForm.value.leverage
}
await leverageLeverApi(params)
ElMessage.success('杠杆修改成功')
}
handleClose()
getAccoundList()
}
const handleClose = () => {
dialogVisible.value = false
}
const handleCancel = () => {
dialogVisible.value = false
}
// 推广相关
const userInfo = getStorage<any>('userInfo')
const copyText = ref(userInfo.invite_url)
const qrCanvas = ref()
const promotionStats = ref({
startTime: '2026-03-20'
})
// 生成二维码
const generateBasic = async () => {
const userInfoData = getStorage<any>('userInfo')
if (!userInfoData?.invite_url || !qrCanvas.value) return
try {
await QRCode.toCanvas(qrCanvas.value, userInfoData.invite_url, {
width: 120,
margin: 2,
color: {
dark: '#165DFF',
light: '#ffffff'
}
})
} catch (err) {
console.error('生成二维码失败', err)
}
}
// 复制链接
const handleCopyUrl = async () => {
const success = await copyToClipboard(copyText.value)
if (success) {
ElMessage.success('复制成功')
} else {
ElMessage.error('复制失败,请手动复制')
}
}
async function copyToClipboard(text: string) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text)
return true
}
// 降级方案
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
const success = document.execCommand('copy')
document.body.removeChild(textarea)
return success
} catch (err) {
console.error('复制失败:', err)
return false
}
}
// 初始化
onMounted(async () => {
await Promise.all([
getAccoundList(),
getWalletCapitaInfo(),
getLeverOptions()
])
if (userInfo.type * 1 === 2 && userInfo.role_id * 1 === 5) {
generateBasic()
}
})
</script>
<style scoped lang="scss">
// 变量
$primary-color: #165DFF;
$success-color: #52c41a;
$warning-color: #fa8c16;
$danger-color: #ff4d4f;
$text-primary: #1f1f1f;
$text-secondary: #666666;
$text-tertiary: #999999;
$border-color: #e8e8e8;
$bg-light: #f7f8fa;
$shadow-light: 0 2px 12px rgba(22, 93, 255, 0.08);
$shadow-hover: 0 8px 24px rgba(22, 93, 255, 0.12);
.personal-center {
padding: 24px;
background: $bg-light;
min-height: 100%;
}
.section {
background: #ffffff;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: $shadow-light;
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: $shadow-hover;
}
}
.section-title {
font-size: 18px;
font-weight: 600;
color: $text-primary;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 8px;
.el-icon {
color: $primary-color;
}
.wallet-badge,
.promotion-time {
font-size: 12px;
font-weight: normal;
color: $text-tertiary;
background: $bg-light;
padding: 4px 12px;
border-radius: 4px;
margin-left: auto;
}
.promotion-time {
font-size: 13px;
}
.el-button {
margin-left: auto;
}
}
// 如何开始 - 进度步骤
.getting-started {
.progress-steps {
display: flex;
align-items: flex-start;
justify-content: center;
padding: 20px 0;
}
.step-item {
display: flex;
align-items: center;
position: relative;
flex: 1;
max-width: 200px;
justify-content: center;
&:last-child {
.step-connector {
display: none;
}
}
}
.step-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background: $bg-light;
border: 2px solid $border-color;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
color: $text-tertiary;
transition: all 0.3s ease;
z-index: 2;
.el-icon {
font-size: 24px;
color: #ffffff;
}
}
.step-info {
display: flex;
flex-direction: column;
margin-left: 12px;
}
.step-title {
font-size: 15px;
font-weight: 600;
color: $text-primary;
text-decoration: none;
transition: color 0.3s ease;
&:hover {
color: $primary-color;
}
}
.step-desc {
font-size: 12px;
color: $text-tertiary;
margin-top: 4px;
}
.step-connector {
flex: 1;
height: 2px;
background: $border-color;
margin: 0 16px;
position: relative;
margin-top: -24px;
}
.connector-line {
height: 100%;
width: 0;
background: $primary-color;
transition: width 0.5s ease;
&.filled {
width: 100%;
}
}
.step-item.completed {
.step-icon {
background: $primary-color;
border-color: $primary-color;
color: #ffffff;
}
.step-title {
color: $primary-color;
}
}
.step-item.active {
.step-icon {
background: $primary-color;
border-color: $primary-color;
color: #ffffff;
box-shadow: 0 0 0 4px rgba(22, 93, 255, 0.2);
}
.step-title {
color: $primary-color;
}
}
}
// 常用功能 - 快捷卡片
.quick-actions {
.action-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.action-card {
display: flex;
align-items: center;
padding: 24px;
background: #ffffff;
border: 1px solid $border-color;
border-radius: 12px;
text-decoration: none;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-4px);
box-shadow: $shadow-hover;
border-color: $primary-color;
.card-arrow {
transform: translateX(4px);
color: $primary-color;
}
}
}
.card-icon {
width: 64px;
height: 64px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.card-content {
flex: 1;
margin-left: 16px;
h3 {
font-size: 16px;
font-weight: 600;
color: $text-primary;
margin: 0 0 4px 0;
}
p {
font-size: 13px;
color: $text-tertiary;
margin: 0;
}
}
.card-arrow {
color: $text-tertiary;
transition: all 0.3s ease;
.el-icon {
font-size: 20px;
}
}
}
// 我的钱包 - 通栏大卡片
.my-wallet {
.wallet-card {
background: linear-gradient(135deg, #f0f5ff 0%, #e6f0ff 100%);
border: 1px solid rgba(22, 93, 255, 0.1);
border-radius: 12px;
padding: 32px;
display: flex;
justify-content: space-between;
align-items: flex-start;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
box-shadow: $shadow-hover;
}
}
.wallet-main {
flex: 1;
}
.balance-section {
margin-bottom: 24px;
}
.balance-label {
font-size: 14px;
color: $text-tertiary;
margin-bottom: 8px;
}
.balance-value {
font-size: 42px;
font-weight: 700;
color: $primary-color;
letter-spacing: -1px;
:deep(.animated-number) {
font-variant-numeric: tabular-nums;
}
}
.balance-detail {
display: flex;
gap: 40px;
}
.detail-item {
display: flex;
flex-direction: column;
.label {
font-size: 13px;
color: $text-tertiary;
margin-bottom: 4px;
}
.value {
font-size: 20px;
font-weight: 600;
color: $text-primary;
}
}
.wallet-actions {
display: flex;
gap: 12px;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
background: #ffffff;
border: 1px solid $border-color;
border-radius: 8px;
color: $text-primary;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
.el-icon {
font-size: 16px;
}
&:hover {
background: $primary-color;
border-color: $primary-color;
color: #ffffff;
}
}
}
// 个人账户 - 横向卡片网格
.my-accounts {
.account-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.account-card {
background: #ffffff;
border: 1px solid $border-color;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: translateY(-4px);
box-shadow: $shadow-hover;
border-color: $primary-color;
}
}
.account-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid $border-color;
}
.account-name {
font-size: 16px;
font-weight: 600;
color: $text-primary;
.el-input {
width: 150px;
}
}
.account-actions {
display: flex;
gap: 8px;
.el-icon {
font-size: 16px;
color: $text-tertiary;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.3s ease;
&:hover {
color: $primary-color;
background: rgba(22, 93, 255, 0.1);
}
}
}
.account-number {
font-size: 13px;
color: $text-tertiary;
margin-bottom: 16px;
font-family: 'Monaco', 'Menlo', monospace;
}
.account-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.stat-label {
font-size: 12px;
color: $text-tertiary;
margin-bottom: 4px;
}
.stat-value {
font-size: 16px;
font-weight: 600;
color: $text-primary;
}
.account-loss {
margin-top: 16px;
padding: 12px;
background: rgba(255, 77, 79, 0.05);
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
.loss-label {
font-size: 13px;
color: $danger-color;
}
.loss-value {
font-size: 18px;
font-weight: 700;
color: $danger-color;
}
}
}
// 推广中心 - 左右布局
.promotion {
.promotion-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.promotion-link,
.promotion-qrcode {
h4 {
font-size: 14px;
color: $text-secondary;
margin: 0 0 12px 0;
font-weight: 500;
}
}
.link-box {
display: flex;
gap: 12px;
.link-input {
flex: 1;
padding: 12px 16px;
border: 1px solid $border-color;
border-radius: 8px;
font-size: 14px;
color: $text-primary;
background: $bg-light;
outline: none;
&:focus {
border-color: $primary-color;
}
}
.copy-btn {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6px;
}
}
.qrcode-card {
background: $bg-light;
border-radius: 12px;
padding: 24px;
text-align: center;
canvas {
display: block;
margin: 0 auto 12px;
}
p {
margin: 0;
font-size: 13px;
color: $text-tertiary;
}
}
}
// 弹窗样式
.dialog-tabs {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
// 响应式适配
@media (max-width: 1200px) {
.quick-actions {
.action-cards {
grid-template-columns: repeat(2, 1fr);
}
}
}
@media (max-width: 768px) {
.personal-center {
padding: 16px;
}
.section {
padding: 16px;
margin-bottom: 16px;
}
.getting-started {
.progress-steps {
flex-direction: column;
align-items: center;
gap: 20px;
}
.step-item {
max-width: 100%;
width: 100%;
justify-content: flex-start;
padding-left: 20px;
}
.step-connector {
display: none;
}
}
.quick-actions {
.action-cards {
grid-template-columns: 1fr;
}
}
.my-wallet {
.wallet-card {
flex-direction: column;
gap: 20px;
}
.balance-value {
font-size: 32px;
}
.balance-detail {
flex-wrap: wrap;
gap: 20px;
}
.wallet-actions {
width: 100%;
flex-wrap: wrap;
}
}
.promotion {
.promotion-content {
grid-template-columns: 1fr;
}
}
.my-accounts {
.account-grid {
grid-template-columns: 1fr;
}
}
}
</style>