This commit is contained in:
2026-06-23 09:06:42 +08:00
parent 34de6d5679
commit 1d355d822e
15 changed files with 159 additions and 116 deletions

View File

@ -5,9 +5,11 @@
:breakpoint="992"
>
<TopLoading ref="topLoadingRef" />
<router-view v-slot="{ Component }">
<KeepAlive>
<router-view></router-view>
<component :is="Component" />
</KeepAlive>
</router-view>
</MobileScale>
</template>

View File

@ -15,12 +15,12 @@
<!-- 主体内容区 -->
<main class="main-content">
<router-view v-slot="{ Component }">
<transition
name="fade-transform"
mode="out-in"
style="height: 100%; display: flex; flex-direction: column"
>
<router-view v-slot="{ Component }">
<!-- 使用 :key 确保每次路由变化都会创建新的 ErrorBoundary 实例 -->
<ErrorBoundary :key="route.path + '-' + refreshKey">
<KeepAlive v-if="route.meta.keepAlive">
@ -28,8 +28,8 @@
</KeepAlive>
<component v-else :is="Component" :key="refreshKey" />
</ErrorBoundary>
</router-view>
</transition>
</router-view>
</main>
</section>
</section>
@ -122,6 +122,7 @@ const handleToggleSidebar = (collapsed: boolean) => {
transition: margin-left 0.3s ease;
.main-content {
background-color: #fff;
flex: 1;
overflow-y: auto;
overflow-x: hidden;

View File

@ -7,10 +7,11 @@
:default-active="activeMenu"
:collapse="collapsed"
router
@select="handleSelect"
>
<template v-for="menu in menuList" :key="menu.id">
<!-- 无子菜单 -->
<el-menu-item v-if="!menu.children || menu.children.length === 0" :index="menu.path">
<el-menu-item v-if="!menu.children || menu.children.length === 0" :index="menu.path || `__invalid_${menu.id}`">
<el-icon v-if="menu.icon">
<component :is="iconMap[menu.icon]" />
</el-icon>
@ -18,7 +19,7 @@
</el-menu-item>
<!-- 有子菜单 -->
<el-sub-menu v-else :index="menu.path">
<el-sub-menu v-else :index="menu.path || `__invalid_${menu.id}`">
<template #title>
<el-icon v-if="menu.icon">
<component :is="iconMap[menu.icon]" />
@ -28,7 +29,7 @@
<el-menu-item
v-for="subMenu in menu.children"
:key="subMenu.id"
:index="subMenu.path"
:index="subMenu.path || `__invalid_${subMenu.id}`"
>
<template #title>{{ subMenu.label }}</template>
</el-menu-item>
@ -49,6 +50,7 @@ import {
WarningFilled,
Tools,
} from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/modules/user'
defineProps<{
@ -56,6 +58,7 @@ defineProps<{
}>()
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// index
@ -68,6 +71,24 @@ const menuList = computed(() => {
return userStore.sideMenu || []
})
//
// el-menu router :index
// :pathMatch(.*)* /dashboard
//
const handleSelect = (index: string) => {
if (!index || index.startsWith('__invalid_')) {
ElMessage.warning('该菜单暂未配置访问路径,请联系管理员')
return false
}
//
const hasExact = router.getRoutes().some((r) => r.path === index)
if (!hasExact) {
ElMessage.warning('该页面路由未加载,请稍后重试')
return false
}
return true
}
//
const iconMap: Record<string, any> = {
HomeFilled,
@ -84,11 +105,11 @@ const iconMap: Record<string, any> = {
<style scoped lang="scss">
.sidebar {
position: fixed;
top: 60px;
top: 50px;
left: 0;
bottom: 0;
width: 200px;
background: var(--el-bg-color-page);
background: #fff;
border-right: 1px solid var(--el-border-color-light);
overflow-y: auto;
overflow-x: hidden;

View File

@ -26,17 +26,16 @@ export const handleRouter = (list: any[]):any[] => {
};
export const setRouter = () => {
const menuList = (getStorage('menusInfo') as any[]) || []
const token = getStorage('token')
// 检查是否已存在 dashboard 路由
const hasDashboardRoute = router.getRoutes().some(
(route) => route.path === '/dashboard',
)
if (!token) return
// 用 pathname 集合做幂等去重,避免 addRoute 重复添加同名路由导致匹配失败
const existingPaths = new Set(router.getRoutes().map((r) => r.path))
// 🌟 核心:无论什么角色,始终确保首页路由存在
if (token && !hasDashboardRoute) {
if (!existingPaths.has('/dashboard')) {
router.addRoute('Layout', {
path: '/dashboard',
name: 'Dashboard',
@ -46,16 +45,16 @@ export const setRouter = () => {
keepAlive: false,
},
})
existingPaths.add('/dashboard')
}
if (token && menuList.length > 0) {
if (menuList.length > 0) {
const routeList = handleRouter(menuList).filter(
(item) => item.path && item.path !== '/dashboard',
(item) => item.path && !existingPaths.has(item.path),
)
routeList.forEach((route) => router.addRoute('Layout', route))
}
router.getRoutes().forEach((route) => {
console.log(`路径: ${route.path}, 名称: ${route.name || '无'}`)
routeList.forEach((route) => {
router.addRoute('Layout', route)
existingPaths.add(route.path)
})
}
}

View File

@ -104,25 +104,22 @@ router.beforeEach(async (to, _from) => {
const isLoggedIn = !!userStore.token
const isRealNameVerified = userStore.userInfo?.real_check !== 1
console.log('dynamicRoutesReady', userStore.userInfo)
console.log('isLoggedIn', isLoggedIn)
console.log('isRealNameVerified', isRealNameVerified)
console.log('to.meta.requiresAuth', to.meta.requiresAuth)
console.log('to.meta.isAuthPage', to.meta.isAuthPage)
// 1. 未登录访问需要认证的页 → 登录页
if (!isLoggedIn && to.meta.requiresAuth) {
return '/user/login'
}
// 2. ✅ 动态路由恢复(只执行一次)
// 关键修复:
// a) 不要返回 to.redirectedFrom否则会再次进入守卫形成重入
// b) 返回 next(to.fullPath) 仅在确实新增了路由时触发一次重新匹配;
// c) 用动态路由的 pathname 集合做幂等去重,避免同一路由被 addRoute 多次导致匹配失败。
if (isLoggedIn && !dynamicRoutesReady) {
const menuList = (getStorage('menusInfo') as any[]) || []
// 🌟 核心:无论任何角色,始终确保首页路由存在
const hasDashboardRoute = router.getRoutes().some(
(route) => route.path === '/dashboard',
)
const existingPaths = new Set(router.getRoutes().map((r) => r.path))
const hasDashboardRoute = existingPaths.has('/dashboard')
if (!hasDashboardRoute) {
router.addRoute('Layout', {
path: '/dashboard',
@ -135,15 +132,19 @@ router.beforeEach(async (to, _from) => {
})
}
let added = !hasDashboardRoute
if (menuList.length > 0) {
const routeList = handleRouter(menuList).filter(
(item) => item.path && item.path !== '/dashboard',
(item) => item.path && item.path !== '/dashboard' && !existingPaths.has(item.path),
)
routeList.forEach((route) => router.addRoute('Layout', route))
routeList.forEach((route) => {
router.addRoute('Layout', route)
added = true
})
}
dynamicRoutesReady = true
// 返回目标路由以触发重新匹配
return to.redirectedFrom?.fullPath || true
// 仅当确实新增了路由时才重新匹配目标地址,避免无意义重入
return added ? { path: to.path, query: to.query, hash: to.hash, replace: true } : true
}
// 3. 白名单页面(实名认证、活体认证、登录/注册相关页面)

View File

@ -337,11 +337,11 @@ const getCustomerList = async (id: string) => {
page_size: pageSize.value,
user_name: searchForm.value.accountName,
reg_start_time:
searchForm.value.startTime.length > 0
searchForm.value.startTime?.length > 0
? `${searchForm.value.startTime} 0:00:00`
: '2026-01-01 0:00:00',
reg_end_time:
searchForm.value.endTime.length > 0
searchForm.value.endTime?.length > 0
? `${searchForm.value.endTime} 23:59:59`
: '2026-03-01 23:59:59',
status: searchForm.value.status || '',

View File

@ -92,7 +92,7 @@ const getFundDetailList = async () => {
: '2026-03-15 23:59:59',
user_name: searchForm.value.username,
account_id: searchForm.value.subAccountId,
type: searchForm.value.type,
type: searchForm.value.type || '',
}
//
const data: any = await getFundDetailListApi(params)

View File

@ -84,7 +84,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { onMounted, ref, computed, markRaw } from 'vue'
import {
UserFilled,
User,
@ -135,13 +135,13 @@ const tagType = computed<'danger' | 'success' | 'warning' | '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 }
{ label: '总金额', value: '0', icon: markRaw(Money) },
{ label: '盈利', value: '0', icon: markRaw(Trophy) },
{ label: '手续费佣金', value: '0', icon: markRaw(Coin) },
{ label: '可提现点差佣金', value: '0', icon: markRaw(Wallet) },
{ label: '冻结点差佣金', value: '0', icon: markRaw(Lock) },
{ label: '服务费', value: '0', icon: markRaw(DataAnalysis) },
{ label: '保证金', value: '0', icon: markRaw(TrendCharts) }
])
const quickActions = ref<{ name: string; path: string; icon: any }[]>([])
@ -174,7 +174,7 @@ const initQuickActions = () => {
actions.push({
name: child.name || menu.name,
path: child.path || '/',
icon: iconMap[child.icon || ''] || List
icon: markRaw(iconMap[child.icon || ''] || List)
})
count++
}
@ -183,7 +183,7 @@ const initQuickActions = () => {
actions.push({
name: menu.name,
path: menu.path || '/',
icon: iconMap[menu.icon || ''] || List
icon: markRaw(iconMap[menu.icon || ''] || List)
})
count++
}
@ -197,13 +197,13 @@ const fetchWalletData = async () => {
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 }
{ label: '总金额', value: res.amount ?? 0, icon: markRaw(Money) },
{ label: '盈利', value: res.closing_profit ?? 0, icon: markRaw(Trophy) },
{ label: '手续费佣金', value: res.commission_fee_handling ?? 0, icon: markRaw(Coin) },
{ label: '可提现点差佣金', value: ((res.commission_point_diff || 0) - (res.freeze || 0)) , icon: markRaw(Wallet) },
{ label: '冻结点差佣金', value: res.freeze ?? 0, icon: markRaw(Lock) },
{ label: '服务费', value: res.service ?? 0, icon: markRaw(DataAnalysis) },
{ label: '保证金', value: res.bail ?? 0, icon: markRaw(TrendCharts) }
]
}
} catch (error) {

View File

@ -270,7 +270,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, onMounted, watch, markRaw } from 'vue'
import { ElMessage } from 'element-plus'
import {
Check,
@ -345,7 +345,7 @@ const quickActions = ref([
title: '存款',
desc: '安全便捷的充值服务',
path: '/capital/rechange',
icon: CircleCheckFilled,
icon: markRaw(CircleCheckFilled),
bgColor: 'linear-gradient(135deg, #e6f4ff 0%, #bae0ff 100%)',
iconColor: '#165DFF'
},
@ -354,7 +354,7 @@ const quickActions = ref([
title: '取款',
desc: '快速到账资金提取',
path: '/capital/withdraw',
icon: Money,
icon: markRaw(Money),
bgColor: 'linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%)',
iconColor: '#52c41a'
},
@ -363,7 +363,7 @@ const quickActions = ref([
title: '交易',
desc: '全球金融产品交易',
path: '/trade/exe',
icon: TrendCharts,
icon: markRaw(TrendCharts),
bgColor: 'linear-gradient(135deg, #fff7e6 0%, #ffd591 100%)',
iconColor: '#fa8c16'
}

View File

@ -90,7 +90,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
import { onMounted, ref, computed, markRaw } 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'
@ -114,13 +114,13 @@ const formattedDate = computed(() => {
})
const cardData = ref([
{ label: '代理总数', value: 0, icon: UserFilled },
{ label: '用户总数', value: 0, icon: User },
{ label: '今日新增用户', value: 0, icon: TrendCharts },
{ label: '今日充值金额', value: '0', icon: Money },
{ label: '今日提现金额', value: 0, icon: Wallet },
{ label: '总充值金额', value: 0, icon: Coin },
{ label: '总提现金额', value: 0, icon: Timer }
{ label: '代理总数', value: 0, icon: markRaw(UserFilled) },
{ label: '用户总数', value: 0, icon: markRaw(User) },
{ label: '今日新增用户', value: 0, icon: markRaw(TrendCharts) },
{ label: '今日充值金额', value: '0', icon: markRaw(Money) },
{ label: '今日提现金额', value: 0, icon: markRaw(Wallet) },
{ label: '总充值金额', value: 0, icon: markRaw(Coin) },
{ label: '总提现金额', value: 0, icon: markRaw(Timer) }
])
const quickActions = ref<{ name: string; path: string; icon: any }[]>([])
@ -153,7 +153,7 @@ const initQuickActions = () => {
actions.push({
name: child.name || menu.name,
path: child.path || '/',
icon: iconMap[child.icon || ''] || List
icon: markRaw(iconMap[child.icon || ''] || List)
})
count++
}
@ -162,7 +162,7 @@ const initQuickActions = () => {
actions.push({
name: menu.name,
path: menu.path || '/',
icon: iconMap[menu.icon || ''] || List
icon: markRaw(iconMap[menu.icon || ''] || List)
})
count++
}
@ -179,13 +179,13 @@ const fetchDashboardData = async () => {
today_withdraw_amount, total_recharge_amount, total_withdraw_amount } = res
cardData.value = [
{ label: '代理总数', value: agent_count, icon: UserFilled },
{ label: '用户总数', value: user_count, icon: User },
{ label: '今日新增用户', value: today_user_count, icon: TrendCharts },
{ label: '今日充值金额', value: today_recharge_amount, icon: Money },
{ label: '今日提现金额', value: today_withdraw_amount, icon: Wallet },
{ label: '总充值金额', value: total_recharge_amount, icon: Coin },
{ label: '总提现金额', value: total_withdraw_amount, icon: Timer }
{ label: '代理总数', value: agent_count, icon: markRaw(UserFilled) },
{ label: '用户总数', value: user_count, icon: markRaw(User) },
{ label: '今日新增用户', value: today_user_count, icon: markRaw(TrendCharts) },
{ label: '今日充值金额', value: today_recharge_amount, icon: markRaw(Money) },
{ label: '今日提现金额', value: today_withdraw_amount, icon: markRaw(Wallet) },
{ label: '总充值金额', value: total_recharge_amount, icon: markRaw(Coin) },
{ label: '总提现金额', value: total_withdraw_amount, icon: markRaw(Timer) }
]
}
} catch (error) {
@ -206,7 +206,7 @@ onMounted(() => {
.dashboard {
padding: 20px;
// max-width: 1600px;
margin: 0 auto;
// margin: 0 auto;
}
.page-header {

View File

@ -90,12 +90,12 @@ const getCustomerTradeOrderList = async () => {
const params = {
page: currentPage.value,
page_size: pageSize.value,
user_name: searchForm.value.accountName,
user_name: searchForm.value.accountName || '',
account_id: searchForm.value.subAccountName || '',
order_no: '',
type: searchForm.value.tradeType,
start_time: startTime,
end_time: endTime,
type: searchForm.value.tradeType || '',
start_time: startTime || '',
end_time: endTime || '',
}
console.log(params)
const data: any = await getCustomTradeOrderListApi(params)

View File

@ -1,28 +1,29 @@
import { markRaw } from 'vue'
import { Tools, List, HomeFilled, UserFilled, Platform, BellFilled, GoodsFilled, WarningFilled } from '@element-plus/icons-vue'
// 定义图标映射表配置
export const menuiconMap: Record<string, any> = {
Tools,
List,
HomeFilled,
UserFilled,
Platform,
BellFilled,
GoodsFilled,
WarningFilled
Tools: markRaw(Tools),
List: markRaw(List),
HomeFilled: markRaw(HomeFilled),
UserFilled: markRaw(UserFilled),
Platform: markRaw(Platform),
BellFilled: markRaw(BellFilled),
GoodsFilled: markRaw(GoodsFilled),
WarningFilled: markRaw(WarningFilled)
}
// 定义选择图标选项列表配置
export const menuiconOptions = [
{ label: "无图标", value: "无图标" },
{ label: "Tools(工具)", value: "Tools", icon: Tools },
{ label: "List(列表)", value: "List", icon: List },
{ label: "HomeFilled(首页)", value: "HomeFilled", icon: HomeFilled },
{ label: "UserFilled(用户)", value: "UserFilled", icon: UserFilled },
{ label: "Platform(平台)", value: "Platform", icon: Platform },
{ label: "BellFilled(铃铛)", value: "BellFilled", icon: BellFilled },
{ label: "GoodsFilled(商品)", value: "GoodsFilled", icon: GoodsFilled },
{ label: "WarningFilled(警告)", value: "WarningFilled", icon: WarningFilled },
{ label: "Tools(工具)", value: "Tools", icon: markRaw(Tools) },
{ label: "List(列表)", value: "List", icon: markRaw(List) },
{ label: "HomeFilled(首页)", value: "HomeFilled", icon: markRaw(HomeFilled) },
{ label: "UserFilled(用户)", value: "UserFilled", icon: markRaw(UserFilled) },
{ label: "Platform(平台)", value: "Platform", icon: markRaw(Platform) },
{ label: "BellFilled(铃铛)", value: "BellFilled", icon: markRaw(BellFilled) },
{ label: "GoodsFilled(商品)", value: "GoodsFilled", icon: markRaw(GoodsFilled) },
{ label: "WarningFilled(警告)", value: "WarningFilled", icon: markRaw(WarningFilled) },
]
//定义table配置

View File

@ -8,6 +8,18 @@
<el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
<template #tradeStatus>
<el-select v-model="searchForm.tradeStatus" placeholder="请选择状态" clearable style="width: 200px;">
<el-option :value="1" label="开启" />
<el-option :value="2" label="禁用" />
</el-select>
</template>
<template #onlineStatus>
<el-select v-model="searchForm.onlineStatus" placeholder="请选择上线状态" clearable style="width: 200px;">
<el-option :value="1" label="上线" />
<el-option :value="2" label="下线" />
</el-select>
</template>
<template #button="{ form }">
<el-button type="primary" :disabled="!selectedRow" @click="handleEdit()" v-auth="'contractVariety_editVariety'">
编辑
@ -103,7 +115,9 @@ import { uploadImageApi } from '@/api/modules/upload'
*/
const searchForm = ref({
varietyName: '',
type:''
type:'',
tradeStatus: '',
onlineStatus: '',
})
const searchOptions = ref(search)
@ -235,6 +249,8 @@ const getVarietyList = async () => {
const params = {
name: searchForm.value.varietyName,
type: searchForm.value.type,
online_status: searchForm.value.onlineStatus || '',
status: searchForm.value.tradeStatus || ''
}
try {
const data = await getVarietyListApi(params)

View File

@ -5,6 +5,8 @@ export const search = [
// 品种名称
{ prop: 'varietyName', label: '品种名称', type: 'input' as const, placeholder: '请输入品种名称' },
{ prop: 'type', label: '类型', type: 'select' as const, placeholder: '请选择类型', slot: true, },
{ prop: 'tradeStatus', label: '状态', type: 'select' as const, placeholder: '请选择状态', slot: true, },
{ prop: 'onlineStatus', label: '上线状态', type: 'select' as const, placeholder: '请选择上线状态', slot: true, },
]
/**

View File

@ -1,9 +1,9 @@
<template>
<div class="user-container">
<router-view v-slot="{ Component }">
<transition name="user" mode="out-in">
<div class="user-container">
<component :is="Component" />
</div>
</transition>
</router-view>
</div>
</template>