更新政府端和银行端
This commit is contained in:
294
bank-frontend/src/App.vue
Normal file
294
bank-frontend/src/App.vue
Normal 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>
|
||||
138
bank-frontend/src/components/DynamicMenu.vue
Normal file
138
bank-frontend/src/components/DynamicMenu.vue
Normal 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>
|
||||
279
bank-frontend/src/components/MobileNav.vue
Normal file
279
bank-frontend/src/components/MobileNav.vue
Normal 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>
|
||||
97
bank-frontend/src/config/env.js
Normal file
97
bank-frontend/src/config/env.js
Normal 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
44
bank-frontend/src/main.js
Normal 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')
|
||||
75
bank-frontend/src/router/index.js
Normal file
75
bank-frontend/src/router/index.js
Normal 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
|
||||
149
bank-frontend/src/router/routes.js
Normal file
149
bank-frontend/src/router/routes.js
Normal 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
|
||||
7
bank-frontend/src/stores/index.js
Normal file
7
bank-frontend/src/stores/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 状态管理索引
|
||||
* @file index.js
|
||||
* @description 导出所有状态管理模块
|
||||
*/
|
||||
export { useUserStore } from './user'
|
||||
export { useSettingsStore } from './settings'
|
||||
269
bank-frontend/src/stores/settings.js
Normal file
269
bank-frontend/src/stores/settings.js
Normal 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
|
||||
}
|
||||
})
|
||||
230
bank-frontend/src/stores/user.js
Normal file
230
bank-frontend/src/stores/user.js
Normal 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
|
||||
}
|
||||
})
|
||||
459
bank-frontend/src/styles/global.css
Normal file
459
bank-frontend/src/styles/global.css
Normal 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%;
|
||||
}
|
||||
520
bank-frontend/src/styles/responsive.css
Normal file
520
bank-frontend/src/styles/responsive.css
Normal 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;
|
||||
}
|
||||
}
|
||||
172
bank-frontend/src/styles/theme.js
Normal file
172
bank-frontend/src/styles/theme.js
Normal 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
|
||||
382
bank-frontend/src/utils/api.js
Normal file
382
bank-frontend/src/utils/api.js
Normal 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
|
||||
785
bank-frontend/src/views/Accounts.vue
Normal file
785
bank-frontend/src/views/Accounts.vue
Normal 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>
|
||||
513
bank-frontend/src/views/Dashboard.vue
Normal file
513
bank-frontend/src/views/Dashboard.vue
Normal 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>
|
||||
267
bank-frontend/src/views/Login.vue
Normal file
267
bank-frontend/src/views/Login.vue
Normal 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>
|
||||
1
bank-frontend/src/views/NotFound.vue
Normal file
1
bank-frontend/src/views/NotFound.vue
Normal 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>
|
||||
1
bank-frontend/src/views/Profile.vue
Normal file
1
bank-frontend/src/views/Profile.vue
Normal 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>
|
||||
717
bank-frontend/src/views/Reports.vue
Normal file
717
bank-frontend/src/views/Reports.vue
Normal 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>
|
||||
615
bank-frontend/src/views/Settings.vue
Normal file
615
bank-frontend/src/views/Settings.vue
Normal 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>
|
||||
740
bank-frontend/src/views/Transactions.vue
Normal file
740
bank-frontend/src/views/Transactions.vue
Normal 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>
|
||||
412
bank-frontend/src/views/Users.vue
Normal file
412
bank-frontend/src/views/Users.vue
Normal 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>
|
||||
Reference in New Issue
Block a user