This commit is contained in:
2026-06-22 13:45:13 +08:00
parent bd0e0c51a4
commit 0af7df1766
18 changed files with 655 additions and 34 deletions

View File

@ -5,6 +5,7 @@ export interface LoginParams {
password: string
login_type: number // 1-邮箱登录 2-手机登录
code?: string // 验证码(可选)
device_no?: string // 设备号(可选)
}
// 登录响应类型
export interface LoginResponse {

View File

@ -344,7 +344,7 @@ const getCustomerList = async (id: string) => {
searchForm.value.endTime.length > 0
? `${searchForm.value.endTime} 23:59:59`
: '2026-03-01 23:59:59',
status: searchForm.value.status,
status: searchForm.value.status || '',
}
const data: any = await getCustomerListApi(params)

View File

@ -1,6 +1,6 @@
export const search = [
// 账号名称
{ prop: 'accountName', label: '账号名称', type: 'input' as const, placeholder: '请输入账号名称', width: '150px' },
{ prop: 'accountName', label: '账号名称', type: 'input' as const, placeholder: '请输入账号名称', width: '240px' },
// 开始时间
{
prop: "startTime", label: '注册开始时间', type: 'date' as const, dateType: 'date' as const, placeholder: '请选择注册开始时间', width: '270px', // 时间范围选择器

View File

@ -83,11 +83,11 @@ const getFundDetailList = async () => {
page: currentPage.value,
page_size: pageSize.value,
start_time:
searchForm.value.startTime.length > 0
searchForm.value.startTime?.length > 0
? `${searchForm.value.startTime} 0:00:00`
: '2026-01-01 0:00:00',
end_time:
searchForm.value.endTime.length > 0
searchForm.value.endTime?.length > 0
? `${searchForm.value.endTime} 23:59:59`
: '2026-03-15 23:59:59',
user_name: searchForm.value.username,

View File

@ -43,6 +43,7 @@ export const search = [
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择开始时间',
width: '220px',
clearable: false,
},
//结束时间
{
@ -53,6 +54,7 @@ export const search = [
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择结束时间',
width: '220px',
clearable: false,
},
]
export const tableColumns = [

View File

@ -0,0 +1,458 @@
<template>
<div class="market-maker-dashboard">
<div class="stats-wrapper">
<div class="stats-grid">
<div
v-for="(item, index) in cardData"
:key="index"
class="stat-box"
:class="`stat-box--${index % 7}`"
>
<div class="stat-box__icon">
<el-icon :size="28">
<component :is="item.icon" />
</el-icon>
</div>
<div class="stat-box__info">
<span class="stat-box__value">
<AnimatedNumber :value="Number(item.value) || 0" :decimals="2" />
</span>
<span class="stat-box__label">{{ item.label }}</span>
</div>
</div>
</div>
</div>
<div class="content-area">
<div class="info-panel">
<div class="panel-header">
<el-icon><User /></el-icon>
<span>当前用户</span>
</div>
<div class="panel-content">
<div class="user-info">
<div class="user-avatar">
<el-icon :size="32"><UserFilled /></el-icon>
</div>
<div class="user-details">
<span class="user-name">{{ userStore.userInfo?.username || '-' }}</span>
<el-tag size="small" :type="tagType">
{{ roleName }}
</el-tag>
</div>
</div>
<div class="info-row">
<span class="info-label">用户ID</span>
<span class="info-value">{{ userStore.userInfo?.id || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">最后登录</span>
<span class="info-value">{{ userStore.userInfo?.last_login || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">账户类型</span>
<span class="info-value">{{ roleName }}</span>
</div>
</div>
</div>
<div class="info-panel">
<div class="panel-header">
<el-icon><Operation /></el-icon>
<span>快捷操作</span>
</div>
<div class="panel-content">
<div class="action-list">
<router-link
v-for="action in quickActions"
:key="action.name"
:to="action.path"
class="action-item"
>
<el-icon :size="18">
<component :is="action.icon" />
</el-icon>
<span>{{ action.name }}</span>
<el-icon class="arrow"><Right /></el-icon>
</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import {
UserFilled,
User,
Money,
Wallet,
Coin,
Timer,
Operation,
Right,
List,
Goods,
Document,
Setting,
TrendCharts,
DataAnalysis,
Trophy,
Lock
} from '@element-plus/icons-vue'
import AnimatedNumber from '@/views/agent/user/AnimatedNumber.vue'
import { getWalletCapital } from '@/api/modules/capital/withdraw'
import { useUserStore } from '@/stores/modules/user'
const props = defineProps<{
roleId: number
roleName: string
pageTitle: string
}>()
defineOptions({
name: 'MarketMakerDashboard'
})
const userStore = useUserStore()
const formattedDate = computed(() => {
const now = new Date()
return `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`
})
//
const tagType = computed<'danger' | 'success' | 'warning' | 'primary'>(() => {
if (props.roleId === 2) return 'warning'
if (props.roleId === 3) return 'danger'
if (props.roleId === 4) return 'success'
return 'primary'
})
const cardData = ref([
{ label: '总金额', value: '0', icon: Money },
{ label: '盈利', value: '0', icon: Trophy },
{ label: '手续费佣金', value: '0', icon: Coin },
{ label: '可提现点差佣金', value: '0', icon: Wallet },
{ label: '冻结点差佣金', value: '0', icon: Lock },
{ label: '服务费', value: '0', icon: DataAnalysis },
{ label: '保证金', value: '0', icon: TrendCharts }
])
const quickActions = ref<{ name: string; path: string; icon: any }[]>([])
const iconMap: Record<string, any> = {
user: User,
list: List,
goods: Goods,
document: Document,
setting: Setting,
money: Money,
trendcharts: TrendCharts,
wallet: Wallet,
coin: Coin,
timer: Timer
}
const initQuickActions = () => {
const actions: { name: string; path: string; icon: any }[] = []
const menus = userStore.menusInfo || []
let count = 0
for (const menu of menus) {
if (count >= 4) break
if (menu.son && menu.son.length > 0) {
for (const child of menu.son) {
if (count >= 4) break
if (child.path === '/dashboard') continue
actions.push({
name: child.name || menu.name,
path: child.path || '/',
icon: iconMap[child.icon || ''] || List
})
count++
}
} else {
if (menu.path === '/dashboard') continue
actions.push({
name: menu.name,
path: menu.path || '/',
icon: iconMap[menu.icon || ''] || List
})
count++
}
}
quickActions.value = actions.slice(0, 4)
}
const fetchWalletData = async () => {
try {
const res: any = await getWalletCapital()
if (res) {
cardData.value = [
{ label: '总金额', value: res.amount ?? 0, icon: Money },
{ label: '盈利', value: res.closing_profit ?? 0, icon: Trophy },
{ label: '手续费佣金', value: res.commission_fee_handling ?? 0, icon: Coin },
{ label: '可提现点差佣金', value: ((res.commission_point_diff || 0) - (res.freeze || 0)) , icon: Wallet },
{ label: '冻结点差佣金', value: res.freeze ?? 0, icon: Lock },
{ label: '服务费', value: res.service ?? 0, icon: DataAnalysis },
{ label: '保证金', value: res.bail ?? 0, icon: TrendCharts }
]
}
} catch (error) {
console.error('获取钱包资金失败:', error)
}
}
onMounted(() => {
initQuickActions()
fetchWalletData()
})
</script>
<style scoped lang="scss">
.market-maker-dashboard {
padding: 20px;
margin: 0 auto;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--el-border-color-light);
h1 {
font-size: 22px;
font-weight: 600;
color: var(--el-text-color-primary);
margin: 0;
}
.date {
font-size: 14px;
color: var(--el-text-color-secondary);
}
}
.stats-wrapper {
margin-bottom: 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
@media (max-width: 1200px) {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 900px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 600px) {
grid-template-columns: 1fr;
}
}
.stat-box {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
background: #fff;
border-radius: 12px;
border: 1px solid var(--el-border-color-lighter);
transition: all 0.25s ease;
&:hover {
border-color: var(--el-color-primary-light-5);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
transform: translateY(-2px);
}
&__icon {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
flex-shrink: 0;
}
&__info {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
&__value {
font-size: 24px;
font-weight: 700;
color: var(--el-text-color-primary);
line-height: 1.2;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__label {
font-size: 14px;
color: var(--el-text-color-secondary);
margin-top: 6px;
}
}
.stat-box--0 .stat-box__icon { background: linear-gradient(135deg, #ecf5ff, #d9ecff); color: #409eff; }
.stat-box--1 .stat-box__icon { background: linear-gradient(135deg, #f0f9eb, #e1f3d8); color: #67c23a; }
.stat-box--2 .stat-box__icon { background: linear-gradient(135deg, #fdf6ec, #fbeccb); color: #e6a23c; }
.stat-box--3 .stat-box__icon { background: linear-gradient(135deg, #fef0f0, #fad7d7); color: #f56c6c; }
.stat-box--4 .stat-box__icon { background: linear-gradient(135deg, #f4f4f5, #e4e4e7); color: #909399; }
.stat-box--5 .stat-box__icon { background: linear-gradient(135deg, #fdf5f6, #f9d7e3); color: #c71585; }
.stat-box--6 .stat-box__icon { background: linear-gradient(135deg, #f0f9ff, #d6ecff); color: #0089e5; }
.content-area {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.info-panel {
background: #fff;
border-radius: 12px;
border: 1px solid var(--el-border-color-lighter);
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
.el-icon {
color: var(--el-color-primary);
}
}
.panel-content {
padding: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px dashed var(--el-border-color-lighter);
.user-avatar {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--el-color-primary-light-9), var(--el-color-primary-light-7));
border-radius: 50%;
color: var(--el-color-primary);
}
.user-details {
display: flex;
flex-direction: column;
gap: 6px;
.user-name {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px dashed var(--el-border-color-lighter);
&:last-child {
border-bottom: none;
}
.info-label {
font-size: 14px;
color: var(--el-text-color-secondary);
}
.info-value {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.action-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.action-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--el-fill-color-light);
border-radius: 10px;
font-size: 14px;
color: var(--el-text-color-regular);
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
&:hover {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
.arrow {
opacity: 1;
}
}
.el-icon {
color: var(--el-color-primary);
flex-shrink: 0;
}
.arrow {
margin-left: auto;
font-size: 12px;
opacity: 0.4;
transition: opacity 0.2s ease;
}
}
</style>

View File

@ -81,6 +81,13 @@
</div>
</div>
</div>
<!-- 承包商首页 -->
<MarketMakerDashboard
v-else-if="userRoleId === 2 || userRoleId === 3 ||userRoleId === 4"
:role-id="userRoleId"
role-name="承包商"
page-title="承包商首页"
/>
<Agent v-else />
</template>
@ -88,6 +95,7 @@
import { onMounted, ref, computed } from 'vue'
import { UserFilled, User, TrendCharts, Money, Wallet, Coin, Timer, Operation, Right, List, Goods, Document, Setting } from '@element-plus/icons-vue'
import Agent from './components/view/index.vue'
import MarketMakerDashboard from './components/marketMaker/index.vue'
import AnimatedNumber from '@/views/agent/user/AnimatedNumber.vue'
import { getUserIndex } from '@/api/modules/dashboard'
import { useUserStore } from '@/stores/modules/user'
@ -98,6 +106,7 @@ defineOptions({
const userStore = useUserStore()
const userId = userStore.userInfo?.id
const userRoleId = userStore.userInfo?.role_id
const loginTime = ref('--')

View File

@ -65,6 +65,7 @@ export const search = [
dateType: 'datetimerange' as const,
placeholder: '请选择时间范围',
format: 'YYYY-MM-DD HH:mm:ss',
clearable: false,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
startPlaceholder: '开始时间',
endPlaceholder: '结束时间',

View File

@ -34,6 +34,7 @@ export const search = [
dateType: 'date' as const,
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择开始时间',
clearable: false,
width: '260px',
},
{
@ -43,6 +44,7 @@ export const search = [
dateType: 'date' as const,
valueFormat: 'YYYY-MM-DD',
placeholder: '请选择结束时间',
clearable: false,
width: '260px',
},
]

View File

@ -14,6 +14,7 @@ export const search = [
dateType: 'datetime' as const,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择开始时间',
clearable: false,
width: '200',
},
{
@ -23,6 +24,7 @@ export const search = [
dateType: 'datetime' as const,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择结束时间',
clearable: false,
width: '200',
},
]

View File

@ -347,7 +347,7 @@ const handleSendCaptcha = async () => {
await sendCaptcha(type, receiver, currentState, () => {
const params: any = {
reg_type: userStore.userInfo!.reg_type,
type: type === 'email' ? 'transfer_code' : 'bindMobile_code'
type: type === 'email' ? 'bindEmail_code' : 'bindMobile_code'
};
if (type === 'email') {
params.email = receiver;

View File

@ -19,12 +19,12 @@
<el-button type="primary" text size="small" @click="openDetailDrawer(row)" v-if="row.id">
查看详情
</el-button>
<el-divider direction="vertical" v-if="row.id" />
<el-button type="primary" text size="small" @click="handleChange(row)" v-anth="'user_editUser'">
<el-divider direction="vertical" v-if="row.id" v-auth="'user_editUser'"/>
<el-button type="primary" text size="small" @click="handleChange(row)" v-auth="'user_editUser'">
修改
</el-button>
<el-divider direction="vertical" v-if="row.id" />
<el-button type="warning" text size="small" @click="handleAdjustAmount(row)" v-anth="'user_updateCapital'">
<el-divider direction="vertical" v-if="row.id" v-auth="'user_updateCapital'"/>
<el-button type="warning" text size="small" @click="handleAdjustAmount(row)" v-auth="'user_updateCapital'">
调整金额
</el-button>
</template>

View File

@ -37,14 +37,14 @@
<el-icon><Postcard /></el-icon>
<span>身份证</span>
</div>
<div
<!-- <div
class="type-item"
:class="{ active: form.idType === 'passport' }"
@click="switchType('passport')"
>
<el-icon><Ticket /></el-icon>
<span>护照</span>
</div>
</div> -->
</div>
<!-- 拍摄提示 -->

View File

@ -35,6 +35,7 @@ export const search = [
{ label: '已发布', value: 2 },
],
width: '220px',
clearable: false,
},
]

View File

@ -73,21 +73,31 @@
import { Monitor, Download, Link } from '@element-plus/icons-vue'
defineOptions({ name: 'Exe' })
const downloadUrlMap: Record<string, string> = {
type DownloadType = 'pc' | 'android' | 'ios'
const downloadUrlMap: Record<DownloadType, string> = {
pc: 'https://adminh5.apimcapital.top/pc/Apim Capital-1.0.24.exe',
android: 'https://adminh5.apimcapital.top/app/xxxx.apk',
android: 'https://adminh5.apimcapital.top/app/app-production-release.apk',
ios: 'https://adminh5.apimcapital.top/app/xxxx.apk'
}
const handleDownload = (type: 'pc' | 'android' | 'ios') => {
const url = downloadUrlMap[type]
const a = document.createElement('a')
a.href = url
if (type === 'pc') a.download = 'Apim Capital-1.0.24.exe'
if (type === 'android') a.download = 'ApimCapital.apk'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
const handleDownload = (type: DownloadType) => {
const url: string = downloadUrlMap[type]
// 使 window.open ,
// a.download SPA
const newWindow = window.open(url, '_blank', 'noopener,noreferrer')
// ,
if (!newWindow) {
const a = document.createElement('a')
a.href = url
a.target = '_blank'
a.rel = 'noopener noreferrer'
if (type === 'pc') a.download = 'Apim Capital-1.0.24.exe'
if (type === 'android') a.download = 'ApimCapital.apk'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
}
</script>

View File

@ -36,6 +36,18 @@
<el-input v-model="loginForm.password" placeholder="登录密码" type="password" show-password size="large" @keyup.enter.prevent="handleLogin" />
</el-form-item>
<el-form-item prop="agreeAgreement">
<div style="display: flex;align-items: center; justify-content: flex-start;">
<el-checkbox v-model="loginForm.agreeAgreement"></el-checkbox>
<div style="font-size: 13px;flex-wrap: wrap;margin-left: 8px;line-height: 17px;">
我已阅读并同意
<span class="doc-link" @click="openDoc('服务条款.docx', '服务条款')">服务条款</span>
<span class="doc-link" @click="openDoc('隐私协议.docx', '隐私协议')">隐私协议</span>
<span class="doc-link" @click="openDoc('风险提示.docx', '风险提示')">风险提示</span>
</div>
</div>
</el-form-item>
<el-button type="primary" class="submit-btn" @click="handleLogin" :loading="buttonLoading"> </el-button>
<div class="link-row">
@ -45,16 +57,33 @@
</el-form>
</div>
</div>
<!-- 协议预览弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="70%" top="3vh">
<div class="doc-preview" v-html="docHtml"></div>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">关闭</el-button>
<el-button type="primary" @click="downloadCurrentDoc">下载文档</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import mammoth from 'mammoth'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/modules/user'
import { useButtonLoading } from '@/composables/useLoading'
import { setStorage, getStorage } from '@/utils/storage'
import { countryOptions } from '@/views/user/config/mock'
import { passwordRules, emailRules, phoneRules } from './config/rules'
import { ElMessage } from 'element-plus'
import termFile from '@/assets/file/服务条款.docx?url'
import privacyFile from '@/assets/file/隐私协议.docx?url'
import riskFile from '@/assets/file/风险提示.docx?url'
import type { FormInstance, FormRules } from 'element-plus'
const router = useRouter()
const { buttonLoading, startButtonLoading, stopButtonLoading } = useButtonLoading()
@ -65,18 +94,69 @@ const tabList = ref([
{ label: '手机登录', name: 'phone' },
])
const activeTab = ref<string>('email')
const loginFormRef = ref()
const loginFormRef = ref<FormInstance>()
const loginForm = ref({
email: '',
countryCode: '+86',
phone: '',
password: '',
agreeAgreement: false,
})
const rules = {
const rules: FormRules = {
email: emailRules().email,
phone: phoneRules(loginForm.value).phone,
password: passwordRules(loginForm.value).password,
agreeAgreement: [
{
validator: (rule: any, value: any, callback: any) => {
if (!value) {
callback(new Error('请勾选同意登录协议'))
} else {
callback()
}
},
trigger: 'change',
},
],
}
//
const dialogVisible = ref(false)
const dialogTitle = ref('')
const docHtml = ref('')
const currentDocName = ref('')
const fileMap: Record<string, string> = {
'服务条款.docx': termFile,
'隐私协议.docx': privacyFile,
'风险提示.docx': riskFile,
}
// docx
async function openDoc(fileName: string, title: string) {
currentDocName.value = fileName
dialogTitle.value = title
dialogVisible.value = true
docHtml.value = '加载中...'
const url = fileMap[fileName]
try {
const res = await fetch(url)
const buf = await res.arrayBuffer()
const ret = await mammoth.convertToHtml({ arrayBuffer: buf })
docHtml.value = ret.value
} catch {
docHtml.value = '加载失败,请下载查看'
}
}
//
function downloadCurrentDoc() {
const url = fileMap[currentDocName.value]
const a = document.createElement('a')
a.href = url
a.download = currentDocName.value
a.click()
}
function generateDeviceNo() {
@ -86,6 +166,12 @@ function generateDeviceNo() {
async function handleLogin() {
if (!loginFormRef.value) return
//
if (!loginForm.value.agreeAgreement) {
ElMessage.error('请勾选同意登录协议')
return
}
try {
await loginFormRef.value.validate()
startButtonLoading()
@ -97,7 +183,7 @@ async function handleLogin() {
mobile: loginForm.value.countryCode + loginForm.value.phone,
login_type: activeTab.value === 'email' ? 1 : 2,
password: loginForm.value.password,
device_no: deviceNo+'',
device_no: deviceNo + '',
})
setStorage('device_no', deviceNo)
} finally {
@ -332,4 +418,50 @@ function handleForgotPassword() {
}
}
}
a {
text-decoration: none;
color: var(--el-color-primary);
margin: 0 4px;
}
.doc-link {
color: var(--el-color-primary);
cursor: pointer;
margin: 0 4px;
&:hover {
text-decoration: underline;
}
}
</style>
<style lang="scss">
/* 弹窗预览全局样式不要scoped */
.doc-preview {
max-height: 65vh;
overflow-y: auto;
line-height: 1.8;
font-size: 15px;
padding: 10px;
h1, h2, h3, h4 {
margin: 16px 0 8px;
}
p {
margin: 8px 0;
}
table {
border-collapse: collapse;
width: 100%;
margin: 12px 0;
td, th {
border: 1px solid #ccc;
padding: 6px 10px;
}
}
}
</style>

View File

@ -172,6 +172,7 @@ const fileMap: Record<string, string> = {
// docx
async function openDoc(fileName: string, title: string) {
currentDocName.value = fileName
dialogTitle.value = title
dialogVisible.value = true
docHtml.value = '加载中...'

View File

@ -53,6 +53,7 @@ export const searchOptions = [
placeholder: '开始时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
format: 'YYYY-MM-DD',
clearable: false,
width: '220px',
defaultTime: 'start' as const,
},
@ -61,6 +62,7 @@ export const searchOptions = [
label: '结束时间',
type: 'date' as const,
dateType: 'datetime' as const,
clearable: false,
placeholder: '结束时间',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
format: 'YYYY-MM-DD',