jvs-ppt-ui 0.0.11 → 0.0.14
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/README.md +68 -1
- package/dist/lib/jvs-ppt-ui.es.js +21682 -21520
- package/dist/lib/jvs-ppt-ui.umd.js +85 -85
- package/dist/lib/style.css +1 -1
- package/package.json +1 -1
- package/src/App.vue +2 -2
- package/src/components/CanvasArea.vue +44 -21
- package/src/components/Header.vue +1592 -1564
- package/src/components/PptEditor.vue +5 -0
- package/src/components/PresentationMode.vue +2 -1
- package/src/components/SidebarLeft.vue +17 -5
- package/src/components/TemplateGallery.vue +217 -119
- package/src/components/canvas/elements/CanvasShapeElement.vue +3 -3
- package/src/components/canvas/elements/CanvasTextElement.vue +4 -4
- package/src/composables/canvas/useCanvasShortcuts.ts +50 -1
- package/src/composables/canvas/useCanvasTable.ts +8 -3
- package/src/composables/canvas/useCanvasText.ts +7 -3
- package/src/composables/sidebar/useSidebarElementTransform.ts +17 -4
- package/src/composables/useSlides.ts +33 -14
- package/src/utils/groupFlip.ts +108 -0
|
@@ -80,6 +80,7 @@ const {
|
|
|
80
80
|
aiPptDialogVisible,
|
|
81
81
|
unsplashDialogVisible,
|
|
82
82
|
isPresenting,
|
|
83
|
+
isReadonly,
|
|
83
84
|
importSlides,
|
|
84
85
|
exportSlides,
|
|
85
86
|
currentEditingFormulaEl,
|
|
@@ -110,6 +111,10 @@ const {
|
|
|
110
111
|
uploadMediaResourcesInSlides
|
|
111
112
|
} = slidesState
|
|
112
113
|
|
|
114
|
+
watch(() => props.readonly, (val) => {
|
|
115
|
+
isReadonly.value = !!val
|
|
116
|
+
}, { immediate: true })
|
|
117
|
+
|
|
113
118
|
// 弹层挂载闩锁:首次 visible 后保持挂载(异步组件只加载一次,关闭动画不被 v-if 掐断)
|
|
114
119
|
function latchMount(source: Ref<boolean>) {
|
|
115
120
|
const opened = ref(false)
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
getLineElementPath, getLineElementRenderPath, getLineDashArray, getSvgWidth, getSvgHeight, isEllipseShape
|
|
23
23
|
} from '../utils/line'
|
|
24
|
+
import { isTextContainingElement } from '../utils/groupFlip'
|
|
24
25
|
|
|
25
26
|
const {
|
|
26
27
|
isPresenting, activeIndex, slides, stopPresentation, renderLatexToSvg, notify, execNext, execPrev,
|
|
@@ -1200,7 +1201,7 @@ function getSafeShapeText(el: any): string {
|
|
|
1200
1201
|
width: isAutoSizeWidthText(el) ? 'auto' : (el.style?.width || 'auto'),
|
|
1201
1202
|
height: isAutoSizeHeightText(el) ? 'auto' : (el.style?.height || 'auto'),
|
|
1202
1203
|
zIndex: 3,
|
|
1203
|
-
transform: `rotate(${el.style.rotate || 0}deg) scale(${el
|
|
1204
|
+
transform: `rotate(${el.style.rotate || 0}deg) scale(${!isTextContainingElement(el) && ((el.style as any)?.flipH || el.flipH || el.isFlipH) ? -1 : 1}, ${!isTextContainingElement(el) && ((el.style as any)?.flipV || el.flipV || el.isFlipV) ? -1 : 1})`,
|
|
1204
1205
|
opacity: (el.style as any).opacity !== undefined ? (el.style as any).opacity : 1,
|
|
1205
1206
|
filter: (el.type !== 'image' && el.type !== 'chart' && (el.style as any).boxShadow && (el.style as any).boxShadow !== 'none') ? `drop-shadow(${(el.style as any).boxShadow})` : '',
|
|
1206
1207
|
cursor: (el.link || hasTriggerAnimations(el.id)) ? 'pointer' : 'default',
|
|
@@ -28,6 +28,7 @@ import PresentationGroupChild from './PresentationGroupChild.vue'
|
|
|
28
28
|
import {
|
|
29
29
|
getLineElementPath, getLineElementRenderPath, getLineDashArray, getSvgWidth, getSvgHeight, isEllipseShape, scaleSvgPath
|
|
30
30
|
} from '../utils/line'
|
|
31
|
+
import { isTextContainingElement } from '../utils/groupFlip'
|
|
31
32
|
import { SHAPE_LIST, type ShapePoolItem } from '../utils/shapes'
|
|
32
33
|
|
|
33
34
|
const flatShapePoolItems: ShapePoolItem[] = SHAPE_LIST.flatMap(c => c.children)
|
|
@@ -42,7 +43,7 @@ const {
|
|
|
42
43
|
enterMasterView, addMaster, deleteMaster, addLayout, deleteLayout, duplicateLayout,
|
|
43
44
|
clipboard, slideClipboard, handleUnifiedPaste, sortSlideElements, getEffectiveMasterElementsForSlide,
|
|
44
45
|
getEffectiveSlideBackground, isCanvasDragging, activeSlideVersion, pauseHistory,
|
|
45
|
-
activeSidebarTab
|
|
46
|
+
activeSidebarTab, isReadonly
|
|
46
47
|
} = useSlides()
|
|
47
48
|
|
|
48
49
|
const displaySlides = shallowRef<any[]>([...slides.value])
|
|
@@ -2092,7 +2093,7 @@ const masterGroupedLayouts = computed(() => {
|
|
|
2092
2093
|
|
|
2093
2094
|
<!-- Regular Sidebar Layouts -->
|
|
2094
2095
|
<template v-else>
|
|
2095
|
-
<div class="sidebar-add-slide-container" style="padding: 12px 16px; display: flex; flex-shrink: 0;">
|
|
2096
|
+
<div v-if="!isReadonly" class="sidebar-add-slide-container" style="padding: 12px 16px; display: flex; flex-shrink: 0;">
|
|
2096
2097
|
<div class="custom-split-button">
|
|
2097
2098
|
<!-- Main Add Slide Button -->
|
|
2098
2099
|
<button @click="addSlide" class="split-btn-left">
|
|
@@ -2153,7 +2154,7 @@ const masterGroupedLayouts = computed(() => {
|
|
|
2153
2154
|
</button>
|
|
2154
2155
|
</div> -->
|
|
2155
2156
|
|
|
2156
|
-
<Draggable v-if="activeSidebarTab === 'outline'" class="slide-list" ref="slideListRef" :modelValue="displaySlides"
|
|
2157
|
+
<Draggable v-if="activeSidebarTab === 'outline'" :disabled="isReadonly" class="slide-list" ref="slideListRef" :modelValue="displaySlides"
|
|
2157
2158
|
:animation="200" :scroll="true" :scrollSensitivity="50" @end="handleDragEnd" itemKey="id"
|
|
2158
2159
|
@mouseenter="isMouseOverSlideList = true" @mouseleave="isMouseOverSlideList = false">
|
|
2159
2160
|
<template #item="{ element: slide, index }">
|
|
@@ -2246,7 +2247,7 @@ const masterGroupedLayouts = computed(() => {
|
|
|
2246
2247
|
width: el.style?.width || (el.width !== undefined ? `${el.width}px` : 'auto'),
|
|
2247
2248
|
height: el.style?.height || (el.height !== undefined ? `${el.height}px` : 'auto'),
|
|
2248
2249
|
zIndex: 1,
|
|
2249
|
-
transform: `rotate(${el.style?.rotate || el.rotate || 0}deg) scale(${el
|
|
2250
|
+
transform: `rotate(${el.style?.rotate || el.rotate || 0}deg) scale(${!isTextContainingElement(el) && ((el.style as any)?.flipH || el.flipH || el.isFlipH) ? -1 : 1}, ${!isTextContainingElement(el) && ((el.style as any)?.flipV || el.flipV || el.isFlipV) ? -1 : 1})`,
|
|
2250
2251
|
opacity: el.style?.opacity !== undefined ? el.style.opacity : 1,
|
|
2251
2252
|
pointerEvents: 'none'
|
|
2252
2253
|
}">
|
|
@@ -2347,7 +2348,7 @@ const masterGroupedLayouts = computed(() => {
|
|
|
2347
2348
|
width: el.style?.width || 'auto',
|
|
2348
2349
|
height: el.style?.height || 'auto',
|
|
2349
2350
|
zIndex: 3,
|
|
2350
|
-
transform: `rotate(${el.style.rotate || 0}deg) scale(${el
|
|
2351
|
+
transform: `rotate(${el.style.rotate || 0}deg) scale(${!isTextContainingElement(el) && ((el.style as any)?.flipH || el.flipH || el.isFlipH) ? -1 : 1}, ${!isTextContainingElement(el) && ((el.style as any)?.flipV || el.flipV || el.isFlipV) ? -1 : 1})`,
|
|
2351
2352
|
opacity: (el.style as any).opacity !== undefined ? (el.style as any).opacity : 1,
|
|
2352
2353
|
filter: (el.type !== 'image' && el.type !== 'chart' && (el.style as any).boxShadow && (el.style as any).boxShadow !== 'none') ? `drop-shadow(${(el.style as any).boxShadow})` : ''
|
|
2353
2354
|
}">
|
|
@@ -2759,6 +2760,17 @@ const masterGroupedLayouts = computed(() => {
|
|
|
2759
2760
|
</template>
|
|
2760
2761
|
</template>
|
|
2761
2762
|
|
|
2763
|
+
<template v-else-if="isReadonly">
|
|
2764
|
+
<div class="menu-item" @click="copySlide(rightClickedIndex)">
|
|
2765
|
+
<span class="menu-label">{{ locale === 'zh' ? '复制' : 'Copy' }}</span>
|
|
2766
|
+
<span class="menu-shortcut">Ctrl + C</span>
|
|
2767
|
+
</div>
|
|
2768
|
+
<div class="menu-item" @click="playFromCurrent">
|
|
2769
|
+
<span class="menu-label">{{ locale === 'zh' ? '从当前放映' : 'Play from Current' }}</span>
|
|
2770
|
+
<span class="menu-shortcut">Shift + F5</span>
|
|
2771
|
+
</div>
|
|
2772
|
+
</template>
|
|
2773
|
+
|
|
2762
2774
|
<template v-else>
|
|
2763
2775
|
<div class="menu-item" @click="cutSlide(rightClickedIndex)">
|
|
2764
2776
|
<span class="menu-label">{{ locale === 'zh' ? '剪切' : 'Cut' }}</span>
|
|
@@ -245,13 +245,43 @@ const colorOptions = [
|
|
|
245
245
|
{ name: '黑色系', hex: '#212121' }
|
|
246
246
|
]
|
|
247
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
|
+
|
|
248
272
|
const loading = ref(false)
|
|
249
|
-
const categoryTree = ref<MaterialCategory[]>([])
|
|
250
273
|
const pagedMaterials = ref<MaterialItem[]>([])
|
|
251
274
|
const currentPage = ref(1)
|
|
252
275
|
const pageSize = 24
|
|
253
276
|
const totalRecords = ref(0)
|
|
254
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
|
+
|
|
255
285
|
// 预设类型中文名映射
|
|
256
286
|
const typeNameMap: Record<string, string> = {
|
|
257
287
|
image: '图库图片',
|
|
@@ -279,11 +309,33 @@ async function safeFetchJson(url: string, options: RequestInit = {}): Promise<an
|
|
|
279
309
|
}
|
|
280
310
|
}
|
|
281
311
|
|
|
282
|
-
//
|
|
312
|
+
// 获取有效的公网素材搜索接口 URL(平滑兼容旧配置并默认使用 /jvs-dam/stock/search)
|
|
313
|
+
function getEffectiveStockUrl(): string {
|
|
314
|
+
let url = (materialPageApiUrl?.value && materialPageApiUrl.value.trim()) ||
|
|
315
|
+
(materialApiUrl?.value && materialApiUrl.value.trim()) ||
|
|
316
|
+
DEFAULT_MATERIAL_PAGE_API_URL
|
|
317
|
+
if (url.includes('/api/dam/material/tree') || url.includes('/api/dam/material/page')) {
|
|
318
|
+
url = DEFAULT_MATERIAL_PAGE_API_URL
|
|
319
|
+
}
|
|
320
|
+
return url
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 规范化素材数据项(支持 MediaResource 格式及自定义素材项)
|
|
283
324
|
function normalizeMaterialItem(item: any, defaultCatName = '', defaultType = ''): MaterialItem {
|
|
284
|
-
const
|
|
285
|
-
let
|
|
286
|
-
|
|
325
|
+
const subType = item.assetSubType || item.subType || ''
|
|
326
|
+
let type = item.type || defaultType || ''
|
|
327
|
+
if (!type) {
|
|
328
|
+
if (subType === 'GIF') type = 'gif'
|
|
329
|
+
else if (subType === 'Icons') type = 'sticker'
|
|
330
|
+
else if (subType === 'Vectors') type = 'svg'
|
|
331
|
+
else if (subType === 'Photos' || subType === 'Illustrations') type = 'image'
|
|
332
|
+
else if (item.assetType === 'VIDEO') type = 'video'
|
|
333
|
+
else type = 'image'
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let previewUrl = item.previewUrl || item.image || item.url || item.downloadUrl || item.sourceUrl || ''
|
|
337
|
+
let sourceUrl = item.sourceUrl || item.downloadUrl || item.url || previewUrl
|
|
338
|
+
let url = sourceUrl || previewUrl
|
|
287
339
|
|
|
288
340
|
if (type === 'slide') {
|
|
289
341
|
if (!previewUrl || previewUrl.includes('gold-cover.png')) {
|
|
@@ -294,19 +346,39 @@ function normalizeMaterialItem(item: any, defaultCatName = '', defaultType = '')
|
|
|
294
346
|
}
|
|
295
347
|
}
|
|
296
348
|
|
|
297
|
-
|
|
349
|
+
// 来源平台识别(如 PEXELS, PIXABAY, UNSPLASH, ICONIFY 等)
|
|
350
|
+
const platform = item.sourcePlatform || (item.id && typeof item.id === 'string' && item.id.includes('_') ? item.id.split('_')[0].toUpperCase() : '')
|
|
351
|
+
|
|
352
|
+
const fmt = item.format || getItemFormat({ ...item, url, previewUrl, sourceUrl, type, assetSubType: subType })
|
|
353
|
+
|
|
354
|
+
// 宽高比解析
|
|
355
|
+
let ratio = item.aspectRatio ? String(item.aspectRatio).replace(':', '/') : undefined
|
|
356
|
+
if (!ratio && item.width && item.height) {
|
|
357
|
+
ratio = `${item.width} / ${item.height}`
|
|
358
|
+
}
|
|
298
359
|
|
|
299
360
|
return {
|
|
300
361
|
...item,
|
|
362
|
+
id: item.id || `stock-${Math.random().toString(36).slice(2, 9)}`,
|
|
363
|
+
title: item.title || `${platform ? `${platform} ` : ''}${subType ? typeNameMap[type] || subType : '素材'}`,
|
|
301
364
|
type,
|
|
365
|
+
assetSubType: subType,
|
|
366
|
+
sourcePlatform: platform,
|
|
302
367
|
format: fmt,
|
|
303
|
-
category: item.category || defaultCatName || (
|
|
368
|
+
category: item.category || defaultCatName || (subType ? typeNameMap[type] || subType : '') || type || '素材',
|
|
304
369
|
previewUrl,
|
|
305
|
-
url
|
|
370
|
+
url,
|
|
371
|
+
sourceUrl,
|
|
372
|
+
color: item.color || item.primaryColor || '',
|
|
373
|
+
primaryColor: item.primaryColor || item.color || '',
|
|
374
|
+
aspectRatio: ratio || '4 / 3',
|
|
375
|
+
originalWidth: item.width || item.originalWidth,
|
|
376
|
+
originalHeight: item.height || item.originalHeight,
|
|
377
|
+
tags: Array.isArray(item.tags) ? item.tags : []
|
|
306
378
|
}
|
|
307
379
|
}
|
|
308
380
|
|
|
309
|
-
//
|
|
381
|
+
// 统一数据格式解析(兼容 jvs-dam-2.5 StockController R.ok(list) 及各种分页结构)
|
|
310
382
|
function parseMaterialList(data: any): MaterialItem[] {
|
|
311
383
|
if (!data) return []
|
|
312
384
|
let rawList: any[] = []
|
|
@@ -328,59 +400,20 @@ function parseMaterialList(data: any): MaterialItem[] {
|
|
|
328
400
|
return rawList.map(item => normalizeMaterialItem(item))
|
|
329
401
|
}
|
|
330
402
|
|
|
331
|
-
//
|
|
332
|
-
async function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
: DEFAULT_MATERIAL_API_URL)
|
|
338
|
-
|
|
339
|
-
if (!baseTreeUrl) return
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
loading.value = true
|
|
343
|
-
const q = query.trim()
|
|
344
|
-
const treeUrl = q
|
|
345
|
-
? `${baseTreeUrl}${baseTreeUrl.includes('?') ? '&' : '?'}title=${encodeURIComponent(q)}`
|
|
346
|
-
: baseTreeUrl
|
|
347
|
-
|
|
348
|
-
const data = await safeFetchJson(treeUrl, {
|
|
349
|
-
headers: {
|
|
350
|
-
...(materialHeaders.value || {})
|
|
351
|
-
}
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
if (Array.isArray(data?.data) && data.data.length > 0 && Array.isArray(data.data[0]?.items)) {
|
|
355
|
-
categoryTree.value = data.data.map((cat: any) => ({
|
|
356
|
-
...cat,
|
|
357
|
-
name: cat.name || (cat.type ? typeNameMap[cat.type] : '') || cat.type,
|
|
358
|
-
items: (cat.items || []).map((item: any) => normalizeMaterialItem(item, cat.name, cat.type))
|
|
359
|
-
}))
|
|
360
|
-
} else {
|
|
361
|
-
const flatList = parseMaterialList(data)
|
|
362
|
-
categoryTree.value = flatList.length > 0 ? [
|
|
363
|
-
{ id: 'all-items', type: 'image', name: '素材', hasMore: true, items: flatList }
|
|
364
|
-
] : []
|
|
365
|
-
}
|
|
366
|
-
} catch (err: any) {
|
|
367
|
-
console.error('加载素材树出错:', err)
|
|
368
|
-
} finally {
|
|
369
|
-
loading.value = false
|
|
403
|
+
// 核心检索方法:请求后端 stock/search 接口并发抓取 Pexels, Pixabay, Unsplash, Iconify 等平台
|
|
404
|
+
async function fetchMaterials(page = 1, append = false) {
|
|
405
|
+
// 单页模板独立处理
|
|
406
|
+
if (filterParams.activeType === 'slide') {
|
|
407
|
+
await fetchSlideTemplates(page, append)
|
|
408
|
+
return
|
|
370
409
|
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// 分页与异常熔断状态(彻底杜绝滚动到底部时的无限死循环请求)
|
|
374
|
-
const loadingMore = ref(false)
|
|
375
|
-
const loadFailed = ref(false)
|
|
376
|
-
const loadErrorMsg = ref('')
|
|
377
|
-
const reachEnd = ref(false)
|
|
378
|
-
let scrollThrottleTimer: number | null = null
|
|
379
410
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
411
|
+
const stockUrl = getEffectiveStockUrl()
|
|
412
|
+
if (!stockUrl) {
|
|
413
|
+
if (!append) {
|
|
414
|
+
pagedMaterials.value = []
|
|
415
|
+
totalRecords.value = 0
|
|
416
|
+
}
|
|
384
417
|
reachEnd.value = true
|
|
385
418
|
return
|
|
386
419
|
}
|
|
@@ -391,87 +424,119 @@ async function fetchCategoryPage(type: string, page = 1, append = false, query =
|
|
|
391
424
|
loadFailed.value = false
|
|
392
425
|
loadErrorMsg.value = ''
|
|
393
426
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const q = query.trim()
|
|
427
|
+
const params = new URLSearchParams()
|
|
428
|
+
params.append('current', String(page))
|
|
429
|
+
params.append('size', String(pageSize))
|
|
430
|
+
|
|
431
|
+
const q = filterParams.query.trim()
|
|
399
432
|
if (q) {
|
|
400
|
-
|
|
433
|
+
params.append('keyword', q)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const subType = tabToSubTypeMap[filterParams.activeType]
|
|
437
|
+
if (subType) {
|
|
438
|
+
params.append('subType', subType)
|
|
401
439
|
}
|
|
402
|
-
|
|
440
|
+
|
|
441
|
+
const fullUrl = `${stockUrl}${stockUrl.includes('?') ? '&' : '?'}${params.toString()}`
|
|
442
|
+
const data = await safeFetchJson(fullUrl, {
|
|
403
443
|
headers: {
|
|
404
444
|
...(materialHeaders.value || {})
|
|
405
445
|
}
|
|
406
446
|
})
|
|
407
447
|
|
|
448
|
+
// 针对接口返回非成功状态(如未登录 code: -2 或业务异常)
|
|
449
|
+
if (data && data.code !== undefined && data.code !== 0 && data.code !== 200) {
|
|
450
|
+
if (!append) {
|
|
451
|
+
pagedMaterials.value = []
|
|
452
|
+
totalRecords.value = 0
|
|
453
|
+
}
|
|
454
|
+
reachEnd.value = true
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
408
458
|
const newRecords = parseMaterialList(data)
|
|
409
459
|
|
|
410
|
-
|
|
411
|
-
if (newRecords.length === 0) {
|
|
460
|
+
if (newRecords.length === 0 || newRecords.length < pageSize) {
|
|
412
461
|
reachEnd.value = true
|
|
413
462
|
} else {
|
|
414
463
|
reachEnd.value = false
|
|
415
464
|
}
|
|
416
465
|
|
|
417
466
|
if (append) {
|
|
418
|
-
|
|
467
|
+
const existingIds = new Set(pagedMaterials.value.map(i => i.id))
|
|
468
|
+
const uniqueItems = newRecords.filter(i => !existingIds.has(i.id))
|
|
469
|
+
pagedMaterials.value = [...pagedMaterials.value, ...uniqueItems]
|
|
419
470
|
} else {
|
|
420
471
|
pagedMaterials.value = newRecords
|
|
421
472
|
}
|
|
473
|
+
|
|
422
474
|
currentPage.value = page
|
|
423
|
-
totalRecords.value = data
|
|
475
|
+
totalRecords.value = data?.data?.total ?? data?.total ?? (reachEnd.value ? pagedMaterials.value.length : 9999)
|
|
424
476
|
} catch (err: any) {
|
|
425
|
-
console.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
477
|
+
console.warn('[Stock Search] 加载公网素材出错:', err)
|
|
478
|
+
if (!append) {
|
|
479
|
+
pagedMaterials.value = []
|
|
480
|
+
totalRecords.value = 0
|
|
481
|
+
reachEnd.value = true
|
|
482
|
+
} else {
|
|
483
|
+
loadFailed.value = true
|
|
484
|
+
loadErrorMsg.value = err.message || '网络连接或接口异常'
|
|
485
|
+
}
|
|
429
486
|
} finally {
|
|
430
487
|
loading.value = false
|
|
431
488
|
loadingMore.value = false
|
|
432
489
|
}
|
|
433
490
|
}
|
|
434
491
|
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
492
|
+
// 加载单页 PPT 模板
|
|
493
|
+
async function fetchSlideTemplates(page = 1, append = false) {
|
|
494
|
+
try {
|
|
495
|
+
loading.value = !append
|
|
496
|
+
loadingMore.value = append
|
|
497
|
+
loadFailed.value = false
|
|
498
|
+
loadErrorMsg.value = ''
|
|
499
|
+
|
|
500
|
+
const data = await safeFetchJson('/templates/index.json')
|
|
501
|
+
const templates = parseMaterialList(data)
|
|
502
|
+
if (templates.length > 0) {
|
|
503
|
+
pagedMaterials.value = templates
|
|
504
|
+
totalRecords.value = templates.length
|
|
505
|
+
reachEnd.value = true
|
|
506
|
+
} else {
|
|
507
|
+
pagedMaterials.value = []
|
|
508
|
+
totalRecords.value = 0
|
|
509
|
+
reachEnd.value = true
|
|
449
510
|
}
|
|
450
|
-
|
|
511
|
+
} catch (err: any) {
|
|
512
|
+
pagedMaterials.value = []
|
|
513
|
+
totalRecords.value = 0
|
|
514
|
+
reachEnd.value = true
|
|
515
|
+
} finally {
|
|
516
|
+
loading.value = false
|
|
517
|
+
loadingMore.value = false
|
|
451
518
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
})
|
|
458
|
-
return items
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 聚合当前素材列表
|
|
522
|
+
const rawSourceMaterials = computed<MaterialItem[]>(() => {
|
|
523
|
+
return pagedMaterials.value
|
|
459
524
|
})
|
|
460
525
|
|
|
461
526
|
// 是否还有更多数据可供加载(若已失败或已到底,立即返回 false,防止触底循环)
|
|
462
527
|
const hasMore = computed(() => {
|
|
463
528
|
if (reachEnd.value || loadFailed.value) return false
|
|
464
529
|
if (totalRecords.value > 0) {
|
|
465
|
-
return
|
|
530
|
+
return pagedMaterials.value.length < totalRecords.value
|
|
466
531
|
}
|
|
467
|
-
return
|
|
532
|
+
return !reachEnd.value
|
|
468
533
|
})
|
|
469
534
|
|
|
470
535
|
// 加载更多分页数据
|
|
471
536
|
async function loadMore() {
|
|
472
537
|
if (loading.value || loadingMore.value || !hasMore.value || loadFailed.value || reachEnd.value) return
|
|
473
538
|
const nextPage = currentPage.value + 1
|
|
474
|
-
await
|
|
539
|
+
await fetchMaterials(nextPage, true)
|
|
475
540
|
}
|
|
476
541
|
|
|
477
542
|
// 手动点击重试(重置失败状态并重新触发加载)
|
|
@@ -479,7 +544,7 @@ async function retryLoadMore() {
|
|
|
479
544
|
loadFailed.value = false
|
|
480
545
|
loadErrorMsg.value = ''
|
|
481
546
|
const targetPage = currentPage.value + 1
|
|
482
|
-
await
|
|
547
|
+
await fetchMaterials(targetPage, true)
|
|
483
548
|
}
|
|
484
549
|
|
|
485
550
|
// 瀑布流滚动触底自动加载监听(带 250ms 节流与熔断保护)
|
|
@@ -505,7 +570,15 @@ const displayedMaterials = computed<MaterialItem[]>(() => {
|
|
|
505
570
|
return rawSourceMaterials.value.filter(item => {
|
|
506
571
|
// 1. 类型过滤 (红框2)
|
|
507
572
|
if (filterParams.activeType !== 'all') {
|
|
508
|
-
if (
|
|
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
|
+
}
|
|
509
582
|
}
|
|
510
583
|
// 2. 格式过滤 (红框3)
|
|
511
584
|
if (filterParams.format) {
|
|
@@ -513,8 +586,9 @@ const displayedMaterials = computed<MaterialItem[]>(() => {
|
|
|
513
586
|
if (fmt.toUpperCase() !== filterParams.format.toUpperCase()) return false
|
|
514
587
|
}
|
|
515
588
|
// 3. 颜色过滤 (红框3)
|
|
516
|
-
if (filterParams.color
|
|
517
|
-
|
|
589
|
+
if (filterParams.color) {
|
|
590
|
+
const c = (item.color || item.primaryColor || '').toLowerCase()
|
|
591
|
+
if (!c || c !== filterParams.color.toLowerCase()) return false
|
|
518
592
|
}
|
|
519
593
|
return true
|
|
520
594
|
})
|
|
@@ -541,22 +615,18 @@ async function switchCategoryTab(typeValue: string) {
|
|
|
541
615
|
filterParams.activeType = typeValue
|
|
542
616
|
currentPage.value = 1
|
|
543
617
|
pagedMaterials.value = []
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
await loadTreeMaterials(filterParams.query)
|
|
548
|
-
}
|
|
618
|
+
reachEnd.value = false
|
|
619
|
+
loadFailed.value = false
|
|
620
|
+
await fetchMaterials(1, false)
|
|
549
621
|
}
|
|
550
622
|
|
|
551
623
|
// 搜索处理(红框1)
|
|
552
624
|
async function handleSearch() {
|
|
553
625
|
currentPage.value = 1
|
|
554
626
|
pagedMaterials.value = []
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
await loadTreeMaterials(filterParams.query)
|
|
559
|
-
}
|
|
627
|
+
reachEnd.value = false
|
|
628
|
+
loadFailed.value = false
|
|
629
|
+
await fetchMaterials(1, false)
|
|
560
630
|
}
|
|
561
631
|
|
|
562
632
|
function clearQuery() {
|
|
@@ -591,9 +661,23 @@ function handleSelectMaterial(meta: MaterialItem) {
|
|
|
591
661
|
if (meta.type === 'slide' || meta.slide) {
|
|
592
662
|
handleInsertSlide(meta)
|
|
593
663
|
} else {
|
|
594
|
-
|
|
664
|
+
// 优先采用高清 sourceUrl,回退到 url / previewUrl / downloadUrl
|
|
665
|
+
const imgUrl = meta.sourceUrl || meta.url || meta.previewUrl || meta.downloadUrl
|
|
595
666
|
if (imgUrl) {
|
|
596
|
-
|
|
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)
|
|
597
681
|
notify(`已成功添加素材 "${meta.title || '图片'}" 到画布`)
|
|
598
682
|
} else {
|
|
599
683
|
notify('该素材暂无可用的图片地址')
|
|
@@ -650,7 +734,7 @@ async function handleInsertSlide(meta: MaterialItem) {
|
|
|
650
734
|
}
|
|
651
735
|
|
|
652
736
|
onMounted(() => {
|
|
653
|
-
|
|
737
|
+
fetchMaterials(1, false)
|
|
654
738
|
|
|
655
739
|
// 监听容器大小以实现极致自适应
|
|
656
740
|
if (containerRef.value) {
|
|
@@ -674,8 +758,8 @@ onUnmounted(() => {
|
|
|
674
758
|
window.removeEventListener('keydown', handleKeydown)
|
|
675
759
|
})
|
|
676
760
|
|
|
677
|
-
watch(() => materialTreeApiUrl?.value, () => {
|
|
678
|
-
|
|
761
|
+
watch([() => materialApiUrl?.value, () => materialPageApiUrl?.value, () => materialTreeApiUrl?.value], () => {
|
|
762
|
+
fetchMaterials(1, false)
|
|
679
763
|
})
|
|
680
764
|
</script>
|
|
681
765
|
|
|
@@ -884,6 +968,7 @@ watch(() => materialTreeApiUrl?.value, () => {
|
|
|
884
968
|
<div class="card-bottom-info">
|
|
885
969
|
<div class="format-badge-box">
|
|
886
970
|
<span class="format-tag">{{ getItemFormat(item) }}</span>
|
|
971
|
+
<span v-if="item.sourcePlatform" class="platform-tag">{{ item.sourcePlatform }}</span>
|
|
887
972
|
<span v-if="item.title" class="format-title-hint" :title="item.title">{{ item.title }}</span>
|
|
888
973
|
</div>
|
|
889
974
|
<button type="button" class="more-options-dot-btn" @click.stop="openItemMenu(item)">
|
|
@@ -1483,6 +1568,19 @@ watch(() => materialTreeApiUrl?.value, () => {
|
|
|
1483
1568
|
flex-shrink: 0;
|
|
1484
1569
|
}
|
|
1485
1570
|
|
|
1571
|
+
.platform-tag {
|
|
1572
|
+
font-size: 9px;
|
|
1573
|
+
font-weight: 700;
|
|
1574
|
+
color: #2563eb;
|
|
1575
|
+
background: #eff6ff;
|
|
1576
|
+
border: 1px solid #dbeafe;
|
|
1577
|
+
padding: 1px 5px;
|
|
1578
|
+
border-radius: 3px;
|
|
1579
|
+
letter-spacing: 0.4px;
|
|
1580
|
+
flex-shrink: 0;
|
|
1581
|
+
text-transform: uppercase;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1486
1584
|
.format-title-hint {
|
|
1487
1585
|
font-size: 11px;
|
|
1488
1586
|
color: #64748b;
|
|
@@ -22,7 +22,7 @@ import { getFontVariantStyle } from '../../../configs/fonts'
|
|
|
22
22
|
import { useSlides } from '../../../composables/useSlides'
|
|
23
23
|
import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../textSelectionOverlay'
|
|
24
24
|
|
|
25
|
-
const { shapeFormatPainter } = useSlides()
|
|
25
|
+
const { shapeFormatPainter, isReadonly } = useSlides()
|
|
26
26
|
const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
|
|
27
27
|
const isTextSelectable = computed(() => (props.isEditingText && props.selectedElementId === props.element.id) || isTextPaintActive.value)
|
|
28
28
|
|
|
@@ -262,7 +262,7 @@ function formatShapeHtml(el: any): string {
|
|
|
262
262
|
<div
|
|
263
263
|
class="element-content shape-element-content"
|
|
264
264
|
style="width: 100%; height: 100%; position: relative;"
|
|
265
|
-
@dblclick.stop.prevent="isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event))"
|
|
265
|
+
@dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
|
|
266
266
|
>
|
|
267
267
|
<!-- Circle Shape (with fill) -->
|
|
268
268
|
<div
|
|
@@ -666,7 +666,7 @@ function formatShapeHtml(el: any): string {
|
|
|
666
666
|
@keydown="emit('textKeyDown', $event)"
|
|
667
667
|
@mousemove="emit('mousemove', $event)"
|
|
668
668
|
@mouseleave="emit('mouseleave', $event)"
|
|
669
|
-
@dblclick.stop.prevent="isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event))"
|
|
669
|
+
@dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
|
|
670
670
|
class="shape-text-overlay"
|
|
671
671
|
:style="{
|
|
672
672
|
display: 'block',
|