Unverified Commit d8558a0c authored by LiangLiu's avatar LiangLiu Committed by GitHub
Browse files

update froentend (#470)


Co-authored-by: default avatarqinxinyi <qxy118045534@163.com>
parent 2a31ba43
...@@ -28,10 +28,10 @@ ...@@ -28,10 +28,10 @@
color: white; color: white;
/* Apple 极简黑白风格 - 品牌色 */ /* Apple 极简黑白风格 - 品牌色 */
--brand-primary: #a68ee7; --brand-primary: #90dce1;
--brand-primary-light: #cdbbfe; --brand-primary-light: #90dce1;
--brand-primary-rgb: 179, 151, 255; --brand-primary-rgb: 89, 194, 233;
--brand-primary-light-rgb: 210, 193, 255; --brand-primary-light-rgb: 142, 220, 255;
/* Apple 极简黑白风格 - 基础颜色 */ /* Apple 极简黑白风格 - 基础颜色 */
--text-primary: #1d1d1f; /* 浅色模式主要文字 */ --text-primary: #1d1d1f; /* 浅色模式主要文字 */
...@@ -841,12 +841,6 @@ body { ...@@ -841,12 +841,6 @@ body {
padding: 0.5rem 0.75rem !important; padding: 0.5rem 0.75rem !important;
} }
/* 创建视频页面移动端适配 */
.max-w-4xl.mx-auto {
max-width: 100% !important;
padding: 0 1rem !important;
}
/* 上传区域在移动端调整 */ /* 上传区域在移动端调整 */
.upload-section { .upload-section {
grid-template-columns: 1fr !important; grid-template-columns: 1fr !important;
......
...@@ -55,7 +55,7 @@ async function switchLang() { ...@@ -55,7 +55,7 @@ async function switchLang() {
// }; // };
const languageOptions = ref([ const languageOptions = ref([
{ code: 'zh', name: '中文', flag: 'ZH' }, { code: 'zh', name: '中文', flag: '' },
{ code: 'en', name: 'English', flag: 'EN' } { code: 'en', name: 'English', flag: 'EN' }
]); ]);
......
...@@ -10,7 +10,9 @@ export const locale = i18n.global.locale ...@@ -10,7 +10,9 @@ export const locale = i18n.global.locale
const loginLoading = ref(false); const loginLoading = ref(false);
const initLoading = ref(false); const initLoading = ref(false);
const downloadLoading = ref(false); const downloadLoading = ref(false);
const downloadLoadingMessage = ref('');
const isLoading = ref(false); // 页面加载loading状态 const isLoading = ref(false); // 页面加载loading状态
const isPageLoading = ref(false); // 分页加载loading状态
// 录音相关状态 // 录音相关状态
const isRecording = ref(false); const isRecording = ref(false);
...@@ -45,7 +47,8 @@ export const locale = i18n.global.locale ...@@ -45,7 +47,8 @@ export const locale = i18n.global.locale
confirm: () => { } confirm: () => { }
}); });
const submitting = ref(false); const submitting = ref(false);
const templateLoading = ref(false); // 模板加载状态 const templateLoading = ref(false); // 模板/任务复用加载状态
const templateLoadingMessage = ref('');
const taskSearchQuery = ref(''); const taskSearchQuery = ref('');
const sidebarCollapsed = ref(false); const sidebarCollapsed = ref(false);
const showExpandHint = ref(false); const showExpandHint = ref(false);
...@@ -135,6 +138,7 @@ export const locale = i18n.global.locale ...@@ -135,6 +138,7 @@ export const locale = i18n.global.locale
const templatePaginationKey = ref(0); const templatePaginationKey = ref(0);
const imageHistory = ref([]); const imageHistory = ref([]);
const audioHistory = ref([]); const audioHistory = ref([]);
const ttsHistory = ref([]);
// 模板文件缓存,避免重复下载 // 模板文件缓存,避免重复下载
const currentUser = ref({}); const currentUser = ref({});
...@@ -1194,7 +1198,7 @@ export const locale = i18n.global.locale ...@@ -1194,7 +1198,7 @@ export const locale = i18n.global.locale
}; };
const triggerAudioUpload = () => { const triggerAudioUpload = () => {
const audioInput = document.querySelector('input[type="file"][accept="audio/*"]'); const audioInput = document.querySelector('input[type="file"][data-role="audio-input"]');
if (audioInput) { if (audioInput) {
audioInput.click(); audioInput.click();
} else { } else {
...@@ -1223,7 +1227,7 @@ export const locale = i18n.global.locale ...@@ -1223,7 +1227,7 @@ export const locale = i18n.global.locale
updateUploadedContentStatus(); updateUploadedContentStatus();
console.log('音频已移除'); console.log('音频已移除');
// 重置音频文件输入框,确保可以重新选择相同文件 // 重置音频文件输入框,确保可以重新选择相同文件
const audioInput = document.querySelector('input[type="file"][accept="audio/*"]'); const audioInput = document.querySelector('input[type="file"][data-role="audio-input"]');
if (audioInput) { if (audioInput) {
audioInput.value = ''; audioInput.value = '';
} }
...@@ -1239,7 +1243,14 @@ export const locale = i18n.global.locale ...@@ -1239,7 +1243,14 @@ export const locale = i18n.global.locale
const handleAudioUpload = (event) => { const handleAudioUpload = (event) => {
const file = event.target.files[0]; const file = event.target.files[0];
if (file) { if (file && (file.type?.startsWith('audio/') || file.type?.startsWith('video/'))) {
const allowedVideoTypes = ['video/mp4', 'video/x-m4v', 'video/mpeg'];
if (file.type.startsWith('video/') && !allowedVideoTypes.includes(file.type)) {
showAlert(t('unsupportedVideoFormat'), 'warning');
setCurrentAudioPreview(null);
updateUploadedContentStatus();
return;
}
s2vForm.value.audioFile = file; s2vForm.value.audioFile = file;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
...@@ -1251,6 +1262,9 @@ export const locale = i18n.global.locale ...@@ -1251,6 +1262,9 @@ export const locale = i18n.global.locale
} else { } else {
setCurrentAudioPreview(null); setCurrentAudioPreview(null);
updateUploadedContentStatus(); updateUploadedContentStatus();
if (file) {
showAlert(t('unsupportedAudioOrVideo'), 'warning');
}
} }
}; };
...@@ -2134,15 +2148,15 @@ export const locale = i18n.global.locale ...@@ -2134,15 +2148,15 @@ export const locale = i18n.global.locale
// 分页相关函数 // 分页相关函数
const goToPage = async (page) => { const goToPage = async (page) => {
isLoading.value = true; isPageLoading.value = true;
if (page < 1 || page > pagination.value?.total_pages || page === currentTaskPage.value) { if (page < 1 || page > pagination.value?.total_pages || page === currentTaskPage.value) {
isLoading.value = false; isPageLoading.value = false;
return; return;
} }
currentTaskPage.value = page; currentTaskPage.value = page;
taskPageInput.value = page; // 同步更新输入框 taskPageInput.value = page; // 同步更新输入框
await refreshTasks(); await refreshTasks();
isLoading.value = false; isPageLoading.value = false;
}; };
const jumpToPage = async () => { const jumpToPage = async () => {
...@@ -2157,15 +2171,15 @@ export const locale = i18n.global.locale ...@@ -2157,15 +2171,15 @@ export const locale = i18n.global.locale
// Template分页相关函数 // Template分页相关函数
const goToTemplatePage = async (page) => { const goToTemplatePage = async (page) => {
isLoading.value=true; isPageLoading.value=true;
if (page < 1 || page > templatePagination.value?.total_pages || page === templateCurrentPage.value) { if (page < 1 || page > templatePagination.value?.total_pages || page === templateCurrentPage.value) {
isLoading.value = false; isPageLoading.value = false;
return; return;
} }
templateCurrentPage.value = page; templateCurrentPage.value = page;
templatePageInput.value = page; // 同步更新输入框 templatePageInput.value = page; // 同步更新输入框
await loadImageAudioTemplates(); await loadImageAudioTemplates();
isLoading.value = false; isPageLoading.value = false;
}; };
const jumpToTemplatePage = async () => { const jumpToTemplatePage = async () => {
...@@ -2270,15 +2284,15 @@ export const locale = i18n.global.locale ...@@ -2270,15 +2284,15 @@ export const locale = i18n.global.locale
// 灵感广场分页相关函数 // 灵感广场分页相关函数
const goToInspirationPage = async (page) => { const goToInspirationPage = async (page) => {
isLoading.value = true; isPageLoading.value = true;
if (page < 1 || page > inspirationPagination.value?.total_pages || page === inspirationCurrentPage.value) { if (page < 1 || page > inspirationPagination.value?.total_pages || page === inspirationCurrentPage.value) {
isLoading.value = false; isPageLoading.value = false;
return; return;
} }
inspirationCurrentPage.value = page; inspirationCurrentPage.value = page;
inspirationPageInput.value = page; // 同步更新输入框 inspirationPageInput.value = page; // 同步更新输入框
await loadInspirationData(); await loadInspirationData();
isLoading.value = false; isPageLoading.value = false;
}; };
const jumpToInspirationPage = async () => { const jumpToInspirationPage = async () => {
...@@ -2623,16 +2637,20 @@ export const locale = i18n.global.locale ...@@ -2623,16 +2637,20 @@ export const locale = i18n.global.locale
if (!confirmed) { if (!confirmed) {
return; return;
} }
// 显示删除中的提示
showAlert(t('deletingTaskAlert'), 'info');
const response = await apiRequest(`/api/v1/task/delete?task_id=${taskId}`, { const response = await apiRequest(`/api/v1/task/delete?task_id=${taskId}`, {
method: 'DELETE' method: 'DELETE'
}); });
if (response && response.ok) { if (response && response.ok) {
showAlert(t('taskDeletedSuccessAlert'), 'success'); showAlert(t('taskDeletedSuccessAlert'), 'success');
const deletedTaskIndex = tasks.value.findIndex(task => task.task_id === taskId);
if (deletedTaskIndex !== -1) {
const wasCurrent = currentTask.value?.task_id === taskId;
tasks.value.splice(deletedTaskIndex, 1);
if (wasCurrent) {
currentTask.value = tasks.value[deletedTaskIndex] || tasks.value[deletedTaskIndex - 1] || null;
}
}
refreshTasks(true); // 强制刷新 refreshTasks(true); // 强制刷新
// 如果是从任务详情页删除,删除成功后关闭详情弹窗 // 如果是从任务详情页删除,删除成功后关闭详情弹窗
...@@ -2727,9 +2745,16 @@ export const locale = i18n.global.locale ...@@ -2727,9 +2745,16 @@ export const locale = i18n.global.locale
}; };
const reuseTask = async (task) => { const reuseTask = async (task) => {
if (!task) {
showAlert(t('loadTaskDataFailedAlert'), 'danger');
return;
}
try { try {
templateLoading.value = true;
templateLoadingMessage.value = t('prefillLoadingTask');
// 跳转到任务创建界面 // 跳转到任务创建界面
isCreationAreaExpanded.value=true isCreationAreaExpanded.value = true;
if (showTaskDetailModal.value) { if (showTaskDetailModal.value) {
closeTaskDetailModal(); closeTaskDetailModal();
} }
...@@ -2741,6 +2766,9 @@ export const locale = i18n.global.locale ...@@ -2741,6 +2766,9 @@ export const locale = i18n.global.locale
// 获取当前表单 // 获取当前表单
const currentForm = getCurrentForm(); const currentForm = getCurrentForm();
// 立即切换到创建视图,后续资产异步加载
switchToCreateView();
// 设置模型 // 设置模型
if (task.params && task.params.model_cls) { if (task.params && task.params.model_cls) {
currentForm.model_cls = task.params.model_cls; currentForm.model_cls = task.params.model_cls;
...@@ -2779,6 +2807,9 @@ export const locale = i18n.global.locale ...@@ -2779,6 +2807,9 @@ export const locale = i18n.global.locale
// 加载音频文件 // 加载音频文件
if (audioUrl) { if (audioUrl) {
try { try {
currentForm.audioUrl = audioUrl;
setCurrentAudioPreview(audioUrl);
const audioResponse = await fetch(audioUrl); const audioResponse = await fetch(audioUrl);
if (audioResponse && audioResponse.ok) { if (audioResponse && audioResponse.ok) {
const blob = await audioResponse.blob(); const blob = await audioResponse.blob();
...@@ -2810,13 +2841,6 @@ export const locale = i18n.global.locale ...@@ -2810,13 +2841,6 @@ export const locale = i18n.global.locale
size: file.size, size: file.size,
originalBlobType: blob.type originalBlobType: blob.type
}); });
// 使用FileReader生成data URL,与正常上传保持一致
const reader = new FileReader();
reader.onload = (e) => {
setCurrentAudioPreview(e.target.result);
console.log('复用任务 - 音频预览已设置:', e.target.result.substring(0, 50) + '...');
};
reader.readAsDataURL(file);
} }
} catch (error) { } catch (error) {
console.warn('Failed to load audio file:', error); console.warn('Failed to load audio file:', error);
...@@ -2828,161 +2852,169 @@ export const locale = i18n.global.locale ...@@ -2828,161 +2852,169 @@ export const locale = i18n.global.locale
showAlert(t('taskMaterialReuseSuccessAlert'), 'success'); showAlert(t('taskMaterialReuseSuccessAlert'), 'success');
// 检查当前路由,如果已经在 generate 页面,则滚动到生成区域
const currentRoute = router.currentRoute.value;
if (currentRoute.path === '/generate') {
// 关闭任务详情弹窗
if (showTaskDetailModal.value) {
closeTaskDetailModal();
}
// 等待 DOM 更新后滚动到生成区域
await nextTick();
// 如果之前有展开过创作区域,保持展开状态
const creationArea = document.querySelector('.creation-area');
if (isCreationAreaExpanded.value) {
// 延迟一点时间确保DOM更新完成
setTimeout(() => {
if (creationArea) {
creationArea.classList.add('show');
}
}, 50);
}
// 滚动到顶部
const mainScrollable = document.querySelector('.main-scrollbar');
if (mainScrollable) {
mainScrollable.scrollTo({
top: 0,
behavior: 'smooth'
});
}
} else {
// 不在 generate 页面,跳转过去
switchToCreateView();
}
} catch (error) { } catch (error) {
console.error('Failed to reuse task:', error); console.error('Failed to reuse task:', error);
showAlert(t('loadTaskDataFailedAlert'), 'danger'); showAlert(t('loadTaskDataFailedAlert'), 'danger');
} finally {
templateLoading.value = false;
templateLoadingMessage.value = '';
} }
}; };
const downloadFile = (fileInfo) => { const downloadFile = async (fileInfo) => {
if (!fileInfo || !fileInfo.blob) { if (!fileInfo || !fileInfo.blob) {
showAlert(t('fileUnavailableAlert'), 'danger'); showAlert(t('fileUnavailableAlert'), 'danger');
return; return false;
}
const blob = fileInfo.blob;
const fileName = fileInfo.name || 'download';
const mimeType = blob.type || fileInfo.mimeType || 'application/octet-stream';
const isMobile = typeof navigator !== 'undefined' && /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
if (isMobile && typeof navigator?.canShare === 'function' && typeof navigator?.share === 'function') {
try {
const shareFile = new File([blob], fileName, { type: mimeType });
if (navigator.canShare({ files: [shareFile] })) {
await navigator.share({
files: [shareFile],
title: fileName
});
showAlert(t('downloadSuccessAlert'), 'success');
return true;
}
} catch (error) {
if (error?.name === 'AbortError') {
console.info('User cancelled share dialog');
showAlert(t('downloadCancelledAlert'), 'info');
return false;
}
console.warn('Native share failed, falling back to download link:', error);
}
} }
try { try {
const url = URL.createObjectURL(fileInfo.blob); const objectUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = objectUrl;
a.download = fileInfo.name || 'download'; a.download = fileName;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(objectUrl);
showAlert(t('downloadSuccessAlert'), 'success'); showAlert(t('downloadSuccessAlert'), 'success');
return true;
} catch (error) { } catch (error) {
console.error('Download failed:', error); console.error('Download failed:', error);
showAlert(t('downloadFailedAlert'), 'danger'); showAlert(t('downloadFailedAlert'), 'danger');
return false;
} }
}; };
// 处理文件下载 // 处理文件下载
const handleDownloadFile = async (taskId, fileKey, fileName) => { const handleDownloadFile = async (taskId, fileKey, fileName) => {
if (downloadLoading.value) {
showAlert(t('downloadInProgressNotice'), 'info');
return;
}
downloadLoading.value = true;
downloadLoadingMessage.value = t('downloadPreparing');
try { try {
console.log('开始下载文件:', { taskId, fileKey, fileName }) console.log('开始下载文件:', { taskId, fileKey, fileName });
// 处理文件名,确保有正确的后缀名 // 处理文件名,确保有正确的后缀名
let finalFileName = fileName let finalFileName = fileName;
if (fileName && typeof fileName === 'string') { if (fileName && typeof fileName === 'string') {
// 检查是否已有后缀名 const hasExtension = /\.[a-zA-Z0-9]+$/.test(fileName);
const hasExtension = /\.[a-zA-Z0-9]+$/.test(fileName)
if (!hasExtension) { if (!hasExtension) {
// 没有后缀名,根据文件类型添加 const extension = getFileExtension(fileKey);
const extension = getFileExtension(fileKey) finalFileName = `${fileName}.${extension}`;
finalFileName = `${fileName}.${extension}` console.log('添加后缀名:', finalFileName);
console.log('添加后缀名:', finalFileName)
} }
} else { } else {
// 没有文件名,使用默认名称 finalFileName = `${fileKey}.${getFileExtension(fileKey)}`;
finalFileName = `${fileKey}.${getFileExtension(fileKey)}`
} }
// 先尝试从缓存获取 downloadLoadingMessage.value = t('downloadFetching');
let fileData = getTaskFileFromCache(taskId, fileKey)
console.log('缓存中的文件数据:', fileData)
if (fileData && fileData.blob) {
// 缓存中有blob数据,直接使用
console.log('使用缓存中的文件数据')
downloadFile({ ...fileData, name: finalFileName })
return
}
if (fileData && fileData.url) { let downloadUrl = null;
// 缓存中有URL,使用URL下载
console.log('使用缓存中的URL下载:', fileData.url)
try {
const response = await fetch(fileData.url)
console.log('文件响应状态:', response.status, response.ok)
if (response.ok) { const cachedData = getTaskFileFromCache(taskId, fileKey);
const blob = await response.blob() if (cachedData?.url) {
console.log('文件blob大小:', blob.size) downloadUrl = cachedData.url;
const downloadData = {
blob: blob,
name: finalFileName
}
console.log('构造的文件数据:', downloadData)
downloadFile(downloadData)
return
} else {
console.error('文件响应失败:', response.status, response.statusText)
}
} catch (error) {
console.error('使用缓存URL下载失败:', error)
}
} }
if (!fileData) { if (!downloadUrl) {
console.log('缓存中没有文件,尝试异步获取...') downloadUrl = await getTaskFileUrl(taskId, fileKey);
// 缓存中没有,尝试异步获取 }
const url = await getTaskFileUrl(taskId, fileKey)
console.log('获取到的文件URL:', url)
if (url) { if (!downloadUrl) {
const response = await fetch(url) throw new Error('无法获取文件URL');
console.log('文件响应状态:', response.status, response.ok) }
if (response.ok) { const response = await fetch(downloadUrl);
const blob = await response.blob() if (!response.ok) {
console.log('文件blob大小:', blob.size) throw new Error(`文件响应失败: ${response.status}`);
}
fileData = { const blob = await response.blob();
blob: blob, const isMobileBrowser = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
name: finalFileName
} if (isMobileBrowser) {
console.log('构造的文件数据:', fileData) downloadLoadingMessage.value = '';
} else { downloadLoading.value = false;
console.error('文件响应失败:', response.status, response.statusText) showAlert(t('mobileSaveToAlbumTip'), 'info');
}
const blobUrl = URL.createObjectURL(blob);
const previewWindow = window.open('', '_blank', 'noopener,noreferrer');
if (previewWindow) {
previewWindow.document.write(`<!DOCTYPE html>
<html lang="${locale.value || 'en'}">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${t('mobileSavePreviewTitle')}</title>
<style>
body { margin: 0; background: #000; color: #fff; font-family: system-ui, sans-serif; }
.wrapper { min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; gap: 16px; }
video { width: 100%; height: auto; border-radius: 16px; max-height: calc(100vh - 160px); }
p { text-align: center; line-height: 1.5; font-size: 15px; color: rgba(255,255,255,0.85); }
</style>
</head>
<body>
<div class="wrapper">
<video controls playsinline webkit-playsinline preload="auto" src="${blobUrl}"></video>
<p>${t('mobileSaveInstruction')}</p>
</div>
<script>
window.addEventListener('pagehide', () => URL.revokeObjectURL('${blobUrl}'));
window.addEventListener('beforeunload', () => URL.revokeObjectURL('${blobUrl}'));
</script>
</body>
</html>`);
previewWindow.document.close();
} else { } else {
console.error('无法获取文件URL') URL.revokeObjectURL(blobUrl);
window.location.href = downloadUrl;
} }
return;
} }
if (fileData && fileData.blob) { downloadLoadingMessage.value = t('downloadSaving');
console.log('开始下载文件:', fileData.name) await downloadFile({
downloadFile(fileData) blob,
} else { name: finalFileName,
console.error('文件数据无效:', fileData) mimeType: blob.type
showAlert(t('fileUnavailableAlert'), 'danger') });
}
} catch (error) { } catch (error) {
console.error('下载失败:', error) console.error('下载失败:', error);
showAlert(t('downloadFailedAlert'), 'danger') showAlert(t('downloadFailedAlert'), 'danger');
} finally {
downloadLoading.value = false;
downloadLoadingMessage.value = '';
} }
} }
...@@ -4750,10 +4782,10 @@ export const locale = i18n.global.locale ...@@ -4750,10 +4782,10 @@ export const locale = i18n.global.locale
// 选择分类 // 选择分类
const selectInspirationCategory = async (category) => { const selectInspirationCategory = async (category) => {
isLoading.value = true; isPageLoading.value = true;
// 如果点击的是当前分类,不重复请求 // 如果点击的是当前分类,不重复请求
if (selectedInspirationCategory.value === category) { if (selectedInspirationCategory.value === category) {
isLoading.value = false; isPageLoading.value = false;
return; return;
} }
...@@ -4770,7 +4802,7 @@ export const locale = i18n.global.locale ...@@ -4770,7 +4802,7 @@ export const locale = i18n.global.locale
// 重新加载数据 // 重新加载数据
await loadInspirationData(); // 强制刷新,不使用缓存 await loadInspirationData(); // 强制刷新,不使用缓存
isLoading.value = false; isPageLoading.value = false;
}; };
// 搜索防抖定时器 // 搜索防抖定时器
...@@ -4796,7 +4828,7 @@ export const locale = i18n.global.locale ...@@ -4796,7 +4828,7 @@ export const locale = i18n.global.locale
// 重新加载数据 // 重新加载数据
await loadInspirationData(); // 强制刷新,不使用缓存 await loadInspirationData(); // 强制刷新,不使用缓存
isLoading.value = false; isPageLoading.value = false;
}, 500); // 500ms 防抖延迟 }, 500); // 500ms 防抖延迟
}; };
...@@ -5665,7 +5697,7 @@ export const locale = i18n.global.locale ...@@ -5665,7 +5697,7 @@ export const locale = i18n.global.locale
try { try {
// 开始模板加载 // 开始模板加载
templateLoading.value = true; templateLoading.value = true;
showAlert('模板加载中...', 'info'); templateLoadingMessage.value = t('prefillLoadingTemplate');
// 先设置任务类型 // 先设置任务类型
selectedTaskId.value = item.task_type; selectedTaskId.value = item.task_type;
...@@ -5680,6 +5712,12 @@ export const locale = i18n.global.locale ...@@ -5680,6 +5712,12 @@ export const locale = i18n.global.locale
currentForm.model_cls = item.model_cls || ''; currentForm.model_cls = item.model_cls || '';
currentForm.stage = item.stage || 'single_stage'; currentForm.stage = item.stage || 'single_stage';
// 立即关闭模板详情并切换到创建视图,后续资源异步加载
showTemplateDetailModal.value = false;
selectedTemplate.value = null;
isCreationAreaExpanded.value = true;
switchToCreateView();
// 创建加载Promise数组 // 创建加载Promise数组
const loadingPromises = []; const loadingPromises = [];
...@@ -5783,14 +5821,6 @@ export const locale = i18n.global.locale ...@@ -5783,14 +5821,6 @@ export const locale = i18n.global.locale
await Promise.all(loadingPromises); await Promise.all(loadingPromises);
} }
// 关闭模板详情弹窗(不跳转路由)
showTemplateDetailModal.value = false;
selectedTemplate.value = null;
// 切换到创建视图
isCreationAreaExpanded.value=true;
switchToCreateView();
showAlert(`模板加载完成`, 'success'); showAlert(`模板加载完成`, 'success');
} catch (error) { } catch (error) {
console.error('应用模板失败:', error); console.error('应用模板失败:', error);
...@@ -5798,6 +5828,7 @@ export const locale = i18n.global.locale ...@@ -5798,6 +5828,7 @@ export const locale = i18n.global.locale
} finally { } finally {
// 结束模板加载 // 结束模板加载
templateLoading.value = false; templateLoading.value = false;
templateLoadingMessage.value = '';
} }
}; };
...@@ -6212,6 +6243,75 @@ export const locale = i18n.global.locale ...@@ -6212,6 +6243,75 @@ export const locale = i18n.global.locale
featuredTemplatesLoading.value = false; featuredTemplatesLoading.value = false;
} }
}; };
const removeTtsHistoryEntry = (entryId) => {
if (!entryId) return;
const currentHistory = loadTtsHistory().filter(entry => entry.id !== entryId);
saveTtsHistory(currentHistory);
};
const loadTtsHistory = () => {
try {
const stored = localStorage.getItem('ttsHistory');
if (!stored) return [];
const parsed = JSON.parse(stored);
ttsHistory.value = Array.isArray(parsed) ? parsed : [];
return ttsHistory.value;
} catch (error) {
console.error('加载TTS历史失败:', error);
ttsHistory.value = [];
return [];
}
};
const saveTtsHistory = (historyList) => {
try {
localStorage.setItem('ttsHistory', JSON.stringify(historyList));
ttsHistory.value = historyList;
} catch (error) {
console.error('保存TTS历史失败:', error);
}
};
const addTtsHistoryEntry = (text = '', instruction = '') => {
const trimmedText = (text || '').trim();
const trimmedInstruction = (instruction || '').trim();
if (!trimmedText && !trimmedInstruction) {
return;
}
const currentHistory = loadTtsHistory();
const existingIndex = currentHistory.findIndex(entry =>
entry.text === trimmedText && entry.instruction === trimmedInstruction
);
const timestamp = new Date().toISOString();
if (existingIndex !== -1) {
const existingEntry = currentHistory.splice(existingIndex, 1)[0];
existingEntry.timestamp = timestamp;
currentHistory.unshift(existingEntry);
} else {
currentHistory.unshift({
id: Date.now(),
text: trimmedText,
instruction: trimmedInstruction,
timestamp
});
}
if (currentHistory.length > 20) {
currentHistory.length = 20;
}
saveTtsHistory(currentHistory);
};
const clearTtsHistory = () => {
ttsHistory.value = [];
localStorage.removeItem('ttsHistory');
};
export { export {
// 任务类型下拉菜单 // 任务类型下拉菜单
...@@ -6222,7 +6322,9 @@ export { ...@@ -6222,7 +6322,9 @@ export {
loginLoading, loginLoading,
initLoading, initLoading,
downloadLoading, downloadLoading,
downloadLoadingMessage,
isLoading, isLoading,
isPageLoading,
// 录音相关 // 录音相关
isRecording, isRecording,
...@@ -6245,6 +6347,7 @@ export { ...@@ -6245,6 +6347,7 @@ export {
toggleSmsLogin, toggleSmsLogin,
submitting, submitting,
templateLoading, templateLoading,
templateLoadingMessage,
taskSearchQuery, taskSearchQuery,
currentUser, currentUser,
models, models,
...@@ -6531,4 +6634,10 @@ export { ...@@ -6531,4 +6634,10 @@ export {
initTheme, initTheme,
toggleTheme, toggleTheme,
getThemeIcon, getThemeIcon,
loadTtsHistory,
removeTtsHistoryEntry,
ttsHistory,
addTtsHistoryEntry,
saveTtsHistory,
clearTtsHistory,
}; };
...@@ -9,6 +9,7 @@ import PromptTemplate from '../components/PromptTemplate.vue' ...@@ -9,6 +9,7 @@ import PromptTemplate from '../components/PromptTemplate.vue'
import Voice_tts from '../components/Voice_tts.vue' import Voice_tts from '../components/Voice_tts.vue'
import MediaTemplate from '../components/MediaTemplate.vue' import MediaTemplate from '../components/MediaTemplate.vue'
import Loading from '../components/Loading.vue' import Loading from '../components/Loading.vue'
import SiteFooter from '../components/SiteFooter.vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { isLoading, showVoiceTTSModal, handleAudioUpload, showAlert } from '../utils/other' import { isLoading, showVoiceTTSModal, handleAudioUpload, showAlert } from '../utils/other'
...@@ -59,6 +60,8 @@ const handleTTSComplete = (audioBlob) => { ...@@ -59,6 +60,8 @@ const handleTTSComplete = (audioBlob) => {
<router-view></router-view> <router-view></router-view>
</div> </div>
</div> </div>
<SiteFooter />
</div> </div>
<!-- 全局组件 --> <!-- 全局组件 -->
......
...@@ -90,6 +90,7 @@ onMounted(async () => { ...@@ -90,6 +90,7 @@ onMounted(async () => {
<span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">LightX2V</span> <span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">LightX2V</span>
<i class="fas fa-external-link-alt text-xs text-[#86868b] dark:text-[#98989d] transition-all duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5"></i> <i class="fas fa-external-link-alt text-xs text-[#86868b] dark:text-[#98989d] transition-all duration-200 group-hover:translate-x-0.5 group-hover:-translate-y-0.5"></i>
</a> </a>
</div> </div>
<Alert /> <Alert />
...@@ -99,6 +100,7 @@ onMounted(async () => { ...@@ -99,6 +100,7 @@ onMounted(async () => {
<Loading /> <Loading />
</div> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
......
...@@ -276,35 +276,33 @@ onMounted(async () => { ...@@ -276,35 +276,33 @@ onMounted(async () => {
<template> <template>
<!-- Apple 极简风格分享页面 --> <!-- Apple 极简风格分享页面 -->
<div class="bg-[#f5f5f7] dark:bg-[#000000] transition-colors duration-300 w-full h-full"> <div class="min-h-screen w-full bg-[#f5f5f7] dark:bg-[#000000]">
<!-- 主内容区域 --> <!-- TopBar -->
<div class="flex flex-col w-full h-full"> <topMenu />
<!-- TopBar -->
<topMenu /> <!-- 主要内容区域 -->
<div class="w-full min-h-[calc(100vh-80px)] overflow-y-auto main-scrollbar">
<!-- 滚动内容区域 - 带滚动条 --> <!-- 错误状态 - Apple 风格 -->
<div class="flex-1 overflow-y-auto main-scrollbar"> <div v-if="error" class="flex items-center justify-center min-h-[60vh] px-6">
<!-- 错误状态 - Apple 风格 - 响应式 -->
<div v-if="error" class="flex items-center justify-center min-h-[60vh] px-4 sm:px-6">
<div class="text-center max-w-md"> <div class="text-center max-w-md">
<div class="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 bg-red-500/10 dark:bg-red-400/10 rounded-2xl sm:rounded-3xl mb-4 sm:mb-6"> <div class="inline-flex items-center justify-center w-20 h-20 bg-red-500/10 dark:bg-red-400/10 rounded-3xl mb-6">
<i class="fas fa-exclamation-triangle text-2xl sm:text-3xl text-red-500 dark:text-red-400"></i> <i class="fas fa-exclamation-triangle text-3xl text-red-500 dark:text-red-400"></i>
</div> </div>
<h2 class="text-xl sm:text-2xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 sm:mb-4 tracking-tight">{{ t('shareNotFound') }}</h2> <h2 class="text-2xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-4 tracking-tight">{{ t('shareNotFound') }}</h2>
<p class="text-sm sm:text-base text-[#86868b] dark:text-[#98989d] mb-6 sm:mb-8 tracking-tight">{{ error }}</p> <p class="text-base text-[#86868b] dark:text-[#98989d] mb-8 tracking-tight">{{ error }}</p>
<button @click="router.push('/')" <button @click="router.push('/')"
class="inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-2.5 sm:py-3 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white rounded-full text-sm sm:text-[15px] font-semibold tracking-tight transition-all duration-200 hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100"> class="inline-flex items-center justify-center gap-2 px-8 py-3 bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] text-white rounded-full text-[15px] font-semibold tracking-tight transition-all duration-200 hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100">
<i class="fas fa-home text-xs sm:text-sm"></i> <i class="fas fa-home text-sm"></i>
<span>{{ t('backToHome') }}</span> <span>{{ t('backToHome') }}</span>
</button> </button>
</div> </div>
</div> </div>
<!-- 分享内容 - Apple 风格 - 响应式布局 --> <!-- 分享内容 - Apple 风格 -->
<div v-else-if="shareData" class="flex flex-col lg:grid lg:grid-cols-2 gap-8 lg:gap-16 w-full max-w-7xl mx-auto px-4 sm:px-6 lg:px-12 py-8 sm:py-12 lg:py-16 items-center"> <div v-else-if="shareData" class="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 w-full max-w-7xl mx-auto px-6 sm:px-8 lg:px-12 py-12 lg:py-16 items-center">
<!-- 左侧视频区域 - 响应式尺寸 --> <!-- 左侧视频区域 -->
<div class="flex justify-center items-center w-full order-1"> <div class="flex justify-center items-center">
<div class="w-full max-w-[300px] sm:max-w-[350px] lg:max-w-[400px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-2xl overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative"> <div class="w-full max-w-[400px] aspect-[9/16] bg-black dark:bg-[#000000] rounded-2xl overflow-hidden shadow-[0_8px_24px_rgba(0,0,0,0.15)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.5)] relative">
<!-- 视频加载占位符 - Apple 风格 --> <!-- 视频加载占位符 - Apple 风格 -->
<div v-if="!videoUrl" class="w-full h-full flex flex-col items-center justify-center bg-[#f5f5f7] dark:bg-[#1c1c1e]"> <div v-if="!videoUrl" class="w-full h-full flex flex-col items-center justify-center bg-[#f5f5f7] dark:bg-[#1c1c1e]">
<div class="relative w-12 h-12 mb-6"> <div class="relative w-12 h-12 mb-6">
...@@ -339,94 +337,94 @@ onMounted(async () => { ...@@ -339,94 +337,94 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 右侧信息区域 - Apple 风格 - 响应式 --> <!-- 右侧信息区域 - Apple 风格 -->
<div class="flex items-center justify-center w-full order-2"> <div class="flex items-center justify-center">
<div class="w-full max-w-[300px] sm:max-w-[500px]"> <div class="w-full max-w-[500px]">
<!-- 标题 - Apple 风格 - 响应式字体 --> <!-- 标题 - Apple 风格 -->
<h1 class="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-3 sm:mb-4 tracking-tight leading-tight text-center lg:text-left"> <h1 class="text-4xl sm:text-5xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] mb-4 tracking-tight leading-tight">
{{ getShareTitle() }} {{ getShareTitle() }}
</h1> </h1>
<!-- 描述 - Apple 风格 - 响应式字体 --> <!-- 描述 - Apple 风格 -->
<p class="text-base sm:text-lg text-[#86868b] dark:text-[#98989d] mb-6 sm:mb-8 leading-relaxed tracking-tight text-center lg:text-left"> <p class="text-lg text-[#86868b] dark:text-[#98989d] mb-8 leading-relaxed tracking-tight">
{{ getShareDescription() }} {{ getShareDescription() }}
</p> </p>
<!-- 特性列表 - Apple 风格 - 响应式 --> <!-- 特性列表 - Apple 风格 -->
<div class="grid grid-cols-1 gap-2 sm:gap-3 mb-6 sm:mb-8"> <div class="grid grid-cols-1 gap-3 mb-8">
<div class="flex items-center gap-2.5 sm:gap-3 p-2.5 sm:p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <div class="flex items-center gap-3 p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]">
<div class="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0"> <div class="w-10 h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0">
<i class="fas fa-rocket text-sm sm:text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-rocket text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</div> </div>
<span class="text-xs sm:text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('latestAIModel') }}</span> <span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('latestAIModel') }}</span>
</div> </div>
<div class="flex items-center gap-2.5 sm:gap-3 p-2.5 sm:p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <div class="flex items-center gap-3 p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]">
<div class="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0"> <div class="w-10 h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0">
<i class="fas fa-bolt text-sm sm:text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-bolt text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</div> </div>
<span class="text-xs sm:text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('oneClickReplication') }}</span> <span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('oneClickReplication') }}</span>
</div> </div>
<div class="flex items-center gap-2.5 sm:gap-3 p-2.5 sm:p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]"> <div class="flex items-center gap-3 p-3 bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-xl transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.08)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.2)]">
<div class="w-9 h-9 sm:w-10 sm:h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0"> <div class="w-10 h-10 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 rounded-lg flex-shrink-0">
<i class="fas fa-user-cog text-sm sm:text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-user-cog text-base text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
</div> </div>
<span class="text-xs sm:text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('customizableCharacter') }}</span> <span class="text-sm font-medium text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('customizableCharacter') }}</span>
</div> </div>
</div> </div>
<!-- 操作按钮 - Apple 风格 - 响应式 --> <!-- 操作按钮 - Apple 风格 -->
<div class="space-y-2.5 sm:space-y-3 mb-6 sm:mb-8"> <div class="space-y-3 mb-8">
<button @click="createSimilar" <button @click="createSimilar"
class="w-full rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] border-0 px-6 sm:px-8 py-3 sm:py-3.5 text-sm sm:text-[15px] font-semibold text-white hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100 transition-all duration-200 ease-out tracking-tight flex items-center justify-center gap-2"> class="w-full rounded-full bg-[color:var(--brand-primary)] dark:bg-[color:var(--brand-primary-light)] border-0 px-8 py-3.5 text-[15px] font-semibold text-white hover:scale-[1.02] hover:shadow-[0_8px_24px_rgba(var(--brand-primary-rgb),0.35)] dark:hover:shadow-[0_8px_24px_rgba(var(--brand-primary-light-rgb),0.4)] active:scale-100 transition-all duration-200 ease-out tracking-tight flex items-center justify-center gap-2">
<i class="fas fa-magic text-sm"></i> <i class="fas fa-magic text-sm"></i>
<span>{{ getShareButtonText() }}</span> <span>{{ getShareButtonText() }}</span>
</button> </button>
<!-- 详细信息按钮 --> <!-- 详细信息按钮 -->
<button @click="showDetails = !showDetails" <button @click="showDetails = !showDetails"
class="w-full rounded-full bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 px-6 sm:px-8 py-2.5 sm:py-3 text-sm sm:text-[15px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98] transition-all duration-200 tracking-tight flex items-center justify-center gap-2"> class="w-full rounded-full bg-white dark:bg-[#3a3a3c] border border-black/8 dark:border-white/8 px-8 py-3 text-[15px] font-medium text-[#1d1d1f] dark:text-[#f5f5f7] hover:bg-white/80 dark:hover:bg-[#3a3a3c]/80 hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_4px_12px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_4px_12px_rgba(0,0,0,0.3)] active:scale-[0.98] transition-all duration-200 tracking-tight flex items-center justify-center gap-2">
<i :class="showDetails ? 'fas fa-chevron-up' : 'fas fa-info-circle'" class="text-sm"></i> <i :class="showDetails ? 'fas fa-chevron-up' : 'fas fa-info-circle'" class="text-sm"></i>
<span>{{ showDetails ? t('hideDetails') : t('showDetails') }}</span> <span>{{ showDetails ? t('hideDetails') : t('showDetails') }}</span>
</button> </button>
</div> </div>
<!-- 技术信息 - Apple 风格 - 响应式 --> <!-- 技术信息 - Apple 风格 - 响应式 -->
<div class="text-center lg:text-left pt-4 sm:pt-6 border-t border-black/8 dark:border-white/8"> <div class="text-center pt-4 sm:pt-6 border-t border-black/8 dark:border-white/8">
<a href="https://github.com/ModelTC/LightX2V" <a href="https://github.com/ModelTC/LightX2V"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-flex items-center gap-2 text-xs sm:text-sm text-[#86868b] dark:text-[#98989d] hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)] transition-colors tracking-tight"> class="inline-flex items-center gap-2 text-sm text-[#86868b] dark:text-[#98989d] hover:text-[color:var(--brand-primary)] dark:hover:text-[color:var(--brand-primary-light)] transition-colors tracking-tight">
<i class="fab fa-github text-sm sm:text-base"></i> <i class="fab fa-github text-base"></i>
<span>{{ t('poweredByLightX2V') }}</span> <span>{{ t('poweredByLightX2V') }}</span>
<i class="fas fa-external-link-alt text-[10px] sm:text-xs"></i> <i class="fas fa-external-link-alt text-xs"></i>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- 详细信息面板 - Apple 风格 - 响应式 --> <!-- 详细信息面板 - Apple 风格 -->
<div v-if="showDetails && shareData" class="w-full bg-white dark:bg-[#1c1c1e] border-t border-black/8 dark:border-white/8 py-8 sm:py-12 lg:py-16"> <div v-if="showDetails && shareData" class="w-full bg-white dark:bg-[#1c1c1e] border-t border-black/8 dark:border-white/8 py-16">
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-12"> <div class="max-w-6xl mx-auto px-6 sm:px-8 lg:px-12">
<!-- 输入素材标题 - Apple 风格 - 响应式 --> <!-- 输入素材标题 - Apple 风格 -->
<h2 class="text-xl sm:text-2xl lg:text-3xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-3 mb-6 sm:mb-8 lg:mb-10 tracking-tight"> <h2 class="text-2xl sm:text-3xl font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] flex items-center justify-center gap-3 mb-10 tracking-tight">
<i class="fas fa-upload text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-upload text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<span>{{ t('inputMaterials') }}</span> <span>{{ t('inputMaterials') }}</span>
</h2> </h2>
<!-- 三个卡片 - Apple 风格 - 响应式竖向排列 --> <!-- 三个并列的分块卡片 - Apple 风格 -->
<div class="flex flex-col md:grid md:grid-cols-3 gap-4 sm:gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- 图片卡片 - Apple 风格 --> <!-- 图片卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]">
<!-- 卡片头部 - 响应式 --> <!-- 卡片头部 -->
<div class="flex items-center justify-between px-4 sm:px-5 py-3 sm:py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8"> <div class="flex items-center justify-between px-5 py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-2 sm:gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-image text-base sm:text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-image text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<h3 class="text-sm sm:text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('image') }}</h3> <h3 class="text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('image') }}</h3>
</div> </div>
</div> </div>
<!-- 卡片内容 - 响应式 - 带滚动条 --> <!-- 卡片内容 -->
<div class="p-4 sm:p-6 min-h-[150px] sm:min-h-[200px] max-h-[300px] overflow-y-auto main-scrollbar"> <div class="p-6 min-h-[200px]">
<div v-if="getImageMaterials().length > 0"> <div v-if="getImageMaterials().length > 0">
<div v-for="[inputName, url] in getImageMaterials()" :key="inputName" class="rounded-xl overflow-hidden border border-black/8 dark:border-white/8"> <div v-for="[inputName, url] in getImageMaterials()" :key="inputName" class="rounded-xl overflow-hidden border border-black/8 dark:border-white/8">
<img :src="url" :alt="inputName" <img :src="url" :alt="inputName"
...@@ -442,17 +440,17 @@ onMounted(async () => { ...@@ -442,17 +440,17 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 音频卡片 - Apple 风格 - 响应式 --> <!-- 音频卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]">
<!-- 卡片头部 - 响应式 --> <!-- 卡片头部 -->
<div class="flex items-center justify-between px-4 sm:px-5 py-3 sm:py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8"> <div class="flex items-center justify-between px-5 py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-2 sm:gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-music text-base sm:text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-music text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<h3 class="text-sm sm:text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('audio') }}</h3> <h3 class="text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('audio') }}</h3>
</div> </div>
</div> </div>
<!-- 卡片内容 - 响应式 - 带滚动条 --> <!-- 卡片内容 -->
<div class="p-4 sm:p-6 min-h-[150px] sm:min-h-[200px] max-h-[300px] overflow-y-auto main-scrollbar"> <div class="p-6 min-h-[200px]">
<div v-if="getAudioMaterials().length > 0" class="space-y-4"> <div v-if="getAudioMaterials().length > 0" class="space-y-4">
<div v-for="[inputName, url] in getAudioMaterials()" :key="inputName"> <div v-for="[inputName, url] in getAudioMaterials()" :key="inputName">
<audio :src="url" controls class="w-full rounded-xl"></audio> <audio :src="url" controls class="w-full rounded-xl"></audio>
...@@ -465,23 +463,23 @@ onMounted(async () => { ...@@ -465,23 +463,23 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 提示词卡片 - Apple 风格 - 响应式 --> <!-- 提示词卡片 - Apple 风格 -->
<div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]"> <div class="bg-white/80 dark:bg-[#2c2c2e]/80 backdrop-blur-[20px] border border-black/8 dark:border-white/8 rounded-2xl overflow-hidden transition-all duration-200 hover:bg-white dark:hover:bg-[#3a3a3c] hover:border-black/12 dark:hover:border-white/12 hover:shadow-[0_8px_24px_rgba(0,0,0,0.1)] dark:hover:shadow-[0_8px_24px_rgba(0,0,0,0.3)]">
<!-- 卡片头部 - 响应式 --> <!-- 卡片头部 -->
<div class="flex items-center justify-between px-4 sm:px-5 py-3 sm:py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8"> <div class="flex items-center justify-between px-5 py-4 bg-[color:var(--brand-primary)]/5 dark:bg-[color:var(--brand-primary-light)]/10 border-b border-black/8 dark:border-white/8">
<div class="flex items-center gap-2 sm:gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-file-alt text-base sm:text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i> <i class="fas fa-file-alt text-lg text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)]"></i>
<h3 class="text-sm sm:text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('prompt') }}</h3> <h3 class="text-base font-semibold text-[#1d1d1f] dark:text-[#f5f5f7] tracking-tight">{{ t('prompt') }}</h3>
</div> </div>
<button v-if="shareData.prompt" <button v-if="shareData.prompt"
@click="copyPrompt(shareData.prompt)" @click="copyPrompt(shareData.prompt)"
class="w-7 h-7 sm:w-8 sm:h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100" class="w-8 h-8 flex items-center justify-center bg-[color:var(--brand-primary)]/10 dark:bg-[color:var(--brand-primary-light)]/15 border border-[color:var(--brand-primary)]/20 dark:border-[color:var(--brand-primary-light)]/20 text-[color:var(--brand-primary)] dark:text-[color:var(--brand-primary-light)] rounded-lg transition-all duration-200 hover:scale-110 active:scale-100"
:title="t('copy')"> :title="t('copy')">
<i class="fas fa-copy text-[10px] sm:text-xs"></i> <i class="fas fa-copy text-xs"></i>
</button> </button>
</div> </div>
<!-- 卡片内容 - 响应式 --> <!-- 卡片内容 -->
<div class="p-4 sm:p-6 min-h-[150px] sm:min-h-[200px]"> <div class="p-6 min-h-[200px]">
<div v-if="shareData.prompt" class="bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[10px] border border-black/6 dark:border-white/6 rounded-xl p-4"> <div v-if="shareData.prompt" class="bg-white/50 dark:bg-[#1e1e1e]/50 backdrop-blur-[10px] border border-black/6 dark:border-white/6 rounded-xl p-4">
<p class="text-sm text-[#1d1d1f] dark:text-[#f5f5f7] leading-relaxed tracking-tight break-words">{{ shareData.prompt }}</p> <p class="text-sm text-[#1d1d1f] dark:text-[#f5f5f7] leading-relaxed tracking-tight break-words">{{ shareData.prompt }}</p>
</div> </div>
...@@ -494,13 +492,12 @@ onMounted(async () => { ...@@ -494,13 +492,12 @@ onMounted(async () => {
</div> </div>
</div> </div>
</div> </div>
</div>
</div> </div>
</div>
<!-- 全局路由跳转Loading覆盖层 - Apple 风格 --> <!-- 全局路由跳转Loading覆盖层 - Apple 风格 -->
<div v-show="isLoading" class="fixed inset-0 bg-[#f5f5f7] dark:bg-[#000000] flex items-center justify-center z-[9999]"> <div v-show="isLoading" class="fixed inset-0 bg-[#f5f5f7] dark:bg-[#000000] flex items-center justify-center z-[9999]">
<Loading /> <Loading />
</div>
</div> </div>
</template> </template>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment