修改保险后端代码,政府前端代码

This commit is contained in:
2025-09-22 17:56:30 +08:00
parent 3143c3ad0b
commit 02a25515a9
206 changed files with 35119 additions and 43073 deletions

View File

@@ -9,7 +9,7 @@
</div>
<!-- 桌面端布局 -->
<a-layout v-else style="min-height: 100vh">
<a-layout v-else class="desktop-layout">
<a-layout-header class="header">
<div class="logo">
<a-button
@@ -50,24 +50,24 @@
</div>
</a-layout-header>
<a-layout>
<a-layout class="main-layout">
<a-layout-sider
class="sidebar"
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 class="content-layout">
<a-layout-content class="main-content">
<div class="content-wrapper">
<router-view />
</div>
</a-layout-content>
<a-layout-footer style="text-align: center">
<a-layout-footer class="footer">
银行管理后台系统 ©2025
</a-layout-footer>
</a-layout>
@@ -156,14 +156,32 @@ onUnmounted(() => {
</script>
<style scoped>
/* 桌面端样式 */
/* 桌面端布局样式 */
.desktop-layout {
height: 100vh;
overflow: hidden;
}
.main-layout {
height: calc(100vh - 64px);
overflow: hidden;
}
/* 头部样式 */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: space-between;
background: #001529;
color: white;
padding: 0 24px;
height: 64px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.logo {
@@ -179,6 +197,114 @@ onUnmounted(() => {
gap: 16px;
}
/* 侧边栏样式 */
.sidebar {
position: fixed;
top: 64px;
left: 0;
height: calc(100vh - 64px);
background: #001529 !important;
z-index: 999;
overflow-y: auto;
overflow-x: hidden;
}
.sidebar::-webkit-scrollbar {
width: 6px;
}
.sidebar::-webkit-scrollbar-track {
background: #001529;
}
.sidebar::-webkit-scrollbar-thumb {
background: #1890ff;
border-radius: 3px;
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: #40a9ff;
}
/* 内容区域样式 */
.content-layout {
margin-left: 200px;
height: calc(100vh - 64px);
transition: margin-left 0.2s;
}
.main-content {
height: calc(100vh - 64px - 70px);
overflow-y: auto;
overflow-x: hidden;
background: #f0f2f5;
}
.content-wrapper {
padding: 24px;
min-height: 100%;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.footer {
height: 70px;
line-height: 70px;
text-align: center;
background: #fff;
border-top: 1px solid #f0f0f0;
color: #666;
}
/* 侧边栏折叠时的样式 */
.desktop-layout :deep(.ant-layout-sider-collapsed) {
width: 80px !important;
min-width: 80px !important;
max-width: 80px !important;
flex: 0 0 80px !important;
}
.desktop-layout :deep(.ant-layout-sider-collapsed) + .content-layout {
margin-left: 80px;
transition: margin-left 0.2s;
}
/* 响应式支持 */
@media (max-width: 768px) {
.content-layout {
margin-left: 0 !important;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s;
}
.sidebar.ant-layout-sider-collapsed {
transform: translateX(0);
}
}
/* 内容区域滚动条样式 */
.main-content::-webkit-scrollbar {
width: 8px;
}
.main-content::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 4px;
}
.main-content::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 4px;
}
.main-content::-webkit-scrollbar-thumb:hover {
background: #bfbfbf;
}
/* 移动端布局样式 */
.mobile-layout {
min-height: 100vh;

View File

@@ -56,15 +56,24 @@ const openKeys = ref([])
// 菜单项
const menuItems = computed(() => {
const userRole = userStore.getUserRoleName()
return getMenuItems(routes, userRole)
try {
const userRole = userStore.getUserRoleName() || 'user'
return getMenuItems(routes, userRole)
} catch (error) {
console.error('获取菜单项失败:', error)
return []
}
})
// 处理菜单点击
const handleMenuClick = ({ key }) => {
const menuItem = findMenuItem(menuItems.value, key)
if (menuItem && menuItem.path) {
router.push(menuItem.path)
try {
const menuItem = findMenuItem(menuItems.value, key)
if (menuItem && menuItem.path) {
router.push(menuItem.path)
}
} catch (error) {
console.error('菜单点击处理失败:', error)
}
}
@@ -75,28 +84,43 @@ const handleOpenChange = (keys) => {
// 查找菜单项
const findMenuItem = (items, key) => {
for (const item of items) {
if (item.key === key) {
return item
try {
if (!Array.isArray(items)) {
return null
}
if (item.children) {
const found = findMenuItem(item.children, key)
if (found) return found
for (const item of items) {
if (item && item.key === key) {
return item
}
if (item && item.children) {
const found = findMenuItem(item.children, key)
if (found) return found
}
}
return null
} catch (error) {
console.error('查找菜单项失败:', error)
return null
}
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]
try {
if (!newPath) return
const pathSegments = newPath.split('/').filter(Boolean)
if (pathSegments.length > 1) {
const parentKey = pathSegments[0]
if (parentKey && !openKeys.value.includes(parentKey)) {
openKeys.value = [parentKey]
}
}
} catch (error) {
console.error('路由监听处理失败:', error)
}
},
{ immediate: true }
@@ -106,12 +130,34 @@ watch(
<style scoped>
.ant-menu {
border-right: none;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.ant-menu::-webkit-scrollbar {
width: 6px;
}
.ant-menu::-webkit-scrollbar-track {
background: #001529;
}
.ant-menu::-webkit-scrollbar-thumb {
background: #1890ff;
border-radius: 3px;
}
.ant-menu::-webkit-scrollbar-thumb:hover {
background: #40a9ff;
}
.ant-menu-item,
.ant-menu-submenu-title {
display: flex;
align-items: center;
height: 48px;
line-height: 48px;
}
.ant-menu-item .anticon,
@@ -135,4 +181,20 @@ watch(
.ant-menu-submenu-open > .ant-menu-submenu-title {
color: #1890ff;
}
/* 折叠状态下的样式优化 */
.ant-menu-inline-collapsed .ant-menu-item {
padding: 0 24px;
text-align: center;
}
.ant-menu-inline-collapsed .ant-menu-submenu-title {
padding: 0 24px;
text-align: center;
}
.ant-menu-inline-collapsed .ant-menu-item .anticon,
.ant-menu-inline-collapsed .ant-menu-submenu-title .anticon {
margin-right: 0;
}
</style>

View File

@@ -10,7 +10,14 @@ import {
TransactionOutlined,
BarChartOutlined,
SettingOutlined,
LoginOutlined
LoginOutlined,
FileTextOutlined,
SafetyOutlined,
LineChartOutlined,
CreditCardOutlined,
DesktopOutlined,
TeamOutlined,
UserSwitchOutlined
} from '@ant-design/icons-vue'
// 路由配置
@@ -40,6 +47,39 @@ const routes = [
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/project-list',
name: 'ProjectList',
component: () => import('@/views/ProjectList.vue'),
meta: {
title: '项目清单',
icon: FileTextOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: '/system-check',
name: 'SystemCheck',
component: () => import('@/views/SystemCheck.vue'),
meta: {
title: '系统日检',
icon: SafetyOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/market-trends',
name: 'MarketTrends',
component: () => import('@/views/MarketTrends.vue'),
meta: {
title: '市场行情',
icon: LineChartOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/users',
name: 'Users',
@@ -73,6 +113,81 @@ const routes = [
roles: ['admin', 'manager', 'teller', 'user']
}
},
{
path: '/loan-management',
name: 'LoanManagement',
component: () => import('@/views/LoanManagement.vue'),
meta: {
title: '贷款管理',
icon: CreditCardOutlined,
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
},
children: [
{
path: 'products',
name: 'LoanProducts',
component: () => import('@/views/loan/LoanProducts.vue'),
meta: {
title: '贷款商品',
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: 'applications',
name: 'LoanApplications',
component: () => import('@/views/loan/LoanApplications.vue'),
meta: {
title: '贷款申请进度',
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: 'contracts',
name: 'LoanContracts',
component: () => import('@/views/loan/LoanContracts.vue'),
meta: {
title: '贷款合同',
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
},
{
path: 'release',
name: 'LoanRelease',
component: () => import('@/views/loan/LoanRelease.vue'),
meta: {
title: '贷款解押',
requiresAuth: true,
roles: ['admin', 'manager', 'teller']
}
}
]
},
{
path: '/hardware-management',
name: 'HardwareManagement',
component: () => import('@/views/HardwareManagement.vue'),
meta: {
title: '硬件管理',
icon: DesktopOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/employee-management',
name: 'EmployeeManagement',
component: () => import('@/views/EmployeeManagement.vue'),
meta: {
title: '员工管理',
icon: TeamOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
}
},
{
path: '/reports',
name: 'Reports',
@@ -81,7 +196,7 @@ const routes = [
title: '报表统计',
icon: BarChartOutlined,
requiresAuth: true,
roles: ['admin', 'manager']
roles: ['admin', 'manager', 'teller']
}
},
{
@@ -101,8 +216,8 @@ const routes = [
component: () => import('@/views/Profile.vue'),
meta: {
title: '个人中心',
icon: UserSwitchOutlined,
requiresAuth: true,
hideInMenu: true,
roles: ['admin', 'manager', 'teller', 'user']
}
},
@@ -132,18 +247,21 @@ export function filterRoutesByRole(routes, userRole) {
}
// 获取菜单项
export function getMenuItems(routes, userRole) {
export function getMenuItems(routes, userRole, parentPath = '') {
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
}))
.map(route => {
const fullPath = parentPath ? `${parentPath}/${route.path}` : route.path
return {
key: route.name,
title: route.meta?.title || route.name,
icon: route.meta?.icon,
path: fullPath,
children: route.children ? getMenuItems(route.children, userRole, fullPath) : undefined
}
})
}
export default routes

View File

@@ -41,7 +41,7 @@ export const useUserStore = defineStore('user', () => {
try {
const { api } = await import('@/utils/api')
// 尝试调用一个需要认证的API来验证token
await api.get('/users/profile')
await api.auth.getCurrentUser()
return true
} catch (error) {
if (error.message && error.message.includes('认证已过期')) {
@@ -57,7 +57,7 @@ export const useUserStore = defineStore('user', () => {
async function login(username, password, retryCount = 0) {
try {
const { api } = await import('@/utils/api')
const result = await api.login(username, password)
const result = await api.auth.login(username, password)
// 登录成功后设置token和用户数据
if (result.success && result.data.token) {
@@ -66,10 +66,10 @@ export const useUserStore = defineStore('user', () => {
id: result.data.user.id,
username: result.data.user.username,
email: result.data.user.email,
real_name: result.data.user.real_name,
real_name: result.data.user.name,
phone: result.data.user.phone,
avatar: result.data.user.avatar,
role: result.data.user.role,
role: { name: result.data.user.role },
status: result.data.user.status
}
@@ -95,7 +95,7 @@ export const useUserStore = defineStore('user', () => {
try {
// 调用后端登出接口
const { api } = await import('@/utils/api')
await api.post('/users/logout')
await api.auth.logout()
} catch (error) {
console.error('登出请求失败:', error)
} finally {

View File

@@ -101,7 +101,7 @@ export const api = {
* @returns {Promise} 登录结果
*/
async login(username, password) {
const response = await fetch(`${API_CONFIG.baseUrl}/api/users/login`, {
const response = await fetch(`${API_CONFIG.baseUrl}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -202,6 +202,81 @@ export const api = {
return handleResponse(response)
},
// 认证相关API
auth: {
/**
* 用户登录
* @param {string} username - 用户名
* @param {string} password - 密码
* @returns {Promise} 登录结果
*/
async login(username, password) {
return api.post('/auth/login', { username, password })
},
/**
* 用户登出
* @returns {Promise} 登出结果
*/
async logout() {
return api.post('/auth/logout')
},
/**
* 刷新令牌
* @returns {Promise} 刷新结果
*/
async refreshToken() {
return api.post('/auth/refresh')
},
/**
* 获取当前用户信息
* @returns {Promise} 用户信息
*/
async getCurrentUser() {
return api.get('/auth/me')
},
/**
* 修改密码
* @param {Object} data - 密码数据
* @returns {Promise} 修改结果
*/
async changePassword(data) {
return api.post('/auth/change-password', data)
}
},
// 仪表盘API
dashboard: {
/**
* 获取仪表盘统计数据
* @returns {Promise} 统计数据
*/
async getStats() {
return api.get('/dashboard')
},
/**
* 获取图表数据
* @param {Object} params - 查询参数
* @returns {Promise} 图表数据
*/
async getChartData(params = {}) {
return api.get('/dashboard/charts', { params })
},
/**
* 获取最近交易记录
* @param {Object} params - 查询参数
* @returns {Promise} 交易记录
*/
async getRecentTransactions(params = {}) {
return api.get('/dashboard/recent-transactions', { params })
}
},
// 用户管理API
users: {
/**
@@ -228,7 +303,7 @@ export const api = {
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/users/register', data)
return api.post('/users', data)
},
/**
@@ -260,6 +335,16 @@ export const api = {
return api.put(`/users/${id}/status`, data)
},
/**
* 重置用户密码
* @param {number} id - 用户ID
* @param {Object} data - 密码数据
* @returns {Promise} 重置结果
*/
async resetPassword(id, data) {
return api.post(`/users/${id}/reset-password`, data)
},
/**
* 获取用户账户列表
* @param {number} userId - 用户ID
@@ -267,6 +352,32 @@ export const api = {
*/
async getAccounts(userId) {
return api.get(`/users/${userId}/accounts`)
},
/**
* 获取当前用户信息
* @returns {Promise} 用户信息
*/
async getProfile() {
return api.get('/users/profile')
},
/**
* 更新当前用户信息
* @param {Object} data - 更新数据
* @returns {Promise} 更新结果
*/
async updateProfile(data) {
return api.put('/users/profile', data)
},
/**
* 修改当前用户密码
* @param {Object} data - 密码数据
* @returns {Promise} 修改结果
*/
async changePassword(data) {
return api.put('/users/change-password', data)
}
},
@@ -330,6 +441,173 @@ export const api = {
}
},
// 贷款产品API
loanProducts: {
/**
* 获取贷款产品列表
* @param {Object} params - 查询参数
* @returns {Promise} 产品列表
*/
async getList(params = {}) {
return api.get('/loan-products', { params })
},
/**
* 获取产品详情
* @param {number} id - 产品ID
* @returns {Promise} 产品详情
*/
async getById(id) {
return api.get(`/loan-products/${id}`)
},
/**
* 创建产品
* @param {Object} data - 产品数据
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/loan-products', data)
},
/**
* 更新产品
* @param {number} id - 产品ID
* @param {Object} data - 产品数据
* @returns {Promise} 更新结果
*/
async update(id, data) {
return api.put(`/loan-products/${id}`, data)
},
/**
* 删除产品
* @param {number} id - 产品ID
* @returns {Promise} 删除结果
*/
async delete(id) {
return api.delete(`/loan-products/${id}`)
},
/**
* 更新产品状态
* @param {number} id - 产品ID
* @param {Object} data - 状态数据
* @returns {Promise} 更新结果
*/
async updateStatus(id, data) {
return api.put(`/loan-products/${id}/status`, data)
},
/**
* 获取产品统计
* @returns {Promise} 统计数据
*/
async getStats() {
return api.get('/loan-products/stats/overview')
}
},
// 员工管理API
employees: {
/**
* 获取员工列表
* @param {Object} params - 查询参数
* @returns {Promise} 员工列表
*/
async getList(params = {}) {
return api.get('/employees', { params })
},
/**
* 获取员工详情
* @param {number} id - 员工ID
* @returns {Promise} 员工详情
*/
async getById(id) {
return api.get(`/employees/${id}`)
},
/**
* 创建员工
* @param {Object} data - 员工数据
* @returns {Promise} 创建结果
*/
async create(data) {
return api.post('/employees', data)
},
/**
* 更新员工
* @param {number} id - 员工ID
* @param {Object} data - 员工数据
* @returns {Promise} 更新结果
*/
async update(id, data) {
return api.put(`/employees/${id}`, data)
},
/**
* 删除员工
* @param {number} id - 员工ID
* @returns {Promise} 删除结果
*/
async delete(id) {
return api.delete(`/employees/${id}`)
},
/**
* 获取员工统计
* @returns {Promise} 统计数据
*/
async getStats() {
return api.get('/employees/stats/overview')
}
},
// 报表统计API
reports: {
/**
* 获取交易报表
* @param {Object} params - 查询参数
* @returns {Promise} 交易报表
*/
async getTransactions(params = {}) {
return api.get('/reports/transactions', { params })
},
/**
* 获取用户报表
* @param {Object} params - 查询参数
* @returns {Promise} 用户报表
*/
async getUsers(params = {}) {
return api.get('/reports/users', { params })
},
/**
* 获取账户报表
* @param {Object} params - 查询参数
* @returns {Promise} 账户报表
*/
async getAccounts(params = {}) {
return api.get('/reports/accounts', { params })
},
/**
* 导出报表
* @param {string} type - 报表类型
* @param {string} format - 导出格式
* @param {Object} params - 查询参数
* @returns {Promise} 导出结果
*/
async export(type, format = 'excel', params = {}) {
return api.get(`/reports/export/${type}`, {
params: { format, ...params }
})
}
},
// 交易管理API
transactions: {
/**

View File

@@ -205,6 +205,7 @@
import { defineComponent, ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { api } from '@/utils/api';
export default defineComponent({
name: 'AccountsPage',
@@ -358,13 +359,21 @@ export default defineComponent({
const fetchAccounts = async (params = {}) => {
loading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getAccounts(params);
// accounts.value = response.data;
// pagination.total = response.total;
const response = await api.accounts.getList({
page: pagination.current,
pageSize: pagination.pageSize,
search: searchText.value,
type: typeFilter.value,
status: statusFilter.value,
...params
});
// 模拟数据
setTimeout(() => {
if (response.success) {
accounts.value = response.data.accounts || [];
pagination.total = response.data.pagination?.total || 0;
} else {
message.error(response.message || '获取账户列表失败');
// 使用模拟数据作为备用
const mockAccounts = [
{
id: 1,
@@ -421,10 +430,29 @@ export default defineComponent({
];
accounts.value = mockAccounts;
pagination.total = mockAccounts.length;
loading.value = false;
}, 500);
}
} catch (error) {
console.error('获取账户列表失败:', error);
message.error('获取账户列表失败');
// 使用模拟数据作为备用
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: '主要储蓄账户'
}
];
accounts.value = mockAccounts;
pagination.total = mockAccounts.length;
} finally {
loading.value = false;
}
};
@@ -433,22 +461,29 @@ export default defineComponent({
const fetchUsers = async () => {
usersLoading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getUsers();
// usersList.value = response.data;
// 模拟数据
setTimeout(() => {
const response = await api.users.getList({ page: 1, pageSize: 100 });
if (response.success) {
usersList.value = response.data.users || [];
} else {
// 使用模拟数据作为备用
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) {
console.error('获取用户列表失败:', error);
message.error('获取用户列表失败');
// 使用模拟数据作为备用
usersList.value = [
{ id: 1, username: 'zhangsan', name: '张三' },
{ id: 2, username: 'lisi', name: '李四' },
{ id: 3, username: 'wangwu', name: '王五' },
{ id: 4, username: 'zhaoliu', name: '赵六' },
];
} finally {
usersLoading.value = false;
}
};

View File

@@ -210,27 +210,38 @@ const fetchStats = async () => {
try {
loading.value = true
// 模拟数据实际应该调用API
// 获取仪表盘统计数据
const statsResult = await api.dashboard.getStats()
if (statsResult.success) {
const data = statsResult.data
stats.value = {
totalUsers: data.overview?.totalUsers || 0,
totalAccounts: data.overview?.totalAccounts || 0,
todayTransactions: data.today?.transactionCount || 0,
totalAssets: data.overview?.totalBalance || 0
}
}
// 获取最近交易
const transactionResult = await api.dashboard.getRecentTransactions({
limit: 10
})
if (transactionResult.success) {
recentTransactions.value = transactionResult.data || []
}
} catch (error) {
console.error('获取统计数据失败:', error)
message.error('获取统计数据失败')
// 如果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
}

View File

@@ -0,0 +1,759 @@
<template>
<div class="employee-management">
<div class="page-header">
<h1>员工管理</h1>
<p>管理和维护银行员工信息</p>
</div>
<div class="content">
<!-- 员工概览 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="员工总数"
:value="employeeStats.total"
:value-style="{ color: '#1890ff' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="在职员工"
:value="employeeStats.active"
:value-style="{ color: '#52c41a' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="离职员工"
:value="employeeStats.inactive"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="本月入职"
:value="employeeStats.newHires"
:value-style="{ color: '#faad14' }"
/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索员工姓名或工号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="员工状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="active">在职</a-select-option>
<a-select-option value="inactive">离职</a-select-option>
<a-select-option value="suspended">停职</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="departmentFilter"
placeholder="部门"
allow-clear
@change="handleFilter"
>
<a-select-option value="admin">行政部</a-select-option>
<a-select-option value="finance">财务部</a-select-option>
<a-select-option value="it">技术部</a-select-option>
<a-select-option value="hr">人事部</a-select-option>
<a-select-option value="sales">销售部</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="positionFilter"
placeholder="职位"
allow-clear
@change="handleFilter"
>
<a-select-option value="manager">经理</a-select-option>
<a-select-option value="supervisor">主管</a-select-option>
<a-select-option value="staff">员工</a-select-option>
<a-select-option value="intern">实习生</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-space>
<a-button type="primary" @click="handleAddEmployee">
<PlusOutlined />
添加员工
</a-button>
<a-button @click="handleExport">
<DownloadOutlined />
导出
</a-button>
</a-space>
</a-col>
</a-row>
</div>
<!-- 员工列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredEmployees"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'department'">
<a-tag :color="getDepartmentColor(record.department)">
{{ getDepartmentText(record.department) }}
</a-tag>
</template>
<template v-else-if="column.key === 'position'">
{{ getPositionText(record.position) }}
</template>
<template v-else-if="column.key === 'avatar'">
<a-avatar :src="record.avatar" :size="32">
{{ record.name.charAt(0) }}
</a-avatar>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button
type="link"
size="small"
@click="handleToggleStatus(record)"
:danger="record.status === 'active'"
>
{{ record.status === 'active' ? '停职' : '复职' }}
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 员工详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="员工详情"
width="800px"
:footer="null"
>
<div v-if="selectedEmployee" class="employee-detail">
<div class="employee-header">
<a-avatar :src="selectedEmployee.avatar" :size="64">
{{ selectedEmployee.name.charAt(0) }}
</a-avatar>
<div class="employee-info">
<h3>{{ selectedEmployee.name }}</h3>
<p>{{ getPositionText(selectedEmployee.position) }} - {{ getDepartmentText(selectedEmployee.department) }}</p>
<a-tag :color="getStatusColor(selectedEmployee.status)">
{{ getStatusText(selectedEmployee.status) }}
</a-tag>
</div>
</div>
<a-descriptions :column="2" bordered style="margin-top: 24px">
<a-descriptions-item label="工号">
{{ selectedEmployee.employeeId }}
</a-descriptions-item>
<a-descriptions-item label="姓名">
{{ selectedEmployee.name }}
</a-descriptions-item>
<a-descriptions-item label="性别">
{{ selectedEmployee.gender }}
</a-descriptions-item>
<a-descriptions-item label="年龄">
{{ selectedEmployee.age }}
</a-descriptions-item>
<a-descriptions-item label="手机号">
{{ selectedEmployee.phone }}
</a-descriptions-item>
<a-descriptions-item label="邮箱">
{{ selectedEmployee.email }}
</a-descriptions-item>
<a-descriptions-item label="身份证号">
{{ selectedEmployee.idCard }}
</a-descriptions-item>
<a-descriptions-item label="入职日期">
{{ selectedEmployee.hireDate }}
</a-descriptions-item>
<a-descriptions-item label="部门">
<a-tag :color="getDepartmentColor(selectedEmployee.department)">
{{ getDepartmentText(selectedEmployee.department) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="职位">
{{ getPositionText(selectedEmployee.position) }}
</a-descriptions-item>
<a-descriptions-item label="直属上级">
{{ selectedEmployee.supervisor }}
</a-descriptions-item>
<a-descriptions-item label="薪资等级">
{{ selectedEmployee.salaryLevel }}
</a-descriptions-item>
<a-descriptions-item label="工作地点">
{{ selectedEmployee.workLocation }}
</a-descriptions-item>
<a-descriptions-item label="紧急联系人">
{{ selectedEmployee.emergencyContact }}
</a-descriptions-item>
<a-descriptions-item label="紧急联系电话">
{{ selectedEmployee.emergencyPhone }}
</a-descriptions-item>
<a-descriptions-item label="个人简介" :span="2">
{{ selectedEmployee.bio || '暂无' }}
</a-descriptions-item>
</a-descriptions>
<!-- 工作经历 -->
<div class="work-experience" v-if="selectedEmployee && selectedEmployee.experience">
<h4>工作经历</h4>
<a-timeline>
<a-timeline-item
v-for="exp in selectedEmployee.experience"
:key="exp.id"
>
<div class="experience-item">
<div class="experience-header">
<span class="experience-title">{{ exp.title }}</span>
<span class="experience-period">{{ exp.startDate }} - {{ exp.endDate || '至今' }}</span>
</div>
<div class="experience-company">{{ exp.company }}</div>
<div class="experience-description">{{ exp.description }}</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, DownloadOutlined } from '@ant-design/icons-vue'
import { api } from '@/utils/api'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const departmentFilter = ref(undefined)
const positionFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedEmployee = ref(null)
// 员工统计
const employeeStats = ref({
total: 156,
active: 142,
inactive: 14,
newHires: 8
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '头像',
dataIndex: 'avatar',
key: 'avatar',
width: 80
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 100
},
{
title: '工号',
dataIndex: 'employeeId',
key: 'employeeId',
width: 120
},
{
title: '部门',
dataIndex: 'department',
key: 'department',
width: 100
},
{
title: '职位',
dataIndex: 'position',
key: 'position',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120
},
{
title: '入职日期',
dataIndex: 'hireDate',
key: 'hireDate',
width: 120
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 员工数据
const employees = ref([])
// 模拟员工数据(作为备用)
const mockEmployees = [
{
id: 1,
name: '张三',
employeeId: 'EMP001',
gender: '男',
age: 28,
phone: '13800138000',
email: 'zhangsan@bank.com',
idCard: '110101199001011234',
hireDate: '2020-03-15',
department: 'admin',
position: 'manager',
status: 'active',
supervisor: '李总',
salaryLevel: 'L5',
workLocation: '总行',
emergencyContact: '张四',
emergencyPhone: '13900139000',
bio: '具有5年银行管理经验擅长团队管理和业务规划',
avatar: null,
experience: [
{
id: 1,
title: '行政经理',
company: '某银行',
startDate: '2020-03-15',
endDate: null,
description: '负责行政部日常管理工作'
},
{
id: 2,
title: '行政专员',
company: '某银行',
startDate: '2018-06-01',
endDate: '2020-03-14',
description: '负责行政事务处理'
}
]
},
{
id: 2,
name: '李四',
employeeId: 'EMP002',
gender: '女',
age: 25,
phone: '13900139000',
email: 'lisi@bank.com',
idCard: '110101199002021234',
hireDate: '2021-07-01',
department: 'finance',
position: 'staff',
status: 'active',
supervisor: '王经理',
salaryLevel: 'L3',
workLocation: '分行',
emergencyContact: '李五',
emergencyPhone: '13700137000',
bio: '财务专业毕业具有3年财务工作经验',
avatar: null,
experience: [
{
id: 1,
title: '财务专员',
company: '某银行',
startDate: '2021-07-01',
endDate: null,
description: '负责财务核算和报表编制'
}
]
},
{
id: 3,
name: '王五',
employeeId: 'EMP003',
gender: '男',
age: 32,
phone: '13700137000',
email: 'wangwu@bank.com',
idCard: '110101199003031234',
hireDate: '2019-01-10',
department: 'it',
position: 'supervisor',
status: 'inactive',
supervisor: '赵总',
salaryLevel: 'L4',
workLocation: '总行',
emergencyContact: '王六',
emergencyPhone: '13600136000',
bio: '计算机专业具有8年IT工作经验擅长系统开发',
avatar: null,
experience: [
{
id: 1,
title: '技术主管',
company: '某银行',
startDate: '2019-01-10',
endDate: '2024-01-15',
description: '负责技术团队管理和系统开发'
}
]
}
]
// 计算属性
const filteredEmployees = computed(() => {
let result = employees.value
if (searchText.value) {
result = result.filter(employee =>
employee.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
employee.employeeId.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(employee => employee.status === statusFilter.value)
}
if (departmentFilter.value) {
result = result.filter(employee => employee.department === departmentFilter.value)
}
if (positionFilter.value) {
result = result.filter(employee => employee.position === positionFilter.value)
}
return result
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleAddEmployee = () => {
message.info('添加员工功能开发中...')
}
const handleExport = () => {
message.info('导出功能开发中...')
}
const handleView = (record) => {
selectedEmployee.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑员工: ${record.name}`)
}
const handleToggleStatus = (record) => {
const newStatus = record.status === 'active' ? 'inactive' : 'active'
record.status = newStatus
message.success(`员工已${newStatus === 'active' ? '复职' : '停职'}`)
}
const getStatusColor = (status) => {
const colors = {
active: 'green',
inactive: 'red',
suspended: 'orange'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
active: '在职',
inactive: '离职',
suspended: '停职'
}
return texts[status] || status
}
const getDepartmentColor = (department) => {
const colors = {
admin: 'blue',
finance: 'green',
it: 'purple',
hr: 'orange',
sales: 'cyan'
}
return colors[department] || 'default'
}
const getDepartmentText = (department) => {
const texts = {
admin: '行政部',
finance: '财务部',
it: '技术部',
hr: '人事部',
sales: '销售部'
}
return texts[department] || department
}
const getPositionText = (position) => {
const texts = {
manager: '经理',
supervisor: '主管',
staff: '员工',
intern: '实习生'
}
return texts[position] || position
}
// API调用函数
const fetchEmployees = async (params = {}) => {
try {
loading.value = true
const response = await api.employees.getList({
page: pagination.value.current,
limit: pagination.value.pageSize,
search: searchText.value,
status: statusFilter.value,
department: departmentFilter.value,
position: positionFilter.value,
...params
})
if (response.success) {
employees.value = response.data.employees || []
pagination.value.total = response.data.pagination?.total || 0
} else {
message.error(response.message || '获取员工列表失败')
// 使用模拟数据作为备用
employees.value = mockEmployees
pagination.value.total = mockEmployees.length
}
} catch (error) {
console.error('获取员工列表失败:', error)
message.error('获取员工列表失败')
// 使用模拟数据作为备用
employees.value = mockEmployees
pagination.value.total = mockEmployees.length
} finally {
loading.value = false
}
}
const fetchEmployeeStats = async () => {
try {
const response = await api.employees.getStats()
if (response.success) {
employeeStats.value = {
total: response.data.total || 0,
active: response.data.active || 0,
inactive: response.data.inactive || 0,
newHires: 0 // 这个需要单独计算
}
}
} catch (error) {
console.error('获取员工统计失败:', error)
}
}
const handleSearch = () => {
pagination.value.current = 1
fetchEmployees()
}
const handleFilter = () => {
pagination.value.current = 1
fetchEmployees()
}
const handleTableChange = (paginationInfo) => {
pagination.value = paginationInfo
fetchEmployees()
}
// 生命周期
onMounted(() => {
fetchEmployees()
fetchEmployeeStats()
})
</script>
<style scoped>
.employee-management {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.overview-section {
margin-bottom: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.employee-detail {
padding: 16px 0;
}
.employee-header {
display: flex;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
}
.employee-info {
margin-left: 16px;
}
.employee-info h3 {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
}
.employee-info p {
margin: 0 0 8px 0;
color: #666;
font-size: 14px;
}
.work-experience {
margin-top: 24px;
}
.work-experience h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.experience-item {
padding: 8px 0;
}
.experience-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.experience-title {
font-weight: 600;
font-size: 14px;
}
.experience-period {
color: #999;
font-size: 12px;
}
.experience-company {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.experience-description {
color: #333;
font-size: 12px;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,741 @@
<template>
<div class="hardware-management">
<div class="page-header">
<h1>硬件管理</h1>
<p>管理和监控银行硬件设备状态</p>
</div>
<div class="content">
<!-- 设备概览 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="设备总数"
:value="hardwareStats.total"
:value-style="{ color: '#1890ff' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="在线设备"
:value="hardwareStats.online"
:value-style="{ color: '#52c41a' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="离线设备"
:value="hardwareStats.offline"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="故障设备"
:value="hardwareStats.fault"
:value-style="{ color: '#faad14' }"
/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索设备名称或编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="设备状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="online">在线</a-select-option>
<a-select-option value="offline">离线</a-select-option>
<a-select-option value="fault">故障</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="设备类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="atm">ATM机</a-select-option>
<a-select-option value="pos">POS机</a-select-option>
<a-select-option value="server">服务器</a-select-option>
<a-select-option value="network">网络设备</a-select-option>
<a-select-option value="printer">打印机</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="locationFilter"
placeholder="设备位置"
allow-clear
@change="handleFilter"
>
<a-select-option value="branch1">总行</a-select-option>
<a-select-option value="branch2">分行</a-select-option>
<a-select-option value="branch3">支行</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-space>
<a-button type="primary" @click="handleAddDevice">
<PlusOutlined />
添加设备
</a-button>
<a-button @click="handleRefresh">
<ReloadOutlined />
刷新
</a-button>
</a-space>
</a-col>
</a-row>
</div>
<!-- 设备列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredDevices"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'location'">
{{ getLocationText(record.location) }}
</template>
<template v-else-if="column.key === 'lastCheckTime'">
{{ record.lastCheckTime || '未检查' }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleCheck(record)">
检查
</a-button>
<a-button type="link" size="small" @click="handleMaintenance(record)">
维护
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 设备详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="设备详情"
width="800px"
:footer="null"
>
<div v-if="selectedDevice" class="device-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="设备名称">
{{ selectedDevice.name }}
</a-descriptions-item>
<a-descriptions-item label="设备编号">
{{ selectedDevice.deviceNumber }}
</a-descriptions-item>
<a-descriptions-item label="设备类型">
<a-tag :color="getTypeColor(selectedDevice.type)">
{{ getTypeText(selectedDevice.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="设备状态">
<a-tag :color="getStatusColor(selectedDevice.status)">
{{ getStatusText(selectedDevice.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="设备位置">
{{ getLocationText(selectedDevice.location) }}
</a-descriptions-item>
<a-descriptions-item label="IP地址">
{{ selectedDevice.ipAddress }}
</a-descriptions-item>
<a-descriptions-item label="MAC地址">
{{ selectedDevice.macAddress }}
</a-descriptions-item>
<a-descriptions-item label="序列号">
{{ selectedDevice.serialNumber }}
</a-descriptions-item>
<a-descriptions-item label="购买日期">
{{ selectedDevice.purchaseDate }}
</a-descriptions-item>
<a-descriptions-item label="保修期至">
{{ selectedDevice.warrantyDate }}
</a-descriptions-item>
<a-descriptions-item label="最后检查时间">
{{ selectedDevice.lastCheckTime || '未检查' }}
</a-descriptions-item>
<a-descriptions-item label="负责人">
{{ selectedDevice.manager }}
</a-descriptions-item>
<a-descriptions-item label="设备描述" :span="2">
{{ selectedDevice.description }}
</a-descriptions-item>
</a-descriptions>
<!-- 设备监控数据 -->
<div class="device-monitoring" v-if="selectedDevice.monitoring">
<h4>监控数据</h4>
<a-row :gutter="16">
<a-col :span="8">
<a-statistic
title="CPU使用率"
:value="selectedDevice.monitoring.cpuUsage"
suffix="%"
:value-style="{ color: selectedDevice.monitoring.cpuUsage > 80 ? '#ff4d4f' : '#52c41a' }"
/>
</a-col>
<a-col :span="8">
<a-statistic
title="内存使用率"
:value="selectedDevice.monitoring.memoryUsage"
suffix="%"
:value-style="{ color: selectedDevice.monitoring.memoryUsage > 80 ? '#ff4d4f' : '#52c41a' }"
/>
</a-col>
<a-col :span="8">
<a-statistic
title="磁盘使用率"
:value="selectedDevice.monitoring.diskUsage"
suffix="%"
:value-style="{ color: selectedDevice.monitoring.diskUsage > 80 ? '#ff4d4f' : '#52c41a' }"
/>
</a-col>
</a-row>
</div>
<!-- 设备历史 -->
<div class="device-history" v-if="selectedDevice.history">
<h4>设备历史</h4>
<a-timeline>
<a-timeline-item
v-for="record in selectedDevice.history"
:key="record.id"
:color="getHistoryColor(record.action)"
>
<div class="history-item">
<div class="history-header">
<span class="history-action">{{ getHistoryActionText(record.action) }}</span>
<span class="history-time">{{ record.time }}</span>
</div>
<div class="history-user">操作人:{{ record.operator }}</div>
<div class="history-comment" v-if="record.comment">
备注{{ record.comment }}
</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const locationFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedDevice = ref(null)
// 硬件统计
const hardwareStats = ref({
total: 156,
online: 142,
offline: 8,
fault: 6
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '设备名称',
dataIndex: 'name',
key: 'name',
width: 150
},
{
title: '设备编号',
dataIndex: 'deviceNumber',
key: 'deviceNumber',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '位置',
dataIndex: 'location',
key: 'location',
width: 100
},
{
title: 'IP地址',
dataIndex: 'ipAddress',
key: 'ipAddress',
width: 120
},
{
title: '负责人',
dataIndex: 'manager',
key: 'manager',
width: 100
},
{
title: '最后检查',
dataIndex: 'lastCheckTime',
key: 'lastCheckTime',
width: 150
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 模拟设备数据
const devices = ref([
{
id: 1,
name: 'ATM-001',
deviceNumber: 'ATM-202401180001',
type: 'atm',
status: 'online',
location: 'branch1',
ipAddress: '192.168.1.101',
macAddress: '00:1B:44:11:3A:B7',
serialNumber: 'SN123456789',
purchaseDate: '2023-01-15',
warrantyDate: '2026-01-15',
lastCheckTime: '2024-01-18 09:30:00',
manager: '张三',
description: '大堂ATM机支持存取款和转账功能',
monitoring: {
cpuUsage: 45,
memoryUsage: 60,
diskUsage: 35
},
history: [
{
id: 1,
action: 'install',
operator: '技术部',
time: '2023-01-15 10:00:00',
comment: '设备安装完成'
},
{
id: 2,
action: 'check',
operator: '张三',
time: '2024-01-18 09:30:00',
comment: '日常检查,设备运行正常'
}
]
},
{
id: 2,
name: 'POS-001',
deviceNumber: 'POS-202401180002',
type: 'pos',
status: 'offline',
location: 'branch2',
ipAddress: '192.168.2.101',
macAddress: '00:1B:44:11:3A:B8',
serialNumber: 'SN123456790',
purchaseDate: '2023-03-20',
warrantyDate: '2026-03-20',
lastCheckTime: '2024-01-17 16:30:00',
manager: '李四',
description: '收银台POS机支持刷卡和扫码支付',
monitoring: null,
history: [
{
id: 1,
action: 'install',
operator: '技术部',
time: '2023-03-20 14:00:00',
comment: '设备安装完成'
},
{
id: 2,
action: 'offline',
operator: '系统',
time: '2024-01-18 08:00:00',
comment: '设备离线'
}
]
},
{
id: 3,
name: 'SERVER-001',
deviceNumber: 'SRV-202401180003',
type: 'server',
status: 'fault',
location: 'branch1',
ipAddress: '192.168.1.10',
macAddress: '00:1B:44:11:3A:B9',
serialNumber: 'SN123456791',
purchaseDate: '2022-12-01',
warrantyDate: '2025-12-01',
lastCheckTime: '2024-01-18 08:15:00',
manager: '王五',
description: '核心业务服务器,运行银行核心系统',
monitoring: {
cpuUsage: 95,
memoryUsage: 98,
diskUsage: 85
},
history: [
{
id: 1,
action: 'install',
operator: '技术部',
time: '2022-12-01 09:00:00',
comment: '服务器安装完成'
},
{
id: 2,
action: 'fault',
operator: '系统',
time: '2024-01-18 08:15:00',
comment: '服务器故障CPU使用率过高'
}
]
}
])
// 计算属性
const filteredDevices = computed(() => {
let result = devices.value
if (searchText.value) {
result = result.filter(device =>
device.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
device.deviceNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(device => device.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(device => device.type === typeFilter.value)
}
if (locationFilter.value) {
result = result.filter(device => device.location === locationFilter.value)
}
return result
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleAddDevice = () => {
message.info('添加设备功能开发中...')
}
const handleRefresh = () => {
loading.value = true
setTimeout(() => {
loading.value = false
message.success('数据已刷新')
}, 1000)
}
const handleView = (record) => {
selectedDevice.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑设备: ${record.name}`)
}
const handleCheck = (record) => {
record.lastCheckTime = new Date().toLocaleString()
record.history.push({
id: Date.now(),
action: 'check',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: '设备检查完成'
})
message.success('设备检查完成')
}
const handleMaintenance = (record) => {
record.status = 'maintenance'
record.history.push({
id: Date.now(),
action: 'maintenance',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: '设备进入维护状态'
})
message.success('设备已进入维护状态')
}
const getStatusColor = (status) => {
const colors = {
online: 'green',
offline: 'red',
fault: 'orange',
maintenance: 'blue'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
online: '在线',
offline: '离线',
fault: '故障',
maintenance: '维护中'
}
return texts[status] || status
}
const getTypeColor = (type) => {
const colors = {
atm: 'blue',
pos: 'green',
server: 'purple',
network: 'orange',
printer: 'cyan'
}
return colors[type] || 'default'
}
const getTypeText = (type) => {
const texts = {
atm: 'ATM机',
pos: 'POS机',
server: '服务器',
network: '网络设备',
printer: '打印机'
}
return texts[type] || type
}
const getLocationText = (location) => {
const texts = {
branch1: '总行',
branch2: '分行',
branch3: '支行'
}
return texts[location] || location
}
const getHistoryColor = (action) => {
const colors = {
install: 'blue',
check: 'green',
maintenance: 'orange',
fault: 'red',
offline: 'red'
}
return colors[action] || 'default'
}
const getHistoryActionText = (action) => {
const texts = {
install: '设备安装',
check: '设备检查',
maintenance: '设备维护',
fault: '设备故障',
offline: '设备离线'
}
return texts[action] || action
}
// 生命周期
onMounted(() => {
pagination.value.total = devices.value.length
})
</script>
<style scoped>
.hardware-management {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.overview-section {
margin-bottom: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.device-detail {
padding: 16px 0;
}
.device-monitoring {
margin-top: 24px;
padding: 16px;
background: #f5f5f5;
border-radius: 8px;
}
.device-monitoring h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.device-history {
margin-top: 24px;
}
.device-history h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.history-item {
padding: 8px 0;
}
.history-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.history-action {
font-weight: 600;
}
.history-time {
color: #999;
font-size: 12px;
}
.history-user {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.history-comment {
color: #333;
font-size: 12px;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<div class="loan-management">
<div class="page-header">
<h1>贷款管理</h1>
<p>管理银行贷款相关业务</p>
</div>
<div class="content">
<a-row :gutter="16">
<a-col :span="6">
<a-card hoverable @click="goToProducts" class="menu-card">
<div class="menu-item">
<CreditCardOutlined class="menu-icon" />
<div class="menu-content">
<h3>贷款商品</h3>
<p>管理和配置银行贷款产品</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card hoverable @click="goToApplications" class="menu-card">
<div class="menu-item">
<FileTextOutlined class="menu-icon" />
<div class="menu-content">
<h3>贷款申请进度</h3>
<p>管理和跟踪贷款申请流程</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card hoverable @click="goToContracts" class="menu-card">
<div class="menu-item">
<FileProtectOutlined class="menu-icon" />
<div class="menu-content">
<h3>贷款合同</h3>
<p>管理和跟踪贷款合同状态</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card hoverable @click="goToRelease" class="menu-card">
<div class="menu-item">
<UnlockOutlined class="menu-icon" />
<div class="menu-content">
<h3>贷款解押</h3>
<p>管理和处理贷款抵押物解押业务</p>
</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 子路由出口 -->
<div class="sub-route-content">
<router-view />
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
import {
CreditCardOutlined,
FileTextOutlined,
FileProtectOutlined,
UnlockOutlined
} from '@ant-design/icons-vue'
const router = useRouter()
const goToProducts = () => {
router.push('/loan-management/products')
}
const goToApplications = () => {
router.push('/loan-management/applications')
}
const goToContracts = () => {
router.push('/loan-management/contracts')
}
const goToRelease = () => {
router.push('/loan-management/release')
}
</script>
<style scoped>
.loan-management {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.menu-card {
margin-bottom: 16px;
cursor: pointer;
transition: all 0.3s;
}
.menu-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.menu-item {
display: flex;
align-items: center;
padding: 16px 0;
}
.menu-icon {
font-size: 32px;
color: #1890ff;
margin-right: 16px;
}
.menu-content h3 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
}
.menu-content p {
margin: 0;
color: #666;
font-size: 12px;
line-height: 1.4;
}
.sub-route-content {
margin-top: 24px;
}
</style>

View File

@@ -0,0 +1,517 @@
<template>
<div class="market-trends">
<div class="page-header">
<h1>市场行情</h1>
<p>实时金融市场数据和趋势分析</p>
</div>
<div class="content">
<!-- 市场概览 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="上证指数"
:value="marketData.shanghaiIndex"
:precision="2"
:value-style="{ color: marketData.shanghaiIndexChange >= 0 ? '#52c41a' : '#ff4d4f' }"
>
<template #suffix>
<span :style="{ color: marketData.shanghaiIndexChange >= 0 ? '#52c41a' : '#ff4d4f' }">
{{ marketData.shanghaiIndexChange >= 0 ? '+' : '' }}{{ marketData.shanghaiIndexChange }}%
</span>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="深证成指"
:value="marketData.shenzhenIndex"
:precision="2"
:value-style="{ color: marketData.shenzhenIndexChange >= 0 ? '#52c41a' : '#ff4d4f' }"
>
<template #suffix>
<span :style="{ color: marketData.shenzhenIndexChange >= 0 ? '#52c41a' : '#ff4d4f' }">
{{ marketData.shenzhenIndexChange >= 0 ? '+' : '' }}{{ marketData.shenzhenIndexChange }}%
</span>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="创业板指"
:value="marketData.chinextIndex"
:precision="2"
:value-style="{ color: marketData.chinextIndexChange >= 0 ? '#52c41a' : '#ff4d4f' }"
>
<template #suffix>
<span :style="{ color: marketData.chinextIndexChange >= 0 ? '#52c41a' : '#ff4d4f' }">
{{ marketData.chinextIndexChange >= 0 ? '+' : '' }}{{ marketData.chinextIndexChange }}%
</span>
</template>
</a-statistic>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="人民币汇率"
:value="marketData.exchangeRate"
:precision="4"
:value-style="{ color: marketData.exchangeRateChange >= 0 ? '#ff4d4f' : '#52c41a' }"
>
<template #suffix>
<span :style="{ color: marketData.exchangeRateChange >= 0 ? '#ff4d4f' : '#52c41a' }">
{{ marketData.exchangeRateChange >= 0 ? '+' : '' }}{{ marketData.exchangeRateChange }}%
</span>
</template>
</a-statistic>
</a-card>
</a-col>
</a-row>
</div>
<!-- 图表区域 -->
<div class="charts-section">
<a-row :gutter="16">
<a-col :span="12">
<a-card title="股指走势图" :bordered="false">
<div ref="indexChart" style="height: 300px;"></div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="汇率走势图" :bordered="false">
<div ref="exchangeChart" style="height: 300px;"></div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 银行股行情 -->
<div class="bank-stocks-section">
<a-card title="银行股行情" :bordered="false">
<template #extra>
<a-button @click="handleRefreshStocks">
<ReloadOutlined />
刷新
</a-button>
</template>
<a-table
:columns="stockColumns"
:data-source="bankStocks"
:pagination="false"
:loading="stocksLoading"
row-key="code"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'change'">
<span :style="{ color: record.change >= 0 ? '#52c41a' : '#ff4d4f' }">
{{ record.change >= 0 ? '+' : '' }}{{ record.change }}%
</span>
</template>
<template v-else-if="column.key === 'volume'">
{{ formatVolume(record.volume) }}
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" @click="handleViewStockDetail(record)">
详情
</a-button>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 市场新闻 -->
<div class="news-section">
<a-card title="市场新闻" :bordered="false">
<a-list
:data-source="marketNews"
:loading="newsLoading"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<a @click="handleViewNews(item)">{{ item.title }}</a>
</template>
<template #description>
<div class="news-meta">
<span>{{ item.source }}</span>
<span>{{ item.time }}</span>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</div>
</div>
<!-- 股票详情模态框 -->
<a-modal
v-model:open="stockDetailVisible"
title="股票详情"
width="600px"
:footer="null"
>
<div v-if="selectedStock" class="stock-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="股票代码">
{{ selectedStock.code }}
</a-descriptions-item>
<a-descriptions-item label="股票名称">
{{ selectedStock.name }}
</a-descriptions-item>
<a-descriptions-item label="当前价格">
¥{{ selectedStock.price }}
</a-descriptions-item>
<a-descriptions-item label="涨跌幅">
<span :style="{ color: selectedStock.change >= 0 ? '#52c41a' : '#ff4d4f' }">
{{ selectedStock.change >= 0 ? '+' : '' }}{{ selectedStock.change }}%
</span>
</a-descriptions-item>
<a-descriptions-item label="成交量">
{{ formatVolume(selectedStock.volume) }}
</a-descriptions-item>
<a-descriptions-item label="成交额">
{{ formatAmount(selectedStock.amount) }}
</a-descriptions-item>
<a-descriptions-item label="最高价">
¥{{ selectedStock.high }}
</a-descriptions-item>
<a-descriptions-item label="最低价">
¥{{ selectedStock.low }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
import * as echarts from 'echarts'
// 响应式数据
const indexChart = ref(null)
const exchangeChart = ref(null)
const stocksLoading = ref(false)
const newsLoading = ref(false)
const stockDetailVisible = ref(false)
const selectedStock = ref(null)
// 市场数据
const marketData = ref({
shanghaiIndex: 3245.67,
shanghaiIndexChange: 1.25,
shenzhenIndex: 12345.89,
shenzhenIndexChange: -0.85,
chinextIndex: 2567.34,
chinextIndexChange: 2.15,
exchangeRate: 7.2345,
exchangeRateChange: -0.12
})
// 表格列配置
const stockColumns = [
{
title: '股票代码',
dataIndex: 'code',
key: 'code',
width: 100
},
{
title: '股票名称',
dataIndex: 'name',
key: 'name',
width: 150
},
{
title: '当前价格',
dataIndex: 'price',
key: 'price',
width: 100
},
{
title: '涨跌幅',
dataIndex: 'change',
key: 'change',
width: 100
},
{
title: '成交量',
dataIndex: 'volume',
key: 'volume',
width: 120
},
{
title: '成交额',
dataIndex: 'amount',
key: 'amount',
width: 120
},
{
title: '操作',
key: 'action',
width: 80
}
]
// 银行股数据
const bankStocks = ref([
{
code: '600036',
name: '招商银行',
price: 45.67,
change: 2.15,
volume: 12345678,
amount: 562345678,
high: 46.20,
low: 44.80
},
{
code: '000001',
name: '平安银行',
price: 12.34,
change: -1.25,
volume: 8765432,
amount: 108234567,
high: 12.50,
low: 12.10
},
{
code: '600000',
name: '浦发银行',
price: 8.95,
change: 0.85,
volume: 15678901,
amount: 140345678,
high: 9.05,
low: 8.85
},
{
code: '601166',
name: '兴业银行',
price: 18.76,
change: -0.45,
volume: 9876543,
amount: 185234567,
high: 18.90,
low: 18.50
}
])
// 市场新闻数据
const marketNews = ref([
{
id: 1,
title: '央行宣布降准0.5个百分点释放流动性约1万亿元',
source: '央行官网',
time: '2024-01-18 09:30',
content: '中国人民银行决定下调金融机构存款准备金率0.5个百分点...'
},
{
id: 2,
title: '银保监会发布新规,加强银行风险管理',
source: '银保监会',
time: '2024-01-18 08:45',
content: '为进一步加强银行业风险管理,银保监会发布新规...'
},
{
id: 3,
title: '多家银行发布2023年业绩预告净利润普遍增长',
source: '财经网',
time: '2024-01-18 08:20',
content: '截至1月18日已有15家银行发布2023年业绩预告...'
}
])
// 方法
const handleRefreshStocks = async () => {
stocksLoading.value = true
// 模拟刷新数据
await new Promise(resolve => setTimeout(resolve, 1000))
stocksLoading.value = false
message.success('数据已刷新')
}
const handleViewStockDetail = (record) => {
selectedStock.value = record
stockDetailVisible.value = true
}
const handleViewNews = (item) => {
message.info(`查看新闻: ${item.title}`)
}
const formatVolume = (volume) => {
if (volume >= 100000000) {
return (volume / 100000000).toFixed(2) + '亿'
} else if (volume >= 10000) {
return (volume / 10000).toFixed(2) + '万'
}
return volume.toString()
}
const formatAmount = (amount) => {
if (amount >= 100000000) {
return (amount / 100000000).toFixed(2) + '亿'
} else if (amount >= 10000) {
return (amount / 10000).toFixed(2) + '万'
}
return amount.toString()
}
const initCharts = () => {
nextTick(() => {
// 股指走势图
if (indexChart.value) {
const indexChartInstance = echarts.init(indexChart.value)
const indexOption = {
title: {
text: '股指走势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['上证指数', '深证成指', '创业板指'],
top: 30
},
xAxis: {
type: 'category',
data: ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '14:00', '14:30', '15:00']
},
yAxis: {
type: 'value'
},
series: [
{
name: '上证指数',
type: 'line',
data: [3200, 3215, 3230, 3245, 3250, 3245, 3240, 3245, 3245.67],
smooth: true
},
{
name: '深证成指',
type: 'line',
data: [12200, 12250, 12300, 12350, 12380, 12350, 12320, 12340, 12345.89],
smooth: true
},
{
name: '创业板指',
type: 'line',
data: [2500, 2520, 2540, 2560, 2570, 2560, 2550, 2560, 2567.34],
smooth: true
}
]
}
indexChartInstance.setOption(indexOption)
}
// 汇率走势图
if (exchangeChart.value) {
const exchangeChartInstance = echarts.init(exchangeChart.value)
const exchangeOption = {
title: {
text: '人民币汇率走势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '14:00', '14:30', '15:00']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'USD/CNY',
type: 'line',
data: [7.2450, 7.2400, 7.2350, 7.2300, 7.2320, 7.2340, 7.2360, 7.2340, 7.2345],
smooth: true,
lineStyle: {
color: '#ff4d4f'
}
}
]
}
exchangeChartInstance.setOption(exchangeOption)
}
})
}
// 生命周期
onMounted(() => {
initCharts()
})
</script>
<style scoped>
.market-trends {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.overview-section {
margin-bottom: 24px;
}
.charts-section {
margin-bottom: 24px;
}
.bank-stocks-section {
margin-bottom: 24px;
}
.news-section {
margin-bottom: 24px;
}
.news-meta {
display: flex;
justify-content: space-between;
color: #999;
font-size: 12px;
}
.stock-detail {
padding: 16px 0;
}
</style>

View File

@@ -1 +1,462 @@
<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>
<template>
<div class="personal-center">
<div class="page-header">
<h1>个人中心</h1>
<p>管理个人信息和账户设置</p>
</div>
<div class="content">
<a-row :gutter="24">
<!-- 个人信息卡片 -->
<a-col :span="8">
<a-card title="个人信息" :bordered="false">
<div class="profile-card">
<div class="avatar-section">
<a-avatar :size="80" :src="userInfo.avatar">
{{ userInfo.name.charAt(0) }}
</a-avatar>
<a-button type="link" @click="handleChangeAvatar">
更换头像
</a-button>
</div>
<div class="user-info">
<h3>{{ userInfo.name }}</h3>
<p>{{ userInfo.position }} - {{ userInfo.department }}</p>
<a-tag :color="getStatusColor(userInfo.status)">
{{ getStatusText(userInfo.status) }}
</a-tag>
</div>
</div>
</a-card>
</a-col>
<!-- 账户信息 -->
<a-col :span="16">
<a-card title="账户信息" :bordered="false">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="用户名">
{{ userInfo.username }}
</a-descriptions-item>
<a-descriptions-item label="邮箱">
{{ userInfo.email }}
</a-descriptions-item>
<a-descriptions-item label="手机号">
{{ userInfo.phone }}
</a-descriptions-item>
<a-descriptions-item label="工号">
{{ userInfo.employeeId }}
</a-descriptions-item>
<a-descriptions-item label="入职日期">
{{ userInfo.hireDate }}
</a-descriptions-item>
<a-descriptions-item label="最后登录">
{{ userInfo.lastLogin }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
<!-- 功能菜单 -->
<div class="function-menu">
<a-row :gutter="16">
<a-col :span="6">
<a-card hoverable @click="handleEditProfile">
<div class="menu-item">
<UserOutlined class="menu-icon" />
<div class="menu-content">
<h4>编辑资料</h4>
<p>修改个人信息</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card hoverable @click="handleChangePassword">
<div class="menu-item">
<LockOutlined class="menu-icon" />
<div class="menu-content">
<h4>修改密码</h4>
<p>更改登录密码</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card hoverable @click="handleSecuritySettings">
<div class="menu-item">
<SafetyOutlined class="menu-icon" />
<div class="menu-content">
<h4>安全设置</h4>
<p>账户安全配置</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card hoverable @click="handleNotificationSettings">
<div class="menu-item">
<BellOutlined class="menu-icon" />
<div class="menu-content">
<h4>通知设置</h4>
<p>消息通知配置</p>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
<!-- 最近活动 -->
<div class="recent-activities">
<a-card title="最近活动" :bordered="false">
<a-timeline>
<a-timeline-item
v-for="activity in recentActivities"
:key="activity.id"
:color="getActivityColor(activity.type)"
>
<div class="activity-item">
<div class="activity-header">
<span class="activity-title">{{ activity.title }}</span>
<span class="activity-time">{{ activity.time }}</span>
</div>
<div class="activity-description">{{ activity.description }}</div>
</div>
</a-timeline-item>
</a-timeline>
</a-card>
</div>
</div>
<!-- 编辑资料模态框 -->
<a-modal
v-model:open="editModalVisible"
title="编辑资料"
@ok="handleEditSubmit"
@cancel="handleEditCancel"
>
<a-form :model="editForm" layout="vertical">
<a-form-item label="姓名" required>
<a-input v-model:value="editForm.name" />
</a-form-item>
<a-form-item label="邮箱" required>
<a-input v-model:value="editForm.email" />
</a-form-item>
<a-form-item label="手机号" required>
<a-input v-model:value="editForm.phone" />
</a-form-item>
<a-form-item label="个人简介">
<a-textarea v-model:value="editForm.bio" :rows="3" />
</a-form-item>
</a-form>
</a-modal>
<!-- 修改密码模态框 -->
<a-modal
v-model:open="passwordModalVisible"
title="修改密码"
@ok="handlePasswordSubmit"
@cancel="handlePasswordCancel"
>
<a-form :model="passwordForm" layout="vertical">
<a-form-item label="原密码" required>
<a-input-password v-model:value="passwordForm.oldPassword" />
</a-form-item>
<a-form-item label="新密码" required>
<a-input-password v-model:value="passwordForm.newPassword" />
</a-form-item>
<a-form-item label="确认新密码" required>
<a-input-password v-model:value="passwordForm.confirmPassword" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
UserOutlined,
LockOutlined,
SafetyOutlined,
BellOutlined
} from '@ant-design/icons-vue'
// 响应式数据
const editModalVisible = ref(false)
const passwordModalVisible = ref(false)
const editForm = ref({
name: '',
email: '',
phone: '',
bio: ''
})
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
// 用户信息
const userInfo = ref({
name: '张三',
username: 'zhangsan',
email: 'zhangsan@bank.com',
phone: '13800138000',
employeeId: 'EMP001',
position: '经理',
department: '行政部',
status: 'active',
hireDate: '2020-03-15',
lastLogin: '2024-01-18 09:30:00',
avatar: null,
bio: '具有5年银行管理经验擅长团队管理和业务规划'
})
// 最近活动
const recentActivities = ref([
{
id: 1,
type: 'login',
title: '登录系统',
description: '在总行办公室登录系统',
time: '2024-01-18 09:30:00'
},
{
id: 2,
type: 'profile',
title: '更新个人信息',
description: '修改了个人简介',
time: '2024-01-17 16:45:00'
},
{
id: 3,
type: 'password',
title: '修改密码',
description: '成功修改登录密码',
time: '2024-01-16 14:20:00'
},
{
id: 4,
type: 'system',
title: '系统通知',
description: '收到新的系统更新通知',
time: '2024-01-15 10:15:00'
}
])
// 方法
const handleChangeAvatar = () => {
message.info('更换头像功能开发中...')
}
const handleEditProfile = () => {
editForm.value = {
name: userInfo.value.name,
email: userInfo.value.email,
phone: userInfo.value.phone,
bio: userInfo.value.bio
}
editModalVisible.value = true
}
const handleChangePassword = () => {
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: ''
}
passwordModalVisible.value = true
}
const handleSecuritySettings = () => {
message.info('安全设置功能开发中...')
}
const handleNotificationSettings = () => {
message.info('通知设置功能开发中...')
}
const handleEditSubmit = () => {
if (!editForm.value.name || !editForm.value.email || !editForm.value.phone) {
message.error('请填写完整信息')
return
}
userInfo.value.name = editForm.value.name
userInfo.value.email = editForm.value.email
userInfo.value.phone = editForm.value.phone
userInfo.value.bio = editForm.value.bio
editModalVisible.value = false
message.success('个人信息更新成功')
}
const handleEditCancel = () => {
editModalVisible.value = false
}
const handlePasswordSubmit = () => {
if (!passwordForm.value.oldPassword || !passwordForm.value.newPassword || !passwordForm.value.confirmPassword) {
message.error('请填写完整信息')
return
}
if (passwordForm.value.newPassword !== passwordForm.value.confirmPassword) {
message.error('两次输入的新密码不一致')
return
}
if (passwordForm.value.newPassword.length < 6) {
message.error('新密码长度不能少于6位')
return
}
passwordModalVisible.value = false
message.success('密码修改成功')
}
const handlePasswordCancel = () => {
passwordModalVisible.value = false
}
const getStatusColor = (status) => {
const colors = {
active: 'green',
inactive: 'red',
suspended: 'orange'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
active: '在职',
inactive: '离职',
suspended: '停职'
}
return texts[status] || status
}
const getActivityColor = (type) => {
const colors = {
login: 'green',
profile: 'blue',
password: 'orange',
system: 'purple'
}
return colors[type] || 'default'
}
// 生命周期
onMounted(() => {
// 初始化数据
})
</script>
<style scoped>
.personal-center {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.profile-card {
text-align: center;
}
.avatar-section {
margin-bottom: 16px;
}
.user-info h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
}
.user-info p {
margin: 0 0 8px 0;
color: #666;
font-size: 14px;
}
.function-menu {
margin: 24px 0;
}
.menu-item {
display: flex;
align-items: center;
padding: 16px 0;
}
.menu-icon {
font-size: 24px;
color: #1890ff;
margin-right: 16px;
}
.menu-content h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.menu-content p {
margin: 0;
color: #666;
font-size: 12px;
}
.recent-activities {
margin-top: 24px;
}
.activity-item {
padding: 8px 0;
}
.activity-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.activity-title {
font-weight: 600;
font-size: 14px;
}
.activity-time {
color: #999;
font-size: 12px;
}
.activity-description {
color: #666;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,435 @@
<template>
<div class="project-list">
<div class="page-header">
<h1>项目清单</h1>
<p>管理和查看银行项目信息</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="8">
<a-input-search
v-model:value="searchText"
placeholder="搜索项目名称或编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="6">
<a-select
v-model:value="statusFilter"
placeholder="项目状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="planning">规划中</a-select-option>
<a-select-option value="active">进行中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="suspended">已暂停</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-select
v-model:value="priorityFilter"
placeholder="优先级"
allow-clear
@change="handleFilter"
>
<a-select-option value="high"></a-select-option>
<a-select-option value="medium"></a-select-option>
<a-select-option value="low"></a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleAddProject">
<PlusOutlined />
新建项目
</a-button>
</a-col>
</a-row>
</div>
<!-- 项目列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredProjects"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'priority'">
<a-tag :color="getPriorityColor(record.priority)">
{{ getPriorityText(record.priority) }}
</a-tag>
</template>
<template v-else-if="column.key === 'progress'">
<a-progress
:percent="record.progress"
:status="record.progress === 100 ? 'success' : 'active'"
size="small"
/>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-popconfirm
title="确定要删除这个项目吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" size="small" danger>
删除
</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 项目详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="项目详情"
width="800px"
:footer="null"
>
<div v-if="selectedProject" class="project-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="项目名称">
{{ selectedProject.name }}
</a-descriptions-item>
<a-descriptions-item label="项目编号">
{{ selectedProject.code }}
</a-descriptions-item>
<a-descriptions-item label="项目状态">
<a-tag :color="getStatusColor(selectedProject.status)">
{{ getStatusText(selectedProject.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="优先级">
<a-tag :color="getPriorityColor(selectedProject.priority)">
{{ getPriorityText(selectedProject.priority) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="负责人">
{{ selectedProject.manager }}
</a-descriptions-item>
<a-descriptions-item label="开始日期">
{{ selectedProject.startDate }}
</a-descriptions-item>
<a-descriptions-item label="预计完成日期">
{{ selectedProject.endDate }}
</a-descriptions-item>
<a-descriptions-item label="实际完成日期">
{{ selectedProject.actualEndDate || '未完成' }}
</a-descriptions-item>
<a-descriptions-item label="项目进度" :span="2">
<a-progress
:percent="selectedProject.progress"
:status="selectedProject.progress === 100 ? 'success' : 'active'"
/>
</a-descriptions-item>
<a-descriptions-item label="项目描述" :span="2">
{{ selectedProject.description }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const priorityFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedProject = ref(null)
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '项目名称',
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: '项目编号',
dataIndex: 'code',
key: 'code',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '优先级',
dataIndex: 'priority',
key: 'priority',
width: 100
},
{
title: '负责人',
dataIndex: 'manager',
key: 'manager',
width: 120
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
width: 150
},
{
title: '开始日期',
dataIndex: 'startDate',
key: 'startDate',
width: 120
},
{
title: '预计完成',
dataIndex: 'endDate',
key: 'endDate',
width: 120
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right'
}
]
// 模拟项目数据
const projects = ref([
{
id: 1,
name: '核心银行系统升级',
code: 'PRJ-001',
status: 'active',
priority: 'high',
manager: '张三',
progress: 75,
startDate: '2024-01-01',
endDate: '2024-06-30',
actualEndDate: null,
description: '升级核心银行系统,提升性能和安全性'
},
{
id: 2,
name: '移动银行APP开发',
code: 'PRJ-002',
status: 'planning',
priority: 'medium',
manager: '李四',
progress: 20,
startDate: '2024-02-01',
endDate: '2024-08-31',
actualEndDate: null,
description: '开发新一代移动银行应用程序'
},
{
id: 3,
name: '风险管理系统',
code: 'PRJ-003',
status: 'completed',
priority: 'high',
manager: '王五',
progress: 100,
startDate: '2023-10-01',
endDate: '2024-01-31',
actualEndDate: '2024-01-25',
description: '建立完善的风险管理和监控系统'
},
{
id: 4,
name: '数据仓库建设',
code: 'PRJ-004',
status: 'suspended',
priority: 'low',
manager: '赵六',
progress: 40,
startDate: '2023-12-01',
endDate: '2024-05-31',
actualEndDate: null,
description: '建设企业级数据仓库和分析平台'
}
])
// 计算属性
const filteredProjects = computed(() => {
let result = projects.value
if (searchText.value) {
result = result.filter(project =>
project.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
project.code.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(project => project.status === statusFilter.value)
}
if (priorityFilter.value) {
result = result.filter(project => project.priority === priorityFilter.value)
}
return result
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleAddProject = () => {
message.info('新建项目功能开发中...')
}
const handleView = (record) => {
selectedProject.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑项目: ${record.name}`)
}
const handleDelete = (id) => {
const index = projects.value.findIndex(project => project.id === id)
if (index > -1) {
projects.value.splice(index, 1)
message.success('项目删除成功')
}
}
const getStatusColor = (status) => {
const colors = {
planning: 'blue',
active: 'green',
completed: 'success',
suspended: 'orange'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
planning: '规划中',
active: '进行中',
completed: '已完成',
suspended: '已暂停'
}
return texts[status] || status
}
const getPriorityColor = (priority) => {
const colors = {
high: 'red',
medium: 'orange',
low: 'green'
}
return colors[priority] || 'default'
}
const getPriorityText = (priority) => {
const texts = {
high: '高',
medium: '中',
low: '低'
}
return texts[priority] || priority
}
// 生命周期
onMounted(() => {
pagination.value.total = projects.value.length
})
</script>
<style scoped>
.project-list {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.project-detail {
padding: 16px 0;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,528 @@
<template>
<div class="system-check">
<div class="page-header">
<h1>系统日检</h1>
<p>每日系统健康检查和监控</p>
</div>
<div class="content">
<!-- 检查概览 -->
<div class="overview-section">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<a-statistic
title="检查项目总数"
:value="checkItems.length"
:value-style="{ color: '#1890ff' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="通过项目"
:value="passedItems"
:value-style="{ color: '#52c41a' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="警告项目"
:value="warningItems"
:value-style="{ color: '#faad14' }"
/>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a-statistic
title="错误项目"
:value="errorItems"
:value-style="{ color: '#ff4d4f' }"
/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<a-space>
<a-button type="primary" @click="handleStartCheck" :loading="checking">
<PlayCircleOutlined />
开始检查
</a-button>
<a-button @click="handleRefresh">
<ReloadOutlined />
刷新状态
</a-button>
<a-button @click="handleExportReport">
<DownloadOutlined />
导出报告
</a-button>
</a-space>
</div>
<!-- 检查项目列表 -->
<div class="check-list-section">
<a-table
:columns="columns"
:data-source="checkItems"
:pagination="false"
:loading="checking"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'category'">
<a-tag :color="getCategoryColor(record.category)">
{{ getCategoryText(record.category) }}
</a-tag>
</template>
<template v-else-if="column.key === 'lastCheckTime'">
{{ record.lastCheckTime || '未检查' }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleCheckItem(record)">
立即检查
</a-button>
<a-button type="link" size="small" @click="handleViewDetail(record)">
查看详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<!-- 检查历史 -->
<div class="history-section">
<h3>检查历史</h3>
<a-table
:columns="historyColumns"
:data-source="checkHistory"
:pagination="historyPagination"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'duration'">
{{ record.duration }}
</template>
</template>
</a-table>
</div>
</div>
<!-- 检查详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="检查详情"
width="600px"
:footer="null"
>
<div v-if="selectedItem" class="check-detail">
<a-descriptions :column="1" bordered>
<a-descriptions-item label="检查项目">
{{ selectedItem.name }}
</a-descriptions-item>
<a-descriptions-item label="检查类别">
<a-tag :color="getCategoryColor(selectedItem.category)">
{{ getCategoryText(selectedItem.category) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="当前状态">
<a-tag :color="getStatusColor(selectedItem.status)">
{{ getStatusText(selectedItem.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="最后检查时间">
{{ selectedItem.lastCheckTime || '未检查' }}
</a-descriptions-item>
<a-descriptions-item label="检查描述">
{{ selectedItem.description }}
</a-descriptions-item>
<a-descriptions-item label="检查结果" v-if="selectedItem.result">
<pre>{{ selectedItem.result }}</pre>
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
PlayCircleOutlined,
ReloadOutlined,
DownloadOutlined
} from '@ant-design/icons-vue'
// 响应式数据
const checking = ref(false)
const detailModalVisible = ref(false)
const selectedItem = ref(null)
// 分页配置
const historyPagination = ref({
current: 1,
pageSize: 10,
total: 0
})
// 表格列配置
const columns = [
{
title: '检查项目',
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: '类别',
dataIndex: 'category',
key: 'category',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '最后检查时间',
dataIndex: 'lastCheckTime',
key: 'lastCheckTime',
width: 180
},
{
title: '描述',
dataIndex: 'description',
key: 'description'
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right'
}
]
const historyColumns = [
{
title: '检查时间',
dataIndex: 'checkTime',
key: 'checkTime',
width: 180
},
{
title: '检查项目',
dataIndex: 'itemName',
key: 'itemName',
width: 200
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
width: 100
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark'
}
]
// 检查项目数据
const checkItems = ref([
{
id: 1,
name: '数据库连接检查',
category: 'database',
status: 'success',
lastCheckTime: '2024-01-18 09:30:15',
description: '检查数据库连接是否正常',
result: '数据库连接正常,响应时间: 15ms'
},
{
id: 2,
name: 'API服务检查',
category: 'api',
status: 'success',
lastCheckTime: '2024-01-18 09:30:20',
description: '检查API服务是否正常运行',
result: 'API服务正常所有接口响应正常'
},
{
id: 3,
name: '磁盘空间检查',
category: 'system',
status: 'warning',
lastCheckTime: '2024-01-18 09:30:25',
description: '检查服务器磁盘空间使用情况',
result: '磁盘使用率: 85%,建议清理日志文件'
},
{
id: 4,
name: '内存使用检查',
category: 'system',
status: 'success',
lastCheckTime: '2024-01-18 09:30:30',
description: '检查服务器内存使用情况',
result: '内存使用率: 65%,正常'
},
{
id: 5,
name: '网络连接检查',
category: 'network',
status: 'error',
lastCheckTime: '2024-01-18 09:30:35',
description: '检查网络连接状态',
result: '网络连接异常无法访问外部API'
},
{
id: 6,
name: '日志文件检查',
category: 'log',
status: 'success',
lastCheckTime: '2024-01-18 09:30:40',
description: '检查日志文件是否正常生成',
result: '日志文件正常生成,无异常'
}
])
// 检查历史数据
const checkHistory = ref([
{
id: 1,
checkTime: '2024-01-18 09:30:00',
itemName: '数据库连接检查',
status: 'success',
duration: 2,
remark: '检查通过'
},
{
id: 2,
checkTime: '2024-01-18 09:30:05',
itemName: 'API服务检查',
status: 'success',
duration: 3,
remark: '检查通过'
},
{
id: 3,
checkTime: '2024-01-18 09:30:10',
itemName: '磁盘空间检查',
status: 'warning',
duration: 1,
remark: '磁盘使用率较高'
}
])
// 计算属性
const passedItems = computed(() =>
checkItems.value.filter(item => item.status === 'success').length
)
const warningItems = computed(() =>
checkItems.value.filter(item => item.status === 'warning').length
)
const errorItems = computed(() =>
checkItems.value.filter(item => item.status === 'error').length
)
// 方法
const handleStartCheck = async () => {
checking.value = true
message.loading('正在执行系统检查...', 0)
// 模拟检查过程
for (let i = 0; i < checkItems.value.length; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
// 随机设置检查结果
const statuses = ['success', 'warning', 'error']
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]
checkItems.value[i].status = randomStatus
checkItems.value[i].lastCheckTime = new Date().toLocaleString()
// 添加检查历史
checkHistory.value.unshift({
id: Date.now() + i,
checkTime: new Date().toLocaleString(),
itemName: checkItems.value[i].name,
status: randomStatus,
duration: Math.floor(Math.random() * 5) + 1,
remark: randomStatus === 'success' ? '检查通过' :
randomStatus === 'warning' ? '存在警告' : '检查失败'
})
}
checking.value = false
message.destroy()
message.success('系统检查完成')
}
const handleRefresh = () => {
message.success('状态已刷新')
}
const handleExportReport = () => {
message.info('导出报告功能开发中...')
}
const handleCheckItem = async (record) => {
checking.value = true
message.loading(`正在检查 ${record.name}...`, 0)
// 模拟单个项目检查
await new Promise(resolve => setTimeout(resolve, 2000))
const statuses = ['success', 'warning', 'error']
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)]
record.status = randomStatus
record.lastCheckTime = new Date().toLocaleString()
checking.value = false
message.destroy()
message.success(`${record.name} 检查完成`)
}
const handleViewDetail = (record) => {
selectedItem.value = record
detailModalVisible.value = true
}
const getStatusColor = (status) => {
const colors = {
success: 'green',
warning: 'orange',
error: 'red',
pending: 'blue'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
success: '正常',
warning: '警告',
error: '错误',
pending: '待检查'
}
return texts[status] || status
}
const getCategoryColor = (category) => {
const colors = {
database: 'blue',
api: 'green',
system: 'orange',
network: 'purple',
log: 'cyan'
}
return colors[category] || 'default'
}
const getCategoryText = (category) => {
const texts = {
database: '数据库',
api: 'API服务',
system: '系统资源',
network: '网络连接',
log: '日志文件'
}
return texts[category] || category
}
// 生命周期
onMounted(() => {
historyPagination.value.total = checkHistory.value.length
})
</script>
<style scoped>
.system-check {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.overview-section {
margin-bottom: 24px;
}
.action-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.check-list-section {
margin-bottom: 32px;
}
.history-section h3 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.check-detail {
padding: 16px 0;
}
.check-detail pre {
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
}
</style>

View File

@@ -114,6 +114,7 @@
import { defineComponent, ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { api } from '@/utils/api';
export default defineComponent({
name: 'UsersPage',
@@ -220,25 +221,33 @@ export default defineComponent({
const fetchUsers = async (params = {}) => {
loading.value = true;
try {
// 这里应该是实际的API调用
// const response = await api.getUsers(params);
// users.value = response.data;
// pagination.total = response.total;
const response = await api.users.getList({
page: params.page || pagination.current,
limit: params.pageSize || pagination.pageSize,
search: params.search || searchQuery.value,
...params
});
// 模拟数据
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);
if (response.success) {
users.value = response.data.users || [];
pagination.total = response.data.pagination?.total || 0;
} else {
message.error(response.message || '获取用户列表失败');
}
} catch (error) {
console.error('获取用户列表失败:', error);
message.error('获取用户列表失败');
// 如果API调用失败使用模拟数据
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;
} finally {
loading.value = false;
}
};
@@ -280,11 +289,15 @@ export default defineComponent({
// 删除用户
const deleteUser = async (id) => {
try {
// 这里应该是实际的API调用
// await api.deleteUser(id);
message.success('用户删除成功');
fetchUsers({ page: pagination.current });
const response = await api.users.delete(id);
if (response.success) {
message.success('用户删除成功');
fetchUsers({ page: pagination.current });
} else {
message.error(response.message || '删除用户失败');
}
} catch (error) {
console.error('删除用户失败:', error);
message.error('删除用户失败');
}
};
@@ -294,18 +307,30 @@ export default defineComponent({
userFormRef.value.validate().then(async () => {
submitting.value = true;
try {
let response;
if (isEditing.value) {
// 编辑用户
// await api.updateUser(userForm.id, userForm);
message.success('用户更新成功');
response = await api.users.update(userForm.id, userForm);
if (response.success) {
message.success('用户更新成功');
} else {
message.error(response.message || '更新用户失败');
return;
}
} else {
// 添加用户
// await api.createUser(userForm);
message.success('用户添加成功');
response = await api.users.create(userForm);
if (response.success) {
message.success('用户添加成功');
} else {
message.error(response.message || '添加用户失败');
return;
}
}
userModalVisible.value = false;
fetchUsers({ page: pagination.current });
} catch (error) {
console.error('用户操作失败:', error);
message.error(isEditing.value ? '更新用户失败' : '添加用户失败');
} finally {
submitting.value = false;

View File

@@ -0,0 +1,647 @@
<template>
<div class="loan-applications">
<div class="page-header">
<h1>贷款申请进度</h1>
<p>管理和跟踪贷款申请流程</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索申请人或申请编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="申请状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="贷款类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="personal">个人贷款</a-select-option>
<a-select-option value="business">企业贷款</a-select-option>
<a-select-option value="mortgage">抵押贷款</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model:value="dateRange"
@change="handleFilter"
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleAddApplication">
<PlusOutlined />
新建申请
</a-button>
</a-col>
</a-row>
</div>
<!-- 申请列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredApplications"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.amount) }}
</template>
<template v-else-if="column.key === 'progress'">
<a-progress
:percent="getProgressPercent(record.status)"
:status="getProgressStatus(record.status)"
size="small"
/>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button
type="link"
size="small"
@click="handleApprove(record)"
v-if="record.status === 'pending'"
>
审核
</a-button>
<a-button
type="link"
size="small"
@click="handleReject(record)"
v-if="record.status === 'pending'"
danger
>
拒绝
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 申请详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="申请详情"
width="800px"
:footer="null"
>
<div v-if="selectedApplication" class="application-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="申请编号">
{{ selectedApplication.applicationNumber }}
</a-descriptions-item>
<a-descriptions-item label="申请人">
{{ selectedApplication.applicantName }}
</a-descriptions-item>
<a-descriptions-item label="申请类型">
<a-tag :color="getTypeColor(selectedApplication.type)">
{{ getTypeText(selectedApplication.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="申请状态">
<a-tag :color="getStatusColor(selectedApplication.status)">
{{ getStatusText(selectedApplication.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="申请金额">
{{ formatAmount(selectedApplication.amount) }}
</a-descriptions-item>
<a-descriptions-item label="申请期限">
{{ selectedApplication.term }} 个月
</a-descriptions-item>
<a-descriptions-item label="申请时间">
{{ selectedApplication.applicationTime }}
</a-descriptions-item>
<a-descriptions-item label="预计利率">
{{ selectedApplication.interestRate }}%
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ selectedApplication.phone }}
</a-descriptions-item>
<a-descriptions-item label="身份证号">
{{ selectedApplication.idCard }}
</a-descriptions-item>
<a-descriptions-item label="申请用途" :span="2">
{{ selectedApplication.purpose }}
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
{{ selectedApplication.remark || '无' }}
</a-descriptions-item>
</a-descriptions>
<!-- 审核记录 -->
<div class="audit-records" v-if="selectedApplication.auditRecords">
<h4>审核记录</h4>
<a-timeline>
<a-timeline-item
v-for="record in selectedApplication.auditRecords"
:key="record.id"
:color="getAuditColor(record.action)"
>
<div class="audit-item">
<div class="audit-header">
<span class="audit-action">{{ getAuditActionText(record.action) }}</span>
<span class="audit-time">{{ record.time }}</span>
</div>
<div class="audit-user">审核人{{ record.auditor }}</div>
<div class="audit-comment" v-if="record.comment">
备注{{ record.comment }}
</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
</div>
</a-modal>
<!-- 审核模态框 -->
<a-modal
v-model:open="auditModalVisible"
title="贷款审核"
@ok="handleAuditSubmit"
@cancel="handleAuditCancel"
>
<a-form :model="auditForm" layout="vertical">
<a-form-item label="审核结果" required>
<a-radio-group v-model:value="auditForm.action">
<a-radio value="approve">通过</a-radio>
<a-radio value="reject">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="审核意见" required>
<a-textarea
v-model:value="auditForm.comment"
placeholder="请输入审核意见"
:rows="4"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const dateRange = ref([])
const detailModalVisible = ref(false)
const auditModalVisible = ref(false)
const selectedApplication = ref(null)
const auditForm = ref({
action: 'approve',
comment: ''
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '申请编号',
dataIndex: 'applicationNumber',
key: 'applicationNumber',
width: 150
},
{
title: '申请人',
dataIndex: 'applicantName',
key: 'applicantName',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '申请金额',
dataIndex: 'amount',
key: 'amount',
width: 120
},
{
title: '申请时间',
dataIndex: 'applicationTime',
key: 'applicationTime',
width: 150
},
{
title: '进度',
dataIndex: 'progress',
key: 'progress',
width: 150
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 模拟申请数据
const applications = ref([
{
id: 1,
applicationNumber: 'APP-202401180001',
applicantName: '张三',
type: 'personal',
status: 'pending',
amount: 200000,
term: 24,
interestRate: 6.5,
applicationTime: '2024-01-18 09:30:00',
phone: '13800138000',
idCard: '110101199001011234',
purpose: '个人消费',
remark: '',
auditRecords: [
{
id: 1,
action: 'submit',
auditor: '张三',
time: '2024-01-18 09:30:00',
comment: '提交申请'
}
]
},
{
id: 2,
applicationNumber: 'APP-202401180002',
applicantName: '李四',
type: 'business',
status: 'approved',
amount: 1000000,
term: 36,
interestRate: 5.8,
applicationTime: '2024-01-17 14:20:00',
phone: '13900139000',
idCard: '110101199002021234',
purpose: '企业经营',
remark: '',
auditRecords: [
{
id: 1,
action: 'submit',
auditor: '李四',
time: '2024-01-17 14:20:00',
comment: '提交申请'
},
{
id: 2,
action: 'approve',
auditor: '王经理',
time: '2024-01-18 10:15:00',
comment: '资料齐全,符合条件,同意放款'
}
]
},
{
id: 3,
applicationNumber: 'APP-202401180003',
applicantName: '王五',
type: 'mortgage',
status: 'rejected',
amount: 500000,
term: 120,
interestRate: 4.5,
applicationTime: '2024-01-16 16:45:00',
phone: '13700137000',
idCard: '110101199003031234',
purpose: '购房',
remark: '',
auditRecords: [
{
id: 1,
action: 'submit',
auditor: '王五',
time: '2024-01-16 16:45:00',
comment: '提交申请'
},
{
id: 2,
action: 'reject',
auditor: '赵经理',
time: '2024-01-17 11:30:00',
comment: '抵押物价值不足,不符合放款条件'
}
]
}
])
// 计算属性
const filteredApplications = computed(() => {
let result = applications.value
if (searchText.value) {
result = result.filter(app =>
app.applicantName.toLowerCase().includes(searchText.value.toLowerCase()) ||
app.applicationNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(app => app.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(app => app.type === typeFilter.value)
}
return result
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleAddApplication = () => {
message.info('新建申请功能开发中...')
}
const handleView = (record) => {
selectedApplication.value = record
detailModalVisible.value = true
}
const handleApprove = (record) => {
selectedApplication.value = record
auditForm.value.action = 'approve'
auditForm.value.comment = ''
auditModalVisible.value = true
}
const handleReject = (record) => {
selectedApplication.value = record
auditForm.value.action = 'reject'
auditForm.value.comment = ''
auditModalVisible.value = true
}
const handleAuditSubmit = () => {
if (!auditForm.value.comment) {
message.error('请输入审核意见')
return
}
// 更新申请状态
selectedApplication.value.status = auditForm.value.action === 'approve' ? 'approved' : 'rejected'
// 添加审核记录
selectedApplication.value.auditRecords.push({
id: Date.now(),
action: auditForm.value.action,
auditor: '当前用户',
time: new Date().toLocaleString(),
comment: auditForm.value.comment
})
auditModalVisible.value = false
message.success('审核完成')
}
const handleAuditCancel = () => {
auditModalVisible.value = false
selectedApplication.value = null
}
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
approved: 'green',
rejected: 'red',
processing: 'blue'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
processing: '处理中'
}
return texts[status] || status
}
const getTypeColor = (type) => {
const colors = {
personal: 'blue',
business: 'green',
mortgage: 'purple'
}
return colors[type] || 'default'
}
const getTypeText = (type) => {
const texts = {
personal: '个人贷款',
business: '企业贷款',
mortgage: '抵押贷款'
}
return texts[type] || type
}
const getProgressPercent = (status) => {
const percents = {
pending: 25,
processing: 50,
approved: 100,
rejected: 100
}
return percents[status] || 0
}
const getProgressStatus = (status) => {
if (status === 'approved') return 'success'
if (status === 'rejected') return 'exception'
return 'active'
}
const getAuditColor = (action) => {
const colors = {
submit: 'blue',
approve: 'green',
reject: 'red'
}
return colors[action] || 'default'
}
const getAuditActionText = (action) => {
const texts = {
submit: '提交申请',
approve: '审核通过',
reject: '审核拒绝'
}
return texts[action] || action
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// 生命周期
onMounted(() => {
pagination.value.total = applications.value.length
})
</script>
<style scoped>
.loan-applications {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.application-detail {
padding: 16px 0;
}
.audit-records {
margin-top: 24px;
}
.audit-records h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.audit-item {
padding: 8px 0;
}
.audit-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.audit-action {
font-weight: 600;
}
.audit-time {
color: #999;
font-size: 12px;
}
.audit-user {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.audit-comment {
color: #333;
font-size: 12px;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
}
</style>

View File

@@ -0,0 +1,716 @@
<template>
<div class="loan-contracts">
<div class="page-header">
<h1>贷款合同</h1>
<p>管理和跟踪贷款合同状态</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索合同编号或客户姓名"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="合同状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="draft">草稿</a-select-option>
<a-select-option value="pending">待签署</a-select-option>
<a-select-option value="signed">已签署</a-select-option>
<a-select-option value="active">生效中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="terminated">已终止</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="合同类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="personal">个人贷款</a-select-option>
<a-select-option value="business">企业贷款</a-select-option>
<a-select-option value="mortgage">抵押贷款</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model:value="dateRange"
@change="handleFilter"
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleCreateContract">
<PlusOutlined />
新建合同
</a-button>
</a-col>
</a-row>
</div>
<!-- 合同列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredContracts"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.amount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button type="link" size="small" @click="handleDownload(record)">
下载
</a-button>
<a-button
type="link"
size="small"
@click="handleSign(record)"
v-if="record.status === 'pending'"
>
签署
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 合同详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="合同详情"
width="900px"
:footer="null"
>
<div v-if="selectedContract" class="contract-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="合同编号">
{{ selectedContract.contractNumber }}
</a-descriptions-item>
<a-descriptions-item label="客户姓名">
{{ selectedContract.customerName }}
</a-descriptions-item>
<a-descriptions-item label="合同类型">
<a-tag :color="getTypeColor(selectedContract.type)">
{{ getTypeText(selectedContract.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="合同状态">
<a-tag :color="getStatusColor(selectedContract.status)">
{{ getStatusText(selectedContract.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="贷款金额">
{{ formatAmount(selectedContract.amount) }}
</a-descriptions-item>
<a-descriptions-item label="贷款期限">
{{ selectedContract.term }} 个月
</a-descriptions-item>
<a-descriptions-item label="年利率">
{{ selectedContract.interestRate }}%
</a-descriptions-item>
<a-descriptions-item label="还款方式">
{{ getRepaymentMethodText(selectedContract.repaymentMethod) }}
</a-descriptions-item>
<a-descriptions-item label="合同签署日期">
{{ selectedContract.signDate || '未签署' }}
</a-descriptions-item>
<a-descriptions-item label="合同生效日期">
{{ selectedContract.effectiveDate || '未生效' }}
</a-descriptions-item>
<a-descriptions-item label="到期日期">
{{ selectedContract.maturityDate }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ selectedContract.phone }}
</a-descriptions-item>
<a-descriptions-item label="身份证号">
{{ selectedContract.idCard }}
</a-descriptions-item>
<a-descriptions-item label="合同条款" :span="2">
<div class="contract-terms">
<p v-for="(term, index) in selectedContract.terms" :key="index">
{{ index + 1 }}. {{ term }}
</p>
</div>
</a-descriptions-item>
</a-descriptions>
<!-- 合同历史 -->
<div class="contract-history" v-if="selectedContract.history">
<h4>合同历史</h4>
<a-timeline>
<a-timeline-item
v-for="record in selectedContract.history"
:key="record.id"
:color="getHistoryColor(record.action)"
>
<div class="history-item">
<div class="history-header">
<span class="history-action">{{ getHistoryActionText(record.action) }}</span>
<span class="history-time">{{ record.time }}</span>
</div>
<div class="history-user">操作人{{ record.operator }}</div>
<div class="history-comment" v-if="record.comment">
备注{{ record.comment }}
</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
</div>
</a-modal>
<!-- 合同签署模态框 -->
<a-modal
v-model:open="signModalVisible"
title="合同签署"
@ok="handleSignSubmit"
@cancel="handleSignCancel"
>
<div class="sign-content">
<a-alert
message="请确认合同信息无误后签署"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-form :model="signForm" layout="vertical">
<a-form-item label="签署密码" required>
<a-input-password
v-model:value="signForm.password"
placeholder="请输入签署密码"
/>
</a-form-item>
<a-form-item label="签署备注">
<a-textarea
v-model:value="signForm.comment"
placeholder="请输入签署备注(可选)"
:rows="3"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const dateRange = ref([])
const detailModalVisible = ref(false)
const signModalVisible = ref(false)
const selectedContract = ref(null)
const signForm = ref({
password: '',
comment: ''
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '合同编号',
dataIndex: 'contractNumber',
key: 'contractNumber',
width: 150
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '贷款金额',
dataIndex: 'amount',
key: 'amount',
width: 120
},
{
title: '期限',
dataIndex: 'term',
key: 'term',
width: 80
},
{
title: '年利率',
dataIndex: 'interestRate',
key: 'interestRate',
width: 100
},
{
title: '签署日期',
dataIndex: 'signDate',
key: 'signDate',
width: 120
},
{
title: '操作',
key: 'action',
width: 250,
fixed: 'right'
}
]
// 模拟合同数据
const contracts = ref([
{
id: 1,
contractNumber: 'CON-202401180001',
customerName: '张三',
type: 'personal',
status: 'signed',
amount: 200000,
term: 24,
interestRate: 6.5,
repaymentMethod: 'equal_installment',
signDate: '2024-01-18',
effectiveDate: '2024-01-18',
maturityDate: '2026-01-18',
phone: '13800138000',
idCard: '110101199001011234',
terms: [
'借款人应按期还款,不得逾期',
'借款人应按时支付利息',
'借款人不得将贷款用于非法用途',
'借款人应配合银行进行贷后管理'
],
history: [
{
id: 1,
action: 'create',
operator: '系统',
time: '2024-01-18 09:30:00',
comment: '合同创建'
},
{
id: 2,
action: 'sign',
operator: '张三',
time: '2024-01-18 10:15:00',
comment: '客户签署合同'
}
]
},
{
id: 2,
contractNumber: 'CON-202401180002',
customerName: '李四',
type: 'business',
status: 'pending',
amount: 1000000,
term: 36,
interestRate: 5.8,
repaymentMethod: 'balloon',
signDate: null,
effectiveDate: null,
maturityDate: '2027-01-18',
phone: '13900139000',
idCard: '110101199002021234',
terms: [
'企业应按期还款,不得逾期',
'企业应按时支付利息',
'企业应提供财务报表',
'企业应配合银行进行贷后管理'
],
history: [
{
id: 1,
action: 'create',
operator: '系统',
time: '2024-01-18 14:20:00',
comment: '合同创建'
}
]
},
{
id: 3,
contractNumber: 'CON-202401180003',
customerName: '王五',
type: 'mortgage',
status: 'active',
amount: 500000,
term: 120,
interestRate: 4.5,
repaymentMethod: 'equal_installment',
signDate: '2024-01-17',
effectiveDate: '2024-01-17',
maturityDate: '2034-01-17',
phone: '13700137000',
idCard: '110101199003031234',
terms: [
'借款人应按期还款,不得逾期',
'借款人应按时支付利息',
'抵押物不得转让或处置',
'借款人应配合银行进行贷后管理'
],
history: [
{
id: 1,
action: 'create',
operator: '系统',
time: '2024-01-17 16:45:00',
comment: '合同创建'
},
{
id: 2,
action: 'sign',
operator: '王五',
time: '2024-01-17 17:30:00',
comment: '客户签署合同'
},
{
id: 3,
action: 'activate',
operator: '系统',
time: '2024-01-17 18:00:00',
comment: '合同生效'
}
]
}
])
// 计算属性
const filteredContracts = computed(() => {
let result = contracts.value
if (searchText.value) {
result = result.filter(contract =>
contract.customerName.toLowerCase().includes(searchText.value.toLowerCase()) ||
contract.contractNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(contract => contract.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(contract => contract.type === typeFilter.value)
}
return result
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleCreateContract = () => {
message.info('新建合同功能开发中...')
}
const handleView = (record) => {
selectedContract.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑合同: ${record.contractNumber}`)
}
const handleDownload = (record) => {
message.info(`下载合同: ${record.contractNumber}`)
}
const handleSign = (record) => {
selectedContract.value = record
signForm.value.password = ''
signForm.value.comment = ''
signModalVisible.value = true
}
const handleSignSubmit = () => {
if (!signForm.value.password) {
message.error('请输入签署密码')
return
}
// 更新合同状态
selectedContract.value.status = 'signed'
selectedContract.value.signDate = new Date().toISOString().split('T')[0]
selectedContract.value.effectiveDate = new Date().toISOString().split('T')[0]
// 添加历史记录
selectedContract.value.history.push({
id: Date.now(),
action: 'sign',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: signForm.value.comment || '合同签署'
})
signModalVisible.value = false
message.success('合同签署成功')
}
const handleSignCancel = () => {
signModalVisible.value = false
selectedContract.value = null
}
const getStatusColor = (status) => {
const colors = {
draft: 'default',
pending: 'orange',
signed: 'blue',
active: 'green',
completed: 'success',
terminated: 'red'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
draft: '草稿',
pending: '待签署',
signed: '已签署',
active: '生效中',
completed: '已完成',
terminated: '已终止'
}
return texts[status] || status
}
const getTypeColor = (type) => {
const colors = {
personal: 'blue',
business: 'green',
mortgage: 'purple'
}
return colors[type] || 'default'
}
const getTypeText = (type) => {
const texts = {
personal: '个人贷款',
business: '企业贷款',
mortgage: '抵押贷款'
}
return texts[type] || type
}
const getRepaymentMethodText = (method) => {
const texts = {
equal_installment: '等额本息',
equal_principal: '等额本金',
balloon: '气球贷',
interest_only: '先息后本'
}
return texts[method] || method
}
const getHistoryColor = (action) => {
const colors = {
create: 'blue',
sign: 'green',
activate: 'green',
terminate: 'red'
}
return colors[action] || 'default'
}
const getHistoryActionText = (action) => {
const texts = {
create: '合同创建',
sign: '合同签署',
activate: '合同生效',
terminate: '合同终止'
}
return texts[action] || action
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// 生命周期
onMounted(() => {
pagination.value.total = contracts.value.length
})
</script>
<style scoped>
.loan-contracts {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.contract-detail {
padding: 16px 0;
}
.contract-terms {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
}
.contract-terms p {
margin: 0 0 8px 0;
font-size: 14px;
line-height: 1.5;
}
.contract-terms p:last-child {
margin-bottom: 0;
}
.contract-history {
margin-top: 24px;
}
.contract-history h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.history-item {
padding: 8px 0;
}
.history-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.history-action {
font-weight: 600;
}
.history-time {
color: #999;
font-size: 12px;
}
.history-user {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.history-comment {
color: #333;
font-size: 12px;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
}
.sign-content {
padding: 16px 0;
}
</style>

View File

@@ -0,0 +1,469 @@
<template>
<div class="loan-products">
<div class="page-header">
<h1>贷款商品</h1>
<p>管理和配置银行贷款产品</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="8">
<a-input-search
v-model:value="searchText"
placeholder="搜索产品名称或编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="6">
<a-select
v-model:value="statusFilter"
placeholder="产品状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="active">启用</a-select-option>
<a-select-option value="inactive">停用</a-select-option>
<a-select-option value="draft">草稿</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-select
v-model:value="typeFilter"
placeholder="产品类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="personal">个人贷款</a-select-option>
<a-select-option value="business">企业贷款</a-select-option>
<a-select-option value="mortgage">抵押贷款</a-select-option>
<a-select-option value="credit">信用贷款</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleAddProduct">
<PlusOutlined />
新建产品
</a-button>
</a-col>
</a-row>
</div>
<!-- 产品列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredProducts"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeText(record.type) }}
</a-tag>
</template>
<template v-else-if="column.key === 'interestRate'">
{{ record.interestRate }}% - {{ record.maxInterestRate }}%
</template>
<template v-else-if="column.key === 'amount'">
{{ formatAmount(record.minAmount) }} - {{ formatAmount(record.maxAmount) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button type="link" size="small" @click="handleEdit(record)">
编辑
</a-button>
<a-button
type="link"
size="small"
@click="handleToggleStatus(record)"
:danger="record.status === 'active'"
>
{{ record.status === 'active' ? '停用' : '启用' }}
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 产品详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="产品详情"
width="800px"
:footer="null"
>
<div v-if="selectedProduct" class="product-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="产品名称">
{{ selectedProduct.name }}
</a-descriptions-item>
<a-descriptions-item label="产品编号">
{{ selectedProduct.code }}
</a-descriptions-item>
<a-descriptions-item label="产品类型">
<a-tag :color="getTypeColor(selectedProduct.type)">
{{ getTypeText(selectedProduct.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="产品状态">
<a-tag :color="getStatusColor(selectedProduct.status)">
{{ getStatusText(selectedProduct.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="贷款额度">
{{ formatAmount(selectedProduct.minAmount) }} - {{ formatAmount(selectedProduct.maxAmount) }}
</a-descriptions-item>
<a-descriptions-item label="贷款期限">
{{ selectedProduct.minTerm }} - {{ selectedProduct.maxTerm }} 个月
</a-descriptions-item>
<a-descriptions-item label="利率范围">
{{ selectedProduct.interestRate }}% - {{ selectedProduct.maxInterestRate }}%
</a-descriptions-item>
<a-descriptions-item label="申请条件">
{{ selectedProduct.requirements }}
</a-descriptions-item>
<a-descriptions-item label="产品描述" :span="2">
{{ selectedProduct.description }}
</a-descriptions-item>
</a-descriptions>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import { api } from '@/utils/api'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const detailModalVisible = ref(false)
const selectedProduct = ref(null)
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '产品名称',
dataIndex: 'name',
key: 'name',
width: 200
},
{
title: '产品编号',
dataIndex: 'code',
key: 'code',
width: 120
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '贷款额度',
dataIndex: 'amount',
key: 'amount',
width: 200
},
{
title: '利率范围',
dataIndex: 'interestRate',
key: 'interestRate',
width: 150
},
{
title: '期限',
dataIndex: 'term',
key: 'term',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 120
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 产品数据
const products = ref([])
// 模拟产品数据(作为备用)
const mockProducts = [
{
id: 2,
name: '个人消费贷款',
code: 'LOAN-002',
type: 'personal',
status: 'active',
minAmount: 10000,
maxAmount: 500000,
minTerm: 6,
maxTerm: 60,
interestRate: 6.8,
maxInterestRate: 12.5,
requirements: '年满18周岁有稳定收入来源信用记录良好',
description: '用于个人消费支出的信用贷款产品',
createTime: '2024-01-05'
},
{
id: 3,
name: '企业经营贷款',
code: 'LOAN-003',
type: 'business',
status: 'active',
minAmount: 500000,
maxAmount: 50000000,
minTerm: 12,
maxTerm: 120,
interestRate: 5.2,
maxInterestRate: 8.5,
requirements: '企业成立满2年年营业额达到500万以上',
description: '为企业经营发展提供的流动资金贷款',
createTime: '2024-01-10'
},
{
id: 4,
name: '小微企业贷款',
code: 'LOAN-004',
type: 'business',
status: 'draft',
minAmount: 50000,
maxAmount: 1000000,
minTerm: 6,
maxTerm: 36,
interestRate: 7.5,
maxInterestRate: 10.5,
requirements: '小微企业年营业额100万以上',
description: '专为小微企业提供的快速贷款产品',
createTime: '2024-01-15'
}
];
// 计算属性
const filteredProducts = computed(() => {
let result = products.value
if (searchText.value) {
result = result.filter(product =>
product.name.toLowerCase().includes(searchText.value.toLowerCase()) ||
product.code.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(product => product.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(product => product.type === typeFilter.value)
}
return result
})
// 方法
const handleAddProduct = () => {
message.info('新建产品功能开发中...')
}
const handleView = (record) => {
selectedProduct.value = record
detailModalVisible.value = true
}
const handleEdit = (record) => {
message.info(`编辑产品: ${record.name}`)
}
const handleToggleStatus = (record) => {
const newStatus = record.status === 'active' ? 'inactive' : 'active'
record.status = newStatus
message.success(`产品已${newStatus === 'active' ? '启用' : '停用'}`)
}
const getStatusColor = (status) => {
const colors = {
active: 'green',
inactive: 'red',
draft: 'orange'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
active: '启用',
inactive: '停用',
draft: '草稿'
}
return texts[status] || status
}
const getTypeColor = (type) => {
const colors = {
personal: 'blue',
business: 'green',
mortgage: 'purple',
credit: 'orange'
}
return colors[type] || 'default'
}
const getTypeText = (type) => {
const texts = {
personal: '个人贷款',
business: '企业贷款',
mortgage: '抵押贷款',
credit: '信用贷款'
}
return texts[type] || type
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// API调用函数
const fetchProducts = async (params = {}) => {
try {
loading.value = true
const response = await api.loanProducts.getList({
page: pagination.value.current,
limit: pagination.value.pageSize,
search: searchText.value,
status: statusFilter.value,
type: typeFilter.value,
...params
})
if (response.success) {
products.value = response.data.products || []
pagination.value.total = response.data.pagination?.total || 0
} else {
message.error(response.message || '获取产品列表失败')
// 使用模拟数据作为备用
products.value = mockProducts
pagination.value.total = mockProducts.length
}
} catch (error) {
console.error('获取产品列表失败:', error)
message.error('获取产品列表失败')
// 使用模拟数据作为备用
products.value = mockProducts
pagination.value.total = mockProducts.length
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.value.current = 1
fetchProducts()
}
const handleFilter = () => {
pagination.value.current = 1
fetchProducts()
}
const handleTableChange = (paginationInfo) => {
pagination.value = paginationInfo
fetchProducts()
}
// 生命周期
onMounted(() => {
fetchProducts()
})
</script>
<style scoped>
.loan-products {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.product-detail {
padding: 16px 0;
}
</style>

View File

@@ -0,0 +1,709 @@
<template>
<div class="loan-release">
<div class="page-header">
<h1>贷款解押</h1>
<p>管理和处理贷款抵押物解押业务</p>
</div>
<div class="content">
<!-- 搜索和筛选 -->
<div class="search-section">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchText"
placeholder="搜索客户姓名或合同编号"
enter-button="搜索"
@search="handleSearch"
/>
</a-col>
<a-col :span="4">
<a-select
v-model:value="statusFilter"
placeholder="解押状态"
allow-clear
@change="handleFilter"
>
<a-select-option value="pending">待处理</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
</a-select>
</a-col>
<a-col :span="4">
<a-select
v-model:value="typeFilter"
placeholder="抵押物类型"
allow-clear
@change="handleFilter"
>
<a-select-option value="house">房产</a-select-option>
<a-select-option value="car">车辆</a-select-option>
<a-select-option value="land">土地</a-select-option>
<a-select-option value="equipment">设备</a-select-option>
</a-select>
</a-col>
<a-col :span="6">
<a-range-picker
v-model:value="dateRange"
@change="handleFilter"
/>
</a-col>
<a-col :span="4">
<a-button type="primary" @click="handleCreateRelease">
<PlusOutlined />
新建解押
</a-button>
</a-col>
</a-row>
</div>
<!-- 解押列表 -->
<div class="table-section">
<a-table
:columns="columns"
:data-source="filteredReleases"
:pagination="pagination"
:loading="loading"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'collateralType'">
<a-tag :color="getCollateralTypeColor(record.collateralType)">
{{ getCollateralTypeText(record.collateralType) }}
</a-tag>
</template>
<template v-else-if="column.key === 'loanAmount'">
{{ formatAmount(record.loanAmount) }}
</template>
<template v-else-if="column.key === 'collateralValue'">
{{ formatAmount(record.collateralValue) }}
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="handleView(record)">
查看
</a-button>
<a-button
type="link"
size="small"
@click="handleProcess(record)"
v-if="record.status === 'pending'"
>
处理
</a-button>
<a-button
type="link"
size="small"
@click="handleComplete(record)"
v-if="record.status === 'processing'"
>
完成
</a-button>
<a-button
type="link"
size="small"
@click="handleReject(record)"
v-if="record.status === 'pending'"
danger
>
拒绝
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</div>
<!-- 解押详情模态框 -->
<a-modal
v-model:open="detailModalVisible"
title="解押详情"
width="800px"
:footer="null"
>
<div v-if="selectedRelease" class="release-detail">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="解押编号">
{{ selectedRelease.releaseNumber }}
</a-descriptions-item>
<a-descriptions-item label="客户姓名">
{{ selectedRelease.customerName }}
</a-descriptions-item>
<a-descriptions-item label="合同编号">
{{ selectedRelease.contractNumber }}
</a-descriptions-item>
<a-descriptions-item label="解押状态">
<a-tag :color="getStatusColor(selectedRelease.status)">
{{ getStatusText(selectedRelease.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="抵押物类型">
<a-tag :color="getCollateralTypeColor(selectedRelease.collateralType)">
{{ getCollateralTypeText(selectedRelease.collateralType) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="抵押物描述">
{{ selectedRelease.collateralDescription }}
</a-descriptions-item>
<a-descriptions-item label="贷款金额">
{{ formatAmount(selectedRelease.loanAmount) }}
</a-descriptions-item>
<a-descriptions-item label="抵押物价值">
{{ formatAmount(selectedRelease.collateralValue) }}
</a-descriptions-item>
<a-descriptions-item label="申请时间">
{{ selectedRelease.applicationTime }}
</a-descriptions-item>
<a-descriptions-item label="处理时间">
{{ selectedRelease.processTime || '未处理' }}
</a-descriptions-item>
<a-descriptions-item label="完成时间">
{{ selectedRelease.completeTime || '未完成' }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ selectedRelease.phone }}
</a-descriptions-item>
<a-descriptions-item label="身份证号">
{{ selectedRelease.idCard }}
</a-descriptions-item>
<a-descriptions-item label="申请原因" :span="2">
{{ selectedRelease.reason }}
</a-descriptions-item>
<a-descriptions-item label="备注" :span="2">
{{ selectedRelease.remark || '无' }}
</a-descriptions-item>
</a-descriptions>
<!-- 解押历史 -->
<div class="release-history" v-if="selectedRelease.history">
<h4>解押历史</h4>
<a-timeline>
<a-timeline-item
v-for="record in selectedRelease.history"
:key="record.id"
:color="getHistoryColor(record.action)"
>
<div class="history-item">
<div class="history-header">
<span class="history-action">{{ getHistoryActionText(record.action) }}</span>
<span class="history-time">{{ record.time }}</span>
</div>
<div class="history-user">操作人{{ record.operator }}</div>
<div class="history-comment" v-if="record.comment">
备注{{ record.comment }}
</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
</div>
</a-modal>
<!-- 处理解押模态框 -->
<a-modal
v-model:open="processModalVisible"
title="处理解押申请"
@ok="handleProcessSubmit"
@cancel="handleProcessCancel"
>
<div class="process-content">
<a-form :model="processForm" layout="vertical">
<a-form-item label="处理结果" required>
<a-radio-group v-model:value="processForm.result">
<a-radio value="approve">同意解押</a-radio>
<a-radio value="reject">拒绝解押</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="处理意见" required>
<a-textarea
v-model:value="processForm.comment"
placeholder="请输入处理意见"
:rows="4"
/>
</a-form-item>
<a-form-item label="备注">
<a-textarea
v-model:value="processForm.remark"
placeholder="请输入备注(可选)"
:rows="2"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
// 响应式数据
const loading = ref(false)
const searchText = ref('')
const statusFilter = ref(undefined)
const typeFilter = ref(undefined)
const dateRange = ref([])
const detailModalVisible = ref(false)
const processModalVisible = ref(false)
const selectedRelease = ref(null)
const processForm = ref({
result: 'approve',
comment: '',
remark: ''
})
// 分页配置
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `${range[0]}-${range[1]} 条,共 ${total}`
})
// 表格列配置
const columns = [
{
title: '解押编号',
dataIndex: 'releaseNumber',
key: 'releaseNumber',
width: 150
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
width: 120
},
{
title: '合同编号',
dataIndex: 'contractNumber',
key: 'contractNumber',
width: 150
},
{
title: '抵押物类型',
dataIndex: 'collateralType',
key: 'collateralType',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '贷款金额',
dataIndex: 'loanAmount',
key: 'loanAmount',
width: 120
},
{
title: '抵押物价值',
dataIndex: 'collateralValue',
key: 'collateralValue',
width: 120
},
{
title: '申请时间',
dataIndex: 'applicationTime',
key: 'applicationTime',
width: 150
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
// 模拟解押数据
const releases = ref([
{
id: 1,
releaseNumber: 'REL-202401180001',
customerName: '张三',
contractNumber: 'CON-202401180001',
collateralType: 'house',
status: 'pending',
collateralDescription: '北京市朝阳区某小区3室2厅建筑面积120平米',
loanAmount: 200000,
collateralValue: 500000,
applicationTime: '2024-01-18 09:30:00',
processTime: null,
completeTime: null,
phone: '13800138000',
idCard: '110101199001011234',
reason: '贷款已还清,申请解押房产',
remark: '',
history: [
{
id: 1,
action: 'apply',
operator: '张三',
time: '2024-01-18 09:30:00',
comment: '提交解押申请'
}
]
},
{
id: 2,
releaseNumber: 'REL-202401180002',
customerName: '李四',
contractNumber: 'CON-202401180002',
collateralType: 'car',
status: 'processing',
collateralDescription: '2020年宝马X5车牌号京A12345',
loanAmount: 500000,
collateralValue: 600000,
applicationTime: '2024-01-17 14:20:00',
processTime: '2024-01-18 10:15:00',
completeTime: null,
phone: '13900139000',
idCard: '110101199002021234',
reason: '车辆贷款已还清,申请解押车辆',
remark: '',
history: [
{
id: 1,
action: 'apply',
operator: '李四',
time: '2024-01-17 14:20:00',
comment: '提交解押申请'
},
{
id: 2,
action: 'process',
operator: '王经理',
time: '2024-01-18 10:15:00',
comment: '开始处理解押申请'
}
]
},
{
id: 3,
releaseNumber: 'REL-202401180003',
customerName: '王五',
contractNumber: 'CON-202401180003',
collateralType: 'land',
status: 'completed',
collateralDescription: '北京市海淀区某地块面积500平米',
loanAmount: 1000000,
collateralValue: 2000000,
applicationTime: '2024-01-16 16:45:00',
processTime: '2024-01-17 09:30:00',
completeTime: '2024-01-17 15:20:00',
phone: '13700137000',
idCard: '110101199003031234',
reason: '土地贷款已还清,申请解押土地',
remark: '',
history: [
{
id: 1,
action: 'apply',
operator: '王五',
time: '2024-01-16 16:45:00',
comment: '提交解押申请'
},
{
id: 2,
action: 'process',
operator: '赵经理',
time: '2024-01-17 09:30:00',
comment: '开始处理解押申请'
},
{
id: 3,
action: 'complete',
operator: '赵经理',
time: '2024-01-17 15:20:00',
comment: '解押手续办理完成'
}
]
}
])
// 计算属性
const filteredReleases = computed(() => {
let result = releases.value
if (searchText.value) {
result = result.filter(release =>
release.customerName.toLowerCase().includes(searchText.value.toLowerCase()) ||
release.contractNumber.toLowerCase().includes(searchText.value.toLowerCase())
)
}
if (statusFilter.value) {
result = result.filter(release => release.status === statusFilter.value)
}
if (typeFilter.value) {
result = result.filter(release => release.collateralType === typeFilter.value)
}
return result
})
// 方法
const handleSearch = () => {
// 搜索逻辑已在计算属性中处理
}
const handleFilter = () => {
// 筛选逻辑已在计算属性中处理
}
const handleTableChange = (pag) => {
pagination.value.current = pag.current
pagination.value.pageSize = pag.pageSize
}
const handleCreateRelease = () => {
message.info('新建解押功能开发中...')
}
const handleView = (record) => {
selectedRelease.value = record
detailModalVisible.value = true
}
const handleProcess = (record) => {
selectedRelease.value = record
processForm.value.result = 'approve'
processForm.value.comment = ''
processForm.value.remark = ''
processModalVisible.value = true
}
const handleComplete = (record) => {
record.status = 'completed'
record.completeTime = new Date().toLocaleString()
record.history.push({
id: Date.now(),
action: 'complete',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: '解押手续办理完成'
})
message.success('解押完成')
}
const handleReject = (record) => {
record.status = 'rejected'
record.processTime = new Date().toLocaleString()
record.history.push({
id: Date.now(),
action: 'reject',
operator: '当前用户',
time: new Date().toLocaleString(),
comment: '解押申请被拒绝'
})
message.success('解押申请已拒绝')
}
const handleProcessSubmit = () => {
if (!processForm.value.comment) {
message.error('请输入处理意见')
return
}
const newStatus = processForm.value.result === 'approve' ? 'processing' : 'rejected'
selectedRelease.value.status = newStatus
selectedRelease.value.processTime = new Date().toLocaleString()
selectedRelease.value.history.push({
id: Date.now(),
action: processForm.value.result,
operator: '当前用户',
time: new Date().toLocaleString(),
comment: processForm.value.comment
})
processModalVisible.value = false
message.success('处理完成')
}
const handleProcessCancel = () => {
processModalVisible.value = false
selectedRelease.value = null
}
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
processing: 'blue',
completed: 'green',
rejected: 'red'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
rejected: '已拒绝'
}
return texts[status] || status
}
const getCollateralTypeColor = (type) => {
const colors = {
house: 'blue',
car: 'green',
land: 'orange',
equipment: 'purple'
}
return colors[type] || 'default'
}
const getCollateralTypeText = (type) => {
const texts = {
house: '房产',
car: '车辆',
land: '土地',
equipment: '设备'
}
return texts[type] || type
}
const getHistoryColor = (action) => {
const colors = {
apply: 'blue',
process: 'orange',
complete: 'green',
reject: 'red'
}
return colors[action] || 'default'
}
const getHistoryActionText = (action) => {
const texts = {
apply: '提交申请',
process: '开始处理',
complete: '处理完成',
reject: '申请拒绝'
}
return texts[action] || action
}
const formatAmount = (amount) => {
if (amount >= 10000) {
return (amount / 10000).toFixed(0) + '万'
}
return amount.toString()
}
// 生命周期
onMounted(() => {
pagination.value.total = releases.value.length
})
</script>
<style scoped>
.loan-release {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: #666;
font-size: 14px;
}
.content {
background: #fff;
border-radius: 8px;
padding: 24px;
}
.search-section {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.table-section {
margin-top: 16px;
}
.release-detail {
padding: 16px 0;
}
.release-history {
margin-top: 24px;
}
.release-history h4 {
margin-bottom: 16px;
font-size: 16px;
font-weight: 600;
}
.history-item {
padding: 8px 0;
}
.history-header {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.history-action {
font-weight: 600;
}
.history-time {
color: #999;
font-size: 12px;
}
.history-user {
color: #666;
font-size: 12px;
margin-bottom: 4px;
}
.history-comment {
color: #333;
font-size: 12px;
background: #f5f5f5;
padding: 8px;
border-radius: 4px;
}
.process-content {
padding: 16px 0;
}
</style>