OctopusDiagram.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. <template>
  2. <!-- 章鱼图 - 关键人拓展图谱 -->
  3. <view class="octopus-wrapper" v-if="effects && effects.list && effects.list.length > 0" :style="{ height: canvasHeight + 'px' }">
  4. <canvas
  5. class="octopus-canvas"
  6. canvas-id="octopusCanvas"
  7. id="octopusCanvas"
  8. type="2d"
  9. :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
  10. @touchstart="onTouchStart"
  11. @touchmove="onTouchMove"
  12. @touchend="onTouchEnd" />
  13. <!-- 成效节点点击弹窗(显示关键人列表) -->
  14. <view class="effect-popup" v-if="selectedEffect" :style="effectPopupPosition">
  15. <view class="popup-content">
  16. <view class="popup-header">
  17. <text class="popup-name">{{ selectedEffect.name }}</text>
  18. <text class="popup-tags">
  19. <text class="popup-tag" v-for="t in selectedEffect.effectTypeList" :key="t">{{ t }}</text>
  20. </text>
  21. </view>
  22. <view class="popup-keypersons" v-if="keyPersons[selectedEffect.id] && keyPersons[selectedEffect.id].length > 0">
  23. <text class="popup-title">关键人</text>
  24. <text class="kp-item" v-for="kp in keyPersons[selectedEffect.id]" :key="kp.id">
  25. {{ kp.name }} · {{ kp.title || kp.organization || '' }}
  26. </text>
  27. </view>
  28. <view class="popup-empty" v-else>
  29. <text class="empty-text">暂无关键人,点击添加</text>
  30. </view>
  31. <view class="popup-actions">
  32. <text class="action-btn action-btn-primary" @tap="onAddKeyPerson(selectedEffect)">添加关键人</text>
  33. </view>
  34. </view>
  35. </view>
  36. <!-- 重新分析按钮 -->
  37. <view class="reanalyze-btn" @tap="$emit('reanalyze')">
  38. <text>🔄 重新分析</text>
  39. </view>
  40. </view>
  41. <!-- 空状态 -->
  42. <view class="octopus-empty" v-else-if="effects && effects.list && effects.list.length === 0">
  43. <text class="empty-icon">🧭</text>
  44. <text class="empty-text">暂无成效记录</text>
  45. <text class="empty-hint">记录你的成交成效,画出章鱼图</text>
  46. <text class="action-btn action-btn-primary" @tap="$emit('add-effect')">+ 记录成效</text>
  47. </view>
  48. <!-- 加载状态 -->
  49. <view class="octopus-loading" v-else>
  50. <text class="loading-text">加载章鱼图...</text>
  51. </view>
  52. </template>
  53. <script>
  54. /**
  55. * OctopusDiagram - 章鱼图(关键人拓展)
  56. *
  57. * 三层结构:
  58. * 1. 中心节点:"我"(当前用户)
  59. * 2. 第1层:成效记录节点(标签:金额大/频次高/新成交)
  60. * 3. 第2层:拓展方向节点(虚线,≤8个)
  61. */
  62. export default {
  63. name: 'OctopusDiagram',
  64. props: {
  65. selfId: { type: [Number, String], default: null },
  66. effects: { type: Object, default: null },
  67. expansions: { type: Array, default: function() { return [] } },
  68. canvasWidth: { type: Number, default: 340 },
  69. canvasHeight: { type: Number, default: 420 },
  70. interactive: { type: Boolean, default: true }
  71. },
  72. data: function() {
  73. return {
  74. dpr: 1,
  75. ctx: null,
  76. _canvasNode: null,
  77. _canvasRect: null,
  78. canvasReady: false,
  79. _destroyed: false,
  80. // 节点数据
  81. effectNodes: [],
  82. expansionNodes: [],
  83. centerNode: null,
  84. // 选中状态
  85. selectedEffect: null,
  86. _touchStartPos: null,
  87. // 关键人缓存(effectId -> [{...}])
  88. keyPersons: {},
  89. // 颜色
  90. effectColors: { '金额大': '#F97316', '频次高': '#F59E0B', '新成交': '#10B981' },
  91. expansionColor: '#6366F1'
  92. }
  93. },
  94. computed: {
  95. effectPopupPosition: function() {
  96. if (!this.selectedEffect) return { display: 'none' }
  97. var px = this.selectedEffect.x
  98. var py = this.selectedEffect.y
  99. var halfPopup = 120
  100. var left = Math.max(halfPopup, Math.min(this.canvasWidth - halfPopup, px))
  101. return { left: left + 'px', top: (py - 20) + 'px' }
  102. }
  103. },
  104. watch: {
  105. effects: {
  106. handler: function() {
  107. this.selectedEffect = null
  108. this.buildLayout()
  109. if (this.canvasReady) this.drawAll()
  110. },
  111. deep: true
  112. },
  113. expansions: {
  114. handler: function() {
  115. this.buildExpansionNodes()
  116. if (this.canvasReady) this.drawAll()
  117. },
  118. deep: true
  119. }
  120. },
  121. mounted: function() {
  122. var self = this
  123. this._destroyed = false
  124. try {
  125. this.dpr = uni.getWindowInfo().pixelRatio || 1
  126. } catch (e) {
  127. this.dpr = 1
  128. }
  129. var retryCount = 0
  130. var maxRetry = 5
  131. function tryInit() {
  132. if (self._destroyed) return
  133. self.initCanvas(function(success) {
  134. if (self._destroyed) return
  135. if (!success && retryCount < maxRetry) {
  136. retryCount++
  137. setTimeout(tryInit, 60)
  138. }
  139. })
  140. }
  141. this.$nextTick(function() {
  142. if (!self._destroyed) tryInit()
  143. })
  144. },
  145. beforeDestroy: function() {
  146. this._destroyed = true
  147. },
  148. methods: {
  149. buildLayout: function() {
  150. this.effectNodes = []
  151. this.expansionNodes = []
  152. if (!this.effects || !this.effects.list) return
  153. var cx = this.canvasWidth / 2
  154. var cy = this.canvasHeight / 2
  155. var minDim = Math.min(this.canvasWidth, this.canvasHeight)
  156. var effects = this.effects.list
  157. var n = effects.length
  158. // 成效节点均匀分布在上方半圆
  159. for (var i = 0; i < n; i++) {
  160. var angle = -Math.PI / 2 - (Math.PI / (n + 1)) * (i + 1)
  161. var r = minDim * 0.30
  162. this.effectNodes.push({
  163. id: effects[i].id,
  164. name: '成交 #' + effects[i].id,
  165. displayName: 'E' + (i + 1),
  166. x: cx + r * Math.cos(angle),
  167. y: cy + r * Math.sin(angle),
  168. radius: 26,
  169. effectType: effects[i].effectType,
  170. effectTypeList: effects[i].effectTypeList || [],
  171. effectAmount: effects[i].effectAmount,
  172. color: this._getTypeColor(effects[i].effectType)
  173. })
  174. }
  175. },
  176. buildExpansionNodes: function() {
  177. if (!this.expansions || this.expansions.length === 0) {
  178. this.expansionNodes = []
  179. return
  180. }
  181. var cx = this.canvasWidth / 2
  182. var cy = this.canvasHeight / 2
  183. var minDim = Math.min(this.canvasWidth, this.canvasHeight)
  184. var n = this.expansions.length
  185. var baseRadius = minDim * 0.38
  186. for (var i = 0; i < n; i++) {
  187. var angle = -Math.PI / 2 - (Math.PI / (n + 1)) * (i + 1)
  188. var r = baseRadius + 40
  189. this.expansionNodes.push({
  190. id: 'exp_' + i,
  191. name: this.expansions[i].keyword,
  192. displayName: this.expansions[i].keyword.substring(0, 3),
  193. x: cx + r * Math.cos(angle),
  194. y: cy + r * Math.sin(angle),
  195. radius: 20,
  196. dimension: this.expansions[i].dimension,
  197. isExpansion: true
  198. })
  199. }
  200. },
  201. drawAll: function() {
  202. if (!this.ctx) return
  203. var ctx = this.ctx
  204. var w = this.canvasWidth
  205. var h = this.canvasHeight
  206. ctx.clearRect(0, 0, w, h)
  207. this._drawCenterNode(ctx)
  208. this._drawEdges(ctx)
  209. for (var i = 0; i < this.effectNodes.length; i++) {
  210. this._drawEffectNode(ctx, this.effectNodes[i])
  211. }
  212. for (var j = 0; j < this.expansionNodes.length; j++) {
  213. this._drawExpansionNode(ctx, this.expansionNodes[j])
  214. }
  215. this._drawLabels(ctx)
  216. },
  217. _drawCenterNode: function(ctx) {
  218. var cx = this.canvasWidth / 2
  219. var cy = this.canvasHeight / 2
  220. var r = 30
  221. ctx.beginPath()
  222. ctx.arc(cx, cy, r + 4, 0, Math.PI * 2)
  223. ctx.fillStyle = this._hexToRgba('#10B981', 0.15)
  224. ctx.fill()
  225. ctx.beginPath()
  226. ctx.arc(cx, cy, r, 0, Math.PI * 2)
  227. ctx.fillStyle = '#10B981'
  228. ctx.fill()
  229. ctx.strokeStyle = '#FFFFFF'
  230. ctx.lineWidth = 3
  231. ctx.stroke()
  232. ctx.font = 'bold 18px PingFang SC, sans-serif'
  233. ctx.fillStyle = '#FFFFFF'
  234. ctx.textAlign = 'center'
  235. ctx.textBaseline = 'middle'
  236. ctx.fillText('我', cx, cy + 1)
  237. },
  238. _drawEffectNode: function(ctx, node) {
  239. var x = node.x, y = node.y, r = node.radius
  240. if (x - r > this.canvasWidth || x + r < 0 || y - r > this.canvasHeight || y + r < 0) return
  241. // 光晕
  242. ctx.beginPath()
  243. ctx.arc(x, y, r + 3, 0, Math.PI * 2)
  244. ctx.fillStyle = this._hexToRgba(node.color, 0.12)
  245. ctx.fill()
  246. // 主体
  247. ctx.beginPath()
  248. ctx.arc(x, y, r, 0, Math.PI * 2)
  249. ctx.fillStyle = node.color
  250. ctx.fill()
  251. ctx.strokeStyle = '#FFFFFF'
  252. ctx.lineWidth = 2
  253. ctx.stroke()
  254. // 首字符
  255. ctx.font = 'bold ' + Math.max(14, r * 0.7) + 'px PingFang SC, sans-serif'
  256. ctx.fillStyle = '#FFFFFF'
  257. ctx.textAlign = 'center'
  258. ctx.textBaseline = 'middle'
  259. ctx.fillText(node.displayName, x, y + 1)
  260. },
  261. _drawExpansionNode: function(ctx, node) {
  262. var x = node.x, y = node.y, r = node.radius
  263. if (x - r > this.canvasWidth || x + r < 0 || y - r > this.canvasHeight || y + r < 0) return
  264. // 虚线圆
  265. ctx.beginPath()
  266. ctx.arc(x, y, r, 0, Math.PI * 2)
  267. ctx.strokeStyle = this.expansionColor
  268. ctx.lineWidth = 2
  269. ctx.setLineDash([4, 4])
  270. ctx.stroke()
  271. ctx.setLineDash([])
  272. // 填充
  273. ctx.beginPath()
  274. ctx.arc(x, y, r - 1, 0, Math.PI * 2)
  275. ctx.fillStyle = this._hexToRgba(this.expansionColor, 0.15)
  276. ctx.fill()
  277. // 文字
  278. ctx.font = '11px PingFang SC, sans-serif'
  279. ctx.fillStyle = this.expansionColor
  280. ctx.textAlign = 'center'
  281. ctx.textBaseline = 'middle'
  282. ctx.fillText(node.displayName, x, y + 1)
  283. },
  284. _drawEdges: function(ctx) {
  285. var cx = this.canvasWidth / 2
  286. var cy = this.canvasHeight / 2
  287. for (var i = 0; i < this.effectNodes.length; i++) {
  288. var node = this.effectNodes[i]
  289. ctx.beginPath()
  290. ctx.moveTo(cx, cy)
  291. ctx.lineTo(node.x, node.y)
  292. ctx.strokeStyle = this._hexToRgba(node.color, 0.3)
  293. ctx.lineWidth = 1.5
  294. ctx.stroke()
  295. }
  296. // 成效节点到拓展方向节点的边(虚线)
  297. for (var i = 0; i < this.effectNodes.length && i < this.expansionNodes.length; i++) {
  298. var e1 = this.effectNodes[i]
  299. var e2 = this.expansionNodes[i]
  300. ctx.beginPath()
  301. ctx.moveTo(e1.x, e1.y)
  302. ctx.lineTo(e2.x, e2.y)
  303. ctx.strokeStyle = this._hexToRgba(this.expansionColor, 0.2)
  304. ctx.lineWidth = 1
  305. ctx.setLineDash([3, 3])
  306. ctx.stroke()
  307. ctx.setLineDash([])
  308. }
  309. },
  310. _drawLabels: function(ctx) {
  311. var cx = this.canvasWidth / 2
  312. var cy = this.canvasHeight / 2
  313. // 成效标签(每侧显示)
  314. for (var i = 0; i < this.effectNodes.length; i++) {
  315. var node = this.effectNodes[i]
  316. var dx = node.x - cx
  317. var dy = node.y - cy
  318. var dist = Math.sqrt(dx * dx + dy * dy) || 1
  319. var lx = node.x + (dx / dist) * 36
  320. var ly = node.y + (dy / dist) * 20
  321. ctx.font = '11px PingFang SC, sans-serif'
  322. ctx.fillStyle = '#64748B'
  323. ctx.textAlign = 'center'
  324. ctx.textBaseline = 'middle'
  325. var label = ''
  326. if (node.effectTypeList && node.effectTypeList.length > 0) {
  327. label = node.effectTypeList.slice(0, 2).join('/')
  328. }
  329. if (label) ctx.fillText(label, lx, ly)
  330. }
  331. },
  332. onTouchStart: function(e) {
  333. if (!this.interactive) return
  334. var touches = e.touches || e.changedTouches
  335. if (!touches || touches.length === 0) return
  336. var touch = touches[0]
  337. var rect = this._getCanvasRect()
  338. if (!rect) return
  339. var x = touch.clientX - rect.left
  340. var y = touch.clientY - rect.top
  341. this._touchStartPos = { x: x, y: y }
  342. var hit = this.hitTest(x, y)
  343. if (!hit) {
  344. this.selectedEffect = null
  345. }
  346. },
  347. onTouchMove: function(e) {
  348. if (!this._touchStartPos) return
  349. var touches = e.touches || e.changedTouches
  350. if (!touches || touches.length === 0) return
  351. var touch = touches[0]
  352. var rect = this._getCanvasRect()
  353. if (!rect) return
  354. var x = touch.clientX - rect.left
  355. var y = touch.clientY - rect.top
  356. var dx = x - this._touchStartPos.x
  357. var dy = y - this._touchStartPos.y
  358. if (dx * dx + dy * dy > 100) {
  359. this._touchStartPos = null
  360. }
  361. },
  362. onTouchEnd: function(e) {
  363. if (!this._touchStartPos) return
  364. var touches = e.changedTouches
  365. if (!touches || touches.length === 0) {
  366. this._touchStartPos = null
  367. return
  368. }
  369. var touch = touches[0]
  370. var rect = this._getCanvasRect()
  371. if (!rect) { this._touchStartPos = null; return }
  372. var x = touch.clientX - rect.left
  373. var y = touch.clientY - rect.top
  374. var dx = x - this._touchStartPos.x
  375. var dy = y - this._touchStartPos.y
  376. this._touchStartPos = null
  377. if (dx * dx + dy * dy > 100) return
  378. var hit = this.hitTest(x, y)
  379. if (hit) {
  380. this.selectedEffect = hit
  381. } else {
  382. this.selectedEffect = null
  383. }
  384. },
  385. hitTest: function(x, y) {
  386. // 先检查成效节点
  387. for (var i = 0; i < this.effectNodes.length; i++) {
  388. var n = this.effectNodes[i]
  389. var dx = x - n.x
  390. var dy = y - n.y
  391. if (dx * dx + dy * dy <= n.radius * n.radius) return n
  392. }
  393. // 再检查拓展方向节点
  394. for (var j = 0; j < this.expansionNodes.length; j++) {
  395. var n = this.expansionNodes[j]
  396. var dx = x - n.x
  397. var dy = y - n.y
  398. if (dx * dx + dy * dy <= n.radius * n.radius) {
  399. // 拓展节点 → 触发 expansion-click 事件
  400. this.$emit('expansion-click', n)
  401. return n
  402. }
  403. }
  404. return null
  405. },
  406. onAddKeyPerson: function(effect) {
  407. this.$emit('effect-click', effect)
  408. // 如果有关键人列表则显示,否则触发 add-key-person
  409. if (!this.keyPersons[effect.id] || this.keyPersons[effect.id].length === 0) {
  410. this.$emit('add-key-person')
  411. }
  412. },
  413. initCanvas: function(cb) {
  414. var self = this
  415. uni.createSelectorQuery().in(this)
  416. .select('#octopusCanvas')
  417. .fields({ node: true, size: true, rect: true })
  418. .exec(function(res) {
  419. try {
  420. if (self._destroyed) return
  421. if (!res || !res[0]) { if (cb) cb(false); return }
  422. var info = res[0]
  423. if (!info.node) { if (cb) cb(false); return }
  424. var canvas = info.node
  425. var width = info.width
  426. var height = info.height
  427. if (!width || !height) {
  428. var winWidth = 375
  429. try {
  430. var wi = uni.getWindowInfo()
  431. winWidth = wi.windowWidth || 375
  432. } catch (e) {}
  433. width = Math.round((self.canvasWidth || 340) * winWidth / 750)
  434. height = Math.round((self.canvasHeight || 420) * winWidth / 750)
  435. }
  436. var pixelRatio = 2
  437. try {
  438. var sys = uni.getWindowInfo()
  439. pixelRatio = sys.pixelRatio || 2
  440. } catch (e) {}
  441. canvas.width = width * pixelRatio
  442. canvas.height = height * pixelRatio
  443. var ctx = canvas.getContext('2d')
  444. if (!ctx) { if (cb) cb(false); return }
  445. ctx.scale(pixelRatio, pixelRatio)
  446. self.ctx = ctx
  447. self._canvasNode = canvas
  448. self.canvasWidth = width
  449. self.canvasHeight = height
  450. self.dpr = pixelRatio
  451. self.canvasReady = true
  452. if (info.rect) {
  453. self._canvasRect = { left: info.rect.left || 0, top: info.rect.top || 0 }
  454. }
  455. self.buildLayout()
  456. self.buildExpansionNodes()
  457. self.drawAll()
  458. if (cb) cb(true)
  459. } catch (e) {
  460. if (cb) cb(false)
  461. }
  462. })
  463. },
  464. _getCanvasRect: function() {
  465. if (this._canvasRect) {
  466. return { left: this._canvasRect.left, top: this._canvasRect.top, width: this.canvasWidth, height: this.canvasHeight }
  467. }
  468. return { left: 0, top: 0, width: this.canvasWidth, height: this.canvasHeight }
  469. },
  470. _hexToRgba: function(hex, alpha) {
  471. var r = parseInt(hex.slice(1, 3), 16)
  472. var g = parseInt(hex.slice(3, 5), 16)
  473. var b = parseInt(hex.slice(5, 7), 16)
  474. return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'
  475. },
  476. _getTypeColor: function(type) {
  477. if (!type) return '#64748B'
  478. // 返回第一个类型对应的颜色
  479. var list = []
  480. if (type & 1) list.push('金额大')
  481. if (type & 2) list.push('频次高')
  482. if (type & 4) list.push('新成交')
  483. if (list.length === 0) return '#64748B'
  484. return this.effectColors[list[0]] || '#64748B'
  485. },
  486. formatDate: function(dateStr) {
  487. if (!dateStr) return ''
  488. return String(dateStr).substring(0, 10)
  489. }
  490. }
  491. }
  492. </script>
  493. <style scoped>
  494. .octopus-wrapper {
  495. position: relative;
  496. width: 100%;
  497. min-height: 200px;
  498. height: auto;
  499. margin: 10rpx 0;
  500. background: #FFFFFF;
  501. border-radius: 24rpx;
  502. overflow: hidden;
  503. }
  504. .octopus-canvas {
  505. position: absolute;
  506. top: 0;
  507. left: 0;
  508. z-index: 1;
  509. }
  510. .octopus-empty, .octopus-loading {
  511. display: flex;
  512. flex-direction: column;
  513. align-items: center;
  514. justify-content: center;
  515. padding: 60rpx 20rpx;
  516. background: #FFFFFF;
  517. border-radius: 24rpx;
  518. margin: 10rpx 0;
  519. }
  520. .empty-icon {
  521. font-size: 80rpx;
  522. margin-bottom: 16rpx;
  523. }
  524. .empty-text {
  525. font-size: 28rpx;
  526. font-weight: 600;
  527. color: #1E293B;
  528. margin-bottom: 8rpx;
  529. }
  530. .empty-hint {
  531. font-size: 24rpx;
  532. color: #94A3B8;
  533. margin-bottom: 24rpx;
  534. }
  535. .loading-text {
  536. font-size: 28rpx;
  537. color: #64748B;
  538. }
  539. .action-btn {
  540. display: inline-block;
  541. padding: 12rpx 32rpx;
  542. border-radius: 20rpx;
  543. font-size: 26rpx;
  544. font-weight: 600;
  545. }
  546. .action-btn-primary {
  547. background: #F97316;
  548. color: #FFFFFF;
  549. }
  550. .reanalyze-btn {
  551. position: absolute;
  552. top: 12rpx;
  553. right: 16rpx;
  554. z-index: 10;
  555. background: rgba(255,255,255,0.9);
  556. border-radius: 20rpx;
  557. padding: 8rpx 16rpx;
  558. font-size: 22rpx;
  559. color: #64748B;
  560. }
  561. /* 成效弹窗 */
  562. .effect-popup {
  563. position: absolute;
  564. z-index: 10;
  565. pointer-events: none;
  566. transform: translate(-50%, -100%);
  567. margin-top: -16rpx;
  568. }
  569. .popup-content {
  570. background: #FFFFFF;
  571. border-radius: 16rpx;
  572. padding: 20rpx;
  573. box-shadow: 0 8rpx 24rpx rgba(0,0,0,0.12);
  574. min-width: 200rpx;
  575. max-width: 320rpx;
  576. pointer-events: auto;
  577. }
  578. .popup-header {
  579. display: flex;
  580. align-items: center;
  581. margin-bottom: 12rpx;
  582. }
  583. .popup-name {
  584. font-size: 26rpx;
  585. font-weight: 600;
  586. color: #1E293B;
  587. }
  588. .popup-tags {
  589. display: flex;
  590. flex-wrap: wrap;
  591. margin-top: 4rpx;
  592. }
  593. .popup-tag {
  594. font-size: 18rpx;
  595. padding: 2rpx 10rpx;
  596. border-radius: 6rpx;
  597. margin-right: 8rpx;
  598. margin-top: 4rpx;
  599. background: #FFF7ED;
  600. color: #F97316;
  601. }
  602. .popup-keypersons {
  603. border-top: 1rpx solid #F1F5F9;
  604. padding-top: 10rpx;
  605. margin-top: 8rpx;
  606. }
  607. .popup-title {
  608. font-size: 22rpx;
  609. color: #94A3B8;
  610. display: block;
  611. margin-bottom: 6rpx;
  612. }
  613. .kp-item {
  614. font-size: 22rpx;
  615. color: #475569;
  616. display: block;
  617. margin-bottom: 4rpx;
  618. }
  619. .popup-empty {
  620. padding: 8rpx 0;
  621. }
  622. .empty-text {
  623. font-size: 22rpx;
  624. color: #94A3B8;
  625. }
  626. .popup-actions {
  627. display: flex;
  628. justify-content: center;
  629. margin-top: 12rpx;
  630. padding-top: 12rpx;
  631. border-top: 1rpx solid #F1F5F9;
  632. }
  633. </style>