更新政府端和银行端

This commit is contained in:
2025-09-17 18:04:28 +08:00
parent f35ceef31f
commit e4287b83fe
185 changed files with 78320 additions and 189 deletions

294
bank-frontend/src/App.vue Normal file
View File

@@ -0,0 +1,294 @@
<template>
<div v-if="isLoggedIn">
<!-- 移动端布局 -->
<div v-if="isMobile" class="mobile-layout">
<MobileNav ref="mobileNavRef" />
<div class="mobile-content">
<router-view />
</div>
</div>
<!-- 桌面端布局 -->
<a-layout v-else style="min-height: 100vh">
<a-layout-header class="header">
<div class="logo">
<a-button
type="text"
@click="settingsStore.toggleSidebar"
style="color: white; margin-right: 8px;"
>
<menu-unfold-outlined v-if="sidebarCollapsed" />
<menu-fold-outlined v-else />
</a-button>
银行管理后台系统
</div>
<div class="user-info">
<a-dropdown>
<a-button type="text" style="color: white;">
<user-outlined />
{{ userData?.real_name || userData?.username }}
<down-outlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="goToProfile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="settings" @click="goToSettings">
<setting-outlined />
系统设置
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<a-layout>
<a-layout-sider
width="200"
style="background: #001529"
:collapsed="sidebarCollapsed"
collapsible
>
<DynamicMenu :collapsed="sidebarCollapsed" />
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: '16px 0' }"
>
<router-view />
</a-layout-content>
<a-layout-footer style="text-align: center">
银行管理后台系统 ©2025
</a-layout-footer>
</a-layout>
</a-layout>
</a-layout>
</div>
<div v-else>
<router-view />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import DynamicMenu from './components/DynamicMenu.vue'
import MobileNav from './components/MobileNav.vue'
import { useUserStore, useSettingsStore } from './stores'
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
UserOutlined,
DownOutlined,
SettingOutlined,
LogoutOutlined
} from '@ant-design/icons-vue'
// 使用Pinia状态管理
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const router = useRouter()
// 移动端导航引用
const mobileNavRef = ref()
// 响应式检测
const isMobile = ref(false)
// 检测屏幕尺寸
const checkScreenSize = () => {
isMobile.value = window.innerWidth <= 768
}
// 计算属性
const isLoggedIn = computed(() => userStore.isLoggedIn)
const userData = computed(() => userStore.userData)
const sidebarCollapsed = computed(() => settingsStore.sidebarCollapsed)
// 监听多标签页登录状态同步
const handleStorageChange = (event) => {
if (event.key === 'bank_token' || event.key === 'bank_user') {
userStore.checkLoginStatus()
}
}
// 登出处理
const handleLogout = async () => {
try {
await userStore.logout()
router.push('/login')
} catch (error) {
console.error('登出失败:', error)
}
}
// 跳转到个人中心
const goToProfile = () => {
router.push('/profile')
}
// 跳转到系统设置
const goToSettings = () => {
router.push('/settings')
}
onMounted(() => {
userStore.checkLoginStatus()
checkScreenSize()
window.addEventListener('storage', handleStorageChange)
window.addEventListener('resize', checkScreenSize)
})
onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange)
window.removeEventListener('resize', checkScreenSize)
})
</script>
<style scoped>
/* 桌面端样式 */
.header {
display: flex;
align-items: center;
justify-content: space-between;
background: #001529;
color: white;
padding: 0 24px;
}
.logo {
font-size: 18px;
font-weight: bold;
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
}
/* 移动端布局样式 */
.mobile-layout {
min-height: 100vh;
background: #f0f2f5;
}
.mobile-content {
padding: 12px;
padding-top: 68px; /* 为固定头部留出空间 */
min-height: calc(100vh - 56px);
}
/* 移动端页面内容优化 */
.mobile-layout :deep(.page-header) {
flex-direction: column;
gap: 12px;
align-items: stretch;
}
.mobile-layout :deep(.search-area) {
flex-direction: column;
gap: 8px;
}
.mobile-layout :deep(.search-input) {
width: 100%;
}
.mobile-layout :deep(.search-buttons) {
display: flex;
gap: 8px;
}
.mobile-layout :deep(.search-buttons .ant-btn) {
flex: 1;
height: 40px;
}
/* 移动端表格优化 */
.mobile-layout :deep(.ant-table-wrapper) {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.mobile-layout :deep(.ant-table) {
min-width: 600px;
}
.mobile-layout :deep(.ant-table-thead > tr > th) {
padding: 8px 4px;
font-size: 12px;
white-space: nowrap;
}
.mobile-layout :deep(.ant-table-tbody > tr > td) {
padding: 8px 4px;
font-size: 12px;
}
/* 移动端模态框优化 */
.mobile-layout :deep(.ant-modal) {
margin: 0 !important;
width: 100vw !important;
max-width: 100vw !important;
top: 0 !important;
}
.mobile-layout :deep(.ant-modal-content) {
border-radius: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
.mobile-layout :deep(.ant-modal-body) {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.mobile-layout :deep(.ant-modal-footer) {
border-top: 1px solid #f0f0f0;
padding: 12px 16px;
}
/* 移动端卡片优化 */
.mobile-layout :deep(.ant-card) {
margin-bottom: 12px;
border-radius: 8px;
}
.mobile-layout :deep(.ant-card-body) {
padding: 12px;
}
/* 移动端按钮优化 */
.mobile-layout :deep(.ant-btn) {
min-height: 40px;
border-radius: 6px;
}
.mobile-layout :deep(.ant-space) {
width: 100%;
}
.mobile-layout :deep(.ant-space-item) {
flex: 1;
}
.mobile-layout :deep(.ant-space-item .ant-btn) {
width: 100%;
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<a-menu
:default-selected-keys="[currentRoute]"
:default-open-keys="openKeys"
mode="inline"
theme="dark"
:inline-collapsed="collapsed"
@click="handleMenuClick"
@openChange="handleOpenChange"
>
<template v-for="item in menuItems" :key="item.key">
<a-menu-item v-if="!item.children" :key="item.key">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a-menu-item>
<a-sub-menu v-else :key="item.key">
<template #title>
<component :is="item.icon" />
<span>{{ item.title }}</span>
</template>
<a-menu-item
v-for="child in item.children"
:key="child.key"
>
{{ child.title }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { getMenuItems } from '@/router/routes'
import routes from '@/router/routes'
const props = defineProps({
collapsed: {
type: Boolean,
default: false
}
})
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 当前路由
const currentRoute = computed(() => route.name)
// 展开的菜单项
const openKeys = ref([])
// 菜单项
const menuItems = computed(() => {
const userRole = userStore.getUserRoleName()
return getMenuItems(routes, userRole)
})
// 处理菜单点击
const handleMenuClick = ({ key }) => {
const menuItem = findMenuItem(menuItems.value, key)
if (menuItem && menuItem.path) {
router.push(menuItem.path)
}
}
// 处理子菜单展开/收起
const handleOpenChange = (keys) => {
openKeys.value = keys
}
// 查找菜单项
const findMenuItem = (items, key) => {
for (const item of items) {
if (item.key === key) {
return item
}
if (item.children) {
const found = findMenuItem(item.children, key)
if (found) return found
}
}
return null
}
// 监听路由变化,自动展开对应的子菜单
watch(
() => route.path,
(newPath) => {
const pathSegments = newPath.split('/').filter(Boolean)
if (pathSegments.length > 1) {
const parentKey = pathSegments[0]
if (!openKeys.value.includes(parentKey)) {
openKeys.value = [parentKey]
}
}
},
{ immediate: true }
)
</script>
<style scoped>
.ant-menu {
border-right: none;
}
.ant-menu-item,
.ant-menu-submenu-title {
display: flex;
align-items: center;
}
.ant-menu-item .anticon,
.ant-menu-submenu-title .anticon {
margin-right: 8px;
font-size: 16px;
}
.ant-menu-item-selected {
background-color: #1890ff !important;
}
.ant-menu-item-selected::after {
display: none;
}
.ant-menu-submenu-selected > .ant-menu-submenu-title {
color: #1890ff;
}
.ant-menu-submenu-open > .ant-menu-submenu-title {
color: #1890ff;
}
</style>

View File

@@ -0,0 +1,279 @@
<template>
<a-layout-header class="mobile-header">
<div class="mobile-header-content">
<a-button
type="text"
@click="toggleDrawer"
class="menu-button"
>
<menu-outlined />
</a-button>
<div class="header-title">
银行管理系统
</div>
<a-dropdown>
<a-button type="text" class="user-button">
<user-outlined />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="profile" @click="goToProfile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 移动端抽屉菜单 -->
<a-drawer
v-model:open="drawerVisible"
placement="left"
:width="280"
:body-style="{ padding: 0 }"
>
<div class="drawer-content">
<div class="drawer-header">
<div class="user-info">
<a-avatar :size="48" :src="userData?.avatar">
<user-outlined />
</a-avatar>
<div class="user-details">
<div class="user-name">{{ userData?.real_name || userData?.username }}</div>
<div class="user-role">{{ userData?.role?.display_name || '用户' }}</div>
</div>
</div>
</div>
<a-menu
:default-selected-keys="[currentRoute]"
mode="inline"
theme="light"
@click="handleMenuClick"
>
<template v-for="item in menuItems" :key="item.key">
<a-menu-item v-if="!item.children" :key="item.key">
<component :is="item.icon" />
<span>{{ item.title }}</span>
</a-menu-item>
<a-sub-menu v-else :key="item.key">
<template #title>
<component :is="item.icon" />
<span>{{ item.title }}</span>
</template>
<a-menu-item
v-for="child in item.children"
:key="child.key"
>
{{ child.title }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</div>
</a-drawer>
</a-layout-header>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores'
import { getMenuItems } from '@/router/routes'
import routes from '@/router/routes'
import {
MenuOutlined,
UserOutlined,
LogoutOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 抽屉显示状态
const drawerVisible = ref(false)
// 当前路由
const currentRoute = computed(() => route.name)
// 用户数据
const userData = computed(() => userStore.userData)
// 菜单项
const menuItems = computed(() => {
const userRole = userStore.getUserRoleName()
return getMenuItems(routes, userRole)
})
// 切换抽屉
const toggleDrawer = () => {
drawerVisible.value = !drawerVisible.value
}
// 处理菜单点击
const handleMenuClick = ({ key }) => {
const menuItem = findMenuItem(menuItems.value, key)
if (menuItem && menuItem.path) {
router.push(menuItem.path)
drawerVisible.value = false // 点击后关闭抽屉
}
}
// 查找菜单项
const findMenuItem = (items, key) => {
for (const item of items) {
if (item.key === key) {
return item
}
if (item.children) {
const found = findMenuItem(item.children, key)
if (found) return found
}
}
return null
}
// 跳转到个人中心
const goToProfile = () => {
router.push('/profile')
drawerVisible.value = false
}
// 登出处理
const handleLogout = async () => {
try {
await userStore.logout()
router.push('/login')
} catch (error) {
console.error('登出失败:', error)
}
}
</script>
<style scoped>
.mobile-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: #001529;
padding: 0;
height: 56px;
line-height: 56px;
}
.mobile-header-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 16px;
}
.menu-button,
.user-button {
color: white !important;
border: none !important;
background: transparent !important;
font-size: 18px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.menu-button:hover,
.user-button:hover {
background: rgba(255, 255, 255, 0.1) !important;
}
.header-title {
color: white;
font-size: 16px;
font-weight: 600;
flex: 1;
text-align: center;
margin: 0 16px;
}
.drawer-content {
height: 100%;
display: flex;
flex-direction: column;
}
.drawer-header {
padding: 24px 16px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
}
.user-info {
display: flex;
align-items: center;
gap: 12px;
}
.user-details {
flex: 1;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #262626;
margin-bottom: 4px;
}
.user-role {
font-size: 14px;
color: #8c8c8c;
}
.ant-menu {
flex: 1;
border-right: none;
}
.ant-menu-item,
.ant-menu-submenu-title {
display: flex;
align-items: center;
height: 48px;
line-height: 48px;
}
.ant-menu-item .anticon,
.ant-menu-submenu-title .anticon {
margin-right: 12px;
font-size: 16px;
}
.ant-menu-item-selected {
background-color: #e6f7ff !important;
color: #1890ff !important;
}
.ant-menu-item-selected::after {
display: none;
}
.ant-menu-submenu-selected > .ant-menu-submenu-title {
color: #1890ff;
}
.ant-menu-submenu-open > .ant-menu-submenu-title {
color: #1890ff;
}
</style>

View File

@@ -0,0 +1,97 @@
/**
* 环境配置
* @file env.js
* @description 应用环境配置管理
*/
// 获取环境变量
const getEnvVar = (key, defaultValue = '') => {
return import.meta.env[key] || defaultValue
}
// API配置
export const API_CONFIG = {
// API基础URL
baseUrl: getEnvVar('VITE_API_BASE_URL', 'http://localhost:5351'),
// 是否使用代理
useProxy: getEnvVar('VITE_USE_PROXY', 'true') === 'true',
// 完整的基础URL用于直接请求
fullBaseUrl: getEnvVar('VITE_API_BASE_URL', 'http://localhost:5351'),
// 请求超时时间
timeout: parseInt(getEnvVar('VITE_API_TIMEOUT', '10000')),
// 重试次数
retryCount: parseInt(getEnvVar('VITE_API_RETRY_COUNT', '3'))
}
// 应用配置
export const APP_CONFIG = {
// 应用标题
title: getEnvVar('VITE_APP_TITLE', '银行管理后台系统'),
// 应用版本
version: getEnvVar('VITE_APP_VERSION', '1.0.0'),
// 应用描述
description: getEnvVar('VITE_APP_DESCRIPTION', '专业的银行管理解决方案'),
// 是否显示版本信息
showVersion: getEnvVar('VITE_SHOW_VERSION', 'true') === 'true',
// 是否启用调试模式
debug: getEnvVar('VITE_DEBUG', 'false') === 'true'
}
// 主题配置
export const THEME_CONFIG = {
// 主色调
primaryColor: getEnvVar('VITE_PRIMARY_COLOR', '#1890ff'),
// 是否启用暗色主题
darkMode: getEnvVar('VITE_DARK_MODE', 'false') === 'true',
// 是否启用紧凑模式
compactMode: getEnvVar('VITE_COMPACT_MODE', 'false') === 'true'
}
// 功能配置
export const FEATURE_CONFIG = {
// 是否启用实时通知
enableNotification: getEnvVar('VITE_ENABLE_NOTIFICATION', 'true') === 'true',
// 是否启用数据导出
enableExport: getEnvVar('VITE_ENABLE_EXPORT', 'true') === 'true',
// 是否启用数据导入
enableImport: getEnvVar('VITE_ENABLE_IMPORT', 'true') === 'true',
// 是否启用图表功能
enableCharts: getEnvVar('VITE_ENABLE_CHARTS', 'true') === 'true'
}
// 安全配置
export const SECURITY_CONFIG = {
// Token存储键名
tokenKey: 'bank_token',
// 用户信息存储键名
userKey: 'bank_user',
// Token过期时间小时
tokenExpireHours: parseInt(getEnvVar('VITE_TOKEN_EXPIRE_HOURS', '24')),
// 是否启用自动登录
enableAutoLogin: getEnvVar('VITE_ENABLE_AUTO_LOGIN', 'true') === 'true'
}
// 导出所有配置
export default {
API_CONFIG,
APP_CONFIG,
THEME_CONFIG,
FEATURE_CONFIG,
SECURITY_CONFIG
}

44
bank-frontend/src/main.js Normal file
View File

@@ -0,0 +1,44 @@
/**
* 应用入口文件
* @file main.js
* @description Vue应用初始化和配置
*/
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import { themeConfig } from './styles/theme.js'
import './styles/global.css'
import './styles/responsive.css'
import { useUserStore, useSettingsStore } from './stores'
// 导入图标组件
import * as Icons from '@ant-design/icons-vue'
const app = createApp(App)
const pinia = createPinia()
// 注册所有图标组件
Object.keys(Icons).forEach(key => {
app.component(key, Icons[key])
})
app.use(pinia)
app.use(router)
app.use(Antd, {
theme: themeConfig
})
// 在应用挂载前初始化用户登录状态和设置
const userStore = useUserStore()
const settingsStore = useSettingsStore()
// 检查登录状态
userStore.checkLoginStatus()
// 加载用户设置
settingsStore.loadSettings()
app.mount('#app')

View File

@@ -0,0 +1,75 @@
/**
* 路由配置
* @file index.js
* @description Vue Router 配置和路由守卫
*/
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores'
import routes from './routes'
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 如果有保存的位置,则恢复到保存的位置
if (savedPosition) {
return savedPosition
}
// 否则滚动到顶部
return { top: 0 }
}
})
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 银行管理后台系统` : '银行管理后台系统'
// 获取用户存储
const userStore = useUserStore()
// 如果访问登录页面且已有有效token重定向到仪表盘
if (to.path === '/login' && userStore.token && userStore.isLoggedIn) {
const redirectPath = to.query.redirect || '/dashboard'
next(redirectPath)
return
}
// 检查该路由是否需要登录权限
if (to.meta.requiresAuth) {
// 如果需要登录但用户未登录,则重定向到登录页面
if (!userStore.isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath } // 保存原本要访问的路径,以便登录后重定向
})
} else {
// 检查用户角色权限
if (to.meta.roles && to.meta.roles.length > 0) {
const userRole = userStore.getUserRoleName()
if (!to.meta.roles.includes(userRole)) {
// 用户角色不匹配,重定向到仪表盘
next('/dashboard')
} else {
// 用户已登录且角色匹配,允许访问
next()
}
} else {
// 没有角色限制,允许访问
next()
}
}
} else {
// 不需要登录权限的路由,直接访问
next()
}
})
// 全局后置钩子
router.afterEach((to, from) => {
// 路由切换后的逻辑,如记录访问历史、分析等
console.log(`路由从 ${from.path} 切换到 ${to.path}`)
})
export default router

View File

@@ -0,0 +1,149 @@
/**
* 路由配置
* @file routes.js
* @description 应用路由定义
*/
import {
DashboardOutlined,
UserOutlined,
BankOutlined,
TransactionOutlined,
BarChartOutlined,
SettingOutlined,
LoginOutlined
} from '@ant-design/icons-vue'
// 路由配置
const routes = [
{
path: '/',
redirect: '/dashboard'
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: {
title: '登录',
requiresAuth: false,
hideInMenu: true
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: {
title: '仪表盘',
icon: DashboardOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/users',
name: 'Users',
component: () => import('@/views/Users.vue'),
meta: {
title: '用户管理',
icon: UserOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/accounts',
name: 'Accounts',
component: () => import('@/views/Accounts.vue'),
meta: {
title: '账户管理',
icon: BankOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/transactions',
name: 'Transactions',
component: () => import('@/views/Transactions.vue'),
meta: {
title: '交易管理',
icon: TransactionOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/reports',
name: 'Reports',
component: () => import('@/views/Reports.vue'),
meta: {
title: '报表统计',
icon: BarChartOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/settings',
name: 'Settings',
component: () => import('@/views/Settings.vue'),
meta: {
title: '系统设置',
icon: SettingOutlined,
requiresAuth: true,
roles: ['admin']
}
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile.vue'),
meta: {
title: '个人中心',
requiresAuth: true,
hideInMenu: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
meta: {
title: '页面不存在',
requiresAuth: false,
hideInMenu: true
}
}
]
// 根据用户角色过滤路由
export function filterRoutesByRole(routes, userRole) {
return routes.filter(route => {
// 如果路由没有meta或没有角色限制则允许访问
if (!route.meta || !route.meta.roles) {
return true
}
// 检查用户角色是否在允许的角色列表中
return route.meta.roles.includes(userRole)
})
}
// 获取菜单项
export function getMenuItems(routes, userRole) {
const filteredRoutes = filterRoutesByRole(routes, userRole)
return filteredRoutes
.filter(route => !route.meta || !route.meta.hideInMenu)
.map(route => ({
key: route.name,
title: route.meta?.title || route.name,
icon: route.meta?.icon,
path: route.path,
children: route.children ? getMenuItems(route.children, userRole) : undefined
}))
}
export default routes

View File

@@ -0,0 +1,7 @@
/**
* 状态管理索引
* @file index.js
* @description 导出所有状态管理模块
*/
export { useUserStore } from './user'
export { useSettingsStore } from './settings'

View File

@@ -0,0 +1,269 @@
/**
* 设置状态管理
* @file settings.js
* @description 应用设置相关的状态管理
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
// 侧边栏状态
const sidebarCollapsed = ref(false)
// 主题设置
const theme = ref('light') // light, dark
const primaryColor = ref('#1890ff')
// 语言设置
const language = ref('zh-CN')
// 页面设置
const pageSize = ref(10)
const showBreadcrumb = ref(true)
const showFooter = ref(true)
// 表格设置
const tableSize = ref('middle') // small, middle, large
const tableBordered = ref(false)
const tableStriped = ref(true)
// 表单设置
const formSize = ref('middle')
const formLabelAlign = ref('right')
// 动画设置
const enableAnimation = ref(true)
const animationDuration = ref(300)
// 通知设置
const enableNotification = ref(true)
const enableSound = ref(false)
// 数据设置
const autoRefresh = ref(true)
const refreshInterval = ref(30000) // 30秒
// 切换侧边栏
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
}
// 设置侧边栏状态
function setSidebarCollapsed(collapsed) {
sidebarCollapsed.value = collapsed
}
// 切换主题
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// 设置主题
function setTheme(newTheme) {
theme.value = newTheme
}
// 设置主色调
function setPrimaryColor(color) {
primaryColor.value = color
}
// 设置语言
function setLanguage(lang) {
language.value = lang
}
// 设置页面大小
function setPageSize(size) {
pageSize.value = size
}
// 设置表格大小
function setTableSize(size) {
tableSize.value = size
}
// 设置表格边框
function setTableBordered(bordered) {
tableBordered.value = bordered
}
// 设置表格斑马纹
function setTableStriped(striped) {
tableStriped.value = striped
}
// 设置表单大小
function setFormSize(size) {
formSize.value = size
}
// 设置表单标签对齐
function setFormLabelAlign(align) {
formLabelAlign.value = align
}
// 设置动画
function setAnimation(enabled, duration = 300) {
enableAnimation.value = enabled
animationDuration.value = duration
}
// 设置通知
function setNotification(enabled, sound = false) {
enableNotification.value = enabled
enableSound.value = sound
}
// 设置自动刷新
function setAutoRefresh(enabled, interval = 30000) {
autoRefresh.value = enabled
refreshInterval.value = interval
}
// 重置设置
function resetSettings() {
sidebarCollapsed.value = false
theme.value = 'light'
primaryColor.value = '#1890ff'
language.value = 'zh-CN'
pageSize.value = 10
showBreadcrumb.value = true
showFooter.value = true
tableSize.value = 'middle'
tableBordered.value = false
tableStriped.value = true
formSize.value = 'middle'
formLabelAlign.value = 'right'
enableAnimation.value = true
animationDuration.value = 300
enableNotification.value = true
enableSound.value = false
autoRefresh.value = true
refreshInterval.value = 30000
}
// 保存设置到本地存储
function saveSettings() {
const settings = {
sidebarCollapsed: sidebarCollapsed.value,
theme: theme.value,
primaryColor: primaryColor.value,
language: language.value,
pageSize: pageSize.value,
showBreadcrumb: showBreadcrumb.value,
showFooter: showFooter.value,
tableSize: tableSize.value,
tableBordered: tableBordered.value,
tableStriped: tableStriped.value,
formSize: formSize.value,
formLabelAlign: formLabelAlign.value,
enableAnimation: enableAnimation.value,
animationDuration: animationDuration.value,
enableNotification: enableNotification.value,
enableSound: enableSound.value,
autoRefresh: autoRefresh.value,
refreshInterval: refreshInterval.value
}
localStorage.setItem('bank_settings', JSON.stringify(settings))
}
// 从本地存储加载设置
function loadSettings() {
try {
const saved = localStorage.getItem('bank_settings')
if (saved) {
const settings = JSON.parse(saved)
sidebarCollapsed.value = settings.sidebarCollapsed ?? false
theme.value = settings.theme ?? 'light'
primaryColor.value = settings.primaryColor ?? '#1890ff'
language.value = settings.language ?? 'zh-CN'
pageSize.value = settings.pageSize ?? 10
showBreadcrumb.value = settings.showBreadcrumb ?? true
showFooter.value = settings.showFooter ?? true
tableSize.value = settings.tableSize ?? 'middle'
tableBordered.value = settings.tableBordered ?? false
tableStriped.value = settings.tableStriped ?? true
formSize.value = settings.formSize ?? 'middle'
formLabelAlign.value = settings.formLabelAlign ?? 'right'
enableAnimation.value = settings.enableAnimation ?? true
animationDuration.value = settings.animationDuration ?? 300
enableNotification.value = settings.enableNotification ?? true
enableSound.value = settings.enableSound ?? false
autoRefresh.value = settings.autoRefresh ?? true
refreshInterval.value = settings.refreshInterval ?? 30000
}
} catch (error) {
console.error('加载设置失败:', error)
}
}
// 获取设置快照
function getSettingsSnapshot() {
return {
sidebarCollapsed: sidebarCollapsed.value,
theme: theme.value,
primaryColor: primaryColor.value,
language: language.value,
pageSize: pageSize.value,
showBreadcrumb: showBreadcrumb.value,
showFooter: showFooter.value,
tableSize: tableSize.value,
tableBordered: tableBordered.value,
tableStriped: tableStriped.value,
formSize: formSize.value,
formLabelAlign: formLabelAlign.value,
enableAnimation: enableAnimation.value,
animationDuration: animationDuration.value,
enableNotification: enableNotification.value,
enableSound: enableSound.value,
autoRefresh: autoRefresh.value,
refreshInterval: refreshInterval.value
}
}
return {
// 状态
sidebarCollapsed,
theme,
primaryColor,
language,
pageSize,
showBreadcrumb,
showFooter,
tableSize,
tableBordered,
tableStriped,
formSize,
formLabelAlign,
enableAnimation,
animationDuration,
enableNotification,
enableSound,
autoRefresh,
refreshInterval,
// 方法
toggleSidebar,
setSidebarCollapsed,
toggleTheme,
setTheme,
setPrimaryColor,
setLanguage,
setPageSize,
setTableSize,
setTableBordered,
setTableStriped,
setFormSize,
setFormLabelAlign,
setAnimation,
setNotification,
setAutoRefresh,
resetSettings,
saveSettings,
loadSettings,
getSettingsSnapshot
}
})

View File

@@ -0,0 +1,230 @@
/**
* 用户状态管理
* @file user.js
* @description 用户相关的状态管理
*/
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { SECURITY_CONFIG } from '@/config/env'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref(localStorage.getItem(SECURITY_CONFIG.tokenKey) || '')
const userData = ref(JSON.parse(localStorage.getItem(SECURITY_CONFIG.userKey) || 'null'))
const isLoggedIn = computed(() => !!token.value && !!userData.value)
// 检查登录状态
function checkLoginStatus() {
const savedToken = localStorage.getItem(SECURITY_CONFIG.tokenKey)
const savedUser = localStorage.getItem(SECURITY_CONFIG.userKey)
if (savedToken && savedUser) {
try {
token.value = savedToken
userData.value = JSON.parse(savedUser)
return true
} catch (error) {
console.error('解析用户数据失败', error)
logout()
return false
}
}
return false
}
// 检查token是否有效
async function validateToken() {
if (!token.value) {
return false
}
try {
const { api } = await import('@/utils/api')
// 尝试调用一个需要认证的API来验证token
await api.get('/users/profile')
return true
} catch (error) {
if (error.message && error.message.includes('认证已过期')) {
logout()
return false
}
// 其他错误可能是网络问题不清除token
return true
}
}
// 登录操作
async function login(username, password, retryCount = 0) {
try {
const { api } = await import('@/utils/api')
const result = await api.login(username, password)
// 登录成功后设置token和用户数据
if (result.success && result.data.token) {
token.value = result.data.token
userData.value = {
id: result.data.user.id,
username: result.data.user.username,
email: result.data.user.email,
real_name: result.data.user.real_name,
phone: result.data.user.phone,
avatar: result.data.user.avatar,
role: result.data.user.role,
status: result.data.user.status
}
// 保存到本地存储
localStorage.setItem(SECURITY_CONFIG.tokenKey, result.data.token)
localStorage.setItem(SECURITY_CONFIG.userKey, JSON.stringify(userData.value))
}
return result
} catch (error) {
console.error('登录错误:', error)
// 重试逻辑仅对500错误且重试次数<2
if (error.message.includes('500') && retryCount < 2) {
return login(username, password, retryCount + 1)
}
// 直接抛出错误,由调用方处理
throw error
}
}
// 登出操作
async function logout() {
try {
// 调用后端登出接口
const { api } = await import('@/utils/api')
await api.post('/users/logout')
} catch (error) {
console.error('登出请求失败:', error)
} finally {
// 清除本地状态
token.value = ''
userData.value = null
// 清除本地存储
localStorage.removeItem(SECURITY_CONFIG.tokenKey)
localStorage.removeItem(SECURITY_CONFIG.userKey)
}
}
// 更新用户信息
function updateUserInfo(newUserInfo) {
userData.value = { ...userData.value, ...newUserInfo }
localStorage.setItem(SECURITY_CONFIG.userKey, JSON.stringify(userData.value))
}
// 权限检查方法
function hasPermission(permission) {
if (!userData.value || !userData.value.role) {
return false
}
// 管理员拥有所有权限
if (userData.value.role.name === 'admin') {
return true
}
// 根据角色检查权限
const rolePermissions = {
'admin': ['*'], // 所有权限
'manager': ['user:read', 'user:write', 'account:read', 'account:write', 'transaction:read', 'transaction:write'],
'teller': ['user:read', 'account:read', 'account:write', 'transaction:read', 'transaction:write'],
'user': ['account:read', 'transaction:read']
}
const userRole = userData.value.role.name
const permissions = rolePermissions[userRole] || []
if (Array.isArray(permission)) {
return permission.some(p => permissions.includes(p) || permissions.includes('*'))
}
return permissions.includes(permission) || permissions.includes('*')
}
// 角色检查方法
function hasRole(role) {
if (!userData.value || !userData.value.role) {
return false
}
if (Array.isArray(role)) {
return role.includes(userData.value.role.name)
}
return userData.value.role.name === role
}
// 检查是否可以访问菜单
function canAccessMenu(menuKey) {
if (!userData.value) {
return false
}
// 管理员可以访问所有菜单
if (userData.value.role.name === 'admin') {
return true
}
// 根据角色定义可访问的菜单
const roleMenus = {
'admin': ['*'], // 所有菜单
'manager': ['dashboard', 'users', 'accounts', 'transactions', 'reports'],
'teller': ['dashboard', 'accounts', 'transactions'],
'user': ['dashboard', 'accounts', 'transactions']
}
const userRole = userData.value.role.name
const menus = roleMenus[userRole] || []
return menus.includes(menuKey) || menus.includes('*')
}
// 获取用户角色名称
function getUserRoleName() {
return userData.value?.role?.name || 'user'
}
// 获取用户权限列表
function getUserPermissions() {
const rolePermissions = {
'admin': ['*'],
'manager': ['user:read', 'user:write', 'account:read', 'account:write', 'transaction:read', 'transaction:write'],
'teller': ['user:read', 'account:read', 'account:write', 'transaction:read', 'transaction:write'],
'user': ['account:read', 'transaction:read']
}
const userRole = userData.value?.role?.name || 'user'
return rolePermissions[userRole] || []
}
// 检查用户状态
function isUserActive() {
return userData.value?.status === 'active'
}
// 检查用户是否被锁定
function isUserLocked() {
return userData.value?.status === 'locked' || userData.value?.status === 'suspended'
}
return {
token,
userData,
isLoggedIn,
checkLoginStatus,
validateToken,
login,
logout,
updateUserInfo,
hasPermission,
hasRole,
canAccessMenu,
getUserRoleName,
getUserPermissions,
isUserActive,
isUserLocked
}
})

View File

@@ -0,0 +1,459 @@
/**
* 全局样式
* @file global.css
* @description 全局CSS样式定义
*/
/* 重置样式 */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 14px;
line-height: 1.5715;
color: #262626;
background-color: #f0f2f5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
height: 100%;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 链接样式 */
a {
color: #1890ff;
text-decoration: none;
transition: color 0.3s;
}
a:hover {
color: #40a9ff;
}
a:active {
color: #096dd9;
}
/* 按钮样式增强 */
.ant-btn {
border-radius: 6px;
font-weight: 500;
transition: all 0.3s;
}
.ant-btn-primary {
background: #1890ff;
border-color: #1890ff;
}
.ant-btn-primary:hover {
background: #40a9ff;
border-color: #40a9ff;
}
.ant-btn-primary:active {
background: #096dd9;
border-color: #096dd9;
}
/* 输入框样式增强 */
.ant-input,
.ant-input-password,
.ant-select-selector {
border-radius: 6px;
transition: all 0.3s;
}
.ant-input:focus,
.ant-input-password:focus,
.ant-select-focused .ant-select-selector {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 卡片样式增强 */
.ant-card {
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
transition: all 0.3s;
}
.ant-card:hover {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02), 0 4px 8px 0 rgba(0, 0, 0, 0.05);
}
/* 表格样式增强 */
.ant-table {
border-radius: 8px;
overflow: hidden;
}
.ant-table-thead > tr > th {
background: #fafafa;
font-weight: 600;
color: #262626;
border-bottom: 1px solid #f0f0f0;
}
.ant-table-tbody > tr:hover > td {
background: #f5f5f5;
}
/* 模态框样式增强 */
.ant-modal {
border-radius: 8px;
}
.ant-modal-content {
border-radius: 8px;
overflow: hidden;
}
.ant-modal-header {
border-bottom: 1px solid #f0f0f0;
padding: 16px 24px;
}
.ant-modal-body {
padding: 24px;
}
.ant-modal-footer {
border-top: 1px solid #f0f0f0;
padding: 10px 16px;
}
/* 抽屉样式增强 */
.ant-drawer-content {
border-radius: 8px 0 0 8px;
}
.ant-drawer-header {
border-bottom: 1px solid #f0f0f0;
padding: 16px 24px;
}
.ant-drawer-body {
padding: 24px;
}
/* 页面布局样式 */
.page-container {
padding: 24px;
background: #f0f2f5;
min-height: calc(100vh - 64px);
}
.page-header {
background: #fff;
padding: 16px 24px;
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
}
.page-content {
background: #fff;
padding: 24px;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
}
/* 搜索区域样式 */
.search-area {
background: #fff;
padding: 16px 24px;
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
}
.search-form {
display: flex;
gap: 16px;
align-items: flex-end;
flex-wrap: wrap;
}
.search-form .ant-form-item {
margin-bottom: 0;
}
.search-buttons {
display: flex;
gap: 8px;
margin-left: auto;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
align-items: center;
}
.action-buttons .ant-btn {
border-radius: 6px;
}
/* 状态标签样式 */
.status-tag {
border-radius: 4px;
font-weight: 500;
}
.status-active {
background: #f6ffed;
color: #52c41a;
border-color: #b7eb8f;
}
.status-inactive {
background: #fff2e8;
color: #fa8c16;
border-color: #ffd591;
}
.status-suspended {
background: #fff1f0;
color: #ff4d4f;
border-color: #ffccc7;
}
.status-locked {
background: #f5f5f5;
color: #8c8c8c;
border-color: #d9d9d9;
}
/* 金额显示样式 */
.amount-text {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-weight: 600;
}
.amount-positive {
color: #52c41a;
}
.amount-negative {
color: #ff4d4f;
}
.amount-zero {
color: #8c8c8c;
}
/* 加载状态样式 */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.loading-text {
margin-left: 12px;
color: #8c8c8c;
}
/* 空状态样式 */
.empty-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 200px;
color: #8c8c8c;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #d9d9d9;
}
.empty-text {
font-size: 16px;
margin-bottom: 8px;
}
.empty-description {
font-size: 14px;
color: #8c8c8c;
}
/* 响应式样式 */
@media (max-width: 768px) {
.page-container {
padding: 12px;
}
.page-header {
padding: 12px 16px;
margin-bottom: 12px;
}
.page-content {
padding: 16px;
}
.search-area {
padding: 12px 16px;
margin-bottom: 12px;
}
.search-form {
flex-direction: column;
gap: 12px;
}
.search-buttons {
margin-left: 0;
width: 100%;
}
.search-buttons .ant-btn {
flex: 1;
}
}
/* 打印样式 */
@media print {
.no-print {
display: none !important;
}
.page-container {
padding: 0;
background: #fff;
}
.page-header,
.page-content {
box-shadow: none;
border: 1px solid #d9d9d9;
}
}
/* 动画效果 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(-100%);
}
.slide-leave-to {
transform: translateX(100%);
}
/* 工具类 */
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.mb-0 { margin-bottom: 0; }
.mb-8 { margin-bottom: 8px; }
.mb-16 { margin-bottom: 16px; }
.mb-24 { margin-bottom: 24px; }
.mt-0 { margin-top: 0; }
.mt-8 { margin-top: 8px; }
.mt-16 { margin-top: 16px; }
.mt-24 { margin-top: 24px; }
.ml-0 { margin-left: 0; }
.ml-8 { margin-left: 8px; }
.ml-16 { margin-left: 16px; }
.ml-24 { margin-left: 24px; }
.mr-0 { margin-right: 0; }
.mr-8 { margin-right: 8px; }
.mr-16 { margin-right: 16px; }
.mr-24 { margin-right: 24px; }
.p-0 { padding: 0; }
.p-8 { padding: 8px; }
.p-16 { padding: 16px; }
.p-24 { padding: 24px; }
.flex {
display: flex;
}
.flex-center {
display: flex;
justify-content: center;
align-items: center;
}
.flex-between {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-1 {
flex: 1;
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}

View File

@@ -0,0 +1,520 @@
/**
* 响应式样式
* @file responsive.css
* @description 响应式设计样式定义
*/
/* 移动端样式 */
@media (max-width: 768px) {
/* 布局调整 */
.ant-layout-header {
padding: 0 12px;
}
.ant-layout-sider {
position: fixed !important;
height: 100vh;
z-index: 1000;
}
.ant-layout-content {
margin-left: 0 !important;
}
/* 菜单调整 */
.ant-menu {
border-right: none;
}
.ant-menu-item,
.ant-menu-submenu-title {
padding: 0 16px !important;
}
/* 表格调整 */
.ant-table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.ant-table {
min-width: 600px;
}
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
padding: 8px 4px;
font-size: 12px;
white-space: nowrap;
}
/* 表单调整 */
.ant-form-item {
margin-bottom: 16px;
}
.ant-form-item-label {
padding-bottom: 4px;
}
.ant-form-item-control {
min-height: 32px;
}
/* 按钮调整 */
.ant-btn {
min-height: 40px;
border-radius: 6px;
}
.ant-btn-sm {
min-height: 32px;
}
.ant-btn-lg {
min-height: 48px;
}
/* 输入框调整 */
.ant-input,
.ant-input-password,
.ant-select-selector {
min-height: 40px;
border-radius: 6px;
}
/* 卡片调整 */
.ant-card {
margin-bottom: 12px;
border-radius: 8px;
}
.ant-card-head {
padding: 0 16px;
min-height: 48px;
}
.ant-card-body {
padding: 16px;
}
/* 模态框调整 */
.ant-modal {
margin: 0 !important;
width: 100vw !important;
max-width: 100vw !important;
top: 0 !important;
}
.ant-modal-content {
border-radius: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
.ant-modal-header {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.ant-modal-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.ant-modal-footer {
border-top: 1px solid #f0f0f0;
padding: 12px 16px;
}
/* 抽屉调整 */
.ant-drawer-content {
border-radius: 0;
}
.ant-drawer-header {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.ant-drawer-body {
padding: 16px;
}
/* 分页调整 */
.ant-pagination {
text-align: center;
margin-top: 16px;
}
.ant-pagination-item,
.ant-pagination-prev,
.ant-pagination-next {
min-width: 32px;
height: 32px;
line-height: 30px;
}
/* 标签调整 */
.ant-tag {
margin: 2px;
padding: 2px 8px;
font-size: 12px;
border-radius: 4px;
}
/* 步骤条调整 */
.ant-steps {
margin: 16px 0;
}
.ant-steps-item-title {
font-size: 14px;
}
.ant-steps-item-description {
font-size: 12px;
}
/* 时间轴调整 */
.ant-timeline-item-content {
font-size: 14px;
}
/* 描述列表调整 */
.ant-descriptions-item-label {
font-weight: 600;
color: #262626;
}
.ant-descriptions-item-content {
color: #595959;
}
/* 统计数值调整 */
.ant-statistic-title {
font-size: 14px;
color: #8c8c8c;
}
.ant-statistic-content {
font-size: 24px;
color: #262626;
}
/* 进度条调整 */
.ant-progress-text {
font-size: 12px;
}
/* 工具提示调整 */
.ant-tooltip-inner {
font-size: 12px;
padding: 6px 8px;
}
/* 消息提示调整 */
.ant-message {
top: 8px;
}
.ant-message-notice-content {
padding: 8px 12px;
border-radius: 6px;
}
/* 通知调整 */
.ant-notification {
top: 8px;
right: 8px;
}
.ant-notification-notice {
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* 确认对话框调整 */
.ant-modal-confirm {
margin: 0 16px;
}
.ant-modal-confirm-body {
padding: 16px 0;
}
.ant-modal-confirm-btns {
padding: 8px 0 0 0;
}
/* 选择器调整 */
.ant-select-dropdown {
border-radius: 8px;
}
.ant-select-item {
padding: 8px 12px;
font-size: 14px;
}
/* 日期选择器调整 */
.ant-picker {
width: 100%;
min-height: 40px;
}
.ant-picker-dropdown {
border-radius: 8px;
}
/* 上传组件调整 */
.ant-upload {
width: 100%;
}
.ant-upload-dragger {
padding: 16px;
border-radius: 8px;
}
/* 评分组件调整 */
.ant-rate {
font-size: 20px;
}
/* 开关组件调整 */
.ant-switch {
min-width: 44px;
height: 22px;
}
/* 滑块组件调整 */
.ant-slider {
margin: 16px 0;
}
/* 穿梭框调整 */
.ant-transfer {
width: 100%;
}
.ant-transfer-list {
width: calc(50% - 8px);
}
/* 树形控件调整 */
.ant-tree {
font-size: 14px;
}
.ant-tree-node-content-wrapper {
padding: 4px 8px;
}
/* 锚点调整 */
.ant-anchor {
font-size: 14px;
}
.ant-anchor-link {
padding: 4px 0;
}
/* 回到顶部调整 */
.ant-back-top {
right: 16px;
bottom: 16px;
}
/* 加载中调整 */
.ant-spin-container {
min-height: 200px;
}
.ant-spin-text {
font-size: 14px;
color: #8c8c8c;
}
}
/* 平板样式 */
@media (min-width: 769px) and (max-width: 1024px) {
/* 布局调整 */
.ant-layout-content {
padding: 16px;
}
/* 表格调整 */
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
padding: 12px 8px;
font-size: 13px;
}
/* 卡片调整 */
.ant-card-body {
padding: 20px;
}
/* 模态框调整 */
.ant-modal {
margin: 0 16px;
}
.ant-modal-content {
border-radius: 8px;
}
/* 表单调整 */
.ant-form-item {
margin-bottom: 20px;
}
}
/* 大屏幕样式 */
@media (min-width: 1200px) {
/* 容器最大宽度 */
.page-container {
max-width: 1200px;
margin: 0 auto;
}
/* 表格调整 */
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
padding: 16px 12px;
font-size: 14px;
}
/* 卡片调整 */
.ant-card-body {
padding: 24px;
}
/* 模态框调整 */
.ant-modal {
margin: 0 auto;
}
/* 表单调整 */
.ant-form-item {
margin-bottom: 24px;
}
}
/* 超宽屏幕样式 */
@media (min-width: 1600px) {
/* 容器最大宽度 */
.page-container {
max-width: 1400px;
margin: 0 auto;
}
/* 表格调整 */
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
padding: 20px 16px;
font-size: 14px;
}
/* 卡片调整 */
.ant-card-body {
padding: 32px;
}
/* 表单调整 */
.ant-form-item {
margin-bottom: 32px;
}
}
/* 横屏模式调整 */
@media (orientation: landscape) and (max-height: 500px) {
.ant-layout-header {
height: 48px;
line-height: 48px;
}
.ant-layout-sider {
height: calc(100vh - 48px);
}
.ant-layout-content {
min-height: calc(100vh - 48px);
}
.page-container {
padding: 12px;
}
.page-header {
padding: 8px 16px;
margin-bottom: 8px;
}
.page-content {
padding: 16px;
}
}
/* 高分辨率屏幕调整 */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
border-bottom: 0.5px solid #f0f0f0;
}
.ant-divider {
border-top: 0.5px solid #f0f0f0;
}
.ant-card {
border: 0.5px solid #f0f0f0;
}
}
/* 打印样式 */
@media print {
.ant-layout-header,
.ant-layout-sider,
.ant-layout-footer {
display: none !important;
}
.ant-layout-content {
margin: 0 !important;
padding: 0 !important;
}
.page-container {
padding: 0 !important;
background: #fff !important;
}
.page-header,
.page-content {
box-shadow: none !important;
border: 1px solid #d9d9d9 !important;
page-break-inside: avoid;
}
.ant-table {
border: 1px solid #d9d9d9 !important;
}
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
border: 1px solid #d9d9d9 !important;
padding: 8px !important;
}
.ant-btn {
display: none !important;
}
.no-print {
display: none !important;
}
}

View File

@@ -0,0 +1,172 @@
/**
* 主题配置
* @file theme.js
* @description Ant Design Vue 主题配置
*/
import { THEME_CONFIG } from '@/config/env'
// Ant Design Vue 主题配置
export const themeConfig = {
token: {
// 主色调
colorPrimary: THEME_CONFIG.primaryColor,
// 成功色
colorSuccess: '#52c41a',
// 警告色
colorWarning: '#faad14',
// 错误色
colorError: '#ff4d4f',
// 信息色
colorInfo: THEME_CONFIG.primaryColor,
// 字体大小
fontSize: 14,
// 圆角
borderRadius: 6,
// 组件尺寸
sizeUnit: 4,
sizeStep: 4,
// 控制台高度
controlHeight: 32,
// 行高
lineHeight: 1.5715,
// 字体族
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
},
components: {
// 布局组件
Layout: {
headerBg: '#001529',
siderBg: '#001529',
bodyBg: '#f0f2f5'
},
// 菜单组件
Menu: {
darkItemBg: '#001529',
darkItemSelectedBg: '#1890ff',
darkItemHoverBg: '#1890ff'
},
// 按钮组件
Button: {
borderRadius: 6,
controlHeight: 32
},
// 输入框组件
Input: {
borderRadius: 6,
controlHeight: 32
},
// 表格组件
Table: {
headerBg: '#fafafa',
headerColor: '#262626',
rowHoverBg: '#f5f5f5'
},
// 卡片组件
Card: {
borderRadius: 8,
headerBg: '#fafafa'
},
// 模态框组件
Modal: {
borderRadius: 8
},
// 抽屉组件
Drawer: {
borderRadius: 8
}
}
}
// 暗色主题配置
export const darkThemeConfig = {
token: {
...themeConfig.token,
colorBgBase: '#141414',
colorBgContainer: '#1f1f1f',
colorBgElevated: '#262626',
colorBorder: '#424242',
colorText: '#ffffff',
colorTextSecondary: '#a6a6a6',
colorTextTertiary: '#737373',
colorTextQuaternary: '#595959'
},
components: {
...themeConfig.components,
Layout: {
headerBg: '#141414',
siderBg: '#141414',
bodyBg: '#000000'
},
Menu: {
darkItemBg: '#141414',
darkItemSelectedBg: '#1890ff',
darkItemHoverBg: '#1890ff'
},
Table: {
headerBg: '#262626',
headerColor: '#ffffff',
rowHoverBg: '#262626'
},
Card: {
borderRadius: 8,
headerBg: '#262626'
}
}
}
// 紧凑主题配置
export const compactThemeConfig = {
token: {
...themeConfig.token,
fontSize: 12,
controlHeight: 24,
sizeUnit: 2,
sizeStep: 2
},
components: {
...themeConfig.components,
Button: {
borderRadius: 4,
controlHeight: 24
},
Input: {
borderRadius: 4,
controlHeight: 24
}
}
}
// 根据配置返回主题
export const getThemeConfig = () => {
if (THEME_CONFIG.darkMode) {
return darkThemeConfig
}
if (THEME_CONFIG.compactMode) {
return compactThemeConfig
}
return themeConfig
}
export default themeConfig

View File

@@ -0,0 +1,382 @@
/**
* API请求工具
* @file api.js
* @description 封装银行系统API请求方法
*/
import { API_CONFIG, SECURITY_CONFIG } from '@/config/env'
/**
* 创建请求头自动添加认证Token
* @param {Object} headers - 额外的请求头
* @returns {Object} 合并后的请求头
*/
const createHeaders = (headers = {}) => {
const token = localStorage.getItem(SECURITY_CONFIG.tokenKey)
const defaultHeaders = {
'Content-Type': 'application/json',
}
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`
}
return { ...defaultHeaders, ...headers }
}
/**
* 处理API响应
* @param {Response} response - Fetch API响应对象
* @returns {Promise} 处理后的响应数据
*/
const handleResponse = async (response) => {
// 检查HTTP状态
if (!response.ok) {
// 处理常见错误
if (response.status === 401) {
// 清除无效的认证信息
localStorage.removeItem(SECURITY_CONFIG.tokenKey)
localStorage.removeItem(SECURITY_CONFIG.userKey)
throw new Error('认证已过期,请重新登录')
}
if (response.status === 403) {
throw new Error('您没有权限访问此资源,请联系管理员')
}
if (response.status === 404) {
throw new Error('请求的资源不存在')
}
if (response.status === 500) {
try {
const errorData = await response.json()
throw new Error(errorData.message || '服务器内部错误,请联系管理员')
} catch (e) {
throw new Error('服务器内部错误,请联系管理员')
}
}
// 尝试获取详细错误信息
try {
const errorData = await response.json()
throw new Error(errorData.message || `请求失败: ${response.status} ${response.statusText}`)
} catch (e) {
throw new Error(`请求失败: ${response.status} ${response.statusText}`)
}
}
// 检查响应类型
const contentType = response.headers.get('content-type')
// 如果是blob类型如文件下载直接返回blob
if (contentType && (contentType.includes('text/csv') ||
contentType.includes('application/octet-stream') ||
contentType.includes('application/vnd.ms-excel') ||
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'))) {
return await response.blob()
}
// 返回JSON数据
const result = await response.json()
// 兼容数组响应
if (Array.isArray(result)) {
return result
}
if (!result.success) {
throw new Error(result.message || 'API请求失败')
}
return result
}
/**
* API请求方法
*/
export const api = {
/**
* 登录
* @param {string} username - 用户名
* @param {string} password - 密码
* @returns {Promise} 登录结果
*/
async login(username, password) {
const response = await fetch(`${API_CONFIG.baseUrl}/api/users/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
})
return handleResponse(response)
},
/**
* GET请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async get(endpoint, options = {}) {
let url = `${API_CONFIG.baseUrl}/api${endpoint}`
// 处理查询参数
if (options.params && Object.keys(options.params).length > 0) {
const searchParams = new URLSearchParams()
for (const [key, value] of Object.entries(options.params)) {
if (value !== null && value !== undefined) {
searchParams.append(key, value)
}
}
url += `?${searchParams.toString()}`
}
const response = await fetch(url, {
...options,
method: 'GET',
headers: createHeaders(options.headers),
params: undefined,
})
// 如果指定了responseType为blob直接返回blob
if (options.responseType === 'blob') {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.blob()
}
return handleResponse(response)
},
/**
* POST请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async post(endpoint, data, options = {}) {
const url = `${API_CONFIG.baseUrl}/api${endpoint}`
const response = await fetch(url, {
method: 'POST',
headers: createHeaders(options.headers),
body: JSON.stringify(data),
...options,
})
return handleResponse(response)
},
/**
* PUT请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async put(endpoint, data, options = {}) {
const url = `${API_CONFIG.baseUrl}/api${endpoint}`
const response = await fetch(url, {
method: 'PUT',
headers: createHeaders(options.headers),
body: JSON.stringify(data),
...options,
})
return handleResponse(response)
},
/**
* DELETE请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async delete(endpoint, options = {}) {
const url = `${API_CONFIG.baseUrl}/api${endpoint}`
const response = await fetch(url, {
method: 'DELETE',
headers: createHeaders(options.headers),
...options,
})
return handleResponse(response)
},
// 用户管理API
users: {
/**
* 获取用户列表
* @param {Object} params - 查询参数
* @returns {Promise} 用户列表
*/
async getList(params = {}) {
return api.get('/users', { params })
},
/**
* 获取用户详情
* @param {number} id - 用户ID
* @returns {Promise} 用户详情
*/
async getById(id) {
return api.get(`/users/${id}`)
},
/**
* 创建用户
* @param {Object} data - 用户数据
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/users/register', data)
},
/**
* 更新用户信息
* @param {number} id - 用户ID
* @param {Object} data - 更新数据
* @returns {Promise} 更新结果
*/
async update(id, data) {
return api.put(`/users/${id}`, data)
},
/**
* 删除用户
* @param {number} id - 用户ID
* @returns {Promise} 删除结果
*/
async delete(id) {
return api.delete(`/users/${id}`)
},
/**
* 更新用户状态
* @param {number} id - 用户ID
* @param {Object} data - 状态数据
* @returns {Promise} 更新结果
*/
async updateStatus(id, data) {
return api.put(`/users/${id}/status`, data)
},
/**
* 获取用户账户列表
* @param {number} userId - 用户ID
* @returns {Promise} 账户列表
*/
async getAccounts(userId) {
return api.get(`/users/${userId}/accounts`)
}
},
// 账户管理API
accounts: {
/**
* 获取账户列表
* @param {Object} params - 查询参数
* @returns {Promise} 账户列表
*/
async getList(params = {}) {
return api.get('/accounts', { params })
},
/**
* 获取账户详情
* @param {number} id - 账户ID
* @returns {Promise} 账户详情
*/
async getById(id) {
return api.get(`/accounts/${id}`)
},
/**
* 创建账户
* @param {Object} data - 账户数据
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/accounts', data)
},
/**
* 更新账户状态
* @param {number} id - 账户ID
* @param {Object} data - 状态数据
* @returns {Promise} 更新结果
*/
async updateStatus(id, data) {
return api.put(`/accounts/${id}/status`, data)
},
/**
* 存款
* @param {number} id - 账户ID
* @param {Object} data - 存款数据
* @returns {Promise} 存款结果
*/
async deposit(id, data) {
return api.post(`/accounts/${id}/deposit`, data)
},
/**
* 取款
* @param {number} id - 账户ID
* @param {Object} data - 取款数据
* @returns {Promise} 取款结果
*/
async withdraw(id, data) {
return api.post(`/accounts/${id}/withdraw`, data)
}
},
// 交易管理API
transactions: {
/**
* 获取交易记录列表
* @param {Object} params - 查询参数
* @returns {Promise} 交易记录列表
*/
async getList(params = {}) {
return api.get('/transactions', { params })
},
/**
* 获取交易详情
* @param {number} id - 交易ID
* @returns {Promise} 交易详情
*/
async getById(id) {
return api.get(`/transactions/${id}`)
},
/**
* 转账
* @param {Object} data - 转账数据
* @returns {Promise} 转账结果
*/
async transfer(data) {
return api.post('/transactions/transfer', data)
},
/**
* 撤销交易
* @param {number} id - 交易ID
* @returns {Promise} 撤销结果
*/
async reverse(id) {
return api.post(`/transactions/${id}/reverse`)
},
/**
* 获取交易统计
* @param {Object} params - 查询参数
* @returns {Promise} 交易统计
*/
async getStats(params = {}) {
return api.get('/transactions/stats', { params })
}
}
}
export default api

View File

@@ -0,0 +1,785 @@
<template>
<div class="accounts-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>账户管理</h1>
<p>管理银行账户信息</p>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<a-button type="primary" @click="showAddAccountModal">
<plus-outlined /> 添加账户
</a-button>
<a-input-search
v-model:value="searchQuery"
placeholder="搜索账户..."
style="width: 250px; margin-left: 16px;"
@search="handleSearch"
/>
</div>
<!-- 账户表格 -->
<a-table
:columns="columns"
:data-source="accounts"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<!-- 状态列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusName(record.status) }}
</a-tag>
</template>
<!-- 账户类型列 -->
<template v-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeName(record.type) }}
</a-tag>
</template>
<!-- 余额列 -->
<template v-if="column.key === 'balance'">
<span :style="{ color: record.balance < 0 ? '#f5222d' : '' }">
{{ formatCurrency(record.balance) }}
</span>
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a-space>
<a @click="viewAccount(record)">查看</a>
<a-divider type="vertical" />
<a @click="editAccount(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要冻结此账户吗?"
ok-text="确定"
cancel-text="取消"
@confirm="freezeAccount(record.id)"
v-if="record.status === 'active'"
>
<a class="warning-link">冻结</a>
</a-popconfirm>
<a-popconfirm
title="确定要激活此账户吗?"
ok-text="确定"
cancel-text="取消"
@confirm="activateAccount(record.id)"
v-if="record.status === 'frozen'"
>
<a class="success-link">激活</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 添加/编辑账户对话框 -->
<a-modal
v-model:visible="accountModalVisible"
:title="isEditing ? '编辑账户' : '添加账户'"
@ok="handleAccountFormSubmit"
:confirmLoading="submitting"
>
<a-form
:model="accountForm"
:rules="rules"
ref="accountFormRef"
layout="vertical"
>
<a-form-item label="账户号码" name="accountNumber" v-if="isEditing">
<a-input v-model:value="accountForm.accountNumber" disabled />
</a-form-item>
<a-form-item label="账户名称" name="name">
<a-input v-model:value="accountForm.name" />
</a-form-item>
<a-form-item label="账户类型" name="type">
<a-select v-model:value="accountForm.type">
<a-select-option value="savings">储蓄账户</a-select-option>
<a-select-option value="checking">活期账户</a-select-option>
<a-select-option value="credit">信用账户</a-select-option>
<a-select-option value="loan">贷款账户</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所属用户" name="userId">
<a-select
v-model:value="accountForm.userId"
:loading="usersLoading"
show-search
placeholder="选择用户"
:filter-option="filterUserOption"
>
<a-select-option v-for="user in usersList" :key="user.id" :value="user.id">
{{ user.name }} ({{ user.username }})
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="初始余额" name="balance" v-if="!isEditing">
<a-input-number
v-model:value="accountForm.balance"
:precision="2"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="状态" name="status" v-if="isEditing">
<a-select v-model:value="accountForm.status">
<a-select-option value="active">活跃</a-select-option>
<a-select-option value="frozen">冻结</a-select-option>
<a-select-option value="closed">关闭</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="备注" name="notes">
<a-textarea v-model:value="accountForm.notes" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
<!-- 账户详情对话框 -->
<a-modal
v-model:visible="accountDetailVisible"
title="账户详情"
:footer="null"
width="700px"
>
<template v-if="selectedAccount">
<a-descriptions bordered :column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }">
<a-descriptions-item label="账户号码">{{ selectedAccount.accountNumber }}</a-descriptions-item>
<a-descriptions-item label="账户名称">{{ selectedAccount.name }}</a-descriptions-item>
<a-descriptions-item label="账户类型">{{ getTypeName(selectedAccount.type) }}</a-descriptions-item>
<a-descriptions-item label="账户状态">
<a-tag :color="getStatusColor(selectedAccount.status)">
{{ getStatusName(selectedAccount.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="当前余额">{{ formatCurrency(selectedAccount.balance) }}</a-descriptions-item>
<a-descriptions-item label="所属用户">{{ selectedAccount.userName }}</a-descriptions-item>
<a-descriptions-item label="开户日期">{{ selectedAccount.createdAt }}</a-descriptions-item>
<a-descriptions-item label="最后更新">{{ selectedAccount.updatedAt }}</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">{{ selectedAccount.notes || '无' }}</a-descriptions-item>
</a-descriptions>
<a-divider>最近交易记录</a-divider>
<a-table
:columns="transactionColumns"
:data-source="recentTransactions"
:loading="transactionsLoading"
:pagination="{ pageSize: 5 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getTransactionTypeColor(record.type)">
{{ getTransactionTypeName(record.type) }}
</a-tag>
</template>
<template v-if="column.key === 'amount'">
<span :style="{ color: record.type === 'withdrawal' ? '#f5222d' : '#52c41a' }">
{{ record.type === 'withdrawal' ? '-' : '+' }}{{ formatCurrency(record.amount) }}
</span>
</template>
</template>
</a-table>
<div style="margin-top: 16px; text-align: right;">
<a-button @click="accountDetailVisible = false">关闭</a-button>
</div>
</template>
</a-modal>
</div>
</template>
<script>
import { defineComponent, ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'AccountsPage',
components: {
PlusOutlined
},
setup() {
// 表格列定义
const columns = [
{
title: '账户号码',
dataIndex: 'accountNumber',
key: 'accountNumber',
sorter: true,
},
{
title: '账户名称',
dataIndex: 'name',
key: 'name',
sorter: true,
},
{
title: '账户类型',
dataIndex: 'type',
key: 'type',
filters: [
{ text: '储蓄账户', value: 'savings' },
{ text: '活期账户', value: 'checking' },
{ text: '信用账户', value: 'credit' },
{ text: '贷款账户', value: 'loan' },
],
},
{
title: '所属用户',
dataIndex: 'userName',
key: 'userName',
sorter: true,
},
{
title: '余额',
dataIndex: 'balance',
key: 'balance',
sorter: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '活跃', value: 'active' },
{ text: '冻结', value: 'frozen' },
{ text: '关闭', value: 'closed' },
],
},
{
title: '开户日期',
dataIndex: 'createdAt',
key: 'createdAt',
sorter: true,
},
{
title: '操作',
key: 'action',
},
];
// 交易记录列定义
const transactionColumns = [
{
title: '交易ID',
dataIndex: 'id',
key: 'id',
},
{
title: '交易类型',
dataIndex: 'type',
key: 'type',
},
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
},
{
title: '交易时间',
dataIndex: 'timestamp',
key: 'timestamp',
},
{
title: '备注',
dataIndex: 'description',
key: 'description',
},
];
// 状态变量
const accounts = ref([]);
const loading = ref(false);
const searchQuery = ref('');
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`,
});
// 用户列表相关
const usersList = ref([]);
const usersLoading = ref(false);
// 账户表单相关
const accountFormRef = ref(null);
const accountModalVisible = ref(false);
const isEditing = ref(false);
const submitting = ref(false);
const accountForm = reactive({
id: null,
accountNumber: '',
name: '',
type: 'savings',
userId: null,
balance: 0,
status: 'active',
notes: '',
});
// 账户详情相关
const accountDetailVisible = ref(false);
const selectedAccount = ref(null);
const recentTransactions = ref([]);
const transactionsLoading = ref(false);
// 表单验证规则
const rules = {
name: [
{ required: true, message: '请输入账户名称', trigger: 'blur' },
],
type: [
{ required: true, message: '请选择账户类型', trigger: 'change' },
],
userId: [
{ required: true, message: '请选择所属用户', trigger: 'change' },
],
balance: [
{ required: true, message: '请输入初始余额', trigger: 'blur' },
],
};
// 获取账户列表
const fetchAccounts = async (params = {}) => {
loading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getAccounts(params);
// accounts.value = response.data;
// pagination.total = response.total;
// 模拟数据
setTimeout(() => {
const mockAccounts = [
{
id: 1,
accountNumber: '6225123456789001',
name: '张三储蓄账户',
type: 'savings',
userId: 1,
userName: '张三',
balance: 10000.50,
status: 'active',
createdAt: '2023-01-01',
updatedAt: '2023-09-15',
notes: '主要储蓄账户'
},
{
id: 2,
accountNumber: '6225123456789002',
name: '李四活期账户',
type: 'checking',
userId: 2,
userName: '李四',
balance: 5000.75,
status: 'active',
createdAt: '2023-02-15',
updatedAt: '2023-09-10',
notes: ''
},
{
id: 3,
accountNumber: '6225123456789003',
name: '王五信用卡',
type: 'credit',
userId: 3,
userName: '王五',
balance: -2000.00,
status: 'active',
createdAt: '2023-03-20',
updatedAt: '2023-09-12',
notes: '信用额度: 50000'
},
{
id: 4,
accountNumber: '6225123456789004',
name: '赵六房贷',
type: 'loan',
userId: 4,
userName: '赵六',
balance: -500000.00,
status: 'frozen',
createdAt: '2023-04-10',
updatedAt: '2023-09-01',
notes: '30年房贷'
},
];
accounts.value = mockAccounts;
pagination.total = mockAccounts.length;
loading.value = false;
}, 500);
} catch (error) {
message.error('获取账户列表失败');
loading.value = false;
}
};
// 获取用户列表
const fetchUsers = async () => {
usersLoading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getUsers();
// usersList.value = response.data;
// 模拟数据
setTimeout(() => {
usersList.value = [
{ id: 1, username: 'zhangsan', name: '张三' },
{ id: 2, username: 'lisi', name: '李四' },
{ id: 3, username: 'wangwu', name: '王五' },
{ id: 4, username: 'zhaoliu', name: '赵六' },
];
usersLoading.value = false;
}, 300);
} catch (error) {
message.error('获取用户列表失败');
usersLoading.value = false;
}
};
// 获取账户交易记录
const fetchAccountTransactions = async (accountId) => {
transactionsLoading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getAccountTransactions(accountId);
// recentTransactions.value = response.data;
// 模拟数据
setTimeout(() => {
recentTransactions.value = [
{
id: 'T20230915001',
type: 'deposit',
amount: 1000.00,
timestamp: '2023-09-15 10:30:25',
description: '工资入账'
},
{
id: 'T20230914002',
type: 'withdrawal',
amount: 500.00,
timestamp: '2023-09-14 15:45:12',
description: '超市购物'
},
{
id: 'T20230913003',
type: 'transfer',
amount: 2000.00,
timestamp: '2023-09-13 09:20:45',
description: '转账给李四'
},
{
id: 'T20230912004',
type: 'withdrawal',
amount: 100.00,
timestamp: '2023-09-12 18:10:33',
description: '餐饮消费'
},
{
id: 'T20230911005',
type: 'deposit',
amount: 5000.00,
timestamp: '2023-09-11 14:05:22',
description: '投资回报'
},
];
transactionsLoading.value = false;
}, 400);
} catch (error) {
message.error('获取交易记录失败');
transactionsLoading.value = false;
}
};
// 表格变化处理
const handleTableChange = (pag, filters, sorter) => {
const params = {
page: pag.current,
pageSize: pag.pageSize,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
};
pagination.current = pag.current;
fetchAccounts(params);
};
// 搜索处理
const handleSearch = () => {
pagination.current = 1;
fetchAccounts({ search: searchQuery.value });
};
// 显示添加账户对话框
const showAddAccountModal = () => {
isEditing.value = false;
resetAccountForm();
accountModalVisible.value = true;
};
// 编辑账户
const editAccount = (record) => {
isEditing.value = true;
Object.assign(accountForm, { ...record });
accountModalVisible.value = true;
};
// 查看账户详情
const viewAccount = (record) => {
selectedAccount.value = record;
accountDetailVisible.value = true;
fetchAccountTransactions(record.id);
};
// 冻结账户
const freezeAccount = async (id) => {
try {
// 这里应该是实际的API调用
// await api.updateAccountStatus(id, 'frozen');
message.success('账户已冻结');
fetchAccounts({ page: pagination.current });
} catch (error) {
message.error('冻结账户失败');
}
};
// 激活账户
const activateAccount = async (id) => {
try {
// 这里应该是实际的API调用
// await api.updateAccountStatus(id, 'active');
message.success('账户已激活');
fetchAccounts({ page: pagination.current });
} catch (error) {
message.error('激活账户失败');
}
};
// 提交账户表单
const handleAccountFormSubmit = () => {
accountFormRef.value.validate().then(async () => {
submitting.value = true;
try {
if (isEditing.value) {
// 编辑账户
// await api.updateAccount(accountForm.id, accountForm);
message.success('账户更新成功');
} else {
// 添加账户
// await api.createAccount(accountForm);
message.success('账户添加成功');
}
accountModalVisible.value = false;
fetchAccounts({ page: pagination.current });
} catch (error) {
message.error(isEditing.value ? '更新账户失败' : '添加账户失败');
} finally {
submitting.value = false;
}
});
};
// 重置账户表单
const resetAccountForm = () => {
accountForm.id = null;
accountForm.accountNumber = '';
accountForm.name = '';
accountForm.type = 'savings';
accountForm.userId = null;
accountForm.balance = 0;
accountForm.status = 'active';
accountForm.notes = '';
// 如果表单已经创建,则重置验证
if (accountFormRef.value) {
accountFormRef.value.resetFields();
}
};
// 用户选择过滤
const filterUserOption = (input, option) => {
return (
option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toString().toLowerCase().indexOf(input.toLowerCase()) >= 0
);
};
// 格式化货币
const formatCurrency = (value) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2
}).format(value);
};
// 获取账户状态名称
const getStatusName = (status) => {
const statusMap = {
active: '活跃',
frozen: '冻结',
closed: '关闭',
};
return statusMap[status] || status;
};
// 获取账户状态颜色
const getStatusColor = (status) => {
const colorMap = {
active: 'green',
frozen: 'orange',
closed: 'red',
};
return colorMap[status] || 'default';
};
// 获取账户类型名称
const getTypeName = (type) => {
const typeMap = {
savings: '储蓄账户',
checking: '活期账户',
credit: '信用账户',
loan: '贷款账户',
};
return typeMap[type] || type;
};
// 获取账户类型颜色
const getTypeColor = (type) => {
const colorMap = {
savings: 'blue',
checking: 'green',
credit: 'purple',
loan: 'orange',
};
return colorMap[type] || 'default';
};
// 获取交易类型名称
const getTransactionTypeName = (type) => {
const typeMap = {
deposit: '存款',
withdrawal: '取款',
transfer: '转账',
payment: '支付',
interest: '利息',
fee: '手续费',
};
return typeMap[type] || type;
};
// 获取交易类型颜色
const getTransactionTypeColor = (type) => {
const colorMap = {
deposit: 'green',
withdrawal: 'red',
transfer: 'blue',
payment: 'orange',
interest: 'purple',
fee: 'cyan',
};
return colorMap[type] || 'default';
};
// 生命周期钩子
onMounted(() => {
fetchAccounts();
fetchUsers();
});
return {
columns,
transactionColumns,
accounts,
loading,
searchQuery,
pagination,
usersList,
usersLoading,
accountFormRef,
accountModalVisible,
isEditing,
submitting,
accountForm,
rules,
accountDetailVisible,
selectedAccount,
recentTransactions,
transactionsLoading,
handleTableChange,
handleSearch,
showAddAccountModal,
editAccount,
viewAccount,
freezeAccount,
activateAccount,
handleAccountFormSubmit,
filterUserOption,
formatCurrency,
getStatusName,
getStatusColor,
getTypeName,
getTypeColor,
getTransactionTypeName,
getTransactionTypeColor,
};
},
});
</script>
<style scoped>
.accounts-page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin-bottom: 8px;
font-size: 24px;
font-weight: 500;
}
.page-header p {
color: rgba(0, 0, 0, 0.45);
}
.action-bar {
margin-bottom: 16px;
display: flex;
justify-content: flex-start;
align-items: center;
}
.warning-link {
color: #faad14;
}
.success-link {
color: #52c41a;
}
.danger-link {
color: #ff4d4f;
}
</style>

View File

@@ -0,0 +1,513 @@
<template>
<div class="dashboard">
<!-- 页面头部 -->
<div class="page-header">
<h1>仪表盘</h1>
<p>欢迎使用银行管理后台系统</p>
</div>
<!-- 统计卡片 -->
<a-row :gutter="[16, 16]" class="stats-cards">
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="总用户数"
:value="stats.totalUsers"
:loading="loading"
>
<template #prefix>
<user-outlined style="color: #1890ff" />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="总账户数"
:value="stats.totalAccounts"
:loading="loading"
>
<template #prefix>
<bank-outlined style="color: #52c41a" />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="今日交易数"
:value="stats.todayTransactions"
:loading="loading"
>
<template #prefix>
<transaction-outlined style="color: #fa8c16" />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="总资产"
:value="stats.totalAssets"
:precision="2"
:loading="loading"
suffix="元"
>
<template #prefix>
<dollar-outlined style="color: #f5222d" />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
<!-- 图表区域 -->
<a-row :gutter="[16, 16]" class="charts-section">
<a-col :xs="24" :lg="12">
<a-card title="交易趋势" class="chart-card">
<div ref="transactionChartRef" class="chart-container"></div>
</a-card>
</a-col>
<a-col :xs="24" :lg="12">
<a-card title="账户类型分布" class="chart-card">
<div ref="accountTypeChartRef" class="chart-container"></div>
</a-card>
</a-col>
</a-row>
<!-- 最近交易 -->
<a-row :gutter="[16, 16]" class="recent-section">
<a-col :xs="24" :lg="16">
<a-card title="最近交易" class="recent-card">
<a-table
:columns="transactionColumns"
:data-source="recentTransactions"
:loading="loading"
:pagination="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'amount'">
<span :class="getAmountClass(record.transaction_type)">
{{ formatAmount(record.amount) }}
</span>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.transaction_type)">
{{ getTypeName(record.transaction_type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusName(record.status) }}
</a-tag>
</template>
</template>
</a-table>
</a-card>
</a-col>
<a-col :xs="24" :lg="8">
<a-card title="系统信息" class="system-card">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="系统版本">
v1.0.0
</a-descriptions-item>
<a-descriptions-item label="运行时间">
{{ systemInfo.uptime }}
</a-descriptions-item>
<a-descriptions-item label="数据库状态">
<a-tag color="green">正常</a-tag>
</a-descriptions-item>
<a-descriptions-item label="最后更新">
{{ systemInfo.lastUpdate }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { message } from 'ant-design-vue'
import * as echarts from 'echarts'
import {
UserOutlined,
BankOutlined,
TransactionOutlined,
DollarOutlined
} from '@ant-design/icons-vue'
import { api } from '@/utils/api'
// 响应式数据
const loading = ref(false)
const stats = ref({
totalUsers: 0,
totalAccounts: 0,
todayTransactions: 0,
totalAssets: 0
})
const recentTransactions = ref([])
const systemInfo = ref({
uptime: '0天0小时',
lastUpdate: new Date().toLocaleString()
})
// 图表引用
const transactionChartRef = ref()
const accountTypeChartRef = ref()
let transactionChart = null
let accountTypeChart = null
// 交易表格列配置
const transactionColumns = [
{
title: '交易号',
dataIndex: 'transaction_number',
key: 'transaction_number',
width: 120,
ellipsis: true
},
{
title: '类型',
dataIndex: 'transaction_type',
key: 'type',
width: 80
},
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
width: 100,
align: 'right'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 120
}
]
// 获取统计数据
const fetchStats = async () => {
try {
loading.value = true
// 模拟数据实际应该调用API
stats.value = {
totalUsers: 1250,
totalAccounts: 3420,
todayTransactions: 156,
totalAssets: 12500000.50
}
// 获取最近交易
const transactionResult = await api.transactions.getList({
limit: 10,
page: 1
})
if (transactionResult.success) {
recentTransactions.value = transactionResult.data.transactions || []
}
} catch (error) {
console.error('获取统计数据失败:', error)
message.error('获取统计数据失败')
} finally {
loading.value = false
}
}
// 初始化交易趋势图表
const initTransactionChart = () => {
if (!transactionChartRef.value) return
transactionChart = echarts.init(transactionChartRef.value)
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['存款', '取款', '转账']
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [
{
name: '存款',
type: 'line',
data: [120, 132, 101, 134, 90, 230, 210],
smooth: true
},
{
name: '取款',
type: 'line',
data: [220, 182, 191, 234, 290, 330, 310],
smooth: true
},
{
name: '转账',
type: 'line',
data: [150, 232, 201, 154, 190, 330, 410],
smooth: true
}
]
}
transactionChart.setOption(option)
}
// 初始化账户类型分布图表
const initAccountTypeChart = () => {
if (!accountTypeChartRef.value) return
accountTypeChart = echarts.init(accountTypeChartRef.value)
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
name: '账户类型',
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '储蓄账户' },
{ value: 735, name: '支票账户' },
{ value: 580, name: '信用卡账户' },
{ value: 484, name: '贷款账户' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
accountTypeChart.setOption(option)
}
// 格式化金额
const formatAmount = (amount) => {
return (amount / 100).toFixed(2)
}
// 获取金额样式类
const getAmountClass = (type) => {
if (type === 'deposit' || type === 'transfer_in') {
return 'amount-positive'
} else if (type === 'withdrawal' || type === 'transfer_out') {
return 'amount-negative'
}
return 'amount-zero'
}
// 获取类型名称
const getTypeName = (type) => {
const typeMap = {
'deposit': '存款',
'withdrawal': '取款',
'transfer_in': '转入',
'transfer_out': '转出',
'interest': '利息',
'fee': '手续费'
}
return typeMap[type] || type
}
// 获取类型颜色
const getTypeColor = (type) => {
const colorMap = {
'deposit': 'green',
'withdrawal': 'red',
'transfer_in': 'blue',
'transfer_out': 'orange',
'interest': 'purple',
'fee': 'gray'
}
return colorMap[type] || 'default'
}
// 获取状态名称
const getStatusName = (status) => {
const statusMap = {
'pending': '处理中',
'completed': '已完成',
'failed': '失败',
'cancelled': '已取消'
}
return statusMap[status] || status
}
// 获取状态颜色
const getStatusColor = (status) => {
const colorMap = {
'pending': 'processing',
'completed': 'success',
'failed': 'error',
'cancelled': 'default'
}
return colorMap[status] || 'default'
}
// 处理窗口大小变化
const handleResize = () => {
if (transactionChart) {
transactionChart.resize()
}
if (accountTypeChart) {
accountTypeChart.resize()
}
}
onMounted(() => {
fetchStats()
// 延迟初始化图表确保DOM已渲染
setTimeout(() => {
initTransactionChart()
initAccountTypeChart()
}, 100)
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (transactionChart) {
transactionChart.dispose()
}
if (accountTypeChart) {
accountTypeChart.dispose()
}
window.removeEventListener('resize', handleResize)
})
</script>
<style scoped>
.dashboard {
padding: 0;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: #262626;
}
.page-header p {
margin: 0;
color: #8c8c8c;
font-size: 14px;
}
.stats-cards {
margin-bottom: 24px;
}
.stat-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.stat-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.charts-section {
margin-bottom: 24px;
}
.chart-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.chart-container {
height: 300px;
width: 100%;
}
.recent-section {
margin-bottom: 24px;
}
.recent-card,
.system-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.amount-positive {
color: #52c41a;
font-weight: 600;
}
.amount-negative {
color: #ff4d4f;
font-weight: 600;
}
.amount-zero {
color: #8c8c8c;
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 768px) {
.stats-cards .ant-col {
margin-bottom: 16px;
}
.charts-section .ant-col {
margin-bottom: 16px;
}
.recent-section .ant-col {
margin-bottom: 16px;
}
.chart-container {
height: 250px;
}
}
</style>

View File

@@ -0,0 +1,267 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-header">
<div class="logo">
<bank-outlined />
</div>
<h1 class="title">银行管理后台系统</h1>
<p class="subtitle">专业的银行管理解决方案</p>
</div>
<a-form
:model="loginForm"
:rules="loginRules"
@finish="handleLogin"
class="login-form"
size="large"
>
<a-form-item name="username">
<a-input
v-model:value="loginForm.username"
placeholder="请输入用户名"
:prefix="h(UserOutlined)"
/>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="loginForm.password"
placeholder="请输入密码"
:prefix="h(LockOutlined)"
/>
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="loginForm.remember">
记住密码
</a-checkbox>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="loading"
block
size="large"
>
登录
</a-button>
</a-form-item>
</a-form>
<div class="login-footer">
<p>默认账户admin / Admin123456</p>
<p>测试账户testuser / Test123456</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import { useUserStore } from '@/stores'
import {
BankOutlined,
UserOutlined,
LockOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 登录表单
const loginForm = ref({
username: '',
password: '',
remember: false
})
// 加载状态
const loading = ref(false)
// 表单验证规则
const loginRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '用户名长度在3到50个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度在6到20个字符', trigger: 'blur' }
]
}
// 处理登录
const handleLogin = async () => {
try {
loading.value = true
const result = await userStore.login(
loginForm.value.username,
loginForm.value.password
)
if (result.success) {
message.success('登录成功')
// 重定向到目标页面或仪表盘
const redirectPath = route.query.redirect || '/dashboard'
router.push(redirectPath)
} else {
message.error(result.message || '登录失败')
}
} catch (error) {
console.error('登录错误:', error)
message.error(error.message || '登录失败,请检查网络连接')
} finally {
loading.value = false
}
}
// 页面加载时检查是否已登录
if (userStore.isLoggedIn) {
const redirectPath = route.query.redirect || '/dashboard'
router.push(redirectPath)
}
</script>
<style scoped>
.login-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #f0f2f5;
overflow: hidden;
}
.login-box {
position: relative;
z-index: 2;
width: 400px;
padding: 40px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.logo {
font-size: 48px;
color: #1890ff;
margin-bottom: 16px;
}
.title {
font-size: 24px;
font-weight: 600;
color: #262626;
margin: 0 0 8px 0;
}
.subtitle {
font-size: 14px;
color: #8c8c8c;
margin: 0;
}
.login-form {
margin-bottom: 24px;
}
.login-form .ant-form-item {
margin-bottom: 20px;
}
.login-form .ant-input,
.login-form .ant-input-password {
height: 48px;
border-radius: 8px;
border: 1px solid #d9d9d9;
font-size: 16px;
}
.login-form .ant-input:focus,
.login-form .ant-input-password:focus {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.login-form .ant-btn {
height: 48px;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
background: #1890ff;
border-color: #1890ff;
}
.login-form .ant-btn:hover {
background: #40a9ff;
border-color: #40a9ff;
}
.login-footer {
text-align: center;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.login-footer p {
margin: 4px 0;
font-size: 12px;
color: #8c8c8c;
}
/* 移除渐变与动画背景,仅保留纯色背景 */
/* 响应式设计 */
@media (max-width: 768px) {
.login-box {
width: 90%;
max-width: 400px;
padding: 24px;
margin: 16px;
}
.title {
font-size: 20px;
}
.logo {
font-size: 40px;
}
}
@media (max-width: 480px) {
.login-box {
padding: 20px;
}
.title {
font-size: 18px;
}
.logo {
font-size: 36px;
}
.login-form .ant-input,
.login-form .ant-input-password,
.login-form .ant-btn {
height: 44px;
font-size: 14px;
}
}
</style>

View File

@@ -0,0 +1 @@
<template>\n <div class=page-container>404 Not Found</div>\n</template>\n\n<script setup>\n</script>\n\n<style scoped>\n.page-container { padding: 24px; text-align:center; color:#8c8c8c; }\n</style>

View File

@@ -0,0 +1 @@
<template>\n <div class=page-container>Profile</div>\n</template>\n\n<script setup>\n</script>\n\n<style scoped>\n.page-container { padding: 24px; }\n</style>

View File

@@ -0,0 +1,717 @@
<template>
<div class="reports-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>报表统计</h1>
<p>查看和导出银行业务报表</p>
</div>
<!-- 报表类型选择 -->
<a-card :bordered="false" class="report-selector-card">
<a-tabs v-model:activeKey="activeReportType" @change="handleReportTypeChange">
<a-tab-pane key="transaction" tab="交易报表">
<a-form layout="horizontal" :model="transactionReportForm">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="日期范围" name="dateRange">
<a-range-picker
v-model:value="transactionReportForm.dateRange"
style="width: 100%"
:placeholder="['开始日期', '结束日期']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="交易类型" name="transactionTypes">
<a-select
v-model:value="transactionReportForm.transactionTypes"
mode="multiple"
placeholder="选择交易类型"
style="width: 100%"
:options="transactionTypeOptions"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="报表格式" name="format">
<a-radio-group v-model:value="transactionReportForm.format">
<a-radio value="excel">Excel</a-radio>
<a-radio value="pdf">PDF</a-radio>
<a-radio value="csv">CSV</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="24" style="text-align: right;">
<a-button type="primary" @click="generateTransactionReport">
<file-excel-outlined /> 生成报表
</a-button>
</a-col>
</a-row>
</a-form>
</a-tab-pane>
<a-tab-pane key="account" tab="账户报表">
<a-form layout="horizontal" :model="accountReportForm">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="截止日期" name="endDate">
<a-date-picker
v-model:value="accountReportForm.endDate"
style="width: 100%"
placeholder="选择截止日期"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="账户类型" name="accountTypes">
<a-select
v-model:value="accountReportForm.accountTypes"
mode="multiple"
placeholder="选择账户类型"
style="width: 100%"
:options="accountTypeOptions"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="报表格式" name="format">
<a-radio-group v-model:value="accountReportForm.format">
<a-radio value="excel">Excel</a-radio>
<a-radio value="pdf">PDF</a-radio>
<a-radio value="csv">CSV</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="24" style="text-align: right;">
<a-button type="primary" @click="generateAccountReport">
<file-excel-outlined /> 生成报表
</a-button>
</a-col>
</a-row>
</a-form>
</a-tab-pane>
<a-tab-pane key="user" tab="用户报表">
<a-form layout="horizontal" :model="userReportForm">
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="用户角色" name="roles">
<a-select
v-model:value="userReportForm.roles"
mode="multiple"
placeholder="选择用户角色"
style="width: 100%"
:options="userRoleOptions"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="注册日期" name="registrationDateRange">
<a-range-picker
v-model:value="userReportForm.registrationDateRange"
style="width: 100%"
:placeholder="['开始日期', '结束日期']"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="报表格式" name="format">
<a-radio-group v-model:value="userReportForm.format">
<a-radio value="excel">Excel</a-radio>
<a-radio value="pdf">PDF</a-radio>
<a-radio value="csv">CSV</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="24" style="text-align: right;">
<a-button type="primary" @click="generateUserReport">
<file-excel-outlined /> 生成报表
</a-button>
</a-col>
</a-row>
</a-form>
</a-tab-pane>
</a-tabs>
</a-card>
<!-- 数据可视化区域 -->
<!-- <a-row :gutter="[16, 16]" class="chart-row">
<a-col :xs="24" :lg="12">
<a-card :bordered="false" title="交易金额趋势">
<div class="chart-container">
<div class="chart-placeholder">
<bar-chart-outlined />
<p>交易金额趋势图</p>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="12">
<a-card :bordered="false" title="交易类型分布">
<div class="chart-container">
<div class="chart-placeholder">
<pie-chart-outlined />
<p>交易类型饼图</p>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="12">
<a-card :bordered="false" title="账户余额分布">
<div class="chart-container">
<div class="chart-placeholder">
<fund-outlined />
<p>账户余额分布图</p>
</div>
</div>
</a-card>
</a-col>
<a-col :xs="24" :lg="12">
<a-card :bordered="false" title="用户活跃度">
<div class="chart-container">
<div class="chart-placeholder">
<line-chart-outlined />
<p>用户活跃度趋势图</p>
</div>
</div>
</a-card>
</a-col>
</a-row>
-->
<!-- 最近生成的报表 -->
<a-card :bordered="false" title="最近生成的报表" class="recent-reports-card">
<a-table
:columns="recentReportsColumns"
:data-source="recentReports"
:loading="loading"
:pagination="{ pageSize: 5 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getReportTypeColor(record.type)">
{{ getReportTypeName(record.type) }}
</a-tag>
</template>
<template v-if="column.key === 'format'">
<a-tag :color="getFormatColor(record.format)">
{{ record.format.toUpperCase() }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="downloadReport(record)">
<download-outlined /> 下载
</a>
<a-divider type="vertical" />
<a @click="viewReport(record)">
<eye-outlined /> 查看
</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此报表吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteReport(record.id)"
>
<a class="danger-link">
<delete-outlined /> 删除
</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 报表预览对话框 -->
<a-modal
v-model:visible="reportPreviewVisible"
title="报表预览"
width="800px"
:footer="null"
>
<template v-if="selectedReport">
<div class="report-preview">
<h2>{{ selectedReport.name }}</h2>
<p>生成时间: {{ selectedReport.createdAt }}</p>
<p>报表类型: {{ getReportTypeName(selectedReport.type) }}</p>
<p>格式: {{ selectedReport.format.toUpperCase() }}</p>
<a-divider />
<div class="report-preview-content">
<!-- 这里应该是实际的报表预览内容 -->
<div class="preview-placeholder">
<file-outlined />
<p>报表预览内容</p>
</div>
</div>
<div style="margin-top: 16px; text-align: right;">
<a-button type="primary" @click="downloadReport(selectedReport)" style="margin-right: 8px;">
<download-outlined /> 下载
</a-button>
<a-button @click="reportPreviewVisible = false">关闭</a-button>
</div>
</div>
</template>
</a-modal>
</div>
</template>
<script>
import { defineComponent, ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import {
FileExcelOutlined,
BarChartOutlined,
PieChartOutlined,
FundOutlined,
LineChartOutlined,
DownloadOutlined,
EyeOutlined,
DeleteOutlined,
FileOutlined
} from '@ant-design/icons-vue';
export default defineComponent({
name: 'ReportsPage',
components: {
FileExcelOutlined,
BarChartOutlined,
PieChartOutlined,
FundOutlined,
LineChartOutlined,
DownloadOutlined,
EyeOutlined,
DeleteOutlined,
FileOutlined
},
setup() {
// 活跃的报表类型
const activeReportType = ref('transaction');
// 交易报表表单
const transactionReportForm = reactive({
dateRange: [],
transactionTypes: [],
format: 'excel'
});
// 账户报表表单
const accountReportForm = reactive({
endDate: null,
accountTypes: [],
format: 'excel'
});
// 用户报表表单
const userReportForm = reactive({
roles: [],
registrationDateRange: [],
format: 'excel'
});
// 交易类型选项
const transactionTypeOptions = [
{ label: '存款', value: 'deposit' },
{ label: '取款', value: 'withdrawal' },
{ label: '转账', value: 'transfer' },
{ label: '支付', value: 'payment' },
{ label: '利息', value: 'interest' },
{ label: '手续费', value: 'fee' }
];
// 账户类型选项
const accountTypeOptions = [
{ label: '储蓄账户', value: 'savings' },
{ label: '活期账户', value: 'checking' },
{ label: '信用账户', value: 'credit' },
{ label: '贷款账户', value: 'loan' }
];
// 用户角色选项
const userRoleOptions = [
{ label: '管理员', value: 'admin' },
{ label: '经理', value: 'manager' },
{ label: '柜员', value: 'teller' },
{ label: '普通用户', value: 'user' }
];
// 最近报表列表
const recentReports = ref([]);
const loading = ref(false);
// 最近报表表格列定义
const recentReportsColumns = [
{
title: '报表名称',
dataIndex: 'name',
key: 'name',
},
{
title: '报表类型',
dataIndex: 'type',
key: 'type',
},
{
title: '格式',
dataIndex: 'format',
key: 'format',
},
{
title: '生成时间',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '生成人',
dataIndex: 'createdBy',
key: 'createdBy',
},
{
title: '操作',
key: 'action',
},
];
// 报表预览相关
const reportPreviewVisible = ref(false);
const selectedReport = ref(null);
// 获取最近报表列表
const fetchRecentReports = async () => {
loading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getRecentReports();
// recentReports.value = response.data;
// 模拟数据
setTimeout(() => {
recentReports.value = [
{
id: 'R20230917001',
name: '2023年9月交易报表',
type: 'transaction',
format: 'excel',
createdAt: '2023-09-17 10:30:25',
createdBy: '张三 (管理员)'
},
{
id: 'R20230916001',
name: '储蓄账户余额报表',
type: 'account',
format: 'pdf',
createdAt: '2023-09-16 15:45:12',
createdBy: '李四 (经理)'
},
{
id: 'R20230915001',
name: '用户活跃度报表',
type: 'user',
format: 'csv',
createdAt: '2023-09-15 09:20:45',
createdBy: '张三 (管理员)'
},
{
id: 'R20230914001',
name: '信用卡交易报表',
type: 'transaction',
format: 'excel',
createdAt: '2023-09-14 14:10:33',
createdBy: '王五 (柜员)'
},
{
id: 'R20230913001',
name: '新用户注册报表',
type: 'user',
format: 'pdf',
createdAt: '2023-09-13 11:05:22',
createdBy: '李四 (经理)'
},
];
loading.value = false;
}, 500);
} catch (error) {
message.error('获取最近报表列表失败');
loading.value = false;
}
};
// 报表类型变更处理
const handleReportTypeChange = (key) => {
console.log('切换到报表类型:', key);
};
// 生成交易报表
const generateTransactionReport = () => {
if (!transactionReportForm.dateRange || transactionReportForm.dateRange.length !== 2) {
message.warning('请选择日期范围');
return;
}
message.loading('正在生成交易报表,请稍候...', 1.5);
// 这里应该是实际的API调用
// const params = {
// startDate: transactionReportForm.dateRange[0].format('YYYY-MM-DD'),
// endDate: transactionReportForm.dateRange[1].format('YYYY-MM-DD'),
// transactionTypes: transactionReportForm.transactionTypes,
// format: transactionReportForm.format
// };
// api.generateTransactionReport(params).then(response => {
// message.success('交易报表生成成功');
// fetchRecentReports();
// }).catch(error => {
// message.error('交易报表生成失败');
// });
// 模拟生成报表
setTimeout(() => {
message.success('交易报表生成成功');
fetchRecentReports();
}, 2000);
};
// 生成账户报表
const generateAccountReport = () => {
if (!accountReportForm.endDate) {
message.warning('请选择截止日期');
return;
}
message.loading('正在生成账户报表,请稍候...', 1.5);
// 这里应该是实际的API调用
// const params = {
// endDate: accountReportForm.endDate.format('YYYY-MM-DD'),
// accountTypes: accountReportForm.accountTypes,
// format: accountReportForm.format
// };
// api.generateAccountReport(params).then(response => {
// message.success('账户报表生成成功');
// fetchRecentReports();
// }).catch(error => {
// message.error('账户报表生成失败');
// });
// 模拟生成报表
setTimeout(() => {
message.success('账户报表生成成功');
fetchRecentReports();
}, 2000);
};
// 生成用户报表
const generateUserReport = () => {
message.loading('正在生成用户报表,请稍候...', 1.5);
// 这里应该是实际的API调用
// const params = {
// roles: userReportForm.roles,
// startDate: userReportForm.registrationDateRange?.[0]?.format('YYYY-MM-DD'),
// endDate: userReportForm.registrationDateRange?.[1]?.format('YYYY-MM-DD'),
// format: userReportForm.format
// };
// api.generateUserReport(params).then(response => {
// message.success('用户报表生成成功');
// fetchRecentReports();
// }).catch(error => {
// message.error('用户报表生成失败');
// });
// 模拟生成报表
setTimeout(() => {
message.success('用户报表生成成功');
fetchRecentReports();
}, 2000);
};
// 下载报表
const downloadReport = (report) => {
message.success(`正在下载报表: ${report.name}`);
// 实际应用中这里应该调用下载API
};
// 查看报表
const viewReport = (report) => {
selectedReport.value = report;
reportPreviewVisible.value = true;
};
// 删除报表
const deleteReport = (id) => {
// 这里应该是实际的API调用
// api.deleteReport(id).then(response => {
// message.success('报表删除成功');
// fetchRecentReports();
// }).catch(error => {
// message.error('报表删除失败');
// });
// 模拟删除报表
message.success('报表删除成功');
recentReports.value = recentReports.value.filter(report => report.id !== id);
};
// 获取报表类型名称
const getReportTypeName = (type) => {
const typeMap = {
transaction: '交易报表',
account: '账户报表',
user: '用户报表',
};
return typeMap[type] || type;
};
// 获取报表类型颜色
const getReportTypeColor = (type) => {
const colorMap = {
transaction: 'blue',
account: 'green',
user: 'purple',
};
return colorMap[type] || 'default';
};
// 获取格式颜色
const getFormatColor = (format) => {
const colorMap = {
excel: 'green',
pdf: 'red',
csv: 'orange',
};
return colorMap[format] || 'default';
};
// 生命周期钩子
onMounted(() => {
fetchRecentReports();
});
return {
activeReportType,
transactionReportForm,
accountReportForm,
userReportForm,
transactionTypeOptions,
accountTypeOptions,
userRoleOptions,
recentReports,
loading,
recentReportsColumns,
reportPreviewVisible,
selectedReport,
handleReportTypeChange,
generateTransactionReport,
generateAccountReport,
generateUserReport,
downloadReport,
viewReport,
deleteReport,
getReportTypeName,
getReportTypeColor,
getFormatColor,
};
},
});
</script>
<style scoped>
.reports-page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin-bottom: 8px;
font-size: 24px;
font-weight: 500;
}
.page-header p {
color: rgba(0, 0, 0, 0.45);
}
.report-selector-card {
margin-bottom: 24px;
}
.chart-row {
margin-bottom: 24px;
}
.chart-container {
height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.chart-placeholder {
text-align: center;
color: rgba(0, 0, 0, 0.25);
}
.chart-placeholder svg {
font-size: 48px;
margin-bottom: 16px;
}
.recent-reports-card {
margin-bottom: 24px;
}
.danger-link {
color: #ff4d4f;
}
.report-preview {
padding: 16px;
}
.report-preview h2 {
margin-bottom: 16px;
}
.report-preview-content {
min-height: 400px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
padding: 16px;
}
.preview-placeholder {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.25);
}
.preview-placeholder svg {
font-size: 48px;
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,615 @@
<template>
<div class="settings-container">
<a-page-header title="系统设置" sub-title="管理系统配置和参数" />
<a-tabs default-active-key="1" class="settings-tabs">
<a-tab-pane key="1" tab="基本设置">
<a-card title="系统参数配置" class="settings-card">
<a-form :model="basicSettings" layout="vertical">
<a-form-item label="系统名称">
<a-input v-model:value="basicSettings.systemName" placeholder="请输入系统名称" />
</a-form-item>
<a-form-item label="系统描述">
<a-textarea v-model:value="basicSettings.systemDescription" placeholder="请输入系统描述" :rows="4" />
</a-form-item>
<a-form-item label="管理员邮箱">
<a-input v-model:value="basicSettings.adminEmail" placeholder="请输入管理员邮箱" />
</a-form-item>
<a-form-item label="系统维护模式">
<a-switch v-model:checked="basicSettings.maintenanceMode" />
<span class="setting-description">启用后除管理员外的用户将无法登录系统</span>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveBasicSettings">保存设置</a-button>
<a-button style="margin-left: 10px" @click="resetBasicSettings">重置</a-button>
</a-form-item>
</a-form>
</a-card>
</a-tab-pane>
<a-tab-pane key="2" tab="安全设置">
<a-card title="密码策略" class="settings-card">
<a-form :model="securitySettings" layout="vertical">
<a-form-item label="密码最小长度">
<a-input-number v-model:value="securitySettings.minPasswordLength" :min="6" :max="20" />
<span class="setting-description">密码最小长度要求6-20个字符</span>
</a-form-item>
<a-form-item label="密码复杂度要求">
<a-checkbox-group v-model:value="securitySettings.passwordComplexity">
<a-checkbox value="uppercase">必须包含大写字母</a-checkbox>
<a-checkbox value="lowercase">必须包含小写字母</a-checkbox>
<a-checkbox value="numbers">必须包含数字</a-checkbox>
<a-checkbox value="special">必须包含特殊字符</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="密码过期时间">
<a-select v-model:value="securitySettings.passwordExpiry">
<a-select-option value="30">30</a-select-option>
<a-select-option value="60">60</a-select-option>
<a-select-option value="90">90</a-select-option>
<a-select-option value="180">180</a-select-option>
<a-select-option value="365">365</a-select-option>
<a-select-option value="0">永不过期</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="登录失败锁定">
<a-input-number v-model:value="securitySettings.loginAttempts" :min="3" :max="10" />
<span class="setting-description">连续失败次数后锁定账户</span>
</a-form-item>
<a-form-item label="锁定时间">
<a-select v-model:value="securitySettings.lockDuration">
<a-select-option value="15">15分钟</a-select-option>
<a-select-option value="30">30分钟</a-select-option>
<a-select-option value="60">1小时</a-select-option>
<a-select-option value="1440">24小时</a-select-option>
<a-select-option value="-1">需管理员手动解锁</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveSecuritySettings">保存设置</a-button>
<a-button style="margin-left: 10px" @click="resetSecuritySettings">重置</a-button>
</a-form-item>
</a-form>
</a-card>
<a-card title="双因素认证" class="settings-card">
<a-form :model="twoFactorSettings" layout="vertical">
<a-form-item label="启用双因素认证">
<a-switch v-model:checked="twoFactorSettings.enabled" />
<span class="setting-description">要求用户使用双因素认证登录系统</span>
</a-form-item>
<a-form-item label="适用角色">
<a-checkbox-group v-model:value="twoFactorSettings.roles">
<a-checkbox value="admin">管理员</a-checkbox>
<a-checkbox value="manager">经理</a-checkbox>
<a-checkbox value="teller">柜员</a-checkbox>
<a-checkbox value="user">普通用户</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveTwoFactorSettings">保存设置</a-button>
<a-button style="margin-left: 10px" @click="resetTwoFactorSettings">重置</a-button>
</a-form-item>
</a-form>
</a-card>
</a-tab-pane>
<a-tab-pane key="3" tab="系统日志">
<a-card title="日志查询" class="settings-card">
<a-form layout="inline" class="log-search-form">
<a-form-item label="日志类型">
<a-select v-model:value="logQuery.type" style="width: 150px">
<a-select-option value="all">全部</a-select-option>
<a-select-option value="login">登录日志</a-select-option>
<a-select-option value="operation">操作日志</a-select-option>
<a-select-option value="system">系统日志</a-select-option>
<a-select-option value="error">错误日志</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="日期范围">
<a-range-picker v-model:value="logQuery.dateRange" />
</a-form-item>
<a-form-item label="用户">
<a-input v-model:value="logQuery.user" placeholder="用户名" style="width: 150px" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="searchLogs">查询</a-button>
<a-button style="margin-left: 10px" @click="resetLogQuery">重置</a-button>
</a-form-item>
</a-form>
<a-table
:columns="logColumns"
:data-source="logs"
:loading="logsLoading"
:pagination="logPagination"
@change="handleLogTableChange"
class="log-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<a-tag :color="getLogLevelColor(record.level)">{{ record.level }}</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="viewLogDetail(record)">查看详情</a>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
<a-tab-pane key="4" tab="备份与恢复">
<a-card title="数据备份" class="settings-card">
<a-form layout="vertical">
<a-form-item label="自动备份">
<a-switch v-model:checked="backupSettings.autoBackup" />
<span class="setting-description">启用系统自动备份功能</span>
</a-form-item>
<a-form-item label="备份频率">
<a-select v-model:value="backupSettings.frequency" style="width: 200px">
<a-select-option value="daily">每天</a-select-option>
<a-select-option value="weekly">每周</a-select-option>
<a-select-option value="monthly">每月</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="保留备份数量">
<a-input-number v-model:value="backupSettings.keepCount" :min="1" :max="30" />
<span class="setting-description">系统将保留最近的备份数量</span>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveBackupSettings">保存设置</a-button>
<a-button style="margin-left: 10px" @click="resetBackupSettings">重置</a-button>
<a-button type="primary" danger style="margin-left: 10px" @click="createManualBackup">立即备份</a-button>
</a-form-item>
</a-form>
<a-divider />
<h3>备份历史</h3>
<a-table
:columns="backupColumns"
:data-source="backups"
:loading="backupsLoading"
:pagination="{ pageSize: 5 }"
class="backup-table"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="link" @click="downloadBackup(record)">下载</a-button>
<a-button type="link" danger @click="confirmRestoreBackup(record)">恢复</a-button>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
</a-tabs>
<!-- 日志详情弹窗 -->
<a-modal
v-model:visible="logDetailVisible"
title="日志详情"
width="700px"
:footer="null"
>
<a-descriptions bordered :column="1">
<a-descriptions-item label="ID">{{ selectedLog.id }}</a-descriptions-item>
<a-descriptions-item label="时间">{{ selectedLog.timestamp }}</a-descriptions-item>
<a-descriptions-item label="类型">{{ selectedLog.type }}</a-descriptions-item>
<a-descriptions-item label="级别">
<a-tag :color="getLogLevelColor(selectedLog.level)">{{ selectedLog.level }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="用户">{{ selectedLog.user }}</a-descriptions-item>
<a-descriptions-item label="IP地址">{{ selectedLog.ip }}</a-descriptions-item>
<a-descriptions-item label="操作">{{ selectedLog.action }}</a-descriptions-item>
<a-descriptions-item label="详细信息">
<div class="log-detail-content">{{ selectedLog.message }}</div>
</a-descriptions-item>
</a-descriptions>
</a-modal>
<!-- 恢复备份确认弹窗 -->
<a-modal
v-model:visible="restoreConfirmVisible"
title="恢复备份"
@ok="restoreBackup"
okText="确认恢复"
cancelText="取消"
:okButtonProps="{ danger: true }"
>
<p>您确定要恢复到 {{ selectedBackup.created_at }} 的备份吗</p>
<p><strong>警告</strong> 此操作将覆盖当前系统数据且不可撤销</p>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
// 基本设置
const basicSettings = reactive({
systemName: '宁夏银行管理系统',
systemDescription: '专业的银行业务管理解决方案,提供全面的账户管理、交易处理和报表分析功能。',
adminEmail: 'admin@example.com',
maintenanceMode: false
})
// 安全设置
const securitySettings = reactive({
minPasswordLength: 8,
passwordComplexity: ['uppercase', 'lowercase', 'numbers'],
passwordExpiry: '90',
loginAttempts: 5,
lockDuration: '60'
})
// 双因素认证设置
const twoFactorSettings = reactive({
enabled: false,
roles: ['admin', 'manager']
})
// 备份设置
const backupSettings = reactive({
autoBackup: true,
frequency: 'daily',
keepCount: 7
})
// 日志查询参数
const logQuery = reactive({
type: 'all',
dateRange: null,
user: ''
})
// 日志表格列定义
const logColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', sorter: true },
{ title: '类型', dataIndex: 'type', key: 'type', filters: [
{ text: '登录日志', value: 'login' },
{ text: '操作日志', value: 'operation' },
{ text: '系统日志', value: 'system' },
{ text: '错误日志', value: 'error' }
] },
{ title: '级别', dataIndex: 'level', key: 'level' },
{ title: '用户', dataIndex: 'user', key: 'user' },
{ title: '操作', dataIndex: 'action', key: 'action' },
{ title: '消息', dataIndex: 'message', key: 'message', ellipsis: true },
{ title: '操作', key: 'action', width: 100 }
]
// 备份表格列定义
const backupColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
{ title: '大小', dataIndex: 'size', key: 'size' },
{ title: '类型', dataIndex: 'type', key: 'type' },
{ title: '创建者', dataIndex: 'created_by', key: 'created_by' },
{ title: '操作', key: 'action', width: 150 }
]
// 日志数据
const logs = ref([])
const logsLoading = ref(false)
const logPagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
// 备份数据
const backups = ref([])
const backupsLoading = ref(false)
// 日志详情弹窗
const logDetailVisible = ref(false)
const selectedLog = ref({})
// 恢复备份确认弹窗
const restoreConfirmVisible = ref(false)
const selectedBackup = ref({})
// 页面加载时获取数据
onMounted(() => {
fetchSettings()
fetchLogs()
fetchBackups()
})
// 获取系统设置
const fetchSettings = async () => {
try {
// 这里应该调用API获取系统设置
// 模拟API调用
setTimeout(() => {
// 设置已经在上面初始化了
}, 500)
} catch (error) {
message.error('获取系统设置失败')
console.error('获取系统设置失败:', error)
}
}
// 获取日志数据
const fetchLogs = async () => {
logsLoading.value = true
try {
// 这里应该调用API获取日志数据
// 模拟API调用
setTimeout(() => {
logs.value = [
{
id: '1001',
timestamp: '2025-09-17 14:30:25',
type: 'login',
level: 'info',
user: 'admin',
ip: '192.168.1.100',
action: '用户登录',
message: '管理员成功登录系统'
},
{
id: '1002',
timestamp: '2025-09-17 14:35:12',
type: 'operation',
level: 'warning',
user: 'admin',
ip: '192.168.1.100',
action: '修改用户权限',
message: '修改用户 user001 的权限'
},
{
id: '1003',
timestamp: '2025-09-17 14:40:05',
type: 'system',
level: 'error',
user: 'system',
ip: '127.0.0.1',
action: '系统错误',
message: '数据库连接失败: Connection timeout'
}
]
logPagination.total = 100
logsLoading.value = false
}, 500)
} catch (error) {
message.error('获取日志数据失败')
console.error('获取日志数据失败:', error)
logsLoading.value = false
}
}
// 获取备份数据
const fetchBackups = async () => {
backupsLoading.value = true
try {
// 这里应该调用API获取备份数据
// 模拟API调用
setTimeout(() => {
backups.value = [
{
id: '1',
created_at: '2025-09-17 00:00:00',
size: '125.4 MB',
type: '自动备份',
created_by: 'system'
},
{
id: '2',
created_at: '2025-09-16 00:00:00',
size: '124.8 MB',
type: '自动备份',
created_by: 'system'
},
{
id: '3',
created_at: '2025-09-15 14:30:00',
size: '126.2 MB',
type: '手动备份',
created_by: 'admin'
}
]
backupsLoading.value = false
}, 500)
} catch (error) {
message.error('获取备份数据失败')
console.error('获取备份数据失败:', error)
backupsLoading.value = false
}
}
// 保存基本设置
const saveBasicSettings = () => {
// 这里应该调用API保存基本设置
message.success('基本设置保存成功')
}
// 重置基本设置
const resetBasicSettings = () => {
// 重置为初始值或从服务器重新获取
fetchSettings()
message.info('基本设置已重置')
}
// 保存安全设置
const saveSecuritySettings = () => {
// 这里应该调用API保存安全设置
message.success('安全设置保存成功')
}
// 重置安全设置
const resetSecuritySettings = () => {
// 重置为初始值或从服务器重新获取
fetchSettings()
message.info('安全设置已重置')
}
// 保存双因素认证设置
const saveTwoFactorSettings = () => {
// 这里应该调用API保存双因素认证设置
message.success('双因素认证设置保存成功')
}
// 重置双因素认证设置
const resetTwoFactorSettings = () => {
// 重置为初始值或从服务器重新获取
fetchSettings()
message.info('双因素认证设置已重置')
}
// 保存备份设置
const saveBackupSettings = () => {
// 这里应该调用API保存备份设置
message.success('备份设置保存成功')
}
// 重置备份设置
const resetBackupSettings = () => {
// 重置为初始值或从服务器重新获取
fetchSettings()
message.info('备份设置已重置')
}
// 创建手动备份
const createManualBackup = () => {
// 这里应该调用API创建手动备份
message.loading({ content: '正在创建备份...', key: 'backupLoading' })
// 模拟API调用
setTimeout(() => {
message.success({ content: '备份创建成功', key: 'backupLoading' })
fetchBackups() // 刷新备份列表
}, 2000)
}
// 搜索日志
const searchLogs = () => {
// 重置分页到第一页
logPagination.current = 1
fetchLogs()
}
// 重置日志查询条件
const resetLogQuery = () => {
logQuery.type = 'all'
logQuery.dateRange = null
logQuery.user = ''
searchLogs()
}
// 处理日志表格变化(排序、筛选、分页)
const handleLogTableChange = (pagination, filters, sorter) => {
logPagination.current = pagination.current
// 这里可以根据sorter和filters更新查询参数
fetchLogs()
}
// 查看日志详情
const viewLogDetail = (record) => {
selectedLog.value = record
logDetailVisible.value = true
}
// 下载备份
const downloadBackup = (record) => {
// 这里应该调用API下载备份
message.success(`开始下载备份 ${record.id}`)
}
// 确认恢复备份
const confirmRestoreBackup = (record) => {
selectedBackup.value = record
restoreConfirmVisible.value = true
}
// 恢复备份
const restoreBackup = () => {
// 这里应该调用API恢复备份
message.loading({ content: '正在恢复备份...', key: 'restoreLoading' })
// 模拟API调用
setTimeout(() => {
message.success({ content: '备份恢复成功系统将在5秒后刷新', key: 'restoreLoading' })
restoreConfirmVisible.value = false
// 模拟系统刷新
setTimeout(() => {
window.location.reload()
}, 5000)
}, 2000)
}
// 获取日志级别对应的颜色
const getLogLevelColor = (level) => {
const colors = {
info: 'blue',
warning: 'orange',
error: 'red',
debug: 'green'
}
return colors[level] || 'default'
}
</script>
<style scoped>
.settings-container {
padding: 24px;
background-color: #f0f2f5;
min-height: calc(100vh - 64px);
}
.settings-tabs {
background-color: #fff;
padding: 24px;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.settings-card {
margin-bottom: 24px;
}
.setting-description {
color: #8c8c8c;
margin-left: 8px;
font-size: 12px;
}
.log-search-form {
margin-bottom: 24px;
}
.log-table, .backup-table {
margin-top: 16px;
}
.log-detail-content {
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
font-family: monospace;
}
</style>

View File

@@ -0,0 +1,740 @@
<template>
<div class="transactions-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>交易管理</h1>
<p>查询和管理银行交易记录</p>
</div>
<!-- 搜索和筛选区域 -->
<a-card class="filter-card" :bordered="false">
<a-form layout="inline" :model="filterForm">
<a-form-item label="交易类型">
<a-select
v-model:value="filterForm.type"
style="width: 150px"
placeholder="选择交易类型"
allow-clear
>
<a-select-option value="deposit">存款</a-select-option>
<a-select-option value="withdrawal">取款</a-select-option>
<a-select-option value="transfer">转账</a-select-option>
<a-select-option value="payment">支付</a-select-option>
<a-select-option value="interest">利息</a-select-option>
<a-select-option value="fee">手续费</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="账户号码">
<a-input
v-model:value="filterForm.accountNumber"
placeholder="输入账户号码"
style="width: 200px"
allow-clear
/>
</a-form-item>
<a-form-item label="交易日期">
<a-range-picker
v-model:value="filterForm.dateRange"
:placeholder="['开始日期', '结束日期']"
style="width: 300px"
/>
</a-form-item>
<a-form-item label="金额范围">
<a-input-number
v-model:value="filterForm.minAmount"
placeholder="最小金额"
style="width: 120px"
:precision="2"
/>
<span style="margin: 0 8px;"></span>
<a-input-number
v-model:value="filterForm.maxAmount"
placeholder="最大金额"
style="width: 120px"
:precision="2"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<search-outlined /> 搜索
</a-button>
<a-button style="margin-left: 8px" @click="resetFilters">
<reload-outlined /> 重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 交易统计卡片 -->
<!-- <a-row :gutter="[16, 16]" class="stats-cards">
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="今日交易总数"
:value="stats.todayCount"
:loading="statsLoading"
>
<template #prefix>
<transaction-outlined style="color: #1890ff" />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="今日交易总额"
:value="stats.todayAmount"
:precision="2"
:loading="statsLoading"
:formatter="value => `¥${value}`"
>
<template #prefix>
<dollar-outlined style="color: #52c41a" />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="本月交易总数"
:value="stats.monthCount"
:loading="statsLoading"
>
<template #prefix>
<bar-chart-outlined style="color: #fa8c16" />
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :xs="24" :sm="12" :lg="6">
<a-card class="stat-card">
<a-statistic
title="本月交易总额"
:value="stats.monthAmount"
:precision="2"
:loading="statsLoading"
:formatter="value => `¥${value}`"
>
<template #prefix>
<fund-outlined style="color: #722ed1" />
</template>
</a-statistic>
</a-card>
</a-col>
</a-row> -->
<!-- 交易表格 -->
<a-card :bordered="false" class="table-card">
<template #title>
<div class="table-title">
<span>交易记录列表</span>
<a-button type="primary" @click="exportTransactions">
<download-outlined /> 导出数据
</a-button>
</div>
</template>
<a-table
:columns="columns"
:data-source="transactions"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<!-- 交易类型列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getTransactionTypeColor(record.type)">
{{ getTransactionTypeName(record.type) }}
</a-tag>
</template>
<!-- 金额列 -->
<template v-if="column.key === 'amount'">
<span :style="{ color: isNegativeTransaction(record.type) ? '#f5222d' : '#52c41a' }">
{{ isNegativeTransaction(record.type) ? '-' : '+' }}{{ formatCurrency(record.amount) }}
</span>
</template>
<!-- 状态列 -->
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusName(record.status) }}
</a-tag>
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a-space>
<a @click="viewTransactionDetail(record)">详情</a>
<a-divider type="vertical" />
<a @click="printReceipt(record)">打印凭证</a>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 交易详情对话框 -->
<a-modal
v-model:visible="detailModalVisible"
title="交易详情"
:footer="null"
width="700px"
>
<template v-if="selectedTransaction">
<a-descriptions bordered :column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }">
<a-descriptions-item label="交易ID">{{ selectedTransaction.id }}</a-descriptions-item>
<a-descriptions-item label="交易类型">
<a-tag :color="getTransactionTypeColor(selectedTransaction.type)">
{{ getTransactionTypeName(selectedTransaction.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="交易金额">
<span :style="{ color: isNegativeTransaction(selectedTransaction.type) ? '#f5222d' : '#52c41a' }">
{{ isNegativeTransaction(selectedTransaction.type) ? '-' : '+' }}{{ formatCurrency(selectedTransaction.amount) }}
</span>
</a-descriptions-item>
<a-descriptions-item label="交易状态">
<a-tag :color="getStatusColor(selectedTransaction.status)">
{{ getStatusName(selectedTransaction.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="交易时间">{{ selectedTransaction.timestamp }}</a-descriptions-item>
<a-descriptions-item label="账户号码">{{ selectedTransaction.accountNumber }}</a-descriptions-item>
<a-descriptions-item label="账户名称">{{ selectedTransaction.accountName }}</a-descriptions-item>
<a-descriptions-item label="交易渠道">{{ getChannelName(selectedTransaction.channel) }}</a-descriptions-item>
<a-descriptions-item label="交易描述" :span="2">{{ selectedTransaction.description || '无' }}</a-descriptions-item>
<template v-if="selectedTransaction.type === 'transfer'">
<a-descriptions-item label="收款账户">{{ selectedTransaction.targetAccountNumber }}</a-descriptions-item>
<a-descriptions-item label="收款人">{{ selectedTransaction.targetAccountName }}</a-descriptions-item>
</template>
<a-descriptions-item label="交易备注" :span="2">{{ selectedTransaction.notes || '无' }}</a-descriptions-item>
</a-descriptions>
<div style="margin-top: 24px;">
<h3>交易流水</h3>
<a-timeline>
<a-timeline-item color="green">
交易发起 - {{ selectedTransaction.timestamp }}
</a-timeline-item>
<a-timeline-item color="blue">
交易处理 - {{ selectedTransaction.processedTime || '立即处理' }}
</a-timeline-item>
<a-timeline-item :color="selectedTransaction.status === 'completed' ? 'green' : 'red'">
交易{{ selectedTransaction.status === 'completed' ? '完成' : '失败' }} - {{ selectedTransaction.completedTime || selectedTransaction.timestamp }}
</a-timeline-item>
</a-timeline>
</div>
<div style="margin-top: 16px; text-align: right;">
<a-button @click="printReceipt(selectedTransaction)" style="margin-right: 8px;">
<printer-outlined /> 打印凭证
</a-button>
<a-button @click="detailModalVisible = false">关闭</a-button>
</div>
</template>
</a-modal>
</div>
</template>
<script>
import { defineComponent, ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import {
SearchOutlined,
ReloadOutlined,
DownloadOutlined,
TransactionOutlined,
DollarOutlined,
BarChartOutlined,
FundOutlined,
PrinterOutlined
} from '@ant-design/icons-vue';
export default defineComponent({
name: 'TransactionsPage',
components: {
SearchOutlined,
ReloadOutlined,
DownloadOutlined,
TransactionOutlined,
DollarOutlined,
BarChartOutlined,
FundOutlined,
PrinterOutlined
},
setup() {
// 表格列定义
const columns = [
{
title: '交易ID',
dataIndex: 'id',
key: 'id',
sorter: true,
},
{
title: '交易类型',
dataIndex: 'type',
key: 'type',
filters: [
{ text: '存款', value: 'deposit' },
{ text: '取款', value: 'withdrawal' },
{ text: '转账', value: 'transfer' },
{ text: '支付', value: 'payment' },
{ text: '利息', value: 'interest' },
{ text: '手续费', value: 'fee' },
],
},
{
title: '账户号码',
dataIndex: 'accountNumber',
key: 'accountNumber',
sorter: true,
},
{
title: '账户名称',
dataIndex: 'accountName',
key: 'accountName',
sorter: true,
},
{
title: '金额',
dataIndex: 'amount',
key: 'amount',
sorter: true,
},
{
title: '交易时间',
dataIndex: 'timestamp',
key: 'timestamp',
sorter: true,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '已完成', value: 'completed' },
{ text: '处理中', value: 'processing' },
{ text: '失败', value: 'failed' },
],
},
{
title: '交易渠道',
dataIndex: 'channel',
key: 'channel',
filters: [
{ text: '柜台', value: 'counter' },
{ text: '网银', value: 'online' },
{ text: '手机银行', value: 'mobile' },
{ text: 'ATM', value: 'atm' },
],
},
{
title: '操作',
key: 'action',
},
];
// 状态变量
const transactions = ref([]);
const loading = ref(false);
const statsLoading = ref(false);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`,
});
// 筛选表单
const filterForm = reactive({
type: undefined,
accountNumber: '',
dateRange: [],
minAmount: null,
maxAmount: null,
});
// 统计数据
const stats = reactive({
todayCount: 0,
todayAmount: 0,
monthCount: 0,
monthAmount: 0,
});
// 交易详情相关
const detailModalVisible = ref(false);
const selectedTransaction = ref(null);
// 获取交易列表
const fetchTransactions = async (params = {}) => {
loading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getTransactions(params);
// transactions.value = response.data;
// pagination.total = response.total;
// 模拟数据
setTimeout(() => {
const mockTransactions = [
{
id: 'T20230917001',
type: 'deposit',
accountNumber: '6225123456789001',
accountName: '张三储蓄账户',
amount: 5000.00,
timestamp: '2023-09-17 09:30:25',
processedTime: '2023-09-17 09:30:26',
completedTime: '2023-09-17 09:30:28',
status: 'completed',
channel: 'counter',
description: '现金存款',
notes: ''
},
{
id: 'T20230917002',
type: 'withdrawal',
accountNumber: '6225123456789002',
accountName: '李四活期账户',
amount: 2000.00,
timestamp: '2023-09-17 10:15:42',
processedTime: '2023-09-17 10:15:43',
completedTime: '2023-09-17 10:15:45',
status: 'completed',
channel: 'atm',
description: 'ATM取款',
notes: ''
},
{
id: 'T20230917003',
type: 'transfer',
accountNumber: '6225123456789001',
accountName: '张三储蓄账户',
targetAccountNumber: '6225123456789002',
targetAccountName: '李四活期账户',
amount: 3000.00,
timestamp: '2023-09-17 11:05:18',
processedTime: '2023-09-17 11:05:20',
completedTime: '2023-09-17 11:05:22',
status: 'completed',
channel: 'online',
description: '转账给李四',
notes: '项目款'
},
{
id: 'T20230916001',
type: 'payment',
accountNumber: '6225123456789003',
accountName: '王五信用卡',
amount: 1500.00,
timestamp: '2023-09-16 14:30:10',
processedTime: '2023-09-16 14:30:12',
completedTime: '2023-09-16 14:30:15',
status: 'completed',
channel: 'mobile',
description: '电商购物',
notes: ''
},
{
id: 'T20230916002',
type: 'interest',
accountNumber: '6225123456789001',
accountName: '张三储蓄账户',
amount: 125.50,
timestamp: '2023-09-16 23:59:59',
processedTime: '2023-09-16 23:59:59',
completedTime: '2023-09-16 23:59:59',
status: 'completed',
channel: 'system',
description: '利息结算',
notes: '月度利息'
},
{
id: 'T20230915001',
type: 'fee',
accountNumber: '6225123456789003',
accountName: '王五信用卡',
amount: 50.00,
timestamp: '2023-09-15 00:00:01',
processedTime: '2023-09-15 00:00:01',
completedTime: '2023-09-15 00:00:01',
status: 'completed',
channel: 'system',
description: '年费',
notes: ''
},
{
id: 'T20230915002',
type: 'transfer',
accountNumber: '6225123456789004',
accountName: '赵六房贷',
targetAccountNumber: '6225123456789001',
targetAccountName: '张三储蓄账户',
amount: 5000.00,
timestamp: '2023-09-15 09:45:30',
processedTime: '2023-09-15 09:45:32',
status: 'failed',
channel: 'online',
description: '转账',
notes: '余额不足'
},
];
transactions.value = mockTransactions;
pagination.total = mockTransactions.length;
loading.value = false;
}, 500);
} catch (error) {
message.error('获取交易列表失败');
loading.value = false;
}
};
// 获取统计数据
const fetchStats = async () => {
statsLoading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getTransactionStats();
// Object.assign(stats, response.data);
// 模拟数据
setTimeout(() => {
stats.todayCount = 15;
stats.todayAmount = 25000.00;
stats.monthCount = 342;
stats.monthAmount = 1250000.00;
statsLoading.value = false;
}, 300);
} catch (error) {
message.error('获取统计数据失败');
statsLoading.value = false;
}
};
// 表格变化处理
const handleTableChange = (pag, filters, sorter) => {
const params = {
page: pag.current,
pageSize: pag.pageSize,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
};
pagination.current = pag.current;
fetchTransactions(params);
};
// 搜索处理
const handleSearch = () => {
pagination.current = 1;
const params = {
type: filterForm.type,
accountNumber: filterForm.accountNumber,
startDate: filterForm.dateRange?.[0]?.format('YYYY-MM-DD'),
endDate: filterForm.dateRange?.[1]?.format('YYYY-MM-DD'),
minAmount: filterForm.minAmount,
maxAmount: filterForm.maxAmount,
};
fetchTransactions(params);
};
// 重置筛选条件
const resetFilters = () => {
filterForm.type = undefined;
filterForm.accountNumber = '';
filterForm.dateRange = [];
filterForm.minAmount = null;
filterForm.maxAmount = null;
pagination.current = 1;
fetchTransactions();
};
// 查看交易详情
const viewTransactionDetail = (record) => {
selectedTransaction.value = record;
detailModalVisible.value = true;
};
// 打印交易凭证
const printReceipt = (record) => {
message.success(`正在打印交易 ${record.id} 的凭证`);
// 实际应用中这里应该调用打印API或生成PDF
};
// 导出交易数据
const exportTransactions = () => {
message.success('交易数据导出中,请稍候...');
// 实际应用中这里应该调用导出API
setTimeout(() => {
message.success('交易数据导出成功');
}, 1500);
};
// 格式化货币
const formatCurrency = (value) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 2
}).format(value).replace('CN¥', '¥');
};
// 判断是否为负向交易(支出)
const isNegativeTransaction = (type) => {
return ['withdrawal', 'payment', 'fee', 'transfer'].includes(type);
};
// 获取交易类型名称
const getTransactionTypeName = (type) => {
const typeMap = {
deposit: '存款',
withdrawal: '取款',
transfer: '转账',
payment: '支付',
interest: '利息',
fee: '手续费',
};
return typeMap[type] || type;
};
// 获取交易类型颜色
const getTransactionTypeColor = (type) => {
const colorMap = {
deposit: 'green',
withdrawal: 'red',
transfer: 'blue',
payment: 'orange',
interest: 'purple',
fee: 'cyan',
};
return colorMap[type] || 'default';
};
// 获取交易状态名称
const getStatusName = (status) => {
const statusMap = {
completed: '已完成',
processing: '处理中',
failed: '失败',
};
return statusMap[status] || status;
};
// 获取交易状态颜色
const getStatusColor = (status) => {
const colorMap = {
completed: 'green',
processing: 'blue',
failed: 'red',
};
return colorMap[status] || 'default';
};
// 获取交易渠道名称
const getChannelName = (channel) => {
const channelMap = {
counter: '柜台',
online: '网银',
mobile: '手机银行',
atm: 'ATM',
system: '系统',
};
return channelMap[channel] || channel;
};
// 生命周期钩子
onMounted(() => {
fetchTransactions();
fetchStats();
});
return {
columns,
transactions,
loading,
pagination,
filterForm,
stats,
statsLoading,
detailModalVisible,
selectedTransaction,
handleTableChange,
handleSearch,
resetFilters,
viewTransactionDetail,
printReceipt,
exportTransactions,
formatCurrency,
isNegativeTransaction,
getTransactionTypeName,
getTransactionTypeColor,
getStatusName,
getStatusColor,
getChannelName,
};
},
});
</script>
<style scoped>
.transactions-page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin-bottom: 8px;
font-size: 24px;
font-weight: 500;
}
.page-header p {
color: rgba(0, 0, 0, 0.45);
}
.filter-card {
margin-bottom: 24px;
}
.stats-cards {
margin-bottom: 24px;
}
.stat-card {
border-radius: 4px;
}
.table-card {
margin-bottom: 24px;
}
.table-title {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>

View File

@@ -0,0 +1,412 @@
<template>
<div class="users-page">
<!-- 页面头部 -->
<div class="page-header">
<h1>用户管理</h1>
<p>管理系统用户账号和权限</p>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<a-button type="primary" @click="showAddUserModal">
<plus-outlined /> 添加用户
</a-button>
<a-input-search
v-model:value="searchQuery"
placeholder="搜索用户..."
style="width: 250px; margin-left: 16px;"
@search="handleSearch"
/>
</div>
<!-- 用户表格 -->
<a-table
:columns="columns"
:data-source="users"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="id"
>
<!-- 状态列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status === 'active' ? '活跃' : '禁用' }}
</a-tag>
</template>
<!-- 角色列 -->
<template v-if="column.key === 'role'">
<a-tag :color="getRoleColor(record.role)">
{{ getRoleName(record.role) }}
</a-tag>
</template>
<!-- 操作列 -->
<template v-if="column.key === 'action'">
<a-space>
<a @click="editUser(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此用户吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteUser(record.id)"
>
<a class="danger-link">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 添加/编辑用户对话框 -->
<a-modal
v-model:visible="userModalVisible"
:title="isEditing ? '编辑用户' : '添加用户'"
@ok="handleUserFormSubmit"
:confirmLoading="submitting"
>
<a-form
:model="userForm"
:rules="rules"
ref="userFormRef"
layout="vertical"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="userForm.username" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="isEditing ? [] : [{ required: true, message: '请输入密码' }]"
>
<a-input-password v-model:value="userForm.password" :placeholder="isEditing ? '不修改请留空' : '请输入密码'" />
</a-form-item>
<a-form-item label="姓名" name="name">
<a-input v-model:value="userForm.name" />
</a-form-item>
<a-form-item label="角色" name="role">
<a-select v-model:value="userForm.role">
<a-select-option value="admin">管理员</a-select-option>
<a-select-option value="manager">经理</a-select-option>
<a-select-option value="teller">柜员</a-select-option>
<a-select-option value="user">普通用户</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="userForm.status">
<a-select-option value="active">活跃</a-select-option>
<a-select-option value="disabled">禁用</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
import { defineComponent, ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'UsersPage',
components: {
PlusOutlined
},
setup() {
// 表格列定义
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
sorter: true,
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
sorter: true,
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
sorter: true,
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
filters: [
{ text: '管理员', value: 'admin' },
{ text: '经理', value: 'manager' },
{ text: '柜员', value: 'teller' },
{ text: '普通用户', value: 'user' },
],
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '活跃', value: 'active' },
{ text: '禁用', value: 'disabled' },
],
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
sorter: true,
},
{
title: '操作',
key: 'action',
},
];
// 状态变量
const users = ref([]);
const loading = ref(false);
const searchQuery = ref('');
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`,
});
// 用户表单相关
const userFormRef = ref(null);
const userModalVisible = ref(false);
const isEditing = ref(false);
const submitting = ref(false);
const userForm = reactive({
id: null,
username: '',
password: '',
name: '',
role: 'user',
status: 'active',
});
// 表单验证规则
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间', trigger: 'blur' },
],
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' },
],
status: [
{ required: true, message: '请选择状态', trigger: 'change' },
],
};
// 获取用户列表
const fetchUsers = async (params = {}) => {
loading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getUsers(params);
// users.value = response.data;
// pagination.total = response.total;
// 模拟数据
setTimeout(() => {
const mockUsers = [
{ id: 1, username: 'admin', name: '系统管理员', role: 'admin', status: 'active', createdAt: '2023-01-01' },
{ id: 2, username: 'manager1', name: '张经理', role: 'manager', status: 'active', createdAt: '2023-01-02' },
{ id: 3, username: 'teller1', name: '李柜员', role: 'teller', status: 'active', createdAt: '2023-01-03' },
{ id: 4, username: 'user1', name: '王用户', role: 'user', status: 'disabled', createdAt: '2023-01-04' },
];
users.value = mockUsers;
pagination.total = mockUsers.length;
loading.value = false;
}, 500);
} catch (error) {
message.error('获取用户列表失败');
loading.value = false;
}
};
// 表格变化处理
const handleTableChange = (pag, filters, sorter) => {
const params = {
page: pag.current,
pageSize: pag.pageSize,
sortField: sorter.field,
sortOrder: sorter.order,
...filters,
};
pagination.current = pag.current;
fetchUsers(params);
};
// 搜索处理
const handleSearch = () => {
pagination.current = 1;
fetchUsers({ search: searchQuery.value });
};
// 显示添加用户对话框
const showAddUserModal = () => {
isEditing.value = false;
resetUserForm();
userModalVisible.value = true;
};
// 编辑用户
const editUser = (record) => {
isEditing.value = true;
Object.assign(userForm, { ...record, password: '' });
userModalVisible.value = true;
};
// 删除用户
const deleteUser = async (id) => {
try {
// 这里应该是实际的API调用
// await api.deleteUser(id);
message.success('用户删除成功');
fetchUsers({ page: pagination.current });
} catch (error) {
message.error('删除用户失败');
}
};
// 提交用户表单
const handleUserFormSubmit = () => {
userFormRef.value.validate().then(async () => {
submitting.value = true;
try {
if (isEditing.value) {
// 编辑用户
// await api.updateUser(userForm.id, userForm);
message.success('用户更新成功');
} else {
// 添加用户
// await api.createUser(userForm);
message.success('用户添加成功');
}
userModalVisible.value = false;
fetchUsers({ page: pagination.current });
} catch (error) {
message.error(isEditing.value ? '更新用户失败' : '添加用户失败');
} finally {
submitting.value = false;
}
});
};
// 重置用户表单
const resetUserForm = () => {
userForm.id = null;
userForm.username = '';
userForm.password = '';
userForm.name = '';
userForm.role = 'user';
userForm.status = 'active';
// 如果表单已经创建,则重置验证
if (userFormRef.value) {
userFormRef.value.resetFields();
}
};
// 获取角色名称
const getRoleName = (role) => {
const roleMap = {
admin: '管理员',
manager: '经理',
teller: '柜员',
user: '普通用户',
};
return roleMap[role] || role;
};
// 获取角色颜色
const getRoleColor = (role) => {
const colorMap = {
admin: 'red',
manager: 'blue',
teller: 'green',
user: 'default',
};
return colorMap[role] || 'default';
};
// 生命周期钩子
onMounted(() => {
fetchUsers();
});
return {
columns,
users,
loading,
searchQuery,
pagination,
userFormRef,
userModalVisible,
isEditing,
submitting,
userForm,
rules,
handleTableChange,
handleSearch,
showAddUserModal,
editUser,
deleteUser,
handleUserFormSubmit,
getRoleName,
getRoleColor,
};
},
});
</script>
<style scoped>
.users-page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin-bottom: 8px;
font-size: 24px;
font-weight: 500;
}
.page-header p {
color: rgba(0, 0, 0, 0.45);
}
.action-bar {
margin-bottom: 16px;
display: flex;
justify-content: flex-start;
align-items: center;
}
.danger-link {
color: #ff4d4f;
}
</style>