保险前后端,养殖端和保险端小程序

This commit is contained in:
xuqiuyun
2025-09-17 19:01:52 +08:00
parent e4287b83fe
commit 473891163c
218 changed files with 109331 additions and 14103 deletions

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>保险端口后台管理系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

4380
insurance_admin-system/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
{
"name": "insurance-admin-system",
"version": "1.0.0",
"description": "保险端口后台管理系统",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"pinia": "^2.1.6",
"ant-design-vue": "^4.0.0",
"axios": "^1.4.0",
"@ant-design/icons-vue": "^6.1.0",
"echarts": "^5.4.2",
"vue-echarts": "^6.5.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.4.5",
"eslint": "^8.45.0",
"eslint-plugin-vue": "^9.15.1"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,26 @@
<template>
<a-config-provider :locale="zhCN">
<router-view />
</a-config-provider>
</template>
<script setup>
import zhCN from 'ant-design-vue/es/locale/zh_CN'
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
background-color: #f5f5f5;
}
#app {
min-height: 100vh;
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<a-layout style="min-height: 100vh">
<!-- 侧边栏 -->
<a-layout-sider v-model:collapsed="collapsed" collapsible>
<div class="logo">
<h2 v-if="!collapsed">保险管理系统</h2>
<h2 v-else>保险</h2>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
:items="menuItems"
@click="handleMenuClick"
/>
</a-layout-sider>
<!-- 主内容区 -->
<a-layout>
<!-- 头部 -->
<a-layout-header style="background: #fff; padding: 0 16px; display: flex; justify-content: space-between; align-items: center">
<div>
<menu-unfold-outlined
v-if="collapsed"
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
<menu-fold-outlined
v-else
class="trigger"
@click="() => (collapsed = !collapsed)"
/>
</div>
<div>
<a-dropdown>
<a class="ant-dropdown-link" @click.prevent>
<user-outlined />
{{ userStore.userInfo.real_name || '管理员' }}
<down-outlined />
</a>
<template #overlay>
<a-menu>
<a-menu-item key="profile">
<user-outlined />
个人中心
</a-menu-item>
<a-menu-item key="logout" @click="handleLogout">
<logout-outlined />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<!-- 内容区 -->
<a-layout-content style="margin: 16px">
<div :style="{ padding: '24px', background: '#fff', minHeight: '360px' }">
<router-view />
</div>
</a-layout-content>
<!-- 底部 -->
<a-layout-footer style="text-align: center">
保险端口后台管理系统 ©2024
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script setup>
import { ref, computed, h } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import {
MenuUnfoldOutlined,
MenuFoldOutlined,
UserOutlined,
DownOutlined,
LogoutOutlined,
DashboardOutlined,
UserSwitchOutlined,
InsuranceOutlined,
FileTextOutlined,
FileDoneOutlined,
SafetyCertificateOutlined
} from '@ant-design/icons-vue'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const collapsed = ref(false)
const selectedKeys = ref([route.name])
const menuItems = computed(() => [
{
key: 'Dashboard',
icon: () => h(DashboardOutlined),
label: '仪表板',
title: '仪表板'
},
{
key: 'UserManagement',
icon: () => h(UserSwitchOutlined),
label: '用户管理',
title: '用户管理'
},
{
key: 'InsuranceTypeManagement',
icon: () => h(InsuranceOutlined),
label: '保险类型管理',
title: '保险类型管理'
},
{
key: 'ApplicationManagement',
icon: () => h(FileTextOutlined),
label: '保险申请管理',
title: '保险申请管理'
},
{
key: 'PolicyManagement',
icon: () => h(FileDoneOutlined),
label: '保单管理',
title: '保单管理'
},
{
key: 'ClaimManagement',
icon: () => h(SafetyCertificateOutlined),
label: '理赔管理',
title: '理赔管理'
}
])
const handleMenuClick = ({ key }) => {
router.push({ name: key })
}
const handleLogout = () => {
userStore.logout()
router.push('/login')
}
</script>
<style scoped>
.logo {
height: 32px;
margin: 16px;
color: white;
text-align: center;
}
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
</style>

View File

@@ -0,0 +1,14 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './stores'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
const app = createApp(App)
app.use(router)
app.use(store)
app.use(Antd)
app.mount('#app')

View File

@@ -0,0 +1,67 @@
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/components/Layout.vue'
import Login from '@/views/Login.vue'
import Dashboard from '@/views/Dashboard.vue'
import UserManagement from '@/views/UserManagement.vue'
import InsuranceTypeManagement from '@/views/InsuranceTypeManagement.vue'
import ApplicationManagement from '@/views/ApplicationManagement.vue'
import PolicyManagement from '@/views/PolicyManagement.vue'
import ClaimManagement from '@/views/ClaimManagement.vue'
const routes = [
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: Dashboard,
meta: { title: '仪表板' }
},
{
path: 'users',
name: 'UserManagement',
component: UserManagement,
meta: { title: '用户管理' }
},
{
path: 'insurance-types',
name: 'InsuranceTypeManagement',
component: InsuranceTypeManagement,
meta: { title: '保险类型管理' }
},
{
path: 'applications',
name: 'ApplicationManagement',
component: ApplicationManagement,
meta: { title: '保险申请管理' }
},
{
path: 'policies',
name: 'PolicyManagement',
component: PolicyManagement,
meta: { title: '保单管理' }
},
{
path: 'claims',
name: 'ClaimManagement',
component: ClaimManagement,
meta: { title: '理赔管理' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

View File

@@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
export default createPinia()

View File

@@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token'))
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}'))
const setToken = (newToken) => {
token.value = newToken
localStorage.setItem('token', newToken)
}
const setUserInfo = (info) => {
userInfo.value = info
localStorage.setItem('userInfo', JSON.stringify(info))
}
const logout = () => {
token.value = null
userInfo.value = {}
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
}
return {
token,
userInfo,
setToken,
setUserInfo,
logout
}
})

View File

@@ -0,0 +1,87 @@
import axios from 'axios'
import { useUserStore } from '@/stores/user'
// 创建axios实例
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.logout()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
// API接口
export const authAPI = {
login: (data) => api.post('/auth/login', data),
logout: () => api.post('/auth/logout'),
getProfile: () => api.get('/auth/profile')
}
export const userAPI = {
getList: (params) => api.get('/users', { params }),
create: (data) => api.post('/users', data),
update: (id, data) => api.put(`/users/${id}`, data),
delete: (id) => api.delete(`/users/${id}`)
}
export const insuranceTypeAPI = {
getList: (params) => api.get('/insurance-types', { params }),
create: (data) => api.post('/insurance-types', data),
update: (id, data) => api.put(`/insurance-types/${id}`, data),
delete: (id) => api.delete(`/insurance-types/${id}`),
updateStatus: (id, data) => api.patch(`/insurance-types/${id}/status`, data)
}
export const applicationAPI = {
getList: (params) => api.get('/insurance/applications', { params }),
getDetail: (id) => api.get(`/insurance/applications/${id}`),
updateStatus: (id, data) => api.put(`/insurance/applications/${id}/status`, data),
delete: (id) => api.delete(`/insurance/applications/${id}`)
}
export const policyAPI = {
getList: (params) => api.get('/policies', { params }),
getDetail: (id) => api.get(`/policies/${id}`),
updateStatus: (id, data) => api.put(`/policies/${id}/status`, data),
delete: (id) => api.delete(`/policies/${id}`)
}
export const claimAPI = {
getList: (params) => api.get('/claims', { params }),
getDetail: (id) => api.get(`/claims/${id}`),
updateStatus: (id, data) => api.put(`/claims/${id}/status`, data),
delete: (id) => api.delete(`/claims/${id}`)
}
export const dashboardAPI = {
getStats: () => api.get('/system/stats'),
getRecentActivities: () => api.get('/system/logs?limit=10')
}
export default api

View File

@@ -0,0 +1,570 @@
<template>
<div class="application-management">
<a-page-header
title="投保申请管理"
sub-title="管理用户的保险投保申请"
/>
<a-card>
<!-- 搜索区域 -->
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
<a-form-item label="申请人">
<a-input
v-model:value="searchForm.applicant_name"
placeholder="请输入申请人姓名"
style="width: 120px"
/>
</a-form-item>
<a-form-item label="保险类型">
<a-select
v-model:value="searchForm.insurance_type_id"
placeholder="请选择保险类型"
style="width: 120px"
allow-clear
>
<a-select-option v-for="type in insuranceTypes" :key="type.id" :value="type.id">
{{ type.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="申请状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 120px"
allow-clear
>
<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>
</a-form-item>
<a-form-item label="申请时间">
<a-range-picker
v-model:value="searchForm.timeRange"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
style="width: 320px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">查询</a-button>
<a-button @click="resetSearch">重置</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<a-card style="margin-top: 16px">
<!-- 操作按钮 -->
<div style="margin-bottom: 16px">
<a-space>
<a-button type="primary" @click="exportApplications" :loading="exportLoading">
导出申请
</a-button>
<a-button @click="refreshApplications">刷新</a-button>
<a-tag color="blue">
{{ pagination.total }} 条申请
</a-tag>
</a-space>
</div>
<!-- 申请表格 -->
<a-table
:columns="columns"
:data-source="applicationList"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:scroll="{ x: 1200 }"
>
<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 === 'insurance_type_id'">
{{ getInsuranceTypeName(record.insurance_type_id) }}
</template>
<!-- 申请时间 -->
<template v-else-if="column.key === 'created_at'">
{{ formatDateTime(record.created_at) }}
</template>
<!-- 操作 -->
<template v-else-if="column.key === 'action'">
<a-space>
<a-button
size="small"
@click="showApplicationDetail(record)"
>
详情
</a-button>
<a-button
size="small"
type="primary"
@click="approveApplication(record)"
v-if="record.status === 'pending'"
>
通过
</a-button>
<a-button
size="small"
danger
@click="rejectApplication(record)"
v-if="record.status === 'pending'"
>
拒绝
</a-button>
<a-popconfirm
title="确定要删除这个申请吗?"
@confirm="deleteApplication(record)"
>
<a-button size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 申请详情模态框 -->
<a-modal
v-model:visible="detailVisible"
:title="`申请详情 - ${currentApplication?.application_number || ''}`"
width="800px"
:footer="null"
>
<a-descriptions bordered :column="2" v-if="currentApplication">
<a-descriptions-item label="申请编号">
{{ currentApplication.application_number }}
</a-descriptions-item>
<a-descriptions-item label="申请人">
{{ currentApplication.applicant_name }}
</a-descriptions-item>
<a-descriptions-item label="联系电话">
{{ currentApplication.phone }}
</a-descriptions-item>
<a-descriptions-item label="电子邮箱">
{{ currentApplication.email }}
</a-descriptions-item>
<a-descriptions-item label="保险类型">
{{ getInsuranceTypeName(currentApplication.insurance_type_id) }}
</a-descriptions-item>
<a-descriptions-item label="保额">
{{ currentApplication.coverage_amount }}
</a-descriptions-item>
<a-descriptions-item label="保险期限">
{{ currentApplication.insurance_period }}
</a-descriptions-item>
<a-descriptions-item label="申请状态">
<a-tag :color="getStatusColor(currentApplication.status)">
{{ getStatusText(currentApplication.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="申请时间">
{{ formatDateTime(currentApplication.created_at) }}
</a-descriptions-item>
<a-descriptions-item label="审核时间" v-if="currentApplication.reviewed_at">
{{ formatDateTime(currentApplication.reviewed_at) }}
</a-descriptions-item>
<a-descriptions-item label="审核人" v-if="currentApplication.reviewer">
{{ currentApplication.reviewer }}
</a-descriptions-item>
<a-descriptions-item label="拒绝原因" v-if="currentApplication.reject_reason">
{{ currentApplication.reject_reason }}
</a-descriptions-item>
<a-descriptions-item label="备注信息" :span="2">
{{ currentApplication.notes || '无' }}
</a-descriptions-item>
<a-descriptions-item label="申请人信息" :span="2">
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; overflow: auto;">
{{ JSON.stringify(JSON.parse(currentApplication.applicant_info || '{}'), null, 2) }}
</pre>
</a-descriptions-item>
</a-descriptions>
</a-modal>
<!-- 审核模态框 -->
<a-modal
v-model:visible="reviewVisible"
:title="`审核申请 - ${currentApplication?.application_number || ''}`"
width="500px"
:confirm-loading="reviewLoading"
@ok="handleReview"
>
<a-form layout="vertical" :model="reviewForm" v-if="currentApplication">
<a-form-item
label="审核结果"
name="status"
:rules="[{ required: true, message: '请选择审核结果' }]"
>
<a-radio-group v-model:value="reviewForm.status">
<a-radio value="approved">通过</a-radio>
<a-radio value="rejected">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
label="拒绝原因"
name="reject_reason"
v-if="reviewForm.status === 'rejected'"
:rules="[{ required: reviewForm.status === 'rejected', message: '请输入拒绝原因' }]"
>
<a-textarea
v-model:value="reviewForm.reject_reason"
placeholder="请输入拒绝原因"
:rows="3"
/>
</a-form-item>
<a-form-item label="备注信息" name="notes">
<a-textarea
v-model:value="reviewForm.notes"
placeholder="请输入备注信息"
:rows="2"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
const searchForm = reactive({
applicant_name: '',
insurance_type_id: undefined,
status: undefined,
timeRange: []
})
const loading = ref(false)
const exportLoading = ref(false)
const detailVisible = ref(false)
const reviewVisible = ref(false)
const reviewLoading = ref(false)
const currentApplication = ref(null)
const applicationList = ref([
{
id: 1,
application_number: 'APP202401200001',
applicant_name: '张三',
phone: '13800138000',
email: 'zhangsan@example.com',
insurance_type_id: 1,
coverage_amount: 100000,
insurance_period: 10,
status: 'pending',
created_at: '2024-01-20 10:30:25',
applicant_info: '{"age": 30, "gender": "male", "occupation": "engineer"}',
notes: '首次申请'
},
{
id: 2,
application_number: 'APP202401200002',
applicant_name: '李四',
phone: '13900139000',
email: 'lisi@example.com',
insurance_type_id: 2,
coverage_amount: 500000,
insurance_period: 20,
status: 'approved',
created_at: '2024-01-20 09:15:18',
reviewed_at: '2024-01-20 10:00:00',
reviewer: 'admin',
applicant_info: '{"age": 35, "gender": "female", "occupation": "teacher"}',
notes: '优质客户'
},
{
id: 3,
application_number: 'APP202401200003',
applicant_name: '王五',
phone: '13700137000',
email: 'wangwu@example.com',
insurance_type_id: 1,
coverage_amount: 200000,
insurance_period: 15,
status: 'rejected',
created_at: '2024-01-20 08:45:30',
reviewed_at: '2024-01-20 09:30:00',
reviewer: 'admin',
reject_reason: '健康状况不符合要求',
applicant_info: '{"age": 45, "gender": "male", "occupation": "business"}',
notes: '健康告知不完整'
}
])
const insuranceTypes = ref([
{ id: 1, name: '人寿保险', code: 'life' },
{ id: 2, name: '健康保险', code: 'health' },
{ id: 3, name: '意外保险', code: 'accident' },
{ id: 4, name: '财产保险', code: 'property' }
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 3,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total}`
})
const reviewForm = reactive({
status: 'approved',
reject_reason: '',
notes: ''
})
const columns = [
{
title: '申请编号',
dataIndex: 'application_number',
key: 'application_number',
width: 140
},
{
title: '申请人',
dataIndex: 'applicant_name',
key: 'applicant_name',
width: 100
},
{
title: '联系电话',
dataIndex: 'phone',
key: 'phone',
width: 120
},
{
title: '保险类型',
key: 'insurance_type_id',
dataIndex: 'insurance_type_id',
width: 100
},
{
title: '保额(元)',
dataIndex: 'coverage_amount',
key: 'coverage_amount',
width: 100,
align: 'right'
},
{
title: '保险期限',
dataIndex: 'insurance_period',
key: 'insurance_period',
width: 100,
align: 'center'
},
{
title: '申请状态',
key: 'status',
dataIndex: 'status',
width: 100
},
{
title: '申请时间',
key: 'created_at',
dataIndex: 'created_at',
width: 150
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
approved: 'green',
rejected: 'red'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝'
}
return texts[status] || status
}
const getInsuranceTypeName = (typeId) => {
const type = insuranceTypes.value.find(t => t.id === typeId)
return type ? type.name : '未知类型'
}
const formatDateTime = (datetime) => {
if (!datetime) return ''
return datetime.replace('T', ' ').substring(0, 19)
}
const handleSearch = () => {
pagination.current = 1
loadApplications()
}
const resetSearch = () => {
Object.assign(searchForm, {
applicant_name: '',
insurance_type_id: undefined,
status: undefined,
timeRange: []
})
pagination.current = 1
loadApplications()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadApplications()
}
const loadApplications = async () => {
loading.value = true
try {
// const params = {
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm,
// start_time: searchForm.timeRange?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
// end_time: searchForm.timeRange?.[1]?.format('YYYY-MM-DD HH:mm:ss')
// }
// const response = await applicationAPI.getApplications(params)
// applicationList.value = response.data.list
// pagination.total = response.data.total
// 模拟数据过滤
const filteredApps = applicationList.value.filter(app => {
if (searchForm.applicant_name && !app.applicant_name.includes(searchForm.applicant_name)) return false
if (searchForm.insurance_type_id && app.insurance_type_id !== searchForm.insurance_type_id) return false
if (searchForm.status && app.status !== searchForm.status) return false
return true
})
applicationList.value = filteredApps
pagination.total = filteredApps.length
} catch (error) {
message.error('加载申请列表失败')
} finally {
loading.value = false
}
}
const refreshApplications = () => {
loadApplications()
}
const exportApplications = async () => {
exportLoading.value = true
try {
// await applicationAPI.exportApplications(searchForm)
message.success('导出任务已开始,请稍后查看下载')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
}
const showApplicationDetail = (application) => {
currentApplication.value = application
detailVisible.value = true
}
const approveApplication = (application) => {
currentApplication.value = application
reviewForm.status = 'approved'
reviewForm.reject_reason = ''
reviewForm.notes = ''
reviewVisible.value = true
}
const rejectApplication = (application) => {
currentApplication.value = application
reviewForm.status = 'rejected'
reviewForm.reject_reason = ''
reviewForm.notes = ''
reviewVisible.value = true
}
const handleReview = async () => {
reviewLoading.value = true
try {
// await applicationAPI.reviewApplication(currentApplication.value.id, reviewForm)
message.success('审核完成')
reviewVisible.value = false
loadApplications()
} catch (error) {
message.error('审核失败')
} finally {
reviewLoading.value = false
}
}
const deleteApplication = async (application) => {
try {
// await applicationAPI.deleteApplication(application.id)
message.success('删除成功')
loadApplications()
} catch (error) {
message.error('删除失败')
}
}
onMounted(() => {
loadApplications()
// 加载保险类型
loadInsuranceTypes()
})
const loadInsuranceTypes = async () => {
try {
// const response = await insuranceTypeAPI.getInsuranceTypes()
// insuranceTypes.value = response.data
} catch (error) {
message.error('加载保险类型失败')
}
}
</script>
<style scoped>
.application-management {
padding: 0;
}
:deep(.ant-descriptions-item-label) {
width: 100px;
font-weight: 600;
}
:deep(.ant-descriptions-item-content) {
word-break: break-all;
}
:deep(pre) {
margin: 0;
font-size: 12px;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,751 @@
<template>
<div class="claim-management">
<a-page-header
title="理赔管理"
sub-title="管理系统所有理赔申请"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增理赔
</a-button>
</template>
</a-page-header>
<!-- 搜索区域 -->
<a-card style="margin-top: 16px">
<a-form layout="inline" :model="searchForm">
<a-form-item label="理赔单号">
<a-input
v-model:value="searchForm.claim_number"
placeholder="请输入理赔单号"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="保单号">
<a-input
v-model:value="searchForm.policy_number"
placeholder="请输入保单号"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="申请人">
<a-input
v-model:value="searchForm.applicant_name"
placeholder="请输入申请人姓名"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 120px"
>
<a-select-option value="">全部</a-select-option>
<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-option value="completed">已完成</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">
<redo-outlined />
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 理赔表格 -->
<a-card style="margin-top: 16px">
<a-table
:columns="columns"
:data-source="claimList"
:loading="loading"
:pagination="pagination"
@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 === 'claim_amount'">
<span>¥{{ record.claim_amount?.toLocaleString() }}</span>
</template>
<template v-else-if="column.key === 'approved_amount'">
<span v-if="record.approved_amount">¥{{ record.approved_amount?.toLocaleString() }}</span>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" @click="handleView(record)">查看</a-button>
<a-button
size="small"
:type="record.status === 'pending' ? 'primary' : 'default'"
@click="handleProcess(record)"
:disabled="record.status !== 'pending'"
>
处理
</a-button>
<a-dropdown>
<a-button size="small">更多</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleEdit(record)">编辑</a-menu-item>
<a-menu-item @click="handleApprove(record)" :disabled="record.status !== 'pending'">
通过
</a-menu-item>
<a-menu-item @click="handleReject(record)" :disabled="record.status !== 'pending'">
拒绝
</a-menu-item>
<a-menu-item danger @click="handleDelete(record.id)">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 查看详情模态框 -->
<a-modal
v-model:visible="detailVisible"
title="理赔详情"
width="800px"
:footer="null"
>
<a-descriptions
v-if="currentClaim"
title="基本信息"
bordered
:column="2"
>
<a-descriptions-item label="理赔单号">{{ currentClaim.claim_number }}</a-descriptions-item>
<a-descriptions-item label="保单号">{{ currentClaim.policy_number }}</a-descriptions-item>
<a-descriptions-item label="申请人">{{ currentClaim.applicant_name }}</a-descriptions-item>
<a-descriptions-item label="联系电话">{{ currentClaim.phone }}</a-descriptions-item>
<a-descriptions-item label="申请金额">¥{{ currentClaim.claim_amount?.toLocaleString() }}</a-descriptions-item>
<a-descriptions-item label="审核金额" v-if="currentClaim.approved_amount">
¥{{ currentClaim.approved_amount?.toLocaleString() }}
</a-descriptions-item>
<a-descriptions-item label="申请时间">{{ currentClaim.apply_date }}</a-descriptions-item>
<a-descriptions-item label="事故时间">{{ currentClaim.accident_date }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(currentClaim.status)">
{{ getStatusText(currentClaim.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="审核人">{{ currentClaim.reviewer_name || '-' }}</a-descriptions-item>
<a-descriptions-item label="审核时间">{{ currentClaim.review_date || '-' }}</a-descriptions-item>
<a-descriptions-item label="拒绝原因" :span="2" v-if="currentClaim.reject_reason">
{{ currentClaim.reject_reason }}
</a-descriptions-item>
</a-descriptions>
<a-divider />
<a-descriptions
title="事故详情"
bordered
:column="1"
>
<a-descriptions-item label="事故描述">
{{ currentClaim.accident_description }}
</a-descriptions-item>
<a-descriptions-item label="处理过程">
{{ currentClaim.process_description || '暂无处理过程' }}
</a-descriptions-item>
</a-descriptions>
<a-divider />
<a-descriptions
title="相关文档"
bordered
:column="1"
>
<a-descriptions-item label="附件列表">
<div v-if="currentClaim.documents && currentClaim.documents.length > 0">
<a-space direction="vertical" style="width: 100%">
<div v-for="(doc, index) in currentClaim.documents" :key="index" class="document-item">
<file-text-outlined />
<a :href="doc.url" target="_blank">{{ doc.name }}</a>
<span class="document-size">({{ doc.size }})</span>
</div>
</a-space>
</div>
<span v-else>暂无附件</span>
</a-descriptions-item>
</a-descriptions>
</a-modal>
<!-- 处理理赔模态框 -->
<a-modal
v-model:visible="processVisible"
title="处理理赔申请"
width="600px"
@ok="handleProcessOk"
@cancel="handleProcessCancel"
>
<a-form
ref="processFormRef"
:model="processForm"
:rules="processRules"
layout="vertical"
>
<a-form-item label="审核金额" name="approved_amount">
<a-input-number
v-model:value="processForm.approved_amount"
:min="0"
:max="currentClaim?.claim_amount"
:step="100"
style="width: 100%"
placeholder="请输入审核金额"
/>
</a-form-item>
<a-form-item label="处理结果" name="status">
<a-radio-group v-model:value="processForm.status">
<a-radio value="approved">通过</a-radio>
<a-radio value="rejected">拒绝</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="processForm.status === 'rejected'"
label="拒绝原因"
name="reject_reason"
>
<a-textarea
v-model:value="processForm.reject_reason"
placeholder="请输入拒绝原因"
:rows="3"
/>
</a-form-item>
<a-form-item label="处理说明" name="process_description">
<a-textarea
v-model:value="processForm.process_description"
placeholder="请输入处理说明"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 新增/编辑模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="800px"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="理赔单号" name="claim_number">
<a-input v-model:value="formState.claim_number" placeholder="请输入理赔单号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保单号" name="policy_number">
<a-input v-model:value="formState.policy_number" placeholder="请输入保单号" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="申请人姓名" name="applicant_name">
<a-input v-model:value="formState.applicant_name" placeholder="请输入申请人姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="formState.phone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="申请金额" name="claim_amount">
<a-input-number
v-model:value="formState.claim_amount"
:min="0"
:step="100"
style="width: 100%"
placeholder="请输入申请金额"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="申请日期" name="apply_date">
<a-date-picker
v-model:value="formState.apply_date"
style="width: 100%"
placeholder="请选择申请日期"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="事故日期" name="accident_date">
<a-date-picker
v-model:value="formState.accident_date"
style="width: 100%"
placeholder="请选择事故日期"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status" placeholder="请选择状态">
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="processing">处理中</a-select-option>
<a-select-option value="approved">已通过</a-select-option>
<a-select-option value="rejected">已拒绝</a-select-option>
<a-select-option value="completed">已完成</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="事故描述" name="accident_description">
<a-textarea
v-model:value="formState.accident_description"
placeholder="请输入事故详细描述"
:rows="4"
/>
</a-form-item>
<a-form-item label="处理说明" name="process_description">
<a-textarea
v-model:value="formState.process_description"
placeholder="请输入处理说明"
:rows="3"
/>
</a-form-item>
<a-form-item label="拒绝原因" name="reject_reason">
<a-textarea
v-model:value="formState.reject_reason"
placeholder="请输入拒绝原因(如适用)"
:rows="2"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined,
FileTextOutlined
} from '@ant-design/icons-vue'
const loading = ref(false)
const modalVisible = ref(false)
const detailVisible = ref(false)
const processVisible = ref(false)
const editingId = ref(null)
const currentClaim = ref(null)
const claimList = ref([])
const formRef = ref()
const processFormRef = ref()
const searchForm = reactive({
claim_number: '',
policy_number: '',
applicant_name: '',
status: ''
})
const formState = reactive({
claim_number: '',
policy_number: '',
applicant_name: '',
phone: '',
claim_amount: null,
apply_date: null,
accident_date: null,
status: 'pending',
accident_description: '',
process_description: '',
reject_reason: '',
approved_amount: null
})
const processForm = reactive({
approved_amount: null,
status: 'approved',
reject_reason: '',
process_description: ''
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
})
const columns = [
{
title: '理赔单号',
dataIndex: 'claim_number',
key: 'claim_number'
},
{
title: '保单号',
dataIndex: 'policy_number',
key: 'policy_number'
},
{
title: '申请人',
dataIndex: 'applicant_name',
key: 'applicant_name'
},
{
title: '申请金额',
key: 'claim_amount',
dataIndex: 'claim_amount'
},
{
title: '审核金额',
key: 'approved_amount',
dataIndex: 'approved_amount'
},
{
title: '申请日期',
dataIndex: 'apply_date',
key: 'apply_date',
width: 120
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
key: 'action',
width: 250,
fixed: 'right'
}
]
const rules = {
claim_number: [{ required: true, message: '请输入理赔单号' }],
policy_number: [{ required: true, message: '请输入保单号' }],
applicant_name: [{ required: true, message: '请输入申请人姓名' }],
claim_amount: [{ required: true, message: '请输入申请金额' }],
apply_date: [{ required: true, message: '请选择申请日期' }],
accident_date: [{ required: true, message: '请选择事故日期' }],
status: [{ required: true, message: '请选择状态' }],
accident_description: [{ required: true, message: '请输入事故描述' }]
}
const processRules = {
approved_amount: [{ required: true, message: '请输入审核金额' }],
status: [{ required: true, message: '请选择处理结果' }],
reject_reason: [
{
required: true,
message: '请输入拒绝原因',
trigger: 'change',
validator: (_, value) => {
if (processForm.status === 'rejected' && !value) {
return Promise.reject('拒绝时必须填写原因')
}
return Promise.resolve()
}
}
]
}
const modalTitle = computed(() => {
return editingId.value ? '编辑理赔' : '新增理赔'
})
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
approved: 'green',
rejected: 'red',
processing: 'blue',
completed: 'purple'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
pending: '待审核',
approved: '已通过',
rejected: '已拒绝',
processing: '处理中',
completed: '已完成'
}
return texts[status] || '未知'
}
const loadClaims = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...searchForm
}
// 这里应该是实际的API调用
// const response = await claimAPI.getList(params)
// claimList.value = response.data.list
// pagination.total = response.data.total
// 模拟数据
claimList.value = [
{
id: 1,
claim_number: 'CLM20240001',
policy_number: 'POL20240001',
applicant_name: '张三',
phone: '13800138000',
claim_amount: 50000,
approved_amount: 45000,
apply_date: '2024-01-15',
accident_date: '2024-01-10',
status: 'approved',
accident_description: '交通事故,车辆前部受损',
process_description: '已核实事故情况,符合理赔条件',
reject_reason: '',
reviewer_name: '李审核员',
review_date: '2024-01-16',
documents: [
{ name: '事故照片.jpg', size: '2.5MB', url: '#' },
{ name: '维修报价单.pdf', size: '1.2MB', url: '#' }
],
created_at: '2024-01-15 10:00:00',
updated_at: '2024-01-16 14:30:00'
},
{
id: 2,
claim_number: 'CLM20240002',
policy_number: 'POL20240002',
applicant_name: '李四',
phone: '13800138001',
claim_amount: 100000,
approved_amount: null,
apply_date: '2024-01-20',
accident_date: '2024-01-18',
status: 'pending',
accident_description: '家庭财产损失,水管爆裂',
process_description: '',
reject_reason: '',
reviewer_name: null,
review_date: null,
documents: [
{ name: '损失评估报告.pdf', size: '3.1MB', url: '#' }
],
created_at: '2024-01-20 14:30:00',
updated_at: '2024-01-20 14:30:00'
}
]
pagination.total = 2
} catch (error) {
message.error('加载理赔列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadClaims()
}
const resetSearch = () => {
searchForm.claim_number = ''
searchForm.policy_number = ''
searchForm.applicant_name = ''
searchForm.status = ''
handleSearch()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadClaims()
}
const showModal = () => {
editingId.value = null
Object.assign(formState, {
claim_number: '',
policy_number: '',
applicant_name: '',
phone: '',
claim_amount: null,
apply_date: null,
accident_date: null,
status: 'pending',
accident_description: '',
process_description: '',
reject_reason: '',
approved_amount: null
})
modalVisible.value = true
}
const handleView = (record) => {
currentClaim.value = record
detailVisible.value = true
}
const handleProcess = (record) => {
currentClaim.value = record
Object.assign(processForm, {
approved_amount: record.claim_amount,
status: 'approved',
reject_reason: '',
process_description: ''
})
processVisible.value = true
}
const handleEdit = (record) => {
editingId.value = record.id
Object.assign(formState, {
claim_number: record.claim_number,
policy_number: record.policy_number,
applicant_name: record.applicant_name,
phone: record.phone,
claim_amount: record.claim_amount,
apply_date: record.apply_date,
accident_date: record.accident_date,
status: record.status,
accident_description: record.accident_description,
process_description: record.process_description,
reject_reason: record.reject_reason,
approved_amount: record.approved_amount
})
modalVisible.value = true
}
const handleApprove = async (record) => {
try {
// await claimAPI.approve(record.id, { approved_amount: record.claim_amount })
message.success('理赔申请已通过')
loadClaims()
} catch (error) {
message.error('操作失败')
}
}
const handleReject = async (record) => {
message.info('请使用处理功能填写拒绝原因')
}
const handleProcessOk = async () => {
try {
await processFormRef.value.validate()
// await claimAPI.process(currentClaim.value.id, processForm)
message.success('处理完成')
processVisible.value = false
loadClaims()
} catch (error) {
console.log('表单验证失败', error)
}
}
const handleProcessCancel = () => {
processVisible.value = false
}
const handleModalOk = async () => {
try {
await formRef.value.validate()
if (editingId.value) {
// await claimAPI.update(editingId.value, formState)
message.success('理赔更新成功')
} else {
// await claimAPI.create(formState)
message.success('理赔创建成功')
}
modalVisible.value = false
loadClaims()
} catch (error) {
console.log('表单验证失败', error)
}
}
const handleModalCancel = () => {
modalVisible.value = false
}
const handleDelete = async (id) => {
try {
// await claimAPI.delete(id)
message.success('理赔删除成功')
loadClaims()
} catch (error) {
message.error('理赔删除失败')
}
}
onMounted(() => {
loadClaims()
})
</script>
<style scoped>
.claim-management {
padding: 0;
}
.document-item {
display: flex;
align-items: center;
gap: 8px;
}
.document-size {
color: #999;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<div class="dashboard">
<a-page-header
title="仪表板"
sub-title="系统概览和统计数据"
/>
<!-- 统计卡片 -->
<a-row :gutter="16" style="margin-top: 24px">
<a-col :span="6">
<a-card>
<div class="stat-card">
<user-outlined style="color: #1890ff; font-size: 24px" />
<div class="stat-content">
<div class="stat-number">{{ stats.totalUsers || 0 }}</div>
<div class="stat-label">总用户数</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<div class="stat-card">
<insurance-outlined style="color: #52c41a; font-size: 24px" />
<div class="stat-content">
<div class="stat-number">{{ stats.totalApplications || 0 }}</div>
<div class="stat-label">保险申请</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<div class="stat-card">
<file-done-outlined style="color: #faad14; font-size: 24px" />
<div class="stat-content">
<div class="stat-number">{{ stats.totalPolicies || 0 }}</div>
<div class="stat-label">有效保单</div>
</div>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<div class="stat-card">
<safety-certificate-outlined style="color: #f5222d; font-size: 24px" />
<div class="stat-content">
<div class="stat-number">{{ stats.totalClaims || 0 }}</div>
<div class="stat-label">理赔申请</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 图表区域 -->
<a-row :gutter="16" style="margin-top: 24px">
<a-col :span="12">
<a-card title="保险申请趋势" :bordered="false">
<div style="height: 300px">
<!-- 这里可以放置ECharts图表 -->
<div style="text-align: center; padding: 60px 0; color: #999">
<bar-chart-outlined style="font-size: 48px" />
<p>图表区域 - 申请趋势</p>
</div>
</div>
</a-card>
</a-col>
<a-col :span="12">
<a-card title="保单状态分布" :bordered="false">
<div style="height: 300px">
<!-- 这里可以放置ECharts饼图 -->
<div style="text-align: center; padding: 60px 0; color: #999">
<pie-chart-outlined style="font-size: 48px" />
<p>图表区域 - 状态分布</p>
</div>
</div>
</a-card>
</a-col>
</a-row>
<!-- 最近活动 -->
<a-card title="最近活动" style="margin-top: 24px">
<a-list
item-layout="horizontal"
:data-source="recentActivities"
:loading="loading"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta
:description="item.description"
>
<template #title>
<span :style="{ color: getActivityColor(item.type) }">{{ item.title }}</span>
</template>
<template #avatar>
<a-avatar :style="{ backgroundColor: getActivityColor(item.type) }">
{{ getActivityIcon(item.type) }}
</a-avatar>
</template>
</a-list-item-meta>
<div>{{ formatTime(item.created_at) }}</div>
</a-list-item>
</template>
</a-list>
</a-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {
UserOutlined,
InsuranceOutlined,
FileDoneOutlined,
SafetyCertificateOutlined,
BarChartOutlined,
PieChartOutlined
} from '@ant-design/icons-vue'
import { dashboardAPI } from '@/utils/api'
import { message } from 'ant-design-vue'
const loading = ref(false)
const stats = ref({})
const recentActivities = ref([])
const getActivityColor = (type) => {
const colors = {
user: '#1890ff',
application: '#52c41a',
policy: '#faad14',
claim: '#f5222d'
}
return colors[type] || '#666'
}
const getActivityIcon = (type) => {
const icons = {
user: 'U',
application: 'A',
policy: 'P',
claim: 'C'
}
return icons[type] || '?'
}
const formatTime = (time) => {
return new Date(time).toLocaleString()
}
const getLogType = (message) => {
if (message.includes('用户') || message.includes('注册') || message.includes('登录')) {
return 'user'
} else if (message.includes('申请') || message.includes('投保')) {
return 'application'
} else if (message.includes('保单') || message.includes('合同')) {
return 'policy'
} else if (message.includes('理赔') || message.includes('赔偿')) {
return 'claim'
}
return 'system'
}
const getLogTitle = (level, message) => {
if (level === 'info') {
if (message.includes('启动') || message.includes('连接')) {
return '系统操作'
}
return '信息通知'
} else if (level === 'warning') {
return '警告信息'
} else if (level === 'error') {
return '错误报告'
}
return '系统消息'
}
const loadDashboardData = async () => {
loading.value = true
try {
// 获取统计信息
const statsResponse = await dashboardAPI.getStats()
if (statsResponse.status === 'success') {
stats.value = {
totalUsers: statsResponse.data.overview?.users || 0,
totalApplications: statsResponse.data.overview?.applications || 0,
totalPolicies: statsResponse.data.overview?.policies || 0,
totalClaims: statsResponse.data.overview?.claims || 0
}
}
// 获取最近活动(使用系统日志作为活动记录)
const activitiesResponse = await dashboardAPI.getRecentActivities()
if (activitiesResponse.status === 'success') {
recentActivities.value = activitiesResponse.data.logs?.slice(0, 10).map(log => ({
id: log.id,
type: getLogType(log.message),
title: getLogTitle(log.level, log.message),
description: log.message,
created_at: log.timestamp
})) || []
}
} catch (error) {
console.error('加载仪表板数据失败:', error)
message.error('加载数据失败')
// 如果API调用失败使用模拟数据
stats.value = {
totalUsers: 128,
totalApplications: 356,
totalPolicies: 289,
totalClaims: 45
}
recentActivities.value = [
{
id: 1,
type: 'application',
title: '新的保险申请',
description: '张三提交了车险申请',
created_at: new Date().toISOString()
},
{
id: 2,
type: 'policy',
title: '保单生效',
description: '李四的寿险保单已生效',
created_at: new Date(Date.now() - 3600000).toISOString()
},
{
id: 3,
type: 'claim',
title: '理赔申请',
description: '王五提交了医疗险理赔',
created_at: new Date(Date.now() - 7200000).toISOString()
},
{
id: 4,
type: 'user',
title: '新用户注册',
description: '新用户赵六完成注册',
created_at: new Date(Date.now() - 10800000).toISOString()
}
]
} finally {
loading.value = false
}
}
onMounted(() => {
loadDashboardData()
})
</script>
<style scoped>
.stat-card {
display: flex;
align-items: center;
gap: 16px;
}
.stat-content {
flex: 1;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #333;
}
.stat-label {
color: #666;
font-size: 14px;
}
.dashboard {
padding: 0;
}
</style>

View File

@@ -0,0 +1,447 @@
<template>
<div class="insurance-type-management">
<a-page-header
title="保险类型管理"
sub-title="管理系统支持的保险产品类型"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增类型
</a-button>
</template>
</a-page-header>
<!-- 搜索区域 -->
<a-card style="margin-top: 16px">
<a-form layout="inline" :model="searchForm">
<a-form-item label="类型名称">
<a-input
v-model:value="searchForm.name"
placeholder="请输入类型名称"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 120px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="active">启用</a-select-option>
<a-select-option value="inactive">禁用</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">
<redo-outlined />
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 类型表格 -->
<a-card style="margin-top: 16px">
<a-table
:columns="columns"
:data-source="typeList"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status === 'active' ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-else-if="column.key === 'coverage_type'">
<span>{{ getCoverageTypeText(record.coverage_type) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" @click="handleEdit(record)">编辑</a-button>
<a-button
size="small"
:type="record.status === 'active' ? 'danger' : 'primary'"
@click="handleToggleStatus(record)"
>
{{ record.status === 'active' ? '禁用' : '启用' }}
</a-button>
<a-popconfirm
title="确定要删除这个保险类型吗?"
@confirm="handleDelete(record.id)"
>
<a-button size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增/编辑模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="600px"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="类型名称" name="name">
<a-input v-model:value="formState.name" placeholder="请输入类型名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型代码" name="code">
<a-input v-model:value="formState.code" placeholder="请输入类型代码" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="描述" name="description">
<a-textarea
v-model:value="formState.description"
placeholder="请输入类型描述"
:rows="3"
/>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="保障类型" name="coverage_type">
<a-select v-model:value="formState.coverage_type" placeholder="请选择保障类型">
<a-select-option value="life">人寿保险</a-select-option>
<a-select-option value="health">健康保险</a-select-option>
<a-select-option value="property">财产保险</a-select-option>
<a-select-option value="accident">意外保险</a-select-option>
<a-select-option value="travel">旅行保险</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status" placeholder="请选择状态">
<a-select-option value="active">启用</a-select-option>
<a-select-option value="inactive">禁用</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="最小保额" name="min_coverage">
<a-input-number
v-model:value="formState.min_coverage"
:min="0"
:step="1000"
style="width: 100%"
placeholder="最小保额"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="最大保额" name="max_coverage">
<a-input-number
v-model:value="formState.max_coverage"
:min="0"
:step="1000"
style="width: 100%"
placeholder="最大保额"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="保费计算规则" name="premium_rules">
<a-textarea
v-model:value="formState.premium_rules"
placeholder="请输入保费计算规则JSON格式"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined
} from '@ant-design/icons-vue'
const loading = ref(false)
const modalVisible = ref(false)
const editingId = ref(null)
const typeList = ref([])
const formRef = ref()
const searchForm = reactive({
name: '',
status: ''
})
const formState = reactive({
name: '',
code: '',
description: '',
coverage_type: '',
status: 'active',
min_coverage: null,
max_coverage: null,
premium_rules: ''
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '类型名称',
dataIndex: 'name',
key: 'name'
},
{
title: '类型代码',
dataIndex: 'code',
key: 'code'
},
{
title: '保障类型',
key: 'coverage_type',
dataIndex: 'coverage_type'
},
{
title: '最小保额',
dataIndex: 'min_coverage',
key: 'min_coverage',
render: (text) => text ? `¥${text.toLocaleString()}` : '-'
},
{
title: '最大保额',
dataIndex: 'max_coverage',
key: 'max_coverage',
render: (text) => text ? `¥${text.toLocaleString()}` : '-'
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
const rules = {
name: [{ required: true, message: '请输入类型名称' }],
code: [{ required: true, message: '请输入类型代码' }],
coverage_type: [{ required: true, message: '请选择保障类型' }],
status: [{ required: true, message: '请选择状态' }]
}
const modalTitle = computed(() => {
return editingId.value ? '编辑保险类型' : '新增保险类型'
})
const getCoverageTypeText = (type) => {
const types = {
life: '人寿保险',
health: '健康保险',
property: '财产保险',
accident: '意外保险',
travel: '旅行保险'
}
return types[type] || type
}
const loadInsuranceTypes = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...searchForm
}
// 这里应该是实际的API调用
// const response = await insuranceTypeAPI.getList(params)
// typeList.value = response.data.list
// pagination.total = response.data.total
// 模拟数据
typeList.value = [
{
id: 1,
name: '综合意外险',
code: 'ACCIDENT_001',
description: '提供全面的意外伤害保障',
coverage_type: 'accident',
status: 'active',
min_coverage: 100000,
max_coverage: 500000,
premium_rules: '{\"base_rate\": 0.001, \"age_factor\": 1.2}',
created_at: '2024-01-01 10:00:00'
},
{
id: 2,
name: '终身寿险',
code: 'LIFE_001',
description: '提供终身的人寿保障',
coverage_type: 'life',
status: 'active',
min_coverage: 500000,
max_coverage: 2000000,
premium_rules: '{\"base_rate\": 0.0005, \"age_factor\": 1.5}',
created_at: '2024-01-02 14:30:00'
}
]
pagination.total = 2
} catch (error) {
message.error('加载保险类型列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadInsuranceTypes()
}
const resetSearch = () => {
searchForm.name = ''
searchForm.status = ''
handleSearch()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadInsuranceTypes()
}
const showModal = () => {
editingId.value = null
Object.assign(formState, {
name: '',
code: '',
description: '',
coverage_type: '',
status: 'active',
min_coverage: null,
max_coverage: null,
premium_rules: ''
})
modalVisible.value = true
}
const handleEdit = (record) => {
editingId.value = record.id
Object.assign(formState, {
name: record.name,
code: record.code,
description: record.description,
coverage_type: record.coverage_type,
status: record.status,
min_coverage: record.min_coverage,
max_coverage: record.max_coverage,
premium_rules: record.premium_rules
})
modalVisible.value = true
}
const handleModalOk = async () => {
try {
await formRef.value.validate()
if (editingId.value) {
// await insuranceTypeAPI.update(editingId.value, formState)
message.success('保险类型更新成功')
} else {
// await insuranceTypeAPI.create(formState)
message.success('保险类型创建成功')
}
modalVisible.value = false
loadInsuranceTypes()
} catch (error) {
console.log('表单验证失败', error)
}
}
const handleModalCancel = () => {
modalVisible.value = false
}
const handleToggleStatus = async (record) => {
try {
const newStatus = record.status === 'active' ? 'inactive' : 'active'
// await insuranceTypeAPI.update(record.id, { status: newStatus })
message.success('状态更新成功')
loadInsuranceTypes()
} catch (error) {
message.error('状态更新失败')
}
}
const handleDelete = async (id) => {
try {
// await insuranceTypeAPI.delete(id)
message.success('保险类型删除成功')
loadInsuranceTypes()
} catch (error) {
message.error('保险类型删除失败')
}
}
onMounted(() => {
loadInsuranceTypes()
})
</script>
<style scoped>
.insurance-type-management {
padding: 0;
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<div class="login-container">
<div class="login-form">
<div class="login-header">
<h2>保险端口后台管理系统</h2>
<p>请登录您的账户</p>
</div>
<a-form
:model="formState"
name="login"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
name="username"
:rules="[{ required: true, message: '请输入用户名!' }]"
>
<a-input
v-model:value="formState.username"
placeholder="用户名"
size="large"
>
<template #prefix>
<user-outlined />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password
v-model:value="formState.password"
placeholder="密码"
size="large"
>
<template #prefix>
<lock-outlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
size="large"
:loading="loading"
block
>
登录
</a-button>
</a-form-item>
</a-form>
<div class="login-footer">
<p>© 2024 保险端口系统 - 后台管理系统</p>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
import { authAPI } from '@/utils/api'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const formState = reactive({
username: '',
password: ''
})
const onFinish = async (values) => {
loading.value = true
try {
const response = await authAPI.login(values)
if (response.status === 'success') {
userStore.setToken(response.data.token)
userStore.setUserInfo(response.data.user)
message.success('登录成功')
router.push('/dashboard')
} else {
message.error(response.message || '登录失败')
}
} catch (error) {
message.error(error.response?.data?.message || '登录失败')
} finally {
loading.value = false
}
}
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo)
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-form {
width: 400px;
padding: 40px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.login-header {
text-align: center;
margin-bottom: 30px;
}
.login-header h2 {
color: #333;
margin-bottom: 8px;
}
.login-header p {
color: #666;
margin: 0;
}
.login-footer {
text-align: center;
margin-top: 20px;
color: #999;
}
</style>

View File

@@ -0,0 +1,623 @@
<template>
<div class="policy-management">
<a-page-header
title="保单管理"
sub-title="管理系统所有保单信息"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增保单
</a-button>
</template>
</a-page-header>
<!-- 搜索区域 -->
<a-card style="margin-top: 16px">
<a-form layout="inline" :model="searchForm">
<a-form-item label="保单号">
<a-input
v-model:value="searchForm.policy_number"
placeholder="请输入保单号"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="投保人">
<a-input
v-model:value="searchForm.policyholder_name"
placeholder="请输入投保人姓名"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="保险类型">
<a-select
v-model:value="searchForm.insurance_type_id"
placeholder="请选择保险类型"
style="width: 150px"
>
<a-select-option value="">全部类型</a-select-option>
<a-select-option :value="1">综合意外险</a-select-option>
<a-select-option :value="2">终身寿险</a-select-option>
<a-select-option :value="3">健康医疗保险</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 120px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="active">有效</a-select-option>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="expired">已过期</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">
<redo-outlined />
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 保单表格 -->
<a-card style="margin-top: 16px">
<a-table
:columns="columns"
:data-source="policyList"
:loading="loading"
:pagination="pagination"
@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 === 'premium_amount'">
<span>¥{{ record.premium_amount?.toLocaleString() }}</span>
</template>
<template v-else-if="column.key === 'coverage_amount'">
<span>¥{{ record.coverage_amount?.toLocaleString() }}</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" @click="handleView(record)">查看</a-button>
<a-button size="small" @click="handleEdit(record)">编辑</a-button>
<a-button
size="small"
:type="record.status === 'active' ? 'danger' : 'primary'"
@click="handleToggleStatus(record)"
>
{{ record.status === 'active' ? '停用' : '启用' }}
</a-button>
<a-dropdown>
<a-button size="small">更多</a-button>
<template #overlay>
<a-menu>
<a-menu-item @click="handleRenew(record)">续保</a-menu-item>
<a-menu-item @click="handleClaim(record)">理赔</a-menu-item>
<a-menu-item danger @click="handleDelete(record.id)">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 查看详情模态框 -->
<a-modal
v-model:visible="detailVisible"
title="保单详情"
width="800px"
:footer="null"
>
<a-descriptions
v-if="currentPolicy"
title="基本信息"
bordered
:column="2"
>
<a-descriptions-item label="保单号">{{ currentPolicy.policy_number }}</a-descriptions-item>
<a-descriptions-item label="保险类型">{{ currentPolicy.insurance_type_name }}</a-descriptions-item>
<a-descriptions-item label="投保人">{{ currentPolicy.policyholder_name }}</a-descriptions-item>
<a-descriptions-item label="被保险人">{{ currentPolicy.insured_name }}</a-descriptions-item>
<a-descriptions-item label="保费金额">¥{{ currentPolicy.premium_amount?.toLocaleString() }}</a-descriptions-item>
<a-descriptions-item label="保额">¥{{ currentPolicy.coverage_amount?.toLocaleString() }}</a-descriptions-item>
<a-descriptions-item label="保险期间">
{{ currentPolicy.start_date }} {{ currentPolicy.end_date }}
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(currentPolicy.status)">
{{ getStatusText(currentPolicy.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ currentPolicy.created_at }}</a-descriptions-item>
<a-descriptions-item label="最后更新">{{ currentPolicy.updated_at }}</a-descriptions-item>
</a-descriptions>
<a-divider />
<a-descriptions
title="联系信息"
bordered
:column="2"
>
<a-descriptions-item label="联系电话">{{ currentPolicy.phone }}</a-descriptions-item>
<a-descriptions-item label="电子邮箱">{{ currentPolicy.email }}</a-descriptions-item>
<a-descriptions-item label="联系地址" :span="2">
{{ currentPolicy.address }}
</a-descriptions-item>
</a-descriptions>
</a-modal>
<!-- 新增/编辑模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
width="800px"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="保单号" name="policy_number">
<a-input v-model:value="formState.policy_number" placeholder="请输入保单号" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保险类型" name="insurance_type_id">
<a-select v-model:value="formState.insurance_type_id" placeholder="请选择保险类型">
<a-select-option :value="1">综合意外险</a-select-option>
<a-select-option :value="2">终身寿险</a-select-option>
<a-select-option :value="3">健康医疗保险</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="投保人姓名" name="policyholder_name">
<a-input v-model:value="formState.policyholder_name" placeholder="请输入投保人姓名" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="被保险人姓名" name="insured_name">
<a-input v-model:value="formState.insured_name" placeholder="请输入被保险人姓名" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="保费金额" name="premium_amount">
<a-input-number
v-model:value="formState.premium_amount"
:min="0"
:step="100"
style="width: 100%"
placeholder="请输入保费金额"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保额" name="coverage_amount">
<a-input-number
v-model:value="formState.coverage_amount"
:min="0"
:step="1000"
style="width: 100%"
placeholder="请输入保额"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="保险开始日期" name="start_date">
<a-date-picker
v-model:value="formState.start_date"
style="width: 100%"
placeholder="请选择开始日期"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="保险结束日期" name="end_date">
<a-date-picker
v-model:value="formState.end_date"
style="width: 100%"
placeholder="请选择结束日期"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status" placeholder="请选择状态">
<a-select-option value="active">有效</a-select-option>
<a-select-option value="pending">待审核</a-select-option>
<a-select-option value="expired">已过期</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="联系信息">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item name="phone">
<a-input v-model:value="formState.phone" placeholder="联系电话" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item name="email">
<a-input v-model:value="formState.email" placeholder="电子邮箱" />
</a-form-item>
</a-col>
</a-row>
<a-form-item name="address">
<a-textarea
v-model:value="formState.address"
placeholder="联系地址"
:rows="2"
/>
</a-form-item>
</a-form-item>
<a-form-item label="备注" name="remarks">
<a-textarea
v-model:value="formState.remarks"
placeholder="请输入备注信息"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined
} from '@ant-design/icons-vue'
const loading = ref(false)
const modalVisible = ref(false)
const detailVisible = ref(false)
const editingId = ref(null)
const currentPolicy = ref(null)
const policyList = ref([])
const formRef = ref()
const searchForm = reactive({
policy_number: '',
policyholder_name: '',
insurance_type_id: '',
status: ''
})
const formState = reactive({
policy_number: '',
insurance_type_id: null,
policyholder_name: '',
insured_name: '',
premium_amount: null,
coverage_amount: null,
start_date: null,
end_date: null,
status: 'active',
phone: '',
email: '',
address: '',
remarks: ''
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
})
const columns = [
{
title: '保单号',
dataIndex: 'policy_number',
key: 'policy_number'
},
{
title: '保险类型',
dataIndex: 'insurance_type_name',
key: 'insurance_type_name'
},
{
title: '投保人',
dataIndex: 'policyholder_name',
key: 'policyholder_name'
},
{
title: '被保险人',
dataIndex: 'insured_name',
key: 'insured_name'
},
{
title: '保费金额',
key: 'premium_amount',
dataIndex: 'premium_amount'
},
{
title: '保额',
key: 'coverage_amount',
dataIndex: 'coverage_amount'
},
{
title: '保险期间',
key: 'period',
render: (_, record) => `${record.start_date}${record.end_date}`
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
key: 'action',
width: 250,
fixed: 'right'
}
]
const rules = {
policy_number: [{ required: true, message: '请输入保单号' }],
insurance_type_id: [{ required: true, message: '请选择保险类型' }],
policyholder_name: [{ required: true, message: '请输入投保人姓名' }],
insured_name: [{ required: true, message: '请输入被保险人姓名' }],
premium_amount: [{ required: true, message: '请输入保费金额' }],
coverage_amount: [{ required: true, message: '请输入保额' }],
start_date: [{ required: true, message: '请选择开始日期' }],
end_date: [{ required: true, message: '请选择结束日期' }],
status: [{ required: true, message: '请选择状态' }]
}
const modalTitle = computed(() => {
return editingId.value ? '编辑保单' : '新增保单'
})
const getStatusColor = (status) => {
const colors = {
active: 'green',
pending: 'orange',
expired: 'default',
cancelled: 'red'
}
return colors[status] || 'default'
}
const getStatusText = (status) => {
const texts = {
active: '有效',
pending: '待审核',
expired: '已过期',
cancelled: '已取消'
}
return texts[status] || '未知'
}
const loadPolicies = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...searchForm
}
// 这里应该是实际的API调用
// const response = await policyAPI.getList(params)
// policyList.value = response.data.list
// pagination.total = response.data.total
// 模拟数据
policyList.value = [
{
id: 1,
policy_number: 'POL20240001',
insurance_type_id: 1,
insurance_type_name: '综合意外险',
policyholder_name: '张三',
insured_name: '张三',
premium_amount: 1200,
coverage_amount: 500000,
start_date: '2024-01-01',
end_date: '2025-01-01',
status: 'active',
phone: '13800138000',
email: 'zhangsan@example.com',
address: '北京市朝阳区',
created_at: '2024-01-01 10:00:00',
updated_at: '2024-01-01 10:00:00'
},
{
id: 2,
policy_number: 'POL20240002',
insurance_type_id: 2,
insurance_type_name: '终身寿险',
policyholder_name: '李四',
insured_name: '李四',
premium_amount: 5000,
coverage_amount: 1000000,
start_date: '2024-01-02',
end_date: '2074-01-02',
status: 'active',
phone: '13800138001',
email: 'lisi@example.com',
address: '上海市浦东新区',
created_at: '2024-01-02 14:30:00',
updated_at: '2024-01-02 14:30:00'
}
]
pagination.total = 2
} catch (error) {
message.error('加载保单列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadPolicies()
}
const resetSearch = () => {
searchForm.policy_number = ''
searchForm.policyholder_name = ''
searchForm.insurance_type_id = ''
searchForm.status = ''
handleSearch()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadPolicies()
}
const showModal = () => {
editingId.value = null
Object.assign(formState, {
policy_number: '',
insurance_type_id: null,
policyholder_name: '',
insured_name: '',
premium_amount: null,
coverage_amount: null,
start_date: null,
end_date: null,
status: 'active',
phone: '',
email: '',
address: '',
remarks: ''
})
modalVisible.value = true
}
const handleView = (record) => {
currentPolicy.value = record
detailVisible.value = true
}
const handleEdit = (record) => {
editingId.value = record.id
Object.assign(formState, {
policy_number: record.policy_number,
insurance_type_id: record.insurance_type_id,
policyholder_name: record.policyholder_name,
insured_name: record.insured_name,
premium_amount: record.premium_amount,
coverage_amount: record.coverage_amount,
start_date: record.start_date,
end_date: record.end_date,
status: record.status,
phone: record.phone,
email: record.email,
address: record.address,
remarks: record.remarks
})
modalVisible.value = true
}
const handleModalOk = async () => {
try {
await formRef.value.validate()
if (editingId.value) {
// await policyAPI.update(editingId.value, formState)
message.success('保单更新成功')
} else {
// await policyAPI.create(formState)
message.success('保单创建成功')
}
modalVisible.value = false
loadPolicies()
} catch (error) {
console.log('表单验证失败', error)
}
}
const handleModalCancel = () => {
modalVisible.value = false
}
const handleToggleStatus = async (record) => {
try {
const newStatus = record.status === 'active' ? 'cancelled' : 'active'
// await policyAPI.update(record.id, { status: newStatus })
message.success('状态更新成功')
loadPolicies()
} catch (error) {
message.error('状态更新失败')
}
}
const handleRenew = async (record) => {
message.info('续保功能开发中')
}
const handleClaim = async (record) => {
message.info('理赔功能开发中')
}
const handleDelete = async (id) => {
try {
// await policyAPI.delete(id)
message.success('保单删除成功')
loadPolicies()
} catch (error) {
message.error('保单删除失败')
}
}
onMounted(() => {
loadPolicies()
})
</script>
<style scoped>
.policy-management {
padding: 0;
}
</style>

View File

@@ -0,0 +1,521 @@
<template>
<div class="system-logs">
<a-page-header
title="系统日志"
sub-title="查看系统操作和运行日志"
/>
<a-card>
<!-- 搜索区域 -->
<a-form layout="inline" :model="searchForm" @finish="handleSearch">
<a-form-item label="操作类型">
<a-select
v-model:value="searchForm.log_type"
placeholder="请选择操作类型"
style="width: 120px"
allow-clear
>
<a-select-option value="login">登录</a-select-option>
<a-select-option value="create">新增</a-select-option>
<a-select-option value="update">修改</a-select-option>
<a-select-option value="delete">删除</a-select-option>
<a-select-option value="export">导出</a-select-option>
<a-select-option value="system">系统操作</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="操作模块">
<a-select
v-model:value="searchForm.module"
placeholder="请选择操作模块"
style="width: 120px"
allow-clear
>
<a-select-option value="user">用户管理</a-select-option>
<a-select-option value="policy">保单管理</a-select-option>
<a-select-option value="claim">理赔管理</a-select-option>
<a-select-option value="type">保险类型</a-select-option>
<a-select-option value="system">系统设置</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="操作人">
<a-input
v-model:value="searchForm.operator"
placeholder="请输入操作人"
style="width: 120px"
/>
</a-form-item>
<a-form-item label="操作时间">
<a-range-picker
v-model:value="searchForm.timeRange"
:show-time="{ format: 'HH:mm' }"
format="YYYY-MM-DD HH:mm"
style="width: 320px"
/>
</a-form-item>
<a-form-item label="IP地址">
<a-input
v-model:value="searchForm.ip_address"
placeholder="请输入IP地址"
style="width: 120px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit">查询</a-button>
<a-button @click="resetSearch">重置</a-button>
<a-button @click="exportLogs" :loading="exportLoading">导出</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<a-card style="margin-top: 16px">
<!-- 操作按钮 -->
<div style="margin-bottom: 16px">
<a-space>
<a-button
type="primary"
danger
@click="clearLogs"
:loading="clearLoading"
>
清空日志
</a-button>
<a-button @click="refreshLogs">刷新</a-button>
<a-tag color="blue">
{{ pagination.total }} 条记录
</a-tag>
</a-space>
</div>
<!-- 日志表格 -->
<a-table
:columns="columns"
:data-source="logList"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
:scroll="{ x: 1200 }"
>
<template #bodyCell="{ column, record }">
<!-- 操作类型 -->
<template v-if="column.key === 'log_type'">
<a-tag :color="getLogTypeColor(record.log_type)">
{{ getLogTypeText(record.log_type) }}
</a-tag>
</template>
<!-- 操作模块 -->
<template v-else-if="column.key === 'module'">
<a-tag color="blue">
{{ getModuleText(record.module) }}
</a-tag>
</template>
<!-- 操作结果 -->
<template v-else-if="column.key === 'result'">
<a-tag :color="record.result === 'success' ? 'green' : 'red'">
{{ record.result === 'success' ? '成功' : '失败' }}
</a-tag>
</template>
<!-- 操作内容 -->
<template v-else-if="column.key === 'content'">
<a-tooltip :title="record.content">
<span class="log-content">{{ record.content }}</span>
</a-tooltip>
</template>
<!-- 操作时间 -->
<template v-else-if="column.key === 'created_at'">
{{ formatDateTime(record.created_at) }}
</template>
<!-- 操作 -->
<template v-else-if="column.key === 'action'">
<a-space>
<a-button
size="small"
@click="showLogDetail(record)"
>
详情
</a-button>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 日志详情模态框 -->
<a-modal
v-model:visible="detailVisible"
title="日志详情"
width="600px"
:footer="null"
>
<a-descriptions bordered :column="1" v-if="currentLog">
<a-descriptions-item label="操作类型">
<a-tag :color="getLogTypeColor(currentLog.log_type)">
{{ getLogTypeText(currentLog.log_type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="操作模块">
<a-tag color="blue">
{{ getModuleText(currentLog.module) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="操作人">
{{ currentLog.operator }}
</a-descriptions-item>
<a-descriptions-item label="操作时间">
{{ formatDateTime(currentLog.created_at) }}
</a-descriptions-item>
<a-descriptions-item label="IP地址">
{{ currentLog.ip_address }}
</a-descriptions-item>
<a-descriptions-item label="操作结果">
<a-tag :color="currentLog.result === 'success' ? 'green' : 'red'">
{{ currentLog.result === 'success' ? '成功' : '失败' }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="操作内容">
<div style="word-break: break-all; white-space: pre-wrap;">
{{ currentLog.content }}
</div>
</a-descriptions-item>
<a-descriptions-item label="请求参数" v-if="currentLog.request_params">
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; overflow: auto;">
{{ JSON.stringify(JSON.parse(currentLog.request_params), null, 2) }}
</pre>
</a-descriptions-item>
<a-descriptions-item label="错误信息" v-if="currentLog.error_message">
<div style="color: #ff4d4f; word-break: break-all; white-space: pre-wrap;">
{{ currentLog.error_message }}
</div>
</a-descriptions-item>
</a-descriptions>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
const searchForm = reactive({
log_type: undefined,
module: undefined,
operator: '',
timeRange: [],
ip_address: ''
})
const loading = ref(false)
const exportLoading = ref(false)
const clearLoading = ref(false)
const detailVisible = ref(false)
const currentLog = ref(null)
const logList = ref([
{
id: 1,
log_type: 'login',
module: 'system',
operator: 'admin',
operator_id: 1,
content: '用户登录系统',
result: 'success',
ip_address: '192.168.1.100',
created_at: '2024-01-20 10:30:25',
request_params: '{"username":"admin"}',
error_message: ''
},
{
id: 2,
log_type: 'create',
module: 'user',
operator: 'admin',
operator_id: 1,
content: '新增用户:张三',
result: 'success',
ip_address: '192.168.1.100',
created_at: '2024-01-20 10:35:18',
request_params: '{"username":"zhangsan","role":"user"}',
error_message: ''
},
{
id: 3,
log_type: 'update',
module: 'policy',
operator: 'admin',
operator_id: 1,
content: '修改保单状态P202401200001',
result: 'success',
ip_address: '192.168.1.100',
created_at: '2024-01-20 11:20:45',
request_params: '{"policy_id":"P202401200001","status":"active"}',
error_message: ''
},
{
id: 4,
log_type: 'delete',
module: 'claim',
operator: 'admin',
operator_id: 1,
content: '删除理赔记录C202401200001',
result: 'success',
ip_address: '192.168.1.100',
created_at: '2024-01-20 14:15:30',
request_params: '{"claim_id":"C202401200001"}',
error_message: ''
},
{
id: 5,
log_type: 'login',
module: 'system',
operator: 'user001',
operator_id: 2,
content: '用户登录失败',
result: 'fail',
ip_address: '192.168.1.101',
created_at: '2024-01-20 15:40:12',
request_params: '{"username":"user001"}',
error_message: '密码错误'
}
])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 5,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) =>
`${range[0]}-${range[1]} 条,共 ${total}`
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 60
},
{
title: '操作类型',
key: 'log_type',
dataIndex: 'log_type',
width: 100
},
{
title: '操作模块',
key: 'module',
dataIndex: 'module',
width: 100
},
{
title: '操作人',
dataIndex: 'operator',
key: 'operator',
width: 100
},
{
title: '操作内容',
key: 'content',
dataIndex: 'content',
width: 200
},
{
title: '操作结果',
key: 'result',
dataIndex: 'result',
width: 80
},
{
title: 'IP地址',
dataIndex: 'ip_address',
key: 'ip_address',
width: 120
},
{
title: '操作时间',
key: 'created_at',
dataIndex: 'created_at',
width: 150
},
{
title: '操作',
key: 'action',
width: 80,
fixed: 'right'
}
]
const getLogTypeColor = (type) => {
const colors = {
login: 'blue',
create: 'green',
update: 'orange',
delete: 'red',
export: 'purple',
system: 'cyan'
}
return colors[type] || 'default'
}
const getLogTypeText = (type) => {
const texts = {
login: '登录',
create: '新增',
update: '修改',
delete: '删除',
export: '导出',
system: '系统操作'
}
return texts[type] || type
}
const getModuleText = (module) => {
const texts = {
user: '用户管理',
policy: '保单管理',
claim: '理赔管理',
type: '保险类型',
system: '系统设置'
}
return texts[module] || module
}
const formatDateTime = (datetime) => {
if (!datetime) return ''
return datetime.replace('T', ' ').substring(0, 19)
}
const handleSearch = () => {
pagination.current = 1
loadLogs()
}
const resetSearch = () => {
Object.assign(searchForm, {
log_type: undefined,
module: undefined,
operator: '',
timeRange: [],
ip_address: ''
})
pagination.current = 1
loadLogs()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadLogs()
}
const loadLogs = async () => {
loading.value = true
try {
// const params = {
// page: pagination.current,
// pageSize: pagination.pageSize,
// ...searchForm,
// start_time: searchForm.timeRange?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
// end_time: searchForm.timeRange?.[1]?.format('YYYY-MM-DD HH:mm:ss')
// }
// const response = await systemAPI.getLogs(params)
// logList.value = response.data.list
// pagination.total = response.data.total
// 模拟数据
const filteredLogs = logList.value.filter(log => {
if (searchForm.log_type && log.log_type !== searchForm.log_type) return false
if (searchForm.module && log.module !== searchForm.module) return false
if (searchForm.operator && !log.operator.includes(searchForm.operator)) return false
if (searchForm.ip_address && !log.ip_address.includes(searchForm.ip_address)) return false
return true
})
logList.value = filteredLogs
pagination.total = filteredLogs.length
} catch (error) {
message.error('加载日志失败')
} finally {
loading.value = false
}
}
const refreshLogs = () => {
loadLogs()
}
const exportLogs = async () => {
exportLoading.value = true
try {
// await systemAPI.exportLogs(searchForm)
message.success('导出任务已开始,请稍后查看下载')
} catch (error) {
message.error('导出失败')
} finally {
exportLoading.value = false
}
}
const clearLogs = async () => {
clearLoading.value = true
try {
// await systemAPI.clearLogs()
message.success('日志已清空')
loadLogs()
} catch (error) {
message.error('清空日志失败')
} finally {
clearLoading.value = false
}
}
const showLogDetail = (log) => {
currentLog.value = log
detailVisible.value = true
}
onMounted(() => {
loadLogs()
})
</script>
<style scoped>
.system-logs {
padding: 0;
}
.log-content {
display: inline-block;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
:deep(.ant-descriptions-item-label) {
width: 100px;
font-weight: 600;
}
:deep(.ant-descriptions-item-content) {
word-break: break-all;
}
:deep(pre) {
margin: 0;
font-size: 12px;
line-height: 1.5;
}
</style>

View File

@@ -0,0 +1,707 @@
<template>
<div class="system-settings">
<a-page-header
title="系统设置"
sub-title="管理系统配置和参数"
/>
<a-tabs v-model:activeKey="activeTab" type="card">
<!-- 基本设置 -->
<a-tab-pane key="general" tab="基本设置">
<a-card title="系统基本信息">
<a-form
ref="generalFormRef"
:model="generalForm"
:rules="generalRules"
layout="vertical"
>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="系统名称" name="system_name">
<a-input v-model:value="generalForm.system_name" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="系统版本" name="system_version">
<a-input v-model:value="generalForm.system_version" disabled />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="系统描述" name="system_description">
<a-textarea
v-model:value="generalForm.system_description"
:rows="3"
placeholder="请输入系统描述"
/>
</a-form-item>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="客服电话" name="customer_service_phone">
<a-input v-model:value="generalForm.customer_service_phone" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="客服邮箱" name="customer_service_email">
<a-input v-model:value="generalForm.customer_service_email" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="公司地址" name="company_address">
<a-textarea
v-model:value="generalForm.company_address"
:rows="2"
placeholder="请输入公司地址"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveGeneralSettings">保存设置</a-button>
<a-button style="margin-left: 8px" @click="resetGeneralForm">重置</a-button>
</a-form-item>
</a-form>
</a-card>
<a-card title="系统状态" style="margin-top: 16px">
<a-descriptions bordered :column="2">
<a-descriptions-item label="运行时间">{{ systemStatus.uptime }}</a-descriptions-item>
<a-descriptions-item label="内存使用">{{ systemStatus.memory_usage }}</a-descriptions-item>
<a-descriptions-item label="数据库状态">
<a-tag :color="systemStatus.database_status === '正常' ? 'green' : 'red'">
{{ systemStatus.database_status }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="最后备份">{{ systemStatus.last_backup }}</a-descriptions-item>
<a-descriptions-item label="用户数量">{{ systemStatus.user_count }}</a-descriptions-item>
<a-descriptions-item label="保单数量">{{ systemStatus.policy_count }}</a-descriptions-item>
</a-descriptions>
</a-card>
</a-tab-pane>
<!-- 邮件设置 -->
<a-tab-pane key="email" tab="邮件设置">
<a-card title="邮件服务器配置">
<a-form
ref="emailFormRef"
:model="emailForm"
:rules="emailRules"
layout="vertical"
>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="SMTP服务器" name="smtp_host">
<a-input v-model:value="emailForm.smtp_host" placeholder="smtp.example.com" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="端口" name="smtp_port">
<a-input-number
v-model:value="emailForm.smtp_port"
:min="1"
:max="65535"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="加密方式" name="smtp_secure">
<a-select v-model:value="emailForm.smtp_secure">
<a-select-option value=""></a-select-option>
<a-select-option value="ssl">SSL</a-select-option>
<a-select-option value="tls">TLS</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="发件邮箱" name="from_email">
<a-input v-model:value="emailForm.from_email" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="发件人名称" name="from_name">
<a-input v-model:value="emailForm.from_name" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="用户名" name="smtp_username">
<a-input v-model:value="emailForm.smtp_username" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="密码" name="smtp_password">
<a-input-password v-model:value="emailForm.smtp_password" />
</a-form-item>
</a-col>
</a-row>
<a-form-item>
<a-button type="primary" @click="saveEmailSettings">保存设置</a-button>
<a-button style="margin-left: 8px" @click="testEmailSettings">测试连接</a-button>
</a-form-item>
</a-form>
</a-card>
<a-card title="邮件模板" style="margin-top: 16px">
<a-tabs type="card">
<a-tab-pane key="welcome" tab="欢迎邮件">
<a-form layout="vertical">
<a-form-item label="邮件主题">
<a-input v-model:value="emailTemplates.welcome.subject" />
</a-form-item>
<a-form-item label="邮件内容">
<a-textarea
v-model:value="emailTemplates.welcome.content"
:rows="6"
placeholder="可使用变量:{username}, {system_name}"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveEmailTemplate('welcome')">保存模板</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="policy" tab="保单通知">
<a-form layout="vertical">
<a-form-item label="邮件主题">
<a-input v-model:value="emailTemplates.policy.subject" />
</a-form-item>
<a-form-item label="邮件内容">
<a-textarea
v-model:value="emailTemplates.policy.content"
:rows="6"
placeholder="可使用变量:{policy_number}, {policyholder_name}, {coverage_amount}"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveEmailTemplate('policy')">保存模板</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="claim" tab="理赔通知">
<a-form layout="vertical">
<a-form-item label="邮件主题">
<a-input v-model:value="emailTemplates.claim.subject" />
</a-form-item>
<a-form-item label="邮件内容">
<a-textarea
v-model:value="emailTemplates.claim.content"
:rows="6"
placeholder="可使用变量:{claim_number}, {applicant_name}, {claim_amount}"
/>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveEmailTemplate('claim')">保存模板</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-card>
</a-tab-pane>
<!-- 通知设置 -->
<a-tab-pane key="notification" tab="通知设置">
<a-card title="系统通知配置">
<a-form layout="vertical">
<a-form-item label="启用邮件通知">
<a-switch v-model:checked="notificationSettings.email_enabled" />
</a-form-item>
<a-form-item label="启用短信通知">
<a-switch v-model:checked="notificationSettings.sms_enabled" />
</a-form-item>
<a-form-item label="启用站内通知">
<a-switch v-model:checked="notificationSettings.in_app_enabled" />
</a-form-item>
<a-divider />
<h3>通知类型</h3>
<a-form-item label="新用户注册">
<a-checkbox-group v-model:value="notificationSettings.types.new_user">
<a-checkbox value="email">邮件</a-checkbox>
<a-checkbox value="sms">短信</a-checkbox>
<a-checkbox value="in_app">站内</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="新保单创建">
<a-checkbox-group v-model:value="notificationSettings.types.new_policy">
<a-checkbox value="email">邮件</a-checkbox>
<a-checkbox value="sms">短信</a-checkbox>
<a-checkbox value="in_app">站内</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="理赔申请提交">
<a-checkbox-group v-model:value="notificationSettings.types.new_claim">
<a-checkbox value="email">邮件</a-checkbox>
<a-checkbox value="sms">短信</a-checkbox>
<a-checkbox value="in_app">站内</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="系统告警">
<a-checkbox-group v-model:value="notificationSettings.types.system_alert">
<a-checkbox value="email">邮件</a-checkbox>
<a-checkbox value="sms">短信</a-checkbox>
<a-checkbox value="in_app">站内</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveNotificationSettings">保存设置</a-button>
</a-form-item>
</a-form>
</a-card>
<a-card title="短信服务配置" style="margin-top: 16px">
<a-form layout="vertical">
<a-form-item label="短信服务商">
<a-select v-model:value="smsSettings.provider">
<a-select-option value="aliyun">阿里云</a-select-option>
<a-select-option value="tencent">腾讯云</a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="Access Key">
<a-input v-model:value="smsSettings.access_key" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="Access Secret">
<a-input-password v-model:value="smsSettings.access_secret" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="签名">
<a-input v-model:value="smsSettings.sign_name" />
</a-form-item>
<a-form-item label="模板ID">
<a-input v-model:value="smsSettings.template_id" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveSmsSettings">保存设置</a-button>
<a-button style="margin-left: 8px" @click="testSmsSettings">测试发送</a-button>
</a-form-item>
</a-form>
</a-card>
</a-tab-pane>
<!-- 备份设置 -->
<a-tab-pane key="backup" tab="备份设置">
<a-card title="数据备份配置">
<a-form layout="vertical">
<a-form-item label="自动备份">
<a-switch v-model:checked="backupSettings.auto_backup" />
</a-form-item>
<a-form-item label="备份频率">
<a-select v-model:value="backupSettings.frequency" :disabled="!backupSettings.auto_backup">
<a-select-option value="daily">每天</a-select-option>
<a-select-option value="weekly">每周</a-select-option>
<a-select-option value="monthly">每月</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="备份时间" v-if="backupSettings.auto_backup">
<a-time-picker
v-model:value="backupSettings.backup_time"
format="HH:mm"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="保留备份天数">
<a-input-number
v-model:value="backupSettings.retention_days"
:min="1"
:max="365"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="备份路径">
<a-input
v-model:value="backupSettings.backup_path"
placeholder="/path/to/backup"
/>
<a-button
type="link"
style="padding-left: 0"
@click="browseBackupPath"
>
选择路径
</a-button>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="saveBackupSettings">保存设置</a-button>
<a-button
style="margin-left: 8px"
@click="manualBackup"
:loading="backupLoading"
>
立即备份
</a-button>
</a-form-item>
</a-form>
</a-card>
<a-card title="备份记录" style="margin-top: 16px">
<a-table
:columns="backupColumns"
:data-source="backupList"
:loading="backupLoading"
:pagination="{ pageSize: 5 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'size'">
<span>{{ formatFileSize(record.size) }}</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" @click="downloadBackup(record)">下载</a-button>
<a-button size="small" @click="restoreBackup(record)">恢复</a-button>
<a-popconfirm
title="确定要删除这个备份吗?"
@confirm="deleteBackup(record)"
>
<a-button size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
const activeTab = ref('general')
// 基本设置
const generalFormRef = ref()
const generalForm = reactive({
system_name: '保险管理系统',
system_version: '1.0.0',
system_description: '专业的保险业务管理系统',
customer_service_phone: '400-123-4567',
customer_service_email: 'service@insurance.com',
company_address: '北京市朝阳区某某大厦A座1001室'
})
const generalRules = {
system_name: [{ required: true, message: '请输入系统名称' }],
customer_service_phone: [{ required: true, message: '请输入客服电话' }],
customer_service_email: [
{ required: true, message: '请输入客服邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
]
}
const systemStatus = reactive({
uptime: '15天2小时',
memory_usage: '45%',
database_status: '正常',
last_backup: '2024-01-20 02:00:00',
user_count: '128',
policy_count: '456'
})
// 邮件设置
const emailFormRef = ref()
const emailForm = reactive({
smtp_host: 'smtp.example.com',
smtp_port: 587,
smtp_secure: 'tls',
from_email: 'noreply@insurance.com',
from_name: '保险管理系统',
smtp_username: 'user@example.com',
smtp_password: '********'
})
const emailRules = {
smtp_host: [{ required: true, message: '请输入SMTP服务器地址' }],
smtp_port: [{ required: true, message: '请输入端口号' }],
from_email: [
{ required: true, message: '请输入发件邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
],
from_name: [{ required: true, message: '请输入发件人名称' }]
}
const emailTemplates = reactive({
welcome: {
subject: '欢迎加入{system_name}',
content: '尊敬的{username},欢迎您使用{system_name}'
},
policy: {
subject: '您的保单已创建成功',
content: '尊敬的{policyholder_name},您的保单{policy_number}已创建成功,保额{coverage_amount}元。'
},
claim: {
subject: '理赔申请已受理',
content: '尊敬的{applicant_name},您的理赔申请{claim_number}已受理,申请金额{claim_amount}元。'
}
})
// 通知设置
const notificationSettings = reactive({
email_enabled: true,
sms_enabled: false,
in_app_enabled: true,
types: {
new_user: ['email', 'in_app'],
new_policy: ['email'],
new_claim: ['email', 'in_app'],
system_alert: ['email', 'sms']
}
})
const smsSettings = reactive({
provider: 'aliyun',
access_key: '',
access_secret: '',
sign_name: '保险服务',
template_id: ''
})
// 备份设置
const backupSettings = reactive({
auto_backup: true,
frequency: 'daily',
backup_time: null,
retention_days: 30,
backup_path: '/backups'
})
const backupLoading = ref(false)
const backupList = ref([
{
id: 1,
name: 'backup_20240120_020000.sql',
size: 1024000,
create_time: '2024-01-20 02:00:00',
type: '自动'
},
{
id: 2,
name: 'backup_20240119_020000.sql',
size: 1023000,
create_time: '2024-01-19 02:00:00',
type: '自动'
}
])
const backupColumns = [
{
title: '备份文件',
dataIndex: 'name',
key: 'name'
},
{
title: '大小',
key: 'size',
dataIndex: 'size'
},
{
title: '备份时间',
dataIndex: 'create_time',
key: 'create_time'
},
{
title: '类型',
dataIndex: 'type',
key: 'type'
},
{
title: '操作',
key: 'action',
width: 200
}
]
const saveGeneralSettings = async () => {
try {
await generalFormRef.value.validate()
// await systemAPI.saveGeneralSettings(generalForm)
message.success('基本设置保存成功')
} catch (error) {
console.log('表单验证失败', error)
}
}
const resetGeneralForm = () => {
generalFormRef.value.resetFields()
}
const saveEmailSettings = async () => {
try {
await emailFormRef.value.validate()
// await systemAPI.saveEmailSettings(emailForm)
message.success('邮件设置保存成功')
} catch (error) {
console.log('表单验证失败', error)
}
}
const testEmailSettings = async () => {
try {
// await systemAPI.testEmailSettings(emailForm)
message.success('邮件测试发送成功')
} catch (error) {
message.error('邮件测试失败')
}
}
const saveEmailTemplate = async (type) => {
try {
// await systemAPI.saveEmailTemplate(type, emailTemplates[type])
message.success('邮件模板保存成功')
} catch (error) {
message.error('保存失败')
}
}
const saveNotificationSettings = async () => {
try {
// await systemAPI.saveNotificationSettings(notificationSettings)
message.success('通知设置保存成功')
} catch (error) {
message.error('保存失败')
}
}
const saveSmsSettings = async () => {
try {
// await systemAPI.saveSmsSettings(smsSettings)
message.success('短信设置保存成功')
} catch (error) {
message.error('保存失败')
}
}
const testSmsSettings = async () => {
try {
// await systemAPI.testSmsSettings(smsSettings)
message.success('短信测试发送成功')
} catch (error) {
message.error('短信测试失败')
}
}
const saveBackupSettings = async () => {
try {
// await systemAPI.saveBackupSettings(backupSettings)
message.success('备份设置保存成功')
} catch (error) {
message.error('保存失败')
}
}
const browseBackupPath = () => {
message.info('路径选择功能开发中')
}
const manualBackup = async () => {
backupLoading.value = true
try {
// await systemAPI.manualBackup()
message.success('备份任务已启动')
} catch (error) {
message.error('备份失败')
} finally {
backupLoading.value = false
}
}
const downloadBackup = (record) => {
message.info(`开始下载备份: ${record.name}`)
}
const restoreBackup = (record) => {
message.info(`开始恢复备份: ${record.name}`)
}
const deleteBackup = (record) => {
message.success(`备份 ${record.name} 已删除`)
}
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
onMounted(() => {
// 加载系统设置
loadSystemSettings()
})
const loadSystemSettings = async () => {
try {
// const settings = await systemAPI.getSettings()
// Object.assign(generalForm, settings.general)
// Object.assign(emailForm, settings.email)
// Object.assign(notificationSettings, settings.notification)
// Object.assign(smsSettings, settings.sms)
// Object.assign(backupSettings, settings.backup)
} catch (error) {
message.error('加载系统设置失败')
}
}
</script>
<style scoped>
.system-settings {
padding: 0;
}
:deep(.ant-tabs-card) .ant-tabs-content {
margin-top: -16px;
}
:deep(.ant-tabs-card) .ant-tabs-content .ant-tabs-tabpane {
background: #fff;
padding: 16px;
}
:deep(.ant-tabs-card) .ant-tabs-bar {
border-color: #fff;
}
:deep(.ant-tabs-card) .ant-tabs-bar .ant-tabs-tab {
border-color: transparent;
background: transparent;
}
:deep(.ant-tabs-card) .ant-tabs-bar .ant-tabs-tab-active {
border-color: #fff;
background: #fff;
}
</style>

View File

@@ -0,0 +1,402 @@
<template>
<div class="user-management">
<a-page-header
title="用户管理"
sub-title="管理系统用户和权限"
>
<template #extra>
<a-button type="primary" @click="showModal">
<plus-outlined />
新增用户
</a-button>
</template>
</a-page-header>
<!-- 搜索区域 -->
<a-card style="margin-top: 16px">
<a-form layout="inline" :model="searchForm">
<a-form-item label="用户名">
<a-input
v-model:value="searchForm.username"
placeholder="请输入用户名"
@pressEnter="handleSearch"
/>
</a-form-item>
<a-form-item label="状态">
<a-select
v-model:value="searchForm.status"
placeholder="请选择状态"
style="width: 120px"
>
<a-select-option value="">全部</a-select-option>
<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-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<search-outlined />
搜索
</a-button>
<a-button style="margin-left: 8px" @click="resetSearch">
<redo-outlined />
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
<!-- 用户表格 -->
<a-card style="margin-top: 16px">
<a-table
:columns="columns"
:data-source="userList"
:loading="loading"
:pagination="pagination"
@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 === 'action'">
<a-space>
<a-button size="small" @click="handleEdit(record)">编辑</a-button>
<a-button
size="small"
:type="record.status === 'active' ? 'danger' : 'primary'"
@click="handleToggleStatus(record)"
>
{{ record.status === 'active' ? '禁用' : '启用' }}
</a-button>
<a-popconfirm
title="确定要删除这个用户吗?"
@confirm="handleDelete(record.id)"
>
<a-button size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
<!-- 新增/编辑模态框 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
layout="vertical"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item label="真实姓名" name="real_name">
<a-input v-model:value="formState.real_name" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" />
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" />
</a-form-item>
<a-form-item label="角色" name="role_id">
<a-select v-model:value="formState.role_id" placeholder="请选择角色">
<a-select-option :value="1">管理员</a-select-option>
<a-select-option :value="2">保险顾问</a-select-option>
<a-select-option :value="3">客服人员</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formState.status" placeholder="请选择状态">
<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-form-item>
<a-form-item v-if="!editingId" label="密码" name="password">
<a-input-password v-model:value="formState.password" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
PlusOutlined,
SearchOutlined,
RedoOutlined
} from '@ant-design/icons-vue'
import { userAPI } from '@/utils/api'
const loading = ref(false)
const modalVisible = ref(false)
const editingId = ref(null)
const userList = ref([])
const formRef = ref()
const searchForm = reactive({
username: '',
status: ''
})
const formState = reactive({
username: '',
real_name: '',
email: '',
phone: '',
role_id: null,
status: 'active',
password: ''
})
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
})
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '用户名',
dataIndex: 'username',
key: 'username'
},
{
title: '真实姓名',
dataIndex: 'real_name',
key: 'real_name'
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email'
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone'
},
{
title: '角色',
dataIndex: 'role_name',
key: 'role_name'
},
{
title: '状态',
key: 'status',
dataIndex: 'status'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right'
}
]
const rules = {
username: [{ required: true, message: '请输入用户名' }],
real_name: [{ required: true, message: '请输入真实姓名' }],
email: [
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
],
phone: [{ required: true, message: '请输入手机号' }],
role_id: [{ required: true, message: '请选择角色' }],
password: [{ required: true, message: '请输入密码' }]
}
const modalTitle = computed(() => {
return editingId.value ? '编辑用户' : '新增用户'
})
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] || '未知'
}
const loadUsers = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...searchForm
}
// 这里应该是实际的API调用
// const response = await userAPI.getList(params)
// userList.value = response.data.list
// pagination.total = response.data.total
// 模拟数据
userList.value = [
{
id: 1,
username: 'admin',
real_name: '管理员',
email: 'admin@example.com',
phone: '13800138000',
role_name: '管理员',
status: 'active',
created_at: '2024-01-01 10:00:00'
},
{
id: 2,
username: 'advisor1',
real_name: '张顾问',
email: 'advisor1@example.com',
phone: '13800138001',
role_name: '保险顾问',
status: 'active',
created_at: '2024-01-02 14:30:00'
}
]
pagination.total = 2
} catch (error) {
message.error('加载用户列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadUsers()
}
const resetSearch = () => {
searchForm.username = ''
searchForm.status = ''
handleSearch()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadUsers()
}
const showModal = () => {
editingId.value = null
Object.assign(formState, {
username: '',
real_name: '',
email: '',
phone: '',
role_id: null,
status: 'active',
password: ''
})
modalVisible.value = true
}
const handleEdit = (record) => {
editingId.value = record.id
Object.assign(formState, {
username: record.username,
real_name: record.real_name,
email: record.email,
phone: record.phone,
role_id: record.role_id,
status: record.status,
password: ''
})
modalVisible.value = true
}
const handleModalOk = async () => {
try {
await formRef.value.validate()
if (editingId.value) {
// await userAPI.update(editingId.value, formState)
message.success('用户更新成功')
} else {
// await userAPI.create(formState)
message.success('用户创建成功')
}
modalVisible.value = false
loadUsers()
} catch (error) {
console.log('表单验证失败', error)
}
}
const handleModalCancel = () => {
modalVisible.value = false
}
const handleToggleStatus = async (record) => {
try {
const newStatus = record.status === 'active' ? 'inactive' : 'active'
// await userAPI.update(record.id, { status: newStatus })
message.success('状态更新成功')
loadUsers()
} catch (error) {
message.error('状态更新失败')
}
}
const handleDelete = async (id) => {
try {
// await userAPI.delete(id)
message.success('用户删除成功')
loadUsers()
} catch (error) {
message.error('用户删除失败')
}
}
onMounted(() => {
loadUsers()
})
</script>
<style scoped>
.user-management {
padding: 0;
}
</style>

View File

@@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3001,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})