纯前端处理 OFD 格式文件

06-01 1400阅读

OFD 是什么

首先我们了解一下 OFD 文件,以下来自 ChatGPT 回答:

OFD(Open Fixed Document)是一种基于中国的开放文档格式,用于表示电子文档和印刷文档。它是中国国家标准之一,主要用于政府、企业和行业之间的文档交换和存档。OFD 文件通常用于电子发票、合同、报告、审批文件等需要长期存档的文件类型

需知

来自公司需求,第一次听到 OFD 文件,常见的格式我相信大家多多少少都处理过,网上的各种处理库也不少,但由于 OFD 是国产文件,可能网上的资源较少,我找了一个还比较流行的库,但是不太符合公司的需求,所以我在库中提取了较为关键的 js 文件,然后自己实现了一遍,当然以下效果也不是我们的需求,我大概简化了一下并做了一些功能

完整代码会贴到最后,文章中只说下逻辑,样式和模板不多说啥,复制下来看一遍就懂了,我代码中写的注释超他吗清楚和详细,很多废话注释,看不惯的不看哈

之前伪造的发票文件违规了... 现在下方展示全是空白页,不会影响理解逻辑和功能

相关技术或库

Scss:用来编写基本样式,使用 Vs Code 插件 编译

Vue:用不用都行,不想使用 Vue 的自己看着代码改,我注释写的很清除

Print:Print.js 库,处理文件打印,为了简化需求,使用库,逻辑中基本没有什么打印逻辑

Ofd:从某个库提炼出的关键 js 文件,库名应该就叫 ofd.js

Gsap:用来做页面滚动,例如分页时定位到指定位置等等...

需要相关文件和插件的请私信联系我

功能点图例

这里可以点开大图看一下我标注的说明,对后面代码理解有帮助

纯前端处理 OFD 格式文件

效果 - 导航目录显示和隐藏

点击导航中的目录按钮,可切换左侧目录的显示和隐藏

纯前端处理 OFD 格式文件

效果 - 下载

点击下载按钮触发下载

纯前端处理 OFD 格式文件

效果 - 打印

点击打印按钮触发打印

纯前端处理 OFD 格式文件

效果 - 缩放

100% - 200%,js 中有配置项,想要更大或者更小的缩放比例,可以自己看着代码调试,当时做到最大 200%,是因为这个插件渲染出来的 Dom 好像最大就只能是一个固定尺寸,反正对于我的需求足够了,这里还有个防抖哈,不想要的自己移除,原因是如果渲染的 ofd 文件页面过多,会卡顿,每次改变应该都要重新渲染的,所以设置了防抖,防止频繁调整时重新渲染

纯前端处理 OFD 格式文件

效果 - 分页

点击后可以导航到指定页

纯前端处理 OFD 格式文件

效果 - 目录

显示页码和当前页高亮,可以点击目录项导航到指定页

纯前端处理 OFD 格式文件

效果 - 预览

最终预览区域,滚动到对应位置,会同步目录的高亮还有分页页码

纯前端处理 OFD 格式文件

实现逻辑

数据区,你不需要太在意这里,只是如果下面哪个地方使用到了,你能理解

data: {
    // 后端文件地址
    oldfileUrl: 'http://192.168.1.126:3000/增值税电子普通发票.ofd',
    // 文件名称
    filename: 'filename.ofd',
    // 渲染结果
    domsResult: null,
    // 渲染加载中
    renderLoading: false,
    // 当前页数索引
    pagesindex: 0,
    // 总页数
    totalpages: 1,
    // 当前缩放比例 | 1 - 2
    scaling: 1.5,
    // 左侧目录是否隐藏
    catalogueHide: false,
    // 缩放防抖计时器
    renderTimer: null,
    // 滚动时长
    scrollDuration: 500,
    // 当前预览滚动是否为控制滚动
    previewScrollControl: false,
    // 恢复预览滚动监听计时器
    previewScrollWatchTimer: null
}

1. 将后端文件地址转为 Blob,如果你是前端直接通过用户选择来获取 File 的,可以在代码中跳过这一步

ofdFileLoad() {
    // 渲染开始
    this.renderLoading = true
    // 解析 Url
    let parsedUrl = new URL(this.oldfileUrl)
    // 从路径中提取文件名
    let fileName = decodeURIComponent(parsedUrl.pathname.split('/').pop())
    // 文件读取
    fetch(this.oldfileUrl)
    
    // 成功 | 转为 Blob
    .then(response => response.blob())
    // 成功 | 调用 ofdFileBlob
    .then(blob => this.ofdFileBlob(blob, fileName))
    // 失败
    .catch(error => console.error('File fetch error:', error))
}

2. 处理为 Blob 之后,转为 File 对象,使用 ofd 库来处理,这里 ofd 库处理后,会得到一个对象,里面有一些信息,还有它转换成功后的 Dom,也就是咱们要渲染的数据

ofdFileBlob(blob, fileName) {
    // 将 Blob 转为 File 对象
    let file = new File([blob], fileName, { type: blob.type })
    // 赋值名称
    this.filename = fileName
    // 使用 Ofd 处理 File 内容
    ofd.parseOfdDocument({
        ofd: file,
        fail: error => console.log(error),
        success: result => {
            this.domsResult = result[0]
            this.renderDocument()
        }
    })
}

3. 处理完成,调用对应的渲染函数,并使用 requestAnimationFrame 来回调 Dom 加载完成,因为这个渲染的时间还是不短的,我们可以等待 Dom 渲染完成后再处理其他操作

renderDocument() {
    // 赋值目录页码
    this.totalpages = this.domsResult.pages.length
    // 调用 renderCatalogue,渲染目录
    this.renderCatalogue()
    // 调用 renderPreview,渲染目录
    this.renderPreview()
    // 等待 Dom 渲染完成
    requestAnimationFrame(() => this.renderFinish())
}

4. 接着在 renderCatalogue 和 renderPreview 里根据 ofdFileBlob 函数中获取到的 Dom 信息,分别渲染目录和预览

// 渲染目录函数
renderCatalogue() {
    // 目录
    let catalogueList = this.$refs.catalogueList
    // 目录渲染器
    let catalogueListRender = ofd.renderOfd(catalogueList.offsetWidth, this.domsResult) 
    // 遍历目录渲染器
    catalogueListRender.forEach((item, index) => {
        // 创建 Li 元素
        let li = document.createElement('LI')
        // 创建 Div 元素
        let div = document.createElement('DIV')
        // 创建 Span 元素
        let span = document.createElement('SPAN')
        // 给创建的 Li 元素添加类
        li.classList.add('item')
        // 如果这是第一个 li,添加选中
        if (index == 0) li.classList.add('pitch')
        // 给创建的 Div 元素添加类
        div.classList.add('item-ofd')
        // 添加 Item 到 div
        div.appendChild(item)
        // 给创建的 Span 元素添加类
        span.classList.add('item-page')
        // 添加页码到 Span
        span.innerHTML = index + 1
        // 将 Div 添加到 li
        li.appendChild(div)
        // 将 Span 添加到 li
        li.appendChild(span)
        // 将 Li 添加到 catalogueList
        catalogueList.appendChild(li)
        
    })
},
// 渲染预览函数
renderPreview() {
    // 预览元素
    let preview = this.$refs.preview
    // 预览渲染器,最大尺寸为 1050 ? (存疑)
    let previewRender = ofd.renderOfd(this.scaling * 525, this.domsResult)
    // 清空预览元素内容
    preview.innerHTML = ''
    // 遍历预览渲染器
    previewRender.forEach((item, index) => {
        // 创建 Div 元素
        let div = document.createElement('DIV')
        // 给创建的 Div 元素添加类
        div.classList.add('item-ofd')
        // 给创建的 Div 元素添加样式
        div.style.paddingTop = this.scaling * 40 + 'px'
        // 最后一个 Div 元素设置其他样式
        if (index + 1 == previewRender.length) div.style.paddingBottom = this.scaling * 40 + 'px'
        // 添加 Item 到 div
        div.appendChild(item)
        // 添加 item 到 displayArea
        preview.appendChild(div)
    })
}

5. 接着,我们在渲染完成的 renderFinish 函数中处理操作事件

renderFinish() {
    // 渲染完成
    this.renderLoading = false
    // 目录显示切换按钮点击
    this.$refs.catalogueButton.addEventListener('click', ev => this.catalogueHide = !this.catalogueHide)
    // 打印按钮点击
    this.$refs.printButton.addEventListener('click', ev => printJS({printable:'previewPrint',type:'html'}))
    // 下载按钮点击
    this.$refs.downloadButton.addEventListener('click', ev => this.$refs.fileDownload.click())
    // 缩小按钮点击
    this.$refs.decreaseButton.addEventListener('click', ev => this.adjustScaling('decrease'))
    // 放大按钮点击
    this.$refs.increaseButton.addEventListener('click', ev => this.adjustScaling('increase'))
    // 上页按钮点击
    this.$refs.prevButton.addEventListener('click', ev => this.switchPages(this.pagesindex - 1, 'click-button'))
    // 下页按钮点击
    this.$refs.nextButton.addEventListener('click', ev => this.switchPages(this.pagesindex + 1, 'click-button'))
    // 监听左侧目录项点击
    this.$refs.catalogueList.querySelectorAll('.item').forEach((item, index) => {
        item.addEventListener('click', event => {
            this.switchPages(index, 'click-catalogue')
        })
    })
    // 调用事件,监听预览区域的可见元素变化
    this.watchPreviewVisibleChange()
}

6. 缩放操作函数

adjustScaling(type) {
    // 缩小
    if (type == 'decrease' && this.scaling > 1) this.scaling -= 0.1
    // 放大
    if (type == 'increase' && this.scaling  {
        // 重新渲染
        this.renderPreview()
        
        // 重新监听预览区域的可见元素变化
        this.watchPreviewVisibleChange()
    }, 400)
}

7. 监听预览区域的可见元素变化,也就是同步目录高亮和分页的关键代码

watchPreviewVisibleChange() {
    // 所有元素
    let sign = this.$refs.preview.querySelectorAll('.item-ofd')
    
    // 创建 Intersection Observer 对象
    let observer = new IntersectionObserver(entries => {
        // 循环处理
        entries.forEach(entry => {
            
            // 元素是否可见
            if (entry.isIntersecting) {
                // 如果没有标记, 再触发
                if (!entry.target.hasAttribute('data-triggered')) {
                    // 获取当前可见的元素索引
                    let index = Array.from(sign).indexOf(entry.target)
                    // 向元素追加标记, 确保每次进入视口时只触发一次
                    entry.target.setAttribute('data-triggered', 'true')
                    // 触发
                    this.switchPages(index, 'scroll-preview')
                }
            }
            // 元素不可见
            else{
                // 移除元素追加的标记, 使下次进入时可以触发
                entry.target.removeAttribute('data-triggered')
            }
        })
    
    // 设置阈值
    }, { threshold: 0.5 } )
    // 循环调用
    sign.forEach(item => observer.observe(item))
}

8. 切换页码函数,例如滚动预览部分时、点击上下页按钮时,点击左侧目录时的操作

switchPages(index, type) {
    // 停止操作,当前页码不符合切换条件
    if (index  (this.totalpages - 1)) {
        return
    }
    // 赋值页码
    this.pagesindex = index
    
    // 切换目录选中
    this.changeCataloguePitch(index)
    // 触发类型是 click-button 或者 click-catalogue
    if (type == 'click-button' || type == 'click-catalogue') {
        
        // 当前预览滚动为控制滚动
        this.previewScrollControl = true
        // 清除先前的计时器,避免状态混乱
        clearTimeout(this.previewScrollWatchTimer)
        // 设置计时器,恢复 previewScrollControl 状态
        setTimeout(() => this.previewScrollControl = false, this.scrollDuration)
    }
    // 触发类型是 click-button
    if (type == 'click-button') {
        // 滚动目录
        this.scrollCatalogue(index)
        // 滚动预览
        this.scrollPreviewVw(index)
    }
    // 触发类型是 click-catalogue
    if (type == 'click-catalogue') {
        // 滚动预览
        this.scrollPreviewVw(index)
    }
    // 触发类型是 scroll-preview,并且 this.previewScrollControl 为 false
    if (type == 'scroll-preview' && !this.previewScrollControl) {
        // 滚动目录
        this.scrollCatalogue(index)
    }
}

9. 切换目录的选中函数

changeCataloguePitch(index) {
    // 目录项
    let catalogueItem = this.$refs.catalogue.querySelectorAll('.list .item')
    // 移除目录项所有的选中
    catalogueItem.forEach(item => item.classList.remove('pitch'))
    // 选中当前的目录项
    catalogueItem[index].classList.add('pitch')
}

10. 控制目录滚动 和 控制预览滚动

// 控制目录滚动
scrollCatalogue(index) {
    // 目录
    let catalogue = this.$refs.catalogue
    // 目录项
    let catalogueItem = this.$refs.catalogue.querySelectorAll('.list .item')
    // 目录位置
    let catalogueRect = catalogue.getBoundingClientRect()
    // 目录项位置
    let nowItemRect = catalogueItem[index].getBoundingClientRect()
    
    // 使用 gsap 滚动
    gsap.to(catalogue, {
        scrollTo: {
            y: nowItemRect.top - catalogueRect.top + catalogue.scrollTop,
            autoKill: true
        },
        duration: this.scrollDuration / 1000
    })
},
// 控制预览滚动
scrollPreviewVw(index) {
    // 预览
    let preview = this.$refs.preview
    // 预览项
    let previewItem = this.$refs.preview.querySelectorAll('.item-ofd')
    // 预览位置
    let previewRect = preview.getBoundingClientRect()
    // 预览项位置
    let nowItemRect = previewItem[index].getBoundingClientRect()
    
    // 使用 gsap 滚动
    gsap.to(preview, {
        scrollTo: {
            y: nowItemRect.top - previewRect.top + preview.scrollTop + 1,
            autoKill: true
        },
        duration: this.scrollDuration / 1000
    })
}

完整代码



    
    
    
    
     Analysis ofd 


    
        
        
            
                
            
             
                {{ filename }}
            
            
                
                    
                        
                    
                    
                         {{ pagesindex + 1 }} 
                         / 
                         {{ totalpages }} 
                    
                    
                        
                    
                
                
                
                
                    
                        
                    
                     
                        {{ (scaling * 100).toFixed() }} %
                    
                    
                        
                    
                
            
            
                
            
            
                
            
        
        
        
            
             
                
                    
                 
            
            
            
            
        
        
        
            
             Loading 
        
        
        
    














    new Vue({ 
        el: '.container',
        data: {
            // 后端文件地址
            oldfileUrl: 'http://192.168.1.126:3000/增值税电子普通发票.ofd',
            // 文件名称
            filename: 'filename.ofd',
            // 渲染结果
            domsResult: null,
            // 渲染加载中
            renderLoading: false,
            // 当前页数索引
            pagesindex: 0,
            // 总页数
            totalpages: 1,
            // 当前缩放比例 | 1 - 2
            scaling: 1.5,
            // 左侧目录是否隐藏
            catalogueHide: false,
            // 缩放防抖计时器
            renderTimer: null,
            // 滚动时长
            scrollDuration: 500,
            // 当前预览滚动是否为控制滚动
            previewScrollControl: false,
            // 恢复预览滚动监听计时器
            previewScrollWatchTimer: null
        },
        methods: {
            // 加载 OFD 文件
            ofdFileLoad() {
                // 渲染开始
                this.renderLoading = true
        
                // 解析 Url
                let parsedUrl = new URL(this.oldfileUrl)
                // 从路径中提取文件名
                let fileName = decodeURIComponent(parsedUrl.pathname.split('/').pop())
                // 文件读取
                fetch(this.oldfileUrl)
                
                // 成功 | 转为 Blob
                .then(response => response.blob())
                // 成功 | 调用 ofdFileBlob
                .then(blob => this.ofdFileBlob(blob, fileName))
                // 失败
                .catch(error => console.error('File fetch error:', error))
            },
            // OFD 加载成功,处理得到的 Blob
            ofdFileBlob(blob, fileName) {
                // 将 Blob 转为 File 对象
                let file = new File([blob], fileName, { type: blob.type })
                // 赋值名称
                this.filename = fileName
                // 使用 Ofd 处理 File 内容
                ofd.parseOfdDocument({
                    ofd: file,
                    fail: error => console.log(error),
                    success: result => {
                        this.domsResult = result[0]
                        this.renderDocument()
                    }
                })
            },
            // Blob 处理完成,开始渲染元素
            renderDocument() {
        
                // 赋值目录页码
                this.totalpages = this.domsResult.pages.length
                // 调用 renderCatalogue,渲染目录
                this.renderCatalogue()
                // 调用 renderPreview,渲染目录
                this.renderPreview()
                // 等待 Dom 渲染完成
                requestAnimationFrame(() => this.renderFinish())
            },
            // 渲染目录函数
            renderCatalogue() {
                // 目录
                let catalogueList = this.$refs.catalogueList
                // 目录渲染器
                let catalogueListRender = ofd.renderOfd(catalogueList.offsetWidth, this.domsResult) 
                // 遍历目录渲染器
                catalogueListRender.forEach((item, index) => {
                    // 创建 Li 元素
                    let li = document.createElement('LI')
                    // 创建 Div 元素
                    let div = document.createElement('DIV')
                    // 创建 Span 元素
                    let span = document.createElement('SPAN')
                    // 给创建的 Li 元素添加类
                    li.classList.add('item')
                    // 如果这是第一个 li,添加选中
                    if (index == 0) li.classList.add('pitch')
                    // 给创建的 Div 元素添加类
                    div.classList.add('item-ofd')
                    // 添加 Item 到 div
                    div.appendChild(item)
                    // 给创建的 Span 元素添加类
                    span.classList.add('item-page')
                    // 添加页码到 Span
                    span.innerHTML = index + 1
                    // 将 Div 添加到 li
                    li.appendChild(div)
                    // 将 Span 添加到 li
                    li.appendChild(span)
                    // 将 Li 添加到 catalogueList
                    catalogueList.appendChild(li)
                    
                })
            },
            // 渲染预览函数
            renderPreview() {
                // 预览元素
                let preview = this.$refs.preview
                // 预览渲染器,最大尺寸为 1050 ? (存疑)
                let previewRender = ofd.renderOfd(this.scaling * 525, this.domsResult)
                // 清空预览元素内容
                preview.innerHTML = ''
                // 遍历预览渲染器
                previewRender.forEach((item, index) => {
                    // 创建 Div 元素
                    let div = document.createElement('DIV')
                    // 给创建的 Div 元素添加类
                    div.classList.add('item-ofd')
                    // 给创建的 Div 元素添加样式
                    div.style.paddingTop = this.scaling * 40 + 'px'
                    // 最后一个 Div 元素设置其他样式
                    if (index + 1 == previewRender.length) div.style.paddingBottom = this.scaling * 40 + 'px'
                    // 添加 Item 到 div
                    div.appendChild(item)
                    // 添加 item 到 displayArea
                    preview.appendChild(div)
                })
            },
            // 渲染完成
            renderFinish() {
                // 渲染完成
                this.renderLoading = false
                // 目录显示切换按钮点击
                this.$refs.catalogueButton.addEventListener('click', ev => this.catalogueHide = !this.catalogueHide)
                // 打印按钮点击
                this.$refs.printButton.addEventListener('click', ev => printJS({printable:'previewPrint',type:'html'}))
                // 下载按钮点击
                this.$refs.downloadButton.addEventListener('click', ev => this.$refs.fileDownload.click())
                // 缩小按钮点击
                this.$refs.decreaseButton.addEventListener('click', ev => this.adjustScaling('decrease'))
                // 放大按钮点击
                this.$refs.increaseButton.addEventListener('click', ev => this.adjustScaling('increase'))
                // 上页按钮点击
                this.$refs.prevButton.addEventListener('click', ev => this.switchPages(this.pagesindex - 1, 'click-button'))
                // 下页按钮点击
                this.$refs.nextButton.addEventListener('click', ev => this.switchPages(this.pagesindex + 1, 'click-button'))
                // 监听左侧目录项点击
                this.$refs.catalogueList.querySelectorAll('.item').forEach((item, index) => {
                    item.addEventListener('click', event => {
                        this.switchPages(index, 'click-catalogue')
                    })
                })
                // 调用事件,监听预览区域的可见元素变化
                this.watchPreviewVisibleChange()
            },
            // 调整缩放
            adjustScaling(type) {
                // 缩小
                if (type == 'decrease' && this.scaling > 1) this.scaling -= 0.1
                // 放大
                if (type == 'increase' && this.scaling  {
                    // 重新渲染
                    this.renderPreview()
                    
                    // 重新监听预览区域的可见元素变化
                    this.watchPreviewVisibleChange()
                }, 400)
            },
            // 监听预览区域的可见元素变化
            watchPreviewVisibleChange() {
                // 所有元素
                let sign = this.$refs.preview.querySelectorAll('.item-ofd')
                
                // 创建 Intersection Observer 对象
                let observer = new IntersectionObserver(entries => {
                    // 循环处理
                    entries.forEach(entry => {
                        
                        // 元素是否可见
                        if (entry.isIntersecting) {
                            // 如果没有标记, 再触发
                            if (!entry.target.hasAttribute('data-triggered')) {
                                // 获取当前可见的元素索引
                                let index = Array.from(sign).indexOf(entry.target)
                                // 向元素追加标记, 确保每次进入视口时只触发一次
                                entry.target.setAttribute('data-triggered', 'true')
                                // 触发
                                this.switchPages(index, 'scroll-preview')
                            }
                        }
                        // 元素不可见
                        else{
                            // 移除元素追加的标记, 使下次进入时可以触发
                            entry.target.removeAttribute('data-triggered')
                        }
                    })
                
                // 设置阈值
                }, { threshold: 0.5 } )
                // 循环调用
                sign.forEach(item => observer.observe(item))
            },
            // 切换页码
            switchPages(index, type) {
                // 停止操作,当前页码不符合切换条件
                if (index  (this.totalpages - 1)) {
                    return
                }
                // 赋值页码
                this.pagesindex = index
                
                // 切换目录选中
                this.changeCataloguePitch(index)
                // 触发类型是 click-button 或者 click-catalogue
                if (type == 'click-button' || type == 'click-catalogue') {
                    
                    // 当前预览滚动为控制滚动
                    this.previewScrollControl = true
                    // 清除先前的计时器,避免状态混乱
                    clearTimeout(this.previewScrollWatchTimer)
                    // 设置计时器,恢复 previewScrollControl 状态
                    setTimeout(() => this.previewScrollControl = false, this.scrollDuration)
                }
                // 触发类型是 click-button
                if (type == 'click-button') {
                    // 滚动目录
                    this.scrollCatalogue(index)
                    // 滚动预览
                    this.scrollPreviewVw(index)
                }
                // 触发类型是 click-catalogue
                if (type == 'click-catalogue') {
                    // 滚动预览
                    this.scrollPreviewVw(index)
                }
                // 触发类型是 scroll-preview,并且 this.previewScrollControl 为 false
                if (type == 'scroll-preview' && !this.previewScrollControl) {
                    // 滚动目录
                    this.scrollCatalogue(index)
                }
            },
            // 切换目录选中
            changeCataloguePitch(index) {
                // 目录项
                let catalogueItem = this.$refs.catalogue.querySelectorAll('.list .item')
                // 移除目录项所有的选中
                catalogueItem.forEach(item => item.classList.remove('pitch'))
                // 选中当前的目录项
                catalogueItem[index].classList.add('pitch')
            },
            // 控制目录滚动
            scrollCatalogue(index) {
                // 目录
                let catalogue = this.$refs.catalogue
                // 目录项
                let catalogueItem = this.$refs.catalogue.querySelectorAll('.list .item')
                // 目录位置
                let catalogueRect = catalogue.getBoundingClientRect()
                // 目录项位置
                let nowItemRect = catalogueItem[index].getBoundingClientRect()
                
                // 使用 gsap 滚动
                gsap.to(catalogue, {
                    scrollTo: {
                        y: nowItemRect.top - catalogueRect.top + catalogue.scrollTop,
                        autoKill: true
                    },
                    duration: this.scrollDuration / 1000
                })
            },
            // 控制预览滚动
            scrollPreviewVw(index) {
                // 预览
                let preview = this.$refs.preview
                // 预览项
                let previewItem = this.$refs.preview.querySelectorAll('.item-ofd')
                // 预览位置
                let previewRect = preview.getBoundingClientRect()
                // 预览项位置
                let nowItemRect = previewItem[index].getBoundingClientRect()
                
                // 使用 gsap 滚动
                gsap.to(preview, {
                    scrollTo: {
                        y: nowItemRect.top - previewRect.top + preview.scrollTop + 1,
                        autoKill: true
                    },
                    duration: this.scrollDuration / 1000
                })
            }
        },
        mounted() {
            this.ofdFileLoad()
        }
    })

样式(scss)

*{
    margin: 0px;
    padding: 0px;
    box-sizing: border-box;
    font-family: PingFang SC;
    user-select: none;
    list-style: none;
}
::-webkit-scrollbar { 
    width: 10px; 
    height: 10px; 
}
::-webkit-scrollbar-track { 
    background: transparent; 
}
::-webkit-scrollbar-thumb { 
    background: #383d48;
    cursor: pointer; 
}
html{
    width: 100vw;
    height: 100vh;
    overflow: hidden;
}
.container{
    width: 100vw;
    height: 100vh;
    color: #FFFFFF;
    background: #0D1117;
    position: relative;
    .navigation{
        width: 100%;
        height: 60px;
        background: #23272F;
        border-bottom: 1px solid #484F58;
        display: flex;
        align-items: center;
        padding: 0px 24px;
        white-space: nowrap;
        .icons{
            width: 40px;
            height: 30px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            border-radius: 4px;
            transition: ease .3s;
        }
        .icons:hover{
            background: #383d48;
        }
        .filename{
            font-size: 16px;
            margin-left: 8px;
        }
        .printButton{
            margin-right: 14px;
        }
        .toolbar{
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            .deco{
                height: 24px;
                width: 1px;
                background: #484F58;
                margin: 0px 24px;
            }
            .chunk{
                display: flex;
                align-items: center;
                .texts{
                    background: #383d48;
                    font-size: 14px;
                    height: 30px;
                    display: flex;
                    align-items: center;
                    padding: 0px 14px;
                    margin: 0px 10px;
                    border-radius: 4px;
                }
            }
        }
    }
    .containers{
        width: 100vw;
        height: calc(100vh - 60px);
        display: flex;
        .catalogue{
            width: 240px;
            height: 100%;
            background: #23272F;
            border-right: 1px solid #484F58;
            overflow-x: hidden;
            overflow-y: auto;
            transition: ease .3s;
            .list{
                display: flex;
                align-items: center;
                flex-direction: column;
                margin: 0px 25px 35px 25px;
                .item{
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    padding-top: 35px;
                    .item-ofd{
                        cursor: pointer;
                        opacity: .5;
                        border-radius: 2px;
                        border: 5px solid transparent;
                        transition: ease .5s;
                        overflow: hidden;
                    }
                    .item-page{
                        margin-top: 10px;
                    }
                }
                .item:hover{
                    .item-ofd{
                        opacity: 0.7;
                    }
                }
                .pitch{
                    .item-ofd{
                        cursor: default;
                        border: 5px solid #9682E2;
                        opacity: 1;
                    }
                }
                .pitch:hover{
                    .item-ofd{
                        opacity: 1;
                    }
                }
            }
        }
        .catalogueHide{
            width: 0px;
        }
        .preview{
            flex: 1;
            overflow: auto;
            .item-ofd{
                display: flex;
                justify-content: center;
            }
        }
    }
    .renderLoading{
        width: 100vw;
        height: 100vh;
        position: absolute;
        top: 0px;
        left: 0px;
        background: rgba($color: #000000, $alpha: .5);
        backdrop-filter: blur(5px);
        -webkit-backdrop-filter: blur(5px);
        color: #FFFFFF;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        font-size: 16px;
        span{
            margin-top: 10px;
            font-family: Consolas, PingFang SC;
        }
    }
}
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码