play_info.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <template>
  2. <el-container>
  3. <el-header style="margin-top: 10px">
  4. <el-row :gutter="20">
  5. <el-col :span="12">
  6. <el-card shadow="hover" class="stat-card">
  7. <el-row :gutter="20">
  8. <el-col :span="8">
  9. <h2 style="color: green; font-size: 30px">{{ totalNum }}</h2>
  10. <p class="success">总发布量</p>
  11. </el-col>
  12. <el-col :span="16">
  13. <div ref="pushLine" style="height: 150px"></div>
  14. </el-col>
  15. </el-row>
  16. </el-card>
  17. </el-col>
  18. <el-col :span="6">
  19. <el-card shadow="hover" class="stat-card">
  20. <h2 style="color: orange; font-size: 30px">{{ imageNum }}</h2>
  21. <p class="warning">图片发布</p>
  22. </el-card>
  23. </el-col>
  24. <el-col :span="6">
  25. <el-card shadow="hover" class="stat-card">
  26. <h2 style="color: green; font-size: 30px">{{ videoNum }}</h2>
  27. <p class="success">视频发布</p>
  28. </el-card>
  29. </el-col>
  30. </el-row>
  31. </el-header>
  32. <el-main style="margin-top: 90px">
  33. <el-card shadow="hover" style="margin-top: 10px; height: 60px">
  34. <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="40px">
  35. <el-form-item label="">
  36. <el-button type="warning" plain icon="Download" @click="handleExport">报表导出</el-button>
  37. </el-form-item>
  38. <el-form-item label="" prop="sourceType">
  39. <el-input v-model="queryParams.sourceName" style="width: 150px" placeholder="请输入文件名称" @input="getRecordList"
  40. clearable></el-input>
  41. </el-form-item>
  42. <el-form-item label="" prop="sourceType">
  43. <el-select v-model="queryParams.sourceType" style="width: 150px" placeholder="请选择类型" @change="getRecordList"
  44. clearable>
  45. <el-option v-for="dict in smsb_source_type" :key="dict.value" :label="dict.label" :value="dict.value"/>
  46. </el-select>
  47. </el-form-item>
  48. <el-form-item label="" prop="sourceTag">
  49. <el-select v-model="queryParams.sourceTag" style="width: 150px" placeholder="请选择分类" @change="getRecordList"
  50. clearable>
  51. <el-option v-for="dict in smsb_source_classify" :key="dict.value" :label="dict.label"
  52. :value="dict.value" />
  53. </el-select>
  54. </el-form-item>
  55. <el-form-item label="" prop="dataRage" style="width: 500px">
  56. <el-col :span="12" style="text-align: right">
  57. <el-radio-group v-model="timeRadio" size="small" @change="handleDateRangeChange">
  58. <!-- <el-radio-button label="今日" value="today" />-->
  59. <el-radio-button label="近7天" value="week" />
  60. <el-radio-button label="近30天" value="month" />
  61. <el-radio-button label="自定义" value="diy" />
  62. </el-radio-group>
  63. </el-col>
  64. <el-col :span="12" style="text-align: right">
  65. <el-date-picker v-model="dateRange" type="daterange" range-separator="-"
  66. start-placeholder="开始日期" :disabled="diyFlag" :clearable="false" @change="handleDateRangeChange" end-placeholder="结束日期" style="margin-left: 10px; margin-right: 30px" />
  67. </el-col>
  68. </el-form-item>
  69. </el-form>
  70. </el-card>
  71. <el-card style="margin-top: 10px">
  72. <div class="table-content">
  73. <el-table v-loading="loading" :data="playRecordList">
  74. <el-table-column label="资源ID" align="left" prop="sourceId" width="250" />
  75. <el-table-column label="资源名称" align="left" prop="fileName" :show-overflow-tooltip="true" />
  76. <el-table-column label="播放次数" align="center" prop="playTimes" width="200" />
  77. <el-table-column label="播放时长" align="center" prop="playDuration" width="200" />
  78. <el-table-column label="分类" align="center" prop="fileTag" width="120">
  79. <template #default="scope">
  80. <dict-tag :options="smsb_source_classify" :value="scope.row.fileTag" />
  81. </template>
  82. </el-table-column>
  83. <el-table-column label="类型" align="center" prop="fileType" width="120">
  84. <template #default="scope">
  85. <dict-tag :options="smsb_source_type" :value="scope.row.fileType" />
  86. </template>
  87. </el-table-column>
  88. <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
  89. <template #default="scope">
  90. <el-tooltip content="查看详情" placement="top">
  91. <el-button link type="primary" icon="View" @click="handleView(scope.row)"></el-button>
  92. </el-tooltip>
  93. </template>
  94. </el-table-column>
  95. </el-table>
  96. </div>
  97. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
  98. v-model:limit="queryParams.pageSize" @pagination="getRecordList" />
  99. </el-card>
  100. </el-main>
  101. </el-container>
  102. <el-dialog :title="dialog.title" v-model="dialog.visible" width="1200px" append-to-body :style="{ height: '850px' }">
  103. <div>
  104. <el-row :gutter="20">
  105. <el-col :span="16" style="text-align: right">
  106. <el-radio-group v-model="DTimeRadio" size="small" @change="DHandleDateRangeChange">
  107. <!-- <el-radio-button label="今日" value="today" />-->
  108. <el-radio-button label="近7天" value="week" />
  109. <el-radio-button label="近30天" value="month" />
  110. </el-radio-group>
  111. </el-col>
  112. <el-col :span="8" style="text-align: right">
  113. <el-date-picker :disabled="true" v-model="DDateRange" type="daterange" range-separator="-"
  114. start-placeholder="开始日期" end-placeholder="结束日期" style="margin-left: 10px; margin-right: 30px" />
  115. </el-col>
  116. </el-row>
  117. </div>
  118. <div ref="tadLine" class="chart-placeholder"></div>
  119. <div>
  120. <el-row :gutter="20">
  121. <el-col :span="12">
  122. <el-table v-loading="loading" :data="deviceRecordList">
  123. <el-table-column label="设备名称" align="center" prop="deviceName" />
  124. <el-table-column label="播放次数" align="center" prop="playTimes" />
  125. <el-table-column label="播放时长" align="center" prop="duration" />
  126. </el-table>
  127. </el-col>
  128. <el-col :span="12">
  129. <el-table v-loading="loading" :data="itemRecordList">
  130. <el-table-column label="组名称" align="center" prop="itemName" />
  131. <el-table-column label="类型" align="center" prop="itemType">
  132. <template #default="scope">
  133. <dict-tag :options="smsb_item_type" :value="scope.row.itemType" />
  134. </template>
  135. </el-table-column>
  136. <el-table-column label="播放次数" align="center" prop="playTimes" />
  137. <el-table-column label="播放时长" align="center" prop="duration" />
  138. </el-table>
  139. </el-col>
  140. </el-row>
  141. </div>
  142. </el-dialog>
  143. </template>
  144. <script setup lang="ts">
  145. import * as echarts from 'echarts';
  146. import {
  147. getPushLine,
  148. listDeviceAndItem,
  149. listPlayRecordSummary,
  150. pushNumber,
  151. timesAndDurationLine
  152. } from '@/api/smsb/source/play_record';
  153. import {SourcePlayRecordForm, SourcePlayRecordQuery, SourcePlayRecordVO} from '@/api/smsb/source/play_record_type';
  154. import {onUnmounted} from 'vue';
  155. const {proxy} = getCurrentInstance() as ComponentInternalInstance;
  156. const {smsb_source_classify, smsb_source_type, smsb_item_type} = toRefs<any>(
  157. proxy?.useDict('smsb_source_classify', 'smsb_source_type', 'smsb_item_type')
  158. );
  159. const loading = ref(true);
  160. const playRecordList = ref<SourcePlayRecordVO[]>([]);
  161. const deviceRecordList = ref<SourcePlayRecordVO[]>([]);
  162. const itemRecordList = ref<SourcePlayRecordVO[]>([]);
  163. const timeRadio = ref('week');
  164. const DTimeRadio = ref('week');
  165. const dateRange = ref(['2025-01-01', '2025-01-01']);
  166. const DDateRange = ref(['2025-01-01', '2025-01-01']);
  167. const totalNum = ref(0);
  168. const imageNum = ref(0);
  169. const videoNum = ref(0);
  170. const pushLine = ref();
  171. const total = ref(0);
  172. const tadLine = ref();
  173. const dialogSourceId = ref();
  174. const diyFlag = ref(true);
  175. const dialog = reactive<DialogOption>({
  176. visible: false,
  177. title: ''
  178. });
  179. const data = reactive<PageData<SourcePlayRecordForm, SourcePlayRecordQuery>>({
  180. form: {},
  181. queryParams: {
  182. pageNum: 1,
  183. pageSize: 10,
  184. sourceType: null,
  185. sourceTag: null,
  186. startTime: null,
  187. endTime: null,
  188. sourceName: null
  189. },
  190. rules: {}
  191. });
  192. const getTimesAndDurationLine = async () => {
  193. const params = {
  194. startTime: DDateRange.value[0],
  195. endTime: DDateRange.value[1],
  196. sourceId: dialogSourceId.value
  197. };
  198. const res = await timesAndDurationLine(params);
  199. const playDurationList = res.data.playDurationList.map((seconds) => Math.round((seconds / 60) * 100) / 100);
  200. if (!tadLine.value) {
  201. // console.log('[tadLine] ref is null when initializing echarts');
  202. } else {
  203. // console.log('[tadLine] ref:', tadLine.value);
  204. }
  205. if (tadLine.value) {
  206. echarts.dispose(tadLine.value);
  207. // console.log('[tadLine] echarts instance disposed before init');
  208. }
  209. const tadLineInstance = echarts.init(tadLine.value, 'macaroons');
  210. // console.log('[tadLine] echarts instance created:', tadLineInstance);
  211. tadLineInstance.setOption({
  212. title: {
  213. text: ''
  214. },
  215. tooltip: {
  216. trigger: 'axis'
  217. },
  218. legend: {
  219. data: ['播放次数', '播放时长(分钟)']
  220. },
  221. grid: {
  222. left: '3%',
  223. right: '4%',
  224. bottom: '3%',
  225. containLabel: true
  226. },
  227. toolbox: {
  228. feature: {
  229. saveAsImage: {}
  230. }
  231. },
  232. xAxis: {
  233. type: 'category',
  234. boundaryGap: false,
  235. data: res.data.timeList
  236. },
  237. yAxis: {
  238. type: 'value'
  239. },
  240. series: [
  241. {
  242. name: '播放次数',
  243. type: 'line',
  244. stack: 'Total',
  245. data: res.data.playTimesList
  246. },
  247. {
  248. name: '播放时长(分钟)',
  249. type: 'line',
  250. stack: 'Total',
  251. data: playDurationList
  252. }
  253. ]
  254. });
  255. };
  256. const { queryParams, form, rules } = toRefs(data);
  257. const getPushNumber = async () => {
  258. const res = await pushNumber();
  259. totalNum.value = res.data.totalNum;
  260. imageNum.value = res.data.imageNum;
  261. videoNum.value = res.data.videoNum;
  262. const lineRes = await getPushLine();
  263. if (!pushLine.value) {
  264. // console.log('[pushLine] ref is null when initializing echarts');
  265. } else {
  266. // console.log('[pushLine] ref:', pushLine.value);
  267. }
  268. if (pushLine.value) {
  269. echarts.dispose(pushLine.value);
  270. // console.log('[pushLine] echarts instance disposed before init');
  271. }
  272. const pushLineInstance = echarts.init(pushLine.value, 'macaroons');
  273. // console.log('[pushLine] echarts instance created:', pushLineInstance);
  274. pushLineInstance.setOption({
  275. title: {
  276. text: ''
  277. },
  278. tooltip: {
  279. trigger: 'axis'
  280. },
  281. legend: {
  282. data: ['']
  283. },
  284. grid: {
  285. left: '3%',
  286. right: '4%',
  287. bottom: '3%',
  288. containLabel: true
  289. },
  290. toolbox: {
  291. feature: {
  292. saveAsImage: {}
  293. }
  294. },
  295. xAxis: {
  296. type: 'category',
  297. boundaryGap: false,
  298. data: lineRes.data.timeList
  299. },
  300. yAxis: {
  301. type: 'value'
  302. },
  303. series: [
  304. {
  305. name: '',
  306. type: 'line',
  307. stack: 'Total',
  308. data: lineRes.data.numberList
  309. }
  310. ]
  311. });
  312. };
  313. /** 导出按钮操作 */
  314. const handleExport = () => {
  315. proxy?.download(
  316. 'source/playRecord/summary/export',
  317. {
  318. ...queryParams.value
  319. },
  320. `playRecord_${new Date().getTime()}.xlsx`
  321. );
  322. };
  323. const handleView = async (row?: SourcePlayRecordVO) => {
  324. dialog.title = row.fileName;
  325. dialog.visible = true;
  326. dialogSourceId.value = row.sourceId;
  327. DHandleDateRangeChange();
  328. };
  329. const getDeviceItemList = async () => {
  330. const params = {
  331. startTime: DDateRange.value[0],
  332. endTime: DDateRange.value[1],
  333. sourceId: dialogSourceId.value
  334. };
  335. const res = await listDeviceAndItem(params);
  336. deviceRecordList.value = res.data.deviceList;
  337. itemRecordList.value = res.data.itemList;
  338. };
  339. /** 查询资源播放记录列表 */
  340. const getRecordList = async () => {
  341. loading.value = true;
  342. queryParams.value.startTime = dateRange.value[0];
  343. queryParams.value.endTime = dateRange.value[1];
  344. const res = await listPlayRecordSummary(queryParams.value);
  345. playRecordList.value = res.rows;
  346. playRecordList.value.forEach((item) => {
  347. item.playDuration = formatDuration(item.playDuration);
  348. });
  349. total.value = res.total;
  350. loading.value = false;
  351. };
  352. // 格式化秒数为 HH:mm:ss
  353. const formatDuration = (seconds) => {
  354. const hours = Math.floor(seconds / 3600);
  355. const minutes = Math.floor((seconds % 3600) / 60);
  356. const secs = seconds % 60;
  357. const paddedHours = String(hours).padStart(2, '0');
  358. const paddedMinutes = String(minutes).padStart(2, '0');
  359. const paddedSeconds = String(secs).padStart(2, '0');
  360. return `${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
  361. };
  362. const handleDateRangeChange = () => {
  363. const rangeType = timeRadio.value;
  364. const today = new Date();
  365. const startDate = new Date();
  366. const endDate = new Date();
  367. switch (rangeType) {
  368. case 'week':
  369. startDate.setDate(today.getDate() - 7);
  370. dateRange.value = [formatDate(startDate), formatDate(endDate)];
  371. diyFlag.value = true;
  372. break;
  373. case 'month':
  374. startDate.setMonth(today.getMonth() - 1);
  375. dateRange.value = [formatDate(startDate), formatDate(endDate)];
  376. diyFlag.value = true;
  377. break;
  378. case "diy" :
  379. diyFlag.value = false;
  380. dateRange.value = [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])];
  381. break;
  382. default:
  383. break
  384. // throw new Error('Invalid range type');
  385. }
  386. getRecordList();
  387. getPushNumber();
  388. };
  389. const DHandleDateRangeChange = () => {
  390. const rangeType = DTimeRadio.value;
  391. const today = new Date();
  392. const startDate = new Date();
  393. const endDate = new Date();
  394. switch (rangeType) {
  395. case 'today':
  396. break;
  397. case 'week':
  398. startDate.setDate(today.getDate() - 7);
  399. break;
  400. case 'month':
  401. startDate.setMonth(today.getMonth() - 1);
  402. break;
  403. default:
  404. throw new Error('Invalid range type');
  405. }
  406. DDateRange.value = [formatDate(startDate), formatDate(endDate)];
  407. getTimesAndDurationLine();
  408. getDeviceItemList();
  409. };
  410. const formatDate = (date: Date) => {
  411. const year = date.getFullYear();
  412. const month = String(date.getMonth() + 1).padStart(2, '0');
  413. const day = String(date.getDate()).padStart(2, '0');
  414. return `${year}-${month}-${day}`;
  415. };
  416. onMounted(() => {
  417. handleDateRangeChange();
  418. // getPushNumber();
  419. // getRecordList();
  420. });
  421. onUnmounted(() => {
  422. if (pushLine?.value) {
  423. echarts.dispose(pushLine.value);
  424. // console.log('[play_info] pushLine disposed on unmount');
  425. }
  426. if (tadLine?.value) {
  427. echarts.dispose(tadLine.value);
  428. // console.log('[play_info] tadLine disposed on unmount');
  429. }
  430. });
  431. </script>
  432. <style scoped>
  433. .stat-card {
  434. text-align: center;
  435. height: 170px;
  436. }
  437. .number {
  438. font-size: 24px;
  439. font-weight: bold;
  440. }
  441. .success {
  442. color: green;
  443. }
  444. .danger {
  445. color: red;
  446. }
  447. .warning {
  448. color: orange;
  449. }
  450. .chart-placeholder {
  451. height: 250px;
  452. /*background: #f5f5f5;*/
  453. border-radius: 8px;
  454. }
  455. .disk-progress .el-progress--line {
  456. margin-bottom: 15px;
  457. max-width: 600px;
  458. margin-top: 50px;
  459. }
  460. </style>