当前位置:   article > 正文

Vue如何实现PDF批注(使用pdf-lib库,附代码)

pdf-lib

最终实现的功能有:自由线条和绘制矩形框框进行批注,改变线条的颜色,文字批注,插入字符串和撤回;

首先展示一下最终实现的效果:

开发前准备(还需要到github上下载字体包STSong.TTF):

  1. npm install pdf-lib
  2. npm install @pdf-lib/fontkit
  3. npm install jquery

下面是全部实现代码,大体逻辑就是:

  1. PDF渲染:

    • 使用mounted生命周期钩子使用fetch API获取PDF文档,并使用PDFDocument.load方法加载为PDF文档对象。
    • 在函数modifyPdf()中将线条数组和内容数组添加到pdf文档中,在使用createObjectURL生成iframe的src。
  2. 批注功能:

    • 组件允许用户添加各种类型的批注,包括自由线条、矩形和文字。
    • 批注存储在contentListAlllineListAll数组中,代表所有页面上的所有批注。
    • 当渲染特定页面的PDF(modifyPdf方法)时,当前页面的批注被过滤并存储在contentListlineList数组中。
    • 使用mousemovemousedownmouseup事件实现文本批注的拖放功能。
    • startDrawingdrawstopDrawing方法处理自由线条的创建。
    • drawLineDone方法处理矩形批注的创建。
  3. 文本批注编辑:

    • 双击文本批注会打开一个编辑框(activeClick方法),允许用户修改文本。
    • 当保存编辑后,会调用submitEdit方法。
  1. <template>
  2. <div>
  3. <div class="choosed-box">
  4. <div class="choose-line">
  5. <h3 style="margin: 0;line-height: 40px;margin-right: 10px;font-weight: 500;">线条形状:</h3>
  6. <el-select v-model="choosedLineValue" placeholder="请选择形状" style="width: 60%;" @change="switchDrawingMode">
  7. <el-option
  8. v-for="item in options"
  9. :key="item.value"
  10. :label="item.label"
  11. :value="item.value"
  12. />
  13. </el-select>
  14. </div>
  15. <div class="choose-color">
  16. <h3 style="margin: 0;line-height: 40px;margin-right: 10px;font-weight: 500;">线条颜色:</h3>
  17. <el-color-picker v-model="lineColor" />
  18. </div>
  19. <div class="choose-color">
  20. <h3 style="margin: 0;line-height: 40px;margin-right: 10px;font-weight: 500;">文本批注:</h3>
  21. <el-tooltip
  22. class="flex-center"
  23. style="font-size: 18px; margin-left: 10px; display: flex !important;"
  24. effect="dark"
  25. content="双击进行文字批注"
  26. placement="right"
  27. >
  28. <i class="el-icon-question" />
  29. </el-tooltip>
  30. </div>
  31. <div class="writing-box">
  32. <button class="button" @click="insertSignature">
  33. 插入教师签名
  34. <svg fill="currentColor" viewBox="0 0 24 24" class="icon">
  35. <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" />
  36. </svg>
  37. </button>
  38. </div>
  39. <div class="writing-box">
  40. <button class="button" @click="insertDate">
  41. 插入日期
  42. <svg fill="currentColor" viewBox="0 0 24 24" class="icon">
  43. <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" />
  44. </svg>
  45. </button>
  46. </div>
  47. <div class="writing-box">
  48. <button class="delete-button" @click="undoDrawing">
  49. Back
  50. </button>
  51. <el-tooltip
  52. class="flex-center"
  53. style="font-size: 18px; margin-left: 10px; display: flex !important;"
  54. effect="dark"
  55. content="将根据选择的线条形状撤回"
  56. placement="right"
  57. >
  58. <i class="el-icon-question" />
  59. </el-tooltip>
  60. </div>
  61. </div>
  62. <div class="view-box" :style="`width:${pdfSize.width}px;height:${pdfSize.height}px;user-select:none`">
  63. <iframe v-show="pdfContent !== null" ref="pdfViewer" :src="`${pdfContent}#scrollbars=0&toolbar=0&statusbar=0`" width="100%" height="100%" />
  64. <div class="board" @dblclick.stop="activeEdit($event)" @click.stop="closeEdit" @mousedown="moveDownPosition" @mousemove="movePosition" @mouseup="mouseupPosition">
  65. <!-- 文字显示 -->
  66. <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)">
  67. <i class="el-icon-circle-close closediv" size="32" @click.stop="removediv(d,index)" />
  68. <div v-html="d.inputValue" />
  69. </div>
  70. <!-- 标注框显示 -->
  71. <svg id="svgRect" class="svgRect">
  72. <g>
  73. <rect v-for="(d,index) in lineList" :key="index" :width="d.width" :height="d.height" :x="d.x" :y="d.y" class="svgrect" />
  74. </g>
  75. </svg>
  76. <!-- 绘制自由线条的svg -->
  77. <svg id="svgLine" class="svgLine" @mousedown.stop="startDrawing" @mousemove.stop="draw" @mouseup.stop="stopDrawing">
  78. <g>
  79. <path v-for="(line, index) in freehandLines" :key="index" :d="line.pathData" :stroke="line.color" stroke-width="2" fill="none" />
  80. </g>
  81. </svg>
  82. </div>
  83. <!-- 弹出写文字的框 -->
  84. <div v-if="inputShow" :style="inputPosition" class="inputBox">
  85. <textarea v-model="inputContent.inputValue" :rows="10" style="width:300px;" />
  86. <!-- <editor v-model="inputContent.inputValue" @blur="closeEdit" style='width:300px;height:200px'/> -->
  87. <div class="sureBtn">
  88. <el-button size="mini" @click="closeEdit">取消</el-button>
  89. <el-button type="success" size="mini" @click="submitEdit">确定</el-button>
  90. </div>
  91. </div>
  92. <!-- 换页 -->
  93. <div style="display: flex;flex-direction: row;height: 80px;justify-content: center;align-items: center;">
  94. <el-pagination background layout="prev, pager, next" :total="total" :current-page="page" :page-size="1" @current-change="d => (page=d) && modifyPdf(d)" />
  95. <el-button @click="saveAs">保存</el-button>
  96. </div>
  97. </div>
  98. </div>
  99. </template>
  100. <script>
  101. import { PDFDocument, rgb } from 'pdf-lib'
  102. import fontkit from '@pdf-lib/fontkit'
  103. import $ from 'jquery'
  104. export default {
  105. name: 'PdfEdit',
  106. props: {
  107. pdfParam: {
  108. type: String,
  109. default: ''
  110. },
  111. pdfIdMsg: {
  112. type: Object,
  113. required: true
  114. }
  115. },
  116. // components: { Editor },
  117. data() {
  118. return {
  119. userName: this.$store.state.user.realname,
  120. pdfSize: {
  121. width: 0,
  122. height: 0
  123. },
  124. lineColor: 'black',
  125. inputPosition: {
  126. position: 'absolute',
  127. top: '0px',
  128. left: '0px',
  129. zIndex: 4
  130. },
  131. options: [{
  132. value: '选项1',
  133. label: '自由线条'
  134. }, {
  135. value: '选项2',
  136. label: '矩形'
  137. }],
  138. choosedLineValue: '',
  139. inputShow: false,
  140. inputContent: {
  141. inputValue: null,
  142. contentsList: [],
  143. top: 0,
  144. left: 0,
  145. show: 1
  146. },
  147. contentListAll: [],
  148. contentList: [],
  149. pdfUrl: '',
  150. pdfContent: '',
  151. total: 0,
  152. pdfDoc: {},
  153. page: 1,
  154. height: 0,
  155. node: null,
  156. select: this.$store.state.word.select,
  157. groupId: this.$store.state.word.select.groupId,
  158. experienceId: this.$store.state.word.select.experienceId,
  159. pdfTempDoc: null,
  160. lineListAll: [],
  161. lineList: [],
  162. ubuntuFont: null,
  163. testParam: '',
  164. drawing: false,
  165. pathData: '',
  166. freehandLines: [] // 用于存储自由线条的数组
  167. }
  168. },
  169. watch: {
  170. lineColor: function(newColor) {
  171. // 更新样式
  172. const svgRects = document.querySelectorAll('.svgrect')
  173. svgRects.forEach((rect) => {
  174. rect.style.stroke = newColor
  175. })
  176. const svgPaths = document.querySelectorAll('.svgLine path')
  177. svgPaths.forEach((path) => {
  178. path.style.stroke = newColor
  179. })
  180. }
  181. // ...其他 watch 监听
  182. },
  183. created() {
  184. const select = {
  185. experienceId: this.pdfIdMsg.eid,
  186. wordId: this.pdfIdMsg.wid,
  187. userId: this.pdfIdMsg.uid,
  188. experienceName: '',
  189. experienceScore: '',
  190. groupName: '',
  191. groupId: this.pdfIdMsg.gid
  192. }
  193. this.$store.commit('word/SET_SELECT', select)
  194. },
  195. mounted() {
  196. this.pdfUrl = process.env.VUE_APP_TARGET_API + '/ljkj_experienceFile/' + this.pdfIdMsg.uid + '.' + this.pdfIdMsg.gid + '.' + this.pdfIdMsg.eid + '.pdf'
  197. var url = this.pdfUrl + '?' + Math.random()// 去除缓存
  198. const _this = this
  199. async function getPdfContent() { // 加载pdf,并分页
  200. const arrayBuffer = await fetch(url, { mode: 'cors' }).then((res) => res.arrayBuffer())
  201. _this.pdfDoc = await PDFDocument.load(arrayBuffer)
  202. _this.total = _this.pdfDoc.getPages().length// 总页数
  203. _this.modifyPdf(1)// 显示第一页
  204. // _this.drawLineDone()// 添加画线事件
  205. }
  206. getPdfContent()
  207. },
  208. methods: {
  209. // 文字拖动事件
  210. moveDownPosition(position) {
  211. const index = $(position.target).parents('.divbox').data('id')
  212. this.node = this.contentListAll[index]
  213. },
  214. movePosition(position) {
  215. if (this.node) {
  216. // eslint-disable-next-line no-empty
  217. 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)) {
  218. } else {
  219. this.node.left = this.node.left * 1 + position.movementX * 1
  220. this.node.top = this.node.top * 1 + position.movementY * 1
  221. }
  222. }
  223. },
  224. mouseupPosition(position) {
  225. this.node = null
  226. },
  227. // 切换绘制方式时调用
  228. switchDrawingMode() {
  229. // 清除之前绑定的所有事件
  230. // 根据选择的形状调用对应的绘制函数
  231. if (this.choosedLineValue === '选项2') {
  232. this.drawLineDone()
  233. } else if (this.choosedLineValue === '选项1') {
  234. $('#svgLine').off('mousedown mousemove mouseup')
  235. }
  236. },
  237. getFormattedDate() {
  238. const today = new Date()
  239. // 获取年、月、日
  240. const year = today.getFullYear().toString()
  241. const month = ('0' + (today.getMonth() + 1)).slice(-2) // 月份从0开始,需要加1
  242. const day = ('0' + today.getDate()).slice(-2)
  243. // 组合成 "YYYY-MM-DD" 格式
  244. const formattedDate = `${year}-${month}-${day}`
  245. return formattedDate
  246. },
  247. insertDate() {
  248. const insertDate = this.getFormattedDate()
  249. const insertContent = {
  250. contentsList: [],
  251. inputValue: insertDate,
  252. top: '20',
  253. left: '20',
  254. page: 1,
  255. show: 0
  256. }
  257. var inputContent = JSON.parse(JSON.stringify(insertContent))
  258. this.contentListAll.push(inputContent)
  259. this.contentList.push(inputContent)
  260. console.log(insertDate)
  261. },
  262. insertSignature() {
  263. const insertName = this.userName
  264. let paramLeft = this.pdfSize.width - 80
  265. paramLeft = paramLeft.toString()
  266. const insertContent = {
  267. contentsList: [],
  268. inputValue: insertName,
  269. top: '20',
  270. left: paramLeft,
  271. page: 1,
  272. show: 0
  273. }
  274. var inputContent = JSON.parse(JSON.stringify(insertContent))
  275. this.contentListAll.push(inputContent)
  276. this.contentList.push(inputContent)
  277. },
  278. // 添加画线矩形事件
  279. drawLineDone() {
  280. var _this = this
  281. $('#svgLine').mousedown(function(position) {
  282. const that = this
  283. const x1 = position.offsetX
  284. const y1 = position.offsetY
  285. const str = {
  286. page: _this.page,
  287. x: x1 * 1,
  288. y: y1 * 1,
  289. width: 0,
  290. height: 0
  291. }
  292. _this.lineList.push(str)
  293. $(this).mousemove(function(e) {
  294. const x = e.offsetX
  295. const y = e.offsetY
  296. const width = x - x1 * 1
  297. const height = y - y1 * 1
  298. if (width > 0) {
  299. str.width = width
  300. } else {
  301. str.width = -width
  302. str.x = x
  303. str.y = y
  304. }
  305. if (height > 0) {
  306. str.height = height
  307. } else {
  308. str.height = -height
  309. str.x = x
  310. str.y = y
  311. }
  312. })
  313. $(this).mouseup(function(e) {
  314. $(that).unbind('mousemove')
  315. $(that).unbind('mouseup')
  316. _this.lineListAll.push(str)
  317. // 手动设置新矩形的颜色
  318. const newRect = document.querySelector('.svgrect:last-child')
  319. if (newRect) {
  320. newRect.style.stroke = _this.lineColor
  321. }
  322. })
  323. })
  324. },
  325. // 删除文字
  326. removediv(d, index) {
  327. this.$confirm('确定要删除该标记吗?', '提示', {
  328. type: 'warning'
  329. }).then(() => {
  330. this.contentList.splice(index, 1)
  331. let temp = ''
  332. this.contentListAll.map((val, index) => {
  333. if (val === d) {
  334. temp = index
  335. return
  336. }
  337. })
  338. this.contentListAll.splice(temp, 1)
  339. })
  340. },
  341. // 自由线条
  342. startDrawing(event) {
  343. if (this.choosedLineValue === '选项1') {
  344. this.drawing = true
  345. const { offsetX, offsetY } = event
  346. this.pathData = `M ${offsetX} ${offsetY}`
  347. // 新增:添加新的自由线条对象到数组
  348. this.freehandLines.push({
  349. pathData: `M ${offsetX} ${offsetY}`,
  350. color: this.lineColor
  351. })
  352. }
  353. },
  354. draw(event) {
  355. if (this.drawing && this.choosedLineValue === '选项1') {
  356. const { offsetX, offsetY } = event
  357. this.pathData += ` L ${offsetX} ${offsetY}`
  358. // 新增:更新最后一个自由线条对象的 pathData
  359. if (this.freehandLines.length > 0) {
  360. this.freehandLines[this.freehandLines.length - 1].pathData += ` L ${offsetX} ${offsetY}`
  361. }
  362. }
  363. },
  364. stopDrawing() {
  365. if (this.choosedLineValue === '选项1') {
  366. this.drawing = false
  367. }
  368. },
  369. undoDrawing() {
  370. // 在这里根据选择的值进行处理
  371. if (this.choosedLineValue === '选项1') {
  372. // 处理选项一的情况,撤回自由线条
  373. this.undoFreehandLines()
  374. } else if (this.choosedLineValue === '选项2') {
  375. // 处理选项二的情况,撤回矩形线条
  376. this.undoRectangles()
  377. }
  378. },
  379. undoFreehandLines() {
  380. // 实现撤回自由线条的逻辑
  381. // 你需要根据你的数据结构删除最后一条自由线条
  382. if (this.freehandLines.length > 0) {
  383. this.freehandLines.pop()
  384. }
  385. },
  386. undoRectangles() {
  387. // 实现撤回矩形线条的逻辑
  388. // 你需要根据你的数据结构删除最后一条矩形线条
  389. if (this.lineList.length > 0) {
  390. this.lineList.pop()
  391. }
  392. },
  393. // 显示pdf和各未保存的标记
  394. async modifyPdf(p) { // p是显示第几页
  395. this.contentList = []
  396. this.lineList = []
  397. this.closeEdit()
  398. for (let i = 0; i < this.contentListAll.length; i++) { // 该页所存在的文字
  399. if (this.contentListAll[i].page === p) {
  400. this.contentList.push(JSON.parse(JSON.stringify(this.contentListAll[i])))
  401. }
  402. }
  403. for (let i = 0; i < this.lineListAll.length; i++) { // 该页所存在的线
  404. if (this.lineListAll[i].page === p) {
  405. this.lineList.push(JSON.parse(JSON.stringify(this.lineListAll[i])))
  406. }
  407. }
  408. const _this = this
  409. const page = _this.pdfDoc.getPage(p * 1 - 1)
  410. const { width, height } = page.getSize()
  411. this.height = height
  412. _this.pdfSize.width = `${width + 2}`
  413. _this.pdfSize.height = `${height + 2}`
  414. const pdfTempDoc = await PDFDocument.create()// pdf的显示
  415. const copiedPages = await pdfTempDoc.copyPages(_this.pdfDoc, [p * 1 - 1])
  416. pdfTempDoc.addPage(copiedPages[0])
  417. if (window.navigator && window.navigator.msSaveOrOpenBlob) {
  418. const blob = new Blob([await pdfTempDoc.save()], { type: 'application/pdf' })
  419. console.log(blob)
  420. } else {
  421. const pdfUrl = URL.createObjectURL(
  422. new Blob([await pdfTempDoc.save()], { type: 'application/pdf' })
  423. )
  424. _this.pdfContent = pdfUrl
  425. }
  426. },
  427. // 在PDF-lib库中,borderColor 期望的类型是 Color,而不是简单的字符串。你可以使用PDF-lib提供的 rgb 函数来创建颜色对象。
  428. hexToRgb(hex) {
  429. // 去掉可能的 # 前缀
  430. hex = hex.replace(/^#/, '')
  431. // 解析RGB值
  432. const bigint = parseInt(hex, 16)
  433. const r = (bigint >> 16) & 255
  434. const g = (bigint >> 8) & 255
  435. const b = bigint & 255
  436. return { r, g, b }
  437. },
  438. // 保存pdf
  439. async saveAs() {
  440. const { r, g, b } = this.hexToRgb(this.lineColor)
  441. const paramColor = rgb(r / 255, g / 255, b / 255)
  442. const pages = this.pdfDoc.getPages()
  443. const url = require('@/assets/STSong.TTF')
  444. const fontBytes = await fetch(url).then((res) => res.arrayBuffer())// 添加字体包,没有字体包不显示中文
  445. this.pdfDoc.registerFontkit(fontkit)
  446. this.ubuntuFont = await this.pdfDoc.embedFont(fontBytes, { subset: true })// 不加subset:true,pdf会变得很大
  447. // 把所有的文字和线框的标记都画到pdf上去
  448. for (let k = 0; k < this.page; k++) {
  449. const firstPage = pages[k]
  450. for (let i = 0; i < this.contentListAll.length; i++) {
  451. const content = this.contentListAll[i]
  452. if (content.page - 1 === k) {
  453. // for (let j = 0; j < content.contentsList.length; j++) {
  454. const text = content.inputValue
  455. firstPage.drawText(text, {
  456. x: content.left * 1,
  457. y: this.height * 1 - content.top * 1,
  458. size: 14,
  459. font: this.ubuntuFont,
  460. color: paramColor
  461. })
  462. }
  463. // }
  464. }
  465. for (let i = 0; i < this.lineListAll.length; i++) {
  466. const content = this.lineListAll[i]
  467. if (content.page - 1 === k) {
  468. const firstPage = pages[k]
  469. firstPage.drawRectangle({
  470. x: content.x * 1,
  471. y: this.height * 1 - content.y * 1,
  472. width: content.width * 1,
  473. height: -content.height * 1,
  474. borderWidth: 2,
  475. borderColor: paramColor,
  476. opacity: 0,
  477. borderOpacity: 1
  478. })
  479. }
  480. }
  481. }
  482. // 保存自由线条
  483. for (let k = 0; k < this.page; k++) {
  484. const firstPage = pages[k]
  485. // 保存自由线条
  486. for (let i = 0; i < this.freehandLines.length; i++) {
  487. const line = this.freehandLines[i]
  488. if (line.page - 1 === k) {
  489. firstPage.drawSvgPath(line.pathData, {
  490. borderColor: paramColor,
  491. borderWidth: 2
  492. })
  493. }
  494. }
  495. }
  496. // 把pdf转化成base64
  497. const pdfContent = await this.pdfDoc.saveAsBase64({
  498. dataUri: true
  499. })
  500. const base64Data = pdfContent.split(',')[1]
  501. // 将Base64字符串转换为Uint8Array
  502. const byteCharacters = atob(base64Data)
  503. const byteNumbers = new Array(byteCharacters.length)
  504. for (let i = 0; i < byteCharacters.length; i++) {
  505. byteNumbers[i] = byteCharacters.charCodeAt(i)
  506. }
  507. const uint8Array = new Uint8Array(byteNumbers)
  508. // 创建Blob对象
  509. const blob = new Blob([uint8Array], { type: 'application/pdf' })
  510. // 现在,'blob' 是一个Blob对象,你可以使用它进行后续操作
  511. // 此处调接口,把base64返给后台
  512. const pdfRawData = blob
  513. const htmlString = ''
  514. this.$store.dispatch('word/uploadExperiencePdf', {
  515. htmlString,
  516. pdfRawData
  517. })
  518. this.contentListAll = []
  519. this.contentList = []
  520. this.lineListAll = []
  521. this.lineList = []
  522. this.modifyPdf(this.page)
  523. },
  524. // 双击打开写字板
  525. activeEdit(e) {
  526. const { offsetX, offsetY } = e
  527. this.inputPosition.top = `${offsetY}px`
  528. this.inputPosition.left = `${offsetX}px`
  529. this.inputShow = true
  530. this.inputContent.top = `${offsetY}`
  531. this.inputContent.left = `${offsetX}`
  532. this.inputContent.page = this.page
  533. this.inputContent.show = 0
  534. },
  535. // 保存文字
  536. submitEdit() {
  537. if (this.inputContent.show === 0) {
  538. var inputContent = JSON.parse(JSON.stringify(this.inputContent))
  539. console.log(inputContent)
  540. this.contentListAll.push(inputContent)
  541. this.contentList.push(inputContent)
  542. }
  543. this.closeEdit()
  544. },
  545. // 关闭写字板
  546. closeEdit() {
  547. this.inputContent = JSON.parse(JSON.stringify(this.inputContent))
  548. this.inputContent.inputValue = null
  549. this.inputShow = false
  550. },
  551. // 双击修改文字标记
  552. activeClick(d) {
  553. this.inputContent = d
  554. this.inputPosition.left = `${d.left}px`
  555. this.inputPosition.top = `${d.top}px`
  556. this.inputContent.show = 1
  557. this.inputShow = true
  558. }
  559. }
  560. }
  561. </script>
  562. <style scoped>
  563. .view-box {
  564. position: relative;
  565. border: 1px solid #ccc;
  566. width: 100%;
  567. height: 600px;
  568. margin: 10px auto;
  569. margin-bottom: 50px;
  570. }
  571. .pdf-input {
  572. width: 100px;
  573. line-height: 20px;
  574. border: 1px solid #ccc;
  575. background: #eee;
  576. z-index: 3;
  577. }
  578. .inputBox {
  579. background: #fff;
  580. padding: 5px;
  581. border-radius: 5px;
  582. }
  583. .sureBtn {
  584. text-align: right;
  585. margin-top: 10px;
  586. }
  587. .board {
  588. position: absolute;
  589. top: 0;
  590. left: 0;
  591. width: 100%;
  592. height: 100%;
  593. cursor: pointer;
  594. }
  595. .svgrect {
  596. stroke: rgb(237, 10, 10);
  597. stroke-width: 2;
  598. position: relation;
  599. fill-opacity: 0;
  600. }
  601. .svgLine {
  602. position: absolute;
  603. top: 0;
  604. left: 0;
  605. width: 100%;
  606. height: 100%;
  607. cursor: pointer;
  608. z-index: 1;
  609. }
  610. .svgRect {
  611. position: absolute;
  612. top: 0;
  613. left: 0;
  614. width: 100%;
  615. height: 100%;
  616. cursor: pointer;
  617. z-index: 1;
  618. }
  619. .divbox {
  620. cursor: move;
  621. border: 1px solid red;
  622. z-index: 3;
  623. padding: 5px;
  624. white-space: nowrap;
  625. }
  626. .movediv {
  627. position: absolute;
  628. top: -8px;
  629. left: -8px;
  630. }
  631. .closediv {
  632. position: absolute;
  633. top: -8px;
  634. right: -8px;
  635. cursor: pointer;
  636. background: #f64404;
  637. color: #fff;
  638. border-radius: 50%;
  639. }
  640. .choosed-box {
  641. width: 230px;
  642. position: absolute;
  643. top: 93px;
  644. right: 0;
  645. }
  646. .back-box {
  647. position: absolute;
  648. top: 93px;
  649. right: 150px;
  650. }
  651. .choose-line {
  652. height: 40px;
  653. display: flex;
  654. flex-direction: row;
  655. margin-bottom: 15px;
  656. }
  657. .choose-color,
  658. .writing-box {
  659. height: 40px;
  660. display: flex;
  661. flex-direction: row;
  662. margin-bottom: 15px;
  663. }
  664. .button {
  665. position: relative;
  666. transition: all 0.3s ease-in-out;
  667. box-shadow: 0px 10px 20px rgba(0, 0, 0, 0.2);
  668. padding-block: 0.5rem;
  669. padding-inline: 1.25rem;
  670. background-color: #1E96E1;
  671. border-radius: 9999px;
  672. display: flex;
  673. align-items: center;
  674. justify-content: center;
  675. color: #ffff;
  676. gap: 10px;
  677. font-weight: bold;
  678. border: 3px solid #ffffff4d;
  679. outline: none;
  680. overflow: hidden;
  681. font-size: 15px;
  682. }
  683. .icon {
  684. width: 24px;
  685. height: 24px;
  686. transition: all 0.3s ease-in-out;
  687. }
  688. .button:hover {
  689. transform: scale(1.05);
  690. border-color: #fff9;
  691. }
  692. .button:hover .icon {
  693. transform: translate(4px);
  694. }
  695. .button:hover::before {
  696. animation: shine 1.5s ease-out infinite;
  697. }
  698. .button::before {
  699. content: "";
  700. position: absolute;
  701. width: 100px;
  702. height: 100%;
  703. background-image: linear-gradient(
  704. 120deg,
  705. rgba(255, 255, 255, 0) 30%,
  706. rgba(255, 255, 255, 0.8),
  707. rgba(255, 255, 255, 0) 70%
  708. );
  709. top: 0;
  710. left: -100px;
  711. opacity: 0.6;
  712. }
  713. @keyframes shine {
  714. 0% {
  715. left: -100px;
  716. }
  717. 60% {
  718. left: 100%;
  719. }
  720. to {
  721. left: 100%;
  722. }
  723. }
  724. .delete-button {
  725. background-color: #1E96E1;
  726. color: #fff;
  727. font-size: 14px;
  728. border: 0.5px solid rgba(0, 0, 0, 0.1);
  729. padding-bottom: 8px;
  730. width: 60px;
  731. height: 65px;
  732. border-radius: 15px 15px 12px 12px;
  733. cursor: pointer;
  734. position: relative;
  735. will-change: transform;
  736. transition: all .1s ease-in-out 0s;
  737. user-select: none;
  738. /* Add gradient shading to each side */
  739. background-image: linear-gradient(to right, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0)),
  740. linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
  741. background-position: bottom right, bottom right;
  742. background-size: 100% 100%, 100% 100%;
  743. background-repeat: no-repeat;
  744. box-shadow: inset -4px -10px 0px rgba(255, 255, 255, 0.4),
  745. inset -4px -8px 0px rgba(0, 0, 0, 0.3),
  746. 0px 2px 1px rgba(0, 0, 0, 0.3),
  747. 0px 2px 1px rgba(255, 255, 255, 0.1);
  748. transform: perspective(70px) rotateX(5deg) rotateY(0deg);
  749. }
  750. .delete-button::after {
  751. content: '';
  752. position: absolute;
  753. top: 0;
  754. bottom: 0;
  755. left: 0;
  756. right: 0;
  757. background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), rgba(0, 0, 0, 0.5));
  758. z-index: -1;
  759. border-radius: 15px;
  760. box-shadow: inset 4px 0px 0px rgba(255, 255, 255, 0.1),
  761. inset 4px -8px 0px rgba(0, 0, 0, 0.3);
  762. transition: all .1s ease-in-out 0s;
  763. }
  764. .delete-button::before {
  765. content: '';
  766. position: absolute;
  767. top: 0;
  768. bottom: 0;
  769. left: 0;
  770. right: 0;
  771. background-image: linear-gradient(to right, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0)),
  772. linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0));
  773. background-position: bottom right, bottom right;
  774. background-size: 100% 100%, 100% 100%;
  775. background-repeat: no-repeat;
  776. z-index: -1;
  777. border-radius: 15px;
  778. transition: all .1s ease-in-out 0s;
  779. }
  780. .delete-button:active {
  781. will-change: transform;
  782. transform: perspective(80px) rotateX(5deg) rotateY(1deg) translateY(3px) scale(0.96);
  783. height: 64px;
  784. border: 0.25px solid rgba(0, 0, 0, 0.2);
  785. box-shadow: inset -4px -8px 0px rgba(255, 255, 255, 0.2),
  786. inset -4px -6px 0px rgba(0, 0, 0, 0.8),
  787. 0px 1px 0px rgba(0, 0, 0, 0.9),
  788. 0px 1px 0px rgba(255, 255, 255, 0.2);
  789. transition: all .1s ease-in-out 0s;
  790. }
  791. .delete-button::after:active {
  792. background-image: linear-gradient(to bottom,rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.2));
  793. }
  794. .delete-button:active::before {
  795. content: "";
  796. display: block;
  797. position: absolute;
  798. top: 5%;
  799. left: 20%;
  800. width: 50%;
  801. height: 80%;
  802. background-color: rgba(255, 255, 255, 0.1);
  803. animation: overlay 0.1s ease-in-out 0s;
  804. pointer-events: none;
  805. }
  806. @keyframes overlay {
  807. from {
  808. opacity: 0;
  809. }
  810. to {
  811. opacity: 1;
  812. }
  813. }
  814. .delete-button:focus {
  815. outline: none;
  816. }
  817. </style>

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Guff_9hys/article/detail/903208
推荐阅读
相关标签
  

闽ICP备14008679号