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 @@
-
-
-
搜索
重置
@@ -64,7 +56,11 @@
-
+
+
+ {{ getAiTypeLabel(scope.row.aiId) }}
+
+
@@ -79,7 +75,13 @@
>保存
-
+
+
+ 停用
+ 正常
+ {{ scope.row.status }}
+
+
@@ -112,8 +114,15 @@
-
-
+
+
+
+
@@ -188,15 +197,22 @@ export default {
chineseContent: null,
englishContent: null,
imageUrl: null,
+ aiId: null,
status: null,
},
// 表单参数
form: {},
+ // AI类型选项
+ aiTypeOptions: [
+ { value: 1, label: "快捷生图" },
+ { value: 11, label: "一键脱衣" },
+ { value: 12, label: "图生图2" },
+ { value: 13, label: "一键换脸" },
+ { value: 21, label: "快捷生视频" },
+ { value: 22, label: "视频换脸" }
+ ],
// 表单校验
rules: {
- name: [
- { required: true, message: "模版名称不能为空", trigger: "blur" },
- ],
chineseContent: [
{ required: true, message: "模版中文不能为空", trigger: "blur" },
],
@@ -238,6 +254,7 @@ export default {
chineseContent: null,
englishContent: null,
imageUrl: null,
+ aiId: null,
status: 1, // 默认状态为正常
remark: null,
createTime: null,
@@ -248,6 +265,14 @@ export default {
}
this.resetForm("form")
},
+ /** 获取AI类型标签 */
+ getAiTypeLabel(aiId) {
+ if (aiId == null) return '-'
+ // 处理数字或字符串类型的 aiId
+ const value = typeof aiId === 'string' ? parseInt(aiId) : aiId
+ const option = this.aiTypeOptions.find(item => item.value === value)
+ return option ? option.label : aiId || '-'
+ },
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1
diff --git a/portal-ui/src/components/RechargePc.vue b/portal-ui/src/components/RechargePc.vue
index 7fea399..81bc963 100644
--- a/portal-ui/src/components/RechargePc.vue
+++ b/portal-ui/src/components/RechargePc.vue
@@ -73,47 +73,6 @@
-
-
-
-
-
-
- {{ $t('common.recharge') }}
-
-
-
-
-
- {{ $t('common.cancel') }}
-
-
- {{ $t('common.ok') }}
-
-
-
@@ -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 || []