RadarChart.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. <template>
  2. <view class="radar-chart-wrap">
  3. <view
  4. class="radar-container"
  5. :style="containerStyle"
  6. >
  7. <canvas
  8. id="radarCanvas"
  9. type="2d"
  10. class="radar-canvas"
  11. ></canvas>
  12. <!-- 透明点击感应层 -->
  13. <view class="radar-tap-layer" @click="onCanvasTap"></view>
  14. </view>
  15. </view>
  16. </template>
  17. <script>
  18. /**
  19. * RadarChart - 六维雷达图 Canvas 组件
  20. *
  21. * 基于 HTML5 Canvas 2D 渲染,支持 DPR 适配、入场动画、点击事件。
  22. * 用于「心智」页面展示六维认知能力评分。
  23. *
  24. * 用法:
  25. * <RadarChart
  26. * :dimensions="dimList"
  27. * :scores="[85, 72, 68, 90, 55, 78]"
  28. * childName="小明"
  29. * @dimensionClick="onDimClick"
  30. * />
  31. */
  32. export default {
  33. props: {
  34. /** 六维标签 [{label, key}, ...],顺序:感知/专注/记忆/逻辑/空间/加工速度 */
  35. dimensions: {
  36. type: Array,
  37. default: function() {
  38. return []
  39. }
  40. },
  41. /** 六维分值 0-100,与 dimensions 顺序一一对应 */
  42. scores: {
  43. type: Array,
  44. default: function() {
  45. return []
  46. }
  47. },
  48. /** 填充颜色(可选,默认暖橙) */
  49. fillColor: {
  50. type: String,
  51. default: '#F97316'
  52. },
  53. /** 网格颜色(可选,默认浅灰) */
  54. gridColor: {
  55. type: String,
  56. default: '#E2E8F0'
  57. },
  58. /** 标签颜色(可选,默认深灰) */
  59. labelColor: {
  60. type: String,
  61. default: '#1E293B'
  62. },
  63. /** 图表标题(显示在中心) */
  64. childName: {
  65. type: String,
  66. default: ''
  67. },
  68. /** 图表宽度(rpx) */
  69. width: {
  70. type: Number,
  71. default: 600
  72. },
  73. /** 图表高度(rpx) */
  74. height: {
  75. type: Number,
  76. default: 600
  77. },
  78. /** 平均值分数 0-100,与 dimensions 顺序一一对应(可选,显示为灰色虚线) */
  79. avgScores: {
  80. type: Array,
  81. default: function() {
  82. return []
  83. }
  84. },
  85. /** 平均值填充颜色(可选,默认灰色) */
  86. avgFillColor: {
  87. type: String,
  88. default: '#CBD5E1'
  89. },
  90. /** 平均值网格颜色(可选,默认浅灰) */
  91. avgGridColor: {
  92. type: String,
  93. default: '#E2E8F0'
  94. },
  95. /** 平均值标签颜色(可选,默认浅灰) */
  96. avgLabelColor: {
  97. type: String,
  98. default: '#94A3B8'
  99. }
  100. },
  101. data: function() {
  102. return {
  103. canvasReady: false,
  104. canvasWidth: 0,
  105. canvasHeight: 0,
  106. ctx: null,
  107. dpr: 1,
  108. _canvasNode: null,
  109. _canvasLeft: 0,
  110. _canvasTop: 0,
  111. /** 动画插值中的分数 */
  112. _animScores: [],
  113. _animId: null
  114. }
  115. },
  116. computed: {
  117. /** 维度数量(灵活支持 N 维雷达图) */
  118. dimCount: function() {
  119. return this.dimensions ? this.dimensions.length : 0
  120. },
  121. /** 容器内联样式 */
  122. containerStyle: function() {
  123. return {
  124. width: this.width + 'rpx',
  125. height: this.height + 'rpx'
  126. }
  127. },
  128. /** N 边形的 N 个顶点的标准角度(弧度),从顶部顺时针 */
  129. vertexAngles: function() {
  130. var n = this.dimCount
  131. if (n < 3) return []
  132. var angles = []
  133. for (var i = 0; i < n; i++) {
  134. angles.push(-Math.PI / 2 + (i * 2 * Math.PI) / n)
  135. }
  136. return angles
  137. }
  138. },
  139. watch: {
  140. scores: {
  141. handler: function() {
  142. if (this.canvasReady) {
  143. this._startAnimation()
  144. }
  145. },
  146. deep: true
  147. },
  148. avgScores: {
  149. handler: function() {
  150. if (this.canvasReady) {
  151. this._doRender()
  152. }
  153. },
  154. deep: true
  155. }
  156. },
  157. mounted: function() {
  158. var self = this
  159. // 等 DOM 布局完成后初始化 Canvas(带重试,应对微信小程序首次渲染 Canvas 节点延迟)
  160. var retryCount = 0
  161. var maxRetry = 5
  162. function tryInit() {
  163. self.initCanvas(function(success) {
  164. if (!success && retryCount < maxRetry) {
  165. retryCount++
  166. setTimeout(tryInit, 60)
  167. }
  168. })
  169. }
  170. setTimeout(tryInit, 100)
  171. },
  172. beforeDestroy: function() {
  173. if (this._animId) {
  174. this._cancelAnim(this._animId)
  175. this._animId = null
  176. }
  177. },
  178. methods: {
  179. /* ===========================
  180. * 工具:分数归一化(null/undefined/NaN 一律按 0 处理)
  181. * 防止后端返回 null score 时 Canvas 计算 NaN 导致 moveTo/lineTo 静默失败
  182. * =========================== */
  183. _safeScore: function(v) {
  184. if (typeof v !== 'number' || isNaN(v) || v === null) return 0
  185. return v
  186. },
  187. /* ===========================
  188. * Canvas 初始化
  189. * =========================== */
  190. initCanvas: function(cb) {
  191. var self = this
  192. uni.createSelectorQuery().in(this)
  193. .select('#radarCanvas')
  194. .fields({ node: true, size: true, rect: true })
  195. .exec(function(res) {
  196. if (!res || !res[0]) { if (cb) cb(false); return }
  197. var info = res[0]
  198. if (!info.node) { if (cb) cb(false); return }
  199. var canvas = info.node
  200. var width = info.width
  201. var height = info.height
  202. if (!width || !height) { if (cb) cb(false); return }
  203. var system = uni.getWindowInfo()
  204. var pixelRatio = system.pixelRatio || 2
  205. canvas.width = width * pixelRatio
  206. canvas.height = height * pixelRatio
  207. var ctx = canvas.getContext('2d')
  208. ctx.scale(pixelRatio, pixelRatio)
  209. self.ctx = ctx
  210. self._canvasNode = canvas
  211. self.canvasWidth = width
  212. self.canvasHeight = height
  213. self.dpr = pixelRatio
  214. self._canvasLeft = info.left || 0
  215. self._canvasTop = info.top || 0
  216. self.canvasReady = true
  217. // 有分数则播放动画,否则只画网格
  218. if (self.scores && self.scores.length >= self.dimCount && self.dimCount >= 3) {
  219. self._startAnimation()
  220. } else {
  221. self._doRender()
  222. }
  223. if (cb) cb(true)
  224. })
  225. },
  226. /* ===========================
  227. * 渲染
  228. * =========================== */
  229. _doRender: function() {
  230. if (!this.ctx) { return }
  231. var ctx = this.ctx
  232. var w = this.canvasWidth
  233. var h = this.canvasHeight
  234. var cx = w / 2
  235. var cy = h / 2
  236. var outerR = Math.min(w, h) * 0.38
  237. var angles = this.vertexAngles
  238. ctx.clearRect(0, 0, w, h)
  239. var n = this.dimCount
  240. // 1. 绘制背景网格
  241. this._drawGrid(ctx, cx, cy, outerR, angles, n)
  242. // 2. 如果有平均值,先画平均值(灰色虚线)
  243. if (this.avgScores && this.avgScores.length >= n && n >= 3) {
  244. this._drawAvgScoreArea(ctx, cx, cy, outerR, angles, n)
  245. }
  246. // 3. 绘制用户分数区域
  247. this._drawScoreArea(ctx, cx, cy, outerR, angles, n)
  248. // 4. 绘制顶点标记
  249. this._drawVertices(ctx, cx, cy, outerR, angles, n)
  250. // 5. 绘制分数文字
  251. this._drawScoreText(ctx, cx, cy, outerR, angles, n)
  252. // 6. 绘制维度标签
  253. this._drawLabels(ctx, cx, cy, outerR, angles, n)
  254. // 7. 绘制中心标题
  255. this._drawCenterText(ctx, cx, cy)
  256. },
  257. /**
  258. * 绘制同心六边形网格 + 辐射线
  259. */
  260. _drawGrid: function(ctx, cx, cy, outerR, angles, n) {
  261. var levels = [0.25, 0.5, 0.75, 1.0]
  262. var gridColor = this.gridColor
  263. // 同心 N 边形
  264. ctx.strokeStyle = gridColor
  265. ctx.lineWidth = 1
  266. for (var li = 0; li < levels.length; li++) {
  267. var r = outerR * levels[li]
  268. ctx.beginPath()
  269. for (var ai2 = 0; ai2 < n; ai2++) {
  270. var vx_ = cx + r * Math.cos(angles[ai2])
  271. var vy_ = cy + r * Math.sin(angles[ai2])
  272. if (ai2 === 0) {
  273. ctx.moveTo(vx_, vy_)
  274. } else {
  275. ctx.lineTo(vx_, vy_)
  276. }
  277. }
  278. ctx.closePath()
  279. ctx.stroke()
  280. }
  281. // 辐射线(中心到各顶点)
  282. ctx.strokeStyle = gridColor
  283. ctx.lineWidth = 1
  284. ctx.beginPath()
  285. for (var ai3 = 0; ai3 < n; ai3++) {
  286. ctx.moveTo(cx, cy)
  287. ctx.lineTo(
  288. cx + outerR * Math.cos(angles[ai3]),
  289. cy + outerR * Math.sin(angles[ai3])
  290. )
  291. }
  292. ctx.stroke()
  293. },
  294. /**
  295. * 绘制分数填充多边形
  296. */
  297. _drawScoreArea: function(ctx, cx, cy, outerR, angles, n) {
  298. var scores = this._animScores
  299. // 防御:动画分数未就绪时直接用 props(避免首次渲染只有描边没有填充)
  300. if (!scores || scores.length < n) {
  301. scores = this.scores
  302. }
  303. if (!scores || scores.length < n) { return }
  304. // 构建顶点数据
  305. var points = []
  306. for (var i = 0; i < n; i++) {
  307. var score = this._safeScore(scores[i])
  308. var r = outerR * Math.max(0, Math.min(1, score / 100))
  309. points.push({
  310. x: cx + r * Math.cos(angles[i]),
  311. y: cy + r * Math.sin(angles[i])
  312. })
  313. }
  314. // 填充多边形
  315. ctx.beginPath()
  316. for (var pi = 0; pi < points.length; pi++) {
  317. if (pi === 0) {
  318. ctx.moveTo(points[pi].x, points[pi].y)
  319. } else {
  320. ctx.lineTo(points[pi].x, points[pi].y)
  321. }
  322. }
  323. ctx.closePath()
  324. // 实色半透明填充(微信 Canvas 2D 径向渐变 rgba 颜色停靠可能静默失败)
  325. var baseColor = this.fillColor || '#F97316'
  326. ctx.fillStyle = this.hexToRgba(baseColor, 0.15)
  327. ctx.fill()
  328. // 描边
  329. ctx.strokeStyle = this.fillColor || '#F97316'
  330. ctx.lineWidth = 2.5
  331. ctx.stroke()
  332. },
  333. /**
  334. * 绘制平均值填充多边形(灰色虚线)
  335. */
  336. _drawAvgScoreArea: function(ctx, cx, cy, outerR, angles, n) {
  337. var scores = this.avgScores
  338. if (!scores || scores.length < n) { return }
  339. // 构建顶点数据
  340. var points = []
  341. for (var i = 0; i < n; i++) {
  342. var score = scores[i] || 0
  343. var r = outerR * Math.max(0, Math.min(1, score / 100))
  344. points.push({
  345. x: cx + r * Math.cos(angles[i]),
  346. y: cy + r * Math.sin(angles[i])
  347. })
  348. }
  349. // 填充多边形(半透明灰色)
  350. ctx.beginPath()
  351. for (var pi = 0; pi < points.length; pi++) {
  352. if (pi === 0) {
  353. ctx.moveTo(points[pi].x, points[pi].y)
  354. } else {
  355. ctx.lineTo(points[pi].x, points[pi].y)
  356. }
  357. }
  358. ctx.closePath()
  359. var baseColor = this.avgFillColor || '#CBD5E1'
  360. ctx.fillStyle = 'rgba(203,213,225,0.15)'
  361. ctx.fill()
  362. // 描边(灰色虚线)
  363. ctx.save()
  364. ctx.setLineDash([6, 4])
  365. ctx.strokeStyle = baseColor
  366. ctx.lineWidth = 2
  367. ctx.stroke()
  368. ctx.restore()
  369. },
  370. /**
  371. * 在各顶点绘制小圆点
  372. */
  373. _drawVertices: function(ctx, cx, cy, outerR, angles, n) {
  374. var scores = this._animScores
  375. if (!scores || scores.length < n) { return }
  376. ctx.fillStyle = this.fillColor || '#F97316'
  377. ctx.shadowColor = this.hexToRgba(this.fillColor || '#F97316', 0.3)
  378. ctx.shadowBlur = 6
  379. for (var i = 0; i < n; i++) {
  380. var score = this._safeScore(scores[i])
  381. var r = outerR * Math.max(0, Math.min(1, score / 100))
  382. var vx = cx + r * Math.cos(angles[i])
  383. var vy = cy + r * Math.sin(angles[i])
  384. ctx.beginPath()
  385. ctx.arc(vx, vy, 4.5, 0, 2 * Math.PI)
  386. ctx.fill()
  387. }
  388. ctx.shadowColor = 'transparent'
  389. ctx.shadowBlur = 0
  390. },
  391. /**
  392. * 在各顶点附近显示分值(百分比)
  393. */
  394. _drawScoreText: function(ctx, cx, cy, outerR, angles, n) {
  395. var scores = this._animScores
  396. if (!scores || scores.length < n) { return }
  397. ctx.font = '11px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif'
  398. ctx.fillStyle = this.fillColor || '#F97316'
  399. ctx.textAlign = 'center'
  400. ctx.textBaseline = 'middle'
  401. for (var i = 0; i < n; i++) {
  402. var score = this._safeScore(scores[i])
  403. // 分数文字放在顶点向外偏移 16px 处
  404. var vertexR = outerR * Math.max(0, Math.min(1, score / 100))
  405. var textR = vertexR + 16
  406. var tx = cx + textR * Math.cos(angles[i])
  407. var ty = cy + textR * Math.sin(angles[i])
  408. var intScore = Math.round(score)
  409. ctx.fillText(intScore + '%', tx, ty)
  410. }
  411. },
  412. /**
  413. * 绘制维度名称标签(在六边形外围)
  414. */
  415. _drawLabels: function(ctx, cx, cy, outerR, angles, n) {
  416. var dims = this.dimensions
  417. if (!dims || dims.length < n) { return }
  418. var labelOffset = 36
  419. for (var i = 0; i < n; i++) {
  420. var dim = dims[i]
  421. if (!dim) { continue }
  422. var angle = angles[i]
  423. var labelR = outerR + labelOffset
  424. var lx = cx + labelR * Math.cos(angle)
  425. var ly = cy + labelR * Math.sin(angle)
  426. // 根据角度确定文字对齐方式
  427. var align = 'center'
  428. var baseline = 'middle'
  429. // 将角度归一化到 0 ~ 2PI
  430. var a = ((angle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI)
  431. if (a < (1 * Math.PI) / 6 || a >= (11 * Math.PI) / 6) {
  432. // 右侧(-30°~30°):上右 / 下右
  433. if (a < Math.PI / 2) {
  434. // 下右
  435. align = 'left'
  436. baseline = 'top'
  437. } else {
  438. // 上右
  439. align = 'left'
  440. baseline = 'bottom'
  441. }
  442. } else if (a < (5 * Math.PI) / 6) {
  443. // 底部(30°~150°)
  444. align = 'center'
  445. baseline = 'top'
  446. } else if (a < (7 * Math.PI) / 6) {
  447. // 左侧(150°~210°)
  448. if (a < Math.PI) {
  449. // 下左
  450. align = 'right'
  451. baseline = 'top'
  452. } else {
  453. // 上左
  454. align = 'right'
  455. baseline = 'bottom'
  456. }
  457. } else {
  458. // 顶部(210°~330°)
  459. align = 'center'
  460. baseline = 'bottom'
  461. }
  462. ctx.font = 'bold 13px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif'
  463. ctx.fillStyle = this.labelColor || '#1E293B'
  464. ctx.textAlign = align
  465. ctx.textBaseline = baseline
  466. ctx.fillText(dim.label || '', lx, ly)
  467. }
  468. },
  469. /**
  470. * 在图表中心绘制孩子名字
  471. */
  472. _drawCenterText: function(ctx, cx, cy) {
  473. var name = this.childName
  474. if (!name) { return }
  475. ctx.font = 'bold 15px -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif'
  476. ctx.fillStyle = '#F97316'
  477. ctx.textAlign = 'center'
  478. ctx.textBaseline = 'middle'
  479. ctx.fillText(name, cx, cy)
  480. },
  481. /* ===========================
  482. * 动画控制
  483. * =========================== */
  484. _startAnimation: function() {
  485. var self = this
  486. var n = this.dimCount
  487. var target = []
  488. for (var i = 0; i < n; i++) {
  489. // 关键修复:后端可能返回 null score,这里归一化避免 NaN 传播导致 Canvas 静默失败
  490. target.push(self._safeScore(this.scores[i]))
  491. }
  492. var start = []
  493. for (var si = 0; si < n; si++) { start.push(0) }
  494. var startTime = Date.now()
  495. var duration = 600
  496. if (this._animId) {
  497. this._cancelAnim(this._animId)
  498. this._animId = null
  499. }
  500. function tick() {
  501. var elapsed = Date.now() - startTime
  502. var progress = Math.min(1, elapsed / duration)
  503. // ease-out cubic
  504. var eased = 1 - Math.pow(1 - progress, 3)
  505. for (var i2 = 0; i2 < n; i2++) {
  506. self._animScores[i2] = start[i2] + (target[i2] - start[i2]) * eased
  507. }
  508. self._doRender()
  509. if (progress < 1) {
  510. self._animId = self._requestAnim(tick)
  511. } else {
  512. // 确保最终为精确值
  513. for (var j2 = 0; j2 < n; j2++) {
  514. self._animScores[j2] = target[j2]
  515. }
  516. self._doRender()
  517. self._animId = null
  518. }
  519. }
  520. self._animScores = target.slice(0)
  521. self._animId = self._requestAnim(tick)
  522. },
  523. /**
  524. * 跨环境 requestAnimationFrame
  525. */
  526. _requestAnim: function(callback) {
  527. if (this._canvasNode && typeof this._canvasNode.requestAnimationFrame === 'function') {
  528. return this._canvasNode.requestAnimationFrame(callback)
  529. }
  530. // fallback
  531. return setTimeout(callback, 16)
  532. },
  533. /**
  534. * 跨环境 cancelAnimationFrame
  535. */
  536. _cancelAnim: function(id) {
  537. if (this._canvasNode && typeof this._canvasNode.cancelAnimationFrame === 'function') {
  538. this._canvasNode.cancelAnimationFrame(id)
  539. } else {
  540. clearTimeout(id)
  541. }
  542. },
  543. /* ===========================
  544. * 点击检测
  545. * =========================== */
  546. /** 将十六进制颜色转 rgba */
  547. hexToRgba: function(hex, alpha) {
  548. if (!hex) return 'rgba(0,0,0,' + alpha + ')'
  549. var h = hex.replace('#', '')
  550. var r = parseInt(h.substring(0, 2), 16)
  551. var g = parseInt(h.substring(2, 4), 16)
  552. var b = parseInt(h.substring(4, 6), 16)
  553. return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
  554. },
  555. onCanvasTap: function(e) {
  556. if (!this.canvasReady) { return }
  557. var pageX
  558. var pageY
  559. // 从事件中获取页面坐标
  560. if (e.detail && typeof e.detail.x === 'number') {
  561. pageX = e.detail.x
  562. pageY = e.detail.y
  563. } else if (e.touches && e.touches[0]) {
  564. pageX = e.touches[0].x || e.touches[0].clientX || 0
  565. pageY = e.touches[0].y || e.touches[0].clientY || 0
  566. } else if (e.changedTouches && e.changedTouches[0]) {
  567. pageX = e.changedTouches[0].x || e.changedTouches[0].clientX || 0
  568. pageY = e.changedTouches[0].y || e.changedTouches[0].clientY || 0
  569. } else {
  570. return
  571. }
  572. // 转为 canvas 相对坐标
  573. var relX = pageX - (this._canvasLeft || 0)
  574. var relY = pageY - (this._canvasTop || 0)
  575. if (relX < 0 || relY < 0 || relX > this.canvasWidth || relY > this.canvasHeight) {
  576. return
  577. }
  578. var n = this.dimCount
  579. var cx = this.canvasWidth / 2
  580. var cy = this.canvasHeight / 2
  581. var outerR = Math.min(this.canvasWidth, this.canvasHeight) * 0.38
  582. var angles = this.vertexAngles
  583. var labelOffset = 36
  584. var hitRadius = 44
  585. // 检测点击是否落在某个维度标签附近
  586. for (var i = 0; i < n; i++) {
  587. var dim = this.dimensions[i]
  588. if (!dim) { continue }
  589. var angle = angles[i]
  590. var labelR = outerR + labelOffset
  591. var lx = cx + labelR * Math.cos(angle)
  592. var ly = cy + labelR * Math.sin(angle)
  593. var dx = relX - lx
  594. var dy = relY - ly
  595. var dist = Math.sqrt(dx * dx + dy * dy)
  596. if (dist < hitRadius) {
  597. this.$emit('dimensionClick', dim.key || '')
  598. return
  599. }
  600. }
  601. }
  602. }
  603. }
  604. </script>
  605. <style scoped>
  606. .radar-chart-wrap {
  607. margin: 20rpx 30rpx;
  608. background: #FFFFFF;
  609. border-radius: 20rpx;
  610. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
  611. overflow: hidden;
  612. display: flex;
  613. justify-content: center;
  614. align-items: center;
  615. padding: 16rpx 0;
  616. }
  617. .radar-container {
  618. position: relative;
  619. display: flex;
  620. justify-content: center;
  621. align-items: center;
  622. }
  623. .radar-canvas {
  624. display: block;
  625. width: 100%;
  626. height: 100%;
  627. }
  628. .radar-tap-layer {
  629. position: absolute;
  630. top: 0;
  631. left: 0;
  632. right: 0;
  633. bottom: 0;
  634. z-index: 2;
  635. }
  636. </style>