jvs-ppt-ui 0.0.17 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jvs-ppt-ui",
3
3
  "private": false,
4
- "version": "0.0.17",
4
+ "version": "0.0.18",
5
5
  "type": "module",
6
6
  "main": "./dist/lib/jvs-ppt-ui.umd.js",
7
7
  "module": "./dist/lib/jvs-ppt-ui.es.js",
@@ -58,6 +58,7 @@ import {
58
58
  } from '../utils/line'
59
59
  import { type SlideTextType, TEXT_TYPE_CONFIGS } from '../utils/textTypes'
60
60
  import { defaultStyleRecipe } from '../composables/presetThemes'
61
+ import { useDamAuth } from './material/useDamAuth'
61
62
 
62
63
  const {
63
64
  activeSlide, selectedElementId, selectedElementIds, deleteElement, combineElements, uncombineElement, updateGroupBounds, zoomRatio, fitTrigger, activeTool, pendingTextType, pendingShapeSubtype, pendingShapeViewBox, pendingLineContent, activeIndex, slides, undo, redo, notify,
@@ -72,6 +73,7 @@ const {
72
73
  isMaterialEnabled, materialPopoverVisible, placeholderMaterialTargetId, setPlaceholderMaterialTargetId, replacePlaceholderElement
73
74
  } = useSlides()
74
75
  const activeTab = activeTabRaw as any
76
+ const { isDamLoggedIn, sharedMaterialMainTab, setSharedMaterialMainTab, refreshSessionState } = useDamAuth()
75
77
 
76
78
  const isDragging = ref(false)
77
79
 
@@ -1958,7 +1960,8 @@ function handleWindowMouseDown(e: MouseEvent) {
1958
1960
  target.closest('.el-dialog') ||
1959
1961
  target.closest('.el-overlay') ||
1960
1962
  target.closest('.custom-fullscreen-preview-modal') ||
1961
- target.closest('.placeholder-action-popover')
1963
+ target.closest('.placeholder-action-popover') ||
1964
+ target.closest('.material-drawer-container')
1962
1965
  ) {
1963
1966
  return
1964
1967
  }
@@ -2313,6 +2316,7 @@ function handleImagePlaceholderClick(elId: string) {
2313
2316
  triggerPlaceholderImageUpload(elId)
2314
2317
  return
2315
2318
  }
2319
+ refreshSessionState()
2316
2320
  activePlaceholderMenuElId.value = elId
2317
2321
  }
2318
2322
 
@@ -2321,8 +2325,7 @@ function handlePlaceholderMenuUpload(elId: string) {
2321
2325
  triggerPlaceholderImageUpload(elId)
2322
2326
  }
2323
2327
 
2324
- function handlePlaceholderMenuMaterial(elId: string) {
2325
- activePlaceholderMenuElId.value = null
2328
+ function bindPlaceholderTarget(elId: string) {
2326
2329
  if (setPlaceholderMaterialTargetId) {
2327
2330
  setPlaceholderMaterialTargetId(elId)
2328
2331
  } else if (placeholderMaterialTargetId) {
@@ -2334,6 +2337,23 @@ function handlePlaceholderMenuMaterial(elId: string) {
2334
2337
  if (materialPopoverVisible) {
2335
2338
  materialPopoverVisible.value = false
2336
2339
  }
2340
+ }
2341
+
2342
+ function handlePlaceholderMenuMaterial(elId: string) {
2343
+ activePlaceholderMenuElId.value = null
2344
+ bindPlaceholderTarget(elId)
2345
+ if (sharedMaterialMainTab.value === 'create') {
2346
+ setSharedMaterialMainTab('search')
2347
+ }
2348
+ activeTab.value = 'material'
2349
+ }
2350
+
2351
+ function handlePlaceholderMenuAiCreate(elId: string) {
2352
+ activePlaceholderMenuElId.value = null
2353
+ refreshSessionState()
2354
+ if (!isDamLoggedIn.value) return
2355
+ bindPlaceholderTarget(elId)
2356
+ setSharedMaterialMainTab('create')
2337
2357
  activeTab.value = 'material'
2338
2358
  }
2339
2359
 
@@ -2351,7 +2371,7 @@ const getPlaceholderActionMenuStyle = (el: any) => {
2351
2371
  const iconHalfWPhysical = Math.min(28, rect.width * 0.225) * scale
2352
2372
 
2353
2373
  const menuW = 148
2354
- const menuH = 80
2374
+ const menuH = isDamLoggedIn.value ? 114 : 80
2355
2375
  let left = centerXPhysical + iconHalfWPhysical + 8
2356
2376
  if (left + menuW > slideW - 8) {
2357
2377
  left = Math.max(8, centerXPhysical - iconHalfWPhysical - 8 - menuW)
@@ -9064,6 +9084,18 @@ async function handleGlobalPaste(e: ClipboardEvent) {
9064
9084
  </svg>
9065
9085
  <span>从素材库选择</span>
9066
9086
  </button>
9087
+ <button
9088
+ v-if="isDamLoggedIn"
9089
+ type="button"
9090
+ class="placeholder-action-item is-ai-create"
9091
+ @click.stop="handlePlaceholderMenuAiCreate(selectedElement.id)"
9092
+ >
9093
+ <svg viewBox="0 0 24 24" class="placeholder-action-icon">
9094
+ <path d="M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9L12 3z" />
9095
+ <path d="M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9L19 15z" />
9096
+ </svg>
9097
+ <span>AI生成</span>
9098
+ </button>
9067
9099
  </div>
9068
9100
  </div>
9069
9101
  </div>
@@ -12,6 +12,9 @@ import {
12
12
  } from '@element-plus/icons-vue'
13
13
  import { useSlides, DEFAULT_MATERIAL_PAGE_API_URL } from '../composables/useSlides'
14
14
  import { isImagePlaceholder } from '../utils/canvas/elementStyles'
15
+ import { useDamAuth } from './material/useDamAuth'
16
+ import MyAssetsPanel from './material/MyAssetsPanel.vue'
17
+ import CreateMaterialPanel from './material/CreateMaterialPanel.vue'
15
18
 
16
19
  const {
17
20
  addElement,
@@ -103,6 +106,44 @@ const subTypeNameMap: Record<StockSubType, string> = {
103
106
  Videos: '高清视频'
104
107
  }
105
108
 
109
+ // 顶层 3-Tab 状态(检索 / 我的素材 / 创建素材)与 jvs_session_v3 鉴权
110
+ export type MaterialMainTab = 'search' | 'mine' | 'create'
111
+ const { isDamLoggedIn, sharedMaterialMainTab: activeMainTab, refreshSessionState } = useDamAuth()
112
+
113
+ const myAssetsRef = ref<InstanceType<typeof MyAssetsPanel> | null>(null)
114
+ const createPanelRef = ref<InstanceType<typeof CreateMaterialPanel> | null>(null)
115
+
116
+ // 未登录点击受限 Tab 时的顶部轻量胶囊提示(不遮挡 Tab,不暴露技术字段)
117
+ const guestToastVisible = ref(false)
118
+ const guestToastText = ref('请先登录后使用该功能')
119
+ let guestToastTimer: ReturnType<typeof setTimeout> | null = null
120
+
121
+ function showGuestAuthToast(tabName: string) {
122
+ guestToastText.value = tabName === '我的素材'
123
+ ? '请先登录后查看「我的素材」'
124
+ : '请先登录后使用「创建素材」'
125
+ guestToastVisible.value = true
126
+ if (guestToastTimer) clearTimeout(guestToastTimer)
127
+ guestToastTimer = setTimeout(() => {
128
+ guestToastVisible.value = false
129
+ }, 2200)
130
+ }
131
+
132
+ function handleSwitchMainTab(tab: MaterialMainTab) {
133
+ refreshSessionState()
134
+ if (tab !== 'search' && !isDamLoggedIn.value) {
135
+ showGuestAuthToast(tab === 'mine' ? '我的素材' : '创建素材')
136
+ return
137
+ }
138
+ activeMainTab.value = tab
139
+ }
140
+
141
+ watch(isDamLoggedIn, (loggedIn) => {
142
+ if (!loggedIn && activeMainTab.value !== 'search') {
143
+ activeMainTab.value = 'search'
144
+ }
145
+ })
146
+
106
147
  // 视图状态:先图二 (overview) 再图三 (detail)
107
148
  const viewMode = ref<'overview' | 'detail'>('overview')
108
149
  const overviewQuery = ref('')
@@ -248,6 +289,96 @@ function openPreview(item: MaterialItem, e?: Event) {
248
289
  previewVisible.value = true
249
290
  }
250
291
 
292
+ // 供「我的素材」与「创建素材」子面板复用的全屏大图/视频预览
293
+ function openCustomListPreview(item: MaterialItem, list: MaterialItem[], e?: Event) {
294
+ if (e) {
295
+ e.stopPropagation()
296
+ e.preventDefault()
297
+ }
298
+
299
+ const currentUrl = isVideoMaterial(item)
300
+ ? (item.sourceUrl || item.url || item.downloadUrl || getMaterialThumb(item))
301
+ : getMaterialThumb(item)
302
+
303
+ if (!currentUrl) {
304
+ notify('该素材暂无可用预览地址')
305
+ return
306
+ }
307
+
308
+ const sourceList = Array.isArray(list) && list.length > 0 ? list : [item]
309
+ const validItems: PreviewItem[] = []
310
+
311
+ sourceList.forEach(m => {
312
+ const isVid = isVideoMaterial(m)
313
+ const u = isVid
314
+ ? (m.sourceUrl || m.url || m.downloadUrl || getMaterialThumb(m))
315
+ : getMaterialThumb(m)
316
+ if (u) {
317
+ validItems.push({
318
+ url: u,
319
+ isVideo: isVid && isVideoUrl(u),
320
+ title: m.title || '素材预览',
321
+ format: getItemFormat(m),
322
+ meta: m
323
+ })
324
+ }
325
+ })
326
+
327
+ const targetIdx = validItems.findIndex(i => i.url === currentUrl)
328
+ if (targetIdx >= 0) {
329
+ previewInitialIndex.value = targetIdx
330
+ } else {
331
+ validItems.unshift({
332
+ url: currentUrl,
333
+ isVideo: isVideoMaterial(item) && isVideoUrl(currentUrl),
334
+ title: item.title || '素材预览',
335
+ format: getItemFormat(item),
336
+ meta: item
337
+ })
338
+ previewInitialIndex.value = 0
339
+ }
340
+
341
+ previewList.value = validItems
342
+
343
+ if (materialPopoverVisible && materialPopoverVisible.value) {
344
+ wasPopoverOpenBeforePreview.value = true
345
+ materialPopoverVisible.value = false
346
+ } else {
347
+ wasPopoverOpenBeforePreview.value = false
348
+ }
349
+
350
+ previewVisible.value = true
351
+ }
352
+
353
+ // 将「检索」或「我的素材」中的图片一键送入「创建素材」作为参考图
354
+ function handleSendToCreateRef(item: MaterialItem, e?: Event) {
355
+ if (e) {
356
+ e.stopPropagation()
357
+ e.preventDefault()
358
+ }
359
+ refreshSessionState()
360
+ if (!isDamLoggedIn.value) {
361
+ showGuestAuthToast('创建素材')
362
+ return
363
+ }
364
+ const thumbUrl = getMaterialThumb(item)
365
+ if (!thumbUrl) {
366
+ notify('该素材暂无可用参考图地址')
367
+ return
368
+ }
369
+ activeMainTab.value = 'create'
370
+ nextTick(() => {
371
+ createPanelRef.value?.addReferenceItem({
372
+ url: thumbUrl,
373
+ name: item.title || '参考图'
374
+ })
375
+ })
376
+ }
377
+
378
+ function handleOnMaterialGenerated() {
379
+ myAssetsRef.value?.reloadAssets?.()
380
+ }
381
+
251
382
  function closePreview() {
252
383
  previewVisible.value = false
253
384
  if (wasPopoverOpenBeforePreview.value && materialPopoverVisible) {
@@ -770,9 +901,21 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
770
901
 
771
902
  <template>
772
903
  <div class="material-drawer-container">
904
+ <!-- 未登录点击「我的素材/创建素材」时的顶部轻量胶囊提示 -->
905
+ <transition name="guest-toast-fade">
906
+ <div v-if="guestToastVisible" class="guest-auth-toast">
907
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="#d97706" stroke-width="2.2">
908
+ <circle cx="12" cy="12" r="10" />
909
+ <line x1="12" y1="8" x2="12" y2="12" />
910
+ <line x1="12" y1="16" x2="12.01" y2="16" />
911
+ </svg>
912
+ <span>{{ guestToastText }}</span>
913
+ </div>
914
+ </transition>
915
+
773
916
  <!-- 顶部抽屉标题栏:图二显示「素材库」,图三显示「← 分类/搜索词」 -->
774
917
  <div class="material-drawer-header">
775
- <div v-if="viewMode === 'overview'" class="drawer-header-left">
918
+ <div v-if="activeMainTab !== 'search' || viewMode === 'overview'" class="drawer-header-left">
776
919
  <span class="drawer-header-title">素材库</span>
777
920
  </div>
778
921
  <button v-else type="button" class="drawer-back-btn" @click="backToOverview">
@@ -793,8 +936,77 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
793
936
  </button>
794
937
  </div>
795
938
 
796
- <!-- ==================== 图二:素材库分类概览视图 ==================== -->
797
- <div v-if="viewMode === 'overview'" class="material-overview-body">
939
+ <!-- 顶层 3-Tab 分段切换器(检索 | 我的素材 | 创建素材) -->
940
+ <div v-if="activeMainTab !== 'search' || viewMode === 'overview'" class="material-top-tabs-wrap">
941
+ <div class="material-top-segmented">
942
+ <button
943
+ type="button"
944
+ class="material-main-tab-btn"
945
+ :class="{ active: activeMainTab === 'search' }"
946
+ @click="handleSwitchMainTab('search')"
947
+ >
948
+ <span>检索</span>
949
+ </button>
950
+
951
+ <button
952
+ type="button"
953
+ class="material-main-tab-btn"
954
+ :class="{
955
+ active: activeMainTab === 'mine',
956
+ 'is-disabled': !isDamLoggedIn
957
+ }"
958
+ :title="!isDamLoggedIn ? '请先登录后查看我的素材' : '查看我的项目与个人素材'"
959
+ @click="handleSwitchMainTab('mine')"
960
+ >
961
+ <span>我的素材</span>
962
+ <svg
963
+ v-if="!isDamLoggedIn"
964
+ class="tab-lock-svg"
965
+ viewBox="0 0 24 24"
966
+ width="11"
967
+ height="11"
968
+ fill="none"
969
+ stroke="currentColor"
970
+ stroke-width="2.2"
971
+ >
972
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
973
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
974
+ </svg>
975
+ </button>
976
+
977
+ <button
978
+ type="button"
979
+ class="material-main-tab-btn"
980
+ :class="{
981
+ active: activeMainTab === 'create',
982
+ 'is-disabled': !isDamLoggedIn
983
+ }"
984
+ :title="!isDamLoggedIn ? '请先登录后使用创建素材' : 'AI 生成图片或视频素材'"
985
+ @click="handleSwitchMainTab('create')"
986
+ >
987
+ <span>创建素材</span>
988
+ <span v-if="isDamLoggedIn" class="tab-ai-spark">✦</span>
989
+ <svg
990
+ v-else
991
+ class="tab-lock-svg"
992
+ viewBox="0 0 24 24"
993
+ width="11"
994
+ height="11"
995
+ fill="none"
996
+ stroke="currentColor"
997
+ stroke-width="2.2"
998
+ >
999
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
1000
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
1001
+ </svg>
1002
+ </button>
1003
+ </div>
1004
+ </div>
1005
+
1006
+ <!-- ==================== Tab 1:检索(公网素材分类概览 & 详情瀑布流) ==================== -->
1007
+ <template v-if="activeMainTab === 'search'">
1008
+ <!-- 图二:素材库分类概览视图 -->
1009
+ <div v-if="viewMode === 'overview'" class="material-overview-body">
798
1010
  <!-- 搜索框 -->
799
1011
  <div class="overview-search-wrap">
800
1012
  <div class="overview-search-box">
@@ -873,6 +1085,15 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
873
1085
  <span v-if="cat.subType === 'Videos' || isVideoMaterial(item)" class="video-badge-icon">
874
1086
  <el-icon><VideoPlay /></el-icon>
875
1087
  </span>
1088
+ <button
1089
+ v-if="isDamLoggedIn && cat.subType !== 'Videos' && !isVideoMaterial(item) && getMaterialThumb(item)"
1090
+ type="button"
1091
+ class="overview-card-ref-btn"
1092
+ title="作为参考图发送到「创建素材」"
1093
+ @click.stop.prevent="handleSendToCreateRef(item, $event)"
1094
+ >
1095
+ @参考
1096
+ </button>
876
1097
  </div>
877
1098
  </template>
878
1099
 
@@ -1014,6 +1235,17 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
1014
1235
  <el-icon><VideoPlay /></el-icon>
1015
1236
  </span>
1016
1237
 
1238
+ <!-- 悬浮设为参考图按钮 -->
1239
+ <button
1240
+ v-if="isDamLoggedIn && filterParams.activeSubType !== 'Videos' && !isVideoMaterial(item) && getMaterialThumb(item)"
1241
+ type="button"
1242
+ class="detail-card-ref-btn"
1243
+ title="作为参考图发送到「创建素材」"
1244
+ @click.stop.prevent="handleSendToCreateRef(item, $event)"
1245
+ >
1246
+ @参考
1247
+ </button>
1248
+
1017
1249
  <!-- 悬浮预览小按钮 -->
1018
1250
  <button
1019
1251
  v-if="getMaterialThumb(item)"
@@ -1064,6 +1296,25 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
1064
1296
  </div>
1065
1297
  </div>
1066
1298
  </div>
1299
+ </template>
1300
+
1301
+ <!-- ==================== Tab 2:我的素材(jvs-material-ui 资产库接入) ==================== -->
1302
+ <MyAssetsPanel
1303
+ v-else-if="activeMainTab === 'mine' && isDamLoggedIn"
1304
+ ref="myAssetsRef"
1305
+ @select="handleSelectMaterial"
1306
+ @preview="openCustomListPreview"
1307
+ @use-as-ref="(item) => handleSendToCreateRef(item)"
1308
+ />
1309
+
1310
+ <!-- ==================== Tab 3:创建素材(AI 图片生成器 & 视频生成) ==================== -->
1311
+ <CreateMaterialPanel
1312
+ v-else-if="activeMainTab === 'create' && isDamLoggedIn"
1313
+ ref="createPanelRef"
1314
+ @select="handleSelectMaterial"
1315
+ @preview="openCustomListPreview"
1316
+ @generated="handleOnMaterialGenerated"
1317
+ />
1067
1318
 
1068
1319
  <!-- 全屏大图/视频预览 Modal -->
1069
1320
  <teleport to="body">
@@ -1146,6 +1397,7 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
1146
1397
 
1147
1398
  <style scoped lang="scss">
1148
1399
  .material-drawer-container {
1400
+ position: relative;
1149
1401
  display: flex;
1150
1402
  flex-direction: column;
1151
1403
  width: 288px;
@@ -1157,18 +1409,112 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
1157
1409
  user-select: none;
1158
1410
  }
1159
1411
 
1412
+ /* 未登录点击受限 Tab 时的顶部轻量胶囊提示 */
1413
+ .guest-auth-toast {
1414
+ position: absolute;
1415
+ top: 10px;
1416
+ left: 50%;
1417
+ transform: translateX(-50%);
1418
+ background: #fffbeb;
1419
+ border: 1px solid #fde68a;
1420
+ color: #b45309;
1421
+ font-size: 11.5px;
1422
+ font-weight: 500;
1423
+ padding: 5px 12px;
1424
+ border-radius: 99px;
1425
+ white-space: nowrap;
1426
+ z-index: 200;
1427
+ box-shadow: 0 4px 12px rgba(217, 119, 6, 0.12);
1428
+ display: flex;
1429
+ align-items: center;
1430
+ gap: 5px;
1431
+ pointer-events: none;
1432
+ }
1433
+
1434
+ .guest-toast-fade-enter-active,
1435
+ .guest-toast-fade-leave-active {
1436
+ transition: opacity 0.18s ease, transform 0.18s ease;
1437
+ }
1438
+ .guest-toast-fade-enter-from,
1439
+ .guest-toast-fade-leave-to {
1440
+ opacity: 0;
1441
+ transform: translate(-50%, -6px);
1442
+ }
1443
+
1160
1444
  /* 顶部栏(图二:素材库 / 图三:← 分类名) */
1161
1445
  .material-drawer-header {
1162
1446
  display: flex;
1163
1447
  align-items: center;
1164
1448
  justify-content: space-between;
1165
- height: 48px;
1449
+ height: 44px;
1166
1450
  padding: 0 16px;
1167
- border-bottom: 1px solid #f1f5f9;
1451
+ border-bottom: none;
1168
1452
  flex-shrink: 0;
1169
1453
  box-sizing: border-box;
1170
1454
  }
1171
1455
 
1456
+ /* 顶层 3-Tab 分段切换器(检索 | 我的素材 | 创建素材) */
1457
+ .material-top-tabs-wrap {
1458
+ padding: 2px 16px 10px;
1459
+ border-bottom: 1px solid #f1f5f9;
1460
+ flex-shrink: 0;
1461
+ }
1462
+
1463
+ .material-top-segmented {
1464
+ display: grid;
1465
+ grid-template-columns: repeat(3, 1fr);
1466
+ background: #f1f5f9;
1467
+ padding: 3px;
1468
+ border-radius: 8px;
1469
+ gap: 2px;
1470
+ }
1471
+
1472
+ .material-main-tab-btn {
1473
+ height: 28px;
1474
+ border: none;
1475
+ background: transparent;
1476
+ border-radius: 6px;
1477
+ font-size: 12px;
1478
+ font-weight: 500;
1479
+ color: #64748b;
1480
+ cursor: pointer;
1481
+ display: inline-flex;
1482
+ align-items: center;
1483
+ justify-content: center;
1484
+ gap: 3px;
1485
+ padding: 0 4px;
1486
+ white-space: nowrap;
1487
+ transition: all 0.18s ease;
1488
+
1489
+ &:hover:not(.active):not(.is-disabled) {
1490
+ color: #1e293b;
1491
+ }
1492
+
1493
+ &.active {
1494
+ background: #ffffff;
1495
+ color: #1e6fff;
1496
+ font-weight: 600;
1497
+ box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
1498
+ }
1499
+
1500
+ &.is-disabled {
1501
+ color: #94a3b8;
1502
+ cursor: not-allowed;
1503
+ opacity: 0.72;
1504
+ }
1505
+ }
1506
+
1507
+ .tab-ai-spark {
1508
+ font-size: 11px;
1509
+ line-height: 1;
1510
+ color: #8b5cf6;
1511
+ }
1512
+
1513
+ .tab-lock-svg {
1514
+ flex-shrink: 0;
1515
+ color: #94a3b8;
1516
+ }
1517
+
1172
1518
  .drawer-header-left {
1173
1519
  display: flex;
1174
1520
  align-items: center;
@@ -1382,6 +1728,32 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
1382
1728
  &:hover .overview-thumb-img {
1383
1729
  transform: scale(1.04);
1384
1730
  }
1731
+
1732
+ &:hover .overview-card-ref-btn {
1733
+ opacity: 1;
1734
+ }
1735
+ }
1736
+
1737
+ .overview-card-ref-btn {
1738
+ position: absolute;
1739
+ left: 5px;
1740
+ bottom: 5px;
1741
+ height: 19px;
1742
+ padding: 0 6px;
1743
+ border-radius: 4px;
1744
+ border: none;
1745
+ background: rgba(15, 23, 42, 0.68);
1746
+ color: #ffffff;
1747
+ font-size: 10px;
1748
+ font-weight: 500;
1749
+ cursor: pointer;
1750
+ opacity: 0;
1751
+ transition: all 0.15s ease;
1752
+ z-index: 3;
1753
+
1754
+ &:hover {
1755
+ background: #1e6fff;
1756
+ }
1385
1757
  }
1386
1758
 
1387
1759
  .overview-thumb-img {
@@ -1579,11 +1951,34 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
1579
1951
  transform: scale(1.03);
1580
1952
  }
1581
1953
 
1582
- &:hover .detail-card-preview-btn {
1954
+ &:hover .detail-card-preview-btn,
1955
+ &:hover .detail-card-ref-btn {
1583
1956
  opacity: 1;
1584
1957
  }
1585
1958
  }
1586
1959
 
1960
+ .detail-card-ref-btn {
1961
+ position: absolute;
1962
+ left: 6px;
1963
+ bottom: 6px;
1964
+ height: 20px;
1965
+ padding: 0 6px;
1966
+ border-radius: 4px;
1967
+ border: none;
1968
+ background: rgba(15, 23, 42, 0.68);
1969
+ color: #ffffff;
1970
+ font-size: 10px;
1971
+ font-weight: 500;
1972
+ cursor: pointer;
1973
+ opacity: 0;
1974
+ transition: all 0.15s ease;
1975
+ z-index: 3;
1976
+
1977
+ &:hover {
1978
+ background: #1e6fff;
1979
+ }
1980
+ }
1981
+
1587
1982
  .detail-card-img {
1588
1983
  width: 100%;
1589
1984
  height: auto;