forked from kongfp/Tp_Web2.0
feat: 视频分析页面开发
parent
8330d1df8c
commit
237d5a60d8
@ -1,2 +1,4 @@
|
|||||||
const { VITE_APP_BASE_URL } = import.meta.env;
|
const { VITE_APP_BASE_URL } = import.meta.env;
|
||||||
export const baseUrlApi = (url: string) => `${VITE_APP_BASE_URL}/api/${url}`;
|
import { getConfig } from "@/config";
|
||||||
|
export const baseUrlApi = (url: string) =>
|
||||||
|
`${getConfig().AdminHostUrl || VITE_APP_BASE_URL}/api/${url}`;
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
import { http } from "@/utils/http";
|
||||||
|
import { baseUrlApi } from "./utils";
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
msg: any;
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
person?: Array<any>;
|
||||||
|
car?: Array<any>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
type Data = {
|
||||||
|
success: boolean;
|
||||||
|
msg: any;
|
||||||
|
count: number;
|
||||||
|
results?: Array<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPicList = (params?: object) => {
|
||||||
|
return http.request<Result>("get", baseUrlApi("tps/pics"), { params });
|
||||||
|
};
|
||||||
|
export const getTPList = (params?: object) => {
|
||||||
|
return http.request<Data>("get", baseUrlApi("tps"), { params });
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
path: "/videoParse",
|
||||||
|
// redirect: "/error/403",
|
||||||
|
meta: {
|
||||||
|
icon: "lollipop",
|
||||||
|
title: "视频分析",
|
||||||
|
showLink: true,
|
||||||
|
rank: 10
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/videoParse/index",
|
||||||
|
name: "VideoParsePage",
|
||||||
|
component: () => import("@/views/videoParse/index.vue"),
|
||||||
|
meta: {
|
||||||
|
title: "视频分析"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
} as RouteConfigsTable;
|
@ -0,0 +1,77 @@
|
|||||||
|
import { addDialog } from "@/components/ReDialog";
|
||||||
|
// import { ref, h } from "vue";
|
||||||
|
import OurPlayer from "@/components/VideoPlayer/OurPlayer.vue";
|
||||||
|
// import { http } from "@/utils/http";
|
||||||
|
|
||||||
|
export function videoUtil() {
|
||||||
|
// const currentPlayingIndex = ref(-1);
|
||||||
|
const columns: TableColumnList = [
|
||||||
|
{
|
||||||
|
label: "视频内时间",
|
||||||
|
prop: "relative_time",
|
||||||
|
minWidth: 100,
|
||||||
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "异常类型",
|
||||||
|
prop: "exception_type",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "异常ID",
|
||||||
|
prop: "abnormal_id",
|
||||||
|
minWidth: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "异常行为",
|
||||||
|
prop: "abnormal_behavior",
|
||||||
|
minWidth: 100,
|
||||||
|
slot: "violation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "异常关键帧",
|
||||||
|
slot: "image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "异常视频定位",
|
||||||
|
slot: "video"
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// label: "视频",
|
||||||
|
// slot: "video"
|
||||||
|
// },
|
||||||
|
];
|
||||||
|
function openDialog(title = "视频", row) {
|
||||||
|
const highlights = [
|
||||||
|
// 进度条时间点高亮
|
||||||
|
{
|
||||||
|
text: row.event_type,
|
||||||
|
time: row.relative_time
|
||||||
|
}
|
||||||
|
];
|
||||||
|
sessionStorage.setItem("video_url", row.abnormal_video);
|
||||||
|
sessionStorage.setItem("highlights", JSON.stringify(highlights));
|
||||||
|
addDialog({
|
||||||
|
title: `${title}用户`,
|
||||||
|
width: "40%",
|
||||||
|
draggable: true,
|
||||||
|
fullscreenIcon: true,
|
||||||
|
closeOnClickModal: false,
|
||||||
|
contentRenderer: () => <OurPlayer></OurPlayer>
|
||||||
|
// return h(
|
||||||
|
// "video",
|
||||||
|
// {
|
||||||
|
// controls: true,
|
||||||
|
// id: "video-" + index,
|
||||||
|
// onPlay: () => playVideo(row, index)
|
||||||
|
// },
|
||||||
|
// [h("source", { src: row.video_dir, type: "video/mp4" })]
|
||||||
|
// );
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns,
|
||||||
|
openDialog
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,454 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useWindowSize } from "@vueuse/core";
|
||||||
|
import { Help } from "@element-plus/icons-vue";
|
||||||
|
import { ref, reactive, onMounted } from "vue";
|
||||||
|
import { PureTableBar } from "@/components/RePureTableBar";
|
||||||
|
import { PureTable } from "@pureadmin/table";
|
||||||
|
import { type PaginationProps } from "@pureadmin/table";
|
||||||
|
import { getPicList, getTPList } from "@/api/videoParse";
|
||||||
|
import { getToken } from "@/utils/auth";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { videoUtil } from "./hook";
|
||||||
|
|
||||||
|
import type { UploadProps, UploadUserFile } from "element-plus";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "videoParse"
|
||||||
|
});
|
||||||
|
const { columns, openDialog } = videoUtil();
|
||||||
|
const fileList = ref<UploadUserFile[]>([
|
||||||
|
{
|
||||||
|
name: "element-plus-logo.svg",
|
||||||
|
url: "https://element-plus.org/images/element-plus-logo.svg"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleRemove: UploadProps["onRemove"] = (file, uploadFiles) => {
|
||||||
|
console.log(file, uploadFiles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview: UploadProps["onPreview"] = uploadFile => {
|
||||||
|
console.log(uploadFile);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExceed: UploadProps["onExceed"] = (files, uploadFiles) => {
|
||||||
|
ElMessage.warning(
|
||||||
|
`The limit is 3, you selected ${files.length} files this time, add up to ${
|
||||||
|
files.length + uploadFiles.length
|
||||||
|
} totally`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeRemove: UploadProps["beforeRemove"] = uploadFile => {
|
||||||
|
return ElMessageBox.confirm(
|
||||||
|
`Cancel the transfer of ${uploadFile.name} ?`
|
||||||
|
).then(
|
||||||
|
() => true,
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const { height } = useWindowSize();
|
||||||
|
|
||||||
|
const videoData = ref([]);
|
||||||
|
const loading = ref(true);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const totalNumber = ref(0);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
// const isDisplay = true;
|
||||||
|
const pagination = reactive<PaginationProps>({
|
||||||
|
total: totalNumber.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
currentPage: currentPage.value,
|
||||||
|
background: true
|
||||||
|
});
|
||||||
|
const personOddList = ref([]);
|
||||||
|
const personEvenList = ref([]);
|
||||||
|
const carOddList = ref([]);
|
||||||
|
const carEvenList = ref([]);
|
||||||
|
|
||||||
|
function handleUpdate(row) {
|
||||||
|
openDialog("我的", row);
|
||||||
|
console.log(row);
|
||||||
|
}
|
||||||
|
function handleSizeChange(val) {
|
||||||
|
pageSize.value = val;
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCurrentChange(val) {
|
||||||
|
currentPage.value = val;
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
function onSearch() {
|
||||||
|
const params = {
|
||||||
|
video_name: "test1",
|
||||||
|
page: currentPage.value || undefined,
|
||||||
|
page_size: pageSize.value || undefined
|
||||||
|
};
|
||||||
|
getTPList(params).then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
const dataList = response;
|
||||||
|
totalNumber.value = dataList.count;
|
||||||
|
pagination.total = dataList.count;
|
||||||
|
videoData.value = dataList.results;
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getPicList(params).then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
const dataList = response;
|
||||||
|
personOddList.value = dataList.data.person.filter(
|
||||||
|
item => item.id % 2 == 1
|
||||||
|
);
|
||||||
|
personEvenList.value = dataList.data.person.filter(
|
||||||
|
item => item.id % 2 == 0
|
||||||
|
);
|
||||||
|
carOddList.value = dataList.data.car.filter(item => item.id % 2 == 1);
|
||||||
|
carEvenList.value = dataList.data.car.filter(item => item.id % 2 == 0);
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function spanStyle(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 1:
|
||||||
|
return "span-first";
|
||||||
|
case 2:
|
||||||
|
return "span-second";
|
||||||
|
case 3:
|
||||||
|
return "span-third";
|
||||||
|
default:
|
||||||
|
"";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<el-row :gutter="24">
|
||||||
|
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
||||||
|
<el-card
|
||||||
|
class="box-card"
|
||||||
|
:style="{ height: `calc(${height}px - 150px)` }"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>视频分析</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="file-upload flex justify-around">
|
||||||
|
<div class="upload-box">
|
||||||
|
<el-upload
|
||||||
|
v-model:file-list="fileList"
|
||||||
|
class="upload"
|
||||||
|
:headers="{ token: 'Bearer ' + getToken().accessToken }"
|
||||||
|
action="http://192.168.10.13:8000/api/tps/upload_video"
|
||||||
|
multiple
|
||||||
|
:on-preview="handlePreview"
|
||||||
|
:on-remove="handleRemove"
|
||||||
|
:before-remove="beforeRemove"
|
||||||
|
:limit="1"
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
>
|
||||||
|
<span>{{ "选择分析文件" }}</span
|
||||||
|
><el-button type="primary">选择文件</el-button>
|
||||||
|
<!-- <template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
jpg/png files with a size less than 500KB.
|
||||||
|
</div>
|
||||||
|
</template> -->
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<el-button type="success" size="large">
|
||||||
|
分析 <el-icon class="el-icon--right"><Help /></el-icon
|
||||||
|
></el-button>
|
||||||
|
</div>
|
||||||
|
<el-divider border-style="dashed" />
|
||||||
|
<!-- <el-empty description="暂无数据" v-show="videoData.length == 0" /> -->
|
||||||
|
<div class="analysis-content flex justify-between">
|
||||||
|
<div
|
||||||
|
class="analysis-box"
|
||||||
|
:style="{ height: `calc(${height}px - 60vh - 150px)` }"
|
||||||
|
>
|
||||||
|
<span class="analysis-box-label">被执法人员推定</span>
|
||||||
|
<div
|
||||||
|
class="analysis-table"
|
||||||
|
:style="{ height: `calc(${height}px - 60vh - 240px)` }"
|
||||||
|
>
|
||||||
|
<div class="analysis-table-left">
|
||||||
|
<div class="analysis-box-lable w-full flex justify-end">
|
||||||
|
<span>目标清晰度</span>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar :height="`calc(${height}px - 60vh - 220px)`">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in personOddList"
|
||||||
|
:key="index"
|
||||||
|
class="scrollbar-item"
|
||||||
|
>
|
||||||
|
<span :class="spanStyle(item.id)">{{ item.id }}</span>
|
||||||
|
<el-image
|
||||||
|
style="height: 100px"
|
||||||
|
:src="item.pic_path"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
:preview-src-list="[item.pic_path]"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span>{{ item.pic_quality }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
<el-divider
|
||||||
|
direction="vertical"
|
||||||
|
:style="{ height: `calc(${height}px - 60vh - 220px)` }"
|
||||||
|
/>
|
||||||
|
<div class="analysis-table-left">
|
||||||
|
<div class="analysis-box-lable w-full flex justify-end">
|
||||||
|
<span>目标清晰度</span>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar :height="`calc(${height}px - 60vh - 220px)`">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in personEvenList"
|
||||||
|
:key="index"
|
||||||
|
class="scrollbar-item"
|
||||||
|
>
|
||||||
|
<span :class="spanStyle(item.id)">{{ item.id }}</span>
|
||||||
|
<el-image
|
||||||
|
style="height: 100px"
|
||||||
|
:src="item.pic_path"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
:preview-src-list="[item.pic_path]"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span>{{ item.pic_quality }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="analysis-box"
|
||||||
|
:style="{ height: `calc(${height}px - 60vh - 150px)` }"
|
||||||
|
>
|
||||||
|
<span class="analysis-box-label">被执法车辆推定</span>
|
||||||
|
<div
|
||||||
|
class="analysis-table"
|
||||||
|
:style="{ height: `calc(${height}px - 60vh - 240px)` }"
|
||||||
|
>
|
||||||
|
<div class="analysis-table-left">
|
||||||
|
<div class="analysis-box-lable w-full flex justify-end">
|
||||||
|
<span>目标清晰度</span>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar :height="`calc(${height}px - 60vh - 220px)`">
|
||||||
|
<div
|
||||||
|
v-for="item in carOddList"
|
||||||
|
:key="item.id"
|
||||||
|
class="scrollbar-item"
|
||||||
|
>
|
||||||
|
<span :class="spanStyle(item.id)">{{ item.id }}</span>
|
||||||
|
<div class="defect-image">
|
||||||
|
<el-image
|
||||||
|
style="height: 100px"
|
||||||
|
:src="item.pic_path"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
:preview-src-list="[item.pic_path]"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span>{{ item.car_number }}</span>
|
||||||
|
</div>
|
||||||
|
<span>{{ item.pic_quality }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
<el-divider
|
||||||
|
direction="vertical"
|
||||||
|
:style="{ height: `calc(${height}px - 60vh - 240px)` }"
|
||||||
|
/>
|
||||||
|
<div class="analysis-table-left">
|
||||||
|
<div class="analysis-box-lable w-full flex justify-end">
|
||||||
|
<span>目标清晰度</span>
|
||||||
|
</div>
|
||||||
|
<el-scrollbar :height="`calc(${height}px - 60vh - 220px)`">
|
||||||
|
<div
|
||||||
|
v-for="item in carEvenList"
|
||||||
|
:key="item.id"
|
||||||
|
class="scrollbar-item"
|
||||||
|
>
|
||||||
|
<span :class="spanStyle(item.id)">{{ item.id }}</span>
|
||||||
|
<div class="defect-image">
|
||||||
|
<el-image
|
||||||
|
style="height: 100px"
|
||||||
|
:src="item.pic_path"
|
||||||
|
:zoom-rate="1.2"
|
||||||
|
:preview-src-list="[item.pic_path]"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
<span>{{ item.car_number }}</span>
|
||||||
|
</div>
|
||||||
|
<span>{{ item.pic_quality }}</span>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<PureTableBar
|
||||||
|
title="视频分析列表"
|
||||||
|
:columns="columns"
|
||||||
|
@refresh="onSearch"
|
||||||
|
>
|
||||||
|
<template v-slot="{ size, dynamicColumns }">
|
||||||
|
<pure-table
|
||||||
|
border
|
||||||
|
adaptive
|
||||||
|
align-whole="center"
|
||||||
|
table-layout="auto"
|
||||||
|
:loading="loading"
|
||||||
|
:size="size"
|
||||||
|
:data="videoData"
|
||||||
|
:columns="dynamicColumns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:paginationSmall="size === 'small' ? true : false"
|
||||||
|
:header-cell-style="{
|
||||||
|
background: 'var(--el-table-row-hover-bg-color)',
|
||||||
|
color: 'var(--el-text-color-primary)'
|
||||||
|
}"
|
||||||
|
@page-size-change="handleSizeChange"
|
||||||
|
@page-current-change="handleCurrentChange"
|
||||||
|
>
|
||||||
|
<template #violation="{ row }">
|
||||||
|
{{ row.is_violation ? "是" : "否" }}
|
||||||
|
</template>
|
||||||
|
<template #image="{ row, index }">
|
||||||
|
<el-image
|
||||||
|
preview-teleported
|
||||||
|
loading="lazy"
|
||||||
|
:src="row.abnormal_pic"
|
||||||
|
:preview-src-list="[row.abnormal_pic]"
|
||||||
|
:initial-index="index"
|
||||||
|
fit="cover"
|
||||||
|
class="w-[100px] h-[100px]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #video="{ row }">
|
||||||
|
<el-button
|
||||||
|
class="reset-margin"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
:size="size"
|
||||||
|
@click="handleUpdate(row)"
|
||||||
|
>
|
||||||
|
播放视频
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</pure-table>
|
||||||
|
</template>
|
||||||
|
</PureTableBar>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.card-header {
|
||||||
|
span {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload {
|
||||||
|
height: 60px;
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
width: 300px;
|
||||||
|
|
||||||
|
.upload {
|
||||||
|
span {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-content {
|
||||||
|
.analysis-box {
|
||||||
|
width: 48%;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 5px 5px 5px rgb(0 0 0 / 2.5%), 5px -5px 5px rgb(0 0 0 / 2.5%),
|
||||||
|
-5px 5px 5px rgb(0 0 0 / 2.5%), -5px -5px 5px rgb(0 0 0 / 2.5%);
|
||||||
|
// background: skyblue;
|
||||||
|
// height: 400px;
|
||||||
|
.analysis-box-label {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-table {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.analysis-table-left {
|
||||||
|
width: 45%;
|
||||||
|
|
||||||
|
.analysis-box-lable {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.defect-image {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
span {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 7px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgb(0 0 0 / 50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.analysis-box-line) {
|
||||||
|
height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-first {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e02828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-second {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e58b03;
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-third {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #516ee0;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue