fix: 前端优化 支付相关,保存视频,模版内容

This commit is contained in:
old burden 2026-01-22 15:21:48 +08:00
parent 73888efd14
commit 4ee2c02daf
4 changed files with 430 additions and 220 deletions

View File

@ -1,14 +1,6 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="模版名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入模版名称"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@ -64,7 +56,11 @@
<el-table v-loading="loading" :data="templateList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="模版名称" align="center" prop="name" />
<el-table-column label="AI类型" align="center" prop="aiId">
<template slot-scope="scope">
<span>{{ getAiTypeLabel(scope.row.aiId) }}</span>
</template>
</el-table-column>
<el-table-column label="模版中文" align="center" prop="chineseContent" />
<el-table-column label="模版英文" align="center" prop="englishContent" />
<el-table-column label="模版图片URL" align="center" prop="imageUrl">
@ -79,7 +75,13 @@
>保存</el-button>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" />
<el-table-column label="状态" align="center" prop="status">
<template slot-scope="scope">
<span v-if="scope.row.status === 0">停用</span>
<span v-else-if="scope.row.status === 1">正常</span>
<span v-else>{{ scope.row.status }}</span>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
@ -112,8 +114,15 @@
<!-- 添加或修改AI模版对话框 -->
<el-dialog :title="title" :visible.sync="open" width="75%" :close-on-click-modal="false" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="模版名称" prop="name">
<el-input v-model="form.name" placeholder="请输入模版名称" />
<el-form-item label="AI类型" prop="aiId">
<el-select v-model="form.aiId" placeholder="请选择AI类型" clearable>
<el-option
v-for="item in aiTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="模版中文">
<el-input v-model="form.chineseContent" placeholder="请输入模版中文"/>
@ -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

View File

@ -73,47 +73,6 @@
</mf-button>
</div>
</mf-dialog>
<!-- 银行卡信息输入对话框 -->
<mf-dialog
:visible="cardInfoVisible"
modal-class="card-info-dialog"
class="card-info-dialog-wrapper"
:title="$t('common.recharge')"
hideTitle
:footer="false"
unmountOnClose
@cancel="cancelCardInfo">
<div class="card-info-close">
<mf-icon
value="icon-close"
@click="cancelCardInfo" />
</div>
<div class="card-info-title">
{{ $t('common.recharge') }}
</div>
<mf-input
class="card-info-input"
:placeholder="$t('common.cardNo')"
v-model="cardno" />
<mf-input
class="card-info-input"
:placeholder="$t('common.cardName')"
v-model="cardname" />
<div class="card-info-submit">
<mf-button
size="large"
@click="cancelCardInfo">
{{ $t('common.cancel') }}
</mf-button>
<mf-button
size="large"
type="primary"
:loading="loading"
@click="submitCardInfo">
{{ $t('common.ok') }}
</mf-button>
</div>
</mf-dialog>
</div>
</template>
@ -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') || '充值成功,请等待处理')
} else {
//
// URL
this.orderNo = res.data?.orderNo;
this.rechargeUrl = res.data?.payUrl;
if (res.data?.payUrl) {
window.open(res.data.payUrl)
}
} else {
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;

View File

@ -10,6 +10,15 @@
{{ $t('common.uploadImageTip') }}
</div>
</div>
<!-- 上传方式选择 -->
<div class="upload-type-select">
<a-radio-group v-model="uploadType" @change="handleUploadTypeChange">
<a-radio value="upload">{{ $t('common.uploadImage') }}</a-radio>
<a-radio value="template">{{ $t('common.selectTemplate') }}</a-radio>
</a-radio-group>
</div>
<!-- 上传图片方式 -->
<div v-if="uploadType === 'upload'" class="upload-content">
<mf-image-upload
listType="draggable"
uploadHeight="200px"
@ -24,6 +33,27 @@
:content="$t('common.uploadLastPlaceholder')"
v-model="lastUrl" />
</div>
<!-- 选择模版方式 -->
<div v-else class="template-content">
<div v-if="selectedTemplatePreview" class="template-preview">
<a-image
:src="selectedTemplatePreview"
fit="contain"
:preview="false"
class="preview-image" />
<div class="template-preview-actions">
<a-button type="primary" @click="openTemplateDialog">
{{ $t('common.reselectTemplate') }}
</a-button>
</div>
</div>
<div v-else class="template-placeholder">
<mf-button type="primary" size="large" @click="openTemplateDialog">
{{ $t('common.selectTemplate') }}
</mf-button>
</div>
</div>
</div>
<div
class="tags"
v-for="tag in tags">
@ -59,6 +89,41 @@
{{ $t('common.generateTip') }}
</div>
</div>
<!-- 模版选择弹窗 -->
<a-modal
v-model:visible="templateDialogVisible"
:title="$t('common.selectTemplate')"
:footer="false"
width="50%"
@cancel="handleTemplateDialogClose">
<div class="template-dialog-wrapper">
<div class="template-dialog-content">
<a-spin :loading="templateLoading">
<div class="template-grid">
<div
class="template-item"
v-for="template in templateList"
:key="template.id"
:class="{ 'selected': selectedTemplate && selectedTemplate.id === template.id }"
@click="selectTemplate(template)">
<a-image
:src="getImageUrl(template.imageUrl || template.img_url)"
:preview="false"
fit="cover" />
<div class="template-name">{{ getTemplateName(template) }}</div>
</div>
</div>
<div v-if="templateList.length === 0" class="empty-template">
{{ $t('common.noTemplates') }}
</div>
</a-spin>
</div>
<div class="template-dialog-footer">
<a-button @click="handleTemplateDialogClose">{{ $t('common.cancel') }}</a-button>
<a-button type="primary" :disabled="!selectedTemplate" @click="handleConfirmTemplate">{{ $t('common.confirm') }}</a-button>
</div>
</div>
</a-modal>
<div
class="right"
v-if="showResult">
@ -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,9 +211,12 @@ export default {
},
mounted() {
let { url, text } = this.$route.query || {}
// URL
if (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,6 +625,20 @@ export default {
}
}
.upload-type-select {
margin-bottom: 16px;
:deep(.arco-radio-group) {
.arco-radio {
margin-right: 24px;
&-label {
font-size: 14px;
color: #5c5d68;
}
}
}
}
.upload-content {
.mf-image-upload {
margin-bottom: 16px;
@ -410,6 +648,43 @@ export default {
}
}
.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;
}
}
}
.tags {
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
@ -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 {

View File

@ -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 || []