diff --git a/电商AI平台/assets/icons.js b/电商AI平台/assets/icons.js index c5b55b4..44ff598 100644 --- a/电商AI平台/assets/icons.js +++ b/电商AI平台/assets/icons.js @@ -1,6 +1,7 @@ (function () { const PATHS = { home: '', + layoutDashboard: '', package: '', boxes: '', clapperboard: '', @@ -8,7 +9,8 @@ video: '', sparkles: '', images: '', - library: '', + folder: '', + library: '', users: '', wallet: '', creditCard: '', @@ -50,10 +52,11 @@ }; const ALIASES = { - dashboard: 'home', + dashboard: 'layoutDashboard', products: 'package', projects: 'clapperboard', assetFactory: 'sparkles', + library: 'folder', account: 'creditCard', billing: 'creditCard', team: 'users', diff --git a/电商AI平台/assets/shell.js b/电商AI平台/assets/shell.js index da9f0ed..ee32507 100644 --- a/电商AI平台/assets/shell.js +++ b/电商AI平台/assets/shell.js @@ -12,7 +12,7 @@ const ShellIcon = (name, opts) => window.IconKit ? window.IconKit.svg(name, opts const NAV = [ { id: 'dashboard', label: '工作台', href: 'index.html', - icon: ShellIcon('home') + icon: ShellIcon('dashboard') }, { id: 'products', label: '商品库', href: 'products.html', badge: '7', @@ -28,7 +28,7 @@ const NAV = [ }, { id: 'library', label: '资产库', href: 'library.html', - icon: ShellIcon('images') + icon: ShellIcon('library') }, { id: 'team', label: '团队', href: 'team.html', diff --git a/电商AI平台/library.html b/电商AI平台/library.html index 011a6f8..070402b 100644 --- a/电商AI平台/library.html +++ b/电商AI平台/library.html @@ -306,7 +306,7 @@
-
场 1 · 深夜办公桌完成
-
15s · 1080×1920 · ¥0.45
-
- - +
场 1 · 深夜办公桌完成
+
15s · 1080×1920 · ¥0.45
+
+ + +
@@ -1896,11 +1935,12 @@
-
场 2 · 面膜包装/特写完成
-
12s · 1080×1920 · ¥0.45
-
- - +
场 2 · 面膜包装/特写完成
+
12s · 1080×1920 · ¥0.45
+
+ + +
@@ -1910,11 +1950,12 @@
-
场 3 · 化妆台/产品定格完成
-
13s · 1080×1920 · ¥0.45
-
- - +
场 3 · 化妆台/产品定格完成
+
13s · 1080×1920 · ¥0.45
+
+ + +
@@ -4497,15 +4538,19 @@ function openTriLightbox(ver, isAdopted, prodName) { const sec = s - m * 60; return String(m).padStart(2,'0') + ':' + sec.toFixed(2).padStart(5,'0'); } - // 磁吸时间轴 · 仅 data-dur (当前时长 · 秒) + data-max (源最大 · 可恢复至此) + // 时间轴数据 · data-start(时间线起点) + data-dur(当前时长) + data-max(源最大) + data-in(源素材入点) const MIN_DUR = 0.2; const $laneB = document.querySelector('#ed-timeline .bgm-track .lane'); const lanes = { video: $laneV, subtitle: $laneS, bgm: $laneB }; + const ALIGN_EPS = 0.12; + const $alignGuide = document.createElement('span'); + $alignGuide.className = 'tl-align-guide'; + $tl.appendChild($alignGuide); const D = (c) => Number(c.dataset.dur || 1); const M = (c) => Number(c.dataset.max || 1); - const setD = (c, v) => { c.dataset.dur = String(Math.max(MIN_DUR, Math.min(M(c), v))); }; + const IN = (c) => Number(c.dataset.in || 0); function clipDur(c) { return D(c); } - function clipStart(c) { + function compactStart(c) { let acc = 0; for (const sib of c.parentElement.querySelectorAll('.clip')) { if (sib === c) return acc; @@ -4513,19 +4558,171 @@ function openTriLightbox(ver, isAdopted, prodName) { } return 0; } + function clipStart(c) { + const start = Number(c.dataset.start); + return Number.isFinite(start) ? start : compactStart(c); + } function clipEnd(c) { return clipStart(c) + D(c); } - // 磁吸布局:每轨片段按 DOM 顺序紧贴排列 · 无 gap + function sourceDur(c) { return Math.max(M(c), D(c), MIN_DUR); } + function clampSourceIn(c, sourceIn = IN(c)) { + const maxIn = Math.max(0, sourceDur(c) - D(c)); + return Math.max(0, Math.min(maxIn, sourceIn)); + } + function syncVideoPreview(c) { + if (!c.classList.contains('video')) return; + const dur = Math.max(D(c), MIN_DUR); + const srcDur = Math.max(sourceDur(c), dur); + const sourceIn = clampSourceIn(c); + c.dataset.in = String(sourceIn); + c.style.setProperty('--src-width', (srcDur / dur * 100) + '%'); + c.style.setProperty('--src-offset', (srcDur ? (-(sourceIn / srcDur) * 100) : 0) + '%'); + } + function freezeLaneStarts(lane) { + let acc = 0; + for (const clip of lane.querySelectorAll('.clip')) { + const explicit = Number(clip.dataset.start); + const start = Number.isFinite(explicit) ? explicit : acc; + clip.dataset.start = String(start); + acc = start + D(clip); + } + } + function compactLane(lane) { + let acc = 0; + for (const clip of lane.querySelectorAll('.clip')) { + clip.dataset.start = String(acc); + acc += D(clip); + } + layoutLane(lane); + } + function snapLane(lane) { + lane.classList.add('is-snapping'); + compactLane(lane); + window.setTimeout(() => lane.classList.remove('is-snapping'), 180); + } + function settleLane(lane) { + if (lane === lanes.video) snapLane(lane); + else layoutLane(lane); + } + // 绝对布局:默认紧贴排列;trim 后允许保留被裁剪端的可见空隙 function layoutLane(lane) { let acc = 0; for (const clip of lane.querySelectorAll('.clip')) { const dur = D(clip); - clip.style.left = (acc / TOTAL * 100) + '%'; + const explicit = Number(clip.dataset.start); + const start = Number.isFinite(explicit) ? explicit : acc; + clip.style.left = (start / TOTAL * 100) + '%'; clip.style.width = (dur / TOTAL * 100) + '%'; - acc += dur; + syncVideoPreview(clip); + acc = start + dur; } } - function layoutAll() { - Object.values(lanes).forEach(l => l && layoutLane(l)); + function videoBoundaries() { + const points = []; + $laneV.querySelectorAll('.clip.video').forEach(c => { + points.push(clipStart(c), clipEnd(c)); + }); + return points; + } + function nearestVideoBoundary(edges) { + let best = null; + const points = videoBoundaries(); + edges.forEach((edge, edgeIndex) => { + points.forEach(time => { + const diff = Math.abs(edge - time); + if (diff <= ALIGN_EPS && (!best || diff < best.diff)) { + best = { edge, edgeIndex, time, diff }; + } + }); + }); + return best; + } + function showAlignGuideAt(time) { + const laneRect = $laneV.getBoundingClientRect(); + const tlRect = $tl.getBoundingClientRect(); + const rulerRect = $ruler.getBoundingClientRect(); + const lastLane = $laneB || $laneS || $laneV; + const lastRect = lastLane.getBoundingClientRect(); + $alignGuide.style.left = (laneRect.left + (time / TOTAL) * laneRect.width - tlRect.left) + 'px'; + $alignGuide.style.top = (rulerRect.top - tlRect.top) + 'px'; + $alignGuide.style.height = (lastRect.bottom - rulerRect.top) + 'px'; + $alignGuide.classList.add('show'); + } + function hideAlignGuide() { + $alignGuide.classList.remove('show'); + } + function guideEdgeToVideo(edge) { + const match = nearestVideoBoundary([edge]); + if (!match) { + hideAlignGuide(); + return null; + } + showAlignGuideAt(match.time); + return match.time; + } + function guideClipToVideo(start, dur) { + const safeDur = Math.max(MIN_DUR, Math.min(dur, TOTAL)); + const clampedStart = Math.max(0, Math.min(TOTAL - safeDur, start)); + const match = nearestVideoBoundary([clampedStart, clampedStart + safeDur]); + if (!match) { + hideAlignGuide(); + return clampedStart; + } + const nextStart = match.edgeIndex === 0 ? match.time : match.time - safeDur; + showAlignGuideAt(match.time); + return Math.max(0, Math.min(TOTAL - safeDur, nextStart)); + } + function ensureDropGhost(lane) { + let ghost = lane.querySelector('.tl-insert-ghost'); + if (!ghost) { + ghost = document.createElement('span'); + ghost.className = 'tl-insert-ghost'; + lane.appendChild(ghost); + } + return ghost; + } + function removeDropGhost(lane) { + lane.querySelector('.tl-insert-ghost')?.remove(); + } + function placeDropGhost(ghost, start, dur) { + ghost.style.left = (start / TOTAL * 100) + '%'; + ghost.style.width = (dur / TOTAL * 100) + '%'; + } + function insertIndexForCenter(siblings, centerT) { + let acc = 0; + for (let i = 0; i < siblings.length; i++) { + const mid = acc + D(siblings[i]) / 2; + if (centerT < mid) return i; + acc += D(siblings[i]); + } + return siblings.length; + } + function previewReorder(lane, dragged, insertIndex) { + const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== dragged); + const ghost = ensureDropGhost(lane); + let acc = 0; + for (let i = 0; i <= siblings.length; i++) { + if (i === insertIndex) { + placeDropGhost(ghost, acc, D(dragged)); + acc += D(dragged); + } + const sibling = siblings[i]; + if (sibling) { + sibling.dataset.start = String(acc); + sibling.style.left = (acc / TOTAL * 100) + '%'; + sibling.style.width = (D(sibling) / TOTAL * 100) + '%'; + syncVideoPreview(sibling); + acc += D(sibling); + } + } + } + function commitReorder(lane, dragged, insertIndex) { + const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== dragged); + dragged.remove(); + if (insertIndex >= siblings.length) lane.appendChild(dragged); + else lane.insertBefore(dragged, siblings[insertIndex]); + } + function compactAll() { + Object.values(lanes).forEach(l => l && compactLane(l)); } function clipAtTimeOnTrack(track, t) { const lane = lanes[track]; @@ -4601,8 +4798,10 @@ function openTriLightbox(ver, isAdopted, prodName) { if (_bodyDragMoved) return; // 拖动重排刚结束 · 抑制 click e.stopPropagation(); selectClip(clip); - currentTime = clipStart(clip); - updateTimeUI(); + if (clip.dataset.track === 'video') { + currentTime = clipStart(clip); + updateTimeUI(); + } }); }); } @@ -4615,7 +4814,6 @@ function openTriLightbox(ver, isAdopted, prodName) { updateTimeUI(); } $laneV.addEventListener('click', e => laneSeek($laneV, e)); - $laneS.addEventListener('click', e => laneSeek($laneS, e)); $ruler.addEventListener('click', (e) => { const rect = $ruler.getBoundingClientRect(); @@ -4625,23 +4823,35 @@ function openTriLightbox(ver, isAdopted, prodName) { }); let dragging = false; - $playhead.querySelector('.ph-grab').addEventListener('mousedown', (e) => { + let playheadDragOffset = 0; + function playheadCenterX() { + const rect = $laneS.getBoundingClientRect(); + return rect.left + (currentTime / TOTAL) * rect.width; + } + function seekPlayhead(clientX) { + const rect = $laneS.getBoundingClientRect(); + const pct = Math.max(0, Math.min(1, (clientX - playheadDragOffset - rect.left) / rect.width)); + currentTime = pct * TOTAL; + updateTimeUI(); + } + function startPlayheadDrag(e) { e.preventDefault(); + e.stopPropagation(); dragging = true; + playheadDragOffset = e.clientX - playheadCenterX(); if (playing) pause(); $playhead.classList.add('is-dragging'); $tl.classList.add('is-dragging-playhead'); - }); + } + $playhead.addEventListener('mousedown', startPlayheadDrag); document.addEventListener('mousemove', (e) => { if (!dragging) return; - const rect = $laneS.getBoundingClientRect(); - const pct = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); - currentTime = pct * TOTAL; - updateTimeUI(); + seekPlayhead(e.clientX); }); document.addEventListener('mouseup', () => { if (!dragging) return; dragging = false; + playheadDragOffset = 0; $playhead.classList.remove('is-dragging'); $tl.classList.remove('is-dragging-playhead'); }); @@ -4707,12 +4917,14 @@ function openTriLightbox(ver, isAdopted, prodName) { function deleteSelected() { if (!selectedEl) return; + const lane = selectedEl.parentElement; const next = selectedEl.nextElementSibling?.classList.contains('clip') ? selectedEl.nextElementSibling : selectedEl.previousElementSibling?.classList.contains('clip') ? selectedEl.previousElementSibling : null; selectedEl.remove(); + settleLane(lane); if (next) selectClip(next); else { selectedEl = null; updateInspector(); updateActionButtons(); } renumberVideo(); @@ -4727,6 +4939,10 @@ function openTriLightbox(ver, isAdopted, prodName) { dup.querySelectorAll('[data-trim]').forEach(t => t.remove()); delete dup.dataset.boundClick; selectedEl.after(dup); + if (selectedEl.parentElement !== lanes.video) { + dup.dataset.start = String(Math.max(0, Math.min(TOTAL - D(dup), clipEnd(selectedEl)))); + } + settleLane(selectedEl.parentElement); bindClipClicks(); renumberVideo(); selectClip(dup); @@ -4743,12 +4959,16 @@ function openTriLightbox(ver, isAdopted, prodName) { // 左半:dur=max=leftDur(切开后双方各自独立 · 不能再恢复跨越切点) selectedEl.dataset.dur = String(leftDur); selectedEl.dataset.max = String(leftDur); + selectedEl.dataset.in = '0'; + selectedEl.dataset.start = String(s); const right = selectedEl.cloneNode(true); right.classList.remove('selected'); right.querySelectorAll('[data-trim]').forEach(t => t.remove()); delete right.dataset.boundClick; right.dataset.dur = String(rightDur); right.dataset.max = String(rightDur); + right.dataset.in = '0'; + right.dataset.start = String(s + leftDur); // 右半若是视频片段 · 重新生成胶卷帧条数量(按时长成比例) if (right.classList.contains('video')) { const oldFrames = right.querySelector('.frames'); @@ -4776,32 +4996,93 @@ function openTriLightbox(ver, isAdopted, prodName) { }); } - // Trim 把手 · 三轨通用 · 磁吸:邻居自动跟随前移/后退 + // Trim 把手 · 三轨通用 · 左右边界按真实裁剪方向反馈 document.addEventListener('mousedown', (e) => { const handle = e.target.closest('[data-trim]'); if (!handle || !selectedEl) return; e.preventDefault(); e.stopPropagation(); const side = handle.dataset.trim; const lane = selectedEl.parentElement; + const isVideoTrack = lane === lanes.video; + freezeLaneStarts(lane); const laneRect = lane.getBoundingClientRect(); const startMouseX = e.clientX; + const startStart = clipStart(selectedEl); const startDur = D(selectedEl); + const startEnd = startStart + startDur; + const startIn = clampSourceIn(selectedEl); + const startSourceDur = sourceDur(selectedEl); + const startOut = Math.max(0, startSourceDur - startIn - startDur); + const prevClip = selectedEl.previousElementSibling?.classList.contains('clip') ? selectedEl.previousElementSibling : null; + const nextClip = selectedEl.nextElementSibling?.classList.contains('clip') ? selectedEl.nextElementSibling : null; + const prevEnd = prevClip ? clipEnd(prevClip) : 0; + const nextStart = nextClip ? clipStart(nextClip) : TOTAL; + let didTrim = false; function onMove(ev) { const dx = ev.clientX - startMouseX; + if (Math.abs(dx) > 1) didTrim = true; const dt = (dx / laneRect.width) * TOTAL; - // 左把手:右拖缩短 / 左拖恢复(直至 max) - // 右把手:右拖扩长(直至 max)/ 左拖缩短 - const newDur = side === 'l' ? (startDur - dt) : (startDur + dt); - setD(selectedEl, newDur); + if (!isVideoTrack) { + if (side === 'l') { + const maxStart = startEnd - MIN_DUR; + let newStart = Math.max(0, Math.min(maxStart, startStart + dt)); + const guidedStart = guideEdgeToVideo(newStart); + if (guidedStart !== null) newStart = Math.max(0, Math.min(maxStart, guidedStart)); + selectedEl.dataset.start = String(newStart); + selectedEl.dataset.dur = String(Math.max(MIN_DUR, startEnd - newStart)); + } else { + const minEnd = startStart + MIN_DUR; + let newEnd = Math.max(minEnd, Math.min(TOTAL, startEnd + dt)); + const guidedEnd = guideEdgeToVideo(newEnd); + if (guidedEnd !== null) newEnd = Math.max(minEnd, Math.min(TOTAL, guidedEnd)); + selectedEl.dataset.start = String(startStart); + selectedEl.dataset.dur = String(Math.max(MIN_DUR, newEnd - startStart)); + } + layoutLane(lane); + updateInspector(); + updateActionButtons(); + return; + } + hideAlignGuide(); + if (side === 'l') { + const minStart = Math.max(prevEnd, startStart - startIn); + const maxStart = startEnd - MIN_DUR; + const newStart = Math.max(minStart, Math.min(maxStart, startStart + dt)); + const newDur = Math.max(MIN_DUR, startEnd - newStart); + const newIn = Math.max(0, Math.min(startSourceDur - newDur, startIn + (newStart - startStart))); + selectedEl.dataset.start = String(newStart); + selectedEl.dataset.dur = String(newDur); + selectedEl.dataset.in = String(newIn); + } else { + const minEnd = startStart + MIN_DUR; + const maxEnd = Math.min(nextStart, startEnd + startOut, TOTAL); + const newEnd = Math.max(minEnd, Math.min(maxEnd, startEnd + dt)); + selectedEl.dataset.start = String(startStart); + selectedEl.dataset.dur = String(newEnd - startStart); + selectedEl.dataset.in = String(startIn); + } layoutLane(lane); updateInspector(); + updateActionButtons(); } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); document.body.style.cursor = ''; - updateTimeUI(); + hideAlignGuide(); + if (isVideoTrack) { + snapLane(lane); + if (didTrim && selectedEl) { + currentTime = side === 'l' ? clipStart(selectedEl) : clipEnd(selectedEl); + } + updateInspector(); + updateTimeUI(); + } else { + layoutLane(lane); + updateInspector(); + updateActionButtons(); + } } document.body.style.cursor = 'ew-resize'; document.addEventListener('mousemove', onMove); @@ -4812,49 +5093,78 @@ function openTriLightbox(ver, isAdopted, prodName) { let _bodyDragMoved = false; document.addEventListener('mousedown', (e) => { const clip = e.target.closest('#ed-timeline .clip'); - if (!clip || !clip.classList.contains('selected')) return; + if (!clip) return; if (e.target.closest('[data-trim]')) return; if (e.target.closest('.ph-grab')) return; e.preventDefault(); _bodyDragMoved = false; + if (clip !== selectedEl) selectClip(clip); const lane = clip.parentElement; + const isVideoTrack = lane === lanes.video; + freezeLaneStarts(lane); const laneRect = lane.getBoundingClientRect(); const startMouseX = e.clientX; const startStartT = clipStart(clip); + const clipDuration = D(clip); + let dropIndex = null; function onMove(ev) { const dx = ev.clientX - startMouseX; if (!_bodyDragMoved && Math.abs(dx) < 5) return; _bodyDragMoved = true; - clip.style.transform = 'translateX(' + dx + 'px)'; - clip.style.zIndex = '6'; - clip.style.opacity = '.88'; + clip.classList.add('dragging'); + const dt = (dx / laneRect.width) * TOTAL; + if (!isVideoTrack) { + const previewStart = guideClipToVideo(startStartT + dt, clipDuration); + clip.dataset.start = String(previewStart); + clip.style.left = (previewStart / TOTAL * 100) + '%'; + clip.style.width = (clipDuration / TOTAL * 100) + '%'; + updateInspector(); + updateActionButtons(); + document.body.style.cursor = 'grabbing'; + return; + } + hideAlignGuide(); + lane.classList.add('is-reordering'); + const newCenter = Math.max(clipDuration / 2, Math.min(TOTAL - clipDuration / 2, startStartT + clipDuration / 2 + dt)); + const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip); + dropIndex = insertIndexForCenter(siblings, newCenter); + previewReorder(lane, clip, dropIndex); + const previewStart = Math.max(0, Math.min(TOTAL - clipDuration, newCenter - clipDuration / 2)); + clip.dataset.start = String(previewStart); + clip.style.left = (previewStart / TOTAL * 100) + '%'; + clip.style.width = (clipDuration / TOTAL * 100) + '%'; + syncVideoPreview(clip); document.body.style.cursor = 'grabbing'; } function onUp(ev) { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); - clip.style.transform = ''; - clip.style.zIndex = ''; - clip.style.opacity = ''; + removeDropGhost(lane); + lane.classList.remove('is-reordering'); + clip.classList.remove('dragging'); document.body.style.cursor = ''; + hideAlignGuide(); if (!_bodyDragMoved) return; - const dx = ev.clientX - startMouseX; - const dt = (dx / laneRect.width) * TOTAL; - const newCenter = startStartT + D(clip) / 2 + dt; - const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip); - let acc = 0; - let insertBefore = null; - for (const s of siblings) { - const mid = acc + D(s) / 2; - if (newCenter < mid) { insertBefore = s; break; } - acc += D(s); + if (!isVideoTrack) { + layoutLane(lane); + selectClip(clip); + updateInspector(); + updateActionButtons(); + setTimeout(() => { _bodyDragMoved = false; }, 50); + return; } - clip.remove(); - if (insertBefore) lane.insertBefore(clip, insertBefore); - else lane.appendChild(clip); - layoutLane(lane); + if (dropIndex === null) { + const dx = ev.clientX - startMouseX; + const dt = (dx / laneRect.width) * TOTAL; + const newCenter = Math.max(clipDuration / 2, Math.min(TOTAL - clipDuration / 2, startStartT + clipDuration / 2 + dt)); + const siblings = [...lane.querySelectorAll('.clip')].filter(c => c !== clip); + dropIndex = insertIndexForCenter(siblings, newCenter); + } + commitReorder(lane, clip, dropIndex); + snapLane(lane); if (lane === lanes.video) renumberVideo(); + selectClip(clip); updateTimeUI(); setTimeout(() => { _bodyDragMoved = false; }, 50); } @@ -4863,7 +5173,7 @@ function openTriLightbox(ver, isAdopted, prodName) { }); // 初始化:磁吸布局 + 绑定 - layoutAll(); + compactAll(); bindClipClicks(); updateTimeUI(); })(); diff --git a/电商AI平台/products.html b/电商AI平台/products.html index b63ffe6..9b838ee 100644 --- a/电商AI平台/products.html +++ b/电商AI平台/products.html @@ -626,7 +626,7 @@
@@ -405,40 +453,40 @@
- -
-

危险操作

-
// 这些操作不可撤销,请确认后再执行
- -
-
导出我的数据
// 项目 + 资产元数据
-
- // 准备时间约 24 小时,完成后邮件通知 - -
-
-
-
退出登录
-
- // 仅退出当前设备,数据保留 - -
-
-
-
注销账号
-
- // 团队余额清零、所有项目作为孤儿归档 - -
-
-
-
// Airshelf · v2.1 · build 20260521
+ + +