jvs-ppt-ui 0.0.18 → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jvs-ppt-ui",
3
3
  "private": false,
4
- "version": "0.0.18",
4
+ "version": "0.0.19",
5
5
  "type": "module",
6
6
  "main": "./dist/lib/jvs-ppt-ui.umd.js",
7
7
  "module": "./dist/lib/jvs-ppt-ui.es.js",
@@ -108,7 +108,12 @@ const subTypeNameMap: Record<StockSubType, string> = {
108
108
 
109
109
  // 顶层 3-Tab 状态(检索 / 我的素材 / 创建素材)与 jvs_session_v3 鉴权
110
110
  export type MaterialMainTab = 'search' | 'mine' | 'create'
111
- const { isDamLoggedIn, sharedMaterialMainTab: activeMainTab, refreshSessionState } = useDamAuth()
111
+ const {
112
+ isDamLoggedIn,
113
+ sharedMaterialMainTab: activeMainTab,
114
+ refreshSessionState,
115
+ probeRemoteSessionV3
116
+ } = useDamAuth()
112
117
 
113
118
  const myAssetsRef = ref<InstanceType<typeof MyAssetsPanel> | null>(null)
114
119
  const createPanelRef = ref<InstanceType<typeof CreateMaterialPanel> | null>(null)
@@ -129,11 +134,14 @@ function showGuestAuthToast(tabName: string) {
129
134
  }, 2200)
130
135
  }
131
136
 
132
- function handleSwitchMainTab(tab: MaterialMainTab) {
137
+ async function handleSwitchMainTab(tab: MaterialMainTab) {
133
138
  refreshSessionState()
134
139
  if (tab !== 'search' && !isDamLoggedIn.value) {
135
- showGuestAuthToast(tab === 'mine' ? '我的素材' : '创建素材')
136
- return
140
+ const verified = await probeRemoteSessionV3(true)
141
+ if (!verified && !isDamLoggedIn.value) {
142
+ showGuestAuthToast(tab === 'mine' ? '我的素材' : '创建素材')
143
+ return
144
+ }
137
145
  }
138
146
  activeMainTab.value = tab
139
147
  }
@@ -351,15 +359,18 @@ function openCustomListPreview(item: MaterialItem, list: MaterialItem[], e?: Eve
351
359
  }
352
360
 
353
361
  // 将「检索」或「我的素材」中的图片一键送入「创建素材」作为参考图
354
- function handleSendToCreateRef(item: MaterialItem, e?: Event) {
362
+ async function handleSendToCreateRef(item: MaterialItem, e?: Event) {
355
363
  if (e) {
356
364
  e.stopPropagation()
357
365
  e.preventDefault()
358
366
  }
359
367
  refreshSessionState()
360
368
  if (!isDamLoggedIn.value) {
361
- showGuestAuthToast('创建素材')
362
- return
369
+ const verified = await probeRemoteSessionV3(true)
370
+ if (!verified && !isDamLoggedIn.value) {
371
+ showGuestAuthToast('创建素材')
372
+ return
373
+ }
363
374
  }
364
375
  const thumbUrl = getMaterialThumb(item)
365
376
  if (!thumbUrl) {
@@ -522,20 +522,13 @@ function extractUploadedUrl(res: any): string {
522
522
  async function handleFileUploadChange(e: Event) {
523
523
  const input = e.target as HTMLInputElement
524
524
  const files = input.files ? Array.from(input.files) : []
525
- input.value = ''
526
525
  if (files.length === 0) return
527
526
 
528
527
  isUploading.value = true
529
528
  try {
530
- // 确保获取到默认项目 ID
531
- let targetProjId = selectedProjectId.value
532
- if (!targetProjId) {
533
- if (projects.value.length === 0) {
534
- await fetchProjectsAndFolders()
535
- }
536
- const personal = projects.value.find(p => p.type === 'PERSONAL') || projects.value[0]
537
- targetProjId = personal?.id || ''
538
- }
529
+ // 若用户已在下拉树中选择了具体项目则传入,否则交由后端 /jvs-dam/upload/file 自动归入当前用户的个人项目
530
+ const personal = projects.value.find(p => p.type === 'PERSONAL') || projects.value[0]
531
+ const targetProjId = selectedProjectId.value || personal?.id || ''
539
532
 
540
533
  let uploadedCount = 0
541
534
  for (const file of files) {
@@ -544,6 +537,7 @@ async function handleFileUploadChange(e: Event) {
544
537
  formData.append('file', file)
545
538
  formData.append('path', '/dam/media/')
546
539
  formData.append('fileName', file.name)
540
+ formData.append('name', file.name)
547
541
  formData.append('id', String(Date.now()))
548
542
  if (targetProjId) formData.append('projectId', targetProjId)
549
543
  if (selectedFolderId.value) formData.append('folderId', selectedFolderId.value)
@@ -556,25 +550,6 @@ async function handleFileUploadChange(e: Event) {
556
550
  const fileUrl = extractUploadedUrl(uploadRes)
557
551
  if (fileUrl) {
558
552
  uploadedCount++
559
- // 同步登记为数字资产(若后端 upload/file 未自动入库 asset 表,则补偿调用 POST /asset)
560
- if (targetProjId) {
561
- try {
562
- await damFetchJson('/asset', {
563
- method: 'POST',
564
- body: JSON.stringify({
565
- projectId: targetProjId,
566
- folderId: selectedFolderId.value || undefined,
567
- name: file.name,
568
- assetType: isVid ? 'VIDEO' : 'IMAGE',
569
- sourceType: 'UPLOAD',
570
- fileUrl,
571
- fileExtension: file.name.split('.').pop()?.toLowerCase() || (isVid ? 'mp4' : 'png')
572
- })
573
- })
574
- } catch {
575
- // 忽略重复登记异常
576
- }
577
- }
578
553
  }
579
554
  }
580
555
 
@@ -588,6 +563,7 @@ async function handleFileUploadChange(e: Event) {
588
563
  notify(`上传素材失败: ${err.message || '网络异常'}`)
589
564
  } finally {
590
565
  isUploading.value = false
566
+ input.value = ''
591
567
  }
592
568
  }
593
569
 
@@ -44,24 +44,63 @@ function readStorageKey(storage: Storage | undefined, key: string): string {
44
44
  return ''
45
45
  }
46
46
 
47
+ /**
48
+ * 向 Storage 写入 jvs- 标准包装格式数据(对齐 jvs-material-ui / jvs-narrivo-ai-ui 的 setStore 规范)
49
+ */
50
+ function writeJvsStore(name: string, content: string): void {
51
+ if (typeof window === 'undefined' || !window.localStorage || !name) return
52
+ try {
53
+ const payload = JSON.stringify({
54
+ dataType: typeof content,
55
+ content,
56
+ datetime: Date.now()
57
+ })
58
+ window.localStorage.setItem(`jvs-${name}`, payload)
59
+ if (window.sessionStorage) {
60
+ window.sessionStorage.setItem(`jvs-${name}`, payload)
61
+ }
62
+ } catch {
63
+ // 忽略隐私模式或配额异常
64
+ }
65
+ }
66
+
67
+ function removeJvsStore(name: string): void {
68
+ if (typeof window === 'undefined') return
69
+ try {
70
+ window.localStorage?.removeItem(`jvs-${name}`)
71
+ window.localStorage?.removeItem(name)
72
+ window.sessionStorage?.removeItem(`jvs-${name}`)
73
+ window.sessionStorage?.removeItem(name)
74
+ } catch {
75
+ // ignore
76
+ }
77
+ }
78
+
47
79
  /**
48
80
  * 提取 jvs_session_v3 登录凭证
49
81
  * 判定顺序:
50
- * 1. document.cookie 中的 jvs_session_v3
51
- * 2. localStorage / sessionStorage 中的 jvs_session_v3、jvs-jvs_session_v3、jvs-sessionV3、sessionV3
52
- * 3. materialHeaders 中显式传入的 jvs_session_v3
82
+ * 1. document.cookie 中的 jvs_session_v3 或 Set-Cookie(非 HttpOnly 场景)
83
+ * 2. localStorage / sessionStorage 中的 jvs_session_v3、jvs-jvs_session_v3、jvs-sessionV3、sessionV3、jvs-access_token
84
+ * 3. materialHeaders 中显式传入的 jvs_session_v3 或 Authorization
53
85
  */
54
86
  export function resolveJvsSessionV3(extraHeaders?: Record<string, string>): string {
55
87
  if (typeof window === 'undefined') return ''
56
88
 
57
- // 1. Cookie: jvs_session_v3
58
- const fromCookie = getCookieValue('jvs_session_v3')
89
+ // 1. Cookie: jvs_session_v3(若未开启 HttpOnly 可直接读取)
90
+ const fromCookie = getCookieValue('jvs_session_v3') || getCookieValue('Set-Cookie')
59
91
  if (fromCookie && fromCookie.trim()) {
60
92
  return fromCookie.trim()
61
93
  }
62
94
 
63
95
  // 2. LocalStorage / SessionStorage
64
- const candidateKeys = ['jvs_session_v3', 'jvs-jvs_session_v3', 'jvs-sessionV3', 'sessionV3']
96
+ const candidateKeys = [
97
+ 'jvs_session_v3',
98
+ 'jvs-jvs_session_v3',
99
+ 'jvs-sessionV3',
100
+ 'sessionV3',
101
+ 'jvs-access_token',
102
+ 'access_token'
103
+ ]
65
104
  for (const key of candidateKeys) {
66
105
  const localVal = readStorageKey(window.localStorage, key)
67
106
  if (localVal) return localVal
@@ -71,7 +110,11 @@ export function resolveJvsSessionV3(extraHeaders?: Record<string, string>): stri
71
110
 
72
111
  // 3. 外部注入的 Headers
73
112
  if (extraHeaders) {
74
- const headerVal = extraHeaders['jvs_session_v3'] || extraHeaders['JVS_SESSION_V3']
113
+ const headerVal =
114
+ extraHeaders['jvs_session_v3'] ||
115
+ extraHeaders['JVS_SESSION_V3'] ||
116
+ (extraHeaders['Authorization'] ? extraHeaders['Authorization'].replace(/^Bearer\s+/i, '') : '') ||
117
+ (extraHeaders['authorization'] ? extraHeaders['authorization'].replace(/^Bearer\s+/i, '') : '')
75
118
  if (typeof headerVal === 'string' && headerVal.trim()) {
76
119
  return headerVal.trim()
77
120
  }
@@ -80,6 +123,21 @@ export function resolveJvsSessionV3(extraHeaders?: Record<string, string>): stri
80
123
  return ''
81
124
  }
82
125
 
126
+ /**
127
+ * 提取服务网关 Origin 前缀(如配置了完整域名则复用该域名,否则使用当前站点相对路径 '')
128
+ */
129
+ export function resolveGatewayOrigin(configuredUrl?: string): string {
130
+ if (configuredUrl && /^https?:\/\//i.test(configuredUrl.trim())) {
131
+ try {
132
+ const u = new URL(configuredUrl.trim())
133
+ return u.origin
134
+ } catch {
135
+ return ''
136
+ }
137
+ }
138
+ return ''
139
+ }
140
+
83
141
  /**
84
142
  * 提取 DAM 服务基础路径(默认 /jvs-dam)
85
143
  */
@@ -101,6 +159,16 @@ export type DamMainTab = 'search' | 'mine' | 'create'
101
159
  */
102
160
  const sharedMaterialMainTab = ref<DamMainTab>('search')
103
161
 
162
+ /**
163
+ * 针对主域 .bctools.cn 下 HttpOnly Cookie (jvs_session_v3) 的服务端换取/探测状态
164
+ * 说明:当 jvs_session_v3 设置了 HttpOnly=true 时,前端 document.cookie 无法直接读取,
165
+ * 需通过同域/跨子域携带 Cookie 请求 /jvs-system/login/token(对齐 jvs-material-ui / jvs-narrivo-ai-ui)换取 sessionV3。
166
+ */
167
+ const remoteSessionToken = ref<string>('')
168
+ const remoteCookieSessionVerified = ref<boolean>(false)
169
+ let inFlightSessionProbe: Promise<boolean> | null = null
170
+ let hasInitialProbed = false
171
+
104
172
  export function setSharedMaterialMainTab(tab: DamMainTab) {
105
173
  sharedMaterialMainTab.value = tab
106
174
  }
@@ -113,20 +181,157 @@ export function useDamAuth() {
113
181
  const sessionTick = ref(0)
114
182
  let pollTimer: ReturnType<typeof setInterval> | null = null
115
183
 
184
+ const damBaseUrl = computed(() => resolveDamBaseUrl(materialApiUrl.value))
185
+ const gatewayOrigin = computed(() => resolveGatewayOrigin(materialApiUrl.value))
186
+
187
+ /**
188
+ * 通过服务端接口换取/校验主域 (.bctools.cn) 下的 HttpOnly jvs_session_v3 Cookie
189
+ */
190
+ async function probeRemoteSessionV3(force = false): Promise<boolean> {
191
+ if (typeof window === 'undefined') return false
192
+
193
+ const localToken = resolveJvsSessionV3(materialHeaders.value) || remoteSessionToken.value
194
+ if (localToken && !force && hasInitialProbed) {
195
+ return true
196
+ }
197
+
198
+ if (inFlightSessionProbe) {
199
+ return inFlightSessionProbe
200
+ }
201
+
202
+ inFlightSessionProbe = (async () => {
203
+ hasInitialProbed = true
204
+ const origin = gatewayOrigin.value
205
+
206
+ // 1. 标准 JVS 换取方式:请求 /jvs-system/login/token(浏览器自动携带 .bctools.cn 下的 HttpOnly jvs_session_v3)
207
+ try {
208
+ const tokenResp = await fetch(`${origin}/jvs-system/login/token`, {
209
+ method: 'GET',
210
+ credentials: 'include',
211
+ headers: {
212
+ 'jvs-rule-ua': encodeURI('2.4'),
213
+ ...(materialHeaders.value || {})
214
+ }
215
+ })
216
+
217
+ if (tokenResp.ok) {
218
+ const text = (await tokenResp.text()).trim()
219
+ if (text && !text.startsWith('<')) {
220
+ const parsed = JSON.parse(text)
221
+ const code = parsed?.code ?? parsed?.data?.code
222
+ const tokenData = parsed?.data?.data !== undefined ? parsed.data.data : parsed?.data
223
+
224
+ if (code === 0) {
225
+ const tokenStr =
226
+ typeof tokenData === 'string'
227
+ ? tokenData.trim()
228
+ : typeof tokenData?.access_token === 'string'
229
+ ? tokenData.access_token.trim()
230
+ : ''
231
+
232
+ if (tokenStr) {
233
+ remoteSessionToken.value = tokenStr
234
+ remoteCookieSessionVerified.value = true
235
+ writeJvsStore('sessionV3', tokenStr)
236
+ writeJvsStore('access_token', tokenStr)
237
+ sessionTick.value++
238
+
239
+ // 若本地尚无 tenantId,顺带拉取当前租户 ID
240
+ if (!readStorageKey(window.localStorage, 'jvs-tenantId')) {
241
+ fetch(`${origin}/jvs-system/index/this/tenant`, {
242
+ method: 'GET',
243
+ credentials: 'include',
244
+ headers: {
245
+ 'jvs-rule-ua': encodeURI('2.4'),
246
+ Authorization: `Bearer ${tokenStr}`
247
+ }
248
+ })
249
+ .then(r => r.json())
250
+ .then(tRes => {
251
+ const tData = tRes?.data?.data || tRes?.data
252
+ if (tData?.id) {
253
+ writeJvsStore('tenantId', String(tData.id))
254
+ }
255
+ })
256
+ .catch(() => {})
257
+ }
258
+
259
+ return true
260
+ } else {
261
+ // /jvs-system/login/token 明确返回 code=0 但无 token,说明主域未登录或已退出
262
+ remoteSessionToken.value = ''
263
+ remoteCookieSessionVerified.value = false
264
+ removeJvsStore('sessionV3')
265
+ removeJvsStore('access_token')
266
+ sessionTick.value++
267
+ return false
268
+ }
269
+ }
270
+ }
271
+ }
272
+ } catch {
273
+ // 忽略 /jvs-system/login/token 网络或未代理异常,继续尝试步骤 2
274
+ }
275
+
276
+ // 2. 兜底探测:若当前站点未代理 /jvs-system,但代理了 /jvs-dam 且网关直接透传 .bctools.cn 的 HttpOnly Cookie
277
+ try {
278
+ const probeResp = await fetch(`${damBaseUrl.value}/project/my`, {
279
+ method: 'GET',
280
+ credentials: 'include',
281
+ headers: buildDamHeaders(false)
282
+ })
283
+ if (probeResp.ok) {
284
+ const text = (await probeResp.text()).trim()
285
+ if (text && !text.startsWith('<')) {
286
+ const parsed = JSON.parse(text)
287
+ const code = parsed?.code ?? parsed?.data?.code
288
+ if (code === 0 || code === 200) {
289
+ remoteCookieSessionVerified.value = true
290
+ sessionTick.value++
291
+ return true
292
+ }
293
+ }
294
+ }
295
+ } catch {
296
+ // ignore
297
+ }
298
+
299
+ return Boolean(resolveJvsSessionV3(materialHeaders.value))
300
+ })()
301
+
302
+ try {
303
+ return await inFlightSessionProbe
304
+ } finally {
305
+ inFlightSessionProbe = null
306
+ }
307
+ }
308
+
116
309
  const refreshSessionState = () => {
117
310
  sessionTick.value++
311
+ if (!resolveJvsSessionV3(materialHeaders.value) && !remoteSessionToken.value && !remoteCookieSessionVerified.value) {
312
+ void probeRemoteSessionV3(false)
313
+ }
314
+ }
315
+
316
+ const handleWindowFocus = () => {
317
+ sessionTick.value++
318
+ if (!resolveJvsSessionV3(materialHeaders.value) && !remoteSessionToken.value && !remoteCookieSessionVerified.value) {
319
+ void probeRemoteSessionV3(false)
320
+ }
118
321
  }
119
322
 
120
323
  onMounted(() => {
121
324
  window.addEventListener('storage', refreshSessionState)
122
- window.addEventListener('focus', refreshSessionState)
123
- // 定期轻量同步 Cookie 中 jvs_session_v3 的变化
124
- pollTimer = setInterval(refreshSessionState, 2000)
325
+ window.addEventListener('focus', handleWindowFocus)
326
+ // 立即探测一次主域 (.bctools.cn) HttpOnly jvs_session_v3 登录态
327
+ void probeRemoteSessionV3(false)
328
+ // 定期轻量同步状态变化
329
+ pollTimer = setInterval(refreshSessionState, 3000)
125
330
  })
126
331
 
127
332
  onUnmounted(() => {
128
333
  window.removeEventListener('storage', refreshSessionState)
129
- window.removeEventListener('focus', refreshSessionState)
334
+ window.removeEventListener('focus', handleWindowFocus)
130
335
  if (pollTimer) {
131
336
  clearInterval(pollTimer)
132
337
  pollTimer = null
@@ -134,15 +339,15 @@ export function useDamAuth() {
134
339
  })
135
340
 
136
341
  const sessionV3 = computed(() => {
137
- // 依赖 sessionTick 以感知 cookie 变化
138
342
  void sessionTick.value
139
- return resolveJvsSessionV3(materialHeaders.value)
343
+ return remoteSessionToken.value || resolveJvsSessionV3(materialHeaders.value)
140
344
  })
141
345
 
142
- /** 是否已登录(仅当 jvs_session_v3 存在有效内容时为 true) */
143
- const isDamLoggedIn = computed(() => Boolean(sessionV3.value))
144
-
145
- const damBaseUrl = computed(() => resolveDamBaseUrl(materialApiUrl.value))
346
+ /** 是否已登录(jvs_session_v3 存在有效内容,或通过主域 HttpOnly Cookie 换取/验证通过) */
347
+ const isDamLoggedIn = computed(() => {
348
+ void sessionTick.value
349
+ return Boolean(sessionV3.value || remoteCookieSessionVerified.value)
350
+ })
146
351
 
147
352
  /**
148
353
  * 构造携带鉴权信息的 Headers
@@ -233,6 +438,7 @@ export function useDamAuth() {
233
438
  sharedMaterialMainTab,
234
439
  setSharedMaterialMainTab,
235
440
  refreshSessionState,
441
+ probeRemoteSessionV3,
236
442
  buildDamHeaders,
237
443
  damFetchJson
238
444
  }