index.vue 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203
  1. <template>
  2. <div class="home">
  3. <div class="map" id="map"></div>
  4. <div class="left-part">
  5. <el-card>
  6. <template #header>
  7. <div class="custom-title">
  8. <img src="@/assets/images/title.svg" />
  9. <span>本月</span>
  10. </div>
  11. </template>
  12. <div class="custom-card">
  13. <div class="card-item">
  14. <span>{{ statData.monthCount }}</span>
  15. <span>所有数量</span>
  16. </div>
  17. <div class="card-item">
  18. <span>{{ statData.monthAlready }}</span>
  19. <span>已解决</span>
  20. </div>
  21. <div class="card-item">
  22. <span>{{ statData.monthNot }}</span>
  23. <span>未解决</span>
  24. </div>
  25. <div class="card-item">
  26. <span>{{ statData.monthReport }}</span>
  27. <span>已上报</span>
  28. </div>
  29. </div>
  30. </el-card>
  31. <el-card style="margin-top: 10px">
  32. <template #header>
  33. <div class="custom-title">
  34. <img src="@/assets/images/title.svg" />
  35. <span>本周</span>
  36. </div>
  37. </template>
  38. <div class="custom-card week-card">
  39. <div class="card-item">
  40. <span>{{ statData.weekCount }}</span>
  41. <span>所有数量</span>
  42. </div>
  43. <div class="card-item">
  44. <span>{{ statData.weekAlready }}</span>
  45. <span>已解决</span>
  46. </div>
  47. <div class="card-item">
  48. <span>{{ statData.weekNot }}</span>
  49. <span>未解决</span>
  50. </div>
  51. <div class="card-item">
  52. <span>{{ statData.weekReport }}</span>
  53. <span>已上报</span>
  54. </div>
  55. </div>
  56. </el-card>
  57. <el-card style="margin-top: 10px">
  58. <template #header>
  59. <div class="custom-title">
  60. <img src="@/assets/images/title.svg" />
  61. <span>当天</span>
  62. </div>
  63. </template>
  64. <div class="custom-card day-card">
  65. <div class="card-item">
  66. <span>{{ statData.dayCount }}</span>
  67. <span>所有数量</span>
  68. </div>
  69. <div class="card-item">
  70. <span>{{ statData.dayAlready }}</span>
  71. <span>已解决</span>
  72. </div>
  73. <div class="card-item">
  74. <span>{{ statData.dayNot }}</span>
  75. <span>未解决</span>
  76. </div>
  77. <div class="card-item">
  78. <span>{{ statData.dayReport }}</span>
  79. <span>已上报</span>
  80. </div>
  81. </div>
  82. </el-card>
  83. <el-card style="margin-top: 10px">
  84. <template #header>
  85. <div class="custom-title">
  86. <img src="@/assets/images/title.svg" />
  87. <span>统计分析</span>
  88. </div>
  89. </template>
  90. <BaseChart width="100%" height="100%" :option="pieOptions" />
  91. </el-card>
  92. </div>
  93. <div class="expandOrFold" :style="{ 'right': showList ? '550px' : '0' }" @click="showList = !showList">
  94. <img v-if="showList" src="@/assets/images/fold.svg" alt="" />
  95. <img v-else src="@/assets/images/expand.svg" alt="" />
  96. </div>
  97. <div class="right-part" v-show="showList">
  98. <div class="event-title">
  99. <div>
  100. <el-select style="width: 100px" v-model="queryParams.level" clearable placeholder="事件等级">
  101. <el-option v-for="dict in event_level" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
  102. </el-select>
  103. <el-select style="width: 100px; margin-left: 10px" v-model="queryParams.params.lx" filterable clearable placeholder="事件类型">
  104. <el-option v-for="dict in eventTypeOptions" :key="dict" :label="dict" :value="dict"></el-option>
  105. </el-select>
  106. <el-date-picker
  107. v-model="queryParams.dateRange"
  108. style="margin-left: 10px"
  109. type="daterange"
  110. unlink-panels
  111. range-separator="~"
  112. start-placeholder="开始日期"
  113. end-placeholder="结束日期"
  114. :shortcuts="shortcuts"
  115. value-format="YYYY-MM-DD HH:mm:ss"
  116. />
  117. </div>
  118. <div style="margin-top: 10px">
  119. <el-select style="width: 210px" v-model="queryParams.params.deviceId" clearable placeholder="设备">
  120. <el-option v-for="dict in deviceList" :key="dict.name" :label="dict.name" :value="dict.id"></el-option>
  121. </el-select>
  122. <el-input v-model="queryParams.params.key" style="margin-left: 10px; width: 165px" placeholder="输入地点关键词" :suffix-icon="Search" />
  123. <div style="margin-left: 10px">
  124. <el-button color="#1890FF" style="color: #fff" @click="getList">查询</el-button>
  125. <el-button @click="resetQuery">重置</el-button>
  126. </div>
  127. </div>
  128. </div>
  129. <div class="event-list">
  130. <el-table :data="eventList" :show-header="false" :border="false" style="width: 100%; --el-table-border-color: none">
  131. <el-table-column prop="level" width="80">
  132. <template #default="{ row }">
  133. {{ event_level.filter((item) => item.value == row.level)[0]?.label }}
  134. </template>
  135. </el-table-column>
  136. <el-table-column prop="ext2.lx" show-overflow-tooltip width="100" />
  137. <el-table-column prop="addr" show-overflow-tooltip />
  138. <el-table-column prop="createTimeFormat" width="100" />
  139. <el-table-column prop="status" width="70">
  140. <template #default="{ row }">
  141. <span v-if="row.status == 3" style="color: #5f86fd">已上报</span>
  142. <span v-else-if="row.status == 1" style="color: #26c768">已解决</span>
  143. <span v-else-if="row.status == 2" style="color: #ff6124">未解决</span>
  144. <span v-else>{{ event_status.filter((item) => item.value == row.status)[0]?.label }}</span>
  145. </template>
  146. </el-table-column>
  147. <el-table-column width="60">
  148. <template #default="{ row }">
  149. <el-button link type="primary" @click="showDetails(row)">查看</el-button>
  150. </template>
  151. </el-table-column>
  152. </el-table>
  153. </div>
  154. <div class="event-footer">
  155. <pagination
  156. background
  157. layout="prev, pager, next"
  158. v-show="total > 0"
  159. :total="total"
  160. v-model:page="queryParams.pageNum"
  161. v-model:limit="queryParams.pageSize"
  162. @pagination="getList"
  163. />
  164. </div>
  165. </div>
  166. <el-dialog v-model="dialog.visible" :title="dialog.title" width="1000px" top="0" append-to-body @close="dialogClose">
  167. <template #title>
  168. <div style="display: flex; justify-content: space-between; align-items: center; padding-right: 30px">
  169. <span>事件详情</span>
  170. <el-button @click="saveClick">保存</el-button>
  171. </div>
  172. </template>
  173. <div class="dialog-loading-warp">
  174. <el-form ref="addFormRef" :model="form" label-position="top" label-width="90px" label-suffix=":">
  175. <el-row :gutter="20">
  176. <el-col :span="8">
  177. <el-form-item label="事件ID" prop="id">
  178. <el-input v-model="form.id" placeholder="请输入" readonly />
  179. </el-form-item>
  180. </el-col>
  181. <el-col :span="8">
  182. <el-form-item label="事件类型" prop="ext2.lx">
  183. <el-select v-model="form.ext2.lx" clearable filterable allow-create placeholder="请选择" @change="formSubmit('lx', 'ext2')">
  184. <el-option v-for="dict in eventTypeOptions" :key="dict" :label="dict" :value="dict"></el-option>
  185. </el-select>
  186. </el-form-item>
  187. </el-col>
  188. <el-col :span="8">
  189. <el-form-item label="发生时间" prop="createTime">
  190. <el-date-picker
  191. style="width: 100%"
  192. v-model="form.createTime"
  193. value-format="YYYY-MM-DD HH:mm:ss"
  194. @change="formSubmit('createTime')"
  195. type="date"
  196. placeholder="请选择"
  197. />
  198. </el-form-item>
  199. </el-col>
  200. <el-col :span="8">
  201. <el-form-item label="事件等级" prop="level">
  202. <el-select v-model="form.level" clearable placeholder="请选择" @change="formSubmit('level')">
  203. <el-option v-for="dict in event_level" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
  204. </el-select>
  205. </el-form-item>
  206. </el-col>
  207. <el-col :span="8">
  208. <el-form-item label="桩号" prop="ext2.zh">
  209. <el-input v-model="form.ext2.zh" placeholder="请输入" @blur="formSubmit('zh', 'ext2')" />
  210. </el-form-item>
  211. </el-col>
  212. <el-col :span="8">
  213. <el-form-item label="状态" prop="status">
  214. <el-select v-model="form.status" clearable placeholder="请选择" @change="formSubmit('status')">
  215. <el-option v-for="dict in event_status" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
  216. </el-select>
  217. </el-form-item>
  218. </el-col>
  219. <el-col :span="24">
  220. <el-form-item label="地点描述" prop="addr">
  221. <el-input v-model="form.addr" type="textarea" placeholder="请输入" @blur="formSubmit('addr')" />
  222. </el-form-item>
  223. </el-col>
  224. </el-row>
  225. </el-form>
  226. <div class="event-media" v-if="form.imgList && form.imgList.length">
  227. <div class="event-media-title">现场画面</div>
  228. <div class="event-media-content">
  229. <div class="imgs">
  230. <el-image v-for="item in form.imgList" :key="item" :src="item" show-progress :preview-src-list="form.imgList" fit="cover" />
  231. </div>
  232. </div>
  233. </div>
  234. <div class="event-media" v-if="form.videoList && form.videoList.length" style="margin-top: 10px">
  235. <div class="event-media-title">现场视频</div>
  236. <div class="event-media-content">
  237. <div class="videos">
  238. <div v-for="(item, index) in form.videoList" :key="index" :id="`dplayer${index}`"></div>
  239. </div>
  240. </div>
  241. </div>
  242. <div class="report-btn">
  243. <img @click="generateClick" src="@/assets/images/report.svg" alt="" />
  244. </div>
  245. <template v-if="showReport">
  246. <div class="report-editor">
  247. <div class="report-title">报告内容:</div>
  248. <div style="border: 1px solid #d9d9d9; margin-top: 10px">
  249. <!-- <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" mode="simple" /> -->
  250. <Editor style="height: 500px; overflow-y: hidden" v-model="valueHtml" :defaultConfig="editorConfig" @onCreated="handleCreated" />
  251. </div>
  252. </div>
  253. <div style="margin-top: 20px">
  254. <div class="report-title">格式选择:</div>
  255. <el-select style="width: 300px; margin-top: 10px" v-model="reportForm.type" clearable placeholder="请选择">
  256. <el-option label="pdf" value="pdf"></el-option>
  257. <el-option label="word" value="docx"></el-option>
  258. </el-select>
  259. </div>
  260. <div style="margin-top: 20px">
  261. <el-button color="#2AB55C" style="color: #fff; margin-bottom: 20px" @click="dialogVisible = true">导出报告</el-button>
  262. </div>
  263. <el-dialog v-model="dialogVisible" title="导出信息" width="500">
  264. <el-form ref="reportFormRef" :model="reportForm" label-width="90px" label-suffix=":">
  265. <el-row :gutter="20">
  266. <el-col :span="24">
  267. <el-form-item label="期数" prop="seq" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
  268. <el-input v-model="reportForm.seq" placeholder="请输入" />
  269. </el-form-item>
  270. </el-col>
  271. <el-col :span="24">
  272. <el-form-item label="发送范围" prop="fw" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
  273. <el-input v-model="reportForm.fw" placeholder="请输入" />
  274. </el-form-item>
  275. </el-col>
  276. <el-col :span="24">
  277. <el-form-item label="备案号" prop="ba" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
  278. <el-input v-model="reportForm.ba" placeholder="请输入" />
  279. </el-form-item>
  280. </el-col>
  281. <el-col :span="24">
  282. <el-form-item label="值班员" prop="zby" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
  283. <el-input v-model="reportForm.zby" placeholder="请输入" />
  284. </el-form-item>
  285. </el-col>
  286. <el-col :span="24">
  287. <el-form-item label="审核" prop="sh" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
  288. <el-input v-model="reportForm.sh" placeholder="请输入" />
  289. </el-form-item>
  290. </el-col>
  291. <el-col :span="24">
  292. <el-form-item label="签发" prop="qf" :rules="[{ required: true, message: '请输入', trigger: 'blur' }]">
  293. <el-input v-model="reportForm.qf" placeholder="请输入" />
  294. </el-form-item>
  295. </el-col>
  296. </el-row>
  297. </el-form>
  298. <template #footer>
  299. <div class="dialog-footer">
  300. <el-button type="primary" :loading="downLoading" @click="htmlToPdfFn">确 认</el-button>
  301. </div>
  302. </template>
  303. </el-dialog>
  304. </template>
  305. </div>
  306. </el-dialog>
  307. </div>
  308. </template>
  309. <script setup lang="ts">
  310. import { Search } from '@element-plus/icons-vue';
  311. import BaseChart from '@/components/BaseChart/index.vue';
  312. import monitorIcon from '@/assets/images/monitor.svg';
  313. import monitorEventIcon from '@/assets/images/monitor-event.svg';
  314. import DPlayer from 'dplayer';
  315. import '@wangeditor/editor/dist/css/style.css'; // 引入 css
  316. import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
  317. import { downloadFile } from '@/utils/download.js';
  318. import { listEvent, generateReport, updateEvent, getEvent, getEventTypes } from '@/api/system/event';
  319. import { listDevice } from '@/api/system/device';
  320. import { dateFormat } from '@/utils/index';
  321. import { ElLoading } from 'element-plus';
  322. import { base64Encoded, base64Decoded } from '@/utils/crypto';
  323. const { proxy } = getCurrentInstance();
  324. const bdmap = ref(null);
  325. const downLoading = ref(false);
  326. const { event_level } = toRefs(proxy?.useDict('event_level'));
  327. const { event_status } = toRefs(proxy?.useDict('event_status'));
  328. const editorRef = shallowRef();
  329. const editorConfig = { placeholder: '请输入内容...' };
  330. // 内容 HTML
  331. const valueHtml = ref('');
  332. const handleCreated = (editor) => {
  333. editorRef.value = editor; // 记录 editor 实例,重要!
  334. };
  335. const showReport = ref(false);
  336. const reportFormRef = ref(null);
  337. const reportTime = ref('');
  338. const queryParams = ref({
  339. pageNum: 1,
  340. pageSize: 15,
  341. level: undefined,
  342. dateRange: undefined,
  343. params: {
  344. lx: undefined,
  345. key: undefined,
  346. deviceId: undefined,
  347. beginCreateTime: '',
  348. endCreateTime: ''
  349. }
  350. });
  351. const dialog = reactive({
  352. visible: false,
  353. title: '事件详情',
  354. loading: null
  355. });
  356. const form = ref<any>({});
  357. const statData = reactive({
  358. monthCount: undefined,
  359. monthAlready: undefined,
  360. monthNot: undefined,
  361. monthReport: undefined,
  362. weekCount: undefined,
  363. weekAlready: undefined,
  364. weekNot: undefined,
  365. weekReport: undefined,
  366. dayCount: undefined,
  367. dayAlready: undefined,
  368. dayNot: undefined,
  369. dayReport: undefined,
  370. already: undefined,
  371. not: undefined,
  372. report: undefined
  373. });
  374. const showList = ref(true);
  375. const shortcuts = [
  376. {
  377. text: '最近一周',
  378. value: () => {
  379. const end = new Date();
  380. const start = new Date();
  381. start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
  382. return [start, end];
  383. }
  384. },
  385. {
  386. text: '最近一个月',
  387. value: () => {
  388. const end = new Date();
  389. const start = new Date();
  390. start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
  391. return [start, end];
  392. }
  393. },
  394. {
  395. text: '最近三个月',
  396. value: () => {
  397. const end = new Date();
  398. const start = new Date();
  399. start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
  400. return [start, end];
  401. }
  402. }
  403. ];
  404. const pieOptions = computed(() => {
  405. const options = {
  406. tooltip: {
  407. trigger: 'item'
  408. },
  409. legend: {
  410. orient: 'horizontal',
  411. left: 'center',
  412. itemWidth: 15,
  413. itemHeight: 8,
  414. itemGap: 10
  415. },
  416. series: [
  417. {
  418. type: 'pie',
  419. radius: ['40%', '80%'],
  420. center: ['50%', '60%'],
  421. data: [
  422. {
  423. value: statData.already,
  424. name: '已解决',
  425. itemStyle: {
  426. color: '#FAC858'
  427. }
  428. },
  429. {
  430. value: statData.not,
  431. name: '未解决',
  432. itemStyle: {
  433. color: '#93BEFF'
  434. }
  435. },
  436. {
  437. value: statData.report,
  438. name: '已上报',
  439. itemStyle: {
  440. color: '#507AFC'
  441. }
  442. }
  443. ],
  444. labelLine: {
  445. show: false
  446. },
  447. label: {
  448. show: false,
  449. position: 'center'
  450. }
  451. }
  452. ]
  453. };
  454. return options;
  455. });
  456. const eventList = ref([]);
  457. const total = ref(0);
  458. const dialogVisible = ref(false);
  459. const reportForm = reactive({
  460. 'year': null,
  461. 'seq': 1,
  462. 'title': '',
  463. 'month': null,
  464. 'day': null,
  465. 'content': '',
  466. 'fw': '市委、市政府总值班室',
  467. 'qf': '宗仁',
  468. 'sh': '朱劲',
  469. 'ps': '',
  470. 'ba': 'KB608',
  471. 'zby': '83194295',
  472. 'tpl': 'sg.docx',
  473. 'type': 'pdf'
  474. });
  475. const reportSeq = ref(0);
  476. const deviceList = ref([]);
  477. const hasEventDevices = ref([]);
  478. const eventTypeOptions = ref([]);
  479. onMounted(() => {
  480. try {
  481. const map = new BMapGL.Map('map'); // 创建地图实例
  482. const point = new BMapGL.Point(118.879999, 32.016216); // 创建点坐标
  483. map.centerAndZoom(point, 14);
  484. map.enableScrollWheelZoom(true);
  485. bdmap.value = map;
  486. map.addEventListener('click', function (e) {
  487. const { lng, lat } = e.latlng;
  488. console.log(e.latlng);
  489. });
  490. } catch (e) {}
  491. getEventTypeOptions();
  492. getList();
  493. getStat();
  494. getDeviceList();
  495. });
  496. const getEventTypeOptions = () => {
  497. getEventTypes().then(({ code, data }) => {
  498. if (code === 200) {
  499. eventTypeOptions.value = data;
  500. }
  501. });
  502. };
  503. const getList = async () => {
  504. const { dateRange } = queryParams.value;
  505. if (dateRange && dateRange.length) {
  506. queryParams.value.params.beginCreateTime = dateRange[0];
  507. queryParams.value.params.endCreateTime = dateRange[1];
  508. } else {
  509. queryParams.value.params.beginCreateTime = undefined;
  510. queryParams.value.params.endCreateTime = undefined;
  511. }
  512. const res = await listEvent(queryParams.value);
  513. eventList.value = res.rows.map((item) => ({
  514. ...item,
  515. status: item.status || '2',
  516. createTimeFormat: dateFormat(new Date(item.createTime), 'yyyy-MM-dd'),
  517. ext1: item.ext1 ? JSON.parse(item.ext1) : [],
  518. ext2: item.ext2 ? JSON.parse(item.ext2) : {}
  519. }));
  520. total.value = res.total;
  521. };
  522. const getStat = async () => {
  523. const monthRange = getTimeRange('month');
  524. const monthRes = await listEvent({
  525. params: {
  526. beginCreateTime: monthRange.start,
  527. endCreateTime: monthRange.end
  528. }
  529. });
  530. statData.monthCount = monthRes.total;
  531. statData.monthAlready = monthRes.rows.filter((item) => item.status == '1').length;
  532. statData.monthNot = monthRes.rows.filter((item) => item.status == '2' || item.status === null).length;
  533. statData.monthReport = monthRes.rows.filter((item) => item.status == '3').length;
  534. const weekRange = getTimeRange('week', { firstDayOfWeek: 0 });
  535. const weekRes = await listEvent({
  536. params: {
  537. beginCreateTime: weekRange.start,
  538. endCreateTime: weekRange.end
  539. }
  540. });
  541. statData.weekCount = weekRes.total;
  542. statData.weekAlready = weekRes.rows.filter((item) => item.status == '1').length;
  543. statData.weekNot = weekRes.rows.filter((item) => item.status == '2' || item.status === null).length;
  544. statData.weekReport = weekRes.rows.filter((item) => item.status == '3').length;
  545. const todayRange = getTimeRange('day');
  546. const todayRes = await listEvent({
  547. params: {
  548. beginCreateTime: todayRange.start,
  549. endCreateTime: todayRange.end
  550. }
  551. });
  552. statData.dayCount = todayRes.total;
  553. statData.dayAlready = todayRes.rows.filter((item) => item.status == '1').length;
  554. statData.dayNot = todayRes.rows.filter((item) => item.status == '2' || item.status === null).length;
  555. statData.dayReport = todayRes.rows.filter((item) => item.status == '3').length;
  556. const res = await listEvent({});
  557. statData.already = res.rows.filter((item) => item.status == '1').length;
  558. statData.not = res.rows.filter((item) => item.status == '2' || item.status === null).length;
  559. statData.report = res.rows.filter((item) => item.status == '3').length;
  560. };
  561. const getTimeRange = (unit = 'day', options: any = {}) => {
  562. const now = new Date();
  563. const { firstDayOfWeek = 1 } = options;
  564. // 定义结果容器
  565. const result = {
  566. unit,
  567. startDate: null,
  568. endDate: null,
  569. start: '',
  570. end: '',
  571. description: '',
  572. duration: 0
  573. };
  574. // 内部工具函数:格式化日期时间为字符串
  575. const format = (date) => {
  576. const year = date.getFullYear();
  577. const month = String(date.getMonth() + 1).padStart(2, '0');
  578. const day = String(date.getDate()).padStart(2, '0');
  579. const hours = String(date.getHours()).padStart(2, '0');
  580. const minutes = String(date.getMinutes()).padStart(2, '0');
  581. const seconds = String(date.getSeconds()).padStart(2, '0');
  582. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  583. };
  584. // 处理不同类型的时间范围
  585. switch (unit.toLowerCase()) {
  586. case 'day':
  587. // 当天范围 (00:00:00.000 - 23:59:59.999)
  588. const dayStart = new Date(now);
  589. dayStart.setHours(0, 0, 0, 0);
  590. const dayEnd = new Date(now);
  591. dayEnd.setHours(23, 59, 59, 999);
  592. result.startDate = dayStart;
  593. result.endDate = dayEnd;
  594. result.start = format(dayStart);
  595. result.end = format(dayEnd);
  596. result.description = `当天范围: ${result.start} 至 ${result.end}`;
  597. result.duration = 1;
  598. break;
  599. case 'week':
  600. // 计算周起始日偏移
  601. const dayOfWeek = now.getDay();
  602. const diffToFirstDay = (dayOfWeek < firstDayOfWeek ? 7 : 0) + dayOfWeek - firstDayOfWeek;
  603. // 本周开始 (周一 00:00:00.000)
  604. const weekStart = new Date(now);
  605. weekStart.setDate(now.getDate() - diffToFirstDay);
  606. weekStart.setHours(0, 0, 0, 0);
  607. // 本周结束 (周日 23:59:59.999)
  608. const weekEnd = new Date(weekStart);
  609. weekEnd.setDate(weekStart.getDate() + 6);
  610. weekEnd.setHours(23, 59, 59, 999);
  611. // 设置结果
  612. result.startDate = weekStart;
  613. result.endDate = weekEnd;
  614. result.start = format(weekStart);
  615. result.end = format(weekEnd);
  616. result.duration = 7;
  617. // 生成描述信息
  618. const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
  619. const startDayName = weekDays[firstDayOfWeek];
  620. const endDayName = weekDays[(firstDayOfWeek + 6) % 7];
  621. result.description = `本周范围 (${startDayName}至${endDayName}): ${result.start} 至 ${result.end}`;
  622. break;
  623. case 'month':
  624. // 本月第一天 (1号 00:00:00.000)
  625. const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  626. monthStart.setHours(0, 0, 0, 0);
  627. // 本月最后一天 (月末日 23:59:59.999)
  628. const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
  629. monthEnd.setHours(23, 59, 59, 999);
  630. // 设置结果
  631. result.startDate = monthStart;
  632. result.endDate = monthEnd;
  633. result.start = format(monthStart);
  634. result.end = format(monthEnd);
  635. // 计算天数
  636. const dayDiff = Math.floor((monthEnd - monthStart) / (1000 * 60 * 60 * 24)) + 1;
  637. result.duration = dayDiff;
  638. // 获取月份名称
  639. const monthNames = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
  640. result.description = `${monthNames[now.getMonth()]}范围 (共${dayDiff}天): ${result.start} 至 ${result.end}`;
  641. break;
  642. default:
  643. throw new Error(`无效的时间单位: "${unit}". 请使用 'day', 'week' 或 'month'`);
  644. }
  645. return result;
  646. };
  647. const resetQuery = () => {
  648. queryParams.value = {
  649. pageNum: 1,
  650. pageSize: 15,
  651. level: undefined,
  652. dateRange: undefined,
  653. params: {
  654. lx: undefined,
  655. key: undefined,
  656. deviceId: undefined,
  657. beginCreateTime: '',
  658. endCreateTime: ''
  659. }
  660. };
  661. getList();
  662. };
  663. const getDeviceList = () => {
  664. listDevice({}).then(async ({ code, rows }) => {
  665. if (code === 200) {
  666. await getHasEventDeviceList();
  667. deviceList.value = rows.map((item) => {
  668. let hasEvent = 0;
  669. if (hasEventDevices.value.some((el) => el.id == item.id)) {
  670. hasEvent = 1;
  671. }
  672. return {
  673. ...item,
  674. hasEvent
  675. };
  676. });
  677. showMarks();
  678. }
  679. });
  680. };
  681. const getHasEventDeviceList = async () => {
  682. await listDevice({ params: { hasevent: '1' } }).then(({ code, rows }) => {
  683. if (code === 200) {
  684. hasEventDevices.value = rows;
  685. }
  686. });
  687. };
  688. onBeforeUnmount(() => {
  689. const editor = editorRef.value;
  690. if (editor == null) return;
  691. editor.destroy();
  692. });
  693. const showMarks = () => {
  694. bdmap.value.clearOverlays();
  695. deviceList.value
  696. .filter((item) => item.lon && item.lat)
  697. .forEach((item) => {
  698. const pt = new BMapGL.Point(Number(item.lon), Number(item.lat));
  699. const img = item.hasEvent == 1 ? monitorEventIcon : monitorIcon;
  700. const icon = new BMapGL.Icon(img, new BMapGL.Size(30, 30));
  701. const marker = new BMapGL.Marker(pt, { icon });
  702. marker.addEventListener('click', (e) => {
  703. if (item.hasEvent == 1) {
  704. queryParams.value.params.deviceId = item.id;
  705. queryParams.value.pageNum = 1;
  706. getList();
  707. }
  708. });
  709. bdmap.value.addOverlay(marker);
  710. });
  711. };
  712. const showDetails = (row) => {
  713. getEvent(row.id).then(({ code, data }) => {
  714. if (code === 200) {
  715. data.ext1 = JSON.parse(data.ext1) || [];
  716. data.ext2 = JSON.parse(data.ext2) || {};
  717. data.status = data.status || '2';
  718. let imgList = [];
  719. let videoList = [];
  720. if (data.ext1.length) {
  721. videoList = data.ext1
  722. .filter((item) => item.includes('mp4'))
  723. .map((item) => `${import.meta.env.VITE_APP_BASE_HOST}/api/oss/local/upload/${item}`);
  724. imgList = data.ext1
  725. .filter((item) => !item.includes('mp4'))
  726. .map((item) => `${import.meta.env.VITE_APP_BASE_HOST}/api/oss/local/upload/${item}`);
  727. }
  728. if(data.ext2.EventVideoPath){
  729. videoList = [data.ext2.EventVideoPath];
  730. }
  731. form.value = data;
  732. Object.assign(form.value, { imgList, videoList });
  733. dialog.visible = true;
  734. nextTick(() => {
  735. form.value.videoList.forEach((item, index) => {
  736. new DPlayer({
  737. container: document.getElementById(`dplayer${index}`),
  738. video: {
  739. url: item
  740. }
  741. });
  742. });
  743. });
  744. const { content, ext2 } = form.value;
  745. if (content && base64Decoded(content) && ext2.reportTime) {
  746. showReport.value = true;
  747. dealReportData(base64Decoded(content), ext2.reportTime);
  748. }
  749. }
  750. });
  751. };
  752. const generateClick = () => {
  753. dialog.loading = ElLoading.service({
  754. lock: true,
  755. text: '正在生成AI报告...',
  756. fullscreen: false,
  757. target: '.dialog-loading-warp',
  758. background: 'rgba(255, 255, 255, 0.6)'
  759. });
  760. generateReport(form.value.id).then(async ({ code, msg }) => {
  761. dialog.loading.close();
  762. if (code == 200 && msg) {
  763. showReport.value = true;
  764. const { report, time } = JSON.parse(msg).data.outputs;
  765. const reportHtml = report.replace(/```/g, '').replace(/html\n/, '');
  766. reportTime.value = time;
  767. updateEvent({
  768. id: form.value.id,
  769. content: base64Encoded(reportHtml),
  770. ext2: JSON.stringify({
  771. ...form.value.ext2,
  772. reportTime: time
  773. })
  774. });
  775. dealReportData(reportHtml, time);
  776. proxy?.$modal.msgSuccess('报告生成成功');
  777. } else {
  778. proxy?.$modal.msgError('报告生成失败');
  779. }
  780. });
  781. };
  782. const saveClick = async () => {
  783. const { ext2, createTime, level, status, addr } = form.value;
  784. const params = {
  785. id: form.value.id,
  786. createTime,
  787. level,
  788. status,
  789. addr,
  790. ext2: JSON.stringify({
  791. ...ext2
  792. })
  793. };
  794. if (showReport.value) {
  795. let htmlcontent = '';
  796. const text = editorRef.value.getText();
  797. if (text) {
  798. htmlcontent = editorRef.value.getHtml();
  799. }
  800. await updateEvent({
  801. ...params,
  802. content: base64Encoded(htmlcontent),
  803. ext2: JSON.stringify({
  804. ...ext2,
  805. reportTime: reportTime.value
  806. })
  807. });
  808. } else {
  809. await updateEvent(params);
  810. }
  811. getList();
  812. getStat();
  813. getEventTypeOptions();
  814. proxy?.$modal.msgSuccess('保存成功');
  815. };
  816. const extractDate = (dateStr) => {
  817. // 统一处理两种格式的正则表达式
  818. const match = dateStr.match(/(\d{4})[^\d]*(\d{1,2})[^\d]*(\d{1,2})/);
  819. if (!match) return null;
  820. return {
  821. year: parseInt(match[1]),
  822. month: parseInt(match[2]),
  823. day: parseInt(match[3])
  824. };
  825. };
  826. const dealReportData = async (reportHtml, time) => {
  827. const res = await proxy?.getConfigKey('report_seq');
  828. reportSeq.value = Number(res.data);
  829. valueHtml.value = reportHtml;
  830. const { year, month, day } = extractDate(time);
  831. reportForm.year = year;
  832. reportForm.month = month;
  833. reportForm.day = day;
  834. reportForm.seq = reportSeq.value;
  835. };
  836. const extractTitleAndContents = (htmlString) => {
  837. // 提取标题(从 <h2><strong> 中获取)
  838. const titleMatch = htmlString.match(/<h2[^>]*>.*?<strong>(.*?)<\/strong>.*?<\/h2>/is);
  839. const title = titleMatch ? titleMatch[1].trim() : '';
  840. // 提取所有 <p> 内容(从 <p><span> 中获取)
  841. const contentMatches = [...htmlString.matchAll(/<p[^>]*>.*?<span[^>]*>(.*?)<\/span>.*?<\/p>/gis)];
  842. const contents = contentMatches.map((match) => match[1].trim());
  843. return {
  844. title,
  845. contents
  846. };
  847. };
  848. const htmlToPdfFn = async () => {
  849. reportFormRef.value.validate((valid) => {
  850. if (valid) {
  851. downLoading.value = true;
  852. const { title, contents } = extractTitleAndContents(editorRef.value.getHtml());
  853. reportForm.title = title;
  854. reportForm.content = contents.join('');
  855. //import.meta.env.VITE_APP_ENV === 'development' ? 'loadFile/generate' :
  856. let url = `${import.meta.env.VITE_APP_BASE_HOST}/generate`;
  857. if (import.meta.env.DEV) {
  858. url = 'loadFile/generate';
  859. }
  860. downloadFile(url, {
  861. method: 'POST',
  862. data: reportForm,
  863. filename: `${reportForm.title}.${reportForm.type}`,
  864. onProgress: (percent) => {
  865. console.log(percent);
  866. if (percent == 100) {
  867. downLoading.value = false;
  868. proxy?.updateConfigByKey('report_seq', reportSeq.value + 1);
  869. proxy?.$modal.msgSuccess('报告导出成功');
  870. }
  871. }
  872. });
  873. }
  874. });
  875. };
  876. const dialogClose = () => {
  877. showReport.value = false;
  878. dialog.loading && dialog.loading.close();
  879. };
  880. const formSubmit = async (field, obj = '') => {
  881. // const { id } = form.value;
  882. // const params = {
  883. // id
  884. // };
  885. // if (obj) {
  886. // params[obj] = form.value[obj];
  887. // params[obj] = JSON.stringify(params[obj]);
  888. // } else {
  889. // params[field] = form.value[field];
  890. // }
  891. // await updateEvent(params);
  892. // getList();
  893. // if (field === 'status') {
  894. // getStat();
  895. // }
  896. // if (field === 'ext2.lx') {
  897. // getEventTypeOptions();
  898. // }
  899. };
  900. </script>
  901. <style lang="scss" scoped>
  902. .home {
  903. height: 100%;
  904. background-color: #f0f2f5;
  905. position: relative;
  906. .left-part {
  907. position: absolute;
  908. width: 300px;
  909. top: 10px;
  910. left: 20px;
  911. bottom: 10px;
  912. z-index: 10;
  913. display: flex;
  914. flex-direction: column;
  915. > .el-card:nth-child(-n + 3) {
  916. height: 22%;
  917. flex-shrink: 0;
  918. :deep(.el-card__body) {
  919. height: 70%;
  920. }
  921. }
  922. > .el-card:last-child {
  923. flex: 1;
  924. :deep(.el-card__body) {
  925. height: 80%;
  926. }
  927. }
  928. }
  929. .right-part {
  930. position: absolute;
  931. background: #fff;
  932. border-radius: 4px;
  933. top: 0;
  934. width: 550px;
  935. right: 0;
  936. bottom: 0px;
  937. z-index: 10;
  938. box-shadow: 0px 0px 12px rgba(0, 0, 0, 0.12);
  939. display: flex;
  940. flex-direction: column;
  941. }
  942. .expandOrFold {
  943. position: absolute;
  944. top: 50%;
  945. transform: translateY(-50%);
  946. right: 550px;
  947. cursor: pointer;
  948. z-index: 10;
  949. img {
  950. height: 50px;
  951. }
  952. }
  953. }
  954. .custom-title {
  955. display: flex;
  956. align-items: center;
  957. color: #000000;
  958. font-size: 16px;
  959. img {
  960. height: 16px;
  961. margin-right: 5px;
  962. }
  963. }
  964. .custom-card {
  965. display: flex;
  966. justify-content: space-between;
  967. height: 100%;
  968. .card-item {
  969. display: flex;
  970. width: 22.5%;
  971. flex-direction: column;
  972. justify-content: center;
  973. align-items: center;
  974. height: 100%;
  975. background: #e3eefe;
  976. color: #2d5794;
  977. border-radius: 4.8px;
  978. span:first-child {
  979. flex: 1;
  980. font-size: 22px;
  981. margin-top: 10px;
  982. display: flex;
  983. align-items: center;
  984. }
  985. span:last-child {
  986. width: 80%;
  987. text-align: center;
  988. height: 26px;
  989. line-height: 26px;
  990. border-top: 0.6px solid #d0dff4;
  991. font-size: 10px;
  992. }
  993. }
  994. }
  995. .week-card {
  996. .card-item {
  997. background: #fef4e3;
  998. span:first-child {
  999. color: #7c5b22;
  1000. }
  1001. span:last-child {
  1002. border-color: #eddbbc;
  1003. color: #8c692e;
  1004. }
  1005. }
  1006. }
  1007. .day-card {
  1008. .card-item {
  1009. background: #e3fef0;
  1010. span:first-child {
  1011. color: #248647;
  1012. }
  1013. span:last-child {
  1014. border-color: #ade7cd;
  1015. color: #238546;
  1016. }
  1017. }
  1018. }
  1019. .map {
  1020. width: 100%;
  1021. height: 100%;
  1022. }
  1023. .event-title {
  1024. flex-shrink: 0;
  1025. padding: 10px;
  1026. background: linear-gradient(180deg, #ffffff 0%, #f5f5f5 100%);
  1027. border-radius: 4px 4px 0 0;
  1028. > div {
  1029. display: flex;
  1030. align-items: center;
  1031. > .el-select {
  1032. flex-shrink: 0;
  1033. }
  1034. }
  1035. }
  1036. .event-list {
  1037. flex: 1;
  1038. overflow-y: auto;
  1039. :deep() {
  1040. .el-table__cell {
  1041. border: none;
  1042. }
  1043. }
  1044. }
  1045. .event-footer {
  1046. height: 60px;
  1047. display: flex;
  1048. justify-content: flex-end;
  1049. align-items: center;
  1050. padding-right: 20px;
  1051. .pagination-container {
  1052. margin-top: 0;
  1053. }
  1054. :deep() {
  1055. .el-pagination.is-background .btn-next,
  1056. .el-pagination.is-background .btn-prev,
  1057. .el-pagination.is-background .el-pager li {
  1058. background: transparent;
  1059. border: 1px solid #00000026;
  1060. color: #000000a6;
  1061. }
  1062. .el-pagination.is-background .is-active {
  1063. background: #1890ff !important;
  1064. border: none !important;
  1065. color: #fff !important;
  1066. font-weight: normal;
  1067. }
  1068. }
  1069. }
  1070. .event-media {
  1071. border: 1px solid #f0f0f0;
  1072. &-title {
  1073. background: #fbfbfb;
  1074. padding: 10px;
  1075. font-size: 14px;
  1076. color: #000000;
  1077. }
  1078. &-content {
  1079. padding: 20px;
  1080. .imgs {
  1081. display: flex;
  1082. flex-wrap: wrap;
  1083. .el-image {
  1084. height: 100px;
  1085. width: 100px;
  1086. margin-right: 10px;
  1087. border-radius: 4px;
  1088. margin-top: 10px;
  1089. }
  1090. }
  1091. .videos {
  1092. display: flex;
  1093. flex-wrap: wrap;
  1094. div {
  1095. height: 200px;
  1096. width: 32%;
  1097. border-radius: 4px;
  1098. margin-right: 10px;
  1099. margin-top: 10px;
  1100. }
  1101. }
  1102. }
  1103. }
  1104. .report-btn {
  1105. text-align: center;
  1106. margin-top: 20px;
  1107. img {
  1108. height: 62px;
  1109. cursor: pointer;
  1110. }
  1111. }
  1112. .hidden-report {
  1113. position: absolute;
  1114. visibility: hidden;
  1115. left: -9999px;
  1116. top: -9999px;
  1117. width: max-content;
  1118. height: max-content;
  1119. display: inline-block;
  1120. contain: layout;
  1121. }
  1122. .report-content {
  1123. width: 800px;
  1124. height: auto;
  1125. padding: 20px;
  1126. background-color: #fff;
  1127. border: 1px solid #eee;
  1128. }
  1129. .report-editor {
  1130. margin-top: 20px;
  1131. }
  1132. .report-title {
  1133. color: #5d5d5dd9;
  1134. font-size: 14px;
  1135. }
  1136. </style>