This commit is contained in:
lcw
2026-01-20 17:06:37 +08:00
parent 10853312f2
commit 96283c3a0e
35 changed files with 4306 additions and 419 deletions

View File

@ -0,0 +1,185 @@
<template>
<div style="height:100%;width:100%" :id="echartsId"></div>
</template>
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, reactive, defineProps, onUnmounted, watch, nextTick } from "vue";
const props = defineProps({
echartsId:{
type:String,
default:'barId'
},
data:{
type:Object,
default:{
title:'', // 图表标题
color:[], //['#EB00FF','#F57100']
list:[], //[{label:'总数',val:[80,70,60,50]}, {label:'已处置',val:[70,40,30,80]}, ]
xData:[] ,//['09-01','09-02','09-03','09-04']
labelColor:'#000', //横坐标颜色 - 纵坐标颜色 - 标题颜色
rotate:0, //横坐标旋转角度
interval:0, //横坐标间隔
isVertical:false,//是否竖排垂直展示
}
},
dataZoom:{
type:Boolean,
default:false
},
rotate:{
type:Number,
default:0
}
});
watch(()=>props.data,val=>{
nextTick(()=>{
init(val)
})
},{immediate:true,deep:true})
// 初始化
function init (val) {
let color = val.color;
let list = val.list
let series = list.map((item ,idx)=>{
return {
type: "bar",
name:item.label,
data:item.val,
itemStyle:{normal: { color: color[idx] }},
showSymbol:false,
barWidth: '30%', // 柱状图宽度
}
})
chartFn(series)
}
function chartFn(series) {
var myChart = echarts.init(document.getElementById(props.echartsId));
var option = {
title: {
text: props.data.title || '',
left: 'center',
textStyle: {
color: props.data.color[0] || "#000",
fontSize: 14
}
},
grid: {
top: "25%",
right: "0%",
left: "0%",
bottom: "0%", // 增加底部空间为两行X轴标签留出空间
containLabel: true
},
legend: {
data: props.data.list.map(v => { return v.label }),
textStyle: {
color: props.data.color[0] || "#409EFF",
fontSize: 12
},
right: "5%",
top: "10%"
},
tooltip: {
trigger: "axis",
axisPointer: {
type: 'shadow'
},
backgroundColor: "rgba(255,255,255,1)",
padding: [5, 10],
textStyle: { color: "#7588E4" },
extraCssText: "box-shadow: 0 0 5px rgba(0,0,0,0.3)"
},
xAxis: {
type: "category",
data: props.data.xData,
axisTick: { alignWithLabel: true },
axisLine: {
show: true,
lineStyle: {
color: props.data.color[0] || "#409EFF"
}
},
axisLabel: {
rotate: props.rotate, // 设置标签旋转角度
show: true,
color: props.data.color[0] || "#409EFF",
fontSize: 10,
interval: props.data.interval || 0, // 强制显示所有标签
formatter: function(value, index) {
// 组合显示数量和年龄范围,数量在上,范围在下
const bottomValues = props.data.bottomValues || [];
const bottomValue = bottomValues[index] || '';
return `${bottomValue}\n${value}`;
},
margin: 10 // 调整边距
}
},
yAxis: {
type: "value",
axisLabel: {
color: props.data.color[0] || "#409EFF",
fontSize: 10,
formatter: '{value}%' // 显示百分比
},
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: props.data.color[0] || "#409EFF"
}
},
axisTick: { show: false },
axisLine: {
show: true,
lineStyle: {
color: props.data.color[0] || "#409EFF"
}
}
},
series: series.map((item, index) => ({
...item,
label: {
show: true,
position: 'top',
formatter: function(params) {
// 显示顶部百分比标签
return `占比 ${params.value}%`;
},
color: props.data.color[0] || "#409EFF",
fontSize: 10
},
itemStyle: {
normal: {
color: item.itemStyle.normal.color
}
}
})),
// // 底部数值标签
// graphic: props.data.bottomValues ? props.data.bottomValues.map((value, idx) => {
// const percent = (idx + 0.5) / props.data.xData.length * 100;
// return {
// type: 'text',
// left: `${percent}%`,
// bottom: '5%', // 调整到底部显示在x轴标签下方
// style: {
// text: value,
// fill: '#000',
// fontSize: 12
// }
// };
// }) : []
};
option && myChart.setOption(option);
window.addEventListener('resize', function() {
myChart.resize();
})
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,70 @@
<template>
<div class="table-data">
<table border="1" cellspacing="0" cellpadding="0" width="100%">
<tr style="text-align: center;font-size: 12pt;">
<th colspan="6">{{ title }}</th>
</tr>
<tr style="text-align: center;font-size: 12pt;font-weight: normal;">
<th v-if="lx == '0' || lx == '1'"></th>
<th v-for="item in titleList" :key="item">{{ item }}</th>
</tr>
<tr style="text-align: center;font-size: 12pt;font-family: 'Times New Roman';" v-for="value in condition"
:key="value">
<template v-if="lx == '0'">
<td>{{ value.tjlx }}</td>
<td>{{ value.ajsls }}</td>
<td>{{ value.ccs }}</td>
<td>{{ value.wfry }}</td>
</template>
<template v-if="lx == '1'">
<td>{{ value.tjlx }}</td>
<td>{{ value.rlggcx }}</td>
<td>{{ value.qhrsaq }}</td>
<td>{{ value.fhshglcx }}</td>
<td>{{ value.fhggaq }}</td>
</template>
<template v-if="lx == '2'">
<td>{{ value.ajlx }}</td>
<td>{{ value.sl }}</td>
<td>{{ value.zb }}</td>
</template>
<template v-if="lx == '3'">
<td>{{ value.afqy }}</td>
<td>{{ value.sl }}</td>
</template>
<template v-if="lx == '4'">
<td>{{ value.ssbm }}</td>
<td>{{ value.las }}</td>
<td>{{ value.pas }}</td>
<td>{{ value.ccl }}</td>
<td>{{ value.xyrs }}</td>
<td>{{ value.bz }}</td>
</template>
</tr>
</table>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: {
type: String,
default: ''
},
listData: {
type: Array,
default: () => []
},
titleList: {
type: Array,
default: () => []
}, lx: {
type: String,
default: '0'
}
})
const condition = computed(() => {
const data = props.listData.map(item => item)
return data
})
</script>

View File

@ -0,0 +1,464 @@
<template>
<div class="report-container" ref="tableBox">
<div ref="ajReport">
<div class="main-title">{{ wdValue.date }}份全市治安案件形势分析</div>
<div class="section-title">治安案件总体概述</div>
<div class="body-text" v-if="wdValue.zaajZtgsList">
{{ wdValue.ym }}月份全市各级公安机关治安部门受理行政案件{{ wdValue.zaajZtgsList.ajsls }}查处案件{{ wdValue.zaajZtgsList.ccs }}查处率{{
wdValue.zaajZtgsList.ccl }}%查处违法人员{{ wdValue.zaajZtgsList.wfry }}
<TableData title="全市治安案件总体情况表" :listData="wdValue.zaajZtgsList.ztqk" :titleList="['受理治安案件数', '查处数', '查处违法人员数']"
lx="0" />
</div>
<div class="section-title">治安案件形势分析</div>
<div>
<div class="body-text" v-if="wdValue.zaajList.length > 0">
受理案件类别分析{{ wdValue.ym }}月受理的{{ wdValue.zaajZtgsList.ajsls }}起行政案件中<span
v-for="(item, index) in wdValue.zaajList" :key="index">{{ item.label }}{{ item.sy }}起环比{{ item.sySj }}{{
item.hb }}同比{{ item.bySj }}{{ item.tb }}<span v-if="index == wdValue.zaajList.length - 1"></span> <span
v-else></span></span>
<TableData title="全市治安案件总体情况表" :listData="wdValue.ajxsfxList"
:titleList="['扰乱公共秩序', '侵害人身安全和财产权利', '妨害社会管理秩序', '妨害公共安全']" lx="1" />
</div>
<div class="body-text">
查处案件类别分析{{ wdValue.ym }}月份的查处63起行政案件中<span v-for="(item, index) in wdValue.zaajFbqktj" :key="index">
{{ item.ajlx }}{{ item.sl }}占查处案件总数的{{ item.zb }}<span
v-if="index == wdValue.zaajFbqktj.length - 1"></span>
<span v-else></span></span>
<TableData title="全市查处主要治安案件情况" :listData="wdValue.zaajFbqktj" :titleList="['案件类型', '查处数', '占查处总数的百分比']"
lx="2" />
</div>
<div class="body-text">
查处违法犯罪人员年龄分析{{ wdValue.ym }}月份共查处违法人员{{ wdValue.nldtjOnj.ccxyr }}其中<span
v-for="value in wdValue.nldtjOnj.nldtj.data" :key="value.agegroup">
{{ value.agegroup }} {{ value.count }}占查处违法人员总数的{{ value.zb }}<span
v-if="index == wdValue.nldtjOnj.nldtj.data.length - 1"></span><span v-else></span>
</span>
</div>
<div style="height:250px; width:280px;margin: 0 auto;">
<bar v-if="wdValue.nldtjOnj.nldtj" :echartsId="'myBarChart'" :data="{
title: '图一',
color: ['#FF6B6B'],
...wdValue.nldtjOnj.nldtj
}" />
</div>
<div class="body-text">
案件发生区域分析{{wdValue.sajajfsqy.qyfx }}
<TableData title="受理治安案件发生区域" :listData="wdValue.sajajfsqy.xxsj" :titleList="['案件发生区域', '案件数量']" lx="3" />
</div>
<div class="body-text">
治安案件地域分析{{ wdValue.ym }}月份全市治安案件平均查处率为{{ wdValue.zaajZtgsList.ccl }}%<span v-if="parseFloat(wdValue.zaajZtgsList.ccl) > 0"> <span v-for="value in wdValue.jxsajajdyfx.greaterThan" :key="value.ssbm">{{ value.ssbm }}、</span>查处率高,分别为<span v-for="value in wdValue.jxsajajdyfx.lessThan" :key="value.ssbm">{{ value.ccl }}</span><span v-for="value in wdValue.jxsajajdyfx.lessThan" :key="value.ssbm">{{ value.ccl }}</span></span><span></span>
<TableData title="各县(市)公安(分)局治安案件统计表" :listData="wdValue.jxsajajdyfx.res"
:titleList="['部门、(市)公安(分)局', '受理数', '查处数','查处率','查处违法人员数','备注']" lx="4" />
</div>
</div>
<div class="section-title">对策建议</div>
<div class="body-text">
{{ wdValue.xzjyL }}
</div>
</div>
</div>
<div style="display: flex;justify-content: center;align-items: center;">
<el-button type="primary" @click="downloadWithStyles">导出WORD</el-button>
</div>
</template>
<script setup>
import * as echarts from "echarts";
import { xsasjxsajztgs, xsasjxsajccajlbfx, xsasjxsajajxsfx, xsasjxsajnldtj, xsasjxsajajfsqy, xsasjxsajajdyfx, xsasjxsajaltdtj, xsasjxsajfbqktj, xsasjxsajFatdtj, xsasjxsajxyrQkfx, xsasjxsajypbgZttj, xsasjxsajqxajqk } from '@/api/fileapi'
import { ref, onMounted, watch } from 'vue'
import { connectSSEWithPost } from '@/utils/sse'
import { timeValidate } from '@/utils/tools'
import { downloadDocWithStyle, downloadPDF } from "@/utils/export.js"
import TableData from './tableData.vue'
import Bar from './bar.vue'
import Docxtemplater from 'docxtemplater';
import PizZip from 'pizzip';
import { saveAs } from 'file-saver';
const props = defineProps({
search: {
type: Object,
default: () => { }
},
xzlx: {
type: String,
default: ''
}
})
const params = ref({ ...props.search })
onMounted(async () => {
getxsasjxsajztgs()
getxsasjxsajccajlbfx()
getxsasjxsajajxsfx()
getxsasjxsajnldtj()
getxsasjxsajajfsqy()
getxsasjxsajajdyfx()
getconnectSSEWithPost()
})
// 值
const wdValue = ref({
date: timeValidate(new Date(), 'ny'),
ym: timeValidate((() => { const d = new Date(); d.setMonth(d.getMonth() - 1); return d; })(), 'ym'),
td: timeValidate(new Date(), 'td'),
zaajZtgsList: {},
ajxsfxList: [],
zaajList: [],
zaajFbqktj: [],
zaajFbqktjList: {},
nldtjOnj: {
ccxyr: 0,
nldtj: { list: [] }
},
jxsajajdyfx: {},
sajajfsqy: {},
xzjyL:""
})
const ajReport = ref(null)
// 获取报告所有文字内容
const getReportTextContent = () => {
if (!ajReport.value) {
return ''
}
// 提取元素内的所有文本内容
const textContent = ajReport.value.textContent || ''
// 处理空白字符,去除多余的换行和空格
return textContent
.replace(/\s+/g, ' ') // 将多个空白字符替换为单个空格
.trim() // 去除首尾空白
}
// 侦防建议
const getconnectSSEWithPost = () => {
const params = getReportTextContent()
connectSSEWithPost({ prompt: params }, {
onChunk: (content) => {
wdValue.value.xzjyL += content;
},
onComplete: () => {
console.log('SSE连接完成');
},
onError: (error) => {
console.error('SSE连接错误:', error);
}
})
}
// 区域分析
const getxsasjxsajztgs = () => {
// ...params.value
xsasjxsajztgs({ ...params.value }).then(res => {
wdValue.value.zaajZtgsList = res
}).catch(error => {
console.error('请求失败:', error)
})
}
// 治安案件类别分析
const getxsasjxsajccajlbfx = () => {
xsasjxsajccajlbfx({ ...params.value }).then(res => {
wdValue.value.zaajFbqktj = res
}).catch(error => {
console.error('请求失败:', error)
})
}
const xzlx = [
{
key: 'rlggcx',
label: '扰乱公共秩序类',
value: ''
}, {
key: 'qhrsaq',
label: '侵害人身安全和财产权利类',
value: ''
}, {
key: 'fhshglcx',
label: '妨害社会管理秩序类',
value: ''
}, {
key: 'fhggaq',
label: '妨害公共安全类',
value: ''
}
]
const strFun = (str) => {
const newStr = parseInt(str.slice(0, str.length - 1))
return newStr == 0 ? '持平' : newStr > 0 ? `上升${newStr}%` : `下降${Math.abs(newStr)}%`
}
// 治安案件线索分析
const getxsasjxsajajxsfx = () => {
xsasjxsajajxsfx({ ...params.value }).then(res => {
wdValue.value.ajxsfxList = res
const list = res.map(item => item.tjlx)
wdValue.value.zaajList = xzlx.map(item => {
return {
...item,
sySj: list[0],
bySj: list[2],
sy: res[1][item.key],
by: res[2][item.key],
hb: strFun(res[3][item.key]),
tb: strFun(res[4][item.key]),
}
})
console.log(wdValue.value.zaajList);
}).catch(error => {
console.error('请求失败:', error)
})
}
const getxsasjxsajnldtj = () => {
xsasjxsajnldtj({ ...params.value }).then(res => {
wdValue.value.nldtjOnj.ccxyr = res.ccxyr ? res.ccxyr : '0'
wdValue.value.nldtjOnj.nldtj.list = [
{ label: '占比', val: res.nldtj.map(item => (item.count / res.ccxyr * 100).toFixed(2)) },
]
wdValue.value.nldtjOnj.nldtj.xData = res.nldtj.map(item => item.agegroup)
wdValue.value.nldtjOnj.nldtj.bottomValues = res.nldtj.map(item => item.count)
wdValue.value.nldtjOnj.nldtj.data = res.nldtj
}).catch(error => {
console.error('请求失败:', error)
})
}
const getxsasjxsajajfsqy = () => {
xsasjxsajajfsqy({ ...params.value }).then(res => {
wdValue.value.sajajfsqy = res
}).catch(error => {
console.error('请求失败:', error)
})
}
const getxsasjxsajajdyfx = () => {
xsasjxsajajdyfx({ ...params.value }).then(res => {
const { greaterThan, lessThan } = compareCclValues(res);
wdValue.value.jxsajajdyfx = {
greaterThan, lessThan,
res
}
console.log(wdValue.value.jxsajajdyfx);
}).catch(error => {
console.error('请求失败:', error)
})
}
// 比较ccl值的方法
const compareCclValues = (data) => {
// 获取基准ccl值
const baseCcl = wdValue.value.zaajZtgsList.ccl || '0%';
const baseCclValue = parseFloat(baseCcl);
// 初始化结果数组
const greaterThan = [];
const lessThan = [];
// 遍历数据进行比较
data.forEach(item => {
const itemCcl = item.ccl || '0%';
const itemCclValue = parseFloat(itemCcl);
if (itemCclValue > baseCclValue) {
greaterThan.push(item);
} else if (itemCclValue < baseCclValue) {
lessThan.push(item);
}
});
return { greaterThan, lessThan };
}
// 导出word
const tableBox = ref(null);
const downloadWithStyles = async () => {
if (!tableBox.value?.innerHTML) return;
try {
// 将类样式转换为行内样式的函数
const convertClassesToInlineStyles = (html) => {
// 创建临时DOM元素
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// 定义样式映射
const styleMap = {
'.report-container': {
'padding': '20px',
'color': '#000'
},
'.main-title': {
'font-size': '22pt',
'font-family': '黑体, SimHei, Microsoft YaHei',
'text-align': 'center',
'font-weight': 'bold',
'line-height': '30pt'
},
'.section-title': {
'font-size': '16pt',
'font-family': '黑体, SimHei, Microsoft YaHei',
'line-height': '30pt',
'text-indent': '2em',
'font-weight': 'bold'
},
'.body-text': {
'font-size': '16pt',
'font-family': '仿宋_GB2312',
'line-height': '30pt',
'text-indent': '2em'
},
'.font-bold': {
'font-weight': '600'
},
'.right-align': {
'text-align': 'right',
'margin-left': 'auto',
'width': 'fit-content'
}
};
// 遍历所有元素,应用行内样式
const elements = tempDiv.querySelectorAll('*');
elements.forEach(element => {
// 获取元素的所有类名
const classes = element.className.split(' ').filter(c => c);
// 为每个类名应用对应的样式
classes.forEach(className => {
const styles = styleMap[`.${className}`];
if (styles) {
Object.entries(styles).forEach(([property, value]) => {
element.style[property] = value;
});
}
});
});
// 返回转换后的HTML
return tempDiv.innerHTML;
};
// 转换样式并构建导出内容
let inlineStyledContent = convertClassesToInlineStyles(tableBox.value.innerHTML);
// 处理图表转换为图片
const tempExportDiv = document.createElement('div');
tempExportDiv.innerHTML = inlineStyledContent;
// 直接在原始DOM中查找所有图表容器
const originalChartContainers = tableBox.value.querySelectorAll('div[id]');
for (const originalContainer of originalChartContainers) {
try {
// 获取图表实例
const chart = echarts.getInstanceByDom(originalContainer);
if (chart) {
// 确保图表已渲染并获取完整大小
const containerWidth = originalContainer.offsetWidth;
const containerHeight = originalContainer.offsetHeight;
// 调整图表大小以确保完整渲染
chart.resize({
width: containerWidth,
height: containerHeight
});
// 等待一小段时间确保图表渲染完成
await new Promise(resolve => setTimeout(resolve, 100));
// 获取图表的base64图片设置合适的尺寸
const imgSrc = chart.getDataURL({
pixelRatio: 2, // 提高图片清晰度
backgroundColor: '#fff',
width: containerWidth,
height: containerHeight
});
// 在临时DOM中查找对应的容器并替换
const tempContainer = tempExportDiv.querySelector(`div[id="${originalContainer.id}"]`);
if (tempContainer) {
// 创建图片元素替换图表容器
const img = document.createElement('img');
img.src = imgSrc;
// 使用内联样式确保在Word中占满宽度
img.setAttribute('style', 'width: 100%; height: auto; margin: 20px 0;');
// 替换容器
tempContainer.style.textIndent = '0em';
tempContainer.parentNode.replaceChild(img, tempContainer);
tempContainer.style.textIndent = '0em';
}
}
} catch (error) {
console.error('转换图表为图片失败:', error);
}
}
// 获取处理后的HTML
const processedContent = tempExportDiv.innerHTML;
const styledContent = `
<div style="padding: 20px; color: #000;">
${processedContent}
</div>
`;
downloadDocWithStyle(styledContent, `刑事案件分析报告_${timeValidate(new Date(), 'ny')}`);
} catch (error) {
console.error('导出Word失败:', error);
alert('导出Word失败请检查控制台错误信息');
}
}
</script>
<style lang="scss" scoped>
.report-container {
padding: 20px;
height: 90%;
overflow: auto;
color: #000;
}
.main-title {
font-size: 22pt;
font-family: '黑体, SimHei, Microsoft YaHei';
text-align: center;
font-weight: bold;
line-height: 30pt;
}
.section-title {
font-size: 16pt;
font-family: '黑体, SimHei, Microsoft YaHei';
line-height: 30pt;
text-indent: 2em;
font-weight: bold;
}
.body-text {
font-size: 16pt;
// font-family: ', FangSong, Microsoft YaHei';
font-family: '仿宋_GB2312';
line-height: 30pt;
text-indent: 2em;
}
.font-bold {
font-weight: 600;
}
.right-align {
text-align: right;
margin-left: auto;
width: fit-content;
}
</style>
<style lang="scss"></style>

View File

@ -4,6 +4,7 @@
<div class="xsaj">
<JudgmentReport :search="search" v-if="xzlx === '02'" />
<Xsaj :search="search" v-if="xzlx === '03'" />
<Zaaj :search="search" v-if="xzlx === '04'" />
</div>
</el-dialog>
</template>
@ -13,6 +14,8 @@
import { ref, reactive, watch } from 'vue'
import JudgmentReport from './AnalysisReport/index.vue'
import Xsaj from './caseFile/xsaj.vue'
import Zaaj from './caseFile/zaaj.vue'
const title = ref('详情')
const props = defineProps({
visible: {