Vue如何实现PDF批注(使用pdf-lib库,附代码)
vue
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
项目地址:https://gitcode.com/gh_mirrors/vu/vue
免费下载资源
·
最终实现的功能有:自由线条和绘制矩形框框进行批注,改变线条的颜色,文字批注,插入字符串和撤回;
首先展示一下最终实现的效果:
开发前准备(还需要到github上下载字体包STSong.TTF):
npm install pdf-lib
npm install @pdf-lib/fontkit
npm install jquery
下面是全部实现代码,大体逻辑就是:
-
PDF渲染:
- 使用
mounted
生命周期钩子使用fetch
API获取PDF文档,并使用PDFDocument.load
方法加载为PDF文档对象。 - 在函数modifyPdf()中将线条数组和内容数组添加到pdf文档中,在使用createObjectURL生成iframe的src。
- 使用
-
批注功能:
- 组件允许用户添加各种类型的批注,包括自由线条、矩形和文字。
- 批注存储在
contentListAll
和lineListAll
数组中,代表所有页面上的所有批注。 - 当渲染特定页面的PDF(
modifyPdf
方法)时,当前页面的批注被过滤并存储在contentList
和lineList
数组中。 - 使用
mousemove
、mousedown
和mouseup
事件实现文本批注的拖放功能。 startDrawing
、draw
和stopDrawing
方法处理自由线条的创建。drawLineDone
方法处理矩形批注的创建。
-
文本批注编辑:
- 双击文本批注会打开一个编辑框(
activeClick
方法),允许用户修改文本。 - 当保存编辑后,会调用
submitEdit
方法。
- 双击文本批注会打开一个编辑框(
<template>
<div>
<div class="choosed-box">
<div class="choose-line">
<h3 style="margin: 0;line-height: 40px;margin-right: 10px;font-weight: 500;">线条形状:</h3>
<el-select v-model="choosedLineValue" placeholder="请选择形状" style="width: 60%;" @change="switchDrawingMode">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="choose-color">
<h3 style="margin: 0;line-height: 40px;margin-right: 10px;font-weight: 500;">线条颜色:</h3>
<el-color-picker v-model="lineColor" />
</div>
<div class="choose-color">
<h3 style="margin: 0;line-height: 40px;margin-right: 10px;font-weight: 500;">文本批注:</h3>
<el-tooltip
class="flex-center"
style="font-size: 18px; margin-left: 10px; display: flex !important;"
effect="dark"
content="双击进行文字批注"
placement="right"
>
<i class="el-icon-question" />
</el-tooltip>
</div>
<div class="writing-box">
<button class="button" @click="insertSignature">
插入教师签名
<svg fill="currentColor" viewBox="0 0 24 24" class="icon">
<path clip-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm4.28 10.28a.75.75 0 000-1.06l-3-3a.75.75 0 10-1.06 1.06l1.72 1.72H8.25a.75.75 0 000 1.5h5.69l-1.72 1.72a.75.75 0 101.06 1.06l3-3z" fill-rule="evenodd" />
</svg>
</button>
</div>
<div class="writing-box">
<button class="button" @click="insertDate">
插入日期
<svg fill="currentColor" viewBox="0 0 24 24" class="icon">
<path clip-rule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm4.28 10.28a.75.75 0 000-1.06l-3-3a.75.75 0 10-1.06 1.06l1.72 1.72H8.25a.75.75 0 000 1.5h5.69l-1.72 1.72a.75.75 0 101.06 1.06l3-3z" fill-rule="evenodd" />
</svg>
</button>
</div>
<div class="writing-box">
<button class="delete-button" @click="undoDrawing">
Back
</button>
<el-tooltip
class="flex-center"
style="font-size: 18px; margin-left: 10px; display: flex !important;"
effect="dark"
content="将根据选择的线条形状撤回"
placement="right"
>
<i class="el-icon-question" />
</el-tooltip>
</div>
</div>
<div class="view-box" :style="`width:${pdfSize.width}px;height:${pdfSize.height}px;user-select:none`">
<iframe v-show="pdfContent !== null" ref="pdfViewer" :src="`${pdfContent}#scrollbars=0&toolbar=0&statusbar=0`" width="100%" height="100%" />
<div class="board" @dblclick.stop="activeEdit($event)" @click.stop="closeEdit" @mousedown="moveDownPosition" @mousemove="movePosition" @mouseup="mouseupPosition">
<!-- 文字显示 -->
<div v-for="(d,index) in contentList" :key="index" :data-id="index" class="divbox" :style="`position:absolute;z-index:3;left:${d.left-6}px;top:${d.top-8}px;color:red`" @dblclick.stop="activeClick(d)">
<i class="el-icon-circle-close closediv" size="32" @click.stop="removediv(d,index)" />
<div v-html="d.inputValue" />
</div>
<!-- 标注框显示 -->
<svg id="svgRect" class="svgRect">
<g>
<rect v-for="(d,index) in lineList" :key="index" :width="d.width" :height="d.height" :x="d.x" :y="d.y" class="svgrect" />
</g>
</svg>
<!-- 绘制自由线条的svg -->
<svg id="svgLine" class="svgLine" @mousedown.stop="startDrawing" @mousemove.stop="draw" @mouseup.stop="stopDrawing">
<g>
<path v-for="(line, index) in freehandLines" :key="index" :d="line.pathData" :stroke="line.color" stroke-width="2" fill="none" />
</g>
</svg>
</div>
<!-- 弹出写文字的框 -->
<div v-if="inputShow" :style="inputPosition" class="inputBox">
<textarea v-model="inputContent.inputValue" :rows="10" style="width:300px;" />
<!-- <editor v-model="inputContent.inputValue" @blur="closeEdit" style='width:300px;height:200px'/> -->
<div class="sureBtn">
<el-button size="mini" @click="closeEdit">取消</el-button>
<el-button type="success" size="mini" @click="submitEdit">确定</el-button>
</div>
</div>
<!-- 换页 -->
<div style="display: flex;flex-direction: row;height: 80px;justify-content: center;align-items: center;">
<el-pagination background layout="prev, pager, next" :total="total" :current-page="page" :page-size="1" @current-change="d => (page=d) && modifyPdf(d)" />
<el-button @click="saveAs">保存</el-button>
</div>
</div>
</div>
</template>
<script>
import { PDFDocument, rgb } from 'pdf-lib'
import fontkit from '@pdf-lib/fontkit'
import $ from 'jquery'
export default {
name: 'PdfEdit',
props: {
pdfParam: {
type: String,
default: ''
},
pdfIdMsg: {
type: Object,
required: true
}
},
// components: { Editor },
data() {
return {
userName: this.$store.state.user.realname,
pdfSize: {
width: 0,
height: 0
},
lineColor: 'black',
inputPosition: {
position: 'absolute',
top: '0px',
left: '0px',
zIndex: 4
},
options: [{
value: '选项1',
label: '自由线条'
}, {
value: '选项2',
label: '矩形'
}],
choosedLineValue: '',
inputShow: false,
inputContent: {
inputValue: null,
contentsList: [],
top: 0,
left: 0,
show: 1
},
contentListAll: [],
contentList: [],
pdfUrl: '',
pdfContent: '',
total: 0,
pdfDoc: {},
page: 1,
height: 0,
node: null,
select: this.$store.state.word.select,
groupId: this.$store.state.word.select.groupId,
experienceId: this.$store.state.word.select.experienceId,
pdfTempDoc: null,
lineListAll: [],
lineList: [],
ubuntuFont: null,
testParam: '',
drawing: false,
pathData: '',
freehandLines: [] // 用于存储自由线条的数组
}
},
watch: {
lineColor: function(newColor) {
// 更新样式
const svgRects = document.querySelectorAll('.svgrect')
svgRects.forEach((rect) => {
rect.style.stroke = newColor
})
const svgPaths = document.querySelectorAll('.svgLine path')
svgPaths.forEach((path) => {
path.style.stroke = newColor
})
}
// ...其他 watch 监听
},
created() {
const select = {
experienceId: this.pdfIdMsg.eid,
wordId: this.pdfIdMsg.wid,
userId: this.pdfIdMsg.uid,
experienceName: '',
experienceScore: '',
groupName: '',
groupId: this.pdfIdMsg.gid
}
this.$store.commit('word/SET_SELECT', select)
},
mounted() {
this.pdfUrl = process.env.VUE_APP_TARGET_API + '/ljkj_experienceFile/' + this.pdfIdMsg.uid + '.' + this.pdfIdMsg.gid + '.' + this.pdfIdMsg.eid + '.pdf'
var url = this.pdfUrl + '?' + Math.random()// 去除缓存
const _this = this
async function getPdfContent() { // 加载pdf,并分页
const arrayBuffer = await fetch(url, { mode: 'cors' }).then((res) => res.arrayBuffer())
_this.pdfDoc = await PDFDocument.load(arrayBuffer)
_this.total = _this.pdfDoc.getPages().length// 总页数
_this.modifyPdf(1)// 显示第一页
// _this.drawLineDone()// 添加画线事件
}
getPdfContent()
},
methods: {
// 文字拖动事件
moveDownPosition(position) {
const index = $(position.target).parents('.divbox').data('id')
this.node = this.contentListAll[index]
},
movePosition(position) {
if (this.node) {
// eslint-disable-next-line no-empty
if ((this.node.left < 0 && position.movementX < 0) || (this.node.top < 0 && position.movementY < 0) || (this.node.left > this.pdfSize.width && position.movementX > 0) || (this.node.top > this.pdfSize.height && position.movementY > 0)) {
} else {
this.node.left = this.node.left * 1 + position.movementX * 1
this.node.top = this.node.top * 1 + position.movementY * 1
}
}
},
mouseupPosition(position) {
this.node = null
},
// 切换绘制方式时调用
switchDrawingMode() {
// 清除之前绑定的所有事件
// 根据选择的形状调用对应的绘制函数
if (this.choosedLineValue === '选项2') {
this.drawLineDone()
} else if (this.choosedLineValue === '选项1') {
$('#svgLine').off('mousedown mousemove mouseup')
}
},
getFormattedDate() {
const today = new Date()
// 获取年、月、日
const year = today.getFullYear().toString()
const month = ('0' + (today.getMonth() + 1)).slice(-2) // 月份从0开始,需要加1
const day = ('0' + today.getDate()).slice(-2)
// 组合成 "YYYY-MM-DD" 格式
const formattedDate = `${year}-${month}-${day}`
return formattedDate
},
insertDate() {
const insertDate = this.getFormattedDate()
const insertContent = {
contentsList: [],
inputValue: insertDate,
top: '20',
left: '20',
page: 1,
show: 0
}
var inputContent = JSON.parse(JSON.stringify(insertContent))
this.contentListAll.push(inputContent)
this.contentList.push(inputContent)
console.log(insertDate)
},
insertSignature() {
const insertName = this.userName
let paramLeft = this.pdfSize.width - 80
paramLeft = paramLeft.toString()
const insertContent = {
contentsList: [],
inputValue: insertName,
top: '20',
left: paramLeft,
page: 1,
show: 0
}
var inputContent = JSON.parse(JSON.stringify(insertContent))
this.contentListAll.push(inputContent)
this.contentList.push(inputContent)
},
// 添加画线矩形事件
drawLineDone() {
var _this = this
$('#svgLine').mousedown(function(position) {
const that = this
const x1 = position.offsetX
const y1 = position.offsetY
const str = {
page: _this.page,
x: x1 * 1,
y: y1 * 1,
width: 0,
height: 0
}
_this.lineList.push(str)
$(this).mousemove(function(e) {
const x = e.offsetX
const y = e.offsetY
const width = x - x1 * 1
const height = y - y1 * 1
if (width > 0) {
str.width = width
} else {
str.width = -width
str.x = x
str.y = y
}
if (height > 0) {
str.height = height
} else {
str.height = -height
str.x = x
str.y = y
}
})
$(this).mouseup(function(e) {
$(that).unbind('mousemove')
$(that).unbind('mouseup')
_this.lineListAll.push(str)
// 手动设置新矩形的颜色
const newRect = document.querySelector('.svgrect:last-child')
if (newRect) {
newRect.style.stroke = _this.lineColor
}
})
})
},
// 删除文字
removediv(d, index) {
this.$confirm('确定要删除该标记吗?', '提示', {
type: 'warning'
}).then(() => {
this.contentList.splice(index, 1)
let temp = ''
this.contentListAll.map((val, index) => {
if (val === d) {
temp = index
return
}
})
this.contentListAll.splice(temp, 1)
})
},
// 自由线条
startDrawing(event) {
if (this.choosedLineValue === '选项1') {
this.drawing = true
const { offsetX, offsetY } = event
this.pathData = `M ${offsetX} ${offsetY}`
// 新增:添加新的自由线条对象到数组
this.freehandLines.push({
pathData: `M ${offsetX} ${offsetY}`,
color: this.lineColor
})
}
},
draw(event) {
if (this.drawing && this.choosedLineValue === '选项1') {
const { offsetX, offsetY } = event
this.pathData += ` L ${offsetX} ${offsetY}`
// 新增:更新最后一个自由线条对象的 pathData
if (this.freehandLines.length > 0) {
this.freehandLines[this.freehandLines.length - 1].pathData += ` L ${offsetX} ${offsetY}`
}
}
},
stopDrawing() {
if (this.choosedLineValue === '选项1') {
this.drawing = false
}
},
undoDrawing() {
// 在这里根据选择的值进行处理
if (this.choosedLineValue === '选项1') {
// 处理选项一的情况,撤回自由线条
this.undoFreehandLines()
} else if (this.choosedLineValue === '选项2') {
// 处理选项二的情况,撤回矩形线条
this.undoRectangles()
}
},
undoFreehandLines() {
// 实现撤回自由线条的逻辑
// 你需要根据你的数据结构删除最后一条自由线条
if (this.freehandLines.length > 0) {
this.freehandLines.pop()
}
},
undoRectangles() {
// 实现撤回矩形线条的逻辑
// 你需要根据你的数据结构删除最后一条矩形线条
if (this.lineList.length > 0) {
this.lineList.pop()
}
},
// 显示pdf和各未保存的标记
async modifyPdf(p) { // p是显示第几页
this.contentList = []
this.lineList = []
this.closeEdit()
for (let i = 0; i < this.contentListAll.length; i++) { // 该页所存在的文字
if (this.contentListAll[i].page === p) {
this.contentList.push(JSON.parse(JSON.stringify(this.contentListAll[i])))
}
}
for (let i = 0; i < this.lineListAll.length; i++) { // 该页所存在的线
if (this.lineListAll[i].page === p) {
this.lineList.push(JSON.parse(JSON.stringify(this.lineListAll[i])))
}
}
const _this = this
const page = _this.pdfDoc.getPage(p * 1 - 1)
const { width, height } = page.getSize()
this.height = height
_this.pdfSize.width = `${width + 2}`
_this.pdfSize.height = `${height + 2}`
const pdfTempDoc = await PDFDocument.create()// pdf的显示
const copiedPages = await pdfTempDoc.copyPages(_this.pdfDoc, [p * 1 - 1])
pdfTempDoc.addPage(copiedPages[0])
if (window.navigator && window.navigator.msSaveOrOpenBlob) {
const blob = new Blob([await pdfTempDoc.save()], { type: 'application/pdf' })
console.log(blob)
} else {
const pdfUrl = URL.createObjectURL(
new Blob([await pdfTempDoc.save()], { type: 'application/pdf' })
)
_this.pdfContent = pdfUrl
}
},
// 在PDF-lib库中,borderColor 期望的类型是 Color,而不是简单的字符串。你可以使用PDF-lib提供的 rgb 函数来创建颜色对象。
hexToRgb(hex) {
// 去掉可能的 # 前缀
hex = hex.replace(/^#/, '')
// 解析RGB值
const bigint = parseInt(hex, 16)
const r = (bigint >> 16) & 255
const g = (bigint >> 8) & 255
const b = bigint & 255
return { r, g, b }
},
// 保存pdf
async saveAs() {
const { r, g, b } = this.hexToRgb(this.lineColor)
const paramColor = rgb(r / 255, g / 255, b / 255)
const pages = this.pdfDoc.getPages()
const url = require('@/assets/STSong.TTF')
const fontBytes = await fetch(url).then((res) => res.arrayBuffer())// 添加字体包,没有字体包不显示中文
this.pdfDoc.registerFontkit(fontkit)
this.ubuntuFont = await this.pdfDoc.embedFont(fontBytes, { subset: true })// 不加subset:true,pdf会变得很大
// 把所有的文字和线框的标记都画到pdf上去
for (let k = 0; k < this.page; k++) {
const firstPage = pages[k]
for (let i = 0; i < this.contentListAll.length; i++) {
const content = this.contentListAll[i]
if (content.page - 1 === k) {
// for (let j = 0; j < content.contentsList.length; j++) {
const text = content.inputValue
firstPage.drawText(text, {
x: content.left * 1,
y: this.height * 1 - content.top * 1,
size: 14,
font: this.ubuntuFont,
color: paramColor
})
}
// }
}
for (let i = 0; i < this.lineListAll.length; i++) {
const content = this.lineListAll[i]
if (content.page - 1 === k) {
const firstPage = pages[k]
firstPage.drawRectangle({
x: content.x * 1,
y: this.height * 1 - content.y * 1,
width: content.width * 1,
height: -content.height * 1,
borderWidth: 2,
borderColor: paramColor,
opacity: 0,
borderOpacity: 1
})
}
}
}
// 保存自由线条
for (let k = 0; k < this.page; k++) {
const firstPage = pages[k]
// 保存自由线条
for (let i = 0; i < this.freehandLines.length; i++) {
const line = this.freehandLines[i]
if (line.page - 1 === k) {
firstPage.drawSvgPath(line.pathData, {
borderColor: paramColor,
borderWidth: 2
})
}
}
}
// 把pdf转化成base64
const pdfContent = await this.pdfDoc.saveAsBase64({
dataUri: true
})
const base64Data = pdfContent.split(',')[1]
// 将Base64字符串转换为Uint8Array
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i)
}
const uint8Array = new Uint8Array(byteNumbers)
// 创建Blob对象
const blob = new Blob([uint8Array], { type: 'application/pdf' })
// 现在,'blob' 是一个Blob对象,你可以使用它进行后续操作
// 此处调接口,把base64返给后台
const pdfRawData = blob
const htmlString = ''
this.$store.dispatch('word/uploadExperiencePdf', {
htmlString,
pdfRawData
})
this.contentListAll = []
this.contentList = []
this.lineListAll = []
this.lineList = []
this.modifyPdf(this.page)
},
// 双击打开写字板
activeEdit(e) {
const { offsetX, offsetY } = e
this.inputPosition.top = `${offsetY}px`
this.inputPosition.left = `${offsetX}px`
this.inputShow = true
this.inputContent.top = `${offsetY}`
this.inputContent.left = `${offsetX}`
this.inputContent.page = this.page
this.inputContent.show = 0
},
// 保存文字
submitEdit() {
if (this.inputContent.show === 0) {
var inputContent = JSON.parse(JSON.stringify(this.inputContent))
console.log(inputContent)
this.contentListAll.push(inputContent)
this.contentList.push(inputContent)
}
this.closeEdit()
},
// 关闭写字板
closeEdit() {
this.inputContent = JSON.parse(JSON.stringify(this.inputContent))
this.inputContent.inputValue = null
this.inputShow = false
},
// 双击修改文字标记
activeClick(d) {
this.inputContent = d
this.inputPosition.left = `${d.left}px`
this.inputPosition.top = `${d.top}px`
this.inputContent.show = 1
this.inputShow = true
}
}
}
</script>
<style scoped>
.view-box {
position: relative;
border: 1px solid #ccc;
width: 100%;
height: 600px;
margin: 10px auto;
margin-bottom: 50px;
}
.pdf-input {
width: 100px;
line-height: 20px;
border: 1px solid #ccc;
background: #eee;
z-index: 3;
}
.inputBox {
background: #fff;
padding: 5px;
border-radius: 5px;
}
.sureBtn {
text-align: right;
margin-top: 10px;
}
.board {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.svgrect {
stroke: rgb(237, 10, 10);
stroke-width: 2;
position: relation;
fill-opacity: 0;
}
.svgLine {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 1;
}
.svgRect {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 1;
}
.divbox {
cursor: move;
border: 1px solid red;
z-index: 3;
padding: 5px;
white-space: nowrap;
}
.movediv {
position: absolute;
top: -8px;
left: -8px;
}
.closediv {
position: absolute;
top: -8px;
right: -8px;
cursor: pointer;
background: #f64404;
color: #fff;
border-radius: 50%;
}
.choosed-box {
width: 230px;
position: absolute;
top: 93px;
right: 0;
}
.back-box {
position: absolute;
top: 93px;
right: 150px;
}
.choose-line {
height: 40px;
display: flex;
flex-direction: row;
margin-bottom: 15px;
}
.choose-color,
.writing-box {
height: 40px;
display: flex;
flex-direction: row;
margin-bottom: 15px;
}
.button {
position: relative;
transition: all 0.3s ease-in-out;
box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
padding-block: 0.5rem;
padding-inline: 1.25rem;
background-color: #1E96E1;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #ffff;
gap: 10px;
font-weight: bold;
border: 3px solid #ffffff4d;
outline: none;
overflow: hidden;
font-size: 15px;
}
.icon {
width: 24px;
height: 24px;
transition: all 0.3s ease-in-out;
}
.button:hover {
transform: scale(1.05);
border-color: #fff9;
}
.button:hover .icon {
transform: translate(4px);
}
.button:hover::before {
animation: shine 1.5s ease-out infinite;
}
.button::before {
content: "";
position: absolute;
width: 100px;
height: 100%;
background-image: linear-gradient(
120deg,
rgba(255, 255, 255, 0) 30%,
rgba(255, 255, 255, 0.8),
rgba(255, 255, 255, 0) 70%
);
top: 0;
left: -100px;
opacity: 0.6;
}
@keyframes shine {
0% {
left: -100px;
}
60% {
left: 100%;
}
to {
left: 100%;
}
}
.delete-button {
background-color: #1E96E1;
color: #fff;
font-size: 14px;
border: 0.5px solid rgba(0, 0, 0, 0.1);
padding-bottom: 8px;
width: 60px;
height: 65px;
border-radius: 15px 15px 12px 12px;
cursor: pointer;
position: relative;
will-change: transform;
transition: all .1s ease-in-out 0s;
user-select: none;
/* Add gradient shading to each side */
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0)),
linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
background-position: bottom right, bottom right;
background-size: 100% 100%, 100% 100%;
background-repeat: no-repeat;
box-shadow: inset -4px -10px 0px rgba(255, 255, 255, 0.4),
inset -4px -8px 0px rgba(0, 0, 0, 0.3),
0px 2px 1px rgba(0, 0, 0, 0.3),
0px 2px 1px rgba(255, 255, 255, 0.1);
transform: perspective(70px) rotateX(5deg) rotateY(0deg);
}
.delete-button::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.5));
z-index: -1;
border-radius: 15px;
box-shadow: inset 4px 0px 0px rgba(255, 255, 255, 0.1),
inset 4px -8px 0px rgba(0, 0, 0, 0.3);
transition: all .1s ease-in-out 0s;
}
.delete-button::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-image: linear-gradient(to right, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0)),
linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
background-position: bottom right, bottom right;
background-size: 100% 100%, 100% 100%;
background-repeat: no-repeat;
z-index: -1;
border-radius: 15px;
transition: all .1s ease-in-out 0s;
}
.delete-button:active {
will-change: transform;
transform: perspective(80px) rotateX(5deg) rotateY(1deg) translateY(3px) scale(0.96);
height: 64px;
border: 0.25px solid rgba(0, 0, 0, 0.2);
box-shadow: inset -4px -8px 0px rgba(255, 255, 255, 0.2),
inset -4px -6px 0px rgba(0, 0, 0, 0.8),
0px 1px 0px rgba(0, 0, 0, 0.9),
0px 1px 0px rgba(255, 255, 255, 0.2);
transition: all .1s ease-in-out 0s;
}
.delete-button::after:active {
background-image: linear-gradient(to bottom,rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.2));
}
.delete-button:active::before {
content: "";
display: block;
position: absolute;
top: 5%;
left: 20%;
width: 50%;
height: 80%;
background-color: rgba(255, 255, 255, 0.1);
animation: overlay 0.1s ease-in-out 0s;
pointer-events: none;
}
@keyframes overlay {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.delete-button:focus {
outline: none;
}
</style>
GitHub 加速计划 / vu / vue
207.54 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5
* chore: fix link broken
Signed-off-by: snoppy <michaleli@foxmail.com>
* Update packages/template-compiler/README.md [skip ci]
---------
Signed-off-by: snoppy <michaleli@foxmail.com>
Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891
Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
更多推荐
已为社区贡献1条内容
所有评论(0)