feat: 新增AI监控仪表盘前端页面和功能
- 新增AI监控仪表盘入口页面(DashEntry.vue) - 新增AI监控相关API(ai-dash-entry) - 新增导入对话框组件(ImportDialog) - 各模块页面新增导入按钮和功能 - 优化国际化配置和路由权限 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6c6c946b04
commit
e8d78a1aea
71
src/api/prison/ai-dash-entry/index.ts
Normal file
71
src/api/prison/ai-dash-entry/index.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
/** 风险分布数据项 */
|
||||
export interface RiskDistributionVO {
|
||||
name: string
|
||||
value: number
|
||||
color: string
|
||||
}
|
||||
|
||||
/** 风险趋势数据项 */
|
||||
export interface RiskTrendVO {
|
||||
month: string
|
||||
highRisk: number
|
||||
warning: number
|
||||
normal: number
|
||||
}
|
||||
|
||||
/** AI心航360°统计数据 */
|
||||
export interface AiDashEntryStatisticsVO {
|
||||
// 统计卡片
|
||||
totalCount: number
|
||||
monthlyNewCount: number
|
||||
monthlyChange: number
|
||||
highRiskCount: number
|
||||
highRiskMonthlyNew: number
|
||||
highRiskMonthlyChange: number
|
||||
warningCount: number
|
||||
warningMonthlyNew: number
|
||||
warningMonthlyChange: number
|
||||
normalCount: number
|
||||
normalMonthlyNew: number
|
||||
normalMonthlyChange: number
|
||||
// 图表数据
|
||||
riskDistribution: RiskDistributionVO[]
|
||||
riskTrendData: RiskTrendVO[]
|
||||
}
|
||||
|
||||
/** 重点关注对象 */
|
||||
export interface FocusPersonVO {
|
||||
id: number
|
||||
name: string
|
||||
gender: string
|
||||
age: number
|
||||
riskLevelType: string
|
||||
riskLevel: string
|
||||
supervisionArea: string
|
||||
psychologicalRiskLevel: string
|
||||
isNew: boolean
|
||||
}
|
||||
|
||||
/** 重点关注对象分页请求 */
|
||||
export interface FocusPersonPageReqVO {
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
riskLevelType?: string
|
||||
name?: string
|
||||
areaId?: number
|
||||
}
|
||||
|
||||
/** AI心航360° API */
|
||||
export const AiDashEntryApi = {
|
||||
/** 获取AI心航360°统计数据 */
|
||||
getStatistics: async (): Promise<AiDashEntryStatisticsVO> => {
|
||||
return await request.get({ url: '/prison/dashboard/ai-dash-entry/statistics' })
|
||||
},
|
||||
|
||||
/** 获取重点关注对象分页列表 */
|
||||
getFocusPersonPage: async (params: FocusPersonPageReqVO) => {
|
||||
return await request.get({ url: '/prison/dashboard/ai-dash-entry/focus-person-page', params })
|
||||
}
|
||||
}
|
||||
@ -90,5 +90,17 @@ export const ConsumptionApi = {
|
||||
// 导出消费订单 Excel
|
||||
exportConsumption: async (params: ConsumptionPageParams) => {
|
||||
return await request.download({ url: `/prison/consumption/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取导入模板
|
||||
getImportTemplate: async () => {
|
||||
return await request.download({ url: `/prison/consumption/get-import-template` })
|
||||
},
|
||||
|
||||
// 导入消费记录
|
||||
importConsumption: async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return await request.upload({ url: `/prison/consumption/import`, data: formData })
|
||||
}
|
||||
}
|
||||
|
||||
@ -119,5 +119,10 @@ export const RiskApi = {
|
||||
// 导出风险评估 Excel
|
||||
exportRisk: async (params) => {
|
||||
return await request.download({ url: `/prison/risk/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取导入模板
|
||||
getImportTemplate: async () => {
|
||||
return await request.download({ url: `/prison/risk/get-import-template` })
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,5 +72,10 @@ export const RiskAssessmentApi = {
|
||||
// 导出危险评估 Excel
|
||||
exportRiskAssessment: async (params: RiskAssessmentPageParams) => {
|
||||
return await request.download({ url: `/prison/risk-assessment/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取导入模板
|
||||
getImportTemplate: async () => {
|
||||
return await request.download({ url: `/prison/risk-assessment/get-import-template` })
|
||||
}
|
||||
}
|
||||
@ -74,5 +74,10 @@ export const ScoreApi = {
|
||||
// 导出计分考核 Excel
|
||||
exportScore: async (params: ScorePageParams) => {
|
||||
return await request.download({ url: `/prison/score/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取导入模板
|
||||
getImportTemplate: async () => {
|
||||
return await request.download({ url: `/prison/score/get-import-template` })
|
||||
}
|
||||
}
|
||||
@ -114,6 +114,11 @@ export const SituationApi = {
|
||||
return await request.download({ url: `/prison/situation/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取导入模板
|
||||
getImportTemplate: async () => {
|
||||
return await request.download({ url: `/prison/situation/get-import-template` })
|
||||
},
|
||||
|
||||
// 导出 AreaApi 供页面使用
|
||||
AreaApi
|
||||
}
|
||||
|
||||
@ -169,6 +169,11 @@ export const WarningApi = {
|
||||
return await request.download({ url: `/prison/warning/export-excel`, params })
|
||||
},
|
||||
|
||||
// 获取导入模板
|
||||
getImportTemplate: async () => {
|
||||
return await request.download({ url: `/prison/warning/get-import-template` })
|
||||
},
|
||||
|
||||
// 导出 AreaApi 供页面使用
|
||||
AreaApi
|
||||
}
|
||||
|
||||
BIN
src/assets/imgs/avatar.png
Normal file
BIN
src/assets/imgs/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
163
src/components/ImportDialog/index.vue
Normal file
163
src/components/ImportDialog/index.vue
Normal file
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="title" width="400px">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
v-model:file-list="fileList"
|
||||
:action="importUrl"
|
||||
:headers="uploadHeaders"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleError"
|
||||
:accept="accept"
|
||||
drag
|
||||
>
|
||||
<Icon icon="ep:upload-filled" class="text-48px text-gray-400" />
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip text-center">
|
||||
<div class="el-upload__tip">
|
||||
<el-checkbox v-model="updateSupport" v-if="showUpdateSupport" />
|
||||
是否更新已经存在的数据
|
||||
</div>
|
||||
<span>仅允许导入 xls、xlsx 格式文件。</span>
|
||||
<el-link
|
||||
v-if="templateUrl"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
style="font-size: 12px; vertical-align: baseline"
|
||||
@click="downloadTemplate"
|
||||
>
|
||||
下载模板
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" :disabled="fileList.length === 0" :loading="loading" @click="submitFileForm">
|
||||
确 定
|
||||
</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
import download from '@/utils/download'
|
||||
|
||||
defineOptions({ name: 'ImportDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
importUrl: string // 导入接口地址
|
||||
templateUrl?: string // 模板下载接口地址(可选,不传则不显示下载模板链接)
|
||||
templateName?: string // 模板文件名
|
||||
title?: string // 对话框标题
|
||||
showUpdateSupport?: boolean // 是否显示更新支持选项
|
||||
accept?: string // 接受的文件类型
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['success'])
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const updateSupport = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
const uploadRef = ref()
|
||||
|
||||
/** 上传请求头 - 使用 ref 而不是 computed,在提交时动态设置 */
|
||||
const uploadHeaders = ref<Record<string, string>>({})
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
fileList.value = []
|
||||
updateSupport.value = false
|
||||
}
|
||||
defineExpose({ open })
|
||||
|
||||
/** 文件数量超出限制 */
|
||||
const handleExceed = () => {
|
||||
message.error('最多只能上传一个文件!')
|
||||
}
|
||||
|
||||
/** 上传成功 */
|
||||
const handleSuccess = (response: any) => {
|
||||
loading.value = false
|
||||
if (response.code === 0) {
|
||||
const data = response.data
|
||||
let text = `导入成功 ${data.successCount} 条`
|
||||
if (data.failureCount > 0) {
|
||||
text += `,失败 ${data.failureCount} 条`
|
||||
// 显示失败详情
|
||||
if (data.failureRecords && Object.keys(data.failureRecords).length > 0) {
|
||||
const failDetails = Object.entries(data.failureRecords)
|
||||
.map(([row, reason]) => `第${row}行:${reason}`)
|
||||
.join('\n')
|
||||
message.alert(`${text}\n\n失败详情:\n${failDetails}`)
|
||||
} else {
|
||||
message.warning(text)
|
||||
}
|
||||
} else {
|
||||
message.success(text)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
emit('success')
|
||||
} else {
|
||||
message.error(response.msg || '导入失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 上传失败 */
|
||||
const handleError = () => {
|
||||
loading.value = false
|
||||
message.error('上传失败,请检查文件格式')
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitFileForm = () => {
|
||||
if (fileList.value.length === 0) {
|
||||
message.warning('请选择要上传的文件')
|
||||
return
|
||||
}
|
||||
// 在提交时才获取最新的 token 和 tenant-id
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: 'Bearer ' + getAccessToken()
|
||||
}
|
||||
const tenantId = getTenantId()
|
||||
if (tenantId) {
|
||||
headers['tenant-id'] = String(tenantId)
|
||||
}
|
||||
uploadHeaders.value = headers
|
||||
loading.value = true
|
||||
uploadRef.value?.submit()
|
||||
}
|
||||
|
||||
/** 下载模板 */
|
||||
const downloadTemplate = async () => {
|
||||
if (!props.templateUrl) return
|
||||
try {
|
||||
// 拼接完整的 API 地址:VITE_BASE_URL + VITE_API_URL + templateUrl
|
||||
const apiUrl = import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + props.templateUrl
|
||||
const response = await fetch(apiUrl, {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + getAccessToken(),
|
||||
'tenant-id': getTenantId() || ''
|
||||
}
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
const blob = await response.blob()
|
||||
download.excel(blob, props.templateName || '导入模板.xls')
|
||||
} catch (e) {
|
||||
console.error('下载模板失败:', e)
|
||||
message.error('下载模板失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -82,14 +82,18 @@ service.interceptors.request.use(
|
||||
const isFormUrlEncoded =
|
||||
contentType === 'application/x-www-form-urlencoded' ||
|
||||
contentType.includes('application/x-www-form-urlencoded')
|
||||
const isMultipartFormData =
|
||||
contentType === 'multipart/form-data' ||
|
||||
contentType.includes('multipart/form-data')
|
||||
if (isFormUrlEncoded) {
|
||||
// 使用表单序列化
|
||||
if (config.data && typeof config.data !== 'string') {
|
||||
config.data = qs.stringify(config.data, { allowDots: true, indices: false })
|
||||
}
|
||||
} else {
|
||||
} else if (!isMultipartFormData) {
|
||||
// 默认使用 JSON 序列化,确保数组被正确序列化为 JSON 数组
|
||||
// 这包括 'application/json' 以及其他情况
|
||||
// 注意:multipart/form-data 类型不进行转换,需要保持 FormData 对象原样
|
||||
if (config.data && typeof config.data === 'object') {
|
||||
config.data = JSON.stringify(config.data)
|
||||
config.headers['Content-Type'] = 'application/json'
|
||||
|
||||
@ -113,8 +113,8 @@ export default {
|
||||
small: 'Small'
|
||||
},
|
||||
login: {
|
||||
welcome: 'Welcome to the system',
|
||||
message: 'Backstage management system',
|
||||
welcome: 'Welcome to AI Xinhang 360°',
|
||||
message: 'Focusing on psychological needs of different individuals, building a full-process, intelligent one-stop psychological evaluation service platform, making psychological assessment more professional, efficient, and caring.',
|
||||
tenantname: 'TenantName',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
|
||||
@ -114,8 +114,8 @@ export default {
|
||||
small: '小'
|
||||
},
|
||||
login: {
|
||||
welcome: '欢迎使用本系统',
|
||||
message: '开箱即用的中后台管理系统',
|
||||
welcome: '欢迎使用AI心航360°',
|
||||
message: '聚焦不同人员心理需求,构建全流程、智能化的一站式心理测评服务平台,让心理评估更专业、更高效、更贴心。',
|
||||
tenantname: '租户名称',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
@ -470,4 +470,4 @@ export default {
|
||||
}
|
||||
},
|
||||
'OAuth 2.0': 'OAuth 2.0' // 避免菜单名是 OAuth 2.0 时,一直 warn 报错
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,18 +55,13 @@ const whiteList = [
|
||||
'/register',
|
||||
'/oauthLogin/gitee',
|
||||
'/dashboard', // Dashboard 页面
|
||||
'/dashentry' // DashEntry 页面
|
||||
'/ai-dash-entry' // DashEntry 页面
|
||||
]
|
||||
|
||||
// 路由加载前
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
start()
|
||||
loadStart()
|
||||
// 如果是主页路径或 dashboard 路径,直接放行(跳过权限验证)
|
||||
if (to.path === '/dashboard' || to.path === '/dashentry') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (getAccessToken()) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
@ -100,6 +95,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
}
|
||||
} else {
|
||||
if (whiteList.indexOf(to.path) !== -1) {
|
||||
const permissionStore = usePermissionStoreWithOut()
|
||||
if (permissionStore.getRouters.length === 0) {
|
||||
await permissionStore.generateRoutes()
|
||||
}
|
||||
next()
|
||||
} else {
|
||||
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
|
||||
|
||||
@ -59,7 +59,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/Home/Index.vue'),
|
||||
component: () => import('@/views/DashEntry/DashEntry.vue'),
|
||||
name: 'Index',
|
||||
meta: {
|
||||
title: t('router.home'),
|
||||
@ -186,16 +186,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/dashentry',
|
||||
component: () => import('@/views/DashEntry/DashEntry.vue'),
|
||||
name: 'DashEntry',
|
||||
meta: {
|
||||
hidden: true,
|
||||
title: 'DashEntry',
|
||||
noTagsView: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
component: () => import('@/views/Login/Login.vue'),
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
<!-- 底部表格 -->
|
||||
<div class="table-section">
|
||||
<div class="table-title">重点关注对象列表</div>
|
||||
<el-table :data="paginatedResults" style="width: 100%" stripe>
|
||||
<el-table :data="tableData.results" style="width: 100%" stripe v-loading="loading">
|
||||
<el-table-column prop="name" label="姓名" width="`16.3%`" class-name="name-column">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.isNew" class="new-tag" :class="`tag-${row.riskLevelType}`">新增</span>
|
||||
@ -83,226 +83,276 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
// @ts-ignore
|
||||
import EChart from '@/components/Echart/src/Echart.vue'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { AiDashEntryApi, type AiDashEntryStatisticsVO, type FocusPersonVO } from '@/api/prison/ai-dash-entry'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'DashEntry' })
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
|
||||
// 统计数据
|
||||
const statistics = ref<AiDashEntryStatisticsVO>({
|
||||
totalCount: 0,
|
||||
monthlyNewCount: 0,
|
||||
monthlyChange: 0,
|
||||
highRiskCount: 0,
|
||||
highRiskMonthlyNew: 0,
|
||||
highRiskMonthlyChange: 0,
|
||||
warningCount: 0,
|
||||
warningMonthlyNew: 0,
|
||||
warningMonthlyChange: 0,
|
||||
normalCount: 0,
|
||||
normalMonthlyNew: 0,
|
||||
normalMonthlyChange: 0,
|
||||
riskDistribution: [],
|
||||
riskTrendData: []
|
||||
})
|
||||
|
||||
// 统计数据卡片
|
||||
const statsCards = ref([
|
||||
const statsCards = computed(() => [
|
||||
{
|
||||
title: '全部人员',
|
||||
value: '59人',
|
||||
subtitle: '本月45人 +15',
|
||||
trend: 'up',
|
||||
value: `${statistics.value.totalCount}人`,
|
||||
subtitle: `本月${statistics.value.monthlyNewCount}人 ${statistics.value.monthlyChange >= 0 ? '+' : ''}${statistics.value.monthlyChange}`,
|
||||
trend: statistics.value.monthlyChange >= 0 ? 'up' : 'down',
|
||||
type: 'all',
|
||||
icon: 'ep:user'
|
||||
},
|
||||
{
|
||||
title: '高危人员',
|
||||
value: '12人',
|
||||
subtitle: '本月3人 -3',
|
||||
trend: 'down',
|
||||
value: `${statistics.value.highRiskCount}人`,
|
||||
subtitle: `本月${statistics.value.highRiskMonthlyNew}人 ${statistics.value.highRiskMonthlyChange >= 0 ? '+' : ''}${statistics.value.highRiskMonthlyChange}`,
|
||||
trend: statistics.value.highRiskMonthlyChange >= 0 ? 'up' : 'down',
|
||||
type: 'high',
|
||||
icon: 'ep:warning-filled'
|
||||
},
|
||||
{
|
||||
title: '预警人员',
|
||||
value: '23人',
|
||||
subtitle: '本月4人 +2',
|
||||
trend: 'up',
|
||||
value: `${statistics.value.warningCount}人`,
|
||||
subtitle: `本月${statistics.value.warningMonthlyNew}人 ${statistics.value.warningMonthlyChange >= 0 ? '+' : ''}${statistics.value.warningMonthlyChange}`,
|
||||
trend: statistics.value.warningMonthlyChange >= 0 ? 'up' : 'down',
|
||||
type: 'warning',
|
||||
icon: 'ep:bell'
|
||||
},
|
||||
{
|
||||
title: '普通人员',
|
||||
value: '32人',
|
||||
subtitle: '本月5人 +1',
|
||||
trend: 'up',
|
||||
value: `${statistics.value.normalCount}人`,
|
||||
subtitle: `本月${statistics.value.normalMonthlyNew}人 ${statistics.value.normalMonthlyChange >= 0 ? '+' : ''}${statistics.value.normalMonthlyChange}`,
|
||||
trend: statistics.value.normalMonthlyChange >= 0 ? 'up' : 'down',
|
||||
type: 'normal',
|
||||
icon: 'ep:user-filled'
|
||||
}
|
||||
])
|
||||
|
||||
// 风险等级分布图表配置(环形图)
|
||||
const riskDistributionOptions = computed<EChartsOption>(() => ({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
show: true,
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
itemGap: 20,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
const riskDistributionOptions = computed<EChartsOption>(() => {
|
||||
const distribution = statistics.value.riskDistribution || []
|
||||
const total = distribution.reduce((sum, item) => sum + item.value, 0)
|
||||
|
||||
const data = distribution.map(item => {
|
||||
const percentage = total > 0 ? Math.round((item.value / total) * 100) : 0
|
||||
return {
|
||||
value: item.value,
|
||||
name: `${item.name} (${percentage}%)`,
|
||||
itemStyle: { color: item.color }
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c} ({d}%)'
|
||||
},
|
||||
data: ['普通 (80%)', '预警 (15%)', '高危 (5%)']
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '风险等级分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
legend: {
|
||||
show: true,
|
||||
bottom: 0,
|
||||
left: 'center',
|
||||
itemGap: 20,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{d}%'
|
||||
},
|
||||
emphasis: {
|
||||
data: data.map(item => item.name)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '风险等级分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{ value: 978, name: '普通 (80%)', itemStyle: { color: '#5470c6' } },
|
||||
{ value: 189, name: '预警 (15%)', itemStyle: { color: '#fac858' } },
|
||||
{ value: 67, name: '高危 (5%)', itemStyle: { color: '#ee6666' } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}))
|
||||
formatter: '{b}\n{d}%'
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: data
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 风险趋势图配置(折线图)
|
||||
const riskTrendOptions = computed<EChartsOption>(() => ({
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['高危', '预警', '普通'],
|
||||
bottom: 0
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['2024-10', '2024-11', '2024-12', '2025-01', '2025-02', '2025-03', '2025-04']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: 300,
|
||||
interval: 50
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '高危',
|
||||
type: 'line',
|
||||
data: [50, 52, 48, 51, 49, 50, 48],
|
||||
itemStyle: { color: '#ee6666' },
|
||||
lineStyle: { color: '#ee6666' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
const riskTrendOptions = computed<EChartsOption>(() => {
|
||||
const trendData = statistics.value.riskTrendData || []
|
||||
const months = trendData.map(item => item.month)
|
||||
const highRiskData = trendData.map(item => item.highRisk)
|
||||
const warningData = trendData.map(item => item.warning)
|
||||
const normalData = trendData.map(item => item.normal)
|
||||
|
||||
// 计算最大值用于设置Y轴范围
|
||||
const allValues = [...highRiskData, ...warningData, ...normalData]
|
||||
const maxValue = Math.max(...allValues, 10)
|
||||
const yAxisMax = Math.ceil(maxValue / 50) * 50 + 50
|
||||
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
{
|
||||
name: '预警',
|
||||
type: 'line',
|
||||
data: [150, 180, 170, 190, 185, 175, 180],
|
||||
itemStyle: { color: '#fac858' },
|
||||
lineStyle: { color: '#fac858', type: 'dashed' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
legend: {
|
||||
data: ['高危', '预警', '普通'],
|
||||
bottom: 0
|
||||
},
|
||||
{
|
||||
name: '普通',
|
||||
type: 'line',
|
||||
data: [250, 280, 270, 290, 285, 275, 280],
|
||||
itemStyle: { color: '#666' },
|
||||
lineStyle: { color: '#666' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
}
|
||||
]
|
||||
}))
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: months
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: yAxisMax,
|
||||
interval: Math.ceil(yAxisMax / 6)
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '高危',
|
||||
type: 'line',
|
||||
data: highRiskData,
|
||||
itemStyle: { color: '#ee6666' },
|
||||
lineStyle: { color: '#ee6666' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
{
|
||||
name: '预警',
|
||||
type: 'line',
|
||||
data: warningData,
|
||||
itemStyle: { color: '#fac858' },
|
||||
lineStyle: { color: '#fac858', type: 'dashed' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
},
|
||||
{
|
||||
name: '普通',
|
||||
type: 'line',
|
||||
data: normalData,
|
||||
itemStyle: { color: '#666' },
|
||||
lineStyle: { color: '#666' },
|
||||
symbol: 'circle',
|
||||
symbolSize: 6
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref({
|
||||
const tableData = ref<{
|
||||
current: number
|
||||
total: number
|
||||
results: FocusPersonVO[]
|
||||
}>({
|
||||
current: 1,
|
||||
total: 3,
|
||||
results: [
|
||||
{
|
||||
name: '王建国',
|
||||
isNew: true,
|
||||
gender: '男',
|
||||
age: 34,
|
||||
riskLevel: '高危',
|
||||
riskLevelType: 'high',
|
||||
supervisionArea: '第一监区',
|
||||
psychologicalRiskLevel: '一级风险'
|
||||
},
|
||||
{
|
||||
name: '李秀英',
|
||||
isNew: true,
|
||||
gender: '女',
|
||||
age: 29,
|
||||
riskLevel: '预警',
|
||||
riskLevelType: 'warning',
|
||||
supervisionArea: '第二监区',
|
||||
psychologicalRiskLevel: '二级风险'
|
||||
},
|
||||
{
|
||||
name: '张伟',
|
||||
isNew: false,
|
||||
gender: '男',
|
||||
age: 41,
|
||||
riskLevel: '普通',
|
||||
riskLevelType: 'normal',
|
||||
supervisionArea: '第一监区',
|
||||
psychologicalRiskLevel: '三级风险'
|
||||
}
|
||||
]
|
||||
total: 0,
|
||||
results: []
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pageSize = ref(10)
|
||||
|
||||
// 分页后的数据
|
||||
const paginatedResults = computed(() => {
|
||||
const start = (tableData.value.current - 1) * pageSize.value
|
||||
const end = start + pageSize.value
|
||||
return tableData.value.results.slice(start, end)
|
||||
})
|
||||
|
||||
// 处理每页大小变化
|
||||
const handleSizeChange = (val: number) => {
|
||||
pageSize.value = val
|
||||
tableData.value.current = 1 // 重置到第一页
|
||||
tableData.value.current = 1
|
||||
loadFocusPersonPage()
|
||||
}
|
||||
|
||||
// 处理当前页变化
|
||||
const handleCurrentChange = (val: number) => {
|
||||
tableData.value.current = val
|
||||
loadFocusPersonPage()
|
||||
}
|
||||
|
||||
const handleView = (row: any) => {
|
||||
const handleView = (row: FocusPersonVO) => {
|
||||
router.push({
|
||||
name: 'Dashboard',
|
||||
path: '/prisoner/prisoner/dashboard',
|
||||
query: {
|
||||
id: row.id || row.name
|
||||
prisonerId: row.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStatistics = async () => {
|
||||
try {
|
||||
const data = await AiDashEntryApi.getStatistics()
|
||||
if (data) {
|
||||
statistics.value = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
ElMessage.error('加载统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载重点关注对象列表
|
||||
const loadFocusPersonPage = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await AiDashEntryApi.getFocusPersonPage({
|
||||
pageNo: tableData.value.current,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
if (data) {
|
||||
tableData.value.results = data.list || []
|
||||
tableData.value.total = data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载重点关注对象失败:', error)
|
||||
ElMessage.error('加载重点关注对象列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatistics()
|
||||
loadFocusPersonPage()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -67,6 +67,14 @@
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['prison:consumption:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@ -166,6 +174,16 @@
|
||||
|
||||
<!-- 明细查看弹窗 -->
|
||||
<ConsumptionDetailDialog ref="detailDialogRef" />
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportDialog
|
||||
ref="importDialogRef"
|
||||
:import-url="getImportUrl()"
|
||||
template-url="/prison/consumption/get-import-template"
|
||||
template-name="消费记录导入模板.xls"
|
||||
title="导入消费记录"
|
||||
@success="getList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -175,6 +193,7 @@ import download from '@/utils/download'
|
||||
import { ConsumptionApi, Consumption } from '@/api/prison/consumption'
|
||||
import ConsumptionForm from './ConsumptionForm.vue'
|
||||
import ConsumptionDetailDialog from './ConsumptionDetailDialog.vue'
|
||||
import ImportDialog from '@/components/ImportDialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'Consumption' })
|
||||
|
||||
@ -273,6 +292,15 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入操作 */
|
||||
const importDialogRef = ref()
|
||||
const handleImport = () => {
|
||||
importDialogRef.value.open()
|
||||
}
|
||||
const getImportUrl = () => {
|
||||
return import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + '/prison/consumption/import'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@ -76,6 +76,14 @@
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增评估
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['prison:risk:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@ -172,6 +180,16 @@
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<RiskForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportDialog
|
||||
ref="importDialogRef"
|
||||
:import-url="getImportUrl()"
|
||||
template-url="/prison/risk/get-import-template"
|
||||
template-name="风险评估导入模板.xls"
|
||||
title="导入风险评估"
|
||||
@success="getList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -180,6 +198,7 @@ import { formatDateTime, formatDate } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { RiskApi, RiskPageReqVO } from '@/api/prison/risk'
|
||||
import RiskForm from './RiskForm.vue'
|
||||
import ImportDialog from '@/components/ImportDialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'PrisonRisk' })
|
||||
|
||||
@ -275,6 +294,15 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入操作 */
|
||||
const importDialogRef = ref()
|
||||
const handleImport = () => {
|
||||
importDialogRef.value.open()
|
||||
}
|
||||
const getImportUrl = () => {
|
||||
return import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + '/prison/risk/import'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@ -82,6 +82,14 @@
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['prison:risk-assessment:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@ -178,6 +186,16 @@
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<RiskAssessmentForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportDialog
|
||||
ref="importDialogRef"
|
||||
:import-url="getImportUrl()"
|
||||
template-url="/prison/risk-assessment/get-import-template"
|
||||
template-name="危险评估导入模板.xls"
|
||||
title="导入危险评估"
|
||||
@success="getList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -186,6 +204,7 @@ import { formatDateTime } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { RiskAssessmentApi, RiskAssessment } from '@/api/prison/riskassessment'
|
||||
import RiskAssessmentForm from './RiskAssessmentForm.vue'
|
||||
import ImportDialog from '@/components/ImportDialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'RiskAssessment' })
|
||||
|
||||
@ -281,6 +300,15 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入操作 */
|
||||
const importDialogRef = ref()
|
||||
const handleImport = () => {
|
||||
importDialogRef.value.open()
|
||||
}
|
||||
const getImportUrl = () => {
|
||||
return import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + '/prison/risk-assessment/import'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@ -76,6 +76,14 @@
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['prison:score:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@ -164,6 +172,16 @@
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ScoreForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportDialog
|
||||
ref="importDialogRef"
|
||||
:import-url="getImportUrl()"
|
||||
template-url="/prison/score/get-import-template"
|
||||
template-name="计分考核导入模板.xls"
|
||||
title="导入计分考核"
|
||||
@success="getList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -172,6 +190,7 @@ import { formatDateTime } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { ScoreApi, Score } from '@/api/prison/score'
|
||||
import ScoreForm from './ScoreForm.vue'
|
||||
import ImportDialog from '@/components/ImportDialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'Score' })
|
||||
|
||||
@ -266,6 +285,15 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入操作 */
|
||||
const importDialogRef = ref()
|
||||
const handleImport = () => {
|
||||
importDialogRef.value.open()
|
||||
}
|
||||
const getImportUrl = () => {
|
||||
return import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + '/prison/score/import'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@ -85,6 +85,14 @@
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增狱情
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['prison:situation:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@ -184,6 +192,16 @@
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<SituationForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportDialog
|
||||
ref="importDialogRef"
|
||||
:import-url="getImportUrl()"
|
||||
template-url="/prison/situation/get-import-template"
|
||||
template-name="狱情收集导入模板.xls"
|
||||
title="导入狱情收集"
|
||||
@success="getList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -192,6 +210,7 @@ import { formatDateTime } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import { SituationApi, SituationPageReqVO } from '@/api/prison/situation'
|
||||
import SituationForm from './SituationForm.vue'
|
||||
import ImportDialog from '@/components/ImportDialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'PrisonSituation' })
|
||||
|
||||
@ -307,6 +326,15 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入操作 */
|
||||
const importDialogRef = ref()
|
||||
const handleImport = () => {
|
||||
importDialogRef.value.open()
|
||||
}
|
||||
const getImportUrl = () => {
|
||||
return import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + '/prison/situation/import'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
@ -100,6 +100,14 @@
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增预警
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
@click="handleImport"
|
||||
v-hasPermi="['prison:warning:import']"
|
||||
>
|
||||
<Icon icon="ep:upload" class="mr-5px" /> 导入
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@ -201,6 +209,16 @@
|
||||
|
||||
<!-- 预警处置操作弹窗 -->
|
||||
<WarningActionForm ref="actionFormRef" @success="getList" />
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportDialog
|
||||
ref="importDialogRef"
|
||||
:import-url="getImportUrl()"
|
||||
template-url="/prison/warning/get-import-template"
|
||||
template-name="预警管理导入模板.xls"
|
||||
title="导入预警信息"
|
||||
@success="getList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -210,6 +228,7 @@ import download from '@/utils/download'
|
||||
import { WarningApi, WarningPageReqVO } from '@/api/prison/warning'
|
||||
import WarningForm from './WarningForm.vue'
|
||||
import WarningActionForm from './WarningActionForm.vue'
|
||||
import ImportDialog from '@/components/ImportDialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'PrisonWarning' })
|
||||
|
||||
@ -324,6 +343,15 @@ const handleExport = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导入操作 */
|
||||
const importDialogRef = ref()
|
||||
const handleImport = () => {
|
||||
importDialogRef.value.open()
|
||||
}
|
||||
const getImportUrl = () => {
|
||||
return import.meta.env.VITE_BASE_URL + (import.meta.env.VITE_API_URL || '/admin-api') + '/prison/warning/import'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user