jvs-ppt-ui 0.0.14 → 0.0.16
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/dist/lib/check_imgs.html +18 -0
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.css +187 -27
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.js +1 -1
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.json +345 -65
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.ttf +0 -0
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff +0 -0
- package/dist/lib/jvs-ppt-ui/iconfont/iconfont.woff2 +0 -0
- package/dist/lib/jvs-ppt-ui/img/al.png +0 -0
- package/dist/lib/jvs-ppt-ui/img/d.png +0 -0
- package/dist/lib/jvs-ppt-ui/img/dl.png +0 -0
- package/dist/lib/jvs-ppt-ui/img/l.png +0 -0
- package/dist/lib/jvs-ppt-ui.es.js +44973 -43349
- package/dist/lib/jvs-ppt-ui.umd.js +102 -102
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/scripts/check-icons.mjs +22 -0
- package/scripts/patch-anim-icons.mjs +45 -0
- package/src/assets/iconfont/iconfont.css +187 -27
- package/src/assets/iconfont/iconfont.js +1 -1
- package/src/assets/iconfont/iconfont.json +345 -65
- package/src/assets/iconfont/iconfont.ttf +0 -0
- package/src/assets/iconfont/iconfont.woff +0 -0
- package/src/assets/iconfont/iconfont.woff2 +0 -0
- package/src/components/CanvasArea.vue +218 -193
- package/src/components/ChartTypeSelector.vue +509 -148
- package/src/components/Header.vue +327 -44
- package/src/components/PresentationGroupChild.vue +44 -13
- package/src/components/PresentationMode.vue +84 -33
- package/src/components/QuickBorderPopover.vue +51 -11
- package/src/components/QuickColorPopover.vue +65 -21
- package/src/components/SidebarLeft.vue +286 -107
- package/src/components/SidebarRight.vue +45 -38
- package/src/components/TemplateGallery.vue +1110 -1237
- package/src/components/ToastNotification.vue +2 -2
- package/src/components/Toolbar.vue +825 -733
- package/src/components/canvas/CanvasQuickToolbar.vue +17 -30
- package/src/components/canvas/elements/CanvasGroupElement.vue +6 -0
- package/src/components/canvas/elements/CanvasShapeElement.vue +198 -38
- package/src/components/canvas/elements/CanvasTextElement.vue +4 -3
- package/src/components/sidebar/common/SidebarAlignGroup.vue +104 -50
- package/src/components/sidebar/common/SidebarBorderAccordion.vue +375 -148
- package/src/components/sidebar/common/SidebarPositionGroup.vue +358 -149
- package/src/components/sidebar/common/SidebarReflectionAccordion.vue +429 -203
- package/src/components/sidebar/common/SidebarShadowAccordion.vue +844 -416
- package/src/components/sidebar/panels/GradientEditor.vue +671 -316
- package/src/components/sidebar/panels/LayoutThumbSvg.vue +33 -16
- package/src/components/sidebar/panels/SidebarAnimationPanel.vue +701 -138
- package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
- package/src/components/sidebar/panels/SidebarDesignPanel.vue +1754 -416
- package/src/components/sidebar/panels/SidebarMediaFormatPanel.vue +12 -0
- package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +813 -174
- package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
- package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +1938 -1427
- package/src/components/sidebar/panels/SidebarTransitionPanel.vue +791 -210
- package/src/composables/canvas/useCanvasShortcuts.ts +20 -6
- package/src/composables/canvas/useCanvasText.ts +1 -1
- package/src/composables/canvas/useCanvasTransform.ts +44 -0
- package/src/composables/presetThemes.ts +43 -44
- package/src/composables/sidebar/useSidebarAnimation.ts +101 -25
- package/src/composables/sidebar/useSidebarDesign.ts +30 -11
- package/src/composables/sidebar/useSidebarElementTransform.ts +10 -1
- package/src/composables/sidebar/useSidebarImage.ts +10 -2
- package/src/composables/sidebar/useSidebarShapeFormat.ts +562 -107
- package/src/composables/useSlides.ts +331 -153
- package/src/main.ts +2 -0
- package/src/style.css +926 -359
- package/src/utils/canvas/elementStyles.ts +42 -1
|
@@ -1,69 +1,169 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
2
|
+
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
3
3
|
import {
|
|
4
4
|
Search,
|
|
5
5
|
Plus,
|
|
6
6
|
Close,
|
|
7
7
|
CircleCloseFilled,
|
|
8
|
-
ArrowDown,
|
|
9
8
|
ArrowLeft,
|
|
10
9
|
ArrowRight,
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
View,
|
|
11
|
+
VideoPlay
|
|
13
12
|
} from '@element-plus/icons-vue'
|
|
14
|
-
import { useSlides,
|
|
13
|
+
import { useSlides, DEFAULT_MATERIAL_PAGE_API_URL } from '../composables/useSlides'
|
|
15
14
|
|
|
16
15
|
const {
|
|
17
|
-
insertExternalSlide,
|
|
18
16
|
addElement,
|
|
19
17
|
notify,
|
|
20
18
|
materialApiUrl,
|
|
21
19
|
materialTreeApiUrl,
|
|
22
20
|
materialPageApiUrl,
|
|
23
21
|
materialHeaders,
|
|
24
|
-
|
|
25
|
-
activeSlide,
|
|
22
|
+
activeTab,
|
|
26
23
|
materialPopoverVisible
|
|
27
24
|
} = useSlides()
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
export interface MaterialItem {
|
|
27
|
+
id: string | number
|
|
28
|
+
title: string
|
|
29
|
+
pptSource?: string
|
|
30
|
+
category?: string
|
|
31
|
+
type?: string
|
|
32
|
+
assetType?: string
|
|
33
|
+
assetSubType?: string
|
|
34
|
+
subType?: string
|
|
35
|
+
format?: string
|
|
36
|
+
color?: string
|
|
37
|
+
slideIndex?: number
|
|
38
|
+
fileName?: string
|
|
39
|
+
url?: string
|
|
40
|
+
previewUrl?: string
|
|
41
|
+
sourceUrl?: string
|
|
42
|
+
downloadUrl?: string
|
|
43
|
+
elementsCount?: number
|
|
44
|
+
originalWidth?: number
|
|
45
|
+
originalHeight?: number
|
|
46
|
+
originalSize?: { width: number; height: number }
|
|
47
|
+
tags?: string[]
|
|
48
|
+
aspectRatio?: string
|
|
49
|
+
[key: string]: any
|
|
50
|
+
}
|
|
33
51
|
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
export type StockSubType =
|
|
53
|
+
| 'Vectors'
|
|
54
|
+
| 'Photos'
|
|
55
|
+
| 'Illustrations'
|
|
56
|
+
| 'Videos'
|
|
36
57
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
interface OverviewCategoryConfig {
|
|
59
|
+
key: StockSubType
|
|
60
|
+
label: string
|
|
61
|
+
subType: StockSubType
|
|
62
|
+
tags: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 图二:固定 4 大核心分类区块配置,每个分类对应后端 subType 参数独立请求一次接口
|
|
66
|
+
const overviewCategories: OverviewCategoryConfig[] = [
|
|
67
|
+
{
|
|
68
|
+
key: 'Vectors',
|
|
69
|
+
label: '矢量图',
|
|
70
|
+
subType: 'Vectors',
|
|
71
|
+
tags: ['风景', '商务', '科技', '插画', '几何', '背景', '图标']
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: 'Photos',
|
|
75
|
+
label: '照片',
|
|
76
|
+
subType: 'Photos',
|
|
77
|
+
tags: ['风景', '商务', '科技', '人物', '自然', '建筑', '美食', '办公']
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: 'Illustrations',
|
|
81
|
+
label: '插画',
|
|
82
|
+
subType: 'Illustrations',
|
|
83
|
+
tags: ['风景', '商务', '科技', '人物', '自然', '创意', '手绘', '扁平']
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: 'Videos',
|
|
87
|
+
label: '高清视频',
|
|
88
|
+
subType: 'Videos',
|
|
89
|
+
tags: ['风景', '商务', '科技', '自然', '城市', '航拍', '办公']
|
|
44
90
|
}
|
|
45
|
-
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
const subTypeNameMap: Record<StockSubType, string> = {
|
|
94
|
+
Vectors: '矢量图',
|
|
95
|
+
Photos: '照片',
|
|
96
|
+
Illustrations: '插画',
|
|
97
|
+
Videos: '高清视频'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 视图状态:先图二 (overview) 再图三 (detail)
|
|
101
|
+
const viewMode = ref<'overview' | 'detail'>('overview')
|
|
102
|
+
const overviewQuery = ref('')
|
|
103
|
+
const baseDetailTitle = ref('素材库')
|
|
104
|
+
const baseSearchQuery = ref('')
|
|
105
|
+
const activeTag = ref('')
|
|
106
|
+
|
|
107
|
+
// 统一筛选与搜索模型
|
|
108
|
+
const filterParams = reactive<{
|
|
109
|
+
query: string
|
|
110
|
+
activeSubType: StockSubType
|
|
111
|
+
}>({
|
|
112
|
+
query: '',
|
|
113
|
+
activeSubType: 'Vectors'
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// 图三顶部返回栏显示的标题
|
|
117
|
+
const detailHeaderTitle = computed(() => {
|
|
118
|
+
if (activeTag.value) return activeTag.value
|
|
119
|
+
if (baseSearchQuery.value) return baseSearchQuery.value
|
|
120
|
+
return baseDetailTitle.value || subTypeNameMap[filterParams.activeSubType] || '素材库'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// 当前分类下的细分标签列表
|
|
124
|
+
const currentCategoryTags = computed(() => {
|
|
125
|
+
const found = overviewCategories.find(c => c.subType === filterParams.activeSubType)
|
|
126
|
+
if (found) return found.tags
|
|
127
|
+
return ['风景', '商务', '科技', '人物', '自然', '建筑', '办公']
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
function isVideoUrl(url?: string): boolean {
|
|
131
|
+
if (!url) return false
|
|
132
|
+
const clean = url.split('?')[0].toLowerCase()
|
|
133
|
+
return clean.endsWith('.mp4') || clean.endsWith('.webm') || clean.endsWith('.mov') || clean.endsWith('.m4v') || clean.endsWith('.ogg')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isVideoMaterial(item: MaterialItem): boolean {
|
|
137
|
+
return (
|
|
138
|
+
item.type === 'video' ||
|
|
139
|
+
item.assetType === 'VIDEO' ||
|
|
140
|
+
item.assetSubType === 'Videos' ||
|
|
141
|
+
item.subType === 'Videos' ||
|
|
142
|
+
isVideoUrl(item.sourceUrl || item.url || item.downloadUrl)
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getMaterialThumb(item: MaterialItem): string {
|
|
147
|
+
return item.previewUrl || item.image || item.coverUrl || item.thumbnail || item.url || item.sourceUrl || item.downloadUrl || ''
|
|
46
148
|
}
|
|
47
149
|
|
|
48
|
-
// 提取格式标签
|
|
49
150
|
function getItemFormat(item: MaterialItem): string {
|
|
50
151
|
if (item.format) return item.format.toUpperCase()
|
|
51
|
-
const u = (item.url || item.previewUrl || item.fileName || '').toLowerCase()
|
|
152
|
+
const u = (item.sourceUrl || item.url || item.previewUrl || item.fileName || '').toLowerCase()
|
|
153
|
+
if (u.includes('.mp4')) return 'MP4'
|
|
154
|
+
if (u.includes('.mov') || u.includes('.webm')) return 'VIDEO'
|
|
155
|
+
if (u.includes('.svg')) return 'SVG'
|
|
52
156
|
if (u.includes('.png')) return 'PNG'
|
|
53
157
|
if (u.includes('.gif')) return 'GIF'
|
|
54
|
-
if (
|
|
55
|
-
if (
|
|
56
|
-
if (u.includes('.ai')) return 'AI'
|
|
57
|
-
if (u.includes('.mov') || u.includes('.mp4')) return 'MOV'
|
|
58
|
-
if (item.type === 'gif') return 'GIF'
|
|
59
|
-
if (item.type === 'svg') return 'SVG'
|
|
60
|
-
if (item.type === 'sticker') return 'PNG'
|
|
158
|
+
if (item.assetSubType === 'Videos' || item.type === 'video') return 'MP4'
|
|
159
|
+
if (item.assetSubType === 'Vectors' || item.type === 'svg') return 'SVG'
|
|
61
160
|
return 'JPG'
|
|
62
161
|
}
|
|
63
162
|
|
|
64
|
-
//
|
|
163
|
+
// 全屏大图/视频预览状态
|
|
65
164
|
interface PreviewItem {
|
|
66
165
|
url: string
|
|
166
|
+
isVideo: boolean
|
|
67
167
|
title: string
|
|
68
168
|
format: string
|
|
69
169
|
meta: MaterialItem
|
|
@@ -85,19 +185,31 @@ function openPreview(item: MaterialItem, e?: Event) {
|
|
|
85
185
|
e.preventDefault()
|
|
86
186
|
}
|
|
87
187
|
|
|
88
|
-
const currentUrl =
|
|
188
|
+
const currentUrl = isVideoMaterial(item)
|
|
189
|
+
? (item.sourceUrl || item.url || item.downloadUrl || getMaterialThumb(item))
|
|
190
|
+
: getMaterialThumb(item)
|
|
191
|
+
|
|
89
192
|
if (!currentUrl) {
|
|
90
193
|
notify('该素材暂无可用预览地址')
|
|
91
194
|
return
|
|
92
195
|
}
|
|
93
196
|
|
|
197
|
+
const subKey = (item.assetSubType || item.subType || filterParams.activeSubType) as StockSubType
|
|
198
|
+
const sourceList = viewMode.value === 'detail'
|
|
199
|
+
? displayedMaterials.value
|
|
200
|
+
: (overviewItemsMap[subKey] || [])
|
|
201
|
+
|
|
94
202
|
const validItems: PreviewItem[] = []
|
|
95
|
-
|
|
96
|
-
const
|
|
203
|
+
sourceList.forEach(m => {
|
|
204
|
+
const isVid = isVideoMaterial(m)
|
|
205
|
+
const u = isVid
|
|
206
|
+
? (m.sourceUrl || m.url || m.downloadUrl || getMaterialThumb(m))
|
|
207
|
+
: getMaterialThumb(m)
|
|
97
208
|
if (u) {
|
|
98
209
|
validItems.push({
|
|
99
210
|
url: u,
|
|
100
|
-
|
|
211
|
+
isVideo: isVid && isVideoUrl(u),
|
|
212
|
+
title: m.title || '素材预览',
|
|
101
213
|
format: getItemFormat(m),
|
|
102
214
|
meta: m
|
|
103
215
|
})
|
|
@@ -110,7 +222,8 @@ function openPreview(item: MaterialItem, e?: Event) {
|
|
|
110
222
|
} else {
|
|
111
223
|
validItems.unshift({
|
|
112
224
|
url: currentUrl,
|
|
113
|
-
|
|
225
|
+
isVideo: isVideoMaterial(item) && isVideoUrl(currentUrl),
|
|
226
|
+
title: item.title || '素材预览',
|
|
114
227
|
format: getItemFormat(item),
|
|
115
228
|
meta: item
|
|
116
229
|
})
|
|
@@ -119,7 +232,6 @@ function openPreview(item: MaterialItem, e?: Event) {
|
|
|
119
232
|
|
|
120
233
|
previewList.value = validItems
|
|
121
234
|
|
|
122
|
-
// 预览大图时,隐藏素材弹窗,并记录状态以便关闭预览后无缝恢复
|
|
123
235
|
if (materialPopoverVisible && materialPopoverVisible.value) {
|
|
124
236
|
wasPopoverOpenBeforePreview.value = true
|
|
125
237
|
materialPopoverVisible.value = false
|
|
@@ -134,7 +246,6 @@ function closePreview() {
|
|
|
134
246
|
previewVisible.value = false
|
|
135
247
|
if (wasPopoverOpenBeforePreview.value && materialPopoverVisible) {
|
|
136
248
|
wasPopoverOpenBeforePreview.value = false
|
|
137
|
-
// 延时微任务以防预览弹窗的点击事件穿透触发 Popover 的外部关闭逻辑
|
|
138
249
|
setTimeout(() => {
|
|
139
250
|
materialPopoverVisible.value = true
|
|
140
251
|
}, 60)
|
|
@@ -172,125 +283,6 @@ function handleKeydown(e: KeyboardEvent) {
|
|
|
172
283
|
}
|
|
173
284
|
}
|
|
174
285
|
|
|
175
|
-
export interface MaterialItem {
|
|
176
|
-
id: string | number
|
|
177
|
-
title: string
|
|
178
|
-
pptSource?: string
|
|
179
|
-
category?: string
|
|
180
|
-
type?: string
|
|
181
|
-
format?: string
|
|
182
|
-
color?: string
|
|
183
|
-
slideIndex?: number
|
|
184
|
-
fileName?: string
|
|
185
|
-
url?: string
|
|
186
|
-
previewUrl?: string
|
|
187
|
-
downloadUrl?: string
|
|
188
|
-
slide?: any
|
|
189
|
-
elementsCount?: number
|
|
190
|
-
originalWidth?: number
|
|
191
|
-
originalHeight?: number
|
|
192
|
-
originalSize?: { width: number; height: number }
|
|
193
|
-
tags?: string[]
|
|
194
|
-
aspectRatio?: string
|
|
195
|
-
[key: string]: any
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export interface MaterialCategory {
|
|
199
|
-
id: string
|
|
200
|
-
type: string
|
|
201
|
-
name: string
|
|
202
|
-
hasMore?: boolean
|
|
203
|
-
items?: MaterialItem[]
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 统一筛选与搜索模型(红框1 + 红框2 + 红框3)
|
|
207
|
-
const filterParams = reactive({
|
|
208
|
-
query: '', // 红框1:搜索关键词
|
|
209
|
-
activeType: 'all', // 红框2:分类 Tabs
|
|
210
|
-
sort: 'recommend', // 红框3:综合排序
|
|
211
|
-
format: '', // 红框3:格式筛选
|
|
212
|
-
color: '' // 红框3:颜色筛选
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
// 分类 Tabs(红框2)
|
|
216
|
-
const categoryTabs = [
|
|
217
|
-
{ label: '全部', value: 'all' },
|
|
218
|
-
{ label: '图片', value: 'image' },
|
|
219
|
-
{ label: '动图', value: 'gif' },
|
|
220
|
-
{ label: '免抠', value: 'sticker' },
|
|
221
|
-
{ label: '矢量', value: 'svg' },
|
|
222
|
-
{ label: '模板', value: 'slide' }
|
|
223
|
-
]
|
|
224
|
-
|
|
225
|
-
// 排序选项(红框3)
|
|
226
|
-
const sortOptions = [
|
|
227
|
-
{ label: '综合排序', value: 'recommend' },
|
|
228
|
-
{ label: '最新上传', value: 'latest' },
|
|
229
|
-
{ label: '热门下载', value: 'hot' }
|
|
230
|
-
]
|
|
231
|
-
|
|
232
|
-
// 格式选项(红框3下拉)
|
|
233
|
-
const formatOptions = ['JPG', 'PNG', 'GIF', 'SVG', 'EPS', 'PSD', 'MOV', 'AI']
|
|
234
|
-
|
|
235
|
-
// 常用色系
|
|
236
|
-
const colorOptions = [
|
|
237
|
-
{ name: '蓝色系', hex: '#2196F3' },
|
|
238
|
-
{ name: '青色系', hex: '#00BCD4' },
|
|
239
|
-
{ name: '绿色系', hex: '#4CAF50' },
|
|
240
|
-
{ name: '黄色系', hex: '#FFC107' },
|
|
241
|
-
{ name: '橙色系', hex: '#FF9800' },
|
|
242
|
-
{ name: '红色系', hex: '#F44336' },
|
|
243
|
-
{ name: '紫色系', hex: '#9C27B0' },
|
|
244
|
-
{ name: '白色系', hex: '#FFFFFF' },
|
|
245
|
-
{ name: '黑色系', hex: '#212121' }
|
|
246
|
-
]
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* 资产细分子类型枚举 (对应后端 AssetSubType 与 jvs-material-ui)
|
|
250
|
-
*/
|
|
251
|
-
export type StockSubType =
|
|
252
|
-
| 'Photos'
|
|
253
|
-
| 'Illustrations'
|
|
254
|
-
| 'Vectors'
|
|
255
|
-
| 'Icons'
|
|
256
|
-
| 'Videos'
|
|
257
|
-
| 'Music'
|
|
258
|
-
| 'Sound_Effects'
|
|
259
|
-
| 'GIF'
|
|
260
|
-
| '_3D_Models'
|
|
261
|
-
|
|
262
|
-
// 页面分类 Tab -> 后端 /stock/search subType 映射 (对应 jvs-dam-2.5 AssetSubType)
|
|
263
|
-
const tabToSubTypeMap: Record<string, StockSubType | undefined> = {
|
|
264
|
-
all: undefined,
|
|
265
|
-
image: 'Photos',
|
|
266
|
-
gif: 'GIF',
|
|
267
|
-
sticker: 'Icons',
|
|
268
|
-
svg: 'Vectors',
|
|
269
|
-
slide: undefined
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const loading = ref(false)
|
|
273
|
-
const pagedMaterials = ref<MaterialItem[]>([])
|
|
274
|
-
const currentPage = ref(1)
|
|
275
|
-
const pageSize = 24
|
|
276
|
-
const totalRecords = ref(0)
|
|
277
|
-
|
|
278
|
-
// 分页与异常熔断状态(彻底杜绝滚动到底部时的无限死循环请求)
|
|
279
|
-
const loadingMore = ref(false)
|
|
280
|
-
const loadFailed = ref(false)
|
|
281
|
-
const loadErrorMsg = ref('')
|
|
282
|
-
const reachEnd = ref(false)
|
|
283
|
-
let scrollThrottleTimer: number | null = null
|
|
284
|
-
|
|
285
|
-
// 预设类型中文名映射
|
|
286
|
-
const typeNameMap: Record<string, string> = {
|
|
287
|
-
image: '图库图片',
|
|
288
|
-
gif: '动图GIF',
|
|
289
|
-
sticker: '免抠贴纸',
|
|
290
|
-
svg: '矢量图形',
|
|
291
|
-
slide: '单页模板'
|
|
292
|
-
}
|
|
293
|
-
|
|
294
286
|
// 安全的 JSON 请求辅助函数
|
|
295
287
|
async function safeFetchJson(url: string, options: RequestInit = {}): Promise<any> {
|
|
296
288
|
const response = await fetch(url, options)
|
|
@@ -309,7 +301,6 @@ async function safeFetchJson(url: string, options: RequestInit = {}): Promise<an
|
|
|
309
301
|
}
|
|
310
302
|
}
|
|
311
303
|
|
|
312
|
-
// 获取有效的公网素材搜索接口 URL(平滑兼容旧配置并默认使用 /jvs-dam/stock/search)
|
|
313
304
|
function getEffectiveStockUrl(): string {
|
|
314
305
|
let url = (materialPageApiUrl?.value && materialPageApiUrl.value.trim()) ||
|
|
315
306
|
(materialApiUrl?.value && materialApiUrl.value.trim()) ||
|
|
@@ -320,38 +311,22 @@ function getEffectiveStockUrl(): string {
|
|
|
320
311
|
return url
|
|
321
312
|
}
|
|
322
313
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
let type = item.type || defaultType || ''
|
|
314
|
+
function normalizeMaterialItem(item: any, defaultSubType: StockSubType = 'Photos'): MaterialItem {
|
|
315
|
+
const subType = (item.assetSubType || item.subType || defaultSubType) as StockSubType
|
|
316
|
+
let type = item.type || ''
|
|
327
317
|
if (!type) {
|
|
328
|
-
if (subType === '
|
|
329
|
-
else if (subType === 'Icons') type = 'sticker'
|
|
318
|
+
if (subType === 'Videos' || item.assetType === 'VIDEO') type = 'video'
|
|
330
319
|
else if (subType === 'Vectors') type = 'svg'
|
|
331
|
-
else if (subType === 'Photos' || subType === 'Illustrations') type = 'image'
|
|
332
|
-
else if (item.assetType === 'VIDEO') type = 'video'
|
|
333
320
|
else type = 'image'
|
|
334
321
|
}
|
|
335
322
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
if (type === 'slide') {
|
|
341
|
-
if (!previewUrl || previewUrl.includes('gold-cover.png')) {
|
|
342
|
-
previewUrl = DEFAULT_SLIDE_COVER
|
|
343
|
-
}
|
|
344
|
-
if (!url || url.includes('gold-cover.png')) {
|
|
345
|
-
url = DEFAULT_SLIDE_COVER
|
|
346
|
-
}
|
|
347
|
-
}
|
|
323
|
+
const previewUrl = item.previewUrl || item.image || item.coverUrl || item.thumbnail || item.url || item.downloadUrl || item.sourceUrl || ''
|
|
324
|
+
const sourceUrl = item.sourceUrl || item.downloadUrl || item.url || previewUrl
|
|
325
|
+
const url = sourceUrl || previewUrl
|
|
348
326
|
|
|
349
|
-
// 来源平台识别(如 PEXELS, PIXABAY, UNSPLASH, ICONIFY 等)
|
|
350
327
|
const platform = item.sourcePlatform || (item.id && typeof item.id === 'string' && item.id.includes('_') ? item.id.split('_')[0].toUpperCase() : '')
|
|
351
|
-
|
|
352
328
|
const fmt = item.format || getItemFormat({ ...item, url, previewUrl, sourceUrl, type, assetSubType: subType })
|
|
353
329
|
|
|
354
|
-
// 宽高比解析
|
|
355
330
|
let ratio = item.aspectRatio ? String(item.aspectRatio).replace(':', '/') : undefined
|
|
356
331
|
if (!ratio && item.width && item.height) {
|
|
357
332
|
ratio = `${item.width} / ${item.height}`
|
|
@@ -360,12 +335,13 @@ function normalizeMaterialItem(item: any, defaultCatName = '', defaultType = '')
|
|
|
360
335
|
return {
|
|
361
336
|
...item,
|
|
362
337
|
id: item.id || `stock-${Math.random().toString(36).slice(2, 9)}`,
|
|
363
|
-
title: item.title || `${platform ? `${platform} ` : ''}${subType
|
|
338
|
+
title: item.title || `${platform ? `${platform} ` : ''}${subTypeNameMap[subType] || '素材'}`,
|
|
364
339
|
type,
|
|
365
340
|
assetSubType: subType,
|
|
341
|
+
subType,
|
|
366
342
|
sourcePlatform: platform,
|
|
367
343
|
format: fmt,
|
|
368
|
-
category: item.category ||
|
|
344
|
+
category: item.category || subTypeNameMap[subType] || '素材',
|
|
369
345
|
previewUrl,
|
|
370
346
|
url,
|
|
371
347
|
sourceUrl,
|
|
@@ -378,8 +354,7 @@ function normalizeMaterialItem(item: any, defaultCatName = '', defaultType = '')
|
|
|
378
354
|
}
|
|
379
355
|
}
|
|
380
356
|
|
|
381
|
-
|
|
382
|
-
function parseMaterialList(data: any): MaterialItem[] {
|
|
357
|
+
function parseMaterialList(data: any, defaultSubType: StockSubType = 'Photos'): MaterialItem[] {
|
|
383
358
|
if (!data) return []
|
|
384
359
|
let rawList: any[] = []
|
|
385
360
|
if (Array.isArray(data)) rawList = data
|
|
@@ -387,32 +362,199 @@ function parseMaterialList(data: any): MaterialItem[] {
|
|
|
387
362
|
else if (Array.isArray(data?.records)) rawList = data.records
|
|
388
363
|
else if (Array.isArray(data?.data?.list)) rawList = data.data.list
|
|
389
364
|
else if (Array.isArray(data?.list)) rawList = data.list
|
|
390
|
-
else if (Array.isArray(data?.templates)) rawList = data.templates
|
|
391
|
-
else if (Array.isArray(data?.data?.templates)) rawList = data.data.templates
|
|
392
365
|
else if (Array.isArray(data?.data)) {
|
|
393
366
|
if (data.data.length > 0 && Array.isArray(data.data[0]?.items)) {
|
|
394
367
|
return data.data.flatMap((cat: any) =>
|
|
395
|
-
(cat.items || []).map((item: any) => normalizeMaterialItem(item,
|
|
368
|
+
(cat.items || []).map((item: any) => normalizeMaterialItem(item, defaultSubType))
|
|
396
369
|
)
|
|
397
370
|
}
|
|
398
371
|
rawList = data.data
|
|
399
372
|
}
|
|
400
|
-
return rawList.map(item => normalizeMaterialItem(item))
|
|
373
|
+
return rawList.map(item => normalizeMaterialItem(item, defaultSubType))
|
|
401
374
|
}
|
|
402
375
|
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
376
|
+
// ============================================================================
|
|
377
|
+
// 图二:概览页 4 大固定类型(Vectors / Photos / Illustrations / Videos),每个类型独立请求一次接口
|
|
378
|
+
// ============================================================================
|
|
379
|
+
const overviewItemsMap = reactive<Record<StockSubType, MaterialItem[]>>({
|
|
380
|
+
Vectors: [],
|
|
381
|
+
Photos: [],
|
|
382
|
+
Illustrations: [],
|
|
383
|
+
Videos: []
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const overviewLoadingMap = reactive<Record<StockSubType, boolean>>({
|
|
387
|
+
Vectors: false,
|
|
388
|
+
Photos: false,
|
|
389
|
+
Illustrations: false,
|
|
390
|
+
Videos: false
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const rowRefs = reactive<Record<string, HTMLElement | null>>({})
|
|
394
|
+
const rowScrollState = reactive<Record<StockSubType, { canLeft: boolean; canRight: boolean }>>({
|
|
395
|
+
Vectors: { canLeft: false, canRight: false },
|
|
396
|
+
Photos: { canLeft: false, canRight: false },
|
|
397
|
+
Illustrations: { canLeft: false, canRight: false },
|
|
398
|
+
Videos: { canLeft: false, canRight: false }
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
function setRowRef(key: StockSubType, el: any) {
|
|
402
|
+
rowRefs[key] = el as HTMLElement | null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function updateRowScrollState(key: StockSubType) {
|
|
406
|
+
const el = rowRefs[key]
|
|
407
|
+
if (!el) return
|
|
408
|
+
const canLeft = el.scrollLeft > 4
|
|
409
|
+
const canRight = el.scrollWidth - el.clientWidth - el.scrollLeft > 4
|
|
410
|
+
rowScrollState[key] = { canLeft, canRight }
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function scrollCategoryRow(key: StockSubType, dir: 'left' | 'right') {
|
|
414
|
+
const el = rowRefs[key]
|
|
415
|
+
if (!el) return
|
|
416
|
+
const delta = dir === 'left' ? -180 : 180
|
|
417
|
+
el.scrollBy({ left: delta, behavior: 'smooth' })
|
|
418
|
+
setTimeout(() => updateRowScrollState(key), 260)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 针对单个 subType 发起独立 API 请求
|
|
422
|
+
async function fetchOverviewCategory(cat: OverviewCategoryConfig, keyword = '') {
|
|
423
|
+
overviewLoadingMap[cat.key] = true
|
|
424
|
+
try {
|
|
425
|
+
const stockUrl = getEffectiveStockUrl()
|
|
426
|
+
if (!stockUrl) {
|
|
427
|
+
overviewItemsMap[cat.key] = []
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const params = new URLSearchParams()
|
|
432
|
+
params.append('current', '1')
|
|
433
|
+
params.append('size', '12')
|
|
434
|
+
params.append('subType', cat.subType)
|
|
435
|
+
const trimmedKeyword = keyword.trim()
|
|
436
|
+
if (trimmedKeyword) {
|
|
437
|
+
params.append('keyword', trimmedKeyword)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const fullUrl = `${stockUrl}${stockUrl.includes('?') ? '&' : '?'}${params.toString()}`
|
|
441
|
+
const data = await safeFetchJson(fullUrl, {
|
|
442
|
+
headers: {
|
|
443
|
+
...(materialHeaders.value || {})
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
if (data && data.code !== undefined && data.code !== 0 && data.code !== 200) {
|
|
447
|
+
overviewItemsMap[cat.key] = []
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
const list = parseMaterialList(data, cat.subType)
|
|
451
|
+
overviewItemsMap[cat.key] = list.slice(0, 12)
|
|
452
|
+
} catch (err) {
|
|
453
|
+
overviewItemsMap[cat.key] = []
|
|
454
|
+
} finally {
|
|
455
|
+
overviewLoadingMap[cat.key] = false
|
|
456
|
+
nextTick(() => {
|
|
457
|
+
updateRowScrollState(cat.key)
|
|
458
|
+
setTimeout(() => updateRowScrollState(cat.key), 300)
|
|
459
|
+
})
|
|
409
460
|
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 遍历 4 种固定类型,每一个类型单独请求一次接口
|
|
464
|
+
function fetchAllOverviewCategories(keyword = overviewQuery.value) {
|
|
465
|
+
overviewCategories.forEach(cat => {
|
|
466
|
+
fetchOverviewCategory(cat, keyword)
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// 图三:详情页标签栏滚动控制与双列瀑布流加载(带 subType 请求参数)
|
|
472
|
+
// ============================================================================
|
|
473
|
+
const tagTrackRef = ref<HTMLElement | null>(null)
|
|
474
|
+
const tagScrollState = reactive({
|
|
475
|
+
canLeft: false,
|
|
476
|
+
canRight: false
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
function updateTagScrollState() {
|
|
480
|
+
const el = tagTrackRef.value
|
|
481
|
+
if (!el) return
|
|
482
|
+
tagScrollState.canLeft = el.scrollLeft > 4
|
|
483
|
+
tagScrollState.canRight = el.scrollWidth - el.clientWidth - el.scrollLeft > 4
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function scrollTagRow(dir: 'left' | 'right') {
|
|
487
|
+
const el = tagTrackRef.value
|
|
488
|
+
if (!el) return
|
|
489
|
+
const delta = dir === 'left' ? -140 : 140
|
|
490
|
+
el.scrollBy({ left: delta, behavior: 'smooth' })
|
|
491
|
+
setTimeout(() => updateTagScrollState(), 250)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// 从图二点击「查看全部」进入图三
|
|
495
|
+
async function openCategoryDetail(subType: StockSubType) {
|
|
496
|
+
const cat = overviewCategories.find(c => c.subType === subType)
|
|
497
|
+
filterParams.activeSubType = subType
|
|
498
|
+
const q = overviewQuery.value.trim()
|
|
499
|
+
baseDetailTitle.value = cat ? cat.label : subTypeNameMap[subType]
|
|
500
|
+
baseSearchQuery.value = q
|
|
501
|
+
activeTag.value = ''
|
|
502
|
+
filterParams.query = q
|
|
503
|
+
viewMode.value = 'detail'
|
|
504
|
+
currentPage.value = 1
|
|
505
|
+
pagedMaterials.value = []
|
|
506
|
+
reachEnd.value = false
|
|
507
|
+
loadFailed.value = false
|
|
508
|
+
await nextTick()
|
|
509
|
+
updateTagScrollState()
|
|
510
|
+
await fetchMaterials(1, false)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// 图二顶部搜索框回车:按关键词对 4 种固定类型分别重新请求一次接口
|
|
514
|
+
function handleOverviewSearch() {
|
|
515
|
+
fetchAllOverviewCategories(overviewQuery.value)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function clearOverviewQuery() {
|
|
519
|
+
overviewQuery.value = ''
|
|
520
|
+
fetchAllOverviewCategories('')
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 图三点击左上角返回箭头回到图二
|
|
524
|
+
function backToOverview() {
|
|
525
|
+
viewMode.value = 'overview'
|
|
526
|
+
nextTick(() => {
|
|
527
|
+
overviewCategories.forEach(cat => updateRowScrollState(cat.key))
|
|
528
|
+
})
|
|
529
|
+
}
|
|
410
530
|
|
|
531
|
+
// 图三点击细分标签胶囊
|
|
532
|
+
async function selectDetailTag(tag: string) {
|
|
533
|
+
activeTag.value = tag
|
|
534
|
+
filterParams.query = tag ? tag : baseSearchQuery.value
|
|
535
|
+
currentPage.value = 1
|
|
536
|
+
pagedMaterials.value = []
|
|
537
|
+
reachEnd.value = false
|
|
538
|
+
loadFailed.value = false
|
|
539
|
+
await fetchMaterials(1, false)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const loading = ref(false)
|
|
543
|
+
const pagedMaterials = ref<MaterialItem[]>([])
|
|
544
|
+
const currentPage = ref(1)
|
|
545
|
+
const pageSize = 24
|
|
546
|
+
|
|
547
|
+
const loadingMore = ref(false)
|
|
548
|
+
const loadFailed = ref(false)
|
|
549
|
+
const loadErrorMsg = ref('')
|
|
550
|
+
const reachEnd = ref(false)
|
|
551
|
+
|
|
552
|
+
// 图三分页加载:始终携带 subType 请求参数
|
|
553
|
+
async function fetchMaterials(page = 1, append = false) {
|
|
411
554
|
const stockUrl = getEffectiveStockUrl()
|
|
412
555
|
if (!stockUrl) {
|
|
413
556
|
if (!append) {
|
|
414
557
|
pagedMaterials.value = []
|
|
415
|
-
totalRecords.value = 0
|
|
416
558
|
}
|
|
417
559
|
reachEnd.value = true
|
|
418
560
|
return
|
|
@@ -427,17 +569,13 @@ async function fetchMaterials(page = 1, append = false) {
|
|
|
427
569
|
const params = new URLSearchParams()
|
|
428
570
|
params.append('current', String(page))
|
|
429
571
|
params.append('size', String(pageSize))
|
|
572
|
+
params.append('subType', filterParams.activeSubType)
|
|
430
573
|
|
|
431
574
|
const q = filterParams.query.trim()
|
|
432
575
|
if (q) {
|
|
433
576
|
params.append('keyword', q)
|
|
434
577
|
}
|
|
435
578
|
|
|
436
|
-
const subType = tabToSubTypeMap[filterParams.activeType]
|
|
437
|
-
if (subType) {
|
|
438
|
-
params.append('subType', subType)
|
|
439
|
-
}
|
|
440
|
-
|
|
441
579
|
const fullUrl = `${stockUrl}${stockUrl.includes('?') ? '&' : '?'}${params.toString()}`
|
|
442
580
|
const data = await safeFetchJson(fullUrl, {
|
|
443
581
|
headers: {
|
|
@@ -445,19 +583,18 @@ async function fetchMaterials(page = 1, append = false) {
|
|
|
445
583
|
}
|
|
446
584
|
})
|
|
447
585
|
|
|
448
|
-
// 针对接口返回非成功状态(如未登录 code: -2 或业务异常)
|
|
449
586
|
if (data && data.code !== undefined && data.code !== 0 && data.code !== 200) {
|
|
450
587
|
if (!append) {
|
|
451
588
|
pagedMaterials.value = []
|
|
452
|
-
totalRecords.value = 0
|
|
453
589
|
}
|
|
454
590
|
reachEnd.value = true
|
|
455
591
|
return
|
|
456
592
|
}
|
|
457
593
|
|
|
458
|
-
const newRecords = parseMaterialList(data)
|
|
594
|
+
const newRecords = parseMaterialList(data, filterParams.activeSubType)
|
|
459
595
|
|
|
460
|
-
|
|
596
|
+
// 如果本次请求返回的数据量不满 size,则证明没有下一页了
|
|
597
|
+
if (newRecords.length < pageSize) {
|
|
461
598
|
reachEnd.value = true
|
|
462
599
|
} else {
|
|
463
600
|
reachEnd.value = false
|
|
@@ -472,12 +609,10 @@ async function fetchMaterials(page = 1, append = false) {
|
|
|
472
609
|
}
|
|
473
610
|
|
|
474
611
|
currentPage.value = page
|
|
475
|
-
totalRecords.value = data?.data?.total ?? data?.total ?? (reachEnd.value ? pagedMaterials.value.length : 9999)
|
|
476
612
|
} catch (err: any) {
|
|
477
613
|
console.warn('[Stock Search] 加载公网素材出错:', err)
|
|
478
614
|
if (!append) {
|
|
479
615
|
pagedMaterials.value = []
|
|
480
|
-
totalRecords.value = 0
|
|
481
616
|
reachEnd.value = true
|
|
482
617
|
} else {
|
|
483
618
|
loadFailed.value = true
|
|
@@ -489,57 +624,51 @@ async function fetchMaterials(page = 1, append = false) {
|
|
|
489
624
|
}
|
|
490
625
|
}
|
|
491
626
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
loading.value = !append
|
|
496
|
-
loadingMore.value = append
|
|
497
|
-
loadFailed.value = false
|
|
498
|
-
loadErrorMsg.value = ''
|
|
627
|
+
const displayedMaterials = computed<MaterialItem[]>(() => {
|
|
628
|
+
return pagedMaterials.value
|
|
629
|
+
})
|
|
499
630
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
631
|
+
// 将瀑布流列表分配到左右两列(保持加载更多时的绝对稳定,不发生列跳动)
|
|
632
|
+
const waterfallColumns = computed<[MaterialItem[], MaterialItem[]]>(() => {
|
|
633
|
+
const left: MaterialItem[] = []
|
|
634
|
+
const right: MaterialItem[] = []
|
|
635
|
+
let leftHeight = 0
|
|
636
|
+
let rightHeight = 0
|
|
637
|
+
|
|
638
|
+
displayedMaterials.value.forEach((item, idx) => {
|
|
639
|
+
let ratio = 0.75
|
|
640
|
+
const w = Number(item.originalWidth || item.width)
|
|
641
|
+
const h = Number(item.originalHeight || item.height)
|
|
642
|
+
if (w > 0 && h > 0) {
|
|
643
|
+
ratio = Math.max(0.45, Math.min(1.8, h / w))
|
|
644
|
+
} else if (idx % 3 === 1) {
|
|
645
|
+
ratio = 1.25
|
|
646
|
+
} else if (idx % 4 === 2) {
|
|
647
|
+
ratio = 0.65
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (leftHeight <= rightHeight) {
|
|
651
|
+
left.push(item)
|
|
652
|
+
leftHeight += ratio
|
|
506
653
|
} else {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
reachEnd.value = true
|
|
654
|
+
right.push(item)
|
|
655
|
+
rightHeight += ratio
|
|
510
656
|
}
|
|
511
|
-
}
|
|
512
|
-
pagedMaterials.value = []
|
|
513
|
-
totalRecords.value = 0
|
|
514
|
-
reachEnd.value = true
|
|
515
|
-
} finally {
|
|
516
|
-
loading.value = false
|
|
517
|
-
loadingMore.value = false
|
|
518
|
-
}
|
|
519
|
-
}
|
|
657
|
+
})
|
|
520
658
|
|
|
521
|
-
|
|
522
|
-
const rawSourceMaterials = computed<MaterialItem[]>(() => {
|
|
523
|
-
return pagedMaterials.value
|
|
659
|
+
return [left, right]
|
|
524
660
|
})
|
|
525
661
|
|
|
526
|
-
// 是否还有更多数据可供加载(若已失败或已到底,立即返回 false,防止触底循环)
|
|
527
662
|
const hasMore = computed(() => {
|
|
528
|
-
|
|
529
|
-
if (totalRecords.value > 0) {
|
|
530
|
-
return pagedMaterials.value.length < totalRecords.value
|
|
531
|
-
}
|
|
532
|
-
return !reachEnd.value
|
|
663
|
+
return !reachEnd.value && !loadFailed.value
|
|
533
664
|
})
|
|
534
665
|
|
|
535
|
-
// 加载更多分页数据
|
|
536
666
|
async function loadMore() {
|
|
537
667
|
if (loading.value || loadingMore.value || !hasMore.value || loadFailed.value || reachEnd.value) return
|
|
538
668
|
const nextPage = currentPage.value + 1
|
|
539
669
|
await fetchMaterials(nextPage, true)
|
|
540
670
|
}
|
|
541
671
|
|
|
542
|
-
// 手动点击重试(重置失败状态并重新触发加载)
|
|
543
672
|
async function retryLoadMore() {
|
|
544
673
|
loadFailed.value = false
|
|
545
674
|
loadErrorMsg.value = ''
|
|
@@ -547,500 +676,378 @@ async function retryLoadMore() {
|
|
|
547
676
|
await fetchMaterials(targetPage, true)
|
|
548
677
|
}
|
|
549
678
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
679
|
+
function formatMaterialTitle(title?: string, fallback = '素材', maxLen = 12): string {
|
|
680
|
+
const text = (title || '').trim()
|
|
681
|
+
if (!text) return fallback
|
|
682
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 点击素材卡片插入画布(区分视频与图片/矢量/插画)
|
|
686
|
+
function handleSelectMaterial(meta: MaterialItem) {
|
|
687
|
+
const isVid = isVideoMaterial(meta)
|
|
688
|
+
const targetUrl = meta.sourceUrl || meta.url || meta.downloadUrl || meta.previewUrl
|
|
689
|
+
|
|
690
|
+
if (!targetUrl) {
|
|
691
|
+
notify('该素材暂无可用的资源地址')
|
|
553
692
|
return
|
|
554
693
|
}
|
|
555
694
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
695
|
+
let initialDimensions: { width?: number; height?: number } | undefined = undefined
|
|
696
|
+
const rawW = meta.originalWidth || meta.width
|
|
697
|
+
const rawH = meta.originalHeight || meta.height
|
|
698
|
+
if (rawW && rawH && rawW > 0 && rawH > 0) {
|
|
699
|
+
const maxWidth = 480
|
|
700
|
+
const maxHeight = 360
|
|
701
|
+
const scale = Math.min(maxWidth / rawW, maxHeight / rawH, 1)
|
|
702
|
+
initialDimensions = {
|
|
703
|
+
width: Math.round(rawW * scale),
|
|
704
|
+
height: Math.round(rawH * scale)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
560
707
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
708
|
+
if (isVid) {
|
|
709
|
+
addElement('video', targetUrl, undefined, undefined, undefined, initialDimensions)
|
|
710
|
+
notify(`已成功添加视频 "${formatMaterialTitle(meta.title, '高清视频', 12)}" 到画布`)
|
|
711
|
+
} else {
|
|
712
|
+
addElement('image', targetUrl, undefined, undefined, undefined, initialDimensions)
|
|
713
|
+
notify(`已成功添加素材 "${formatMaterialTitle(meta.title, '图片', 12)}" 到画布`)
|
|
565
714
|
}
|
|
566
715
|
}
|
|
567
716
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// 1. 类型过滤 (红框2)
|
|
572
|
-
if (filterParams.activeType !== 'all') {
|
|
573
|
-
if (filterParams.activeType === 'slide') {
|
|
574
|
-
if (item.type !== 'slide' && !item.slide) return false
|
|
575
|
-
} else {
|
|
576
|
-
const subType = tabToSubTypeMap[filterParams.activeType]
|
|
577
|
-
if (subType) {
|
|
578
|
-
if (item.assetSubType && item.assetSubType !== subType) return false
|
|
579
|
-
if (!item.assetSubType && item.type !== filterParams.activeType) return false
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// 2. 格式过滤 (红框3)
|
|
584
|
-
if (filterParams.format) {
|
|
585
|
-
const fmt = getItemFormat(item)
|
|
586
|
-
if (fmt.toUpperCase() !== filterParams.format.toUpperCase()) return false
|
|
587
|
-
}
|
|
588
|
-
// 3. 颜色过滤 (红框3)
|
|
589
|
-
if (filterParams.color) {
|
|
590
|
-
const c = (item.color || item.primaryColor || '').toLowerCase()
|
|
591
|
-
if (!c || c !== filterParams.color.toLowerCase()) return false
|
|
592
|
-
}
|
|
593
|
-
return true
|
|
594
|
-
})
|
|
595
|
-
})
|
|
717
|
+
function handleCloseDrawer() {
|
|
718
|
+
activeTab.value = null
|
|
719
|
+
}
|
|
596
720
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
return match ? match.label : '综合排序'
|
|
721
|
+
onMounted(() => {
|
|
722
|
+
fetchAllOverviewCategories()
|
|
723
|
+
window.addEventListener('keydown', handleKeydown)
|
|
601
724
|
})
|
|
602
725
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const match = colorOptions.find(c => c.hex.toLowerCase() === filterParams.color.toLowerCase())
|
|
606
|
-
return match ? match.name : '已选色'
|
|
726
|
+
onUnmounted(() => {
|
|
727
|
+
window.removeEventListener('keydown', handleKeydown)
|
|
607
728
|
})
|
|
608
729
|
|
|
609
|
-
|
|
610
|
-
|
|
730
|
+
watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => materialTreeApiUrl?.value], () => {
|
|
731
|
+
fetchAllOverviewCategories()
|
|
732
|
+
if (viewMode.value === 'detail') {
|
|
733
|
+
fetchMaterials(1, false)
|
|
734
|
+
}
|
|
611
735
|
})
|
|
736
|
+
</script>
|
|
612
737
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
738
|
+
<template>
|
|
739
|
+
<div class="material-drawer-container">
|
|
740
|
+
<!-- 顶部抽屉标题栏:图二显示「素材库」,图三显示「← 分类/搜索词」 -->
|
|
741
|
+
<div class="material-drawer-header">
|
|
742
|
+
<div v-if="viewMode === 'overview'" class="drawer-header-left">
|
|
743
|
+
<span class="drawer-header-title">素材库</span>
|
|
744
|
+
</div>
|
|
745
|
+
<button v-else type="button" class="drawer-back-btn" @click="backToOverview">
|
|
746
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"
|
|
747
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
748
|
+
<line x1="19" y1="12" x2="5" y2="12" />
|
|
749
|
+
<polyline points="12 19 5 12 12 5" />
|
|
750
|
+
</svg>
|
|
751
|
+
<span class="drawer-header-title">{{ detailHeaderTitle }}</span>
|
|
752
|
+
</button>
|
|
753
|
+
|
|
754
|
+
<button type="button" class="drawer-close-btn" title="关闭面板" @click="handleCloseDrawer">
|
|
755
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"
|
|
756
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
757
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
758
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
759
|
+
</svg>
|
|
760
|
+
</button>
|
|
761
|
+
</div>
|
|
622
762
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
763
|
+
<!-- ==================== 图二:素材库分类概览视图 ==================== -->
|
|
764
|
+
<div v-if="viewMode === 'overview'" class="material-overview-body">
|
|
765
|
+
<!-- 搜索框 -->
|
|
766
|
+
<div class="overview-search-wrap">
|
|
767
|
+
<div class="overview-search-box">
|
|
768
|
+
<el-icon class="overview-search-icon" @click="handleOverviewSearch">
|
|
769
|
+
<Search />
|
|
770
|
+
</el-icon>
|
|
771
|
+
<input
|
|
772
|
+
v-model="overviewQuery"
|
|
773
|
+
type="text"
|
|
774
|
+
placeholder="搜索"
|
|
775
|
+
class="overview-search-input"
|
|
776
|
+
@keyup.enter="handleOverviewSearch"
|
|
777
|
+
/>
|
|
778
|
+
<el-icon
|
|
779
|
+
v-if="overviewQuery"
|
|
780
|
+
class="overview-search-clear"
|
|
781
|
+
@click="clearOverviewQuery"
|
|
782
|
+
>
|
|
783
|
+
<CircleCloseFilled />
|
|
784
|
+
</el-icon>
|
|
785
|
+
</div>
|
|
786
|
+
</div>
|
|
631
787
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
788
|
+
<!-- 4大固定分类横向预览列表(Vectors / Photos / Illustrations / Videos) -->
|
|
789
|
+
<div class="overview-sections-scroll">
|
|
790
|
+
<div
|
|
791
|
+
v-for="cat in overviewCategories"
|
|
792
|
+
:key="cat.key"
|
|
793
|
+
class="overview-category-section"
|
|
794
|
+
>
|
|
795
|
+
<div class="section-header-row">
|
|
796
|
+
<span class="section-cat-title">{{ cat.label }}</span>
|
|
797
|
+
<button type="button" class="section-view-all-btn" @click="openCategoryDetail(cat.subType)">
|
|
798
|
+
查看全部
|
|
799
|
+
</button>
|
|
800
|
+
</div>
|
|
636
801
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
802
|
+
<div class="section-carousel-container">
|
|
803
|
+
<div
|
|
804
|
+
:ref="el => setRowRef(cat.key, el)"
|
|
805
|
+
class="section-carousel-track"
|
|
806
|
+
@scroll="updateRowScrollState(cat.key)"
|
|
807
|
+
>
|
|
808
|
+
<!-- 加载占位骨架 -->
|
|
809
|
+
<template v-if="overviewLoadingMap[cat.key] && (!overviewItemsMap[cat.key] || overviewItemsMap[cat.key].length === 0)">
|
|
810
|
+
<div v-for="n in 3" :key="n" class="overview-skeleton-card"></div>
|
|
811
|
+
</template>
|
|
640
812
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
813
|
+
<!-- 横向缩略图卡片 -->
|
|
814
|
+
<template v-else-if="overviewItemsMap[cat.key] && overviewItemsMap[cat.key].length > 0">
|
|
815
|
+
<div
|
|
816
|
+
v-for="(item, idx) in overviewItemsMap[cat.key]"
|
|
817
|
+
:key="item.id || idx"
|
|
818
|
+
class="overview-thumb-card"
|
|
819
|
+
@click="handleSelectMaterial(item)"
|
|
820
|
+
>
|
|
821
|
+
<video
|
|
822
|
+
v-if="isVideoUrl(getMaterialThumb(item))"
|
|
823
|
+
:src="getMaterialThumb(item)"
|
|
824
|
+
muted
|
|
825
|
+
preload="metadata"
|
|
826
|
+
playsinline
|
|
827
|
+
class="overview-thumb-img"
|
|
828
|
+
@loadeddata="updateRowScrollState(cat.key)"
|
|
829
|
+
@mouseenter="($event.target as HTMLVideoElement)?.play?.()"
|
|
830
|
+
@mouseleave="($event.target as HTMLVideoElement)?.pause?.()"
|
|
831
|
+
/>
|
|
832
|
+
<img
|
|
833
|
+
v-else-if="getMaterialThumb(item)"
|
|
834
|
+
:src="getMaterialThumb(item)"
|
|
835
|
+
:alt="item.title || cat.label"
|
|
836
|
+
loading="lazy"
|
|
837
|
+
class="overview-thumb-img"
|
|
838
|
+
@load="updateRowScrollState(cat.key)"
|
|
839
|
+
/>
|
|
840
|
+
<span v-if="cat.subType === 'Videos' || isVideoMaterial(item)" class="video-badge-icon">
|
|
841
|
+
<el-icon><VideoPlay /></el-icon>
|
|
842
|
+
</span>
|
|
843
|
+
</div>
|
|
844
|
+
</template>
|
|
644
845
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
filterParams.sort = 'recommend'
|
|
651
|
-
filterParams.format = ''
|
|
652
|
-
filterParams.color = ''
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function openItemMenu(item: MaterialItem) {
|
|
656
|
-
notify(`素材: ${item.title || '无标题'} (${getItemFormat(item)})`)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// 点击使用/插入素材至画布
|
|
660
|
-
function handleSelectMaterial(meta: MaterialItem) {
|
|
661
|
-
if (meta.type === 'slide' || meta.slide) {
|
|
662
|
-
handleInsertSlide(meta)
|
|
663
|
-
} else {
|
|
664
|
-
// 优先采用高清 sourceUrl,回退到 url / previewUrl / downloadUrl
|
|
665
|
-
const imgUrl = meta.sourceUrl || meta.url || meta.previewUrl || meta.downloadUrl
|
|
666
|
-
if (imgUrl) {
|
|
667
|
-
// 若包含长宽信息,按比例设定元素尺寸插入画布
|
|
668
|
-
let initialDimensions: { width?: number; height?: number } | undefined = undefined
|
|
669
|
-
const rawW = meta.originalWidth || meta.width
|
|
670
|
-
const rawH = meta.originalHeight || meta.height
|
|
671
|
-
if (rawW && rawH && rawW > 0 && rawH > 0) {
|
|
672
|
-
const maxWidth = 480
|
|
673
|
-
const maxHeight = 360
|
|
674
|
-
const scale = Math.min(maxWidth / rawW, maxHeight / rawH, 1)
|
|
675
|
-
initialDimensions = {
|
|
676
|
-
width: Math.round(rawW * scale),
|
|
677
|
-
height: Math.round(rawH * scale)
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
addElement('image', imgUrl, undefined, undefined, undefined, initialDimensions)
|
|
681
|
-
notify(`已成功添加素材 "${meta.title || '图片'}" 到画布`)
|
|
682
|
-
} else {
|
|
683
|
-
notify('该素材暂无可用的图片地址')
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// 异步加载单页物理 JSON 并执行插入
|
|
689
|
-
async function handleInsertSlide(meta: MaterialItem) {
|
|
690
|
-
try {
|
|
691
|
-
notify(`正在读取页面素材 [${meta.title || '单页模板'}]...`)
|
|
692
|
-
const fallbackSize = (typeof meta.originalWidth === 'number' && typeof meta.originalHeight === 'number')
|
|
693
|
-
? { width: meta.originalWidth, height: meta.originalHeight }
|
|
694
|
-
: undefined
|
|
695
|
-
const inlineSize = meta.originalSize || fallbackSize
|
|
696
|
-
if (meta.slide) {
|
|
697
|
-
insertExternalSlide(meta.slide, inlineSize)
|
|
698
|
-
return
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
let detailUrl = meta.url || meta.downloadUrl || ''
|
|
702
|
-
if (!detailUrl && meta.fileName) {
|
|
703
|
-
const baseUrl = (materialApiUrl.value && materialApiUrl.value.trim()) || DEFAULT_MATERIAL_API_URL
|
|
704
|
-
if (baseUrl.includes('/') && baseUrl.endsWith('.json')) {
|
|
705
|
-
const dir = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1)
|
|
706
|
-
detailUrl = `${dir}slides/${meta.fileName}`
|
|
707
|
-
} else {
|
|
708
|
-
detailUrl = `/templates/slides/${meta.fileName}`
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (!detailUrl) {
|
|
713
|
-
throw new Error('未找到单页素材地址')
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
const data = await safeFetchJson(detailUrl, {
|
|
717
|
-
headers: {
|
|
718
|
-
...(materialHeaders.value || {})
|
|
719
|
-
}
|
|
720
|
-
})
|
|
721
|
-
const slideObj = data.slide || data.data?.slide || (data.elements ? data : null)
|
|
722
|
-
const originalSize = data.originalSize || data.data?.originalSize || fallbackSize
|
|
723
|
-
|
|
724
|
-
if (!slideObj) {
|
|
725
|
-
throw new Error('单页数据格式异常')
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
insertExternalSlide(slideObj, originalSize)
|
|
729
|
-
notify(`已成功插入单页模板 [${meta.title || '素材'}]`)
|
|
730
|
-
} catch (err: any) {
|
|
731
|
-
console.error('加载单页模板失败:', err)
|
|
732
|
-
notify(err.message || '加载单页模板失败')
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
onMounted(() => {
|
|
737
|
-
fetchMaterials(1, false)
|
|
738
|
-
|
|
739
|
-
// 监听容器大小以实现极致自适应
|
|
740
|
-
if (containerRef.value) {
|
|
741
|
-
resizeObserver = new ResizeObserver((entries) => {
|
|
742
|
-
for (const entry of entries) {
|
|
743
|
-
isNarrow.value = entry.contentRect.width < 520
|
|
744
|
-
}
|
|
745
|
-
})
|
|
746
|
-
resizeObserver.observe(containerRef.value)
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// 键盘快捷键监听
|
|
750
|
-
window.addEventListener('keydown', handleKeydown)
|
|
751
|
-
})
|
|
752
|
-
|
|
753
|
-
onUnmounted(() => {
|
|
754
|
-
if (resizeObserver) {
|
|
755
|
-
resizeObserver.disconnect()
|
|
756
|
-
resizeObserver = null
|
|
757
|
-
}
|
|
758
|
-
window.removeEventListener('keydown', handleKeydown)
|
|
759
|
-
})
|
|
846
|
+
<!-- 空状态兜底 -->
|
|
847
|
+
<div v-else class="overview-empty-row" @click="openCategoryDetail(cat.subType)">
|
|
848
|
+
<span>暂无{{ cat.label }}数据</span>
|
|
849
|
+
</div>
|
|
850
|
+
</div>
|
|
760
851
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
852
|
+
<!-- 左侧渐变与左滑箭头 -->
|
|
853
|
+
<div
|
|
854
|
+
v-if="rowScrollState[cat.key]?.canLeft"
|
|
855
|
+
class="carousel-edge-mask left-mask"
|
|
856
|
+
@click.stop="scrollCategoryRow(cat.key, 'left')"
|
|
857
|
+
>
|
|
858
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2"
|
|
859
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
860
|
+
<polyline points="15 18 9 12 15 6" />
|
|
861
|
+
</svg>
|
|
862
|
+
</div>
|
|
765
863
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
:placeholder="isNarrow ? '搜索素材、关键词...' : '搜索素材标题、关键词,按回车检索...'"
|
|
780
|
-
class="native-search-input"
|
|
781
|
-
@keyup.enter="handleSearch"
|
|
782
|
-
/>
|
|
783
|
-
|
|
784
|
-
<!-- 清空图标 -->
|
|
785
|
-
<el-icon
|
|
786
|
-
v-if="filterParams.query"
|
|
787
|
-
class="search-clear-icon"
|
|
788
|
-
@click="clearQuery"
|
|
789
|
-
>
|
|
790
|
-
<CircleCloseFilled />
|
|
791
|
-
</el-icon>
|
|
792
|
-
|
|
793
|
-
<!-- 搜索胶囊按钮(窄屏下仅显示放大镜图标,腾出输入空间) -->
|
|
794
|
-
<button
|
|
795
|
-
type="button"
|
|
796
|
-
class="capsule-search-btn"
|
|
797
|
-
@click="handleSearch"
|
|
798
|
-
:disabled="loading"
|
|
799
|
-
:title="isNarrow ? '搜索素材' : ''"
|
|
800
|
-
>
|
|
801
|
-
<el-icon v-if="!loading"><Search /></el-icon>
|
|
802
|
-
<span v-else class="btn-spinner-ring"></span>
|
|
803
|
-
<span class="btn-text">搜索</span>
|
|
804
|
-
</button>
|
|
864
|
+
<!-- 右侧渐变与右滑箭头 -->
|
|
865
|
+
<div
|
|
866
|
+
v-if="rowScrollState[cat.key]?.canRight"
|
|
867
|
+
class="carousel-edge-mask right-mask"
|
|
868
|
+
@click.stop="scrollCategoryRow(cat.key, 'right')"
|
|
869
|
+
>
|
|
870
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2"
|
|
871
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
872
|
+
<polyline points="9 18 15 12 9 6" />
|
|
873
|
+
</svg>
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
805
877
|
</div>
|
|
806
878
|
</div>
|
|
807
879
|
|
|
808
|
-
<!--
|
|
809
|
-
<div class="
|
|
810
|
-
<!--
|
|
811
|
-
<div class="
|
|
812
|
-
<
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
:class="{ active: filterParams.activeType === tab.value }"
|
|
817
|
-
@click="switchCategoryTab(tab.value)"
|
|
880
|
+
<!-- ==================== 图三:分类/搜索详情瀑布流视图 ==================== -->
|
|
881
|
+
<div v-else class="material-detail-body">
|
|
882
|
+
<!-- 顶部横向细分标签栏 -->
|
|
883
|
+
<div class="detail-tags-bar-wrap">
|
|
884
|
+
<div
|
|
885
|
+
ref="tagTrackRef"
|
|
886
|
+
class="detail-tags-track"
|
|
887
|
+
@scroll="updateTagScrollState"
|
|
818
888
|
>
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
889
|
+
<button
|
|
890
|
+
type="button"
|
|
891
|
+
class="detail-tag-pill"
|
|
892
|
+
:class="{ active: activeTag === '' }"
|
|
893
|
+
@click="selectDetailTag('')"
|
|
894
|
+
>
|
|
895
|
+
全部
|
|
896
|
+
</button>
|
|
897
|
+
<button
|
|
898
|
+
v-for="tag in currentCategoryTags"
|
|
899
|
+
:key="tag"
|
|
900
|
+
type="button"
|
|
901
|
+
class="detail-tag-pill"
|
|
902
|
+
:class="{ active: activeTag === tag }"
|
|
903
|
+
@click="selectDetailTag(tag)"
|
|
904
|
+
>
|
|
905
|
+
{{ tag }}
|
|
906
|
+
</button>
|
|
907
|
+
</div>
|
|
822
908
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
</el-dropdown-item>
|
|
841
|
-
</el-dropdown-menu>
|
|
842
|
-
</template>
|
|
843
|
-
</el-dropdown>
|
|
844
|
-
|
|
845
|
-
<!-- 格式筛选 -->
|
|
846
|
-
<el-dropdown trigger="click" @command="handleFormatChange">
|
|
847
|
-
<span class="toolbar-dropdown-btn" :class="{ 'is-active': !!filterParams.format }">
|
|
848
|
-
{{ filterParams.format ? `格式: ${filterParams.format}` : '格式' }}
|
|
849
|
-
<el-icon class="dropdown-arrow"><ArrowDown /></el-icon>
|
|
850
|
-
</span>
|
|
851
|
-
<template #dropdown>
|
|
852
|
-
<el-dropdown-menu class="custom-dropdown-menu">
|
|
853
|
-
<el-dropdown-item command="" :class="{ 'is-selected': !filterParams.format }">
|
|
854
|
-
全部格式
|
|
855
|
-
</el-dropdown-item>
|
|
856
|
-
<el-dropdown-item
|
|
857
|
-
v-for="fmt in formatOptions"
|
|
858
|
-
:key="fmt"
|
|
859
|
-
:command="fmt"
|
|
860
|
-
:class="{ 'is-selected': filterParams.format === fmt }"
|
|
861
|
-
>
|
|
862
|
-
{{ fmt }}
|
|
863
|
-
</el-dropdown-item>
|
|
864
|
-
</el-dropdown-menu>
|
|
865
|
-
</template>
|
|
866
|
-
</el-dropdown>
|
|
867
|
-
|
|
868
|
-
<!-- 颜色筛选 -->
|
|
869
|
-
<el-dropdown trigger="click" @command="handleColorChange">
|
|
870
|
-
<span class="toolbar-dropdown-btn" :class="{ 'is-active': !!filterParams.color }">
|
|
871
|
-
<span v-if="filterParams.color" class="color-badge-dot" :style="{ backgroundColor: filterParams.color }"></span>
|
|
872
|
-
{{ currentColorLabel }}
|
|
873
|
-
<el-icon class="dropdown-arrow"><ArrowDown /></el-icon>
|
|
874
|
-
</span>
|
|
875
|
-
<template #dropdown>
|
|
876
|
-
<div class="color-picker-palette">
|
|
877
|
-
<div class="palette-header-label">按主色调筛选</div>
|
|
878
|
-
<div class="palette-swatch-grid">
|
|
879
|
-
<div
|
|
880
|
-
class="swatch-item all-swatch"
|
|
881
|
-
:class="{ active: !filterParams.color }"
|
|
882
|
-
@click="handleColorChange('')"
|
|
883
|
-
title="全部颜色"
|
|
884
|
-
>
|
|
885
|
-
全部
|
|
886
|
-
</div>
|
|
887
|
-
<div
|
|
888
|
-
v-for="color in colorOptions"
|
|
889
|
-
:key="color.hex"
|
|
890
|
-
class="swatch-item"
|
|
891
|
-
:class="{ active: filterParams.color === color.hex }"
|
|
892
|
-
:style="{ backgroundColor: color.hex }"
|
|
893
|
-
:title="color.name"
|
|
894
|
-
@click="handleColorChange(color.hex)"
|
|
895
|
-
></div>
|
|
896
|
-
</div>
|
|
897
|
-
</div>
|
|
898
|
-
</template>
|
|
899
|
-
</el-dropdown>
|
|
900
|
-
|
|
901
|
-
<!-- 一键重置 -->
|
|
902
|
-
<button
|
|
903
|
-
v-if="hasActiveFilters"
|
|
904
|
-
class="reset-filters-link"
|
|
905
|
-
@click="resetFilters"
|
|
906
|
-
title="清除所有筛选"
|
|
909
|
+
<!-- 标签栏左侧滚动箭头 -->
|
|
910
|
+
<div
|
|
911
|
+
v-if="tagScrollState.canLeft"
|
|
912
|
+
class="tags-edge-arrow left-arrow"
|
|
913
|
+
@click.stop="scrollTagRow('left')"
|
|
914
|
+
>
|
|
915
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2"
|
|
916
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
917
|
+
<polyline points="15 18 9 12 15 6" />
|
|
918
|
+
</svg>
|
|
919
|
+
</div>
|
|
920
|
+
|
|
921
|
+
<!-- 标签栏右侧滚动箭头 -->
|
|
922
|
+
<div
|
|
923
|
+
v-if="tagScrollState.canRight"
|
|
924
|
+
class="tags-edge-arrow right-arrow"
|
|
925
|
+
@click.stop="scrollTagRow('right')"
|
|
907
926
|
>
|
|
908
|
-
|
|
909
|
-
|
|
927
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2"
|
|
928
|
+
stroke-linecap="round" stroke-linejoin="round">
|
|
929
|
+
<polyline points="9 18 15 12 9 6" />
|
|
930
|
+
</svg>
|
|
931
|
+
</div>
|
|
910
932
|
</div>
|
|
911
|
-
</div>
|
|
912
933
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
934
|
+
<!-- 加载中状态 -->
|
|
935
|
+
<div v-if="loading && displayedMaterials.length === 0" class="gallery-loading-box">
|
|
936
|
+
<div class="loading-ring-spinner"></div>
|
|
937
|
+
<span class="loading-tip-text">正在加载{{ subTypeNameMap[filterParams.activeSubType] }}...</span>
|
|
938
|
+
</div>
|
|
918
939
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
940
|
+
<!-- 双列瀑布流列表 -->
|
|
941
|
+
<div v-else class="detail-waterfall-scroll">
|
|
942
|
+
<div v-if="displayedMaterials.length > 0" class="detail-masonry-wrapper">
|
|
943
|
+
<div class="detail-masonry-columns">
|
|
944
|
+
<div
|
|
945
|
+
v-for="(colItems, colIdx) in waterfallColumns"
|
|
946
|
+
:key="colIdx"
|
|
947
|
+
class="detail-masonry-col"
|
|
948
|
+
>
|
|
949
|
+
<div
|
|
950
|
+
v-for="(item, idx) in colItems"
|
|
951
|
+
:key="item.id || idx"
|
|
952
|
+
class="detail-masonry-card"
|
|
953
|
+
@click="handleSelectMaterial(item)"
|
|
954
|
+
>
|
|
955
|
+
<video
|
|
956
|
+
v-if="isVideoUrl(getMaterialThumb(item))"
|
|
957
|
+
:src="getMaterialThumb(item)"
|
|
958
|
+
muted
|
|
959
|
+
preload="metadata"
|
|
960
|
+
playsinline
|
|
961
|
+
class="detail-card-img"
|
|
962
|
+
@mouseenter="($event.target as HTMLVideoElement)?.play?.()"
|
|
963
|
+
@mouseleave="($event.target as HTMLVideoElement)?.pause?.()"
|
|
938
964
|
/>
|
|
939
|
-
<
|
|
940
|
-
|
|
965
|
+
<img
|
|
966
|
+
v-else-if="getMaterialThumb(item)"
|
|
967
|
+
:src="getMaterialThumb(item)"
|
|
968
|
+
:alt="item.title || '素材'"
|
|
969
|
+
loading="lazy"
|
|
970
|
+
class="detail-card-img"
|
|
971
|
+
/>
|
|
972
|
+
<div v-else class="detail-card-placeholder">
|
|
973
|
+
<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#94a3b8" stroke-width="1.5">
|
|
941
974
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
942
975
|
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
943
976
|
<polyline points="21 15 16 10 5 21" />
|
|
944
977
|
</svg>
|
|
945
978
|
</div>
|
|
946
979
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
v-if="getMaterialThumb(item)"
|
|
951
|
-
type="button"
|
|
952
|
-
class="preview-float-btn"
|
|
953
|
-
title="大图预览"
|
|
954
|
-
@click.stop.prevent="openPreview(item, $event)"
|
|
955
|
-
>
|
|
956
|
-
<el-icon><View /></el-icon>
|
|
957
|
-
</button>
|
|
958
|
-
<div class="insert-float-action">
|
|
959
|
-
<div class="insert-circle-btn">
|
|
960
|
-
<el-icon><Plus /></el-icon>
|
|
961
|
-
</div>
|
|
962
|
-
<span class="insert-tip">点击插入画布</span>
|
|
963
|
-
</div>
|
|
964
|
-
</div>
|
|
965
|
-
</div>
|
|
980
|
+
<span v-if="filterParams.activeSubType === 'Videos' || isVideoMaterial(item)" class="video-badge-icon">
|
|
981
|
+
<el-icon><VideoPlay /></el-icon>
|
|
982
|
+
</span>
|
|
966
983
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
<el-icon><
|
|
984
|
+
<!-- 悬浮预览小按钮 -->
|
|
985
|
+
<button
|
|
986
|
+
v-if="getMaterialThumb(item)"
|
|
987
|
+
type="button"
|
|
988
|
+
class="detail-card-preview-btn"
|
|
989
|
+
title="大图预览"
|
|
990
|
+
@click.stop.prevent="openPreview(item, $event)"
|
|
991
|
+
>
|
|
992
|
+
<el-icon><View /></el-icon>
|
|
976
993
|
</button>
|
|
977
994
|
</div>
|
|
978
995
|
</div>
|
|
979
996
|
</div>
|
|
980
|
-
</div>
|
|
981
|
-
|
|
982
|
-
<!-- 瀑布流底部加载更多与完成状态 -->
|
|
983
|
-
<div class="waterfall-footer-status">
|
|
984
|
-
<!-- 1. 正在加载中 -->
|
|
985
|
-
<div v-if="loadingMore" class="loading-more-row">
|
|
986
|
-
<span class="btn-spinner-ring dark"></span>
|
|
987
|
-
<span>正在加载更多素材...</span>
|
|
988
|
-
</div>
|
|
989
997
|
|
|
990
|
-
<!--
|
|
991
|
-
<div
|
|
992
|
-
<
|
|
993
|
-
|
|
994
|
-
|
|
998
|
+
<!-- 瀑布流底部状态 -->
|
|
999
|
+
<div class="waterfall-footer-status">
|
|
1000
|
+
<div v-if="loadingMore" class="loading-more-row">
|
|
1001
|
+
<span class="btn-spinner-ring dark"></span>
|
|
1002
|
+
<span>正在加载更多...</span>
|
|
1003
|
+
</div>
|
|
1004
|
+
<div v-else-if="loadFailed" class="load-failed-row">
|
|
1005
|
+
<span class="load-error-text">加载失败</span>
|
|
1006
|
+
<button type="button" class="retry-load-btn" @click="retryLoadMore">
|
|
1007
|
+
重试
|
|
1008
|
+
</button>
|
|
1009
|
+
</div>
|
|
1010
|
+
<button
|
|
1011
|
+
v-else-if="hasMore"
|
|
1012
|
+
type="button"
|
|
1013
|
+
class="load-more-action-btn"
|
|
1014
|
+
@click="loadMore"
|
|
1015
|
+
>
|
|
1016
|
+
加载更多
|
|
995
1017
|
</button>
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
<button
|
|
1000
|
-
v-else-if="hasMore"
|
|
1001
|
-
type="button"
|
|
1002
|
-
class="load-more-action-btn"
|
|
1003
|
-
@click="loadMore"
|
|
1004
|
-
>
|
|
1005
|
-
<span>加载更多素材</span>
|
|
1006
|
-
<span v-if="totalRecords > 0" class="load-more-count">({{ displayedMaterials.length }} / {{ totalRecords }})</span>
|
|
1007
|
-
</button>
|
|
1008
|
-
|
|
1009
|
-
<!-- 4. 已全部加载完毕 -->
|
|
1010
|
-
<div v-else class="all-loaded-divider">
|
|
1011
|
-
<span>— 已展示全部素材 (共 {{ displayedMaterials.length }} 项) —</span>
|
|
1018
|
+
<div v-else class="all-loaded-divider">
|
|
1019
|
+
<span>— 已加载全部 —</span>
|
|
1020
|
+
</div>
|
|
1012
1021
|
</div>
|
|
1013
1022
|
</div>
|
|
1014
|
-
</div>
|
|
1015
1023
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
+
<!-- 空状态 -->
|
|
1025
|
+
<div v-else class="gallery-empty-state">
|
|
1026
|
+
<svg viewBox="0 0 24 24" width="40" height="40" fill="none" stroke="#cbd5e1" stroke-width="1.2">
|
|
1027
|
+
<circle cx="11" cy="11" r="8" />
|
|
1028
|
+
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
1029
|
+
</svg>
|
|
1030
|
+
<p class="empty-title">{{ filterParams.query ? `未找到与 "${filterParams.query}" 相关的素材` : '暂无相关素材' }}</p>
|
|
1031
|
+
</div>
|
|
1024
1032
|
</div>
|
|
1025
1033
|
</div>
|
|
1026
1034
|
|
|
1027
|
-
<!--
|
|
1035
|
+
<!-- 全屏大图/视频预览 Modal -->
|
|
1028
1036
|
<teleport to="body">
|
|
1029
1037
|
<transition name="preview-backdrop-fade">
|
|
1030
|
-
<div
|
|
1031
|
-
v-if="previewVisible"
|
|
1038
|
+
<div
|
|
1039
|
+
v-if="previewVisible"
|
|
1032
1040
|
class="custom-fullscreen-preview-modal"
|
|
1033
1041
|
@click.self="closePreview"
|
|
1034
1042
|
>
|
|
1035
|
-
<!-- 顶部浮动信息条 -->
|
|
1036
1043
|
<div class="preview-header-bar">
|
|
1037
1044
|
<div class="preview-title-wrap">
|
|
1038
1045
|
<span class="preview-format-pill">{{ currentPreviewItem?.format || 'IMG' }}</span>
|
|
1039
1046
|
<span class="preview-title-text">{{ currentPreviewItem?.title || '素材大图预览' }}</span>
|
|
1040
1047
|
</div>
|
|
1041
|
-
<button
|
|
1042
|
-
type="button"
|
|
1043
|
-
class="preview-close-cross-btn"
|
|
1048
|
+
<button
|
|
1049
|
+
type="button"
|
|
1050
|
+
class="preview-close-cross-btn"
|
|
1044
1051
|
title="关闭预览 (Esc)"
|
|
1045
1052
|
@click="closePreview"
|
|
1046
1053
|
>
|
|
@@ -1048,45 +1055,49 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
|
|
|
1048
1055
|
</button>
|
|
1049
1056
|
</div>
|
|
1050
1057
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
class="preview-arrow-btn prev-arrow"
|
|
1058
|
+
<button
|
|
1059
|
+
v-if="previewList.length > 1"
|
|
1060
|
+
type="button"
|
|
1061
|
+
class="preview-arrow-btn prev-arrow"
|
|
1056
1062
|
title="上一张 (←)"
|
|
1057
1063
|
@click.stop="prevPreview"
|
|
1058
1064
|
>
|
|
1059
1065
|
<el-icon><ArrowLeft /></el-icon>
|
|
1060
1066
|
</button>
|
|
1061
1067
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
class="preview-arrow-btn next-arrow"
|
|
1068
|
+
<button
|
|
1069
|
+
v-if="previewList.length > 1"
|
|
1070
|
+
type="button"
|
|
1071
|
+
class="preview-arrow-btn next-arrow"
|
|
1067
1072
|
title="下一张 (→)"
|
|
1068
1073
|
@click.stop="nextPreview"
|
|
1069
1074
|
>
|
|
1070
1075
|
<el-icon><ArrowRight /></el-icon>
|
|
1071
1076
|
</button>
|
|
1072
1077
|
|
|
1073
|
-
<!-- 中央超清大图视口 -->
|
|
1074
1078
|
<div class="preview-stage-viewport" @click.self="closePreview">
|
|
1075
|
-
<
|
|
1076
|
-
v-if="currentPreviewItem?.url"
|
|
1077
|
-
:src="currentPreviewItem.url"
|
|
1078
|
-
|
|
1079
|
+
<video
|
|
1080
|
+
v-if="currentPreviewItem?.isVideo && currentPreviewItem?.url"
|
|
1081
|
+
:src="currentPreviewItem.url"
|
|
1082
|
+
controls
|
|
1083
|
+
autoplay
|
|
1084
|
+
playsinline
|
|
1085
|
+
class="preview-stage-img"
|
|
1086
|
+
/>
|
|
1087
|
+
<img
|
|
1088
|
+
v-else-if="currentPreviewItem?.url"
|
|
1089
|
+
:src="currentPreviewItem.url"
|
|
1090
|
+
:alt="currentPreviewItem.title"
|
|
1079
1091
|
class="preview-stage-img"
|
|
1080
1092
|
/>
|
|
1081
1093
|
</div>
|
|
1082
1094
|
|
|
1083
|
-
<!-- 底部工具动作条 -->
|
|
1084
1095
|
<div class="preview-footer-bar">
|
|
1085
1096
|
<span v-if="previewList.length > 1" class="preview-page-indicator">
|
|
1086
1097
|
{{ previewInitialIndex + 1 }} / {{ previewList.length }}
|
|
1087
1098
|
</span>
|
|
1088
|
-
<button
|
|
1089
|
-
type="button"
|
|
1099
|
+
<button
|
|
1100
|
+
type="button"
|
|
1090
1101
|
class="preview-quick-insert-btn"
|
|
1091
1102
|
@click="handleInsertFromPreview"
|
|
1092
1103
|
>
|
|
@@ -1101,746 +1112,609 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
|
|
|
1101
1112
|
</template>
|
|
1102
1113
|
|
|
1103
1114
|
<style scoped lang="scss">
|
|
1104
|
-
.
|
|
1115
|
+
.material-drawer-container {
|
|
1105
1116
|
display: flex;
|
|
1106
1117
|
flex-direction: column;
|
|
1118
|
+
width: 288px;
|
|
1119
|
+
max-width: 100%;
|
|
1107
1120
|
height: 100%;
|
|
1108
|
-
|
|
1121
|
+
background: #ffffff;
|
|
1109
1122
|
box-sizing: border-box;
|
|
1110
|
-
background: var(--bg-surface, #ffffff);
|
|
1111
|
-
gap: 12px;
|
|
1112
1123
|
overflow: hidden;
|
|
1124
|
+
user-select: none;
|
|
1113
1125
|
}
|
|
1114
1126
|
|
|
1115
|
-
/*
|
|
1116
|
-
.
|
|
1117
|
-
width: 100%;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
.search-input-capsule {
|
|
1127
|
+
/* 顶部栏(图二:素材库 / 图三:← 分类名) */
|
|
1128
|
+
.material-drawer-header {
|
|
1121
1129
|
display: flex;
|
|
1122
1130
|
align-items: center;
|
|
1123
|
-
|
|
1124
|
-
height:
|
|
1125
|
-
|
|
1126
|
-
border:
|
|
1127
|
-
|
|
1128
|
-
padding: 3px 4px 3px 14px;
|
|
1131
|
+
justify-content: space-between;
|
|
1132
|
+
height: 48px;
|
|
1133
|
+
padding: 0 16px;
|
|
1134
|
+
border-bottom: 1px solid #f1f5f9;
|
|
1135
|
+
flex-shrink: 0;
|
|
1129
1136
|
box-sizing: border-box;
|
|
1130
|
-
transition: all 0.25s ease;
|
|
1131
1137
|
}
|
|
1132
1138
|
|
|
1133
|
-
.
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1139
|
+
.drawer-header-left {
|
|
1140
|
+
display: flex;
|
|
1141
|
+
align-items: center;
|
|
1142
|
+
min-width: 0;
|
|
1137
1143
|
}
|
|
1138
1144
|
|
|
1145
|
+
.drawer-header-title {
|
|
1146
|
+
font-size: 14px;
|
|
1147
|
+
font-weight: 600;
|
|
1148
|
+
color: #1e293b;
|
|
1149
|
+
white-space: nowrap;
|
|
1150
|
+
overflow: hidden;
|
|
1151
|
+
text-overflow: ellipsis;
|
|
1152
|
+
}
|
|
1139
1153
|
|
|
1140
|
-
.
|
|
1141
|
-
|
|
1154
|
+
.drawer-back-btn {
|
|
1155
|
+
display: inline-flex;
|
|
1156
|
+
align-items: center;
|
|
1157
|
+
gap: 6px;
|
|
1142
1158
|
border: none;
|
|
1143
|
-
outline: none;
|
|
1144
1159
|
background: transparent;
|
|
1145
|
-
|
|
1160
|
+
padding: 4px 0;
|
|
1146
1161
|
color: #1e293b;
|
|
1147
|
-
min-width: 0;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
.native-search-input::placeholder {
|
|
1151
|
-
color: #94a3b8;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
.search-clear-icon {
|
|
1155
|
-
font-size: 16px;
|
|
1156
|
-
color: #94a3b8;
|
|
1157
1162
|
cursor: pointer;
|
|
1158
|
-
|
|
1159
|
-
transition: color 0.
|
|
1160
|
-
flex-shrink: 0;
|
|
1161
|
-
}
|
|
1163
|
+
min-width: 0;
|
|
1164
|
+
transition: color 0.15s ease;
|
|
1162
1165
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1166
|
+
&:hover {
|
|
1167
|
+
color: #1e6fff;
|
|
1168
|
+
}
|
|
1165
1169
|
}
|
|
1166
1170
|
|
|
1167
|
-
.
|
|
1168
|
-
display:
|
|
1171
|
+
.drawer-close-btn {
|
|
1172
|
+
display: flex;
|
|
1169
1173
|
align-items: center;
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
background: #1e293b;
|
|
1174
|
-
color: #ffffff;
|
|
1174
|
+
justify-content: center;
|
|
1175
|
+
width: 28px;
|
|
1176
|
+
height: 28px;
|
|
1175
1177
|
border: none;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
font-weight: 500;
|
|
1178
|
+
background: transparent;
|
|
1179
|
+
color: #475569;
|
|
1179
1180
|
cursor: pointer;
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1181
|
+
border-radius: 6px;
|
|
1182
|
+
padding: 0;
|
|
1183
|
+
transition: all 0.15s ease;
|
|
1183
1184
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1185
|
+
&:hover {
|
|
1186
|
+
background: #f1f5f9;
|
|
1187
|
+
color: #0f172a;
|
|
1188
|
+
}
|
|
1186
1189
|
}
|
|
1187
1190
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
+
/* ==================== 图二:概览视图样式 ==================== */
|
|
1192
|
+
.material-overview-body {
|
|
1193
|
+
flex: 1;
|
|
1194
|
+
min-height: 0;
|
|
1195
|
+
display: flex;
|
|
1196
|
+
flex-direction: column;
|
|
1197
|
+
overflow: hidden;
|
|
1191
1198
|
}
|
|
1192
1199
|
|
|
1193
|
-
.
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
height: 12px;
|
|
1197
|
-
border: 2px solid rgba(255, 255, 255, 0.4);
|
|
1198
|
-
border-top-color: #ffffff;
|
|
1199
|
-
border-radius: 50%;
|
|
1200
|
-
animation: spin 0.8s linear infinite;
|
|
1200
|
+
.overview-search-wrap {
|
|
1201
|
+
padding: 12px 16px 8px;
|
|
1202
|
+
flex-shrink: 0;
|
|
1201
1203
|
}
|
|
1202
1204
|
|
|
1203
|
-
|
|
1204
|
-
.gallery-toolbar-row {
|
|
1205
|
+
.overview-search-box {
|
|
1205
1206
|
display: flex;
|
|
1206
1207
|
align-items: center;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
border-
|
|
1211
|
-
|
|
1208
|
+
height: 36px;
|
|
1209
|
+
background: #f5f6f7;
|
|
1210
|
+
border: 1px solid #e2e8f0;
|
|
1211
|
+
border-radius: 6px;
|
|
1212
|
+
padding: 0 10px;
|
|
1213
|
+
gap: 8px;
|
|
1214
|
+
box-sizing: border-box;
|
|
1215
|
+
transition: all 0.2s ease;
|
|
1216
|
+
|
|
1217
|
+
&:focus-within {
|
|
1218
|
+
background: #ffffff;
|
|
1219
|
+
border-color: #1e6fff;
|
|
1220
|
+
}
|
|
1212
1221
|
}
|
|
1213
1222
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
gap: 4px;
|
|
1219
|
-
background: #f1f5f9;
|
|
1220
|
-
padding: 3px;
|
|
1221
|
-
border-radius: 18px;
|
|
1223
|
+
.overview-search-icon {
|
|
1224
|
+
font-size: 15px;
|
|
1225
|
+
color: #94a3b8;
|
|
1226
|
+
cursor: pointer;
|
|
1222
1227
|
flex-shrink: 0;
|
|
1223
1228
|
}
|
|
1224
1229
|
|
|
1225
|
-
.
|
|
1230
|
+
.overview-search-input {
|
|
1231
|
+
flex: 1;
|
|
1232
|
+
min-width: 0;
|
|
1226
1233
|
border: none;
|
|
1227
1234
|
outline: none;
|
|
1228
1235
|
background: transparent;
|
|
1229
|
-
padding: 5px 12px;
|
|
1230
1236
|
font-size: 13px;
|
|
1231
|
-
color: #
|
|
1232
|
-
font-weight: 500;
|
|
1233
|
-
border-radius: 15px;
|
|
1234
|
-
cursor: pointer;
|
|
1235
|
-
transition: all 0.2s ease;
|
|
1236
|
-
white-space: nowrap;
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
.pill-tab-btn:hover {
|
|
1240
|
-
color: #0f172a;
|
|
1241
|
-
}
|
|
1237
|
+
color: #1e293b;
|
|
1242
1238
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
font-weight: 600;
|
|
1247
|
-
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.15);
|
|
1239
|
+
&::placeholder {
|
|
1240
|
+
color: #94a3b8;
|
|
1241
|
+
}
|
|
1248
1242
|
}
|
|
1249
1243
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
gap: 8px;
|
|
1244
|
+
.overview-search-clear {
|
|
1245
|
+
font-size: 14px;
|
|
1246
|
+
color: #94a3b8;
|
|
1247
|
+
cursor: pointer;
|
|
1255
1248
|
flex-shrink: 0;
|
|
1256
|
-
}
|
|
1257
1249
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
gap: 4px;
|
|
1262
|
-
padding: 4px 8px;
|
|
1263
|
-
font-size: 12.5px;
|
|
1264
|
-
color: #475569;
|
|
1265
|
-
border-radius: 6px;
|
|
1266
|
-
cursor: pointer;
|
|
1267
|
-
transition: all 0.2s;
|
|
1268
|
-
user-select: none;
|
|
1269
|
-
white-space: nowrap;
|
|
1250
|
+
&:hover {
|
|
1251
|
+
color: #64748b;
|
|
1252
|
+
}
|
|
1270
1253
|
}
|
|
1271
1254
|
|
|
1272
|
-
.
|
|
1273
|
-
|
|
1274
|
-
|
|
1255
|
+
.overview-sections-scroll {
|
|
1256
|
+
flex: 1;
|
|
1257
|
+
min-height: 0;
|
|
1258
|
+
overflow-y: auto;
|
|
1259
|
+
padding: 8px 16px 20px;
|
|
1260
|
+
display: flex;
|
|
1261
|
+
flex-direction: column;
|
|
1262
|
+
gap: 18px;
|
|
1263
|
+
|
|
1264
|
+
&::-webkit-scrollbar {
|
|
1265
|
+
width: 4px;
|
|
1266
|
+
}
|
|
1267
|
+
&::-webkit-scrollbar-thumb {
|
|
1268
|
+
background: #e2e8f0;
|
|
1269
|
+
border-radius: 2px;
|
|
1270
|
+
}
|
|
1275
1271
|
}
|
|
1276
1272
|
|
|
1277
|
-
.
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1273
|
+
.overview-category-section {
|
|
1274
|
+
display: flex;
|
|
1275
|
+
flex-direction: column;
|
|
1276
|
+
gap: 8px;
|
|
1281
1277
|
}
|
|
1282
1278
|
|
|
1283
|
-
.
|
|
1284
|
-
|
|
1279
|
+
.section-header-row {
|
|
1280
|
+
display: flex;
|
|
1281
|
+
align-items: center;
|
|
1282
|
+
justify-content: space-between;
|
|
1285
1283
|
}
|
|
1286
1284
|
|
|
1287
|
-
.
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
border-radius: 50%;
|
|
1292
|
-
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
1285
|
+
.section-cat-title {
|
|
1286
|
+
font-size: 13px;
|
|
1287
|
+
font-weight: 500;
|
|
1288
|
+
color: #1e293b;
|
|
1293
1289
|
}
|
|
1294
1290
|
|
|
1295
|
-
.
|
|
1291
|
+
.section-view-all-btn {
|
|
1296
1292
|
border: none;
|
|
1297
|
-
background:
|
|
1293
|
+
background: transparent;
|
|
1294
|
+
padding: 0;
|
|
1298
1295
|
font-size: 12px;
|
|
1299
1296
|
color: #94a3b8;
|
|
1300
1297
|
cursor: pointer;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1298
|
+
transition: color 0.15s ease;
|
|
1299
|
+
|
|
1300
|
+
&:hover {
|
|
1301
|
+
color: #1e6fff;
|
|
1302
|
+
}
|
|
1305
1303
|
}
|
|
1306
1304
|
|
|
1307
|
-
.
|
|
1308
|
-
|
|
1305
|
+
.section-carousel-container {
|
|
1306
|
+
position: relative;
|
|
1307
|
+
width: 100%;
|
|
1308
|
+
overflow: hidden;
|
|
1309
1309
|
}
|
|
1310
1310
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1311
|
+
.section-carousel-track {
|
|
1312
|
+
display: flex;
|
|
1313
|
+
align-items: center;
|
|
1314
|
+
gap: 8px;
|
|
1315
|
+
overflow-x: auto;
|
|
1316
|
+
scroll-behavior: smooth;
|
|
1317
|
+
scrollbar-width: none;
|
|
1318
|
+
-ms-overflow-style: none;
|
|
1319
|
+
|
|
1320
|
+
&::-webkit-scrollbar {
|
|
1321
|
+
display: none;
|
|
1322
|
+
}
|
|
1315
1323
|
}
|
|
1316
1324
|
|
|
1317
|
-
.
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1325
|
+
.overview-skeleton-card {
|
|
1326
|
+
width: 96px;
|
|
1327
|
+
height: 84px;
|
|
1328
|
+
border-radius: 8px;
|
|
1329
|
+
background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%);
|
|
1330
|
+
background-size: 200% 100%;
|
|
1331
|
+
animation: shimmer 1.4s infinite;
|
|
1332
|
+
flex-shrink: 0;
|
|
1322
1333
|
}
|
|
1323
1334
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
gap: 6px;
|
|
1335
|
+
@keyframes shimmer {
|
|
1336
|
+
0% { background-position: 200% 0; }
|
|
1337
|
+
100% { background-position: -200% 0; }
|
|
1328
1338
|
}
|
|
1329
1339
|
|
|
1330
|
-
.
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1340
|
+
.overview-thumb-card {
|
|
1341
|
+
height: 84px;
|
|
1342
|
+
border-radius: 8px;
|
|
1343
|
+
overflow: hidden;
|
|
1344
|
+
background: #f8fafc;
|
|
1345
|
+
flex-shrink: 0;
|
|
1334
1346
|
cursor: pointer;
|
|
1335
|
-
|
|
1336
|
-
transition: all 0.2s ease;
|
|
1337
|
-
}
|
|
1347
|
+
position: relative;
|
|
1338
1348
|
|
|
1339
|
-
.
|
|
1340
|
-
|
|
1341
|
-
|
|
1349
|
+
&:hover .overview-thumb-img {
|
|
1350
|
+
transform: scale(1.04);
|
|
1351
|
+
}
|
|
1342
1352
|
}
|
|
1343
1353
|
|
|
1344
|
-
.
|
|
1345
|
-
|
|
1354
|
+
.overview-thumb-img {
|
|
1355
|
+
height: 84px;
|
|
1356
|
+
width: auto;
|
|
1357
|
+
min-width: 72px;
|
|
1358
|
+
max-width: 148px;
|
|
1359
|
+
object-fit: cover;
|
|
1360
|
+
display: block;
|
|
1361
|
+
border-radius: 8px;
|
|
1362
|
+
transition: transform 0.25s ease;
|
|
1346
1363
|
}
|
|
1347
1364
|
|
|
1348
|
-
.
|
|
1365
|
+
.video-badge-icon {
|
|
1366
|
+
position: absolute;
|
|
1367
|
+
left: 6px;
|
|
1368
|
+
bottom: 6px;
|
|
1369
|
+
width: 20px;
|
|
1370
|
+
height: 20px;
|
|
1371
|
+
border-radius: 50%;
|
|
1372
|
+
background: rgba(15, 23, 42, 0.55);
|
|
1373
|
+
color: #ffffff;
|
|
1349
1374
|
display: flex;
|
|
1350
1375
|
align-items: center;
|
|
1351
1376
|
justify-content: center;
|
|
1352
|
-
font-size:
|
|
1353
|
-
|
|
1354
|
-
background: #f1f5f9;
|
|
1377
|
+
font-size: 12px;
|
|
1378
|
+
pointer-events: none;
|
|
1355
1379
|
}
|
|
1356
1380
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1381
|
+
.overview-empty-row {
|
|
1382
|
+
width: 100%;
|
|
1383
|
+
height: 84px;
|
|
1384
|
+
border-radius: 8px;
|
|
1385
|
+
background: #f8fafc;
|
|
1386
|
+
border: 1px dashed #e2e8f0;
|
|
1359
1387
|
display: flex;
|
|
1360
|
-
flex-direction: column;
|
|
1361
1388
|
align-items: center;
|
|
1362
1389
|
justify-content: center;
|
|
1363
|
-
|
|
1364
|
-
|
|
1390
|
+
font-size: 12px;
|
|
1391
|
+
color: #94a3b8;
|
|
1392
|
+
cursor: pointer;
|
|
1365
1393
|
}
|
|
1366
1394
|
|
|
1367
|
-
.
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1395
|
+
.carousel-edge-mask {
|
|
1396
|
+
position: absolute;
|
|
1397
|
+
top: 0;
|
|
1398
|
+
bottom: 0;
|
|
1399
|
+
width: 30px;
|
|
1400
|
+
display: flex;
|
|
1401
|
+
align-items: center;
|
|
1402
|
+
justify-content: center;
|
|
1403
|
+
cursor: pointer;
|
|
1404
|
+
color: #334155;
|
|
1405
|
+
z-index: 2;
|
|
1406
|
+
transition: opacity 0.15s ease;
|
|
1375
1407
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1408
|
+
&.left-mask {
|
|
1409
|
+
left: 0;
|
|
1410
|
+
background: linear-gradient(90deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0) 100%);
|
|
1411
|
+
justify-content: flex-start;
|
|
1412
|
+
padding-left: 4px;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
&.right-mask {
|
|
1416
|
+
right: 0;
|
|
1417
|
+
background: linear-gradient(270deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0) 100%);
|
|
1418
|
+
justify-content: flex-end;
|
|
1419
|
+
padding-right: 4px;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
&:hover {
|
|
1423
|
+
color: #0f172a;
|
|
1424
|
+
}
|
|
1379
1425
|
}
|
|
1380
1426
|
|
|
1381
|
-
/*
|
|
1382
|
-
.
|
|
1427
|
+
/* ==================== 图三:详情视图样式 ==================== */
|
|
1428
|
+
.material-detail-body {
|
|
1383
1429
|
flex: 1;
|
|
1384
1430
|
min-height: 0;
|
|
1385
|
-
|
|
1386
|
-
|
|
1431
|
+
display: flex;
|
|
1432
|
+
flex-direction: column;
|
|
1433
|
+
overflow: hidden;
|
|
1387
1434
|
}
|
|
1388
1435
|
|
|
1389
|
-
.
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
border-radius: 3px;
|
|
1395
|
-
}
|
|
1396
|
-
.waterfall-scroll-container::-webkit-scrollbar-thumb:hover {
|
|
1397
|
-
background: #94a3b8;
|
|
1436
|
+
.detail-tags-bar-wrap {
|
|
1437
|
+
position: relative;
|
|
1438
|
+
padding: 12px 16px 8px;
|
|
1439
|
+
flex-shrink: 0;
|
|
1440
|
+
overflow: hidden;
|
|
1398
1441
|
}
|
|
1399
1442
|
|
|
1400
|
-
.
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1443
|
+
.detail-tags-track {
|
|
1444
|
+
display: flex;
|
|
1445
|
+
align-items: center;
|
|
1446
|
+
gap: 8px;
|
|
1447
|
+
overflow-x: auto;
|
|
1448
|
+
scroll-behavior: smooth;
|
|
1449
|
+
scrollbar-width: none;
|
|
1450
|
+
-ms-overflow-style: none;
|
|
1404
1451
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
column-count: 3;
|
|
1452
|
+
&::-webkit-scrollbar {
|
|
1453
|
+
display: none;
|
|
1408
1454
|
}
|
|
1409
1455
|
}
|
|
1410
1456
|
|
|
1411
|
-
.
|
|
1412
|
-
|
|
1413
|
-
|
|
1457
|
+
.detail-tag-pill {
|
|
1458
|
+
height: 30px;
|
|
1459
|
+
padding: 0 14px;
|
|
1460
|
+
border-radius: 6px;
|
|
1461
|
+
border: 1px solid #e2e8f0;
|
|
1462
|
+
background: #ffffff;
|
|
1463
|
+
color: #475569;
|
|
1464
|
+
font-size: 12.5px;
|
|
1465
|
+
font-weight: 400;
|
|
1466
|
+
white-space: nowrap;
|
|
1467
|
+
flex-shrink: 0;
|
|
1414
1468
|
cursor: pointer;
|
|
1415
|
-
|
|
1469
|
+
transition: all 0.15s ease;
|
|
1416
1470
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
1422
|
-
border: 1px solid #f1f5f9;
|
|
1423
|
-
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
1424
|
-
display: flex;
|
|
1425
|
-
flex-direction: column;
|
|
1426
|
-
}
|
|
1471
|
+
&:hover {
|
|
1472
|
+
border-color: #cbd5e1;
|
|
1473
|
+
color: #1e293b;
|
|
1474
|
+
}
|
|
1427
1475
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1476
|
+
&.active {
|
|
1477
|
+
background: #ebf3ff;
|
|
1478
|
+
border-color: transparent;
|
|
1479
|
+
color: #1e6fff;
|
|
1480
|
+
font-weight: 500;
|
|
1481
|
+
}
|
|
1432
1482
|
}
|
|
1433
1483
|
|
|
1434
|
-
.
|
|
1435
|
-
position:
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
min-height: 90px;
|
|
1440
|
-
max-height: 260px;
|
|
1484
|
+
.tags-edge-arrow {
|
|
1485
|
+
position: absolute;
|
|
1486
|
+
top: 12px;
|
|
1487
|
+
height: 30px;
|
|
1488
|
+
width: 28px;
|
|
1441
1489
|
display: flex;
|
|
1442
1490
|
align-items: center;
|
|
1443
|
-
|
|
1444
|
-
|
|
1491
|
+
cursor: pointer;
|
|
1492
|
+
color: #334155;
|
|
1493
|
+
z-index: 2;
|
|
1445
1494
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1495
|
+
&.left-arrow {
|
|
1496
|
+
left: 16px;
|
|
1497
|
+
background: linear-gradient(90deg, #ffffff 45%, rgba(255, 255, 255, 0) 100%);
|
|
1498
|
+
justify-content: flex-start;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
&.right-arrow {
|
|
1502
|
+
right: 16px;
|
|
1503
|
+
background: linear-gradient(270deg, #ffffff 45%, rgba(255, 255, 255, 0) 100%);
|
|
1504
|
+
justify-content: flex-end;
|
|
1505
|
+
}
|
|
1453
1506
|
}
|
|
1454
1507
|
|
|
1455
|
-
.
|
|
1456
|
-
|
|
1508
|
+
.detail-waterfall-scroll {
|
|
1509
|
+
flex: 1;
|
|
1510
|
+
min-height: 0;
|
|
1511
|
+
overflow-y: auto;
|
|
1512
|
+
padding: 4px 16px 16px;
|
|
1513
|
+
|
|
1514
|
+
&::-webkit-scrollbar {
|
|
1515
|
+
width: 4px;
|
|
1516
|
+
}
|
|
1517
|
+
&::-webkit-scrollbar-thumb {
|
|
1518
|
+
background: #cbd5e1;
|
|
1519
|
+
border-radius: 2px;
|
|
1520
|
+
}
|
|
1457
1521
|
}
|
|
1458
1522
|
|
|
1459
|
-
.
|
|
1460
|
-
padding: 30px 0;
|
|
1523
|
+
.detail-masonry-columns {
|
|
1461
1524
|
display: flex;
|
|
1462
|
-
align-items:
|
|
1463
|
-
|
|
1525
|
+
align-items: flex-start;
|
|
1526
|
+
gap: 8px;
|
|
1464
1527
|
}
|
|
1465
1528
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
top: 0;
|
|
1470
|
-
left: 0;
|
|
1471
|
-
width: 100%;
|
|
1472
|
-
height: 100%;
|
|
1473
|
-
background: rgba(15, 23, 42, 0.55);
|
|
1474
|
-
backdrop-filter: blur(2px);
|
|
1529
|
+
.detail-masonry-col {
|
|
1530
|
+
flex: 1;
|
|
1531
|
+
min-width: 0;
|
|
1475
1532
|
display: flex;
|
|
1476
1533
|
flex-direction: column;
|
|
1477
|
-
|
|
1478
|
-
justify-content: center;
|
|
1479
|
-
opacity: 0;
|
|
1480
|
-
transition: opacity 0.2s ease;
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
.card-box:hover .card-hover-mask {
|
|
1484
|
-
opacity: 1;
|
|
1534
|
+
gap: 8px;
|
|
1485
1535
|
}
|
|
1486
1536
|
|
|
1487
|
-
.
|
|
1488
|
-
position:
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
border-radius: 50%;
|
|
1494
|
-
background: rgba(255, 255, 255, 0.85);
|
|
1495
|
-
color: #1e293b;
|
|
1496
|
-
border: none;
|
|
1537
|
+
.detail-masonry-card {
|
|
1538
|
+
position: relative;
|
|
1539
|
+
width: 100%;
|
|
1540
|
+
border-radius: 8px;
|
|
1541
|
+
overflow: hidden;
|
|
1542
|
+
background: #f1f5f9;
|
|
1497
1543
|
cursor: pointer;
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1544
|
+
|
|
1545
|
+
&:hover .detail-card-img {
|
|
1546
|
+
transform: scale(1.03);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
&:hover .detail-card-preview-btn {
|
|
1550
|
+
opacity: 1;
|
|
1551
|
+
}
|
|
1503
1552
|
}
|
|
1504
1553
|
|
|
1505
|
-
.
|
|
1506
|
-
|
|
1507
|
-
|
|
1554
|
+
.detail-card-img {
|
|
1555
|
+
width: 100%;
|
|
1556
|
+
height: auto;
|
|
1557
|
+
min-height: 54px;
|
|
1558
|
+
max-height: 240px;
|
|
1559
|
+
object-fit: cover;
|
|
1560
|
+
display: block;
|
|
1561
|
+
border-radius: 8px;
|
|
1562
|
+
transition: transform 0.25s ease;
|
|
1508
1563
|
}
|
|
1509
1564
|
|
|
1510
|
-
.
|
|
1565
|
+
.detail-card-placeholder {
|
|
1566
|
+
height: 90px;
|
|
1511
1567
|
display: flex;
|
|
1512
|
-
flex-direction: column;
|
|
1513
1568
|
align-items: center;
|
|
1514
|
-
|
|
1515
|
-
color: #ffffff;
|
|
1516
|
-
font-size: 12px;
|
|
1517
|
-
font-weight: 600;
|
|
1569
|
+
justify-content: center;
|
|
1518
1570
|
}
|
|
1519
1571
|
|
|
1520
|
-
.
|
|
1521
|
-
|
|
1522
|
-
|
|
1572
|
+
.detail-card-preview-btn {
|
|
1573
|
+
position: absolute;
|
|
1574
|
+
top: 6px;
|
|
1575
|
+
right: 6px;
|
|
1576
|
+
width: 24px;
|
|
1577
|
+
height: 24px;
|
|
1523
1578
|
border-radius: 50%;
|
|
1524
|
-
|
|
1579
|
+
border: none;
|
|
1580
|
+
background: rgba(15, 23, 42, 0.55);
|
|
1525
1581
|
color: #ffffff;
|
|
1526
1582
|
display: flex;
|
|
1527
1583
|
align-items: center;
|
|
1528
1584
|
justify-content: center;
|
|
1529
|
-
font-size: 18px;
|
|
1530
|
-
box-shadow: 0 2px 8px rgba(255, 80, 0, 0.4);
|
|
1531
|
-
transition: transform 0.2s;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
.card-box:hover .insert-circle-btn {
|
|
1535
|
-
transform: scale(1.08);
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
.insert-tip {
|
|
1539
1585
|
font-size: 12px;
|
|
1540
|
-
|
|
1586
|
+
cursor: pointer;
|
|
1587
|
+
opacity: 0;
|
|
1588
|
+
transition: all 0.15s ease;
|
|
1541
1589
|
|
|
1542
|
-
|
|
1543
|
-
.
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
justify-content: space-between;
|
|
1547
|
-
padding: 8px 10px;
|
|
1548
|
-
background: #ffffff;
|
|
1549
|
-
border-top: 1px solid #f8fafc;
|
|
1590
|
+
&:hover {
|
|
1591
|
+
background: rgba(15, 23, 42, 0.8);
|
|
1592
|
+
transform: scale(1.08);
|
|
1593
|
+
}
|
|
1550
1594
|
}
|
|
1551
1595
|
|
|
1552
|
-
|
|
1596
|
+
/* 加载中与底部状态 */
|
|
1597
|
+
.gallery-loading-box {
|
|
1553
1598
|
display: flex;
|
|
1599
|
+
flex-direction: column;
|
|
1554
1600
|
align-items: center;
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
.format-tag {
|
|
1561
|
-
font-size: 10px;
|
|
1562
|
-
font-weight: 600;
|
|
1563
|
-
color: #475569;
|
|
1564
|
-
background: #f1f5f9;
|
|
1565
|
-
padding: 1px 5px;
|
|
1566
|
-
border-radius: 3px;
|
|
1567
|
-
letter-spacing: 0.5px;
|
|
1568
|
-
flex-shrink: 0;
|
|
1601
|
+
justify-content: center;
|
|
1602
|
+
padding: 60px 0;
|
|
1603
|
+
gap: 10px;
|
|
1569
1604
|
}
|
|
1570
1605
|
|
|
1571
|
-
.
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
border:
|
|
1577
|
-
|
|
1578
|
-
border-radius: 3px;
|
|
1579
|
-
letter-spacing: 0.4px;
|
|
1580
|
-
flex-shrink: 0;
|
|
1581
|
-
text-transform: uppercase;
|
|
1606
|
+
.loading-ring-spinner {
|
|
1607
|
+
width: 24px;
|
|
1608
|
+
height: 24px;
|
|
1609
|
+
border: 2.5px solid #e2e8f0;
|
|
1610
|
+
border-top-color: #1e6fff;
|
|
1611
|
+
border-radius: 50%;
|
|
1612
|
+
animation: spin 0.8s linear infinite;
|
|
1582
1613
|
}
|
|
1583
1614
|
|
|
1584
|
-
.
|
|
1585
|
-
font-size:
|
|
1615
|
+
.loading-tip-text {
|
|
1616
|
+
font-size: 12.5px;
|
|
1586
1617
|
color: #64748b;
|
|
1587
|
-
white-space: nowrap;
|
|
1588
|
-
overflow: hidden;
|
|
1589
|
-
text-overflow: ellipsis;
|
|
1590
|
-
max-width: 140px;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
.more-options-dot-btn {
|
|
1594
|
-
border: none;
|
|
1595
|
-
background: transparent;
|
|
1596
|
-
color: #94a3b8;
|
|
1597
|
-
cursor: pointer;
|
|
1598
|
-
padding: 2px 4px;
|
|
1599
|
-
border-radius: 4px;
|
|
1600
|
-
display: flex;
|
|
1601
|
-
align-items: center;
|
|
1602
|
-
transition: all 0.2s;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
.more-options-dot-btn:hover {
|
|
1606
|
-
color: #334155;
|
|
1607
|
-
background: #f1f5f9;
|
|
1608
1618
|
}
|
|
1609
1619
|
|
|
1610
|
-
/* 瀑布流底部加载更多区域 */
|
|
1611
1620
|
.waterfall-footer-status {
|
|
1612
|
-
padding:
|
|
1621
|
+
padding: 14px 0 8px;
|
|
1613
1622
|
display: flex;
|
|
1614
1623
|
align-items: center;
|
|
1615
1624
|
justify-content: center;
|
|
1616
|
-
width: 100%;
|
|
1617
1625
|
}
|
|
1618
1626
|
|
|
1619
1627
|
.loading-more-row {
|
|
1620
1628
|
display: flex;
|
|
1621
1629
|
align-items: center;
|
|
1622
|
-
gap:
|
|
1623
|
-
font-size:
|
|
1630
|
+
gap: 6px;
|
|
1631
|
+
font-size: 12px;
|
|
1624
1632
|
color: #64748b;
|
|
1625
1633
|
}
|
|
1626
1634
|
|
|
1635
|
+
.btn-spinner-ring {
|
|
1636
|
+
display: inline-block;
|
|
1637
|
+
width: 12px;
|
|
1638
|
+
height: 12px;
|
|
1639
|
+
border: 2px solid rgba(30, 111, 255, 0.2);
|
|
1640
|
+
border-top-color: #1e6fff;
|
|
1641
|
+
border-radius: 50%;
|
|
1642
|
+
animation: spin 0.8s linear infinite;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1627
1645
|
.load-failed-row {
|
|
1628
1646
|
display: flex;
|
|
1629
1647
|
align-items: center;
|
|
1630
|
-
gap:
|
|
1631
|
-
background: #fef2f2;
|
|
1632
|
-
border: 1px solid #fecaca;
|
|
1633
|
-
padding: 6px 14px;
|
|
1634
|
-
border-radius: 20px;
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
.load-error-text {
|
|
1648
|
+
gap: 8px;
|
|
1638
1649
|
font-size: 12px;
|
|
1639
|
-
color: #
|
|
1650
|
+
color: #ef4444;
|
|
1640
1651
|
}
|
|
1641
1652
|
|
|
1642
1653
|
.retry-load-btn {
|
|
1643
|
-
border:
|
|
1644
|
-
background: #
|
|
1645
|
-
color: #
|
|
1646
|
-
padding:
|
|
1647
|
-
border-radius:
|
|
1654
|
+
border: 1px solid #fecaca;
|
|
1655
|
+
background: #fef2f2;
|
|
1656
|
+
color: #dc2626;
|
|
1657
|
+
padding: 2px 8px;
|
|
1658
|
+
border-radius: 4px;
|
|
1648
1659
|
font-size: 11.5px;
|
|
1649
1660
|
cursor: pointer;
|
|
1650
|
-
font-weight: 500;
|
|
1651
|
-
transition: all 0.2s;
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
.retry-load-btn:hover {
|
|
1655
|
-
background: #b91c1c;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
.btn-spinner-ring.dark {
|
|
1659
|
-
border-color: rgba(15, 23, 42, 0.15);
|
|
1660
|
-
border-top-color: #0f172a;
|
|
1661
1661
|
}
|
|
1662
1662
|
|
|
1663
1663
|
.load-more-action-btn {
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
padding: 8px 24px;
|
|
1668
|
-
background: #f1f5f9;
|
|
1664
|
+
width: 100%;
|
|
1665
|
+
height: 34px;
|
|
1666
|
+
border-radius: 6px;
|
|
1669
1667
|
border: 1px solid #e2e8f0;
|
|
1670
|
-
|
|
1668
|
+
background: #f8fafc;
|
|
1671
1669
|
color: #334155;
|
|
1672
1670
|
font-size: 12.5px;
|
|
1673
1671
|
font-weight: 500;
|
|
1674
1672
|
cursor: pointer;
|
|
1675
|
-
|
|
1676
|
-
|
|
1673
|
+
display: inline-flex;
|
|
1674
|
+
align-items: center;
|
|
1675
|
+
justify-content: center;
|
|
1676
|
+
transition: all 0.18s ease;
|
|
1677
1677
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.12);
|
|
1684
|
-
}
|
|
1678
|
+
&:hover {
|
|
1679
|
+
background: #eff6ff;
|
|
1680
|
+
border-color: #bfdbfe;
|
|
1681
|
+
color: #1e6fff;
|
|
1682
|
+
}
|
|
1685
1683
|
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1684
|
+
&:active {
|
|
1685
|
+
background: #dbeafe;
|
|
1686
|
+
}
|
|
1689
1687
|
}
|
|
1690
1688
|
|
|
1691
1689
|
.all-loaded-divider {
|
|
1692
|
-
font-size:
|
|
1690
|
+
font-size: 11.5px;
|
|
1693
1691
|
color: #94a3b8;
|
|
1694
|
-
letter-spacing: 0.5px;
|
|
1695
1692
|
}
|
|
1696
1693
|
|
|
1697
|
-
/* 空状态 */
|
|
1698
1694
|
.gallery-empty-state {
|
|
1699
1695
|
display: flex;
|
|
1700
1696
|
flex-direction: column;
|
|
1701
1697
|
align-items: center;
|
|
1702
1698
|
justify-content: center;
|
|
1703
1699
|
padding: 60px 16px;
|
|
1704
|
-
gap:
|
|
1700
|
+
gap: 8px;
|
|
1705
1701
|
text-align: center;
|
|
1706
1702
|
}
|
|
1707
1703
|
|
|
1708
1704
|
.empty-title {
|
|
1709
|
-
font-size:
|
|
1705
|
+
font-size: 12.5px;
|
|
1710
1706
|
color: #64748b;
|
|
1711
1707
|
margin: 0;
|
|
1712
|
-
font-weight: 500;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
.empty-subtitle {
|
|
1716
|
-
font-size: 12px;
|
|
1717
|
-
color: #94a3b8;
|
|
1718
1708
|
}
|
|
1719
1709
|
|
|
1720
1710
|
@keyframes spin {
|
|
1721
1711
|
from { transform: rotate(0deg); }
|
|
1722
1712
|
to { transform: rotate(360deg); }
|
|
1723
1713
|
}
|
|
1724
|
-
|
|
1725
|
-
/* ==========================================================================
|
|
1726
|
-
重点优化:当在右侧边栏(宽度 <= 520px / is-narrow-view)打开时的响应式完美适配
|
|
1727
|
-
========================================================================== */
|
|
1728
|
-
.template-gallery-container.is-narrow-view {
|
|
1729
|
-
padding: 10px 12px 14px;
|
|
1730
|
-
gap: 10px;
|
|
1731
|
-
|
|
1732
|
-
/* 1. 搜索栏收敛紧凑 */
|
|
1733
|
-
.search-input-capsule {
|
|
1734
|
-
height: 38px;
|
|
1735
|
-
padding: 2px 4px 2px 10px;
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
.native-search-input {
|
|
1739
|
-
font-size: 12.5px;
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
.capsule-search-btn {
|
|
1743
|
-
height: 30px;
|
|
1744
|
-
padding: 0 10px;
|
|
1745
|
-
font-size: 12px;
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
/* 在狭窄侧边栏下,隐藏“搜索”文字,只保留放大镜图标,彻底防止截断输入框 */
|
|
1749
|
-
.capsule-search-btn .btn-text {
|
|
1750
|
-
display: none;
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
/* 2. 工具栏分为上下两行,彻底解决挤压溢出 */
|
|
1754
|
-
.gallery-toolbar-row {
|
|
1755
|
-
flex-direction: column;
|
|
1756
|
-
align-items: stretch;
|
|
1757
|
-
gap: 8px;
|
|
1758
|
-
padding-bottom: 8px;
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
/* 第一行:分类 Tabs 单行横向平滑滚动 */
|
|
1762
|
-
.category-pill-group {
|
|
1763
|
-
width: 100%;
|
|
1764
|
-
overflow-x: auto;
|
|
1765
|
-
scrollbar-width: none;
|
|
1766
|
-
-ms-overflow-style: none;
|
|
1767
|
-
display: flex;
|
|
1768
|
-
flex-wrap: nowrap;
|
|
1769
|
-
padding: 2px 4px;
|
|
1770
|
-
box-sizing: border-box;
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
.category-pill-group::-webkit-scrollbar {
|
|
1774
|
-
display: none;
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
.pill-tab-btn {
|
|
1778
|
-
flex-shrink: 0;
|
|
1779
|
-
padding: 4px 10px;
|
|
1780
|
-
font-size: 12px;
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
/* 第二行:多维筛选下拉平铺 */
|
|
1784
|
-
.filter-dropdown-group {
|
|
1785
|
-
width: 100%;
|
|
1786
|
-
display: flex;
|
|
1787
|
-
align-items: center;
|
|
1788
|
-
justify-content: flex-start;
|
|
1789
|
-
gap: 6px;
|
|
1790
|
-
overflow-x: auto;
|
|
1791
|
-
scrollbar-width: none;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
.filter-dropdown-group::-webkit-scrollbar {
|
|
1795
|
-
display: none;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
.toolbar-dropdown-btn {
|
|
1799
|
-
padding: 3px 6px;
|
|
1800
|
-
font-size: 11.5px;
|
|
1801
|
-
flex-shrink: 0;
|
|
1802
|
-
}
|
|
1803
|
-
|
|
1804
|
-
/* 3. 瀑布流自适应为精致的双列(2列)排版 */
|
|
1805
|
-
.waterfall-masonry-layout {
|
|
1806
|
-
column-count: 2 !important;
|
|
1807
|
-
column-gap: 8px !important;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
.waterfall-card-item {
|
|
1811
|
-
margin-bottom: 8px !important;
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
.card-media-viewport {
|
|
1815
|
-
min-height: 60px;
|
|
1816
|
-
max-height: 180px;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
.card-bottom-info {
|
|
1820
|
-
padding: 6px 8px;
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
.format-title-hint {
|
|
1824
|
-
max-width: 80px;
|
|
1825
|
-
font-size: 10px;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
.insert-circle-btn {
|
|
1829
|
-
width: 28px;
|
|
1830
|
-
height: 28px;
|
|
1831
|
-
font-size: 15px;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
.insert-tip {
|
|
1835
|
-
font-size: 10px;
|
|
1836
|
-
}
|
|
1837
|
-
}
|
|
1838
1714
|
</style>
|
|
1839
1715
|
|
|
1840
1716
|
<style>
|
|
1841
|
-
/*
|
|
1842
|
-
全屏大图预览 Modal(脱离一切容器,z-index 最高层级)
|
|
1843
|
-
========================================================================== */
|
|
1717
|
+
/* 全屏大图预览 Modal */
|
|
1844
1718
|
.custom-fullscreen-preview-modal {
|
|
1845
1719
|
position: fixed !important;
|
|
1846
1720
|
inset: 0 !important;
|
|
@@ -1872,7 +1746,7 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
|
|
|
1872
1746
|
}
|
|
1873
1747
|
|
|
1874
1748
|
.preview-format-pill {
|
|
1875
|
-
background: #
|
|
1749
|
+
background: #1e6fff;
|
|
1876
1750
|
color: #ffffff;
|
|
1877
1751
|
font-size: 11px;
|
|
1878
1752
|
font-weight: 700;
|
|
@@ -1983,24 +1857,23 @@ watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => mater
|
|
|
1983
1857
|
align-items: center;
|
|
1984
1858
|
gap: 6px;
|
|
1985
1859
|
padding: 8px 20px;
|
|
1986
|
-
background: #
|
|
1860
|
+
background: #1e6fff;
|
|
1987
1861
|
color: #ffffff;
|
|
1988
1862
|
border: none;
|
|
1989
1863
|
border-radius: 20px;
|
|
1990
1864
|
font-size: 13px;
|
|
1991
1865
|
font-weight: 600;
|
|
1992
1866
|
cursor: pointer;
|
|
1993
|
-
box-shadow: 0 4px 14px rgba(
|
|
1867
|
+
box-shadow: 0 4px 14px rgba(30, 111, 255, 0.4);
|
|
1994
1868
|
transition: all 0.2s;
|
|
1995
1869
|
}
|
|
1996
1870
|
|
|
1997
1871
|
.preview-quick-insert-btn:hover {
|
|
1998
|
-
background: #
|
|
1872
|
+
background: #1557d0;
|
|
1999
1873
|
transform: translateY(-2px);
|
|
2000
|
-
box-shadow: 0 6px 18px rgba(
|
|
1874
|
+
box-shadow: 0 6px 18px rgba(30, 111, 255, 0.5);
|
|
2001
1875
|
}
|
|
2002
1876
|
|
|
2003
|
-
/* 过渡动效 */
|
|
2004
1877
|
.preview-backdrop-fade-enter-active,
|
|
2005
1878
|
.preview-backdrop-fade-leave-active {
|
|
2006
1879
|
transition: opacity 0.25s ease;
|