This commit is contained in:
405
src/views/audit/risks/index.vue
Normal file
405
src/views/audit/risks/index.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<ArtTableFullScreen class="risk-scroll-container">
|
||||
<div class="risk-page" id="table-full-screen">
|
||||
<ElCard shadow="never" class="filter-card">
|
||||
<ElForm>
|
||||
<ElRow :gutter="16" class="filter-grid">
|
||||
<ElCol :xs="24" :sm="12" :md="8" class="filter-col">
|
||||
<ElFormItem>
|
||||
<ElDatePicker
|
||||
v-model="dateRange"
|
||||
type="datetimerange"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="8" class="filter-col">
|
||||
<ElFormItem>
|
||||
<ElSelect v-model="query.risk" clearable placeholder="请选择风险">
|
||||
<ElOption
|
||||
v-for="item in overview?.risks || []"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="8" class="filter-col">
|
||||
<ElFormItem>
|
||||
<ElSelect v-model="query.result" clearable placeholder="请选择结果">
|
||||
<ElOption
|
||||
v-for="item in overview?.results || []"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="8" class="filter-col">
|
||||
<ElFormItem>
|
||||
<ElSelect v-model="query.action" filterable clearable placeholder="请选择动作">
|
||||
<ElOption
|
||||
v-for="item in overview?.actions || []"
|
||||
:key="item.code"
|
||||
:label="item.name"
|
||||
:value="item.code"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="12" :md="8" class="filter-col">
|
||||
<ElFormItem class="query-form-item">
|
||||
<ElButton type="primary" :icon="Search" @click="search">查询</ElButton>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
<div v-if="overview" class="metric-grid"
|
||||
><ElCard shadow="never" class="metric-card"
|
||||
><div class="metric-content"
|
||||
><span class="metric-icon metric-icon--primary"
|
||||
><ElIcon><DataAnalysis /></ElIcon></span
|
||||
><div
|
||||
><span>事件总数</span><strong>{{ overview.total }}</strong></div
|
||||
></div
|
||||
></ElCard
|
||||
><ElCard shadow="never" class="metric-card"
|
||||
><div class="metric-content"
|
||||
><span class="metric-icon metric-icon--danger"
|
||||
><ElIcon><WarningFilled /></ElIcon></span
|
||||
><div
|
||||
><span>高风险信号</span
|
||||
><strong class="danger">{{ signalCount('high_risk') }}</strong></div
|
||||
></div
|
||||
></ElCard
|
||||
><ElCard shadow="never" class="metric-card"
|
||||
><div class="metric-content"
|
||||
><span class="metric-icon metric-icon--warning"
|
||||
><ElIcon><Money /></ElIcon></span
|
||||
><div
|
||||
><span>资金信号</span><strong>{{ signalCount('finance') }}</strong></div
|
||||
></div
|
||||
></ElCard
|
||||
><ElCard shadow="never" class="metric-card"
|
||||
><div class="metric-content"
|
||||
><span class="metric-icon metric-icon--success"
|
||||
><ElIcon><Lock /></ElIcon></span
|
||||
><div
|
||||
><span>安全信号</span><strong>{{ signalCount('security') }}</strong></div
|
||||
></div
|
||||
></ElCard
|
||||
></div
|
||||
>
|
||||
<div v-if="overview" class="overview-grid"
|
||||
><ElCard shadow="never"
|
||||
><template #header
|
||||
><div class="section-title"
|
||||
><ElIcon><WarningFilled /></ElIcon><span>风险分布</span></div
|
||||
></template
|
||||
><AuditDistributionChart
|
||||
title="风险分布"
|
||||
:data="riskChartData"
|
||||
@select="apply('risk', $event)" /></ElCard
|
||||
><ElCard shadow="never"
|
||||
><template #header
|
||||
><div class="section-title"
|
||||
><ElIcon><CircleCheckFilled /></ElIcon><span>结果分布</span></div
|
||||
></template
|
||||
><AuditDistributionChart
|
||||
title="结果分布"
|
||||
:data="resultChartData"
|
||||
@select="apply('result', $event)" /></ElCard
|
||||
><ElCard shadow="never"
|
||||
><template #header
|
||||
><div class="section-title"
|
||||
><ElIcon><Connection /></ElIcon><span>来源分布</span></div
|
||||
></template
|
||||
><AuditDistributionChart
|
||||
title="来源分布"
|
||||
type="bar"
|
||||
:data="sourceChartData"
|
||||
@select="apply('source', $event)" /></ElCard
|
||||
></div>
|
||||
<ElCard shadow="never" class="art-table-card" v-loading="loading"
|
||||
><ArtTableHeader v-model:columns="headerColumns" @refresh="load"
|
||||
><template #left
|
||||
><div class="card-title"
|
||||
><span class="section-title"
|
||||
><ElIcon><DataAnalysis /></ElIcon><span>风险事件明细</span></span
|
||||
></div
|
||||
></template
|
||||
></ArtTableHeader
|
||||
><div class="risk-event-table">
|
||||
<AuditEventTable
|
||||
:items="events"
|
||||
@detail="(row) => router.push(`/audit/events/${encodeURIComponent(row.event_id)}`)"
|
||||
/>
|
||||
</div>
|
||||
<div class="pagination"
|
||||
><ElPagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.page_size"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@change="loadEvents" /></div
|
||||
></ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
CircleCheckFilled,
|
||||
Connection,
|
||||
DataAnalysis,
|
||||
Lock,
|
||||
Money,
|
||||
Search,
|
||||
WarningFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
import { AuditService } from '@/api/modules'
|
||||
import type {
|
||||
AuditEventView,
|
||||
AuditNamedCount,
|
||||
AuditRiskEventQuery,
|
||||
AuditRiskOverview
|
||||
} from '@/types/api'
|
||||
import AuditEventTable from '@/components/business/audit/AuditEventTable.vue'
|
||||
import AuditDistributionChart from '@/components/business/audit/AuditDistributionChart.vue'
|
||||
|
||||
defineOptions({ name: 'AuditRisks' })
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const headerColumns = ref([])
|
||||
const dateRange = ref<string[]>([])
|
||||
const overview = ref<AuditRiskOverview>()
|
||||
const events = ref<AuditEventView[]>([])
|
||||
const total = ref(0)
|
||||
const query = reactive<AuditRiskEventQuery>({ page: 1, page_size: 20 })
|
||||
const aggregateChartData = (items: AuditNamedCount[]) => {
|
||||
const totals = new Map<string, AuditNamedCount>()
|
||||
items.forEach((item) => {
|
||||
const key = item.name || item.code
|
||||
const current = totals.get(key)
|
||||
if (current) current.count += Number(item.count) || 0
|
||||
else totals.set(key, { ...item, count: Number(item.count) || 0 })
|
||||
})
|
||||
return [...totals.values()].sort((a, b) => b.count - a.count)
|
||||
}
|
||||
const riskChartData = computed(() => aggregateChartData(overview.value?.risks || []))
|
||||
const resultChartData = computed(() => aggregateChartData(overview.value?.results || []))
|
||||
const sourceChartData = computed(() => aggregateChartData(overview.value?.sources || []))
|
||||
const syncDates = () => {
|
||||
query.created_from = dateRange.value?.[0] || undefined
|
||||
query.created_to = dateRange.value?.[1] || undefined
|
||||
if (
|
||||
dateRange.value?.length === 2 &&
|
||||
new Date(dateRange.value[1]).getTime() - new Date(dateRange.value[0]).getTime() >
|
||||
31 * 86400000
|
||||
) {
|
||||
ElMessage.warning('风险查询时间范围最长为 31 天')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
const loadEvents = async () => {
|
||||
if (!syncDates()) return
|
||||
const data = (await AuditService.getRiskEvents(query)).data
|
||||
events.value = data.items
|
||||
total.value = data.total
|
||||
}
|
||||
const load = async () => {
|
||||
if (!syncDates()) return
|
||||
loading.value = true
|
||||
try {
|
||||
overview.value = (await AuditService.getRiskOverview(query)).data
|
||||
await loadEvents()
|
||||
} catch {
|
||||
ElMessage.error('风险调查数据加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const search = () => {
|
||||
query.page = 1
|
||||
load()
|
||||
}
|
||||
const signalCount = (code: string) =>
|
||||
overview.value?.signals.find((item) => item.code === code)?.count || 0
|
||||
const apply = (field: 'risk' | 'result' | 'source', code: string) => {
|
||||
;(query as Record<string, unknown>)[field] = code
|
||||
search()
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.risk-scroll-container {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.risk-page {
|
||||
box-sizing: border-box;
|
||||
flex: none !important;
|
||||
height: auto !important;
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.metric-grid,
|
||||
.overview-grid {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.filter-card :deep(.el-form-item) {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-card :deep(.el-form-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.filter-card :deep(.el-select),
|
||||
.filter-card :deep(.el-date-editor) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.query-form-item :deep(.el-button) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metric-grid span {
|
||||
display: block;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
display: inline-flex !important;
|
||||
flex: 0 0 42px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
font-size: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.metric-icon--primary {
|
||||
color: var(--el-color-primary) !important;
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.metric-icon--danger {
|
||||
color: var(--el-color-danger) !important;
|
||||
background: var(--el-color-danger-light-9);
|
||||
}
|
||||
|
||||
.metric-icon--warning {
|
||||
color: var(--el-color-warning) !important;
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
|
||||
.metric-icon--success {
|
||||
color: var(--el-color-success) !important;
|
||||
background: var(--el-color-success-light-9);
|
||||
}
|
||||
|
||||
.metric-grid strong {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: inline-flex;
|
||||
gap: 7px;
|
||||
align-items: center;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.risk-event-table {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.metric-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width >= 1200px) {
|
||||
.filter-col {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 520px) {
|
||||
.risk-page {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user