| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616 |
- <template>
- <el-container class="play-info-container">
- <el-header style="height: auto; padding: 20px;">
- <el-row :gutter="20">
- <el-col :span="12">
- <el-card shadow="hover" class="stat-card overview-card">
- <!-- <template #header>
- <div class="card-header">
- <span>发布趋势</span>
- </div>
- </template>-->
- <el-row :gutter="20" align="middle">
- <el-col :span="8" class="text-center">
- <div class="stat-number total-num">{{ totalNum }}</div>
- <div class="stat-label">总发布量</div>
- </el-col>
- <el-col :span="16">
- <div ref="pushLine" style="height: 120px"></div>
- </el-col>
- </el-row>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover" class="stat-card">
- <div class="stat-content">
- <el-icon class="stat-icon icon-image"><Picture /></el-icon>
- <div>
- <div class="stat-number image-num">{{ imageNum }}</div>
- <div class="stat-label">图片发布</div>
- </div>
- </div>
- </el-card>
- </el-col>
- <el-col :span="6">
- <el-card shadow="hover" class="stat-card">
- <div class="stat-content">
- <el-icon class="stat-icon icon-video"><VideoCameraFilled /></el-icon>
- <div>
- <div class="stat-number video-num">{{ videoNum }}</div>
- <div class="stat-label">视频发布</div>
- </div>
- </div>
- </el-card>
- </el-col>
- </el-row>
- </el-header>
- <el-main style="padding: 0 20px;height: 80vh">
- <el-card shadow="hover">
- <el-form ref="queryFormRef" :model="queryParams" :inline="true">
- <el-row justify="space-between" align="middle" style="width: 100%">
- <el-form-item prop="sourceName">
- <el-input v-model="queryParams.sourceName" placeholder="请输入文件名称" @input="getRecordList" clearable />
- </el-form-item>
- <el-form-item prop="sourceType">
- <el-select v-model="queryParams.sourceType" placeholder="请选择类型" @change="getRecordList" clearable>
- <el-option v-for="dict in smsb_source_type" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item prop="sourceTag">
- <el-select v-model="queryParams.sourceTag" placeholder="请选择分类" @change="getRecordList" clearable>
- <el-option v-for="dict in smsb_source_classify" :key="dict.value" :label="dict.label" :value="dict.value" />
- </el-select>
- </el-form-item>
- <el-form-item prop="dataRage">
- <el-radio-group v-model="timeRadio" @change="handleDateRangeChange" style="margin-right: 10px;">
- <el-radio-button label="近7天" value="week" />
- <el-radio-button label="近30天" value="month" />
- <el-radio-button label="自定义" value="diy" />
- </el-radio-group>
- <el-date-picker v-model="dateRange" type="daterange" range-separator="-" start-placeholder="开始日期"
- end-placeholder="结束日期" :disabled="diyFlag" :clearable="false" @change="handleDateRangeChange" />
- </el-form-item>
- <el-button type="warning" plain icon="Download" style="margin-bottom: 15px" @click="handleExport">报表导出
- </el-button>
- </el-row>
- </el-form>
- <div class="table-content" style="margin-top: 10px;">
- <el-table v-loading="loading" :data="playRecordList" row-key="sourceId" header-cell-class-name="table-header">
- <el-table-column label="资源ID" prop="sourceId" width="250" />
- <el-table-column label="资源名称" prop="fileName" :show-overflow-tooltip="true" />
- <el-table-column label="播放次数" align="center" prop="playTimes" width="150" />
- <el-table-column label="播放时长" align="center" prop="playDuration" width="150" />
- <el-table-column label="分类" align="center" prop="fileTag" width="120">
- <template #default="scope">
- <dict-tag :options="smsb_source_classify" :value="scope.row.fileTag" />
- </template>
- </el-table-column>
- <el-table-column label="类型" align="center" prop="fileType" width="120">
- <template #default="scope">
- <dict-tag :options="smsb_source_type" :value="scope.row.fileType" />
- </template>
- </el-table-column>
- <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120">
- <template #default="scope">
- <el-button link type="primary" icon="View" @click="handleView(scope.row)">查看详情</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
- v-model:limit="queryParams.pageSize" @pagination="getRecordList" />
- </el-card>
- </el-main>
- </el-container>
- <el-dialog :title="dialog.title" v-model="dialog.visible" width="70%" top="5vh" append-to-body
- custom-class="details-dialog">
- <el-container>
- <el-header style="height: auto; padding-bottom: 20px;">
- <el-row justify="end">
- <el-radio-group v-model="DTimeRadio" size="small" @change="DHandleDateRangeChange" style="margin-right: 10px;">
- <el-radio-button label="近7天" value="week" />
- <el-radio-button label="近30天" value="month" />
- </el-radio-group>
- <!-- <el-date-picker style="width: 280px" :disabled="true" v-model="DDateRange" type="daterange" range-separator="-"
- start-placeholder="开始日期" end-placeholder="结束日期" size="small" />-->
- </el-row>
- </el-header>
- <el-main style="padding: 0;">
- <el-card shadow="never" style="margin-bottom: 20px;">
- <div ref="tadLine" class="chart-placeholder"></div>
- </el-card>
- <div style="width: 100%">
- <el-row>
- <el-col :span="12">
- <el-card shadow="never">
- <template #header>
- <div class="card-header">
- <span>设备播放统计</span>
- </div>
- </template>
- <el-table v-loading="loading" :data="deviceRecordList" height="250px">
- <el-table-column label="设备名称" align="center" prop="deviceName" />
- <el-table-column label="播放次数" align="center" prop="playTimes" />
- <el-table-column label="播放时长" align="center" prop="duration" />
- </el-table>
- </el-card>
- </el-col>
- <el-col :span="12">
- <el-card shadow="never">
- <template #header>
- <div class="card-header">
- <span>节目播放统计</span>
- </div>
- </template>
- <el-table v-loading="loading" :data="itemRecordList" height="250px">
- <el-table-column label="组名称" align="center" prop="itemName" />
- <el-table-column label="类型" align="center" prop="itemType">
- <template #default="scope">
- <dict-tag :options="smsb_item_type" :value="scope.row.itemType" />
- </template>
- </el-table-column>
- <el-table-column label="播放次数" align="center" prop="playTimes" />
- <el-table-column label="播放时长" align="center" prop="duration" />
- </el-table>
- </el-card>
- </el-col>
- </el-row>
- </div>
- </el-main>
- </el-container>
- </el-dialog>
- </template>
- <script setup lang="ts">
- import * as echarts from 'echarts';
- import {
- getPushLine,
- listDeviceAndItem,
- listPlayRecordSummary,
- pushNumber,
- timesAndDurationLine
- } from '@/api/smsb/source/play_record';
- import {SourcePlayRecordForm, SourcePlayRecordQuery, SourcePlayRecordVO} from '@/api/smsb/source/play_record_type';
- import {onUnmounted} from 'vue';
- import {Picture, VideoCameraFilled} from '@element-plus/icons-vue';
- const {proxy} = getCurrentInstance() as ComponentInternalInstance;
- const {smsb_source_classify, smsb_source_type, smsb_item_type} = toRefs<any>(
- proxy?.useDict('smsb_source_classify', 'smsb_source_type', 'smsb_item_type')
- );
- const loading = ref(true);
- const playRecordList = ref<SourcePlayRecordVO[]>([]);
- const deviceRecordList = ref<SourcePlayRecordVO[]>([]);
- const itemRecordList = ref<SourcePlayRecordVO[]>([]);
- const timeRadio = ref('week');
- const DTimeRadio = ref('week');
- const dateRange = ref<(string | Date)[]>([]);
- const DDateRange = ref<(string | Date)[]>([]);
- const totalNum = ref(0);
- const imageNum = ref(0);
- const videoNum = ref(0);
- const pushLine = ref();
- const total = ref(0);
- const tadLine = ref();
- const dialogSourceId = ref();
- const diyFlag = ref(true);
- const dialog = reactive<DialogOption>({
- visible: false,
- title: ''
- });
- const data = reactive<PageData<SourcePlayRecordForm, SourcePlayRecordQuery>>({
- form: {},
- queryParams: {
- pageNum: 1,
- pageSize: 10,
- sourceType: null,
- sourceTag: null,
- startTime: null,
- endTime: null,
- sourceName: null
- },
- rules: {}
- });
- // ★★★★★【美化核心】: 定义通用的 ECharts 美化配置 ★★★★★
- const getBaseChartOptions = () => ({
- grid: {
- left: '5%',
- right: '5%',
- bottom: '10%',
- top: '15%',
- containLabel: true
- },
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255, 255, 255, 0.9)',
- borderColor: '#E5E5E5',
- borderWidth: 1,
- textStyle: {
- color: '#333'
- },
- axisPointer: {
- type: 'cross',
- label: {
- backgroundColor: '#6a7985'
- }
- }
- },
- toolbox: {
- show: false,
- feature: {
- saveAsImage: {
- title: '保存图片',
- pixelRatio: 2
- }
- },
- right: '20px'
- },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- axisLine: {
- show: false
- },
- axisTick: {
- show: false
- },
- axisLabel: {
- color: '#888'
- }
- },
- yAxis: {
- type: 'value',
- axisLabel: {
- color: '#888'
- },
- splitLine: {
- lineStyle: {
- type: 'dashed',
- color: '#E5E5E5'
- }
- }
- },
- legend: {
- data: [],
- right: 'center',
- top: '5px',
- textStyle: {
- color: '#555'
- }
- }
- });
- const getTimesAndDurationLine = async () => {
- const params = {
- startTime: DDateRange.value[0],
- endTime: DDateRange.value[1],
- sourceId: dialogSourceId.value
- };
- const res = await timesAndDurationLine(params);
- const playDurationList = res.data.playDurationList.map((seconds: number) => Math.round((seconds / 60) * 100) / 100);
- if (tadLine.value) {
- echarts.dispose(tadLine.value);
- }
- const tadLineInstance = echarts.init(tadLine.value, 'macaroons');
- // 合并通用配置和特定配置
- const option = {
- ...getBaseChartOptions(),
- legend: {
- ...getBaseChartOptions().legend,
- data: ['播放次数', '播放时长(分钟)']
- },
- xAxis: {
- ...getBaseChartOptions().xAxis,
- data: res.data.timeList
- },
- series: [
- {
- name: '播放次数',
- type: 'line',
- smooth: true,
- showSymbol: false,
- itemStyle: { color: '#5470C6' },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(84, 112, 198, 0.4)' },
- { offset: 1, color: 'rgba(84, 112, 198, 0)' }
- ])
- },
- data: res.data.playTimesList
- },
- {
- name: '播放时长(分钟)',
- type: 'line',
- smooth: true,
- showSymbol: false,
- itemStyle: { color: '#91CC75' },
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(145, 204, 117, 0.4)' },
- { offset: 1, color: 'rgba(145, 204, 117, 0)' }
- ])
- },
- data: playDurationList
- }
- ]
- };
- tadLineInstance.setOption(option);
- };
- const { queryParams, form, rules } = toRefs(data);
- // ★★★★★【应用美化】: 修改 getPushNumber 函数 ★★★★★
- const getPushNumber = async () => {
- const res = await pushNumber();
- totalNum.value = res.data.totalNum;
- imageNum.value = res.data.imageNum;
- videoNum.value = res.data.videoNum;
- const lineRes = await getPushLine();
- if (pushLine.value) {
- echarts.dispose(pushLine.value);
- }
- const pushLineInstance = echarts.init(pushLine.value, 'macaroons');
- // 合并通用配置和特定配置
- const option = {
- ...getBaseChartOptions(),
- grid: { // 针对小图表微调 grid
- left: '3%',
- right: '4%',
- bottom: '5%',
- top: '10%',
- containLabel: true
- },
- tooltip: { // 微调 tooltip
- ...getBaseChartOptions().tooltip
- },
- legend: {}, // 这个图表不需要 legend
- xAxis: {
- ...getBaseChartOptions().xAxis,
- data: lineRes.data.timeList
- },
- yAxis: {
- ...getBaseChartOptions().yAxis,
- axisLabel: { show: false }, // 小图表可以隐藏y轴标签
- splitLine: { show: false } // 隐藏分割线
- },
- series: [
- {
- name: '发布量',
- type: 'line',
- smooth: true,
- showSymbol: false,
- itemStyle: { color: '#34a853' }, // 与总发布量颜色一致
- areaStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: 'rgba(52, 168, 83, 0.4)' },
- { offset: 1, color: 'rgba(52, 168, 83, 0)' }
- ])
- },
- data: lineRes.data.numberList
- }
- ]
- };
- pushLineInstance.setOption(option);
- };
- /** 导出按钮操作 */
- const handleExport = () => {
- proxy?.download(
- 'source/playRecord/summary/export',
- {
- ...queryParams.value
- },
- `playRecord_${new Date().getTime()}.xlsx`
- );
- };
- const handleView = async (row?: SourcePlayRecordVO) => {
- dialog.title = "详情 - " + row.fileName;
- dialog.visible = true;
- dialogSourceId.value = row.sourceId;
- DHandleDateRangeChange();
- };
- const getDeviceItemList = async () => {
- const params = {
- startTime: DDateRange.value[0],
- endTime: DDateRange.value[1],
- sourceId: dialogSourceId.value
- };
- const res = await listDeviceAndItem(params);
- deviceRecordList.value = res.data.deviceList;
- itemRecordList.value = res.data.itemList;
- };
- /** 查询资源播放记录列表 */
- const getRecordList = async () => {
- loading.value = true;
- queryParams.value.startTime = dateRange.value[0] as string;
- queryParams.value.endTime = dateRange.value[1] as string;
- const res = await listPlayRecordSummary(queryParams.value);
- playRecordList.value = res.rows;
- playRecordList.value.forEach((item) => {
- item.playDuration = formatDuration(item.playDuration);
- });
- total.value = res.total;
- loading.value = false;
- };
- // 格式化秒数为 HH:mm:ss
- const formatDuration = (seconds: number | null | undefined): string => {
- if (seconds === null || seconds === undefined) return '00:00:00';
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = seconds % 60;
- const paddedHours = String(hours).padStart(2, '0');
- const paddedMinutes = String(minutes).padStart(2, '0');
- const paddedSeconds = String(Math.round(secs)).padStart(2, '0');
- return `${paddedHours}:${paddedMinutes}:${paddedSeconds}`;
- };
- const formatDate = (date: Date | string): string => {
- if (!date) return '';
- const d = new Date(date);
- const year = d.getFullYear();
- const month = String(d.getMonth() + 1).padStart(2, '0');
- const day = String(d.getDate()).padStart(2, '0');
- return `${year}-${month}-${day}`;
- };
- const handleDateRangeChange = () => {
- const rangeType = timeRadio.value;
- const today = new Date();
- let startDate = new Date();
- const endDate = new Date();
- switch (rangeType) {
- case 'week':
- startDate.setDate(today.getDate() - 7);
- dateRange.value = [formatDate(startDate), formatDate(endDate)];
- diyFlag.value = true;
- break;
- case 'month':
- startDate.setMonth(today.getMonth() - 1);
- dateRange.value = [formatDate(startDate), formatDate(endDate)];
- diyFlag.value = true;
- break;
- case "diy" :
- diyFlag.value = false;
- if (dateRange.value && dateRange.value.length === 2) {
- dateRange.value = [formatDate(dateRange.value[0]), formatDate(dateRange.value[1])];
- }
- break;
- default:
- break
- }
- getRecordList();
- getPushNumber();
- };
- const DHandleDateRangeChange = () => {
- const rangeType = DTimeRadio.value;
- const today = new Date();
- let startDate = new Date();
- const endDate = new Date();
- switch (rangeType) {
- case 'today':
- startDate = today;
- break;
- case 'week':
- startDate.setDate(today.getDate() - 7);
- break;
- case 'month':
- startDate.setMonth(today.getMonth() - 1);
- break;
- default:
- throw new Error('Invalid range type');
- }
- DDateRange.value = [formatDate(startDate), formatDate(endDate)];
- getTimesAndDurationLine();
- getDeviceItemList();
- };
- onMounted(() => {
- handleDateRangeChange();
- });
- onUnmounted(() => {
- if (pushLine?.value) {
- echarts.dispose(pushLine.value);
- }
- if (tadLine?.value) {
- echarts.dispose(tadLine.value);
- }
- });
- </script>
- <style lang="scss" scoped>
- .play-info-container {
- background-color: #f0f2f5;
- height: 100%;
- }
- .stat-card {
- border-radius: 12px;
- border: none;
- height: 170px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- &.overview-card {
- .card-header {
- font-weight: bold;
- }
- .text-center {
- border-right: 1px solid #e0e0e0;
- }
- }
- .stat-content {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 20px;
- }
- .stat-icon {
- font-size: 48px;
- &.icon-image { color: #ff9900; }
- &.icon-video { color: #34a853; }
- }
- .stat-number {
- font-size: 28px;
- font-weight: bold;
- line-height: 1.2;
- &.total-num { color: #34a853; }
- &.image-num { color: #ff9900; }
- &.video-num { color: #34a853; }
- }
- .stat-label {
- margin-top: 8px;
- font-size: 14px;
- color: #888;
- }
- }
- .el-card {
- border-radius: 12px;
- }
- .table-header {
- background-color: #f5f7fa !important;
- color: #333;
- font-weight: bold;
- }
- .chart-placeholder {
- height: 280px;
- width: 100%;
- }
- .details-dialog .el-dialog__body {
- padding: 10px 20px 30px 20px;
- max-height: calc(90vh - 120px);
- overflow-y: auto;
- }
- .card-header {
- font-weight: bold;
- font-size: 16px;
- color: #333;
- }
- </style>
|