diff --git a/admin-ui/src/views/ai/template/index.vue b/admin-ui/src/views/ai/template/index.vue index be155ce..4ef3852 100644 --- a/admin-ui/src/views/ai/template/index.vue +++ b/admin-ui/src/views/ai/template/index.vue @@ -1,14 +1,6 @@ - + + + @@ -131,11 +90,7 @@ export default { rechargeRemark: '', payVisible: false, orderNo: null, - showPay: import.meta.env.VITE_SHOW_PAY == "SUCCESS", - cardInfoVisible: false, - cardno: '', - cardname: '', - currentGearId: null + showPay: import.meta.env.VITE_SHOW_PAY == "SUCCESS" } }, props: { @@ -153,12 +108,6 @@ export default { this.orderNo = null this.payVisible = false }, - cancelCardInfo() { - this.cardInfoVisible = false - this.cardno = '' - this.cardname = '' - this.currentGearId = null - }, selectItem(item, idx) { this.selectedIndex = idx this.rechargeRemark = item.title @@ -218,37 +167,13 @@ export default { }, ok() { let selectedItem = this.dataList[this.selectedIndex] - // 判断是否是 jinsha-pay 接口,需要先输入银行卡信息 - if (this.apiUrl && this.apiUrl.includes('jinsha-pay')) { - this.currentGearId = selectedItem.id - this.cardInfoVisible = true - return - } - // 其他支付方式直接调用 this.doRecharge(selectedItem.id) }, - submitCardInfo() { - // 验证银行卡信息 - if (!this.cardno || !this.cardno.trim()) { - this.$message.error(this.$t('common.cardNoRequired') || '请输入银行卡号') - return - } - if (!this.cardname || !this.cardname.trim()) { - this.$message.error(this.$t('common.cardNameRequired') || '请输入银行卡姓名') - return - } - this.doRecharge(this.currentGearId, this.cardno.trim(), this.cardname.trim()) - }, - doRecharge(gearId, cardno, cardname) { + doRecharge(gearId) { this.loading = true let params = { gearId: gearId } - // 如果是 jinsha-pay,添加银行卡信息参数 - if (cardno && cardname) { - params.cardno = cardno - params.cardname = cardname - } this.$axios({ url: this.apiUrl, method: 'get', @@ -262,21 +187,13 @@ export default { return; } if (res.code == 200) { - // jinsha-pay 返回格式: {code: 0, msg: "成功"} - // 其他支付方式返回: {orderNo, payUrl} - if (this.apiUrl && this.apiUrl.includes('jinsha-pay')) { - // jinsha支付成功,关闭对话框 - this.cardInfoVisible = false - this.cardno = '' - this.cardname = '' - this.$message.success(this.$t('common.rechargeSuccessfully') || '充值成功,请等待处理') + // 获取支付URL并跳转 + this.orderNo = res.data?.orderNo; + this.rechargeUrl = res.data?.payUrl; + if (res.data?.payUrl) { + window.open(res.data.payUrl) } else { - // 其他支付方式,跳转支付页面 - this.orderNo = res.data?.orderNo; - this.rechargeUrl = res.data?.payUrl; - if (res.data?.payUrl) { - window.open(res.data.payUrl) - } + this.$message.error(this.$t('common.rechargeFailed') || '获取支付链接失败') } } }) @@ -394,96 +311,6 @@ export default { } } } - -.card-info-dialog { - border-radius: 20px; - overflow: hidden; - background: linear-gradient( - 0deg, - rgba(39, 20, 51, 0.7) 0%, - rgba(230, 33, 122, 0.7) 49% - ); - border-radius: 20px; - border: 2px solid #e6217a; - width: 500px; - top: 45% !important; - transform: translateY(-45%) !important; - &-wrapper { - .arco-modal-mask { - backdrop-filter: blur(10px); - background-color: rgba(0, 0, 0, 0.7); - } - } - - .arco-modal-body { - padding: 24px 30px 30px 30px; - } - - .card-info-close { - position: absolute; - right: 12px; - top: 6px; - cursor: pointer; - color: #fff; - - .mf-icon { - font-size: 14px; - } - } - - .card-info-title { - font-size: 20px; - color: #ffffff; - margin-bottom: 30px; - display: flex; - align-items: center; - justify-content: center; - } - - .card-info-input { - display: flex; - align-items: center; - border-radius: 10px; - justify-content: center; - border-radius: 10px; - border: 1px solid rgba(#ffffff, 0.3); - height: 40px; - margin-top: 20px; - padding: 0 16px; - font-size: 14px; - color: #ffffff; - position: relative; - cursor: pointer; - background-color: transparent; - cursor: text; - font-size: 14px; - - ::placeholder { - color: rgba(#fff, 0.5); - } - - &.arco-input-wrapper:focus-within { - background-color: transparent; - } - - &:hover { - background-color: transparent; - border-color: #fff; - } - } - - .card-info-submit { - display: flex; - align-items: center; - justify-content: center; - margin-top: 30px; - gap: 14px; - .mf-button { - width: 160px; - border-radius: 10px; - } - } -} .mf-recharge-pc { display: flex; flex-flow: wrap; diff --git a/portal-ui/src/views/FastVideo.vue b/portal-ui/src/views/FastVideo.vue index 24a4a6c..25c68f7 100644 --- a/portal-ui/src/views/FastVideo.vue +++ b/portal-ui/src/views/FastVideo.vue @@ -10,19 +10,49 @@ {{ $t('common.uploadImageTip') }} - + +
+ + {{ $t('common.uploadImage') }} + {{ $t('common.selectTemplate') }} + +
+ +
+ - + +
+ +
+
+ +
+ + {{ $t('common.reselectTemplate') }} + +
+
+
+ + {{ $t('common.selectTemplate') }} + +
+
+ + +
+
+ +
+
+ +
{{ getTemplateName(template) }}
+
+
+
+ {{ $t('common.noTemplates') }} +
+
+
+ +
+
@@ -117,7 +182,6 @@ export default { prefixCls: 'fast-video', firstUrl: '', lastUrl: '', - current: 1, text: '', interval: null, videoUrl: null, @@ -130,7 +194,16 @@ export default { tags: [], selectedTags: {}, showResult: !this.$base.isMobile(), - id: null + id: null, + // 上传方式选择 + uploadType: 'upload', // 'upload' 或 'template' + // 模版选择相关 + templateDialogVisible: false, + templateList: [], + templateLoading: false, + selectedTemplate: null, + selectedTemplatePreview: '', // 选中的模版预览图URL + maxPollAttempts: 40 // 最大轮询次数(3秒 * 40 = 120秒超时) } }, beforeUnmount() { @@ -138,8 +211,11 @@ export default { }, mounted() { let { url, text } = this.$route.query || {} + // 安全验证:只接受有效的URL格式 if (url) { - this.firstUrl = url + if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'))) { + this.firstUrl = url + } } if (text) { this.text = text @@ -171,6 +247,100 @@ export default { close() { this.showResult = false }, + // 上传方式改变 + handleUploadTypeChange() { + // 切换上传方式时,清空之前的选择 + if (this.uploadType === 'upload') { + this.selectedTemplatePreview = '' + this.selectedTemplate = null + this.firstUrl = '' + } else { + this.firstUrl = '' + } + }, + // 打开模版选择弹窗 + openTemplateDialog() { + this.templateDialogVisible = true + this.selectedTemplate = null + // 加载模板列表 + this.loadTemplateList() + }, + // 加载模板列表 + loadTemplateList() { + this.templateLoading = true + // 快捷生视频,ai_id = 21 + this.$axios({ + url: 'api/template/getTemplateList', + method: 'GET', + data: { + position: 0, + aiId: 21 + } + }).then(res => { + this.templateList = res.rows || [] + // 如果之前已选择过模版,恢复选中状态 + if (this.selectedTemplatePreview && this.templateList.length > 0) { + const prevTemplate = this.templateList.find(t => + this.getImageUrl(t.imageUrl || t.img_url) === this.selectedTemplatePreview + ) + if (prevTemplate) { + this.selectedTemplate = prevTemplate + } + } + }).catch(err => { + console.error('加载模板列表失败:', err) + this.templateList = [] + }).finally(() => { + this.templateLoading = false + }) + }, + // 选择模板 + selectTemplate(template) { + this.selectedTemplate = template + }, + // 获取图片URL + getImageUrl(url) { + if (!url) return '' + if (url.startsWith('/admin')) { + return '/api' + url + } else if (url.startsWith('/api')) { + return url + } else if (url.startsWith('http://') || url.startsWith('https://')) { + return url + } else { + return '/api' + url + } + }, + // 获取模板名称(根据当前语言) + getTemplateName(template) { + if (!template) return '' + // 如果是中文繁体,显示 chineseContent + if (this.lang === 'zh_HK') { + return template.chineseContent || template.name || '' + } + // 如果是英文,显示 englishContent + else if (this.lang === 'en_US') { + return template.englishContent || template.name || '' + } + // 默认返回 name + return template.name || '' + }, + // 确认选择模板 + handleConfirmTemplate() { + if (this.selectedTemplate) { + const imageUrl = this.getImageUrl(this.selectedTemplate.imageUrl || this.selectedTemplate.img_url) + this.selectedTemplatePreview = imageUrl + this.firstUrl = { + url: imageUrl + } + this.templateDialogVisible = false + } + }, + // 关闭模版选择弹窗 + handleTemplateDialogClose() { + this.templateDialogVisible = false + // 不重置selectedTemplate,保持用户的选择状态,方便二次选择 + }, getTags() { this.$axios({ url: 'api/tag/list', @@ -191,8 +361,51 @@ export default { } }) }, - saveVideo() { - this.$file.downloadFile(this.videoUrl) + async saveVideo() { + if (!this.videoUrl) { + this.$message.error(this.$t('common.downloadFailed') || '视频URL为空') + return + } + try { + // 获取视频的 blob 数据 + const response = await fetch(this.videoUrl) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + const blob = await response.blob() + + // 从 URL 中提取文件名,如果没有则使用默认名称 + const urlParts = this.videoUrl.split('/') + let fileName = urlParts[urlParts.length - 1] || 'video.mp4' + // 移除查询参数 + fileName = fileName.split('?')[0] + // 如果没有扩展名,尝试从 Content-Type 获取 + if (!fileName.includes('.')) { + const contentType = response.headers.get('content-type') || 'video/mp4' + const extension = contentType.split('/')[1]?.split(';')[0] || 'mp4' + // 从URL路径中尝试提取扩展名 + const pathMatch = this.videoUrl.match(/\.(mp4|webm|ogg|mov|avi|wmv|flv|mkv)/i) + if (pathMatch) { + fileName = `video_${new Date().getTime()}.${pathMatch[1]}` + } else { + fileName = `video_${new Date().getTime()}.${extension}` + } + } + + // 创建下载链接 + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = fileName + link.style.display = 'none' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(url) + } catch (error) { + console.error('下载视频失败:', error) + this.$message.error(this.$t('common.downloadFailed') || '下载失败') + } }, destroyInterval() { if (this.interval) { @@ -226,7 +439,16 @@ export default { }, getVideo(videoId) { if (!videoId) return + let pollAttempts = 0 this.interval = setInterval((_) => { + pollAttempts++ + // 超时保护:超过最大轮询次数则停止 + if (pollAttempts > this.maxPollAttempts) { + this.destroyInterval() + this.$message.warning(this.$t('common.videoTimeout')) + this.videoLoading = false + return + } this.$axios({ url: `api/ai/${videoId}`, method: 'GET' @@ -244,11 +466,13 @@ export default { }, 3000) }, generateVideo() { - if (!this.firstUrl || !this.firstUrl.url) { + // 处理 firstUrl,可能是对象或字符串 + const firstImageUrl = typeof this.firstUrl === 'object' ? this.firstUrl.url : this.firstUrl + if (!firstImageUrl) { this.$message.error(this.$t('common.uploadFirstImageError')) return } - if (this.firstUrl.url.startsWith('blob:')) { + if (firstImageUrl.startsWith('blob:')) { this.$message.error(this.$t('common.uploadWaitImageError')) return } @@ -260,7 +484,7 @@ export default { this.generateLoading = true let params = { text: this.text, - firstUrl: this.firstUrl.url, + firstUrl: firstImageUrl, functionType: '21' } if (this.lastUrl && this.lastUrl.url) { @@ -401,11 +625,62 @@ export default { } } - .mf-image-upload { + .upload-type-select { margin-bottom: 16px; + :deep(.arco-radio-group) { + .arco-radio { + margin-right: 24px; + &-label { + font-size: 14px; + color: #5c5d68; + } + } + } + } - &:last-child { - margin-bottom: 0; + .upload-content { + .mf-image-upload { + margin-bottom: 16px; + + &:last-child { + margin-bottom: 0; + } + } + } + + .template-content { + .template-preview { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + overflow: hidden; + background: #1a1b20; + + .preview-image { + width: 100%; + height: 200px; + display: block; + + :deep(.arco-image-img) { + width: 100%; + height: 100%; + object-fit: contain; + } + } + + .template-preview-actions { + padding: 12px; + text-align: center; + } + } + + .template-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 200px; + border: 1px dashed rgba(255, 255, 255, 0.2); + border-radius: 8px; + background: #1a1b20; } } } @@ -491,6 +766,87 @@ export default { } } +// 模版选择弹窗样式 +.template-dialog-wrapper { + display: flex; + flex-direction: column; + max-height: 70vh; + min-height: 350px; +} + +.template-dialog-content { + flex: 1; + overflow-y: auto; + min-height: 0; + + .template-grid { + display: grid; + grid-template-columns: repeat(4, 20%); + gap: 16px; + padding: 10px 0; + + .template-item { + cursor: pointer; + border: 2px solid transparent; + border-radius: 8px; + overflow: hidden; + transition: all 0.3s; + position: relative; + + &:hover { + border-color: var(--color-primary-light-1); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &.selected { + border-color: rgb(var(--primary-6)); + box-shadow: 0 0 0 2px rgba(var(--primary-6), 0.2); + } + + :deep(.arco-image) { + width: 100%; + height: 300px; + display: block; + + .arco-image-img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + .template-name { + padding: 8px; + font-size: 12px; + color: var(--color-text-1); + text-align: center; + background: var(--color-fill-1); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + } + + .empty-template { + text-align: center; + padding: 60px 20px; + color: var(--color-text-3); + font-size: 14px; + } +} + +.template-dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 0 0 0; + border-top: 1px solid var(--color-border-2); + flex-shrink: 0; + background: var(--color-bg-1); +} + @media (max-width: 576px) { .fast-video { .left { diff --git a/portal-ui/src/views/Image.vue b/portal-ui/src/views/Image.vue index 7268d9a..e967613 100644 --- a/portal-ui/src/views/Image.vue +++ b/portal-ui/src/views/Image.vue @@ -266,11 +266,13 @@ export default { // 加载模板列表 loadTemplateList() { this.templateLoading = true + // 根据当前功能类型传递 ai_id 参数 this.$axios({ url: 'api/template/getTemplateList', method: 'GET', data: { - position: 0 + position: 0, + aiId: 11 } }).then(res => { this.templateList = res.rows || []