| | |
| | | <template> |
| | | <div> |
| | | |
| | | <el-button id="button" type="primary" @click="handlePrint">打印版图</el-button> |
| | | <RectRenderer |
| | | ref="rectRenderer" |
| | | :layoutData="layoutData" |
| | | :gw="1400" |
| | | :gh="1100" |
| | | style="width: 100%; height: 800px; position: relative;" |
| | | /> |
| | | <!-- <el-button id="button" type="primary" @click="printReports" style="position: fixed; top: 90px; right: 20px; padding: 20px; background: #409eff; color: white; border: none; border-radius: 5px; cursor: pointer;">--> |
| | | <!-- 打印--> |
| | | <!-- </el-button>--> |
| | | <el-button |
| | | id="button" |
| | | type="primary" |
| | | @click="previewReport" |
| | | :loading="loading" style="position: fixed; top: 90px; right: 40px; padding: 20px; background: #409eff; color: white; border: none; border-radius: 5px; cursor: pointer;" |
| | | :disabled="loading"> |
| | | {{ loading ? '生成中...' : '预览' }} |
| | | </el-button> |
| | | |
| | | <div style="display: flex; align-items: center; gap: 20px; margin-bottom: 20px;"> |
| | | <span>工程编号:</span> |
| | | <el-input readonly placeholder="" style="width: 150px" v-model="processId"></el-input> |
| | | </div> |
| | | |
| | | <div v-if="pdfUrl || pdfLoading" style="margin-top: 20px;"> |
| | | <!-- PDF加载时显示美化后的加载动画 --> |
| | | <div v-if="pdfLoading" style="display: flex; justify-content: center; align-items: center; height: 800px; border: 1px solid #ddd; background-color: #f5f5f5;"> |
| | | <div style="text-align: center;"> |
| | | <div class="loading-animation"> |
| | | <div class="dot-flashing"></div> |
| | | </div> |
| | | <p style="margin-top: 20px; font-size: 18px; color: #666;">PDF文档加载中...</p> |
| | | <p style="font-size: 14px; color: #999; margin-top: 10px;">正在为您准备预览内容</p> |
| | | </div> |
| | | </div> |
| | | <!-- PDF加载完成时显示PDF --> |
| | | <iframe |
| | | v-else |
| | | :src="pdfUrl" style="width: 100%; height: 780px; border: 1px solid #ddd;" |
| | | title="PDF预览"> |
| | | </iframe> |
| | | </div> |
| | | |
| | | <div v-else style="margin-top: 20px;"> |
| | | <div style="display: flex; justify-content: center; align-items: center; height: 780px; border: 1px solid #ddd; background-color: white;"> |
| | | <div style="text-align: center; color: #999;"> |
| | | <p style="font-size: 20px; margin-bottom: 10px;">优化报告预览区</p> |
| | | <p style="font-size: 14px;">点击上方"预览"按钮生成并查看优化报告</p> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div ref="printContainer" style="position: relative;"> |
| | | <RectRenderer |
| | | ref="rectRenderer" |
| | | :layoutData="layoutData" |
| | | :gw="currentGw" |
| | | :gh="currentGh" |
| | | :printLayout="printLayout" |
| | | :printWidth="currentPrintWidth" |
| | | :printHeight="currentPrintHeight" |
| | | :materialDetails="materialDetails" |
| | | :state="state" |
| | | :projectNo="processId" |
| | | style="" |
| | | v-if="dataLoaded" |
| | | /> |
| | | </div> |
| | | </div> |
| | | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref,onMounted } from 'vue'; |
| | | import { ref, onMounted, watch, reactive, inject } from 'vue'; |
| | | import RectRenderer from './page/RectRenderer.vue'; |
| | | import mockLayoutData from '../../../components/pp/MockData'; |
| | | import request from "@/utils/request"; |
| | | import { useI18n } from "vue-i18n"; |
| | | //const layoutData = ref(mockLayoutData); |
| | | import {ElMessage, ElMessageBox} from "element-plus"; |
| | | import requestOptimize from "@/utils/requestOptimize"; |
| | | import useUserInfoStore from "@/stores/userInfo"; |
| | | |
| | | const userStore = useUserInfoStore(); |
| | | |
| | | const props = defineProps({ |
| | | project : null, |
| | | state : null |
| | | }); |
| | | const printLayout = ref('2rows-2cols'); |
| | | const rectRenderer = ref(null); |
| | | |
| | | const savedProjectNo = localStorage.getItem('projectNo'); |
| | | const processId = savedProjectNo; |
| | | const layoutData = ref(null); |
| | | const processId = ref(''); |
| | | const layoutData = ref(); |
| | | const optimizeUse = ref(); |
| | | const reportData = ref(); |
| | | const materialList = ref(); |
| | | const productList = ref(); |
| | | const dataLoaded = ref(false); |
| | | const materialDetails = ref(); |
| | | const injectedProjectNo = inject('projectNo', null); |
| | | const state = ref(); |
| | | const filePath = ref(''); |
| | | const loading = ref(false); |
| | | const pdfLoading = ref(false); |
| | | const printLayouts = ref(); |
| | | const printReport = ref(); |
| | | const layoutRows = ref(); |
| | | const layoutColumns = ref(); |
| | | const glassInfoShow = ref(); |
| | | const cutInfoShow = ref(); |
| | | const username = userStore.user.userName; |
| | | |
| | | const pdfUrl = ref(''); |
| | | |
| | | // 定义不同布局对应的尺寸 |
| | | const layoutDimensions = { |
| | | '4rows-2cols': { width: 1000, height: 1000 }, |
| | | '3rows-2cols': { width: 1000, height: 1000 }, |
| | | '3rows-1col': { width: 1000, height: 1000 }, |
| | | '2rows-2cols': { width: 1400, height: 800 } |
| | | }; |
| | | |
| | | // 当前布局的尺寸 |
| | | const currentGw = ref(layoutDimensions[printLayout.value].width); |
| | | const currentGh = ref(layoutDimensions[printLayout.value].height); |
| | | const currentPrintWidth = ref(layoutDimensions[printLayout.value].width); |
| | | const currentPrintHeight = ref(layoutDimensions[printLayout.value].height); |
| | | |
| | | const selectLayout = () => { |
| | | request.post(`/glassOptimize/selectOptimizeResult/${processId}`) |
| | | .then((res) => { |
| | | if (res.code == 200) { |
| | | try { |
| | | const parsedData = JSON.parse(res.data.data[0].Layouts); |
| | | layoutData.value = parsedData; |
| | | } catch (error) { |
| | | |
| | | |
| | | } |
| | | } else { |
| | | request.post(`/glassOptimize/getOptimizeInfo/${processId.value}`) |
| | | .then((res) => { |
| | | if (res.code == 200) { |
| | | try { |
| | | layoutData.value = res.data.layouts; |
| | | optimizeUse.value=res.data.optimizeUse[0]; |
| | | // 添加控制台输出 |
| | | console.log('layoutData:', layoutData.value); |
| | | console.log('optimizeUse:', optimizeUse.value); |
| | | } catch (error) { |
| | | console.error("解析布局数据失败:", error); |
| | | } |
| | | } else { |
| | | console.error("请求失败,状态码:", res.code); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | console.error("请求失败:", error); |
| | | }); |
| | | }; |
| | | |
| | | const selectReportData= () => { |
| | | request.post(`/glassOptimize/getReportData/${processId.value}`) |
| | | .then((res) => { |
| | | if (res.code == 200) { |
| | | try { |
| | | reportData.value = res.data.reportData[0]; |
| | | console.log('reportData:', reportData.value); |
| | | } catch (error) { |
| | | console.error("解析布局数据失败:", error); |
| | | } |
| | | } else { |
| | | console.error("请求失败,状态码:", res.code); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | console.error("请求失败:", error); |
| | | }); |
| | | }; |
| | | |
| | | const fetchSettings = async (username) => { |
| | | try { |
| | | const response = await request.post(`/glassOptimize/selectOptimizeParms/${username}`); |
| | | if (response.code == 200) { |
| | | if (!response.data) { |
| | | console.error('响应数据为空'); |
| | | return; |
| | | } |
| | | const parsedData = JSON.parse(response.data); |
| | | console.log('设置内容:', parsedData); |
| | | printLayouts.value = parsedData.server.printLayouts; |
| | | printReport.value = parsedData.server.printReport; |
| | | layoutRows.value = parsedData.server.layoutRows; |
| | | layoutColumns.value = parsedData.server.layoutColumns; |
| | | glassInfoShow.value = parsedData.server.glassInfoShow; |
| | | cutInfoShow.value = parsedData.server.cutInfoShow; |
| | | } else { |
| | | console.error('请求失败,状态码:', response.code); |
| | | } |
| | | } catch (error) { |
| | | console.error('请求发生错误:', error); |
| | | } |
| | | }; |
| | | |
| | | |
| | | const selectMaterialData= () => { |
| | | request.post(`/glassOptimize/materialInfo/${processId.value}`) |
| | | .then((res) => { |
| | | if (res.code == 200) { |
| | | try { |
| | | materialList.value = res.data.materialList; |
| | | console.log('materialList:', materialList.value); |
| | | } catch (error) { |
| | | console.error("解析布局数据失败:", error); |
| | | } |
| | | } else { |
| | | console.error("请求失败,状态码:", res.code); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | console.error("请求失败:", error); |
| | | }); |
| | | }; |
| | | |
| | | const selectProductData= () => { |
| | | request.post(`/glassOptimize/getProductList/${processId.value}`) |
| | | .then((res) => { |
| | | if (res.code == 200) { |
| | | try { |
| | | productList.value = res.data.productList; |
| | | console.log('productList:', productList.value); |
| | | } catch (error) { |
| | | console.error("解析布局数据失败:", error); |
| | | } |
| | | } else { |
| | | console.error("请求失败,状态码:", res.code); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | console.error("请求失败:", error); |
| | | }); |
| | | }; |
| | | |
| | | const generateReport = async() => { |
| | | try { |
| | | // 确保有数据可以提交 |
| | | if (!processId) { |
| | | ElMessage.warning('没有可打印的数据'); |
| | | return; |
| | | } |
| | | |
| | | const response = await requestOptimize.post('/api/reports', { |
| | | fileName: processId.value, |
| | | projectNo: processId.value, |
| | | companyName : '1', |
| | | glassThickness : optimizeUse.value.thickness, |
| | | glassType : optimizeUse.value.model, |
| | | quantity : String(optimizeUse.value.processingQuantity), |
| | | printLayouts : printLayouts.value || '0', |
| | | printReport : printReport.value || '0', |
| | | layouts : layoutData.value, |
| | | reportData:{ |
| | | rectangleQuantity: reportData.value.rectangleQuantity, |
| | | otherShapeQuantity: reportData.value.otherShapeQuantity, |
| | | rectangleArea: reportData.value.rectangleArea, |
| | | otherShapeArea: reportData.value.otherShapeArea, |
| | | rectanglePerimeter: reportData.value.rectanglePerimeter, |
| | | otherShapePerimeter: reportData.value.otherShapePerimeter, |
| | | materialList: materialList.value, |
| | | productList: productList.value, |
| | | }, |
| | | layoutSet: { |
| | | layoutRows: parseInt(layoutRows.value) || 2, |
| | | layoutColumns: parseInt(layoutColumns.value) || 2, |
| | | glassInfoShow: parseInt(glassInfoShow.value) || 0, |
| | | cutInfoShow: parseInt(cutInfoShow.value) || 0 |
| | | } |
| | | |
| | | }, { |
| | | headers: { |
| | | 'Content-Type': 'application/json' |
| | | } |
| | | }); |
| | | |
| | | if (response.code == 200) { |
| | | ElMessage.success('保存成功'); |
| | | filePath.value = response.data[0]; |
| | | console.log('filePath:', filePath.value); |
| | | } else { |
| | | ElMessage.error('保存失败,请稍后再试'); |
| | | } |
| | | |
| | | } catch (error) { |
| | | console.error('保存失败:', error); |
| | | ElMessage.error('保存失败,请稍后再试'); |
| | | } |
| | | }; |
| | | |
| | | const printReports = async () => { |
| | | try { |
| | | await generateReport(); |
| | | if (!filePath.value) { |
| | | ElMessage.error('未收到有效的PDF文件路径'); |
| | | return; |
| | | } |
| | | const encodedFilePath = encodeURIComponent(filePath.value); |
| | | |
| | | const response = await request.get('/glassOptimize/reports/pdf', { |
| | | params: { filePath: encodedFilePath }, |
| | | responseType: 'blob', |
| | | headers: { |
| | | 'Accept': 'application/pdf' |
| | | } |
| | | }); |
| | | |
| | | // 检查响应数据是否存在且有效 |
| | | if (!response) { |
| | | ElMessage.error('未能获取到PDF数据'); |
| | | return; |
| | | } |
| | | |
| | | const blob = new Blob([response], { type: 'application/pdf' }); |
| | | |
| | | // 检查 blob 是否有效 |
| | | if (blob.size === 0) { |
| | | ElMessage.error('接收到空的PDF文件'); |
| | | return; |
| | | } |
| | | |
| | | const url = URL.createObjectURL(blob); |
| | | |
| | | // 创建隐藏的 iframe |
| | | const iframe = document.createElement('iframe'); |
| | | iframe.style.position = 'fixed'; |
| | | iframe.style.left = '0'; |
| | | iframe.style.top = '0'; |
| | | iframe.style.width = '0'; |
| | | iframe.style.height = '0'; |
| | | iframe.style.border = 'none'; |
| | | iframe.src = url; |
| | | |
| | | // 标记是否已经清理过资源 |
| | | let isCleanedUp = false; |
| | | // 清理资源函数 |
| | | const cleanup = () => { |
| | | if (isCleanedUp) return; |
| | | isCleanedUp = true; |
| | | URL.revokeObjectURL(url); |
| | | if (iframe.parentNode) { |
| | | iframe.parentNode.removeChild(iframe); |
| | | } |
| | | }; |
| | | |
| | | iframe.onload = () => { |
| | | setTimeout(() => { |
| | | try { |
| | | // 确保 iframe 内容已加载完成 |
| | | if (iframe.contentWindow) { |
| | | iframe.contentWindow.focus(); |
| | | |
| | | // 监听打印事件(某些浏览器支持) |
| | | const handleAfterPrint = () => { |
| | | window.removeEventListener('afterprint', handleAfterPrint); |
| | | // 延迟清理,确保打印完成 |
| | | setTimeout(cleanup, 3000); |
| | | }; |
| | | |
| | | window.addEventListener('afterprint', handleAfterPrint); |
| | | |
| | | // 执行打印 |
| | | iframe.contentWindow.print(); |
| | | ElMessage.success('报告生成成功,已启动打印...'); |
| | | |
| | | // 如果浏览器不支持 afterprint 事件,设置超时清理 |
| | | setTimeout(cleanup, 10000); |
| | | } |
| | | } catch (printError) { |
| | | console.error('打印过程中出错:', printError); |
| | | // 如果程序化打印失败,至少打开 PDF 供用户手动打印 |
| | | window.open(url, '_blank'); |
| | | ElMessage.info('已打开PDF文件,请手动打印'); |
| | | cleanup(); |
| | | } |
| | | }, 1000); // 给更多时间确保PDF完全渲染 |
| | | }; |
| | | |
| | | iframe.onerror = () => { |
| | | console.error('PDF 加载失败'); |
| | | ElMessage.error('PDF 文件加载失败,请重试'); |
| | | cleanup(); |
| | | }; |
| | | |
| | | document.body.appendChild(iframe); |
| | | |
| | | } catch (error) { |
| | | console.error('打印流程异常:', error); |
| | | |
| | | if (error.response) { |
| | | const status = error.response.status; |
| | | if (status === 400) { |
| | | ElMessage.error('文件路径无效或不是PDF'); |
| | | } else if (status === 404) { |
| | | ElMessage.error('PDF 文件不存在'); |
| | | } else { |
| | | ElMessage.error(`服务器错误 (${status}),请稍后再试`); |
| | | } |
| | | } else if (error.request) { |
| | | ElMessage.error('网络错误,请检查连接'); |
| | | } else { |
| | | ElMessage.error('打印失败,请稍后再试'); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | |
| | | |
| | | const previewReport = async () => { |
| | | try { |
| | | loading.value = true; |
| | | ElMessage.info('正在生成报告,请稍候...') |
| | | await generateReport(); |
| | | if (!filePath.value) { |
| | | ElMessage.error('未收到有效的PDF文件路径'); |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | const encodedFilePath = encodeURIComponent(filePath.value); |
| | | |
| | | // 设置PDF加载状态为true |
| | | pdfLoading.value = true; |
| | | |
| | | const response = await request.get('/glassOptimize/reports/pdf', { |
| | | params: { filePath: encodedFilePath }, |
| | | responseType: 'blob', |
| | | headers: { |
| | | 'Accept': 'application/pdf' |
| | | } |
| | | }); |
| | | |
| | | // 检查响应数据是否存在且有效 |
| | | if (!response) { |
| | | ElMessage.error('未能获取到PDF数据'); |
| | | loading.value = false; |
| | | pdfLoading.value = false; // 设置PDF加载状态为false |
| | | return; |
| | | } |
| | | |
| | | const blob = new Blob([response], { type: 'application/pdf' }); |
| | | |
| | | // 检查 blob 是否有效 |
| | | if (blob.size === 0) { |
| | | ElMessage.error('接收到空的PDF文件'); |
| | | loading.value = false; |
| | | pdfLoading.value = false; // 设置PDF加载状态为false |
| | | return; |
| | | } |
| | | |
| | | // 创建PDF URL并赋值给pdfUrl用于界面显示 |
| | | const url = URL.createObjectURL(blob); |
| | | pdfUrl.value = url; |
| | | |
| | | ElMessage.success('报告生成成功,正在预览...'); |
| | | loading.value = false; |
| | | |
| | | // 延迟一小段时间后设置PDF加载完成,确保用户能看到加载状态 |
| | | setTimeout(() => { |
| | | pdfLoading.value = false; |
| | | }, 500); |
| | | |
| | | } catch (error) { |
| | | console.error('预览流程异常:', error); |
| | | loading.value = false; |
| | | pdfLoading.value = false; // 设置PDF加载状态为false |
| | | if (error.response) { |
| | | const status = error.response.status; |
| | | if (status === 400) { |
| | | ElMessage.error('文件路径无效或不是PDF'); |
| | | } else if (status === 404) { |
| | | ElMessage.error('PDF 文件不存在'); |
| | | } else { |
| | | ElMessage.error(`服务器错误 (${status}),请稍后再试`); |
| | | } |
| | | } else if (error.request) { |
| | | ElMessage.error('网络错误,请检查连接'); |
| | | } else { |
| | | ElMessage.error('预览失败,请稍后再试'); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | const config = reactive({ |
| | | columnTypes: '两列', |
| | | rowTypes: '两行', |
| | | plain: true, |
| | | printLayouts: true, |
| | | printReport: true, |
| | | glassInfo: '显示在下侧', |
| | | cutInfo: '显示' |
| | | |
| | | }) |
| | | .catch((error) => { |
| | | console.error("请求失败:", error); |
| | | ElMessage.error(t('basicData.msg.requestFailed')); |
| | | }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | selectLayout(); |
| | | // 优先使用注入的 projectNo,其次使用 props,最后使用 localStorage |
| | | if (injectedProjectNo) { |
| | | processId.value = injectedProjectNo.value || injectedProjectNo; |
| | | } else if (props.project) { |
| | | processId.value = props.project.projectNumber || ''; |
| | | state.value = props.state; |
| | | } else if (savedProjectNo) { |
| | | processId.value = savedProjectNo; |
| | | } |
| | | |
| | | if (processId.value) { |
| | | selectLayout(); |
| | | selectReportData(); |
| | | selectMaterialData(); |
| | | selectProductData(); |
| | | fetchSettings(username); |
| | | } |
| | | |
| | | }); |
| | | |
| | | |
| | | const handlePrint = () => { |
| | | // 创建一个隐藏的iframe |
| | | const iframe = document.createElement('iframe'); |
| | | iframe.style.position = 'fixed'; |
| | | iframe.style.top = '-100vh'; |
| | | iframe.style.left = '-100vw'; |
| | | iframe.style.width = '200%'; |
| | | iframe.style.height = '200%'; |
| | | document.body.appendChild(iframe); |
| | | |
| | | // 将RectRenderer的内容加载到iframe中 |
| | | const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; |
| | | iframeDoc.open(); |
| | | iframeDoc.write(` |
| | | <html> |
| | | <head> |
| | | <title>Print Layout</title> |
| | | <style> |
| | | @page { |
| | | size: A4 landscape; |
| | | margin: 20mm; |
| | | } |
| | | body { |
| | | margin: 0; |
| | | padding: 0; |
| | | } |
| | | .layout-wrapper { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | .layout-container { |
| | | page-break-inside: avoid; |
| | | break-inside: avoid; |
| | | page-break-after: auto; |
| | | margin-bottom: 20mm; |
| | | } |
| | | .layout-item { |
| | | page-break-inside: avoid; |
| | | break-inside: avoid; |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div class="layout-wrapper"> |
| | | ${rectRenderer.value.$el.outerHTML} |
| | | </div> |
| | | </body> |
| | | </html> |
| | | `); |
| | | iframeDoc.close(); |
| | | |
| | | // 设置打印样式 |
| | | const printStyle = iframeDoc.createElement('style'); |
| | | printStyle.type = 'text/css'; |
| | | printStyle.innerHTML = ` |
| | | @page { |
| | | size: A4 landscape; |
| | | margin: 20mm; |
| | | } |
| | | body { |
| | | -webkit-print-color-adjust: exact; |
| | | } |
| | | .layout-wrapper { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | .layout-container { |
| | | page-break-inside: avoid; |
| | | break-inside: avoid; |
| | | page-break-after: auto; |
| | | margin-bottom: 20mm; |
| | | } |
| | | .layout-item { |
| | | page-break-inside: avoid; |
| | | break-inside: avoid; |
| | | } |
| | | `; |
| | | iframeDoc.head.appendChild(printStyle); |
| | | |
| | | // 调整iframe大小以适应内容 |
| | | const contentWidth = rectRenderer.value.$el.offsetWidth; |
| | | const contentHeight = rectRenderer.value.$el.offsetHeight; |
| | | iframe.width = contentWidth + 'px'; |
| | | iframe.height = contentHeight + 'px'; |
| | | |
| | | // 执行打印 |
| | | iframe.contentWindow.print(); |
| | | |
| | | // 清理 |
| | | setTimeout(() => { |
| | | document.body.removeChild(iframe); |
| | | }, 100); |
| | | if (rectRenderer.value) { |
| | | rectRenderer.value.print(); |
| | | } |
| | | }; |
| | | |
| | | const handleLayoutChange = () => { |
| | | // 更新布局尺寸 |
| | | const dimensions = layoutDimensions[printLayout.value]; |
| | | currentGw.value = dimensions.width; |
| | | currentGh.value = dimensions.height; |
| | | currentPrintWidth.value = dimensions.width; |
| | | currentPrintHeight.value = dimensions.height; |
| | | |
| | | if (rectRenderer.value) { |
| | | rectRenderer.value.updateLayout(); |
| | | } |
| | | }; |
| | | |
| | | // 监听布局变化 |
| | | watch(printLayout, (newVal) => { |
| | | handleLayoutChange(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .loading-animation { |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | |
| | | .dot-flashing { |
| | | position: relative; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 5px; |
| | | background-color: #409eff; |
| | | color: #409eff; |
| | | animation: dotFlashing 1s infinite linear alternate; |
| | | animation-delay: .5s; |
| | | } |
| | | |
| | | .dot-flashing::before, .dot-flashing::after { |
| | | content: ''; |
| | | display: inline-block; |
| | | position: absolute; |
| | | top: 0; |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 5px; |
| | | background-color: #409eff; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .dot-flashing::before { |
| | | left: -15px; |
| | | animation: dotFlashing 1s infinite alternate; |
| | | animation-delay: 0s; |
| | | } |
| | | |
| | | .dot-flashing::after { |
| | | left: 15px; |
| | | animation: dotFlashing 1s infinite alternate; |
| | | animation-delay: 1s; |
| | | } |
| | | |
| | | @keyframes dotFlashing { |
| | | 0% { |
| | | background-color: #409eff; |
| | | } |
| | | 50%, |
| | | 100% { |
| | | background-color: #c0d9f7; |
| | | } |
| | | } |
| | | </style> |