This commit is contained in:
parent
06fbca491f
commit
e4497785fd
|
|
@ -1,5 +1,4 @@
|
||||||
<template>
|
<template>
|
||||||
<!-- 预览文件 -->
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="title"
|
:title="title"
|
||||||
|
|
@ -9,88 +8,53 @@
|
||||||
:append-to-body="true"
|
:append-to-body="true"
|
||||||
:width="width > 500 ? '700px' : '500px'"
|
:width="width > 500 ? '700px' : '500px'"
|
||||||
>
|
>
|
||||||
<div style="text-align:center">
|
<div style="text-align: center; height: 70vh; display: flex; align-items: center; justify-content: center;">
|
||||||
<!-- 图片预览 -->
|
<!-- Loading 状态 -->
|
||||||
<template v-if="isImage">
|
<div v-if="loading" class="loading-state">
|
||||||
<div class="image-toolbar">
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> 下载</el-button> -->
|
<p>加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 -->
|
||||||
|
<div v-else-if="error" class="error-state">
|
||||||
|
<el-icon><Warning /></el-icon>
|
||||||
|
<p>文件加载失败</p>
|
||||||
|
<el-button size="small" @click="retryLoad">重试</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<template v-else-if="isImage">
|
||||||
<el-image
|
<el-image
|
||||||
:src="processedFileUrl"
|
:src="processedFileUrl"
|
||||||
:preview-src-list="previewList"
|
:preview-src-list="[processedFileUrl]"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
style="max-width:100%;max-height:70vh"
|
style="max-width: 100%; max-height: 70vh;"
|
||||||
>
|
>
|
||||||
<template #error>
|
<template #error>
|
||||||
<div class="image-slot">
|
<div class="image-error">
|
||||||
<el-icon><Picture /></el-icon>
|
<el-icon><Picture /></el-icon>
|
||||||
|
<p>图片加载失败</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-image>
|
</el-image>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- PDF 预览 -->
|
<!-- PDF 预览 -->
|
||||||
<template v-else>
|
<template v-else-if="isPdf">
|
||||||
<div class="pdf-container">
|
<div class="pdf-viewer">
|
||||||
<!-- PDF加载状态 -->
|
<iframe
|
||||||
<div v-if="pdfLoading" class="pdf-loading">
|
|
||||||
<el-icon class="is-loading"><Loading /></el-icon>
|
|
||||||
<p>PDF加载中...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PDF错误状态 -->
|
|
||||||
<div v-else-if="pdfError" class="pdf-error">
|
|
||||||
<el-icon><Warning /></el-icon>
|
|
||||||
<p>PDF加载失败</p>
|
|
||||||
<el-button size="small" @click="retryLoadPdf">重试</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PDF内容 -->
|
|
||||||
<div v-else class="pdf-content">
|
|
||||||
<!-- PDF工具栏 -->
|
|
||||||
<div class="pdf-toolbar">
|
|
||||||
<div class="pdf-controls">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:disabled="currentPage <= 1"
|
|
||||||
@click="prevPage"
|
|
||||||
>
|
|
||||||
<el-icon><ArrowLeft /></el-icon> 上一页
|
|
||||||
</el-button>
|
|
||||||
<span class="page-info">
|
|
||||||
{{ currentPage }} / {{ numPages }}
|
|
||||||
</span>
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
:disabled="currentPage >= numPages"
|
|
||||||
@click="nextPage"
|
|
||||||
>
|
|
||||||
下一页 <el-icon><ArrowRight /></el-icon>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
<div class="pdf-actions">
|
|
||||||
<!-- <el-button size="small" @click="downloadFile"><el-icon><Download /></el-icon> 下载</el-button> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- PDF显示区域 -->
|
|
||||||
<div class="pdf-viewer" @wheel="handleWheel" ref="pdfViewer">
|
|
||||||
<!-- 注意:vue-pdf 需要安装兼容 Vue3 的版本 -->
|
|
||||||
<!-- <pdf
|
|
||||||
:src="processedFileUrl"
|
:src="processedFileUrl"
|
||||||
:page="currentPage"
|
style="width: 100%; height: 100%; border: none;"
|
||||||
@num-pages="numPages = $event"
|
frameborder="0"
|
||||||
@loaded="onPdfLoaded"
|
></iframe>
|
||||||
@error="onPdfError"
|
</div>
|
||||||
style="width:100%;height:70vh"
|
</template>
|
||||||
/> -->
|
|
||||||
<div class="pdf-placeholder">
|
<!-- 不支持的类型 -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="unsupported">
|
||||||
<el-icon><Document /></el-icon>
|
<el-icon><Document /></el-icon>
|
||||||
<p>PDF预览功能</p>
|
<p>不支持的文件类型</p>
|
||||||
<p>需要安装 vue-pdf 的 Vue3 兼容版本</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -98,294 +62,115 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
Document,
|
|
||||||
Picture,
|
Picture,
|
||||||
Loading,
|
Loading,
|
||||||
Warning,
|
Warning,
|
||||||
ArrowLeft,
|
Document
|
||||||
ArrowRight,
|
|
||||||
Download
|
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
// 注意:需要安装兼容 Vue3 的 pdf 组件
|
|
||||||
// import pdf from 'vue-pdf'
|
|
||||||
import { getFileAsBase64Api } from '@/api/archivesManagement/fileManager/fileManager'
|
import { getFileAsBase64Api } from '@/api/archivesManagement/fileManager/fileManager'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
width: {
|
width: { type: [Number, String], default: 600 },
|
||||||
type: [Number, String],
|
title: { type: String, default: '文件预览' },
|
||||||
default: 600
|
rowData: { type: Object, default: () => ({}) }
|
||||||
},
|
|
||||||
hight: {
|
|
||||||
type: [Number, String],
|
|
||||||
default: 400
|
|
||||||
},
|
|
||||||
dataForm: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: '文件预览'
|
|
||||||
},
|
|
||||||
disabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
isAdd: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
rowData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
projectId: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['closeDialog', 'showColose'])
|
const emit = defineEmits(['closeDialog'])
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const dialogVisible = ref(true)
|
const dialogVisible = ref(true)
|
||||||
const fileUrl = ref('')
|
const fileUrl = ref('')
|
||||||
const fileName = ref('')
|
const fileName = ref('')
|
||||||
const previewList = ref([])
|
const loading = ref(false)
|
||||||
const pdfLoading = ref(false)
|
const error = ref(false)
|
||||||
const pdfError = ref(false)
|
|
||||||
const currentPage = ref(1)
|
|
||||||
const numPages = ref(0)
|
|
||||||
const wheelTimeout = ref(null)
|
|
||||||
const isWheelScrolling = ref(false)
|
|
||||||
const pdfViewer = ref()
|
|
||||||
|
|
||||||
// 计算属性
|
// 判断是否是图片
|
||||||
const isImage = computed(() => {
|
const isImage = computed(() => {
|
||||||
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
const exts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||||
const url = (fileUrl.value || '')
|
|
||||||
const name = (fileName.value || '').toLowerCase()
|
const name = (fileName.value || '').toLowerCase()
|
||||||
return exts.some(ext => url.endsWith(ext) || name.endsWith(ext))
|
return exts.some(ext => name.endsWith(ext))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 判断是否是 PDF
|
||||||
const isPdf = computed(() => {
|
const isPdf = computed(() => {
|
||||||
const exts = ['.pdf']
|
|
||||||
const url = (fileUrl.value || '')
|
|
||||||
const name = (fileName.value || '').toLowerCase()
|
const name = (fileName.value || '').toLowerCase()
|
||||||
return exts.some(ext => url.endsWith(ext) || name.endsWith(ext))
|
return name.endsWith('.pdf')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取图片 MIME 类型
|
||||||
|
function getImageMimeType() {
|
||||||
|
const name = (fileName.value || '').toLowerCase()
|
||||||
|
if (name.endsWith('.png')) return 'image/png'
|
||||||
|
if (name.endsWith('.gif')) return 'image/gif'
|
||||||
|
if (name.endsWith('.bmp')) return 'image/bmp'
|
||||||
|
if (name.endsWith('.webp')) return 'image/webp'
|
||||||
|
return 'image/jpeg'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理后的文件 URL(添加 data: 前缀)
|
||||||
const processedFileUrl = computed(() => {
|
const processedFileUrl = computed(() => {
|
||||||
if (fileUrl.value && !fileUrl.value.startsWith('data:')) {
|
const url = fileUrl.value
|
||||||
|
if (!url) return ''
|
||||||
|
|
||||||
|
if (url.startsWith('data:')) return url
|
||||||
|
|
||||||
if (isPdf.value) {
|
if (isPdf.value) {
|
||||||
return `data:application/pdf;base64,${fileUrl.value}`
|
return `data:application/pdf;base64,${url}`
|
||||||
} else if (isImage.value) {
|
} else if (isImage.value) {
|
||||||
const imageType = getImageMimeType()
|
const mime = getImageMimeType()
|
||||||
return `data:${imageType};base64,${fileUrl.value}`
|
return `data:${mime};base64,${url}`
|
||||||
}
|
}
|
||||||
}
|
return url
|
||||||
return fileUrl.value
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 方法
|
// 关闭弹窗
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
emit('closeDialog')
|
emit('closeDialog')
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 获取文件的base64 */
|
// 获取文件 base64
|
||||||
const getFileAsBase64 = async () => {
|
const loadFile = async () => {
|
||||||
if (isPdf.value) {
|
loading.value = true
|
||||||
pdfLoading.value = true
|
error.value = false
|
||||||
pdfError.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await getFileAsBase64Api({ id: props.rowData.fileId })
|
const res = await getFileAsBase64Api({ id: props.rowData.fileId })
|
||||||
const obj = res.data
|
const obj = res.data.data
|
||||||
fileUrl.value = obj?.fileBase64 || ''
|
|
||||||
fileName.value = obj?.fileName || ''
|
|
||||||
|
|
||||||
if (isImage.value && fileUrl.value) {
|
if (!obj?.fileBase64) {
|
||||||
previewList.value = [processedFileUrl.value]
|
throw new Error('文件数据为空')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
if (isPdf.value) {
|
fileName.value = obj.fileName || ''
|
||||||
pdfError.value = true
|
fileUrl.value = obj.fileBase64
|
||||||
pdfLoading.value = false
|
|
||||||
}
|
loading.value = false
|
||||||
console.error('获取文件失败:', error)
|
} catch (err) {
|
||||||
|
console.error('获取文件失败:', err)
|
||||||
|
error.value = true
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PDF相关方法
|
// 重试
|
||||||
const resetPdfState = () => {
|
const retryLoad = () => {
|
||||||
currentPage.value = 1
|
loadFile()
|
||||||
numPages.value = 0
|
|
||||||
pdfLoading.value = false
|
|
||||||
pdfError.value = false
|
|
||||||
isWheelScrolling.value = false
|
|
||||||
if (wheelTimeout.value) {
|
|
||||||
clearTimeout(wheelTimeout.value)
|
|
||||||
wheelTimeout.value = null
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const onPdfLoaded = () => {
|
|
||||||
pdfLoading.value = false
|
|
||||||
pdfError.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPdfError = (error) => {
|
|
||||||
pdfLoading.value = false
|
|
||||||
pdfError.value = true
|
|
||||||
console.error('PDF加载失败:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const retryLoadPdf = () => {
|
|
||||||
resetPdfState()
|
|
||||||
getFileAsBase64()
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevPage = () => {
|
|
||||||
if (currentPage.value > 1) {
|
|
||||||
currentPage.value--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextPage = () => {
|
|
||||||
if (currentPage.value < numPages.value) {
|
|
||||||
currentPage.value++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
const downloadFile = () => {
|
|
||||||
if (!fileUrl.value) return
|
|
||||||
const filename = fileName.value || '文件'
|
|
||||||
|
|
||||||
let mime = isPdf.value ? 'application/pdf' : getImageMimeType()
|
|
||||||
|
|
||||||
let base64Data = fileUrl.value
|
|
||||||
if (typeof base64Data === 'string' && base64Data.startsWith('data:')) {
|
|
||||||
try {
|
|
||||||
const parts = base64Data.split(',')
|
|
||||||
const header = parts[0]
|
|
||||||
const dataPart = parts[1]
|
|
||||||
const match = header.match(/^data:(.*?);base64$/)
|
|
||||||
if (match && match[1]) mime = match[1]
|
|
||||||
base64Data = dataPart
|
|
||||||
} catch (e) {
|
|
||||||
// 解析失败则按原样处理
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = base64ToBlob(base64Data, mime)
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
const a = document.createElement('a')
|
|
||||||
a.style.display = 'none'
|
|
||||||
a.href = url
|
|
||||||
a.download = filename
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
document.body.removeChild(a)
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// base64转Blob
|
|
||||||
const base64ToBlob = (base64, mime) => {
|
|
||||||
const byteChars = atob(base64)
|
|
||||||
const sliceSize = 1024
|
|
||||||
const byteArrays = []
|
|
||||||
for (let offset = 0; offset < byteChars.length; offset += sliceSize) {
|
|
||||||
const slice = byteChars.slice(offset, offset + sliceSize)
|
|
||||||
const byteNumbers = new Array(slice.length)
|
|
||||||
for (let i = 0; i < slice.length; i++) {
|
|
||||||
byteNumbers[i] = slice.charCodeAt(i)
|
|
||||||
}
|
|
||||||
const byteArray = new Uint8Array(byteNumbers)
|
|
||||||
byteArrays.push(byteArray)
|
|
||||||
}
|
|
||||||
return new Blob(byteArrays, { type: mime || 'application/octet-stream' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取图片MIME类型
|
|
||||||
const getImageMimeType = () => {
|
|
||||||
const name = (fileName.value || '').toLowerCase()
|
|
||||||
if (name.endsWith('.jpg') || name.endsWith('.jpeg')) {
|
|
||||||
return 'image/jpeg'
|
|
||||||
} else if (name.endsWith('.png')) {
|
|
||||||
return 'image/png'
|
|
||||||
} else if (name.endsWith('.gif')) {
|
|
||||||
return 'image/gif'
|
|
||||||
} else if (name.endsWith('.bmp')) {
|
|
||||||
return 'image/bmp'
|
|
||||||
} else if (name.endsWith('.webp')) {
|
|
||||||
return 'image/webp'
|
|
||||||
}
|
|
||||||
return 'image/jpeg'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理滚轮事件
|
|
||||||
const handleWheel = (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
if (isWheelScrolling.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isWheelScrolling.value = true
|
|
||||||
|
|
||||||
if (wheelTimeout.value) {
|
|
||||||
clearTimeout(wheelTimeout.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.deltaY > 0) {
|
|
||||||
nextPage()
|
|
||||||
} else if (event.deltaY < 0) {
|
|
||||||
prevPage()
|
|
||||||
}
|
|
||||||
|
|
||||||
wheelTimeout.value = setTimeout(() => {
|
|
||||||
isWheelScrolling.value = false
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听器
|
|
||||||
watch(isPdf, (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
resetPdfState()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 生命周期
|
// 生命周期
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getFileAsBase64()
|
loadFile()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.image-slot {
|
.loading-state,
|
||||||
width: 100%;
|
.error-state,
|
||||||
height: 240px;
|
.image-error,
|
||||||
display: flex;
|
.unsupported {
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* PDF预览样式 */
|
|
||||||
.pdf-container {
|
|
||||||
width: 100%;
|
|
||||||
height: 70vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-loading,
|
|
||||||
.pdf-error {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -395,89 +180,29 @@ onMounted(() => {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-loading .el-icon {
|
.loading-state .el-icon,
|
||||||
|
.error-state .el-icon,
|
||||||
|
.unsupported .el-icon {
|
||||||
font-size: 32px;
|
font-size: 32px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
animation: rotating 2s linear infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-error .el-icon {
|
.error-state .el-icon {
|
||||||
font-size: 32px;
|
color: #f56c6c;
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #F56C6C;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-content {
|
.unsupported .el-icon {
|
||||||
height: 100%;
|
font-size: 48px;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-bottom: 1px solid #e4e7ed;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-toolbar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 4px 0 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-info {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
min-width: 60px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer {
|
.pdf-viewer {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
overflow: hidden;
|
height: 70vh;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-placeholder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
color: #909399;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-placeholder .el-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotating {
|
@keyframes rotating {
|
||||||
0% {
|
0% { transform: rotate(0deg); }
|
||||||
transform: rotate(0deg);
|
100% { transform: rotate(360deg); }
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue