Compare commits
65 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
0db887f37e | |
|
|
746d73fe15 | |
|
|
8eef251731 | |
|
|
9032bd2f28 | |
|
|
5913ff8044 | |
|
|
8032c35cea | |
|
|
6a7abddfa4 | |
|
|
8afd28ca10 | |
|
|
df3bcf67d6 | |
|
|
19adf685e0 | |
|
|
9250253238 | |
|
|
6037bae970 | |
|
|
249565ef6d | |
|
|
c2c1ff8063 | |
|
|
eb8cab3f5a | |
|
|
fa99a4fb9b | |
|
|
b17aec4a1a | |
|
|
78b32dd252 | |
|
|
0b21a293b0 | |
|
|
37f3237f61 | |
|
|
4ea4d009a6 | |
|
|
dc56530d47 | |
|
|
9f7e43a21a | |
|
|
f3c3e78901 | |
|
|
353d8e0069 | |
|
|
67c252d174 | |
|
|
9a460e2de7 | |
|
|
96646cf5ed | |
|
|
e888da137e | |
|
|
e2ab5bcb13 | |
|
|
9c0f533aff | |
|
|
9705c4f472 | |
|
|
0d4b1d18be | |
|
|
eb5211b9e4 | |
|
|
aec9dc0152 | |
|
|
089ffca4f5 | |
|
|
8faff7562d | |
|
|
2d026feba3 | |
|
|
f075e6b2b1 | |
|
|
d2c1fff13f | |
|
|
a347873a4e | |
|
|
3ffd78f928 | |
|
|
2a91a33825 | |
|
|
b13e51a6ca | |
|
|
0f6695207e | |
|
|
afca767422 | |
|
|
c04c1e572b | |
|
|
ca2989902c | |
|
|
ff18ca4f51 | |
|
|
f9c12bb1e0 | |
|
|
a04bd71afc | |
|
|
4cb3f4eb88 | |
|
|
1edf9601c0 | |
|
|
adcfea0d3c | |
|
|
33a10f55a5 | |
|
|
55588504a1 | |
|
|
d69d16e196 | |
|
|
5ba6c9f746 | |
|
|
0e94beb477 | |
|
|
5ae8614b1d | |
|
|
4bba35a426 | |
|
|
abb8d279c4 | |
|
|
8959b775a4 | |
|
|
aed52b0437 | |
|
|
809b47215c |
|
|
@ -0,0 +1,29 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
unpackage
|
||||||
|
/dist/
|
||||||
|
/manager/dist/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
/test/unit/coverage/
|
||||||
|
/test/e2e/reports/
|
||||||
|
selenium-debug.log
|
||||||
|
nodejs/public/uploads
|
||||||
|
*.zip
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
package-lock.json
|
||||||
|
.history
|
||||||
|
/unpackage/resources/
|
||||||
|
/unpackage/dist/dev
|
||||||
|
/unpackage/dist/build/.automator
|
||||||
|
/unpackage/dist/build/app-plus
|
||||||
|
*.lock
|
||||||
|
/.logs/*
|
||||||
|
/portal-ui/vite.config.js.timestamp-1775717209686-71db82cf15ae7.mjs
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/python" />
|
||||||
|
<excludePattern pattern="*.zip" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,7 @@ VUE_APP_TITLE = 管理系统
|
||||||
ENV = 'production'
|
ENV = 'production'
|
||||||
|
|
||||||
# 若依管理系统/生产环境
|
# 若依管理系统/生产环境
|
||||||
VUE_APP_BASE_API = 'https://admin-api.undressing.name'
|
# VUE_APP_BASE_API = 'http://111.230.37.169:10009'
|
||||||
|
# VUE_APP_BASE_API = 'http://101.96.201.225:8011'
|
||||||
|
VUE_APP_BASE_API = 'http://47.86.170.114:8011'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,4 @@ NODE_ENV = production
|
||||||
# 测试环境配置
|
# 测试环境配置
|
||||||
ENV = 'staging'
|
ENV = 'staging'
|
||||||
|
|
||||||
# 若依管理系统/测试环境
|
VUE_APP_BASE_API = 'http://101.96.201.225:8011'
|
||||||
VUE_APP_BASE_API = '/api'
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ yarn-error.log*
|
||||||
tests/**/coverage/
|
tests/**/coverage/
|
||||||
tests/e2e/reports
|
tests/e2e/reports
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
|
*.zip
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function listDept(query) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/list',
|
||||||
|
method: 'get',
|
||||||
|
params: query
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listDeptExcludeChild(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/list/exclude/' + deptId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDept(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/' + deptId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept',
|
||||||
|
method: 'post',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept',
|
||||||
|
method: 'put',
|
||||||
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delDept(deptId) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/dept/' + deptId,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -81,3 +81,12 @@ export function updatePassword(id, newPassword) {
|
||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 分配归属部门,deptId 可省略或 null 表示清空 */
|
||||||
|
export function assignAiUserDept(data) {
|
||||||
|
return request({
|
||||||
|
url: '/ai/user/dept',
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch">
|
||||||
|
<el-form-item label="部门名称" prop="deptName">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.deptName"
|
||||||
|
placeholder="请输入部门名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter.native="handleQuery"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-select v-model="queryParams.status" placeholder="部门状态" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.label"
|
||||||
|
:value="dict.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</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>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['ai:dept:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="info"
|
||||||
|
plain
|
||||||
|
icon="el-icon-sort"
|
||||||
|
size="mini"
|
||||||
|
@click="toggleExpandAll"
|
||||||
|
>展开/折叠</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-if="refreshTable"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="deptList"
|
||||||
|
row-key="deptId"
|
||||||
|
:default-expand-all="isExpandAll"
|
||||||
|
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
||||||
|
>
|
||||||
|
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
|
||||||
|
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="创建时间" align="center" prop="createTime" width="200">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-edit"
|
||||||
|
@click="handleUpdate(scope.row)"
|
||||||
|
v-hasPermi="['ai:dept:edit']"
|
||||||
|
>修改</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
@click="handleAdd(scope.row)"
|
||||||
|
v-hasPermi="['ai:dept:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.parentId != 0"
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['ai:dept:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24" v-if="form.parentId !== 0">
|
||||||
|
<el-form-item label="上级部门" prop="parentId">
|
||||||
|
<treeselect v-model="form.parentId" :options="deptOptions" :normalizer="normalizer" placeholder="选择上级部门" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="部门名称" prop="deptName">
|
||||||
|
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="显示排序" prop="orderNum">
|
||||||
|
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="负责人" prop="leader">
|
||||||
|
<el-input v-model="form.leader" placeholder="请输入负责人" maxlength="20" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="联系电话" prop="phone">
|
||||||
|
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="部门状态">
|
||||||
|
<el-radio-group v-model="form.status">
|
||||||
|
<el-radio
|
||||||
|
v-for="dict in dict.type.sys_normal_disable"
|
||||||
|
:key="dict.value"
|
||||||
|
:label="dict.value"
|
||||||
|
>{{dict.label}}</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="Byte API Key">
|
||||||
|
<el-input
|
||||||
|
v-model="form.byteApiKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="视频模型">
|
||||||
|
<div class="model-parm-block">
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in modelParamRows"
|
||||||
|
:key="'mp-' + idx"
|
||||||
|
class="model-parm-row"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="row.label"
|
||||||
|
placeholder="显示名称(如 Seedance 2.0)"
|
||||||
|
class="model-parm-input-label"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="row.value"
|
||||||
|
placeholder="Endpoint / 模型 ID(ep-…)"
|
||||||
|
class="model-parm-input-value"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
:disabled="modelParamRows.length <= 1"
|
||||||
|
@click="removeModelParamRow(idx)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="text" icon="el-icon-plus" @click="addModelParamRow">添加模型</el-button>
|
||||||
|
<p class="model-parm-hint">
|
||||||
|
保存为 JSON 写入库表 model_parm;门户「视频生成」按用户所属二级部门读取。
|
||||||
|
留空或未配置时,使用配置文件 portal.video.models。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.model-parm-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.model-parm-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-label {
|
||||||
|
width: 38%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/ai/dept"
|
||||||
|
import Treeselect from "@riophae/vue-treeselect"
|
||||||
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AiDept",
|
||||||
|
dicts: ['sys_normal_disable'],
|
||||||
|
components: { Treeselect },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
showSearch: true,
|
||||||
|
deptList: [],
|
||||||
|
deptOptions: [],
|
||||||
|
title: "",
|
||||||
|
open: false,
|
||||||
|
isExpandAll: true,
|
||||||
|
refreshTable: true,
|
||||||
|
queryParams: {
|
||||||
|
deptName: undefined,
|
||||||
|
status: undefined
|
||||||
|
},
|
||||||
|
form: {},
|
||||||
|
modelParamRows: [{ label: '', value: '' }],
|
||||||
|
rules: {
|
||||||
|
parentId: [
|
||||||
|
{ required: true, message: "上级部门不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
deptName: [
|
||||||
|
{ required: true, message: "部门名称不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
orderNum: [
|
||||||
|
{ required: true, message: "显示排序不能为空", trigger: "blur" }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: "email",
|
||||||
|
message: "请输入正确的邮箱地址",
|
||||||
|
trigger: ["blur", "change"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{
|
||||||
|
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
|
||||||
|
message: "请输入正确的手机号码",
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isSecondLevelCompanyForm() {
|
||||||
|
if (this.form.ancestors === "0,100") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const pid = this.form.parentId
|
||||||
|
if (pid !== undefined && pid !== null && Number(pid) === 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listDept(this.queryParams).then(response => {
|
||||||
|
this.deptList = this.handleTree(response.data, "deptId")
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
normalizer(node) {
|
||||||
|
if (node.children && !node.children.length) {
|
||||||
|
delete node.children
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: node.deptId,
|
||||||
|
label: node.deptName,
|
||||||
|
children: node.children
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.open = false
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.form = {
|
||||||
|
deptId: undefined,
|
||||||
|
parentId: undefined,
|
||||||
|
ancestors: undefined,
|
||||||
|
deptName: undefined,
|
||||||
|
orderNum: undefined,
|
||||||
|
leader: undefined,
|
||||||
|
phone: undefined,
|
||||||
|
email: undefined,
|
||||||
|
byteApiKey: undefined,
|
||||||
|
modelParm: undefined,
|
||||||
|
status: "0"
|
||||||
|
}
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
this.resetForm("form")
|
||||||
|
},
|
||||||
|
handleQuery() {
|
||||||
|
this.getList()
|
||||||
|
},
|
||||||
|
resetQuery() {
|
||||||
|
this.resetForm("queryForm")
|
||||||
|
this.handleQuery()
|
||||||
|
},
|
||||||
|
handleAdd(row) {
|
||||||
|
this.reset()
|
||||||
|
if (row != undefined) {
|
||||||
|
this.form.parentId = row.deptId
|
||||||
|
}
|
||||||
|
this.open = true
|
||||||
|
this.title = "添加部门"
|
||||||
|
listDept().then(response => {
|
||||||
|
this.deptOptions = this.handleTree(response.data, "deptId")
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toggleExpandAll() {
|
||||||
|
this.refreshTable = false
|
||||||
|
this.isExpandAll = !this.isExpandAll
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.refreshTable = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
syncModelRowsFromForm() {
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
const raw = this.form.modelParm
|
||||||
|
if (!raw || String(raw).trim() === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(raw)
|
||||||
|
if (Array.isArray(arr) && arr.length) {
|
||||||
|
this.modelParamRows = arr.map(x => ({
|
||||||
|
label: (x && x.label) ? String(x.label) : '',
|
||||||
|
value: (x && x.value) ? String(x.value) : ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildModelParmPayload() {
|
||||||
|
if (!this.isSecondLevelCompanyForm) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = (this.modelParamRows || []).filter(r => r.label && r.value)
|
||||||
|
this.form.modelParm = rows.length ? JSON.stringify(rows) : ''
|
||||||
|
},
|
||||||
|
addModelParamRow() {
|
||||||
|
this.modelParamRows.push({ label: '', value: '' })
|
||||||
|
},
|
||||||
|
removeModelParamRow(idx) {
|
||||||
|
if (this.modelParamRows.length <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.modelParamRows.splice(idx, 1)
|
||||||
|
},
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset()
|
||||||
|
getDept(row.deptId).then(response => {
|
||||||
|
this.form = response.data
|
||||||
|
this.syncModelRowsFromForm()
|
||||||
|
this.open = true
|
||||||
|
this.title = "修改部门"
|
||||||
|
listDeptExcludeChild(row.deptId).then(response => {
|
||||||
|
this.deptOptions = this.handleTree(response.data, "deptId")
|
||||||
|
if (this.deptOptions.length == 0) {
|
||||||
|
const noResultsOptions = { deptId: this.form.parentId, deptName: this.form.parentName, children: [] }
|
||||||
|
this.deptOptions.push(noResultsOptions)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm: function() {
|
||||||
|
this.$refs["form"].validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
this.buildModelParmPayload()
|
||||||
|
if (this.form.deptId != undefined) {
|
||||||
|
updateDept(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("修改成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
addDept(this.form).then(response => {
|
||||||
|
this.$modal.msgSuccess("新增成功")
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
|
||||||
|
return delDept(row.deptId)
|
||||||
|
}).then(() => {
|
||||||
|
this.getList()
|
||||||
|
this.$modal.msgSuccess("删除成功")
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
@ -50,6 +50,16 @@
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="归属部门" prop="deptId">
|
||||||
|
<treeselect
|
||||||
|
v-model="queryParams.deptId"
|
||||||
|
:options="deptOptions"
|
||||||
|
:normalizer="deptNormalizer"
|
||||||
|
placeholder="全部"
|
||||||
|
clearable
|
||||||
|
style="width: 220px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
<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>
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
|
@ -57,6 +67,27 @@
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-row :gutter="10" class="mb8">
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="handleAdd"
|
||||||
|
v-hasPermi="['ai:user:add']"
|
||||||
|
>新增</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
icon="el-icon-delete"
|
||||||
|
size="mini"
|
||||||
|
:disabled="multiple"
|
||||||
|
@click="handleDelete()"
|
||||||
|
v-hasPermi="['ai:user:remove']"
|
||||||
|
>删除</el-button>
|
||||||
|
</el-col>
|
||||||
<el-col :span="1.5">
|
<el-col :span="1.5">
|
||||||
<el-button
|
<el-button
|
||||||
type="warning"
|
type="warning"
|
||||||
|
|
@ -78,6 +109,7 @@
|
||||||
<el-table-column label="上级ID" align="center" prop="superiorUuid" />
|
<el-table-column label="上级ID" align="center" prop="superiorUuid" />
|
||||||
<el-table-column label="上级账号" align="center" prop="superiorName" />
|
<el-table-column label="上级账号" align="center" prop="superiorName" />
|
||||||
<el-table-column label="用户昵称" align="center" prop="nickname" />
|
<el-table-column label="用户昵称" align="center" prop="nickname" />
|
||||||
|
<el-table-column label="归属部门" align="center" prop="deptName" width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="邮箱" align="center" prop="email" />
|
<el-table-column label="邮箱" align="center" prop="email" />
|
||||||
<el-table-column label="性别" align="center" prop="gender">
|
<el-table-column label="性别" align="center" prop="gender">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
|
@ -103,8 +135,15 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="余额" align="center" prop="balance" />
|
<el-table-column label="余额" align="center" prop="balance" />
|
||||||
<el-table-column label="source" align="center" prop="source" />
|
<el-table-column label="source" align="center" prop="source" />
|
||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="310">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-office-building"
|
||||||
|
@click="handleOpenAssignDept(scope.row)"
|
||||||
|
v-hasPermi="['ai:user:edit']"
|
||||||
|
>分配部门</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -119,6 +158,13 @@
|
||||||
@click="updateBalance(scope.row)"
|
@click="updateBalance(scope.row)"
|
||||||
v-hasPermi="['ai:user:remove']"
|
v-hasPermi="['ai:user:remove']"
|
||||||
>修改余额</el-button>
|
>修改余额</el-button>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
v-hasPermi="['ai:user:remove']"
|
||||||
|
>删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
@ -132,8 +178,24 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 添加或修改ai-用户信息对话框 -->
|
<!-- 添加或修改ai-用户信息对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="520px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="88px">
|
||||||
|
<el-form-item label="用户账号" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="门户登录账号,必填"
|
||||||
|
:disabled="form.id != null"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
:placeholder="form.id != null ? '不填表示不修改密码' : '请输入登录密码'"
|
||||||
|
autocomplete="new-password"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="用户昵称" prop="nickname">
|
<el-form-item label="用户昵称" prop="nickname">
|
||||||
<el-input v-model="form.nickname" placeholder="请输入用户昵称" />
|
<el-input v-model="form.nickname" placeholder="请输入用户昵称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
@ -158,10 +220,10 @@
|
||||||
<el-form-item label="余额" prop="balance">
|
<el-form-item label="余额" prop="balance">
|
||||||
<el-input v-model="form.balance" placeholder="请输入余额" />
|
<el-input v-model="form.balance" placeholder="请输入余额" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-form-item label="备注" prop="remark">
|
|
||||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
|
||||||
</el-form-item>
|
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
<el-button @click="cancel">取 消</el-button>
|
<el-button @click="cancel">取 消</el-button>
|
||||||
|
|
@ -182,6 +244,25 @@
|
||||||
</div>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 分配部门 -->
|
||||||
|
<el-dialog title="分配归属部门" :visible.sync="assignDeptOpen" width="480px" append-to-body @close="cancelAssignDept">
|
||||||
|
<el-form label-width="88px">
|
||||||
|
<el-form-item label="归属部门">
|
||||||
|
<treeselect
|
||||||
|
v-model="assignForm.deptId"
|
||||||
|
:options="deptOptions"
|
||||||
|
:normalizer="deptNormalizer"
|
||||||
|
placeholder="不选则不归属任何部门"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitAssignDept">确 定</el-button>
|
||||||
|
<el-button @click="cancelAssignDept">取 消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 修改余额对话框 -->
|
<!-- 修改余额对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="openUpdateBalance" width="500px" append-to-body>
|
<el-dialog :title="title" :visible.sync="openUpdateBalance" width="500px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
|
|
@ -207,12 +288,17 @@ import {
|
||||||
updateUser,
|
updateUser,
|
||||||
changeBalance,
|
changeBalance,
|
||||||
changeUserStatus,
|
changeUserStatus,
|
||||||
updatePassword
|
updatePassword,
|
||||||
|
assignAiUserDept
|
||||||
} from "@/api/ai/user";
|
} from "@/api/ai/user";
|
||||||
|
import { listDept } from "@/api/ai/dept";
|
||||||
|
import Treeselect from "@riophae/vue-treeselect";
|
||||||
|
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "User",
|
name: "User",
|
||||||
dicts: ["sys_normal_disable", "sys_user_sex"],
|
dicts: ["sys_normal_disable", "sys_user_sex"],
|
||||||
|
components: { Treeselect },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// 遮罩层
|
// 遮罩层
|
||||||
|
|
@ -235,6 +321,12 @@ export default {
|
||||||
open: false,
|
open: false,
|
||||||
openUpdatePassword: false,
|
openUpdatePassword: false,
|
||||||
openUpdateBalance: false,
|
openUpdateBalance: false,
|
||||||
|
assignDeptOpen: false,
|
||||||
|
deptOptions: [],
|
||||||
|
assignForm: {
|
||||||
|
id: null,
|
||||||
|
deptId: null
|
||||||
|
},
|
||||||
// 查询参数
|
// 查询参数
|
||||||
queryParams: {
|
queryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
|
|
@ -252,25 +344,73 @@ export default {
|
||||||
paymentUrl: null,
|
paymentUrl: null,
|
||||||
loginTime: null,
|
loginTime: null,
|
||||||
balance: null,
|
balance: null,
|
||||||
superiorName: null
|
superiorName: null,
|
||||||
|
deptId: null
|
||||||
},
|
},
|
||||||
// 表单参数
|
// 表单参数
|
||||||
form: {},
|
form: {},
|
||||||
// 表单校验
|
// 表单校验
|
||||||
rules: {
|
rules: {
|
||||||
delFlag: [
|
username: [{ required: true, message: "用户账号不能为空", trigger: "blur" }],
|
||||||
{ required: true, message: "删除标志不能为空", trigger: "blur" }
|
password: [
|
||||||
],
|
{
|
||||||
phone: [
|
validator: (rule, value, callback) => {
|
||||||
{ required: true, message: "手机号码不能为空", trigger: "blur" }
|
if (this.form.id == null && (!value || !String(value).trim())) {
|
||||||
|
callback(new Error("密码不能为空"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: "blur"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
this.loadDeptTree();
|
||||||
this.getList();
|
this.getList();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
loadDeptTree() {
|
||||||
|
listDept().then(res => {
|
||||||
|
this.deptOptions = this.handleTree(res.data, "deptId");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deptNormalizer(node) {
|
||||||
|
if (node.children && !node.children.length) {
|
||||||
|
delete node.children;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: node.deptId,
|
||||||
|
label: node.deptName,
|
||||||
|
children: node.children
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleOpenAssignDept(row) {
|
||||||
|
getUser(row.id).then(res => {
|
||||||
|
this.assignForm = {
|
||||||
|
id: res.data.id,
|
||||||
|
deptId: res.data.deptId
|
||||||
|
};
|
||||||
|
this.assignDeptOpen = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
submitAssignDept() {
|
||||||
|
const payload = {
|
||||||
|
id: this.assignForm.id,
|
||||||
|
deptId: this.assignForm.deptId != null ? this.assignForm.deptId : null
|
||||||
|
};
|
||||||
|
assignAiUserDept(payload).then(() => {
|
||||||
|
this.$modal.msgSuccess("已保存");
|
||||||
|
this.assignDeptOpen = false;
|
||||||
|
this.getList();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cancelAssignDept() {
|
||||||
|
this.assignDeptOpen = false;
|
||||||
|
this.assignForm = { id: null, deptId: null };
|
||||||
|
},
|
||||||
// 更多操作触发
|
// 更多操作触发
|
||||||
handleCommand(command, row) {
|
handleCommand(command, row) {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
|
|
@ -353,6 +493,7 @@ export default {
|
||||||
updateBy: null,
|
updateBy: null,
|
||||||
updateTime: null,
|
updateTime: null,
|
||||||
remark: null,
|
remark: null,
|
||||||
|
username: null,
|
||||||
nickname: null,
|
nickname: null,
|
||||||
gender: null,
|
gender: null,
|
||||||
avatar: null,
|
avatar: null,
|
||||||
|
|
@ -407,13 +548,17 @@ export default {
|
||||||
this.$refs["form"].validate(valid => {
|
this.$refs["form"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (this.form.id != null) {
|
if (this.form.id != null) {
|
||||||
updateUser(this.form).then(response => {
|
const payload = { ...this.form };
|
||||||
|
if (!payload.password || !String(payload.password).trim()) {
|
||||||
|
delete payload.password;
|
||||||
|
}
|
||||||
|
updateUser(payload).then(() => {
|
||||||
this.$modal.msgSuccess("修改成功");
|
this.$modal.msgSuccess("修改成功");
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.getList();
|
this.getList();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
addUser(this.form).then(response => {
|
addUser(this.form).then(() => {
|
||||||
this.$modal.msgSuccess("新增成功");
|
this.$modal.msgSuccess("新增成功");
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.getList();
|
this.getList();
|
||||||
|
|
@ -434,11 +579,16 @@ export default {
|
||||||
},
|
},
|
||||||
/** 删除按钮操作 */
|
/** 删除按钮操作 */
|
||||||
handleDelete(row) {
|
handleDelete(row) {
|
||||||
const ids = row.id || this.ids;
|
const ids = row && row.id != null ? row.id : this.ids;
|
||||||
|
if (ids == null || (Array.isArray(ids) && !ids.length)) {
|
||||||
|
this.$modal.msgWarning("请选择要删除的数据");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const idParam = Array.isArray(ids) ? ids.join(",") : ids;
|
||||||
this.$modal
|
this.$modal
|
||||||
.confirm('是否确认删除ai-用户信息编号为"' + ids + '"的数据项?')
|
.confirm('是否确认删除ai-用户信息编号为"' + idParam + '"的数据项?')
|
||||||
.then(function() {
|
.then(() => {
|
||||||
return delUser(ids);
|
return delUser(idParam);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.getList();
|
this.getList();
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 添加或修改部门对话框 -->
|
<!-- 添加或修改部门对话框 -->
|
||||||
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-col :span="24" v-if="form.parentId !== 0">
|
<el-col :span="24" v-if="form.parentId !== 0">
|
||||||
|
|
@ -148,6 +148,67 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="Byte API Key">
|
||||||
|
<el-input
|
||||||
|
v-model="form.byteApiKey"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="火山配置项目">
|
||||||
|
<el-input
|
||||||
|
v-model="form.project"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="选填"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row v-if="isSecondLevelCompanyForm">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="视频模型">
|
||||||
|
<div class="model-parm-block">
|
||||||
|
<div
|
||||||
|
v-for="(row, idx) in modelParamRows"
|
||||||
|
:key="'mp-' + idx"
|
||||||
|
class="model-parm-row"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="row.label"
|
||||||
|
placeholder="显示名称(如 Seedance 2.0)"
|
||||||
|
class="model-parm-input-label"
|
||||||
|
/>
|
||||||
|
<el-input
|
||||||
|
v-model="row.value"
|
||||||
|
placeholder="Endpoint / 模型 ID(ep-…)"
|
||||||
|
class="model-parm-input-value"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-delete"
|
||||||
|
:disabled="modelParamRows.length <= 1"
|
||||||
|
@click="removeModelParamRow(idx)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="text" icon="el-icon-plus" @click="addModelParamRow">添加模型</el-button>
|
||||||
|
<p class="model-parm-hint">
|
||||||
|
保存为 JSON 写入 model_parm;门户「视频生成」按用户所属二级部门读取。
|
||||||
|
留空则使用 portal.video.models。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||||
|
|
@ -157,6 +218,31 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.model-parm-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.model-parm-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-label {
|
||||||
|
width: 38%;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-input-value {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.model-parm-hint {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
|
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
|
||||||
import Treeselect from "@riophae/vue-treeselect"
|
import Treeselect from "@riophae/vue-treeselect"
|
||||||
|
|
@ -191,6 +277,7 @@ export default {
|
||||||
},
|
},
|
||||||
// 表单参数
|
// 表单参数
|
||||||
form: {},
|
form: {},
|
||||||
|
modelParamRows: [{ label: '', value: '' }],
|
||||||
// 表单校验
|
// 表单校验
|
||||||
rules: {
|
rules: {
|
||||||
parentId: [
|
parentId: [
|
||||||
|
|
@ -222,7 +309,54 @@ export default {
|
||||||
created() {
|
created() {
|
||||||
this.getList()
|
this.getList()
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
/** 二级公司:ancestors 为 0,100(即上级为根公司 dept_id=100) */
|
||||||
|
isSecondLevelCompanyForm() {
|
||||||
|
if (this.form.ancestors === "0,100") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const pid = this.form.parentId
|
||||||
|
if (pid !== undefined && pid !== null && Number(pid) === 100) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
syncModelRowsFromForm() {
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
const raw = this.form.modelParm
|
||||||
|
if (!raw || String(raw).trim() === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const arr = JSON.parse(raw)
|
||||||
|
if (Array.isArray(arr) && arr.length) {
|
||||||
|
this.modelParamRows = arr.map(x => ({
|
||||||
|
label: (x && x.label) ? String(x.label) : '',
|
||||||
|
value: (x && x.value) ? String(x.value) : ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildModelParmPayload() {
|
||||||
|
if (!this.isSecondLevelCompanyForm) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = (this.modelParamRows || []).filter(r => r.label && r.value)
|
||||||
|
this.form.modelParm = rows.length ? JSON.stringify(rows) : ''
|
||||||
|
},
|
||||||
|
addModelParamRow() {
|
||||||
|
this.modelParamRows.push({ label: '', value: '' })
|
||||||
|
},
|
||||||
|
removeModelParamRow(idx) {
|
||||||
|
if (this.modelParamRows.length <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.modelParamRows.splice(idx, 1)
|
||||||
|
},
|
||||||
/** 查询部门列表 */
|
/** 查询部门列表 */
|
||||||
getList() {
|
getList() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
|
|
@ -252,13 +386,18 @@ export default {
|
||||||
this.form = {
|
this.form = {
|
||||||
deptId: undefined,
|
deptId: undefined,
|
||||||
parentId: undefined,
|
parentId: undefined,
|
||||||
|
ancestors: undefined,
|
||||||
deptName: undefined,
|
deptName: undefined,
|
||||||
orderNum: undefined,
|
orderNum: undefined,
|
||||||
leader: undefined,
|
leader: undefined,
|
||||||
phone: undefined,
|
phone: undefined,
|
||||||
email: undefined,
|
email: undefined,
|
||||||
|
byteApiKey: undefined,
|
||||||
|
project: undefined,
|
||||||
|
modelParm: undefined,
|
||||||
status: "0"
|
status: "0"
|
||||||
}
|
}
|
||||||
|
this.modelParamRows = [{ label: '', value: '' }]
|
||||||
this.resetForm("form")
|
this.resetForm("form")
|
||||||
},
|
},
|
||||||
/** 搜索按钮操作 */
|
/** 搜索按钮操作 */
|
||||||
|
|
@ -295,6 +434,7 @@ export default {
|
||||||
this.reset()
|
this.reset()
|
||||||
getDept(row.deptId).then(response => {
|
getDept(row.deptId).then(response => {
|
||||||
this.form = response.data
|
this.form = response.data
|
||||||
|
this.syncModelRowsFromForm()
|
||||||
this.open = true
|
this.open = true
|
||||||
this.title = "修改部门"
|
this.title = "修改部门"
|
||||||
listDeptExcludeChild(row.deptId).then(response => {
|
listDeptExcludeChild(row.deptId).then(response => {
|
||||||
|
|
@ -310,6 +450,7 @@ export default {
|
||||||
submitForm: function() {
|
submitForm: function() {
|
||||||
this.$refs["form"].validate(valid => {
|
this.$refs["form"].validate(valid => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
|
this.buildModelParmPayload()
|
||||||
if (this.form.deptId != undefined) {
|
if (this.form.deptId != undefined) {
|
||||||
updateDept(this.form).then(response => {
|
updateDept(this.form).then(response => {
|
||||||
this.$modal.msgSuccess("修改成功")
|
this.$modal.msgSuccess("修改成功")
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ VITE_AMAP_KEY = 6888d721d30981b479ecdabd85f286fe
|
||||||
VITE_AMAP_SAFE_KEY = 23d3fdbc6b4eaf65d4a82a16510938c9
|
VITE_AMAP_SAFE_KEY = 23d3fdbc6b4eaf65d4a82a16510938c9
|
||||||
VITE_REGISTER_SOURCE = ALL
|
VITE_REGISTER_SOURCE = ALL
|
||||||
#VITE_REGISTER_SOURCE = XM001
|
#VITE_REGISTER_SOURCE = XM001
|
||||||
VITE_SHOW_PAY = SUCCESS
|
VITE_SHOW_PAY = SUCCESS
|
||||||
|
VITE_API_URL = http://47.86.170.114:8011/api
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# 端口
|
# 端口
|
||||||
VITE_PORT = 8887
|
VITE_PORT = 8887
|
||||||
VITE_API_URL = https://api.undressing.name/api
|
VITE_API_URL = http://47.86.170.114:8011/api
|
||||||
|
|
@ -10,7 +10,7 @@ yarn-error.log*
|
||||||
/test/e2e/reports/
|
/test/e2e/reports/
|
||||||
selenium-debug.log
|
selenium-debug.log
|
||||||
nodejs/public/uploads
|
nodejs/public/uploads
|
||||||
|
*.zip
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -615,4 +615,4 @@
|
||||||
right: 15px
|
right: 15px
|
||||||
}
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=quill.core.css.map*/
|
/* Quill core styles - source map removed to fix build error */
|
||||||
|
|
@ -1286,4 +1286,4 @@
|
||||||
border: 1px solid #ccc
|
border: 1px solid #ccc
|
||||||
}
|
}
|
||||||
|
|
||||||
/*# sourceMappingURL=quill.snow.css.map*/
|
/* Quill snow styles - source map removed to fix build error */
|
||||||
|
|
@ -8,10 +8,6 @@
|
||||||
fullscreen
|
fullscreen
|
||||||
:class="`${prefixCls}-wrapper`"
|
:class="`${prefixCls}-wrapper`"
|
||||||
:modal-class="`${prefixCls}-dialog`">
|
:modal-class="`${prefixCls}-dialog`">
|
||||||
<mf-video
|
|
||||||
autoplay
|
|
||||||
:controls="false"
|
|
||||||
modelValue="https://images.iqyjsnwv.com/tmp/hello-BCSGJ8fP.mp4" />
|
|
||||||
<div :class="`${prefixCls}-shadow`">
|
<div :class="`${prefixCls}-shadow`">
|
||||||
<div :class="`${prefixCls}-title`">
|
<div :class="`${prefixCls}-title`">
|
||||||
{{ $t('common.fbTitle') }}
|
{{ $t('common.fbTitle') }}
|
||||||
|
|
@ -38,6 +34,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import i18n from '@/lang/i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'mf-forbidden',
|
name: 'mf-forbidden',
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -53,7 +51,13 @@ export default {
|
||||||
this.$router.replace('/403')
|
this.$router.replace('/403')
|
||||||
},
|
},
|
||||||
ok() {
|
ok() {
|
||||||
this.$store.dispatch('main/setForbidden', false)
|
// 满18+:关闭弹窗并进入首页
|
||||||
|
Promise.resolve(this.$store.dispatch('main/setForbidden', false)).finally(() => {
|
||||||
|
// 默认语言:繁体中文(zh_HK)
|
||||||
|
this.$store.dispatch('main/setLanguage', 'zh_HK')
|
||||||
|
i18n.global.locale = 'zh_HK'
|
||||||
|
this.$router.push({ name: 'index' })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,460 @@
|
||||||
|
<template>
|
||||||
|
<div class="rich-editor-root">
|
||||||
|
<!-- 工具栏 -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="tool-btn" type="button" @click="execCommand('bold')"><b>B</b></button>
|
||||||
|
<button class="tool-btn" type="button" @click="execCommand('italic')"><i>I</i></button>
|
||||||
|
<button class="tool-btn" type="button" @click="openImagePicker">{{ $t('common.insertImage') || '插入图片' }}</button>
|
||||||
|
<button class="tool-btn" type="button" @click="clear">清空</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 隐藏的文件输入 -->
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
@change="handleSelectFiles"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 编辑器区域 -->
|
||||||
|
<div
|
||||||
|
ref="editorRef"
|
||||||
|
class="user-input rich-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
:data-placeholder="placeholder || '请输入文本...'"
|
||||||
|
@input="handleInput"
|
||||||
|
@keyup="handleKeyup"
|
||||||
|
@click="handleEditorClick"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
@paste="handlePaste"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- @ 图片选择面板 -->
|
||||||
|
<div v-if="mentionVisible" class="mention-panel">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in mentionImageList"
|
||||||
|
:key="item.url"
|
||||||
|
class="mention-item"
|
||||||
|
@mousedown.prevent="insertMentionImage(item)"
|
||||||
|
>
|
||||||
|
<img :src="item.url" class="mention-thumb" />
|
||||||
|
<span class="mention-label">@图{{ idx + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="mentionImageList.length === 0" class="mention-empty">
|
||||||
|
{{ $t('common.noImageToMention') || '暂无可引用图片' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
||||||
|
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RichTextEditor',
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请输入文本生成视频...'
|
||||||
|
},
|
||||||
|
uploadedImages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue', 'text-change', 'image-upload'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const fileInputRef = ref(null)
|
||||||
|
const savedSelectionRange = ref(null)
|
||||||
|
const mentionVisible = ref(false)
|
||||||
|
const mentionImageList = ref([])
|
||||||
|
const MAX_IMAGE_COUNT = 4
|
||||||
|
|
||||||
|
// 执行编辑器命令
|
||||||
|
const execCommand = (command) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
document.execCommand(command, false, null)
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开图片选择器
|
||||||
|
const openImagePicker = () => {
|
||||||
|
if (fileInputRef.value) {
|
||||||
|
fileInputRef.value.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件选择 - 上传到服务器
|
||||||
|
const handleSelectFiles = async (event) => {
|
||||||
|
const files = event.target.files
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
for (let file of files) {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
// 显示上传中提示
|
||||||
|
const loading = window.$message ? window.$message.loading('上传中...') : null
|
||||||
|
|
||||||
|
// 上传到后端
|
||||||
|
const res = await uploadFile({
|
||||||
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
|
file: file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageUrl = extractUploadUrlFromResponse(res)
|
||||||
|
if (res && (Number(res.code) === 200 || res.code === 200) && imageUrl) {
|
||||||
|
insertImage(imageUrl, file.name)
|
||||||
|
|
||||||
|
// 通知父组件有新图片上传成功(用于@功能)
|
||||||
|
emit('image-upload', { url: imageUrl, name: file.name })
|
||||||
|
} else {
|
||||||
|
console.error('上传失败:', res)
|
||||||
|
alert('图片上传失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传图片出错:', error)
|
||||||
|
alert('图片上传失败: ' + (error.message || '未知错误'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空input
|
||||||
|
event.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入图片
|
||||||
|
const insertImage = (url, name = 'image') => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = url
|
||||||
|
img.alt = name
|
||||||
|
img.style.maxWidth = '100%'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.style.margin = '4px 0'
|
||||||
|
img.setAttribute('data-image-name', name)
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(img)
|
||||||
|
|
||||||
|
// 添加空格
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(img)
|
||||||
|
range.setEndAfter(img)
|
||||||
|
range.insertNode(space)
|
||||||
|
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输入
|
||||||
|
const handleInput = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const content = editorRef.value.innerHTML
|
||||||
|
emit('update:modelValue', content)
|
||||||
|
emit('text-change', content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理按键
|
||||||
|
const handleKeyup = (e) => {
|
||||||
|
if (e.key === '@') {
|
||||||
|
showMentionPanel()
|
||||||
|
} else if (e.key === 'Escape' && mentionVisible.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (e) => {
|
||||||
|
if (e.key === '@') {
|
||||||
|
// 保存当前选择位置
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection && selection.rangeCount > 0) {
|
||||||
|
savedSelectionRange.value = selection.getRangeAt(0).cloneRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPlainTextAtCursor = (text) => {
|
||||||
|
if (!editorRef.value || text == null) return
|
||||||
|
const normalized = String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
let range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value.contains(range.commonAncestorContainer)) {
|
||||||
|
const r = document.createRange()
|
||||||
|
r.selectNodeContents(editorRef.value)
|
||||||
|
r.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(r)
|
||||||
|
range = selection.getRangeAt(0)
|
||||||
|
}
|
||||||
|
range.deleteContents()
|
||||||
|
const lines = normalized.split('\n')
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line) fragment.appendChild(document.createTextNode(line))
|
||||||
|
if (index < lines.length - 1) fragment.appendChild(document.createElement('br'))
|
||||||
|
})
|
||||||
|
range.insertNode(fragment)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = (e) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const cd = e.clipboardData
|
||||||
|
if (!cd) return
|
||||||
|
const items = cd.items
|
||||||
|
if (items) {
|
||||||
|
for (let i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.indexOf('image') !== -1) {
|
||||||
|
const blob = items[i].getAsFile()
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
insertImage(event.target.result, 'pasted-image')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(blob)
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = cd.getData('text/plain') || ''
|
||||||
|
if (!text && cd.getData('text/html')) {
|
||||||
|
const tmp = document.createElement('div')
|
||||||
|
tmp.innerHTML = cd.getData('text/html')
|
||||||
|
text = tmp.innerText || ''
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
insertPlainTextAtCursor(text)
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示 @ 面板
|
||||||
|
const showMentionPanel = () => {
|
||||||
|
if (props.uploadedImages && props.uploadedImages.length > 0) {
|
||||||
|
mentionImageList.value = props.uploadedImages.slice(0, MAX_IMAGE_COUNT)
|
||||||
|
mentionVisible.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入 @ 图片
|
||||||
|
const insertMentionImage = (imageItem) => {
|
||||||
|
if (!editorRef.value || !savedSelectionRange.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
editorRef.value.focus()
|
||||||
|
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection) {
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(savedSelectionRange.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = imageItem.url
|
||||||
|
img.alt = '@图片'
|
||||||
|
img.style.maxWidth = '60px'
|
||||||
|
img.style.height = 'auto'
|
||||||
|
img.style.verticalAlign = 'middle'
|
||||||
|
img.setAttribute('data-mention', 'true')
|
||||||
|
|
||||||
|
const range = savedSelectionRange.value
|
||||||
|
range.deleteContents()
|
||||||
|
range.insertNode(img)
|
||||||
|
|
||||||
|
// 添加空格
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(img)
|
||||||
|
range.setEndAfter(img)
|
||||||
|
range.insertNode(space)
|
||||||
|
|
||||||
|
mentionVisible.value = false
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空编辑器
|
||||||
|
const clear = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.innerHTML = ''
|
||||||
|
handleInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑器点击
|
||||||
|
const handleEditorClick = () => {
|
||||||
|
if (mentionVisible.value) {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 props 变化
|
||||||
|
watch(() => props.modelValue, (newValue) => {
|
||||||
|
if (editorRef.value && editorRef.value.innerHTML !== newValue) {
|
||||||
|
editorRef.value.innerHTML = newValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听上传的图片列表更新
|
||||||
|
watch(() => props.uploadedImages, (newImages) => {
|
||||||
|
mentionImageList.value = newImages || []
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = props.modelValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
editorRef,
|
||||||
|
fileInputRef,
|
||||||
|
mentionVisible,
|
||||||
|
mentionImageList,
|
||||||
|
execCommand,
|
||||||
|
openImagePicker,
|
||||||
|
handleSelectFiles,
|
||||||
|
handleInput,
|
||||||
|
handleKeyup,
|
||||||
|
handleKeydown,
|
||||||
|
handlePaste,
|
||||||
|
handleEditorClick,
|
||||||
|
insertMentionImage,
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.rich-editor-root {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 120px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* 覆盖粘贴残留的内联颜色,保证深底上始终可读 */
|
||||||
|
:deep(*) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:empty:before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #666;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 4px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
background: #1f2128;
|
||||||
|
border: 1px solid #3a3d47;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
width: 280px;
|
||||||
|
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2a2d38;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-thumb {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-label {
|
||||||
|
color: #a1a4b3;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mention-empty {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,600 @@
|
||||||
|
<template>
|
||||||
|
<div class="video-editor-root">
|
||||||
|
<div v-if="showToolbar" class="toolbar">
|
||||||
|
<button class="tool-btn" type="button" @click="openImagePicker">插入参考素材</button>
|
||||||
|
<button class="tool-btn" type="button" @click="clear">清空所有参考素材和文字</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="fileInputRef"
|
||||||
|
class="hidden-input"
|
||||||
|
type="file"
|
||||||
|
accept="image/*,video/*,audio/*"
|
||||||
|
multiple
|
||||||
|
@change="handleSelectFiles" />
|
||||||
|
<div class="editor-wrapper">
|
||||||
|
<div
|
||||||
|
ref="editorRef"
|
||||||
|
class="user-input video-rich-editor"
|
||||||
|
contenteditable="true"
|
||||||
|
:data-placeholder="placeholder || '请输入文本生成视频...'"
|
||||||
|
@input="handleInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@keyup="handleKeyup"
|
||||||
|
@click="handleEditorClick"></div>
|
||||||
|
<div v-if="mentionVisible" class="mention-panel">
|
||||||
|
<div
|
||||||
|
v-for="item in mentionImageList"
|
||||||
|
:key="item.url"
|
||||||
|
class="mention-item"
|
||||||
|
@mousedown.prevent="insertMentionImage(item)">
|
||||||
|
<img v-if="item.mediaType === 'image'" :src="item.url" class="mention-thumb" alt="" />
|
||||||
|
<span v-else class="mention-type-badge">{{ item.mediaType === 'video' ? '视频' : '音频' }}</span>
|
||||||
|
<span class="mention-label">参考{{ item.refNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="mentionImageList.length === 0" class="mention-empty">暂无可引用素材</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
/** 文生视频等场景可隐藏工具栏,仅保留纯文本编辑 */
|
||||||
|
showToolbar: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['text-change'])
|
||||||
|
|
||||||
|
const editorRef = ref(null)
|
||||||
|
const fileInputRef = ref(null)
|
||||||
|
const savedSelectionRange = ref(null)
|
||||||
|
const MAX_IMAGE_COUNT = 9
|
||||||
|
const mentionVisible = ref(false)
|
||||||
|
const mentionImageList = ref([])
|
||||||
|
|
||||||
|
const detectMediaType = (file) => {
|
||||||
|
const mime = (file?.type || '').toLowerCase()
|
||||||
|
if (mime.startsWith('image/')) return 'image'
|
||||||
|
if (mime.startsWith('video/')) return 'video'
|
||||||
|
if (mime.startsWith('audio/')) return 'audio'
|
||||||
|
const lowerName = (file?.name || '').toLowerCase()
|
||||||
|
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(lowerName)) return 'image'
|
||||||
|
if (/\.(mp4|mov|webm|m4v|avi|mkv)$/.test(lowerName)) return 'video'
|
||||||
|
if (/\.(mp3|wav|aac|m4a|ogg|flac)$/.test(lowerName)) return 'audio'
|
||||||
|
return 'image'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPlainText = () => (editorRef.value?.innerText || '').trim()
|
||||||
|
|
||||||
|
const focusEditorToEnd = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection) return
|
||||||
|
const range = document.createRange()
|
||||||
|
range.selectNodeContents(editorRef.value)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
savedSelectionRange.value = range.cloneRange()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSelection = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (editorRef.value?.contains(range.commonAncestorContainer)) {
|
||||||
|
savedSelectionRange.value = range.cloneRange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreSelection = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || !editorRef.value) return
|
||||||
|
selection.removeAllRanges()
|
||||||
|
if (savedSelectionRange.value) {
|
||||||
|
selection.addRange(savedSelectionRange.value)
|
||||||
|
} else {
|
||||||
|
const range = document.createRange()
|
||||||
|
range.selectNodeContents(editorRef.value)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.addRange(range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInput = () => {
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
saveSelection()
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertPlainTextAtCursor = (text) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
const normalizedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||||||
|
editorRef.value.focus()
|
||||||
|
restoreSelection()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
let range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value.contains(range.commonAncestorContainer)) {
|
||||||
|
focusEditorToEnd()
|
||||||
|
const currentSelection = window.getSelection()
|
||||||
|
if (!currentSelection || currentSelection.rangeCount === 0) return
|
||||||
|
range = currentSelection.getRangeAt(0)
|
||||||
|
}
|
||||||
|
range.deleteContents()
|
||||||
|
const lines = normalizedText.split('\n')
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
if (line) fragment.appendChild(document.createTextNode(line))
|
||||||
|
if (index < lines.length - 1) fragment.appendChild(document.createElement('br'))
|
||||||
|
})
|
||||||
|
range.insertNode(fragment)
|
||||||
|
range.collapse(false)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
saveSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaste = (event) => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
event.preventDefault()
|
||||||
|
const clipboardText = event.clipboardData?.getData('text/plain') || ''
|
||||||
|
if (!clipboardText) return
|
||||||
|
insertPlainTextAtCursor(clipboardText)
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMentionImageList = () => {
|
||||||
|
mentionImageList.value = mentionImageList.value.slice(0, MAX_IMAGE_COUNT)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideMentionPanel = () => {
|
||||||
|
mentionVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActiveMentionTriggerBeforeCursor = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return false
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!range.collapsed) return false
|
||||||
|
if (!editorRef.value?.contains(range.commonAncestorContainer)) return false
|
||||||
|
const container = range.startContainer
|
||||||
|
if (container.nodeType !== Node.TEXT_NODE) return false
|
||||||
|
const before = container.data.slice(0, range.startOffset)
|
||||||
|
const atIndex = before.lastIndexOf('@')
|
||||||
|
if (atIndex < 0) return false
|
||||||
|
const afterAt = before.slice(atIndex + 1)
|
||||||
|
return !/\s/.test(afterAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const syncMentionPanelVisibility = () => {
|
||||||
|
const shouldShow = hasActiveMentionTriggerBeforeCursor()
|
||||||
|
if (!shouldShow) {
|
||||||
|
hideMentionPanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateMentionImageList()
|
||||||
|
mentionVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeMentionKeywordBeforeCursor = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!range.collapsed) return
|
||||||
|
const container = range.startContainer
|
||||||
|
if (container.nodeType !== Node.TEXT_NODE) return
|
||||||
|
const before = container.data.slice(0, range.startOffset)
|
||||||
|
const atIndex = before.lastIndexOf('@')
|
||||||
|
if (atIndex < 0) return
|
||||||
|
const afterAt = before.slice(atIndex + 1)
|
||||||
|
if (/\s/.test(afterAt)) return
|
||||||
|
const removeRange = document.createRange()
|
||||||
|
removeRange.setStart(container, atIndex)
|
||||||
|
removeRange.setEnd(container, range.startOffset)
|
||||||
|
removeRange.deleteContents()
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertReference = (item) => {
|
||||||
|
if (!item?.url || !editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
restoreSelection()
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) return
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value.contains(range.commonAncestorContainer)) return
|
||||||
|
|
||||||
|
const refNoText = `[图${item.refNo}]`
|
||||||
|
let node
|
||||||
|
if (item.mediaType === 'image') {
|
||||||
|
const img = document.createElement('img')
|
||||||
|
img.src = item.url
|
||||||
|
img.alt = `reference-${item.refNo}`
|
||||||
|
img.className = 'inline-rich-image'
|
||||||
|
img.setAttribute('data-image-url', item.url)
|
||||||
|
img.setAttribute('data-mention-reference', '1')
|
||||||
|
img.setAttribute('data-reference-no', String(item.refNo))
|
||||||
|
node = img
|
||||||
|
} else {
|
||||||
|
const tag = document.createElement('span')
|
||||||
|
tag.className = 'inline-rich-reference'
|
||||||
|
tag.textContent = refNoText
|
||||||
|
tag.setAttribute('data-reference-url', item.url)
|
||||||
|
tag.setAttribute('data-mention-reference', '1')
|
||||||
|
tag.setAttribute('data-reference-no', String(item.refNo))
|
||||||
|
tag.setAttribute('data-reference-type', item.mediaType)
|
||||||
|
node = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
range.insertNode(node)
|
||||||
|
const space = document.createTextNode(' ')
|
||||||
|
range.setStartAfter(node)
|
||||||
|
range.insertNode(space)
|
||||||
|
range.setStartAfter(space)
|
||||||
|
range.collapse(true)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
saveSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertMentionImage = (item) => {
|
||||||
|
if (!item?.url || !editorRef.value) return
|
||||||
|
editorRef.value.focus()
|
||||||
|
restoreSelection()
|
||||||
|
removeMentionKeywordBeforeCursor()
|
||||||
|
// 删除 @关键字 后必须重新记录选区,否则 insertReference 里 restoreSelection 仍用删除前的 Range,会插错位置甚至覆盖后文
|
||||||
|
saveSelection()
|
||||||
|
insertReference(item)
|
||||||
|
hideMentionPanel()
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyup = (event) => {
|
||||||
|
saveSelection()
|
||||||
|
if (event.key === '@') {
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
hideMentionPanel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEditorClick = () => {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (!selection || selection.rangeCount === 0) {
|
||||||
|
focusEditorToEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const range = selection.getRangeAt(0)
|
||||||
|
if (!editorRef.value?.contains(range.commonAncestorContainer)) {
|
||||||
|
focusEditorToEnd()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveSelection()
|
||||||
|
syncMentionPanelVisibility()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInsertedUniqueImageCount = () => mentionImageList.value.length
|
||||||
|
|
||||||
|
const openImagePicker = () => {
|
||||||
|
const current = getInsertedUniqueImageCount()
|
||||||
|
if (current >= MAX_IMAGE_COUNT) {
|
||||||
|
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileInputRef.value?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadToCos = async (file) => {
|
||||||
|
const res = await uploadFile({
|
||||||
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
|
file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
const codeOk = res && (Number(res.code) === 200 || res.code === 200)
|
||||||
|
const url = extractUploadUrlFromResponse(res)
|
||||||
|
if (codeOk && url) return url
|
||||||
|
throw new Error(res?.msg || '上传失败:未返回文件地址')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectFiles = async (event) => {
|
||||||
|
const input = event.target
|
||||||
|
const files = Array.from(input.files || [])
|
||||||
|
if (!files.length) return
|
||||||
|
const remain = MAX_IMAGE_COUNT - getInsertedUniqueImageCount()
|
||||||
|
if (remain <= 0) {
|
||||||
|
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
|
||||||
|
input.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const selected = files.slice(0, remain)
|
||||||
|
if (files.length > remain) {
|
||||||
|
Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材,本次仅插入 ${remain} 个`)
|
||||||
|
}
|
||||||
|
for (const file of selected) {
|
||||||
|
const mediaType = detectMediaType(file)
|
||||||
|
try {
|
||||||
|
const url = await uploadToCos(file)
|
||||||
|
if (url && !mentionImageList.value.some((item) => item.url === url)) {
|
||||||
|
const refNo = mentionImageList.value.length + 1
|
||||||
|
mentionImageList.value.push({ url, refNo, mediaType })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Message.error('参考素材上传失败')
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.value = ''
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
}
|
||||||
|
|
||||||
|
const getContentItems = () => {
|
||||||
|
const editor = editorRef.value
|
||||||
|
const items = []
|
||||||
|
if (!editor) return items
|
||||||
|
let textBuffer = ''
|
||||||
|
const flushText = () => {
|
||||||
|
const normalized = textBuffer.replace(/\u00a0/g, ' ').replace(/\s+\n/g, '\n')
|
||||||
|
if (normalized.trim()) items.push({ type: 'text', text: normalized.trim() })
|
||||||
|
textBuffer = ''
|
||||||
|
}
|
||||||
|
const walk = (node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
textBuffer += node.textContent || ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) return
|
||||||
|
const el = node
|
||||||
|
const referenceUrl = (
|
||||||
|
el.dataset?.referenceUrl ||
|
||||||
|
el.dataset?.imageUrl ||
|
||||||
|
el.getAttribute('src') ||
|
||||||
|
''
|
||||||
|
).trim()
|
||||||
|
if (el.dataset?.mentionReference === '1' && referenceUrl) {
|
||||||
|
flushText()
|
||||||
|
items.push({
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: { url: referenceUrl },
|
||||||
|
role: 'reference_image'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (el.tagName === 'IMG' && (el.dataset?.imageUrl || el.getAttribute('src'))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (el.tagName === 'BR') {
|
||||||
|
textBuffer += '\n'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
|
||||||
|
Array.from(el.childNodes).forEach(walk)
|
||||||
|
if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
|
||||||
|
}
|
||||||
|
Array.from(editor.childNodes).forEach(walk)
|
||||||
|
flushText()
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (!editorRef.value) return
|
||||||
|
editorRef.value.innerHTML = ''
|
||||||
|
emit('text-change', '')
|
||||||
|
savedSelectionRange.value = null
|
||||||
|
mentionImageList.value = []
|
||||||
|
hideMentionPanel()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 供父组件持久化:参考图模式的 HTML + 素材列表 */
|
||||||
|
const getDraftState = () => ({
|
||||||
|
html: editorRef.value ? editorRef.value.innerHTML : '',
|
||||||
|
mentionList: mentionImageList.value.map(({ url, refNo, mediaType }) => ({
|
||||||
|
url,
|
||||||
|
refNo,
|
||||||
|
mediaType
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const applyDraftState = (draft) => {
|
||||||
|
hideMentionPanel()
|
||||||
|
savedSelectionRange.value = null
|
||||||
|
if (!draft || typeof draft !== 'object') {
|
||||||
|
clear()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mentionImageList.value = Array.isArray(draft.mentionList)
|
||||||
|
? draft.mentionList.map((x) => ({
|
||||||
|
url: x.url,
|
||||||
|
refNo: Number(x.refNo) || 0,
|
||||||
|
mediaType: x.mediaType || 'image'
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.innerHTML = typeof draft.html === 'string' ? draft.html : ''
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
emit('text-change', getPlainText())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
getContentItems,
|
||||||
|
getPlainText,
|
||||||
|
clear,
|
||||||
|
getDraftState,
|
||||||
|
applyDraftState
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
focusEditorToEnd()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.video-editor-root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.editor-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
.tool-btn {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.tool-btn:hover {
|
||||||
|
border-color: rgba(0, 202, 224, 0.4);
|
||||||
|
background: rgba(0, 202, 224, 0.08);
|
||||||
|
}
|
||||||
|
.video-rich-editor {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 220px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.9;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-y: auto;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
caret-color: #00cae0;
|
||||||
|
|
||||||
|
:deep(*) {
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.inline-rich-reference) {
|
||||||
|
color: #5eebf5 !important;
|
||||||
|
background: rgba(0, 202, 224, 0.15);
|
||||||
|
}
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.video-rich-editor:focus {
|
||||||
|
border-color: rgba(0, 202, 224, 0.45);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.15);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.video-rich-editor:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.video-rich-editor :deep(.inline-rich-image) {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: min(260px, 100%);
|
||||||
|
max-height: 148px;
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.video-rich-editor :deep(.inline-rich-reference) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 202, 224, 0.15);
|
||||||
|
color: #5eebf5;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.mention-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(22, 24, 30, 0.98);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.mention-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mention-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
.mention-thumb {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.mention-type-badge {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.mention-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
.mention-empty {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
export default {
|
|
||||||
image1: "One-click undressing",
|
|
||||||
image2: 'Image-to-Image 2',
|
|
||||||
uploadImage: 'Upload Image',
|
|
||||||
uploadImageTip: 'Supports PNG/JPG up to 10MB',
|
|
||||||
uploadPlaceholder: 'Click to upload image',
|
|
||||||
selectImageSource: 'Select Image Source',
|
|
||||||
selectTemplate: 'Select Template',
|
|
||||||
reselectTemplate: 'Reselect Template',
|
|
||||||
noTemplates: 'No Templates Available',
|
|
||||||
tag1: 'Tag Type 1',
|
|
||||||
tag2: 'Tag Type 2',
|
|
||||||
tag3: 'Tag Type 3',
|
|
||||||
generateImage: 'Generate Now (costs {score} balance)',
|
|
||||||
generateImageNow: 'Generate Now',
|
|
||||||
generateTip: 'Tip: After submission, you can view it in "My Works"',
|
|
||||||
generateVideo: 'Generate Video',
|
|
||||||
imageFace: 'Image Face Swap',
|
|
||||||
videoFace: 'Video Face Swap',
|
|
||||||
uploadImageFace: 'Click to upload face image',
|
|
||||||
uploadFaceImageError: 'Please upload a face image',
|
|
||||||
uploadTemplateError: 'Please upload a custom template',
|
|
||||||
uploadTemplate: 'Click to upload custom template',
|
|
||||||
textPlaceholder: 'Describe the image you want to generate',
|
|
||||||
uploadImageError: 'Please upload an image',
|
|
||||||
replaceImage: 'Replace image',
|
|
||||||
textError: 'Please enter a prompt',
|
|
||||||
textVideoPlaceholder: "Describe the video you want to generate",
|
|
||||||
uploadFirstPlaceholder: 'Click to upload first frame',
|
|
||||||
uploadLastPlaceholder: 'Click to upload last frame',
|
|
||||||
uploadFirstImageError: 'Please upload the first frame image',
|
|
||||||
uploadWaitImageError: 'Please wait for the image upload to finish',
|
|
||||||
saveVideo: 'Down Video',
|
|
||||||
videoLoadingText: "Video is creating...",
|
|
||||||
viewVideo: 'View Video',
|
|
||||||
changeFacePrompt: 'Extract the face from the second image and replace the face in the first image',
|
|
||||||
rechartTip1: 'When recharging via wallet transfer, please make sure the wallet is on the correct blockchain network!!!',
|
|
||||||
rechartTip2: 'Recharge may be delayed. Please wait 3–5 minutes before refreshing to check.',
|
|
||||||
walletAddr: 'Wallet Address:',
|
|
||||||
fbTitle: 'Warning! This website is for adults only!',
|
|
||||||
fbContent: 'By entering, you confirm you are 18+.',
|
|
||||||
fbCancel: 'Under 18',
|
|
||||||
fbOK: 'I’m 18+',
|
|
||||||
sorry: 'Sorry!',
|
|
||||||
useLess: 'You cannot use this website...',
|
|
||||||
loginAccount: 'Login Account',
|
|
||||||
logout: 'Logout',
|
|
||||||
userEmailPlaceholder: 'Enter email or username',
|
|
||||||
passwordPlaceholder: 'Enter password',
|
|
||||||
forgetPassword: 'Forgot password?',
|
|
||||||
register: "Register",
|
|
||||||
login: "Login",
|
|
||||||
forgotPassword: 'Forgot password',
|
|
||||||
registerAccount: 'Register Account',
|
|
||||||
usernamePlaceholder: 'Enter username',
|
|
||||||
codePlaceholder: 'Enter verification code',
|
|
||||||
confirmPasswordPlaceholder: 'Confirm password',
|
|
||||||
backToLogin: 'Back to Login',
|
|
||||||
send: 'Send',
|
|
||||||
emailValidPlaceholder: 'Enter a valid email address',
|
|
||||||
confirmPwdValidMsg: 'Passwords do not match',
|
|
||||||
editPassword: 'Edit Password',
|
|
||||||
recharge: 'Recharge',
|
|
||||||
myAccount: 'My Account',
|
|
||||||
moneyInvite: 'Reward Invite',
|
|
||||||
rechargeRecord: 'Recharge Record',
|
|
||||||
resumeRecord: 'Consume Record',
|
|
||||||
inviteRecord: 'Invite Record',
|
|
||||||
username: 'Username',
|
|
||||||
userId: 'User ID',
|
|
||||||
accountAmount: 'Account Balance',
|
|
||||||
contact: 'Contact Us',
|
|
||||||
backToUser: 'Back',
|
|
||||||
moneyTips: 'When you invite a friend to register, you will earn {rate} of their recharge amount each time they recharge.',
|
|
||||||
inviteCode: 'Invite Code',
|
|
||||||
inviteLink: 'Invite Link',
|
|
||||||
saveImage: 'Save Image',
|
|
||||||
totalAmount: 'Total Recharge Amount',
|
|
||||||
amount: 'Amount',
|
|
||||||
sendAmount: 'Gift Amount',
|
|
||||||
rechargeType: 'Recharge Method',
|
|
||||||
rechargeTime: 'Recharge Time',
|
|
||||||
emptyText: 'No Data Available',
|
|
||||||
product: 'Product',
|
|
||||||
resumeAmount: 'Amount',
|
|
||||||
productType: 'Product Type',
|
|
||||||
productTime: 'Creation Time',
|
|
||||||
totalReward: 'Total Reward Balance',
|
|
||||||
rewardAmount: 'Reward Amount',
|
|
||||||
rewardTime: 'Reward Time',
|
|
||||||
reSend: 'Resend',
|
|
||||||
registerSuccessfully: 'Registration Successful',
|
|
||||||
loginSuccessfully: 'Login Successful',
|
|
||||||
passwordResetSuccessfully: 'Password Reset Successful',
|
|
||||||
rechargeSuccessfully: 'Recharge Successful',
|
|
||||||
avatar: 'Avatar',
|
|
||||||
email: 'Email',
|
|
||||||
input: 'Please enter',
|
|
||||||
save: 'Save',
|
|
||||||
editEmail: 'Edit Email',
|
|
||||||
editEmailSuccessfully: 'Email successfully updated',
|
|
||||||
updateAvatarSuccessfully: 'Avatar updated successfully',
|
|
||||||
balenceLow: 'Your balance is low, please recharge',
|
|
||||||
confirm: 'Confirm',
|
|
||||||
cancel: 'Cancel',
|
|
||||||
createFailed: 'Creation failed, balance has been refunded',
|
|
||||||
notice: 'Notice',
|
|
||||||
oldPasswordPlaceholder: 'Enter your old password',
|
|
||||||
newpasswordPlaceholder: 'Enter your new password',
|
|
||||||
switchPageTip: 'A video is currently being generated. After exiting, please check your balance consumption record.',
|
|
||||||
loginless: 'Login information has expired! Please log in again.',
|
|
||||||
createVideo: 'Generate Video (Consumes {price} balance)',
|
|
||||||
ok: 'Confirm',
|
|
||||||
rechartNotice: 'Recharge Instructions',
|
|
||||||
rechargeNotice1: '1. Recharge will be credited immediately. If the hash rate doesn’t change, you can refresh and re-enter.',
|
|
||||||
rechargeNotice2: '2. Limited-time packages are irregular promotions. You can stock up on hash rate during the event and recharge multiple times.',
|
|
||||||
rechargeNotice3: '3. The purchased hash rate has no time limit.',
|
|
||||||
rechargeNotice4: 'Before purchasing a package, please carefully review the details. No returns are supported.',
|
|
||||||
rechargeLeft: 'Balance',
|
|
||||||
dollor: 'USD',
|
|
||||||
isDevelop: 'In development, stay tuned!',
|
|
||||||
copySuccessfully: 'Copy successful',
|
|
||||||
copyLink: 'Copy link',
|
|
||||||
goPay: 'Go to Pay',
|
|
||||||
filePreview: 'Preview',
|
|
||||||
doSame: 'Create Similar',
|
|
||||||
saveQRCode: 'Save QR Code',
|
|
||||||
invitationCodePlaceholder: 'Please enter the invitation code',
|
|
||||||
testRecharge: 'Test Recharge',
|
|
||||||
orderNo: "Order Number",
|
|
||||||
orderNoP: "Order number cannot be empty",
|
|
||||||
emailNotExists: 'Email address is not registered',
|
|
||||||
gearNotExists: 'The selected recharge plan does not exist. Please choose another plan.',
|
|
||||||
myProduct: 'My Works',
|
|
||||||
permissionError: "Your total accumulated recharge is {total}, which does not meet the preview requirement.",
|
|
||||||
createTagFailed: "Generation failed. Please refresh and try again.",
|
|
||||||
loadingText: 'Loading...',
|
|
||||||
hasMore: 'Pull up to load more',
|
|
||||||
noMore: 'No more data',
|
|
||||||
giftAmount: 'Credited Amount',
|
|
||||||
cardNo: 'Card Number',
|
|
||||||
cardName: 'Cardholder Name',
|
|
||||||
cardNoRequired: 'Please enter card number',
|
|
||||||
cardNameRequired: 'Please enter cardholder name',
|
|
||||||
rechargeFailed: 'Recharge failed',
|
|
||||||
// VM支付相关
|
|
||||||
vmCardInfo: 'Credit Card Information',
|
|
||||||
cardNumber: 'Card Number',
|
|
||||||
cardNumberPlaceholder: 'Enter card number',
|
|
||||||
cardNumberRequired: 'Please enter card number',
|
|
||||||
cardNumberInvalid: 'Card number must be 13-19 digits',
|
|
||||||
cvc: 'CVC',
|
|
||||||
cvcPlaceholder: 'Enter CVC',
|
|
||||||
cvcRequired: 'Please enter CVC',
|
|
||||||
cvcInvalid: 'CVC must be 3 digits',
|
|
||||||
expYear: 'Expiration Year',
|
|
||||||
expYearPlaceholder: 'Enter expiration year (e.g., 2027)',
|
|
||||||
expYearRequired: 'Please enter expiration year',
|
|
||||||
expYearInvalid: 'Year must be 4 digits',
|
|
||||||
expMonth: 'Expiration Month',
|
|
||||||
expMonthPlaceholder: 'Select expiration month',
|
|
||||||
expMonthRequired: 'Please select expiration month',
|
|
||||||
emailPlaceholder: 'Enter email address',
|
|
||||||
emailRequired: 'Please enter email',
|
|
||||||
emailInvalid: 'Invalid email format',
|
|
||||||
emailMaxLength: 'Email cannot exceed 30 characters',
|
|
||||||
firstName: 'First Name',
|
|
||||||
firstNamePlaceholder: 'Enter first name',
|
|
||||||
firstNameRequired: 'Please enter first name',
|
|
||||||
firstNameMaxLength: 'First name cannot exceed 30 characters',
|
|
||||||
lastName: 'Last Name',
|
|
||||||
lastNamePlaceholder: 'Enter last name',
|
|
||||||
lastNameRequired: 'Please enter last name',
|
|
||||||
lastNameMaxLength: 'Last name cannot exceed 30 characters',
|
|
||||||
country: 'Country',
|
|
||||||
countryPlaceholder: 'Select country',
|
|
||||||
countryRequired: 'Please select country',
|
|
||||||
submit: 'Submit'
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
import route from './route'
|
|
||||||
import common from './common.js'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
route,
|
|
||||||
common
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
export default {
|
|
||||||
index: 'Home',
|
|
||||||
imageToImage: 'One-click undressing',
|
|
||||||
imageToImage2: 'Image-to-Image2',
|
|
||||||
changeFace: 'Swap Face',
|
|
||||||
changeFaceVideo: 'Swap Video Face',
|
|
||||||
fastImage: 'Gen Image',
|
|
||||||
fastVideo: 'Gen Video',
|
|
||||||
recharge: 'Quick Recharge',
|
|
||||||
help: 'Help Center',
|
|
||||||
moneyInvite: 'Reward Invitation'
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +1,19 @@
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
import Cookies from 'js-cookie'
|
|
||||||
import zh_HK from '@/lang/zh_HK/index.js'
|
import zh_HK from '@/lang/zh_HK/index.js'
|
||||||
import en_US from '@/lang/en_US/index.js'
|
|
||||||
import es_ES from '@/lang/es_ES/index.js'
|
|
||||||
import pt_BR from '@/lang/pt_BR/index.js'
|
|
||||||
import hi_IN from '@/lang/hi_IN/index.js'
|
|
||||||
import ru_RU from '@/lang/ru_RU/index.js'
|
|
||||||
import ar_SA from '@/lang/ar_SA/index.js'
|
|
||||||
import fr_FR from '@/lang/fr_FR/index.js'
|
|
||||||
|
|
||||||
let locale = Cookies.get('language') || 'en_US'
|
// 多语言切换已禁用:全站固定繁体中文
|
||||||
|
let locale = 'zh_HK'
|
||||||
|
|
||||||
/** 各语言在界面上的显示名称 */
|
/** 各语言在界面上的显示名称 */
|
||||||
export const LOCALE_NAMES = {
|
export const LOCALE_NAMES = {
|
||||||
zh_HK: '繁体中文',
|
zh_HK: '繁体中文'
|
||||||
en_US: 'English',
|
|
||||||
es_ES: 'Español',
|
|
||||||
pt_BR: 'Português',
|
|
||||||
hi_IN: 'हिन्दी',
|
|
||||||
ru_RU: 'Русский',
|
|
||||||
ar_SA: 'العربية',
|
|
||||||
fr_FR: 'Français'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
globalInjection: true,
|
globalInjection: true,
|
||||||
locale,
|
locale,
|
||||||
messages: {
|
messages: {
|
||||||
zh_HK,
|
zh_HK
|
||||||
en_US,
|
|
||||||
es_ES,
|
|
||||||
pt_BR,
|
|
||||||
hi_IN,
|
|
||||||
ru_RU,
|
|
||||||
ar_SA,
|
|
||||||
fr_FR
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import en_USLocale from './en/index'
|
|
||||||
import zh_HKLocale from './zh_HK/index'
|
import zh_HKLocale from './zh_HK/index'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
en_US: en_USLocale,
|
|
||||||
zh_HK: zh_HKLocale
|
zh_HK: zh_HKLocale
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,14 @@ export default {
|
||||||
cardNoRequired: '請輸入銀行卡號',
|
cardNoRequired: '請輸入銀行卡號',
|
||||||
cardNameRequired: '請輸入銀行卡姓名',
|
cardNameRequired: '請輸入銀行卡姓名',
|
||||||
rechargeFailed: '充值失敗',
|
rechargeFailed: '充值失敗',
|
||||||
|
// 模型选择
|
||||||
|
selectModel: '選擇模型',
|
||||||
|
seedance20: 'Seedance 2.0',
|
||||||
|
seedance20Fast: 'Seedance 2.0 Fast',
|
||||||
|
// 富文本编辑器
|
||||||
|
insertImage: '插入圖片',
|
||||||
|
mentionImage: '引用圖片',
|
||||||
|
noImageToMention: '暫無可引用的圖片',
|
||||||
// VM支付相關
|
// VM支付相關
|
||||||
vmCardInfo: '信用卡信息',
|
vmCardInfo: '信用卡信息',
|
||||||
cardNumber: '信用卡卡號',
|
cardNumber: '信用卡卡號',
|
||||||
|
|
@ -178,6 +186,9 @@ export default {
|
||||||
country: '國家',
|
country: '國家',
|
||||||
countryPlaceholder: '請選擇國家',
|
countryPlaceholder: '請選擇國家',
|
||||||
countryRequired: '請選擇國家',
|
countryRequired: '請選擇國家',
|
||||||
|
videoGen: '視頻生成',
|
||||||
|
uploadFirstImage: '上傳首圖',
|
||||||
|
insertImage: '插入圖片',
|
||||||
submit: '提交',
|
submit: '提交',
|
||||||
cancel: '取消'
|
cancel: '取消'
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,11 @@ export default {
|
||||||
changeFaceVideo: '視頻換臉',
|
changeFaceVideo: '視頻換臉',
|
||||||
fastImage: '快捷生圖',
|
fastImage: '快捷生圖',
|
||||||
fastVideo: '快捷生視頻',
|
fastVideo: '快捷生視頻',
|
||||||
|
videoGen: '視頻生成',
|
||||||
recharge: '快速充值',
|
recharge: '快速充值',
|
||||||
help: '幫助中心',
|
help: '幫助中心',
|
||||||
moneyInvite: '有獎邀請'
|
moneyInvite: '有獎邀請',
|
||||||
|
assetGroupManage: '資源組管理',
|
||||||
|
thirdPartyAsset: '三方素材管理',
|
||||||
|
generatedAssets: '生成作品庫'
|
||||||
}
|
}
|
||||||
|
|
@ -22,15 +22,11 @@
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-menu-item :key="item.key">
|
<a-menu-item :key="item.key">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<a-image
|
<span
|
||||||
:width="24"
|
class="menu-svg-icon"
|
||||||
:preview="false"
|
:class="{ active: selectedKeys.indexOf(item.key) > -1 }"
|
||||||
:height="24"
|
v-html="getMenuSvg(item.key)"
|
||||||
:src="`/images/nav/${
|
></span>
|
||||||
selectedKeys.indexOf(item.key) > -1
|
|
||||||
? item.icon + '-a'
|
|
||||||
: item.icon
|
|
||||||
}.png`" />
|
|
||||||
</template>
|
</template>
|
||||||
<div class="menu-item">
|
<div class="menu-item">
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
|
|
@ -50,6 +46,9 @@ import cloneDeep from 'lodash-es/cloneDeep'
|
||||||
import { constantRoutes } from '@/router/index'
|
import { constantRoutes } from '@/router/index'
|
||||||
import { generateTitle, generateLang } from '@/utils/i18n'
|
import { generateTitle, generateLang } from '@/utils/i18n'
|
||||||
|
|
||||||
|
/** 左侧导航仅显示这些路由(name 与 router/index.js 一致) */
|
||||||
|
const SIDEBAR_ONLY_ROUTE_NAMES = ['video-gen', 'third-party-asset', 'generated-assets']
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
collapsed: Boolean
|
collapsed: Boolean
|
||||||
})
|
})
|
||||||
|
|
@ -62,9 +61,14 @@ const $base = inject('$base')
|
||||||
const $message = inject('$message')
|
const $message = inject('$message')
|
||||||
|
|
||||||
const menuItems = computed(() => {
|
const menuItems = computed(() => {
|
||||||
return constantRoutes
|
const root = constantRoutes.find((d) => d.path === '/')
|
||||||
.find((d) => d.path == '/')
|
const children = root?.children ?? []
|
||||||
.children.map((route) => ({
|
return children
|
||||||
|
.filter(
|
||||||
|
(r) =>
|
||||||
|
r.meta?.menuItem !== false && SIDEBAR_ONLY_ROUTE_NAMES.includes(r.name)
|
||||||
|
)
|
||||||
|
.map((route) => ({
|
||||||
key: route.name,
|
key: route.name,
|
||||||
label: generateTitle(route.meta?.title),
|
label: generateTitle(route.meta?.title),
|
||||||
meta: route.meta,
|
meta: route.meta,
|
||||||
|
|
@ -140,6 +144,19 @@ const count = (system) => {
|
||||||
? servicePolicyCount.value
|
? servicePolicyCount.value
|
||||||
: agencyPolicyCount.value
|
: agencyPolicyCount.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMenuSvg = (key) => {
|
||||||
|
if (key === 'video-gen') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3a9 9 0 1 0 0 18a9 9 0 0 0 0-18Zm-1.5 5.2a.9.9 0 0 1 1.35-.78l4.7 2.7a.9.9 0 0 1 0 1.56l-4.7 2.7a.9.9 0 0 1-1.35-.78V8.2Z"/></svg>'
|
||||||
|
}
|
||||||
|
if (key === 'third-party-asset') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h7l2 2h7v12H4V6Zm2 4v8h12v-8H6Zm2 2h8v4H8v-4Z"/></svg>'
|
||||||
|
}
|
||||||
|
if (key === 'generated-assets') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l-5.5 9h11L12 2zm0 3.5L14.5 9h-5L12 5.5zM4 13v8h16v-8H4zm2 2h12v4H6v-4z"/><path d="M7 15h10v2H7z"/></svg>'
|
||||||
|
}
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>'
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
.arco-tooltip-content {
|
.arco-tooltip-content {
|
||||||
|
|
@ -166,4 +183,25 @@ const count = (system) => {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-svg-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(255, 255, 255, 0.74);
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-svg-icon:deep(svg) {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-svg-icon.active {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,21 @@
|
||||||
:title="$t('common.myAccount')">
|
:title="$t('common.myAccount')">
|
||||||
<UserAccount />
|
<UserAccount />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane
|
<!-- 隐藏有奖邀请 -->
|
||||||
key="money"
|
<!-- <a-tab-pane key="money" :title="$t('common.moneyInvite')"><Money /></a-tab-pane> -->
|
||||||
:title="$t('common.moneyInvite')">
|
<!-- 隐藏快速充值入口 -->
|
||||||
<Money />
|
<!-- <a-tab-pane
|
||||||
</a-tab-pane>
|
|
||||||
<a-tab-pane
|
|
||||||
key="recharge"
|
key="recharge"
|
||||||
:title="$t('common.rechargeRecord')">
|
:title="$t('common.rechargeRecord')">
|
||||||
<RechargeRecord />
|
<RechargeRecord />
|
||||||
</a-tab-pane>
|
</a-tab-pane> -->
|
||||||
<a-tab-pane
|
<a-tab-pane
|
||||||
key="resume"
|
key="resume"
|
||||||
:title="$t('common.myProduct')">
|
:title="$t('common.myProduct')">
|
||||||
<ResumeRecord />
|
<ResumeRecord />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane
|
<!-- 隐藏邀请消费记录 -->
|
||||||
key="invite"
|
<!-- <a-tab-pane key="invite" :title="$t('common.inviteRecord')"><RewardRecord /></a-tab-pane> -->
|
||||||
:title="$t('common.inviteRecord')">
|
|
||||||
<RewardRecord />
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
</mf-dialog>
|
</mf-dialog>
|
||||||
|
|
@ -63,13 +58,8 @@
|
||||||
<script>
|
<script>
|
||||||
import Forgot from './Forgot.vue'
|
import Forgot from './Forgot.vue'
|
||||||
import Register from './Register.vue'
|
import Register from './Register.vue'
|
||||||
import { mapGetters } from 'vuex'
|
|
||||||
import i18n from '@/lang/i18n'
|
|
||||||
import Money from './Money.vue'
|
|
||||||
import UserAccount from './UserAccount.vue'
|
import UserAccount from './UserAccount.vue'
|
||||||
import RechargeRecord from './RechargeRecord.vue'
|
|
||||||
import ResumeRecord from './ResumeRecord.vue'
|
import ResumeRecord from './ResumeRecord.vue'
|
||||||
import RewardRecord from './RewardRecord.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -91,14 +81,8 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Forgot,
|
Forgot,
|
||||||
Register,
|
Register,
|
||||||
Money,
|
|
||||||
UserAccount,
|
UserAccount,
|
||||||
RechargeRecord,
|
ResumeRecord
|
||||||
ResumeRecord,
|
|
||||||
RewardRecord
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
...mapGetters(['lang'])
|
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.register) {
|
if (this.register) {
|
||||||
|
|
@ -124,12 +108,6 @@ export default {
|
||||||
this.registerVisible = true
|
this.registerVisible = true
|
||||||
this.$emit('cancel')
|
this.$emit('cancel')
|
||||||
},
|
},
|
||||||
changeLang(value) {
|
|
||||||
if (value != this.lang) {
|
|
||||||
this.$store.dispatch('main/setLanguage', value)
|
|
||||||
i18n.global.locale = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
back() {
|
back() {
|
||||||
this.forgotVisible = false
|
this.forgotVisible = false
|
||||||
this.$emit('open')
|
this.$emit('open')
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import Breadcrumb from './Breadcrumb.vue'
|
import Breadcrumb from './Breadcrumb.vue'
|
||||||
|
const LAYOUT_MOBILE_MAX = 768
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app-main',
|
name: 'app-main',
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -33,6 +35,21 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Breadcrumb },
|
components: { Breadcrumb },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
layoutWidth:
|
||||||
|
typeof window !== 'undefined' ? window.innerWidth : LAYOUT_MOBILE_MAX + 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this._layoutOnResize = () => {
|
||||||
|
this.layoutWidth = window.innerWidth
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['cachedViews', 'sidebar']),
|
...mapGetters(['cachedViews', 'sidebar']),
|
||||||
// 相同路由,不同参数缓存问题
|
// 相同路由,不同参数缓存问题
|
||||||
|
|
@ -43,7 +60,10 @@ export default {
|
||||||
return !this.sidebar.opened
|
return !this.sidebar.opened
|
||||||
},
|
},
|
||||||
style() {
|
style() {
|
||||||
let sidebarWidth = this.isCollapsed ? '50px' : this.sidebar.width
|
if (this.layoutWidth <= LAYOUT_MOBILE_MAX) {
|
||||||
|
return { width: '100%' }
|
||||||
|
}
|
||||||
|
const sidebarWidth = this.isCollapsed ? '56px' : this.sidebar.width
|
||||||
return {
|
return {
|
||||||
width: `calc(100% - ${sidebarWidth})`
|
width: `calc(100% - ${sidebarWidth})`
|
||||||
}
|
}
|
||||||
|
|
@ -64,13 +84,15 @@ export default {
|
||||||
.app-main {
|
.app-main {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
&-wrap {
|
&-wrap {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 768px) {
|
||||||
.app-main {
|
.app-main {
|
||||||
&-wrap {
|
&-wrap {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="navbar">
|
<div class="navbar">
|
||||||
<div class="left-menu">
|
<div class="left-menu">
|
||||||
<mf-icon
|
<button
|
||||||
:class="['left-collapse', { isCollapse: isCollapse }]"
|
type="button"
|
||||||
cursor="pointer"
|
class="sidebar-trigger"
|
||||||
@click="$store.dispatch('main/toggleSideBar')"
|
:aria-expanded="sidebar.opened"
|
||||||
value="icon-shrink" />
|
aria-label="展开或收起侧栏"
|
||||||
|
@click="$store.dispatch('main/toggleSideBar')">
|
||||||
|
<mf-icon
|
||||||
|
class="sidebar-trigger__icon sidebar-trigger__icon--desktop"
|
||||||
|
:class="{ isCollapse: isCollapse }"
|
||||||
|
cursor="pointer"
|
||||||
|
value="icon-shrink" />
|
||||||
|
<span class="sidebar-trigger__icon sidebar-trigger__icon--mobile" aria-hidden="true">
|
||||||
|
<span class="hamburger-line" />
|
||||||
|
<span class="hamburger-line" />
|
||||||
|
<span class="hamburger-line" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<div
|
<div
|
||||||
class="logo-wrap"
|
class="logo-wrap"
|
||||||
|
|
@ -31,20 +43,9 @@
|
||||||
</mf-button>
|
</mf-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-menu-item language">
|
<div class="right-menu-item language">
|
||||||
<a-dropdown @select="handleSelect">
|
<mf-button type="text" disabled class="language-display">
|
||||||
<mf-button type="text">
|
{{ localeName }}
|
||||||
{{ localeName }}
|
</mf-button>
|
||||||
<icon-down />
|
|
||||||
</mf-button>
|
|
||||||
<template #content>
|
|
||||||
<a-doption
|
|
||||||
v-for="(name, code) in localeNames"
|
|
||||||
:key="code"
|
|
||||||
:value="code">
|
|
||||||
{{ name }}
|
|
||||||
</a-doption>
|
|
||||||
</template>
|
|
||||||
</a-dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="right-menu-item user"
|
class="right-menu-item user"
|
||||||
|
|
@ -97,7 +98,7 @@ import { mapGetters, mapState } from 'vuex'
|
||||||
import cloneDeep from 'lodash-es/cloneDeep'
|
import cloneDeep from 'lodash-es/cloneDeep'
|
||||||
import { constantRoutes } from '@/router/index.js'
|
import { constantRoutes } from '@/router/index.js'
|
||||||
import Login from './Login.vue'
|
import Login from './Login.vue'
|
||||||
import i18n, { LOCALE_NAMES } from '@/lang/i18n'
|
import { LOCALE_NAMES } from '@/lang/i18n'
|
||||||
import User from './User.vue'
|
import User from './User.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -126,9 +127,6 @@ export default {
|
||||||
demoEnv() {
|
demoEnv() {
|
||||||
return import.meta.env.MODE === 'demo'
|
return import.meta.env.MODE === 'demo'
|
||||||
},
|
},
|
||||||
localeNames() {
|
|
||||||
return LOCALE_NAMES
|
|
||||||
},
|
|
||||||
localeName() {
|
localeName() {
|
||||||
return LOCALE_NAMES[this.lang] || 'English'
|
return LOCALE_NAMES[this.lang] || 'English'
|
||||||
},
|
},
|
||||||
|
|
@ -219,12 +217,6 @@ export default {
|
||||||
this.openLogin()
|
this.openLogin()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSelect(value) {
|
|
||||||
if (value != this.lang) {
|
|
||||||
this.$store.dispatch('main/setLanguage', value)
|
|
||||||
i18n.global.locale = value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
logout() {
|
logout() {
|
||||||
this.$store.dispatch('user/logout2').then((_) => {
|
this.$store.dispatch('user/logout2').then((_) => {
|
||||||
this.$router.replace('/')
|
this.$router.replace('/')
|
||||||
|
|
@ -246,39 +238,71 @@ export default {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
padding-left: 30px;
|
padding-left: 20px;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
|
|
||||||
.left {
|
.left-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
padding-left: 4px;
|
||||||
|
height: 60px;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
&-collapse {
|
.sidebar-trigger {
|
||||||
display: none;
|
display: inline-flex;
|
||||||
transition: 0.25s;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
|
||||||
&.isCollapse {
|
&:hover {
|
||||||
transform: rotate(-180deg);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--desktop {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.25s;
|
||||||
|
|
||||||
|
&.isCollapse {
|
||||||
|
transform: rotate(-180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--mobile {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
width: 22px;
|
||||||
|
padding: 2px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
.hamburger-line {
|
||||||
padding-left: 8px;
|
display: block;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-end;
|
||||||
height: 60px;
|
cursor: pointer;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
|
||||||
.logo {
|
&-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
cursor: pointer;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
|
|
||||||
&-wrap {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,6 +333,12 @@ export default {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-display[disabled] {
|
||||||
|
color: #999999 !important;
|
||||||
|
opacity: 0.85;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.logout {
|
&.logout {
|
||||||
|
|
@ -410,23 +440,23 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 768px) {
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
.left {
|
|
||||||
&-collapse {
|
.left-menu {
|
||||||
display: block;
|
.sidebar-trigger {
|
||||||
font-size: 18px;
|
&__icon--desktop {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon--mobile {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-menu {
|
.logo {
|
||||||
display: flex;
|
display: none;
|
||||||
.logo {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.mf-icon {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export default {
|
||||||
1 100%;
|
1 100%;
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 50px !important;
|
width: 56px !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
|
|
@ -134,20 +134,27 @@ export default {
|
||||||
|
|
||||||
:deep(.arco-menu) {
|
:deep(.arco-menu) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding-left: 16px;
|
padding: 8px 12px 12px;
|
||||||
|
|
||||||
&-item {
|
&-item {
|
||||||
width: 180px;
|
min-height: 48px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 15px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.7);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
&.arco-menu-selected,
|
&.arco-menu-selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgb(var(--primary-6));
|
background-color: rgb(var(--primary-6));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.arco-menu-collapsed) .arco-menu-item {
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.toogle-menu {
|
.toogle-menu {
|
||||||
|
|
@ -172,18 +179,22 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
top: 60px;
|
top: 60px;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 8px 0 32px rgba(0, 0, 0, 0.35);
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
|
min-width: 0 !important;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="app-wrapper">
|
<div class="app-wrapper">
|
||||||
|
<div
|
||||||
|
v-show="showSidebar && mobileDrawerOpen"
|
||||||
|
class="sidebar-backdrop"
|
||||||
|
aria-hidden="true"
|
||||||
|
@click="closeMobileDrawer" />
|
||||||
<div class="fixed-header">
|
<div class="fixed-header">
|
||||||
<nav-bar class="nav-bar" />
|
<nav-bar class="nav-bar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -18,6 +23,8 @@
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { sideBar, appMain, navBar, appFooter } from './components'
|
import { sideBar, appMain, navBar, appFooter } from './components'
|
||||||
|
|
||||||
|
const LAYOUT_MOBILE_MAX = 768
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'layout',
|
name: 'layout',
|
||||||
components: {
|
components: {
|
||||||
|
|
@ -26,16 +33,44 @@ export default {
|
||||||
navBar,
|
navBar,
|
||||||
appFooter
|
appFooter
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
layoutWidth:
|
||||||
|
typeof window !== 'undefined' ? window.innerWidth : LAYOUT_MOBILE_MAX + 1
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['sidebar']),
|
...mapGetters(['sidebar']),
|
||||||
isCollapsed() {
|
isCollapsed() {
|
||||||
return !this.sidebar.opened
|
return !this.sidebar.opened
|
||||||
},
|
},
|
||||||
|
isMobileLayout() {
|
||||||
|
return this.layoutWidth <= LAYOUT_MOBILE_MAX
|
||||||
|
},
|
||||||
|
mobileDrawerOpen() {
|
||||||
|
return this.isMobileLayout && this.sidebar.opened
|
||||||
|
},
|
||||||
// 是否显示菜单
|
// 是否显示菜单
|
||||||
showSidebar() {
|
showSidebar() {
|
||||||
let { menu } = this.$route.query || {}
|
let { menu } = this.$route.query || {}
|
||||||
return menu != 0
|
return menu != 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this._layoutOnResize = () => {
|
||||||
|
this.layoutWidth = window.innerWidth
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener('resize', this._layoutOnResize)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeMobileDrawer() {
|
||||||
|
if (this.isMobileLayout && this.sidebar.opened) {
|
||||||
|
this.$store.dispatch('main/closeSideBar')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -49,6 +84,10 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f0f12;
|
background: #0f0f12;
|
||||||
|
|
||||||
|
.sidebar-backdrop {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.fixed-header {
|
.fixed-header {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
.navbar {
|
.navbar {
|
||||||
|
|
@ -59,6 +98,18 @@ export default {
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: calc(100% - 60px);
|
height: calc(100% - 60px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-wrapper .sidebar-backdrop {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 60px 0 0 0;
|
||||||
|
z-index: 99;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ import router from './router'
|
||||||
|
|
||||||
import '@/assets/styles/base.less'
|
import '@/assets/styles/base.less'
|
||||||
|
|
||||||
import '@/assets/styles/quill.bubble.css'
|
// Quill 样式 - 临时注释避免 source map 文件缺失导致的构建错误
|
||||||
import '@/assets/styles/quill.core.css'
|
// 如果后续需要使用 Quill 富文本编辑器,可取消注释
|
||||||
import '@/assets/styles/quill.snow.css'
|
// import '@/assets/styles/quill.bubble.css'
|
||||||
|
// import '@/assets/styles/quill.core.css'
|
||||||
|
// import '@/assets/styles/quill.snow.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
|
||||||
|
|
@ -69,49 +69,47 @@ export const constantRoutes = [{
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_tst'
|
icon: 'btn_tst'
|
||||||
}
|
}
|
||||||
},
|
}, {
|
||||||
// {
|
path: 'image-to-image?type=2',
|
||||||
// path: 'image-to-image?type=2',
|
name: 'image-to-image2',
|
||||||
// name: 'image-to-image2',
|
component: () => import('@/views/Image.vue'),
|
||||||
// component: () => import('@/views/Image.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'imageToImage2',
|
||||||
// title: 'imageToImage2',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_tst'
|
||||||
// icon: 'btn_tst'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'change-face',
|
||||||
// path: 'change-face',
|
name: 'change-face',
|
||||||
// name: 'change-face',
|
component: () => import('@/views/ChangeFace.vue'),
|
||||||
// component: () => import('@/views/ChangeFace.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'changeFace',
|
||||||
// title: 'changeFace',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_yjhl'
|
||||||
// icon: 'btn_yjhl'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'change-face-video',
|
||||||
// path: 'change-face-video',
|
name: 'change-face-video',
|
||||||
// name: 'change-face-video',
|
component: () => import('@/views/ChangeFace.vue'),
|
||||||
// component: () => import('@/views/ChangeFace.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'changeFaceVideo',
|
||||||
// title: 'changeFaceVideo',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_yjhl'
|
||||||
// icon: 'btn_yjhl'
|
}
|
||||||
// }
|
}, {
|
||||||
// }, {
|
path: 'fast-image',
|
||||||
// path: 'fast-image',
|
name: 'fast-image',
|
||||||
// name: 'fast-image',
|
component: () => import('@/views/FastImage.vue'),
|
||||||
// component: () => import('@/views/FastImage.vue'),
|
meta: {
|
||||||
// meta: {
|
title: 'fastImage',
|
||||||
// title: 'fastImage',
|
menuItem: true,
|
||||||
// menuItem: true,
|
permission: "pass",
|
||||||
// permission: "pass",
|
icon: 'btn_kjst'
|
||||||
// icon: 'btn_kjst'
|
}
|
||||||
// }
|
}, {
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: 'fast-video',
|
path: 'fast-video',
|
||||||
name: 'fast-video',
|
name: 'fast-video',
|
||||||
component: () => import('@/views/FastVideo.vue'),
|
component: () => import('@/views/FastVideo.vue'),
|
||||||
|
|
@ -121,6 +119,66 @@ export const constantRoutes = [{
|
||||||
permission: "pass",
|
permission: "pass",
|
||||||
icon: 'btn_kjsp'
|
icon: 'btn_kjsp'
|
||||||
}
|
}
|
||||||
|
}, {
|
||||||
|
path: 'video-gen',
|
||||||
|
name: 'video-gen',
|
||||||
|
component: () => import('@/views/VideoGen.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'videoGen',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'third-party-asset',
|
||||||
|
name: 'third-party-asset',
|
||||||
|
component: () => import('@/views/ThirdPartyAsset.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'thirdPartyAsset',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
},{
|
||||||
|
path: 'generated-assets',
|
||||||
|
name: 'generated-assets',
|
||||||
|
component: () => import('@/views/GeneratedAssets.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'generatedAssets',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'asset-group-manage',
|
||||||
|
name: 'asset-group-manage',
|
||||||
|
component: () => import('@/views/AssetGroupManage.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'assetGroupManage',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'asset-manage',
|
||||||
|
name: 'asset-manage',
|
||||||
|
component: () => import('@/views/AssetManage.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'assetManage',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
path: 'generated-assets',
|
||||||
|
name: 'generatedAssets',
|
||||||
|
component: () => import('@/views/GeneratedAssets.vue'),
|
||||||
|
meta: {
|
||||||
|
title: 'generatedAssets',
|
||||||
|
menuItem: true,
|
||||||
|
permission: "pass",
|
||||||
|
icon: 'btn_video'
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'recharge',
|
path: 'recharge',
|
||||||
name: 'recharge',
|
name: 'recharge',
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ const state = {
|
||||||
sidebar: {
|
sidebar: {
|
||||||
opened: isMobile() ? false : true,
|
opened: isMobile() ? false : true,
|
||||||
withoutAnimation: false,
|
withoutAnimation: false,
|
||||||
width: Cookies.get('sidebarWidth') || '230px'
|
// 左侧导航默认适中宽度(用户若需更窄/宽可本地调小范围会写 cookie)
|
||||||
|
width: Cookies.get('sidebarWidth') || '268px'
|
||||||
},
|
},
|
||||||
topMenu: Cookies.get('topMenu'),
|
topMenu: Cookies.get('topMenu'),
|
||||||
// 当前语言
|
// 当前语言
|
||||||
language: Cookies.get('language') || 'en_US',
|
// 多语言切换已禁用:全站固定繁体中文(zh_HK)
|
||||||
|
language: 'zh_HK',
|
||||||
// 主题 '':亮色 / dark:暗黑
|
// 主题 '':亮色 / dark:暗黑
|
||||||
theme: $storage.get('theme') || '',
|
theme: $storage.get('theme') || '',
|
||||||
// 系统端
|
// 系统端
|
||||||
|
|
@ -39,8 +41,8 @@ const state = {
|
||||||
showMessage: false,
|
showMessage: false,
|
||||||
messageData: {},
|
messageData: {},
|
||||||
messageCount: 0,
|
messageCount: 0,
|
||||||
// 是否显示18禁弹窗
|
// 是否显示18禁弹窗:默认不拦截,直接进入首页等主页面
|
||||||
showForbidden: true,
|
showForbidden: false,
|
||||||
// 阻止页面切换
|
// 阻止页面切换
|
||||||
showPrevent: false,
|
showPrevent: false,
|
||||||
showLogin: false
|
showLogin: false
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,12 @@ export const isFirefox = (_) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isMobile = _=> {
|
export const isMobile = (_) => {
|
||||||
const userAgent = navigator.userAgent;
|
const userAgent = navigator.userAgent
|
||||||
const isMobile = /iPad|Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
const uaMobile =
|
||||||
const isSmallScreen = window.innerWidth <= 576;
|
/iPad|Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||||
return isMobile || isSmallScreen;
|
userAgent
|
||||||
|
)
|
||||||
|
const isSmallScreen = typeof window !== 'undefined' && window.innerWidth <= 768
|
||||||
|
return uaMobile || isSmallScreen
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* 火山素材相关接口 data 层字段兼容:
|
||||||
|
* Spring/Jackson 序列化为 camelCase(items、totalCount),
|
||||||
|
* 旧前端曾按 PascalCase(Items、TotalCount)取值,此处统一解析。
|
||||||
|
*/
|
||||||
|
export function byteApiItems(data) {
|
||||||
|
if (!data) return []
|
||||||
|
if (Array.isArray(data.items)) return data.items
|
||||||
|
if (Array.isArray(data.Items)) return data.Items
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function byteApiTotalCount(data) {
|
||||||
|
if (!data) return 0
|
||||||
|
const raw = data.totalCount ?? data.TotalCount
|
||||||
|
const n = Number(raw)
|
||||||
|
return Number.isFinite(n) ? n : 0
|
||||||
|
}
|
||||||
|
|
@ -130,6 +130,23 @@ export const convertBase64ToUrl = (base64) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从若依 AjaxResult / 兼容仅返回顶层 url 的上传响应中解析访问地址
|
||||||
|
*/
|
||||||
|
export const extractUploadUrlFromResponse = (res) => {
|
||||||
|
if (!res) return ''
|
||||||
|
if (typeof res.data === 'string' && res.data.trim()) return res.data.trim()
|
||||||
|
if (res.data && typeof res.data === 'object') {
|
||||||
|
const u = res.data.url || res.data.path
|
||||||
|
if (typeof u === 'string' && u.trim()) return u.trim()
|
||||||
|
}
|
||||||
|
if (typeof res.url === 'string' && res.url.trim()) return res.url.trim()
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 门户统一腾讯云 COS:后端 FileController `/api/file/upload`,与 mf-image-upload 一致 */
|
||||||
|
export const PORTAL_TENCENT_COS_UPLOAD_URL = '/api/file/upload'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件上传
|
* 文件上传
|
||||||
* @param {Object} {url, file,name} 文件上传地址 file 文件 name 文件名参数
|
* @param {Object} {url, file,name} 文件上传地址 file 文件 name 文件名参数
|
||||||
|
|
@ -148,12 +165,10 @@ export const uploadFile = ({
|
||||||
formData.append(key, data[key])
|
formData.append(key, data[key])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
// 不要手动设置 Content-Type,否则缺少 boundary,服务端无法解析 multipart,文件参数字段为空
|
||||||
return request({
|
return request({
|
||||||
url: url,
|
url: url,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: formData,
|
data: formData
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -21,7 +21,7 @@ const messages = i18n.global.messages[lang];
|
||||||
* @desc 创建axios实例
|
* @desc 创建axios实例
|
||||||
*/
|
*/
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
// baseURL: process.env.VUE_APP_BASE_API,
|
baseURL: process.env.VUE_APP_BASE_API,
|
||||||
withCredentials: import.meta.env.MODE === 'development',
|
withCredentials: import.meta.env.MODE === 'development',
|
||||||
timeout: 600 * 1000
|
timeout: 600 * 1000
|
||||||
})
|
})
|
||||||
|
|
@ -42,7 +42,10 @@ const logout = () => {
|
||||||
*/
|
*/
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
if (config.url && config.url.includes('file/upload')) {
|
if (
|
||||||
|
config.url &&
|
||||||
|
(config.url.includes('file/upload') || config.url.includes('cos/upload'))
|
||||||
|
) {
|
||||||
config.headers['Content-Type'] =
|
config.headers['Content-Type'] =
|
||||||
'multipart/form-data;boundary = ' + new Date().getTime()
|
'multipart/form-data;boundary = ' + new Date().getTime()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
<template>
|
||||||
|
<div class="asset-group-page">
|
||||||
|
<section class="ag-panel">
|
||||||
|
<div class="ag-head">
|
||||||
|
<h3 class="ag-title">资源组管理</h3>
|
||||||
|
<a-button type="primary" @click="openCreateDialog">新增资源组</a-button>
|
||||||
|
</div>
|
||||||
|
<div class="ag-filter">
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>名称</label>
|
||||||
|
<a-input v-model="filters.name" placeholder="按名称过滤" />
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>资源组编号</label>
|
||||||
|
<a-input v-model="filters.groupIdsText" placeholder="多个编号用英文逗号分隔" />
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>资源组类型</label>
|
||||||
|
<a-select v-model="filters.groupType">
|
||||||
|
<a-option value="AIGC">AIGC(生成类)</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>排序字段</label>
|
||||||
|
<a-select v-model="filters.sortBy">
|
||||||
|
<a-option value="CreateTime">创建时间</a-option>
|
||||||
|
<a-option value="UpdateTime">更新时间</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="ag-field">
|
||||||
|
<label>排序方向</label>
|
||||||
|
<a-select v-model="filters.sortOrder">
|
||||||
|
<a-option value="Desc">从新到旧</a-option>
|
||||||
|
<a-option value="Asc">从旧到新</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="ag-actions">
|
||||||
|
<a-button type="primary" :loading="listLoading" @click="search(1)">查询</a-button>
|
||||||
|
<a-button @click="resetFilters">重置</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="listLoading">
|
||||||
|
<div class="ag-total">总数:{{ totalCount }}</div>
|
||||||
|
<div class="ag-table-wrap">
|
||||||
|
<table class="ag-table">
|
||||||
|
<colgroup>
|
||||||
|
<col class="ag-col-id" />
|
||||||
|
<col class="ag-col-name" />
|
||||||
|
<col class="ag-col-desc" />
|
||||||
|
<col class="ag-col-type" />
|
||||||
|
<col class="ag-col-project" />
|
||||||
|
<col class="ag-col-time" />
|
||||||
|
<col class="ag-col-time" />
|
||||||
|
<col class="ag-col-action" />
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>编号</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>描述</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>项目名称</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>更新时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in items" :key="item.Id || item.id">
|
||||||
|
<td>{{ item.Id || item.id }}</td>
|
||||||
|
<td>{{ item.Name || item.name }}</td>
|
||||||
|
<td>{{ item.Description || item.description || '-' }}</td>
|
||||||
|
<td>{{ formatGroupTypeLabel(item) }}</td>
|
||||||
|
<td>{{ item.ProjectName || item.projectName || '-' }}</td>
|
||||||
|
<td>{{ item.CreateTime || item.createTime || '-' }}</td>
|
||||||
|
<td>{{ item.UpdateTime || item.updateTime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
type="outline"
|
||||||
|
:loading="detailLoadingId === (item.Id || item.id)"
|
||||||
|
@click="getDetail(item)">
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!items.length">
|
||||||
|
<td colspan="8" class="ag-empty">暂无数据</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="ag-pagination">
|
||||||
|
<a-pagination
|
||||||
|
:total="totalCount"
|
||||||
|
:current="filters.pageNumber"
|
||||||
|
:page-size="filters.pageSize"
|
||||||
|
show-total
|
||||||
|
show-jumper
|
||||||
|
show-page-size
|
||||||
|
:page-size-options="listPageSizeOptions"
|
||||||
|
@change="onGroupPageChange"
|
||||||
|
@page-size-change="onPageSizeChange" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-modal v-model:visible="detailVisible" title="资源组详情" :footer="false" width="680px">
|
||||||
|
<pre class="ag-detail">{{ prettyDetail }}</pre>
|
||||||
|
</a-modal>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="createVisible"
|
||||||
|
title="新增资源组"
|
||||||
|
width="520px"
|
||||||
|
:confirm-loading="createLoading"
|
||||||
|
@ok="createGroup">
|
||||||
|
<!-- 弹层 teleported 到 body,使用 FormItem 保证标签与 dark 样式生效 -->
|
||||||
|
<a-form :model="createForm" layout="horizontal" auto-label-width class="ag-create-modal-form">
|
||||||
|
<a-form-item label="名称" required>
|
||||||
|
<a-input
|
||||||
|
v-model="createForm.name"
|
||||||
|
placeholder="请输入资源组名称(≤64 字符)"
|
||||||
|
:max-length="64"
|
||||||
|
allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="描述">
|
||||||
|
<a-textarea
|
||||||
|
v-model="createForm.description"
|
||||||
|
:max-length="300"
|
||||||
|
show-word-limit
|
||||||
|
:auto-size="{ minRows: 3, maxRows: 8 }"
|
||||||
|
placeholder="请输入描述(≤300 字符)" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||||
|
|
||||||
|
const LIST_PAGE_NUMBER_MAX = 100
|
||||||
|
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AssetGroupManage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
createLoading: false,
|
||||||
|
createVisible: false,
|
||||||
|
listLoading: false,
|
||||||
|
detailLoadingId: '',
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
createForm: {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
groupType: 'AIGC'
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
name: '',
|
||||||
|
groupIdsText: '',
|
||||||
|
groupType: 'AIGC',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
},
|
||||||
|
totalCount: 0,
|
||||||
|
items: [],
|
||||||
|
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
prettyDetail() {
|
||||||
|
return this.detailData ? JSON.stringify(this.detailData, null, 2) : '{}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatGroupTypeLabel(item) {
|
||||||
|
const t = String(item?.GroupType || item?.groupType || '').trim()
|
||||||
|
if (!t) return '-'
|
||||||
|
if (t === 'AIGC') return 'AIGC(生成类)'
|
||||||
|
return t
|
||||||
|
},
|
||||||
|
clampGroupPage(n) {
|
||||||
|
const page = Number(n) || 1
|
||||||
|
const size = Number(this.filters.pageSize) || 10
|
||||||
|
let maxPage = LIST_PAGE_NUMBER_MAX
|
||||||
|
if (this.totalCount > 0) {
|
||||||
|
const totalPages = Math.ceil(this.totalCount / size)
|
||||||
|
maxPage = Math.min(LIST_PAGE_NUMBER_MAX, Math.max(1, totalPages))
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(1, page), maxPage)
|
||||||
|
},
|
||||||
|
onGroupPageChange(page) {
|
||||||
|
this.search(this.clampGroupPage(page))
|
||||||
|
},
|
||||||
|
openCreateDialog() {
|
||||||
|
this.createVisible = true
|
||||||
|
},
|
||||||
|
buildGroupIds() {
|
||||||
|
return String(this.filters.groupIdsText || '')
|
||||||
|
.split(',')
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter((x) => !!x)
|
||||||
|
},
|
||||||
|
async createGroup() {
|
||||||
|
const name = String(this.createForm.name || '').trim()
|
||||||
|
if (!name) {
|
||||||
|
this.$message.error('请填写名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.createLoading = true
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/byteAssetGroup/createAssetGroup',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
description: String(this.createForm.description || '').trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('新增成功')
|
||||||
|
this.createForm.name = ''
|
||||||
|
this.createForm.description = ''
|
||||||
|
this.createVisible = false
|
||||||
|
this.search(1)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '新增失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '新增失败')
|
||||||
|
} finally {
|
||||||
|
this.createLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async search(page = this.filters.pageNumber) {
|
||||||
|
this.filters.pageNumber = this.clampGroupPage(page)
|
||||||
|
this.listLoading = true
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
filter: {
|
||||||
|
groupType: this.filters.groupType || 'AIGC'
|
||||||
|
},
|
||||||
|
pageNumber: this.filters.pageNumber,
|
||||||
|
pageSize: this.filters.pageSize,
|
||||||
|
sortBy: this.filters.sortBy,
|
||||||
|
sortOrder: this.filters.sortOrder
|
||||||
|
}
|
||||||
|
const name = String(this.filters.name || '').trim()
|
||||||
|
if (name) payload.filter.name = name
|
||||||
|
const ids = this.buildGroupIds()
|
||||||
|
if (ids.length) payload.filter.groupIds = ids
|
||||||
|
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/byteAssetGroup/listAssetGroups',
|
||||||
|
method: 'POST',
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
const data = res.data || {}
|
||||||
|
this.totalCount = byteApiTotalCount(data)
|
||||||
|
this.items = byteApiItems(data)
|
||||||
|
if (res.code !== 200) {
|
||||||
|
this.$message.error(res.msg || '查询失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询失败')
|
||||||
|
} finally {
|
||||||
|
this.listLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPageSizeChange(size) {
|
||||||
|
const s = Number(size) || 10
|
||||||
|
this.filters.pageSize = LIST_PAGE_SIZE_OPTIONS.includes(s) ? s : 10
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
this.filters = {
|
||||||
|
name: '',
|
||||||
|
groupIdsText: '',
|
||||||
|
groupType: 'AIGC',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
}
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
async getDetail(item) {
|
||||||
|
const id = item?.Id || item?.id
|
||||||
|
if (!id) return
|
||||||
|
this.detailLoadingId = id
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/byteAssetGroup/getAssetGroup',
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: id }
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.detailData = res.data || {}
|
||||||
|
this.detailVisible = true
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询详情失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询详情失败')
|
||||||
|
} finally {
|
||||||
|
this.detailLoadingId = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.asset-group-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
background: #0a0b0d;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-panel {
|
||||||
|
background: rgba(22, 24, 30, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.ag-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-form,
|
||||||
|
.ag-filter {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.ag-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-field label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-total {
|
||||||
|
margin: 6px 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table-wrap {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列宽按百分比分配,随容器(100% 宽)随分辨率伸缩 */
|
||||||
|
.ag-col-id {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.ag-col-name {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
.ag-col-desc {
|
||||||
|
width: 24%;
|
||||||
|
}
|
||||||
|
.ag-col-type {
|
||||||
|
width: 8%;
|
||||||
|
}
|
||||||
|
.ag-col-project {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
.ag-col-time {
|
||||||
|
width: 11%;
|
||||||
|
}
|
||||||
|
.ag-col-action {
|
||||||
|
width: 12%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table th,
|
||||||
|
.ag-table td {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-table th {
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-weight: 600;
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-pagination {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ag-detail {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
color: #d8f4f7;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.ag-form,
|
||||||
|
.ag-filter {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,932 @@
|
||||||
|
<template>
|
||||||
|
<div class="asset-manage-page">
|
||||||
|
<section class="asset-left">
|
||||||
|
<div class="asset-left-head">
|
||||||
|
<div class="panel-title">素材组树</div>
|
||||||
|
</div>
|
||||||
|
<div class="group-tree">
|
||||||
|
<div
|
||||||
|
v-for="g in groups"
|
||||||
|
:key="g.Id || g.id"
|
||||||
|
:class="['group-node', { active: selectedGroupId === (g.Id || g.id) }]"
|
||||||
|
@click="selectGroup(g)">
|
||||||
|
<span class="folder-icon">📁</span>
|
||||||
|
<div class="group-meta">
|
||||||
|
<div class="group-name">{{ g.Name || g.name || (g.Id || g.id) }}</div>
|
||||||
|
<div class="group-id">{{ g.Id || g.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!groups.length" class="empty-tip">暂无素材组,请先在“资源组管理”创建</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="asset-right">
|
||||||
|
<div class="asset-left-head">
|
||||||
|
<div class="panel-title">素材管理</div>
|
||||||
|
<div class="field actions">
|
||||||
|
<a-button type="primary" size="mini" @click="openCreateAssetDialog">新增素材</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-title mtop">查询素材</div>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>素材组</label>
|
||||||
|
<a-select
|
||||||
|
v-model="filters.groupId"
|
||||||
|
placeholder="请选择素材组"
|
||||||
|
allow-clear
|
||||||
|
:loading="groupLoading"
|
||||||
|
@change="onFilterGroupChange">
|
||||||
|
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
||||||
|
{{ (g.Name || g.name || g.Id || g.id) + ' · ' + (g.Id || g.id) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>素材名称</label>
|
||||||
|
<a-input v-model="filters.name" placeholder="按名称筛选" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>状态</label>
|
||||||
|
<a-select v-model="filters.status" placeholder="全部状态">
|
||||||
|
<a-option value="">全部</a-option>
|
||||||
|
<a-option value="Active">可用</a-option>
|
||||||
|
<a-option value="Processing">处理中</a-option>
|
||||||
|
<a-option value="Failed">失败</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>排序字段</label>
|
||||||
|
<a-select v-model="filters.sortBy">
|
||||||
|
<a-option value="CreateTime">创建时间</a-option>
|
||||||
|
<a-option value="UpdateTime">更新时间</a-option>
|
||||||
|
<a-option value="GroupId">素材组</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>排序方向</label>
|
||||||
|
<a-select v-model="filters.sortOrder">
|
||||||
|
<a-option value="Desc">从新到旧</a-option>
|
||||||
|
<a-option value="Asc">从旧到新</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field actions">
|
||||||
|
<a-button type="primary" :loading="listLoading" @click="searchAssets(1)">查询</a-button>
|
||||||
|
<a-button @click="resetFilters">重置</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="listLoading">
|
||||||
|
<div class="total-line">总数:{{ totalCount }}</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="asset-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>素材编号</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>访问地址</th>
|
||||||
|
<th>所属素材组</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="it in items" :key="it.Id || it.id">
|
||||||
|
<td class="asset-td-id">{{ it.Id || it.id }}</td>
|
||||||
|
<td>{{ it.Name || it.name || '-' }}</td>
|
||||||
|
<td class="asset-url-cell">
|
||||||
|
<template v-if="assetUrlPreviewKind(it) === 'image' && !thumbError[it.Id || it.id]">
|
||||||
|
<a
|
||||||
|
:href="assetUrl(it)"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="asset-thumb-link">
|
||||||
|
<img
|
||||||
|
class="asset-url-thumb"
|
||||||
|
:src="assetUrl(it)"
|
||||||
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
@error="onAssetThumbError(it)" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="assetUrlPreviewKind(it) === 'video'">
|
||||||
|
<video
|
||||||
|
class="asset-url-video"
|
||||||
|
:src="assetUrl(it)"
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
preload="metadata"
|
||||||
|
controls />
|
||||||
|
</template>
|
||||||
|
<template v-else-if="assetUrl(it)">
|
||||||
|
<a :href="assetUrl(it)" target="_blank" rel="noreferrer" class="asset-url-plain">{{
|
||||||
|
assetUrl(it)
|
||||||
|
}}</a>
|
||||||
|
</template>
|
||||||
|
<span v-else class="asset-url-empty">-</span>
|
||||||
|
<div
|
||||||
|
v-if="assetUrlCaptionVisible(it)"
|
||||||
|
class="asset-url-caption"
|
||||||
|
:title="assetUrl(it)">
|
||||||
|
{{ assetUrl(it) }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ it.GroupId || it.groupId || '-' }}</td>
|
||||||
|
<td>{{ formatAssetTypeLabel(it) }}</td>
|
||||||
|
<td>{{ formatAssetStatusLabel(it) }}</td>
|
||||||
|
<td>{{ it.CreateTime || it.createTime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a-button size="mini" type="outline" @click="getAsset(it)">详情</a-button>
|
||||||
|
<a-button size="mini" status="danger" @click="deleteAsset(it)">删除</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!items.length">
|
||||||
|
<td colspan="8" class="empty-tip">暂无素材</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pager">
|
||||||
|
<a-pagination
|
||||||
|
:total="totalCount"
|
||||||
|
:current="filters.pageNumber"
|
||||||
|
:page-size="filters.pageSize"
|
||||||
|
show-total
|
||||||
|
show-jumper
|
||||||
|
show-page-size
|
||||||
|
:page-size-options="listPageSizeOptions"
|
||||||
|
@change="onAssetPageChange"
|
||||||
|
@page-size-change="onPageSizeChange" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-modal v-model:visible="detailVisible" title="素材详情" :footer="false" width="700px">
|
||||||
|
<pre class="detail-box">{{ detailText }}</pre>
|
||||||
|
</a-modal>
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="createAssetVisible"
|
||||||
|
title="新增素材"
|
||||||
|
width="520px"
|
||||||
|
:confirm-loading="createLoading"
|
||||||
|
@ok="createAsset">
|
||||||
|
<a-form :model="createForm" layout="horizontal" auto-label-width class="asset-create-modal-form">
|
||||||
|
<a-form-item label="素材组" required>
|
||||||
|
<a-select v-model="createForm.groupId" placeholder="请选择素材组" allow-clear>
|
||||||
|
<a-option v-for="g in groups" :key="g.Id || g.id" :value="String(g.Id || g.id)">
|
||||||
|
{{ g.Name || g.name || (g.Id || g.id) }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="上传文件" required>
|
||||||
|
<div
|
||||||
|
class="asset-file-dropzone"
|
||||||
|
:class="{ 'asset-file-dropzone--dragover': createUploadDragOver }"
|
||||||
|
@click="triggerCreateFilePicker"
|
||||||
|
@dragenter.prevent="onCreateUploadDragEnter"
|
||||||
|
@dragover.prevent="onCreateUploadDragOver"
|
||||||
|
@dragleave.prevent="onCreateUploadDragLeave"
|
||||||
|
@drop.prevent="onCreateUploadDrop">
|
||||||
|
<input
|
||||||
|
ref="createAssetFileInput"
|
||||||
|
class="asset-file-input"
|
||||||
|
type="file"
|
||||||
|
style="display: none"
|
||||||
|
@change="onFileChange" />
|
||||||
|
<div class="asset-file-dropzone-content">
|
||||||
|
<div class="asset-file-dropzone-icon">+</div>
|
||||||
|
<div class="asset-file-dropzone-text">点击选择或拖拽上传</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="createForm.previewUrl && createForm.assetType === 'Image'" class="asset-file-preview">
|
||||||
|
<img :src="createForm.previewUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="asset-file-hint">{{ createForm.fileName || '未选择文件' }}</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="素材名称">
|
||||||
|
<a-input v-model="createForm.name" placeholder="素材名称(可选)" allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="类型">
|
||||||
|
<a-select v-model="createForm.assetType">
|
||||||
|
<a-option value="Image">图片</a-option>
|
||||||
|
<a-option value="Video">视频</a-option>
|
||||||
|
<a-option value="Audio">音频</a-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { byteApiItems, byteApiTotalCount } from '@/utils/byteAssetApi'
|
||||||
|
import { extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL, uploadFile } from '@/utils/file'
|
||||||
|
|
||||||
|
/** 素材树与筛选区素材组列表统一请求体 */
|
||||||
|
const GROUP_LIST_REQUEST_BODY = {
|
||||||
|
filter: { groupType: 'AIGC' },
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
const GROUP_LIST_API = 'api/byteAssetGroup/listAssetGroups'
|
||||||
|
const ASSET_CREATE_API = 'api/byteAsset/createAsset'
|
||||||
|
const ASSET_LIST_API = 'api/byteAsset/listAssets'
|
||||||
|
const ASSET_GET_API = 'api/byteAsset/getAsset'
|
||||||
|
const ASSET_DELETE_API = 'api/byteAsset/deleteAsset'
|
||||||
|
|
||||||
|
/** 列表页码上限(与接口分页一致);每页条数可选项 */
|
||||||
|
const LIST_PAGE_NUMBER_MAX = 100
|
||||||
|
const LIST_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AssetManage',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
groupLoading: false,
|
||||||
|
createLoading: false,
|
||||||
|
listLoading: false,
|
||||||
|
createAssetVisible: false,
|
||||||
|
groups: [],
|
||||||
|
selectedGroupId: '',
|
||||||
|
createForm: {
|
||||||
|
groupId: '',
|
||||||
|
file: null,
|
||||||
|
fileName: '',
|
||||||
|
previewUrl: '',
|
||||||
|
localPreviewUrl: '',
|
||||||
|
name: '',
|
||||||
|
assetType: 'Image'
|
||||||
|
},
|
||||||
|
createUploadDragOver: false,
|
||||||
|
filters: {
|
||||||
|
groupId: '',
|
||||||
|
name: '',
|
||||||
|
status: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
},
|
||||||
|
totalCount: 0,
|
||||||
|
items: [],
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
listPageSizeOptions: LIST_PAGE_SIZE_OPTIONS,
|
||||||
|
thumbError: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
detailText() {
|
||||||
|
return this.detailData ? JSON.stringify(this.detailData, null, 2) : '{}'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadGroups()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
createAssetVisible(val) {
|
||||||
|
if (!val) this.clearCreateUploadPreview()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearCreateUploadPreview() {
|
||||||
|
if (this.createForm?.localPreviewUrl) {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(this.createForm.localPreviewUrl)
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
this.createForm.previewUrl = ''
|
||||||
|
this.createForm.localPreviewUrl = ''
|
||||||
|
},
|
||||||
|
setCreateFormFile(file) {
|
||||||
|
this.createForm.file = file || null
|
||||||
|
this.createForm.fileName = file?.name || ''
|
||||||
|
|
||||||
|
const mime = String(file?.type || '').toLowerCase()
|
||||||
|
// 根据文件类型自动设置素材类型,避免与文件不匹配
|
||||||
|
if (mime.startsWith('image/')) this.createForm.assetType = 'Image'
|
||||||
|
else if (mime.startsWith('video/')) this.createForm.assetType = 'Video'
|
||||||
|
else if (mime.startsWith('audio/')) this.createForm.assetType = 'Audio'
|
||||||
|
|
||||||
|
// 图片预览
|
||||||
|
this.clearCreateUploadPreview()
|
||||||
|
if (mime.startsWith('image/')) {
|
||||||
|
try {
|
||||||
|
this.createForm.localPreviewUrl = URL.createObjectURL(file)
|
||||||
|
this.createForm.previewUrl = this.createForm.localPreviewUrl
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatAssetTypeLabel(it) {
|
||||||
|
const t = String(it?.AssetType || it?.assetType || '').trim()
|
||||||
|
if (!t) return '-'
|
||||||
|
const map = {
|
||||||
|
Image: '图片',
|
||||||
|
Video: '视频',
|
||||||
|
Audio: '音频'
|
||||||
|
}
|
||||||
|
return map[t] || t
|
||||||
|
},
|
||||||
|
formatAssetStatusLabel(it) {
|
||||||
|
const s = String(it?.Status || it?.status || '').trim()
|
||||||
|
if (!s) return '-'
|
||||||
|
const map = {
|
||||||
|
Active: '可用',
|
||||||
|
Processing: '处理中',
|
||||||
|
Failed: '失败'
|
||||||
|
}
|
||||||
|
return map[s] || s
|
||||||
|
},
|
||||||
|
assetUrl(it) {
|
||||||
|
return String(it?.URL || it?.url || '').trim()
|
||||||
|
},
|
||||||
|
/** image | video | link */
|
||||||
|
assetUrlPreviewKind(it) {
|
||||||
|
const u = this.assetUrl(it)
|
||||||
|
if (!u || !/^https?:\/\//i.test(u)) return 'link'
|
||||||
|
const t = String(it?.AssetType || it?.assetType || '')
|
||||||
|
if (/image/i.test(t)) return 'image'
|
||||||
|
if (/video/i.test(t)) return 'video'
|
||||||
|
if (/audio/i.test(t)) return 'link'
|
||||||
|
if (/\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(u)) return 'image'
|
||||||
|
if (/\.(mp4|webm|mov|m4v|ogg)(\?|#|$)/i.test(u)) return 'video'
|
||||||
|
return 'link'
|
||||||
|
},
|
||||||
|
onAssetThumbError(it) {
|
||||||
|
const id = it?.Id || it?.id
|
||||||
|
if (id) this.thumbError = { ...this.thumbError, [id]: true }
|
||||||
|
},
|
||||||
|
assetUrlCaptionVisible(it) {
|
||||||
|
const u = this.assetUrl(it)
|
||||||
|
if (!u) return false
|
||||||
|
const k = this.assetUrlPreviewKind(it)
|
||||||
|
if (k === 'video') return true
|
||||||
|
if (k === 'image' && !this.thumbError[it?.Id || it?.id]) return true
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
clampAssetPage(n) {
|
||||||
|
const page = Number(n) || 1
|
||||||
|
const size = Number(this.filters.pageSize) || 10
|
||||||
|
let maxPage = LIST_PAGE_NUMBER_MAX
|
||||||
|
if (this.totalCount > 0) {
|
||||||
|
const totalPages = Math.ceil(this.totalCount / size)
|
||||||
|
maxPage = Math.min(LIST_PAGE_NUMBER_MAX, Math.max(1, totalPages))
|
||||||
|
}
|
||||||
|
return Math.min(Math.max(1, page), maxPage)
|
||||||
|
},
|
||||||
|
onAssetPageChange(page) {
|
||||||
|
this.searchAssets(this.clampAssetPage(page))
|
||||||
|
},
|
||||||
|
async openCreateAssetDialog() {
|
||||||
|
await this.loadGroups()
|
||||||
|
this.createForm.groupId = this.selectedGroupId || this.createForm.groupId || ''
|
||||||
|
this.createForm.file = null
|
||||||
|
this.createForm.fileName = ''
|
||||||
|
this.createForm.name = ''
|
||||||
|
this.createForm.assetType = 'Image'
|
||||||
|
this.clearCreateUploadPreview()
|
||||||
|
this.createAssetVisible = true
|
||||||
|
},
|
||||||
|
async loadGroups() {
|
||||||
|
this.groupLoading = true
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: GROUP_LIST_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: { ...GROUP_LIST_REQUEST_BODY }
|
||||||
|
})
|
||||||
|
this.groups = byteApiItems(res?.data)
|
||||||
|
if (!this.selectedGroupId && this.groups.length) {
|
||||||
|
const gid = String(this.groups[0].Id || this.groups[0].id || '')
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.filters.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '加载分组失败')
|
||||||
|
} finally {
|
||||||
|
this.groupLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectGroup(g) {
|
||||||
|
const gid = String(g?.Id || g?.id || '')
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.filters.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
onFilterGroupChange(val) {
|
||||||
|
const gid = val != null && val !== '' ? String(val) : ''
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
triggerCreateFilePicker() {
|
||||||
|
const el = this.$refs.createAssetFileInput
|
||||||
|
if (el && typeof el.click === 'function') el.click()
|
||||||
|
},
|
||||||
|
onCreateUploadDragEnter() {
|
||||||
|
this.createUploadDragOver = true
|
||||||
|
},
|
||||||
|
onCreateUploadDragOver() {
|
||||||
|
this.createUploadDragOver = true
|
||||||
|
},
|
||||||
|
onCreateUploadDragLeave() {
|
||||||
|
this.createUploadDragOver = false
|
||||||
|
},
|
||||||
|
onCreateUploadDrop(e) {
|
||||||
|
this.createUploadDragOver = false
|
||||||
|
const file = e?.dataTransfer?.files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
this.setCreateFormFile(file)
|
||||||
|
},
|
||||||
|
onFileChange(e) {
|
||||||
|
const file = e?.target?.files?.[0]
|
||||||
|
this.setCreateFormFile(file)
|
||||||
|
},
|
||||||
|
async createAsset() {
|
||||||
|
const groupId = String(this.createForm.groupId || '').trim()
|
||||||
|
if (!groupId) return this.$message.error('请选择素材组')
|
||||||
|
if (!this.createForm.file) return this.$message.error('请选择上传文件')
|
||||||
|
this.createLoading = true
|
||||||
|
try {
|
||||||
|
// 1) 先走 /api/file/upload 拿到上传后的 url
|
||||||
|
const uploadRes = await uploadFile({
|
||||||
|
url: PORTAL_TENCENT_COS_UPLOAD_URL,
|
||||||
|
file: this.createForm.file,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
const uploadUrl = extractUploadUrlFromResponse(uploadRes)
|
||||||
|
if (!uploadUrl) throw new Error(uploadRes?.msg || '上传后未返回文件地址')
|
||||||
|
|
||||||
|
// 图片上传成功后,在上传组件展示远程预览(替换本地预览)
|
||||||
|
if (this.createForm.assetType === 'Image') {
|
||||||
|
this.clearCreateUploadPreview()
|
||||||
|
this.createForm.previewUrl = uploadUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 再通过 /api/byteAsset/createAsset(JSON) 用 url 创建素材
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_CREATE_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
URL: uploadUrl,
|
||||||
|
groupId,
|
||||||
|
assetType: this.createForm.assetType,
|
||||||
|
name: String(this.createForm.name || '').trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success(this.createForm.assetType === 'Image' ? '上传图片成功' : '上传成功')
|
||||||
|
this.createForm.file = null
|
||||||
|
this.createForm.name = ''
|
||||||
|
const isImage = this.createForm.assetType === 'Image'
|
||||||
|
if (isImage) {
|
||||||
|
// 图片模式下延迟关闭,避免用户来不及看到上传组件缩略图
|
||||||
|
setTimeout(() => {
|
||||||
|
this.createAssetVisible = false
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
this.createAssetVisible = false
|
||||||
|
}
|
||||||
|
this.searchAssets(1)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '新增素材失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '新增素材失败')
|
||||||
|
} finally {
|
||||||
|
this.createLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildListPayload() {
|
||||||
|
const filter = {
|
||||||
|
groupType: 'AIGC'
|
||||||
|
}
|
||||||
|
const gid = String(this.filters.groupId || '').trim()
|
||||||
|
if (gid) filter.groupIds = [gid]
|
||||||
|
const name = String(this.filters.name || '').trim()
|
||||||
|
if (name) filter.name = name
|
||||||
|
if (this.filters.status) filter.statuses = [this.filters.status]
|
||||||
|
return {
|
||||||
|
filter,
|
||||||
|
pageNumber: this.filters.pageNumber,
|
||||||
|
pageSize: this.filters.pageSize,
|
||||||
|
sortBy: this.filters.sortBy,
|
||||||
|
sortOrder: this.filters.sortOrder
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async searchAssets(page = this.filters.pageNumber) {
|
||||||
|
this.filters.pageNumber = this.clampAssetPage(page)
|
||||||
|
this.listLoading = true
|
||||||
|
this.thumbError = {}
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_LIST_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: this.buildListPayload()
|
||||||
|
})
|
||||||
|
this.totalCount = byteApiTotalCount(res?.data)
|
||||||
|
this.items = byteApiItems(res?.data)
|
||||||
|
if (res.code !== 200) {
|
||||||
|
this.$message.error(res.msg || '查询素材失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询素材失败')
|
||||||
|
} finally {
|
||||||
|
this.listLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPageSizeChange(size) {
|
||||||
|
const s = Number(size) || 10
|
||||||
|
this.filters.pageSize = LIST_PAGE_SIZE_OPTIONS.includes(s) ? s : 10
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
resetFilters() {
|
||||||
|
const gid = this.groups.length
|
||||||
|
? String(this.groups[0].Id || this.groups[0].id || '')
|
||||||
|
: String(this.selectedGroupId || '')
|
||||||
|
this.selectedGroupId = gid
|
||||||
|
this.createForm.groupId = gid
|
||||||
|
this.filters = {
|
||||||
|
groupId: gid,
|
||||||
|
name: '',
|
||||||
|
status: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortBy: 'CreateTime',
|
||||||
|
sortOrder: 'Desc'
|
||||||
|
}
|
||||||
|
this.searchAssets(1)
|
||||||
|
},
|
||||||
|
async getAsset(it) {
|
||||||
|
const id = it?.Id || it?.id
|
||||||
|
if (!id) return
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_GET_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: id }
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.detailData = res.data || {}
|
||||||
|
this.detailVisible = true
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询详情失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '查询详情失败')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteAsset(it) {
|
||||||
|
const id = it?.Id || it?.id
|
||||||
|
if (!id) return
|
||||||
|
this.$confirm({
|
||||||
|
title: '删除素材',
|
||||||
|
content: `确认删除素材 ${id} 吗?`,
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: ASSET_DELETE_API,
|
||||||
|
method: 'POST',
|
||||||
|
data: { id: id }
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.$message.success('删除成功')
|
||||||
|
this.searchAssets(this.filters.pageNumber)
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error(e?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.asset-manage-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #0a0b0d;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
.asset-left,
|
||||||
|
.asset-right {
|
||||||
|
background: rgba(22, 24, 30, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.asset-right {
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.panel-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.asset-left-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.group-tree {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
max-height: 72vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.group-node {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.group-node:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
.group-node.active {
|
||||||
|
border-color: rgba(0, 202, 224, 0.45);
|
||||||
|
background: rgba(0, 202, 224, 0.12);
|
||||||
|
}
|
||||||
|
.folder-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.group-name {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.group-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.field label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
.field.actions {
|
||||||
|
align-self: end;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.mtop {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.total-line {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
.table-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.asset-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(1),
|
||||||
|
.asset-table td:nth-child(1) {
|
||||||
|
width: 88px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(2),
|
||||||
|
.asset-table td:nth-child(2) {
|
||||||
|
width: 14%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(3),
|
||||||
|
.asset-table td:nth-child(3) {
|
||||||
|
width: 30%;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(4),
|
||||||
|
.asset-table td:nth-child(4) {
|
||||||
|
width: 10%;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(5),
|
||||||
|
.asset-table td:nth-child(5) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(6),
|
||||||
|
.asset-table td:nth-child(6) {
|
||||||
|
width: 9%;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(7),
|
||||||
|
.asset-table td:nth-child(7) {
|
||||||
|
width: 14%;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.asset-table th:nth-child(8),
|
||||||
|
.asset-table td:nth-child(8) {
|
||||||
|
width: 16%;
|
||||||
|
}
|
||||||
|
.asset-table th,
|
||||||
|
.asset-table td {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.asset-table th {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
.asset-url-cell {
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.asset-thumb-link {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.asset-url-thumb {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 120px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
.asset-url-video {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 160px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
.asset-url-plain {
|
||||||
|
color: rgba(0, 202, 224, 0.95);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.asset-url-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
.asset-url-caption {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.pager {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.empty-tip {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.detail-box {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 8px;
|
||||||
|
max-height: 420px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #d8f4f7;
|
||||||
|
}
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.asset-manage-page {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
/* 弹层挂载到 body,与深色页搭配的标签可读性 */
|
||||||
|
.asset-create-modal-form.arco-form .arco-form-item-label-col .arco-form-item-label,
|
||||||
|
.ag-create-modal-form.arco-form .arco-form-item-label-col .arco-form-item-label {
|
||||||
|
color: var(--color-text-2, rgba(255, 255, 255, 0.78));
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.18);
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone--dragover {
|
||||||
|
border-color: rgba(0, 202, 224, 0.65);
|
||||||
|
background: rgba(0, 202, 224, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid rgba(0, 202, 224, 0.35);
|
||||||
|
background: rgba(0, 202, 224, 0.08);
|
||||||
|
color: rgba(0, 202, 224, 0.8);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-dropzone-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-preview {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 160px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-preview img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-file-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -68,11 +68,28 @@
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="text">
|
<!-- 模型选择 -->
|
||||||
<a-textarea
|
<div class="model-select">
|
||||||
v-model="text"
|
<div class="model-title">{{ $t('common.selectModel') || '选择模型' }}</div>
|
||||||
:placeholder="$t('common.textVideoPlaceholder')" />
|
<a-select v-model="selectedModel" style="width: 100%;">
|
||||||
</div> -->
|
<a-option
|
||||||
|
v-for="option in modelOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本编辑器 -->
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editorContent"
|
||||||
|
:placeholder="$t('common.textVideoPlaceholder') || '请输入文本生成视频...'"
|
||||||
|
:uploaded-images="uploadedImages"
|
||||||
|
@text-change="handleTextChange"
|
||||||
|
@image-upload="handleImageUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
<mf-button
|
<mf-button
|
||||||
class="submit"
|
class="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -175,6 +192,8 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -182,6 +201,7 @@ export default {
|
||||||
firstUrl: '',
|
firstUrl: '',
|
||||||
lastUrl: '',
|
lastUrl: '',
|
||||||
text: '',
|
text: '',
|
||||||
|
editorContent: '',
|
||||||
interval: null,
|
interval: null,
|
||||||
videoUrl: null,
|
videoUrl: null,
|
||||||
videoLoading: false,
|
videoLoading: false,
|
||||||
|
|
@ -202,9 +222,20 @@ export default {
|
||||||
templateLoading: false,
|
templateLoading: false,
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
selectedTemplatePreview: '', // 选中的模版预览图URL
|
selectedTemplatePreview: '', // 选中的模版预览图URL
|
||||||
maxPollAttempts: 40 // 最大轮询次数(3秒 * 40 = 120秒超时)
|
maxPollAttempts: 40, // 最大轮询次数(3秒 * 40 = 120秒超时)
|
||||||
|
// 模型选择
|
||||||
|
modelOptions: [
|
||||||
|
{ label: 'Seedance 2.0', value: 'ep-20260326165811-dlkth' },
|
||||||
|
{ label: 'Seedance 2.0 Fast', value: 'ep-20260326170056-dkj9m' }
|
||||||
|
],
|
||||||
|
selectedModel: 'ep-20260326165811-dlkth',
|
||||||
|
// 已上传的图片列表(用于@功能)
|
||||||
|
uploadedImages: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
RichTextEditor
|
||||||
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
this.destroyInterval()
|
this.destroyInterval()
|
||||||
},
|
},
|
||||||
|
|
@ -316,8 +347,25 @@ export default {
|
||||||
// 繁体中文显示 chineseContent,其他语言显示 englishContent
|
// 繁体中文显示 chineseContent,其他语言显示 englishContent
|
||||||
if (this.lang === 'zh_HK') {
|
if (this.lang === 'zh_HK') {
|
||||||
return template.chineseContent || template.name || ''
|
return template.chineseContent || template.name || ''
|
||||||
|
}else{
|
||||||
|
return template.englishContent || template.name || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 处理富文本内容变化
|
||||||
|
handleTextChange(content) {
|
||||||
|
this.editorContent = content
|
||||||
|
// 同步到旧的 text 字段以保持兼容性
|
||||||
|
this.text = content
|
||||||
|
},
|
||||||
|
// 处理上传的图片(用于@功能)
|
||||||
|
handleImageUpload(imageInfo) {
|
||||||
|
if (imageInfo && imageInfo.url) {
|
||||||
|
this.uploadedImages.push({
|
||||||
|
url: imageInfo.url,
|
||||||
|
name: imageInfo.name || 'image'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return template.englishContent || template.name || ''
|
|
||||||
},
|
},
|
||||||
// 确认选择模板
|
// 确认选择模板
|
||||||
handleConfirmTemplate() {
|
handleConfirmTemplate() {
|
||||||
|
|
@ -477,9 +525,10 @@ export default {
|
||||||
|
|
||||||
this.generateLoading = true
|
this.generateLoading = true
|
||||||
let params = {
|
let params = {
|
||||||
text: this.text,
|
text: this.editorContent || this.text,
|
||||||
firstUrl: firstImageUrl,
|
firstUrl: firstImageUrl,
|
||||||
functionType: '21'
|
functionType: '21',
|
||||||
|
model: this.selectedModel // 新增模型参数
|
||||||
}
|
}
|
||||||
if (this.lastUrl && this.lastUrl.url) {
|
if (this.lastUrl && this.lastUrl.url) {
|
||||||
params.lastUrl = this.lastUrl.url
|
params.lastUrl = this.lastUrl.url
|
||||||
|
|
@ -601,8 +650,76 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-title {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-select) {
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 富文本编辑器样式 */
|
||||||
|
.rich-editor-root {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-placeholder]:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #5c5d68;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&-title {
|
&-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,960 @@
|
||||||
|
<template>
|
||||||
|
<div class="generated-assets-page">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="panel-title">作品库</div>
|
||||||
|
</div>
|
||||||
|
<div class="view-tabs">
|
||||||
|
<a-tabs v-model="activeTab" @change="handleTabChange" type="card">
|
||||||
|
<a-tab-pane key="personal" title="个人" />
|
||||||
|
<a-tab-pane key="department" title="部门" />
|
||||||
|
</a-tabs>
|
||||||
|
</div>
|
||||||
|
<!-- 查询区域 -->
|
||||||
|
<div class="query-section">
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="field">
|
||||||
|
<label>收藏状态</label>
|
||||||
|
<a-select v-model="filters.isTop" clearable placeholder="全部">
|
||||||
|
<a-option :value="null">全部</a-option>
|
||||||
|
<a-option value="Y">已收藏</a-option>
|
||||||
|
<a-option value="N">未收藏</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>开始时间</label>
|
||||||
|
<a-date-picker v-model="filters.beginTime" placeholder="选择开始时间" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>结束时间</label>
|
||||||
|
<a-date-picker v-model="filters.endTime" placeholder="选择结束时间" style="width: 100%" />
|
||||||
|
</div>
|
||||||
|
<div class="field actions">
|
||||||
|
<a-button type="primary" :loading="loading" @click="search(1)">查询</a-button>
|
||||||
|
<a-button @click="resetFilters">重置</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格区域 -->
|
||||||
|
<a-spin :loading="loading">
|
||||||
|
<div class="total-line">总数:{{ pagination.total }}</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table class="asset-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>订单编号</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>模型参数</th>
|
||||||
|
<th>生成结果</th>
|
||||||
|
<th>创建时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in dataList" :key="item.id">
|
||||||
|
<td class="td-id">{{ item.id }}</td>
|
||||||
|
<td>{{ item.orderNum || '-' }}</td>
|
||||||
|
<td>{{ formatType(item.type) }}</td>
|
||||||
|
<td>
|
||||||
|
<a-tag :color="getStatusColor(item.status)">
|
||||||
|
{{ formatStatus(item.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="params-cell">
|
||||||
|
<div v-if="item.model" class="param-tag">模型: {{ item.model }}</div>
|
||||||
|
<div v-if="item.duration" class="param-tag">时长: {{ item.duration }}s</div>
|
||||||
|
<div v-if="item.resolution" class="param-tag">分辨率: {{ item.resolution }}</div>
|
||||||
|
<div v-if="item.ratio" class="param-tag">比例: {{ item.ratio }}</div>
|
||||||
|
<div v-if="item.mode" class="param-tag">模式: {{ formatMode(item.mode) }}</div>
|
||||||
|
<div v-if="!item.model && !item.duration && !item.resolution && !item.ratio && !item.mode" class="param-empty">-</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="result-cell">
|
||||||
|
<div v-if="isVideoResult(item.result)" class="media-preview">
|
||||||
|
<video
|
||||||
|
class="video-thumb"
|
||||||
|
:src="item.result"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
@click.stop="openPreview(item.result, 'video')" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="isImageResult(item.result)" class="media-preview">
|
||||||
|
<img
|
||||||
|
class="image-thumb"
|
||||||
|
:src="item.result"
|
||||||
|
@click.stop="openPreview(item.result, 'image')" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.result && item.result.startsWith('cgt-')" class="task-id">
|
||||||
|
{{ item.result }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="result-empty">-</div>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.createTime || '-' }}</td>
|
||||||
|
<td>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
:type="item.isTop === 'Y' ? 'primary' : 'outline'"
|
||||||
|
:status="item.isTop === 'Y' ? 'success' : 'default'"
|
||||||
|
@click="toggleFavorite(item)">
|
||||||
|
<template v-if="item.isTop === 'Y'">
|
||||||
|
<a-icon name="star-fill" /> 已收藏
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-icon name="star" /> 收藏
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="mini"
|
||||||
|
type="outline"
|
||||||
|
@click="viewDetail(item)">
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!dataList.length">
|
||||||
|
<td colspan="8" class="empty-tip">暂无数据</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="pager">
|
||||||
|
<a-pagination
|
||||||
|
:total="pagination.total"
|
||||||
|
:current="pagination.pageNum"
|
||||||
|
:page-size="pagination.pageSize"
|
||||||
|
show-total
|
||||||
|
show-jumper
|
||||||
|
show-page-size
|
||||||
|
:page-size-options="pagination.pageSizes"
|
||||||
|
@change="changePage"
|
||||||
|
@page-size-change="changePageSize" />
|
||||||
|
</div>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<!-- 详情弹窗 -->
|
||||||
|
<a-modal v-model:visible="detailVisible" title="订单详情" :footer="false" width="850px">
|
||||||
|
<div class="detail-content" v-if="detailData">
|
||||||
|
<div class="detail-form">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">基本信息</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">订单编号</div>
|
||||||
|
<div class="value">{{ detailData.orderNum || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">功能类型</div>
|
||||||
|
<div class="value">{{ formatType(detailData.type) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">扣除积分</div>
|
||||||
|
<div class="value">{{ detailData.amount ? detailData.amount + ' 积分' : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">订单状态</div>
|
||||||
|
<div class="value">
|
||||||
|
<a-tag :color="getStatusColor(detailData.status)">
|
||||||
|
{{ formatStatus(detailData.status) }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">执行状态</div>
|
||||||
|
<div class="value">{{ formatExtStatus(detailData.extStatus) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">收藏状态</div>
|
||||||
|
<div class="value">
|
||||||
|
<a-tag v-if="detailData.isTop === 'Y'" color="orange">已收藏</a-tag>
|
||||||
|
<span v-else class="text-muted">未收藏</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">请求来源</div>
|
||||||
|
<div class="value">{{ detailData.source || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成参数 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">生成参数</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">提示词</div>
|
||||||
|
<div class="value long-text">{{ detailData.text || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">生成模式</div>
|
||||||
|
<div class="value">{{ formatMode(detailData.mode) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">使用模型</div>
|
||||||
|
<div class="value">{{ detailData.model || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">视频时长</div>
|
||||||
|
<div class="value">{{ detailData.duration ? detailData.duration + ' 秒' : '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">分辨率</div>
|
||||||
|
<div class="value">{{ detailData.resolution || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">画面比例</div>
|
||||||
|
<div class="value">{{ detailData.ratio || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 生成结果 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">生成结果</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">生成内容</div>
|
||||||
|
<div class="value media-preview">
|
||||||
|
<video
|
||||||
|
v-if="isVideoResult(detailData.result)"
|
||||||
|
class="detail-video"
|
||||||
|
:src="detailData.result"
|
||||||
|
controls
|
||||||
|
preload="metadata" />
|
||||||
|
<img
|
||||||
|
v-else-if="isImageResult(detailData.result)"
|
||||||
|
class="detail-image"
|
||||||
|
:src="detailData.result"
|
||||||
|
@click="viewImageFull(detailData.result)" />
|
||||||
|
<div v-else class="result-text">{{ detailData.result || '无结果' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row" v-if="detailData.img1">
|
||||||
|
<div class="label">首帧图片</div>
|
||||||
|
<div class="value media-preview">
|
||||||
|
<img class="detail-image small" :src="detailData.img1" @click="viewImageFull(detailData.img1)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row" v-if="detailData.img2">
|
||||||
|
<div class="label">尾帧图片</div>
|
||||||
|
<div class="value media-preview">
|
||||||
|
<img class="detail-image small" :src="detailData.img2" @click="viewImageFull(detailData.img2)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 完整参数 -->
|
||||||
|
<div class="detail-group" v-if="detailData.videoParams">
|
||||||
|
<div class="group-title">完整参数</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">JSON参数</div>
|
||||||
|
<div class="value">
|
||||||
|
<pre class="json-block">{{ formatVideoParams(detailData.videoParams) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间信息 -->
|
||||||
|
<div class="detail-group">
|
||||||
|
<div class="group-title">时间信息</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">创建时间</div>
|
||||||
|
<div class="value">{{ detailData.createTime || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="detail-row">
|
||||||
|
<div class="label">更新时间</div>
|
||||||
|
<div class="value">{{ detailData.updateTime || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 预览弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="previewVisible"
|
||||||
|
:title="previewType === 'video' ? '视频预览' : '图片预览'"
|
||||||
|
:footer="false"
|
||||||
|
width="800px"
|
||||||
|
@cancel="closePreview">
|
||||||
|
<div class="preview-content">
|
||||||
|
<video
|
||||||
|
v-if="previewType === 'video' && previewUrl"
|
||||||
|
class="preview-video"
|
||||||
|
:src="previewUrl"
|
||||||
|
controls
|
||||||
|
autoplay />
|
||||||
|
<img
|
||||||
|
v-else-if="previewType === 'image' && previewUrl"
|
||||||
|
class="preview-image"
|
||||||
|
:src="previewUrl" />
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'GeneratedAssets',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dataList: [],
|
||||||
|
filters: {
|
||||||
|
isTop: null,
|
||||||
|
beginTime: '',
|
||||||
|
endTime: ''
|
||||||
|
},
|
||||||
|
activeTab: 'personal', // 'personal'个人, 'department'部门
|
||||||
|
pagination: {
|
||||||
|
total: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizes: [10, 20, 50, 100]
|
||||||
|
},
|
||||||
|
detailVisible: false,
|
||||||
|
detailData: null,
|
||||||
|
previewVisible: false,
|
||||||
|
previewUrl: '',
|
||||||
|
previewType: 'video'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.search()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 查询数据
|
||||||
|
search(page = 1) {
|
||||||
|
this.pagination.pageNum = page
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
pageNum: this.pagination.pageNum,
|
||||||
|
pageSize: this.pagination.pageSize
|
||||||
|
}
|
||||||
|
console.log("this.activeTab", this.activeTab)
|
||||||
|
// 视图模式参数:personal = 个人, department = 部门视图(传 dept=true)
|
||||||
|
if (this.activeTab === 'department') {
|
||||||
|
params.dept = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 明确传递所有查询条件 - 使用 is_top 作为参数名
|
||||||
|
if (this.filters.isTop != null && String(this.filters.isTop) !== '') {
|
||||||
|
// 如果isTop是null或者空字符串或者全部,则不传递
|
||||||
|
if (this.filters.isTop === null || this.filters.isTop === '' || this.filters.isTop === '全部') {
|
||||||
|
delete params.is_top
|
||||||
|
} else {
|
||||||
|
params.is_top = String(this.filters.isTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.filters.beginTime) {
|
||||||
|
params.beginTime = this.formatDate(this.filters.beginTime)
|
||||||
|
}
|
||||||
|
if (this.filters.endTime) {
|
||||||
|
params.endTime = this.formatDate(this.filters.endTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.$axios({
|
||||||
|
url: 'api/portal/assets/list',
|
||||||
|
method: 'GET',
|
||||||
|
data: params
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
this.loading = false
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.dataList = res.rows || []
|
||||||
|
this.pagination.total = res.total || 0
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '查询失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.loading = false
|
||||||
|
this.$message.error(err?.message || '查询失败')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 重置筛选
|
||||||
|
resetFilters() {
|
||||||
|
this.filters.isTop = null
|
||||||
|
this.filters.beginTime = ''
|
||||||
|
this.filters.endTime = ''
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
// Tabs 切换处理(personal/部门视图)
|
||||||
|
handleTabChange(tabKey) {
|
||||||
|
if (tabKey) {
|
||||||
|
this.activeTab = tabKey
|
||||||
|
}
|
||||||
|
this.pagination.pageNum = 1
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
// 分页变化
|
||||||
|
changePage(page) {
|
||||||
|
this.pagination.pageNum = page
|
||||||
|
this.search(page)
|
||||||
|
},
|
||||||
|
// 每页条数变化
|
||||||
|
changePageSize(pageSize) {
|
||||||
|
this.pagination.pageSize = pageSize
|
||||||
|
this.pagination.pageNum = 1
|
||||||
|
this.search(1)
|
||||||
|
},
|
||||||
|
// 格式化日期
|
||||||
|
formatDate(date) {
|
||||||
|
if (!date) return ''
|
||||||
|
const d = new Date(date)
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
},
|
||||||
|
// 格式化类型
|
||||||
|
formatType(type) {
|
||||||
|
const typeMap = {
|
||||||
|
'11': '图生图',
|
||||||
|
'12': '图生图2',
|
||||||
|
'13': '换脸',
|
||||||
|
'1': '快速生图',
|
||||||
|
'21': '快速生视频'
|
||||||
|
}
|
||||||
|
return typeMap[String(type)] || type || '-'
|
||||||
|
},
|
||||||
|
// 格式化状态
|
||||||
|
formatStatus(status) {
|
||||||
|
const statusMap = {
|
||||||
|
0: '进行中',
|
||||||
|
1: '已完成',
|
||||||
|
2: '失败'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status || '-'
|
||||||
|
},
|
||||||
|
// 格式化扩展状态
|
||||||
|
formatExtStatus(extStatus) {
|
||||||
|
const statusMap = {
|
||||||
|
'running': '执行中',
|
||||||
|
'queued': '队列中',
|
||||||
|
'succeeded': '已完成',
|
||||||
|
'failed': '失败',
|
||||||
|
'cancelled': '已取消',
|
||||||
|
'expired': '超时'
|
||||||
|
}
|
||||||
|
return statusMap[extStatus] || '未知'
|
||||||
|
},
|
||||||
|
// 获取状态颜色
|
||||||
|
getStatusColor(status) {
|
||||||
|
const colorMap = {
|
||||||
|
0: 'blue',
|
||||||
|
1: 'green',
|
||||||
|
2: 'red'
|
||||||
|
}
|
||||||
|
return colorMap[status] || 'default'
|
||||||
|
},
|
||||||
|
// 格式化模式
|
||||||
|
formatMode(mode) {
|
||||||
|
const modeMap = {
|
||||||
|
'text-to-video': '文生视频',
|
||||||
|
'image-first-frame': '图生视频·首帧',
|
||||||
|
'image-first-last-frame': '图生视频·首尾帧',
|
||||||
|
'image-reference': '图生视频·参考图'
|
||||||
|
}
|
||||||
|
return modeMap[mode] || mode || '-'
|
||||||
|
},
|
||||||
|
// 格式化videoParams
|
||||||
|
formatVideoParams(params) {
|
||||||
|
if (!params) return ''
|
||||||
|
try {
|
||||||
|
const obj = typeof params === 'string' ? JSON.parse(params) : params
|
||||||
|
return JSON.stringify(obj, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 判断是否为视频结果
|
||||||
|
isVideoResult(url) {
|
||||||
|
if (!url) return false
|
||||||
|
return /\.(mp4|mov|webm|ogg|m4v|avi|mkv)(\?.*)?$/i.test(url)
|
||||||
|
},
|
||||||
|
// 判断是否为图片结果
|
||||||
|
isImageResult(url) {
|
||||||
|
if (!url) return false
|
||||||
|
return /\.(jpeg|jpg|png|gif|webp|bmp)(\?.*)?$/i.test(url)
|
||||||
|
},
|
||||||
|
// 打开预览
|
||||||
|
openPreview(url, type) {
|
||||||
|
this.previewUrl = url
|
||||||
|
this.previewType = type
|
||||||
|
this.previewVisible = true
|
||||||
|
},
|
||||||
|
// 关闭预览
|
||||||
|
closePreview() {
|
||||||
|
this.previewVisible = false
|
||||||
|
this.previewUrl = ''
|
||||||
|
},
|
||||||
|
// 查看图片全屏
|
||||||
|
viewImageFull(url) {
|
||||||
|
this.$viewerApi({
|
||||||
|
options: {
|
||||||
|
initialViewIndex: 0,
|
||||||
|
toolbar: true
|
||||||
|
},
|
||||||
|
images: [url]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 查看详情
|
||||||
|
viewDetail(item) {
|
||||||
|
this.detailData = item
|
||||||
|
this.detailVisible = true
|
||||||
|
},
|
||||||
|
// 切换收藏状态
|
||||||
|
toggleFavorite(item) {
|
||||||
|
const newIsTop = item.isTop === 'Y' ? 'N' : 'Y'
|
||||||
|
this.$axios({
|
||||||
|
url: '/api/portal/assets/favorite',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
id: item.id,
|
||||||
|
isTop: newIsTop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res.code === 200) {
|
||||||
|
item.isTop = newIsTop
|
||||||
|
this.$message.success(newIsTop === 'Y' ? '收藏成功' : '取消收藏成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '操作失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.$message.error(err?.message || '操作失败')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.generated-assets-page {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 100%;
|
||||||
|
background: #0a0b0d;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-section {
|
||||||
|
background: rgba(22, 24, 30, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.actions {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-line {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列宽定义
|
||||||
|
th:nth-child(1), td:nth-child(1) { width: 60px; }
|
||||||
|
th:nth-child(2), td:nth-child(2) { width: 120px; }
|
||||||
|
th:nth-child(3), td:nth-child(3) { width: 80px; }
|
||||||
|
th:nth-child(4), td:nth-child(4) { width: 80px; }
|
||||||
|
th:nth-child(5), td:nth-child(5) { width: 180px; }
|
||||||
|
th:nth-child(6), td:nth-child(6) { width: 200px; }
|
||||||
|
th:nth-child(7), td:nth-child(7) { width: 140px; }
|
||||||
|
th:nth-child(8), td:nth-child(8) { width: 140px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.td-id {
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.param-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-cell {
|
||||||
|
.media-preview {
|
||||||
|
.video-thumb {
|
||||||
|
width: 160px;
|
||||||
|
height: 90px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-id {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-empty {
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pager {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-tip {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 详情弹窗 - label:value 形式(深色背景白字) */
|
||||||
|
.detail-content {
|
||||||
|
padding: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #1a1f2e;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
.detail-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-group {
|
||||||
|
background: rgba(22, 24, 30, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #00cae0;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(0, 202, 224, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 100px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 13px;
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
color: rgba(255, 255, 255, 0.95);
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
&.long-text {
|
||||||
|
max-height: 110px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-video {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 260px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.03);
|
||||||
|
box-shadow: 0 0 0 3px rgba(0, 202, 224, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
max-height: 160px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-text {
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
font-style: italic;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-block {
|
||||||
|
background: #1a1f2e;
|
||||||
|
color: #a5d6ff;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: 'Consolas', monospace;
|
||||||
|
line-height: 1.4;
|
||||||
|
border: 1px solid rgba(165, 214, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览弹窗样式
|
||||||
|
// Tabs 样式 - 深色主题卡片风格(适配 Arco Design Vue + 项目深色UI)
|
||||||
|
.view-tabs {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
::v-deep(.arco-tabs) {
|
||||||
|
.arco-tabs-nav {
|
||||||
|
background: rgba(22, 24, 30, 0.98) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
padding: 6px !important;
|
||||||
|
margin-bottom: 0;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-tab {
|
||||||
|
color: rgba(255, 255, 255, 0.85) !important;
|
||||||
|
margin: 0 4px !important;
|
||||||
|
padding: 10px 28px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-tab:hover {
|
||||||
|
color: white !important;
|
||||||
|
background: rgba(0, 102, 204, 0.25) !important;
|
||||||
|
border-color: rgba(0, 102, 204, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-tab-active {
|
||||||
|
color: white !important;
|
||||||
|
background: #0066cc !important;
|
||||||
|
border-color: #0066cc !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 102, 204, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-tabs-ink-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前 tabs 仅用于视图切换,不需要内容区域,避免出现白色空框
|
||||||
|
.arco-tabs-content {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-field {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.preview-video {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 作品详情弹窗深色底样式 - 严格符合 GeneratedAssets.vue 深色主题(适配 Arco Design) */
|
||||||
|
::v-deep(.arco-modal) {
|
||||||
|
.arco-modal-content {
|
||||||
|
background-color: #1a1f2e !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-header {
|
||||||
|
background: transparent !important;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-title {
|
||||||
|
color: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-close {
|
||||||
|
color: rgba(255, 255, 255, 0.65) !important;
|
||||||
|
top: 18px;
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #fff !important;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arco-modal-body {
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 detail-content 与页面卡片风格一致(深色背景白字)
|
||||||
|
.detail-content {
|
||||||
|
background: #1a1f2e !important;
|
||||||
|
color: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
padding: 20px !important;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -61,13 +61,28 @@
|
||||||
</a-radio>
|
</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div
|
<!-- 模型选择 -->
|
||||||
class="text"
|
<div class="model-select">
|
||||||
v-if="current == 2">
|
<div class="model-title">{{ $t('common.selectModel') || '选择模型' }}</div>
|
||||||
<a-textarea
|
<a-select v-model="selectedModel" style="width: 100%;">
|
||||||
v-model="text"
|
<a-option
|
||||||
:placeholder="$t('common.textPlaceholder')" />
|
v-for="option in modelOptions"
|
||||||
</div> -->
|
:key="option.value"
|
||||||
|
:value="option.value">
|
||||||
|
{{ option.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本编辑器 -->
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editorContent"
|
||||||
|
:placeholder="$t('common.textPlaceholder') || '请输入文本生成图片...'"
|
||||||
|
:uploaded-images="uploadedImages"
|
||||||
|
@text-change="handleTextChange"
|
||||||
|
@image-upload="handleImageUpload"
|
||||||
|
/>
|
||||||
|
|
||||||
<mf-button
|
<mf-button
|
||||||
class="submit"
|
class="submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -157,6 +172,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
|
|
@ -179,9 +195,20 @@ export default {
|
||||||
templateList: [],
|
templateList: [],
|
||||||
templateLoading: false,
|
templateLoading: false,
|
||||||
selectedTemplate: null,
|
selectedTemplate: null,
|
||||||
selectedTemplatePreview: '' // 选中的模版预览图URL
|
selectedTemplatePreview: '', // 选中的模版预览图URL
|
||||||
|
// 模型选择 - 支持 Seedance 2.0
|
||||||
|
modelOptions: [
|
||||||
|
{ label: 'Seedance 2.0', value: 'ep-20260326165811-dlkth' },
|
||||||
|
{ label: 'Seedance 2.0 Fast', value: 'ep-20260326170056-dkj9m' }
|
||||||
|
],
|
||||||
|
selectedModel: 'ep-20260326165811-dlkth',
|
||||||
|
// 已上传的图片列表(用于@功能)
|
||||||
|
uploadedImages: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
RichTextEditor
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['lang']),
|
...mapGetters(['lang']),
|
||||||
},
|
},
|
||||||
|
|
@ -333,6 +360,21 @@ export default {
|
||||||
this.templateDialogVisible = false
|
this.templateDialogVisible = false
|
||||||
// 不重置selectedTemplate,保持用户的选择状态,方便二次选择
|
// 不重置selectedTemplate,保持用户的选择状态,方便二次选择
|
||||||
},
|
},
|
||||||
|
// 处理富文本内容变化
|
||||||
|
handleTextChange(content) {
|
||||||
|
this.editorContent = content
|
||||||
|
// 同步到旧的 text 字段以保持兼容性
|
||||||
|
this.text = content
|
||||||
|
},
|
||||||
|
// 处理上传的图片(用于@功能)
|
||||||
|
handleImageUpload(imageInfo) {
|
||||||
|
if (imageInfo && imageInfo.url) {
|
||||||
|
this.uploadedImages.push({
|
||||||
|
url: imageInfo.url,
|
||||||
|
name: imageInfo.name || 'image'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
close() {
|
close() {
|
||||||
this.showResult = false
|
this.showResult = false
|
||||||
},
|
},
|
||||||
|
|
@ -396,10 +438,11 @@ export default {
|
||||||
url: 'api/ai/imgToImg',
|
url: 'api/ai/imgToImg',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {
|
data: {
|
||||||
text: this.text,
|
text: this.editorContent || this.text,
|
||||||
firstUrl: this.firstUrl.url,
|
firstUrl: this.firstUrl.url,
|
||||||
functionType: this.current == 1 ? '11' : '12',
|
functionType: this.current == 1 ? '11' : '12',
|
||||||
tags: tags.join(',')
|
tags: tags.join(','),
|
||||||
|
model: this.selectedModel // 新增模型参数
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
|
@ -493,9 +536,77 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload {
|
.upload {
|
||||||
// margin-top: 20px;
|
// margin-top: 20px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-select {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.model-title {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-select) {
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 富文本编辑器样式 */
|
||||||
|
.rich-editor-root {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
|
||||||
|
.tool-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
background-color: rgba(255,255,255,0.05);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
border-color: rgba(255,255,255,0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-editor {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: #1a1b20;
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
outline: none;
|
||||||
|
resize: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: rgb(var(--primary-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-placeholder]:empty::before {
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
color: #5c5d68;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
&-title {
|
&-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,449 @@
|
||||||
|
<template>
|
||||||
|
<div class="tpa-page">
|
||||||
|
<section class="tpa-panel">
|
||||||
|
<div class="tpa-head">
|
||||||
|
<h3 class="tpa-title">三方素材管理</h3>
|
||||||
|
<a-button type="primary" @click="openCreateDialog">新增素材</a-button>
|
||||||
|
</div>
|
||||||
|
<p class="tpa-desc">
|
||||||
|
提交审核通过后,素材会入库并展示在下方列表,可继续查询或删除。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="tpa-filter">
|
||||||
|
<a-input v-model="filters.reviewBatchId" allow-clear placeholder="审核批次 ID" style="width: 220px" />
|
||||||
|
<a-button type="primary" :loading="listLoading" @click="loadList(1)">查询</a-button>
|
||||||
|
<a-button shape="circle" :loading="listLoading" @click="refreshList">
|
||||||
|
<icon-refresh />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="listLoading">
|
||||||
|
<div class="tpa-total">共 {{ total }} 条</div>
|
||||||
|
<div class="tpa-table-wrap">
|
||||||
|
<table class="tpa-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>批次</th>
|
||||||
|
<th>资产ID</th>
|
||||||
|
<th>类型</th>
|
||||||
|
<th>审核状态</th>
|
||||||
|
<th>来源 URL</th>
|
||||||
|
<th>TOS URL</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in rows" :key="row.id">
|
||||||
|
<td>{{ row.id }}</td>
|
||||||
|
<td class="tpa-ellipsis" :title="row.reviewBatchId">{{ row.reviewBatchId }}</td>
|
||||||
|
<td class="tpa-ellipsis" :title="row.assetId">{{ row.assetId || '-' }}</td>
|
||||||
|
<td>{{ formatAssetType(row.assetType) }}</td>
|
||||||
|
<td>{{ formatReviewStatus(row.submitReviewStatus) }}</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="row.sourceUrl" class="tpa-source-preview">
|
||||||
|
<img v-if="isImageType(row.assetType)" :src="row.sourceUrl" alt="来源图片" />
|
||||||
|
<video v-else-if="isVideoType(row.assetType)" :src="row.sourceUrl" controls preload="metadata"></video>
|
||||||
|
<audio v-else-if="isAudioType(row.assetType)" :src="row.sourceUrl" controls preload="none"></audio>
|
||||||
|
<a v-else :href="row.sourceUrl" target="_blank" rel="noopener">打开</a>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</td>
|
||||||
|
<td class="tpa-ellipsis">
|
||||||
|
<a v-if="row.tosUrl" :href="row.tosUrl" target="_blank" rel="noopener">打开</a>
|
||||||
|
<span v-else class="tpa-failed-text">提交失败</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a-button type="outline" status="danger" size="mini" :loading="deletingId === row.id" @click="removeRow(row)">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!rows.length">
|
||||||
|
<td colspan="8" class="tpa-empty">暂无数据</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<a-pagination
|
||||||
|
class="tpa-pagination"
|
||||||
|
:total="total"
|
||||||
|
:current="pageNum"
|
||||||
|
:page-size="pageSize"
|
||||||
|
show-total
|
||||||
|
@change="onPageChange" />
|
||||||
|
</a-spin>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<a-modal v-model:visible="createDialogVisible" title="新增素材" :footer="false" :mask-closable="false" width="680px">
|
||||||
|
<div class="tpa-upload-block">
|
||||||
|
<div class="tpa-type-select">
|
||||||
|
<span class="tpa-type-label">类型选择:</span>
|
||||||
|
<a-radio-group v-model="createMediaType" type="button" @change="onCreateTypeChange">
|
||||||
|
<a-radio value="image">图片</a-radio>
|
||||||
|
<a-radio value="video">视频</a-radio>
|
||||||
|
<a-radio value="audio">音频</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
<div class="tpa-upload-label">添加素材,拖拽或者点击上传</div>
|
||||||
|
<a-upload
|
||||||
|
:key="uploaderKey"
|
||||||
|
draggable
|
||||||
|
multiple
|
||||||
|
:accept="uploadAcceptByType(createMediaType)"
|
||||||
|
:custom-request="customRequest"
|
||||||
|
:show-file-list="true"
|
||||||
|
@change="onUploadChange" />
|
||||||
|
<div class="tpa-upload-actions">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitLoading"
|
||||||
|
:disabled="!pendingUrls.length"
|
||||||
|
@click="submitModeration">
|
||||||
|
提交审核
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="cancelCreateDialog">取消</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ThirdPartyAsset',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
createDialogVisible: false,
|
||||||
|
createMediaType: 'image',
|
||||||
|
uploaderKey: 0,
|
||||||
|
pendingUrls: [],
|
||||||
|
submitLoading: false,
|
||||||
|
listLoading: false,
|
||||||
|
rows: [],
|
||||||
|
total: 0,
|
||||||
|
pageNum: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
filters: {
|
||||||
|
reviewBatchId: ''
|
||||||
|
},
|
||||||
|
deletingId: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadList(1)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isImageType(assetType) {
|
||||||
|
return String(assetType || '').toLowerCase().includes('image')
|
||||||
|
},
|
||||||
|
isAudioType(assetType) {
|
||||||
|
return String(assetType || '').toLowerCase().includes('audio')
|
||||||
|
},
|
||||||
|
isVideoType(assetType) {
|
||||||
|
return String(assetType || '').toLowerCase().includes('video')
|
||||||
|
},
|
||||||
|
formatAssetType(assetType) {
|
||||||
|
if (this.isImageType(assetType)) return '图片'
|
||||||
|
if (this.isAudioType(assetType)) return '音频'
|
||||||
|
if (this.isVideoType(assetType)) return '视频'
|
||||||
|
return '-'
|
||||||
|
},
|
||||||
|
formatReviewStatus(status) {
|
||||||
|
const code = Number(status)
|
||||||
|
if (code === 1) return '审核通过'
|
||||||
|
if (code === 0) return '待审核'
|
||||||
|
return status != null ? String(status) : '-'
|
||||||
|
},
|
||||||
|
openCreateDialog() {
|
||||||
|
this.createDialogVisible = true
|
||||||
|
},
|
||||||
|
onCreateTypeChange() {
|
||||||
|
this.clearPendingUploads()
|
||||||
|
},
|
||||||
|
uploadAcceptByType(mediaType) {
|
||||||
|
if (mediaType === 'audio') return 'audio/*'
|
||||||
|
if (mediaType === 'video') return 'video/*'
|
||||||
|
return 'image/*'
|
||||||
|
},
|
||||||
|
mediaDataKeyByType(mediaType) {
|
||||||
|
if (mediaType === 'audio') return 'audios'
|
||||||
|
if (mediaType === 'video') return 'videos'
|
||||||
|
return 'images'
|
||||||
|
},
|
||||||
|
customRequest(option) {
|
||||||
|
const { onProgress, onError, onSuccess, fileItem } = option
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', fileItem.file)
|
||||||
|
form.append('pathPrefix', 'asset')
|
||||||
|
this.$axios({
|
||||||
|
url: 'api/cos/upload',
|
||||||
|
method: 'post',
|
||||||
|
data: form,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data;boundary=' + new Date().getTime()
|
||||||
|
},
|
||||||
|
onUploadProgress: (evt) => {
|
||||||
|
if (evt.total) onProgress(evt.loaded / evt.total, evt)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (res && res.code === 200 && res.url) {
|
||||||
|
this.pendingUrls.push(res.url)
|
||||||
|
onSuccess(res)
|
||||||
|
} else {
|
||||||
|
const msg = (res && res.msg) || '上传失败'
|
||||||
|
Message.error(msg)
|
||||||
|
onError(new Error(msg))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
onError(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onUploadChange(_list, fileItem) {
|
||||||
|
const st = fileItem && fileItem.status
|
||||||
|
if (st === 'removed' && fileItem.response && fileItem.response.url) {
|
||||||
|
const u = fileItem.response.url
|
||||||
|
const idx = this.pendingUrls.findIndex((x) => x === u)
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.pendingUrls.splice(idx, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearPendingUploads() {
|
||||||
|
this.pendingUrls = []
|
||||||
|
this.uploaderKey += 1
|
||||||
|
},
|
||||||
|
cancelCreateDialog() {
|
||||||
|
this.clearPendingUploads()
|
||||||
|
this.createDialogVisible = false
|
||||||
|
},
|
||||||
|
async submitModeration() {
|
||||||
|
if (!this.pendingUrls.length) {
|
||||||
|
Message.warning('请先上传素材')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
[this.mediaDataKeyByType(this.createMediaType)]: [...this.pendingUrls]
|
||||||
|
}
|
||||||
|
this.submitLoading = true
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/tos/asset',
|
||||||
|
method: 'post',
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
Message.success('已提交审核')
|
||||||
|
this.clearPendingUploads()
|
||||||
|
this.createDialogVisible = false
|
||||||
|
this.loadList(1)
|
||||||
|
} else {
|
||||||
|
Message.error(res.msg || '提交失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(typeof e === 'string' ? e : e?.message || '提交失败')
|
||||||
|
} finally {
|
||||||
|
this.submitLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadList(page) {
|
||||||
|
this.pageNum = page || this.pageNum
|
||||||
|
this.listLoading = true
|
||||||
|
try {
|
||||||
|
const q = {
|
||||||
|
pageNum: this.pageNum,
|
||||||
|
pageSize: this.pageSize
|
||||||
|
}
|
||||||
|
if (this.filters.reviewBatchId) {
|
||||||
|
q.reviewBatchId = this.filters.reviewBatchId
|
||||||
|
}
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: 'api/tos/asset/list',
|
||||||
|
method: 'get',
|
||||||
|
data: q
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
this.rows = res.rows || []
|
||||||
|
this.total = res.total || 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.listLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPageChange(p) {
|
||||||
|
this.pageNum = p
|
||||||
|
this.loadList(p)
|
||||||
|
},
|
||||||
|
refreshList() {
|
||||||
|
this.loadList(this.pageNum || 1)
|
||||||
|
},
|
||||||
|
async removeRow(row) {
|
||||||
|
if (!row || !row.id) return
|
||||||
|
this.deletingId = row.id
|
||||||
|
try {
|
||||||
|
const res = await this.$axios({
|
||||||
|
url: `api/tos/asset/${row.id}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
if (res.code === 200) {
|
||||||
|
Message.success('已删除')
|
||||||
|
this.loadList(this.pageNum)
|
||||||
|
} else {
|
||||||
|
Message.error(res.msg || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Message.error(typeof e === 'string' ? e : e?.message || '删除失败')
|
||||||
|
} finally {
|
||||||
|
this.deletingId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tpa-page {
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.tpa-panel {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
.tpa-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.tpa-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.tpa-desc {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
line-height: 1.5;
|
||||||
|
code {
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tpa-upload-block {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
.tpa-upload-label {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.tpa-type-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.tpa-type-label {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.tpa-upload-actions {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tpa-filter {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.tpa-total {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.tpa-table-wrap {
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(100vh - 360px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.tpa-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 8px 10px;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #6ad0ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tpa-source-preview {
|
||||||
|
width: 140px;
|
||||||
|
min-height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
width: 140px;
|
||||||
|
max-height: 86px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
audio {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tpa-ellipsis {
|
||||||
|
max-width: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tpa-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
padding: 24px !important;
|
||||||
|
}
|
||||||
|
.tpa-failed-text {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.tpa-pagination {
|
||||||
|
margin-top: 16px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -4,7 +4,7 @@
|
||||||
.gradle
|
.gradle
|
||||||
/build/
|
/build/
|
||||||
!gradle/wrapper/gradle-wrapper.jar
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
*.sql
|
||||||
target/
|
target/
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
server_name undressing.top www.undressing.top;
|
||||||
|
index index.html index.htm;
|
||||||
|
ssl_certificate ssl/undressing.top.crt;
|
||||||
|
ssl_certificate_key ssl/undressing.top.key;
|
||||||
|
ssl_session_timeout 5m;
|
||||||
|
# 优化加密套件配置,移除不安全的算法
|
||||||
|
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4:!3DES;
|
||||||
|
# 移除不安全的 TLSv1.1,只保留 TLSv1.2 和 TLSv1.3
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
# 启用 SSL session 缓存,提高性能
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
|
||||||
|
root /data/web/client_web;
|
||||||
|
autoindex off; # 禁用目录列表
|
||||||
|
|
||||||
|
# 安全头部
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
location ~* (\.git|\.env|composer\.json|\.log|\.sql)$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
location ^~ /api/ {
|
||||||
|
proxy_pass http://10.0.0.167:8110;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Real-Port $remote_port;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header REMOTE-HOST $remote_addr;
|
||||||
|
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 600s;
|
||||||
|
proxy_read_timeout 600s;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
# 缓冲配置,提高性能
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_buffer_size 4k;
|
||||||
|
proxy_buffers 8 4k;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
root /data/web/client_web;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -51,6 +51,9 @@ public class AiManagerApiController extends BaseController {
|
||||||
@Anonymous
|
@Anonymous
|
||||||
public AjaxResult selectInfo(String aiType) {
|
public AjaxResult selectInfo(String aiType) {
|
||||||
AiManager aiManager = aiManagerService.selectAiManagerByType(aiType);
|
AiManager aiManager = aiManagerService.selectAiManagerByType(aiType);
|
||||||
|
if (aiManager == null) {
|
||||||
|
return AjaxResult.error("该功能未配置或已停用");
|
||||||
|
}
|
||||||
aiManager.setPrompt(null);
|
aiManager.setPrompt(null);
|
||||||
return AjaxResult.success(aiManager);
|
return AjaxResult.success(aiManager);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,9 @@ package com.ruoyi.api;
|
||||||
|
|
||||||
import com.ruoyi.ai.domain.AiSampleAmount;
|
import com.ruoyi.ai.domain.AiSampleAmount;
|
||||||
import com.ruoyi.ai.domain.AiSampleAmountRecord;
|
import com.ruoyi.ai.domain.AiSampleAmountRecord;
|
||||||
import com.ruoyi.ai.domain.AiStatistics;
|
|
||||||
import com.ruoyi.ai.domain.enums.AiConfigEnum;
|
|
||||||
import com.ruoyi.ai.mapper.AiSampleAmountMapper;
|
import com.ruoyi.ai.mapper.AiSampleAmountMapper;
|
||||||
import com.ruoyi.ai.service.EmailVerifyService;
|
import com.ruoyi.ai.service.EmailVerifyService;
|
||||||
import com.ruoyi.ai.service.IAiSampleAmountRecordService;
|
import com.ruoyi.ai.service.IAiSampleAmountRecordService;
|
||||||
import com.ruoyi.ai.service.IAiStatisticsService;
|
|
||||||
import com.ruoyi.ai.service.IAiUserService;
|
import com.ruoyi.ai.service.IAiUserService;
|
||||||
import com.ruoyi.common.annotation.Anonymous;
|
import com.ruoyi.common.annotation.Anonymous;
|
||||||
import com.ruoyi.common.constant.BalanceChangerConstants;
|
import com.ruoyi.common.constant.BalanceChangerConstants;
|
||||||
|
|
@ -22,10 +19,8 @@ import com.ruoyi.common.core.domain.model.RegisterAiUserBody;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import com.ruoyi.common.exception.job.TaskException;
|
import com.ruoyi.common.exception.job.TaskException;
|
||||||
import com.ruoyi.common.utils.DateUtils;
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
import com.ruoyi.common.utils.IpCountryQueryByApi;
|
|
||||||
import com.ruoyi.common.utils.MessageUtils;
|
import com.ruoyi.common.utils.MessageUtils;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.common.utils.ip.IpUtils;
|
|
||||||
import com.ruoyi.framework.web.service.SysLoginService;
|
import com.ruoyi.framework.web.service.SysLoginService;
|
||||||
import com.ruoyi.quartz.domain.SysJob;
|
import com.ruoyi.quartz.domain.SysJob;
|
||||||
import com.ruoyi.quartz.service.ISysJobService;
|
import com.ruoyi.quartz.service.ISysJobService;
|
||||||
|
|
@ -40,7 +35,10 @@ import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.mail.MessagingException;
|
import javax.mail.MessagingException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -104,8 +102,11 @@ public class AiUserApiController extends BaseController {
|
||||||
// 查询启用状态体验金活动
|
// 查询启用状态体验金活动
|
||||||
AiSampleAmount aiSampleAmount = aiSampleAmountMapper.getSampleAmount();
|
AiSampleAmount aiSampleAmount = aiSampleAmountMapper.getSampleAmount();
|
||||||
if (aiSampleAmount != null) {
|
if (aiSampleAmount != null) {
|
||||||
|
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
|
||||||
|
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||||
|
String orderNo = dateTime + uuid;
|
||||||
// 余额变更记录
|
// 余额变更记录
|
||||||
aiUserService.addUserBalance(aiUser.getId(), aiSampleAmount.getAmount(), BalanceChangerConstants.EXPERIENCE_GOLD_GIFT);
|
aiUserService.addUserBalance(orderNo, aiUser.getId(), aiSampleAmount.getAmount(), BalanceChangerConstants.EXPERIENCE_GOLD_GIFT);
|
||||||
// 新增体验金记录
|
// 新增体验金记录
|
||||||
AiSampleAmountRecord aiSampleAmountRecord = new AiSampleAmountRecord();
|
AiSampleAmountRecord aiSampleAmountRecord = new AiSampleAmountRecord();
|
||||||
aiSampleAmountRecord.setUserId(aiUser.getId());
|
aiSampleAmountRecord.setUserId(aiUser.getId());
|
||||||
|
|
@ -218,6 +219,7 @@ public class AiUserApiController extends BaseController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询返佣比例配置
|
* 查询返佣比例配置
|
||||||
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@GetMapping("/getRebateConfig")
|
@GetMapping("/getRebateConfig")
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import com.ruoyi.common.annotation.Anonymous;
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.domain.model.LoginAiUser;
|
import com.ruoyi.common.core.domain.model.LoginAiUser;
|
||||||
import com.ruoyi.common.utils.AwsS3Util;
|
|
||||||
import com.ruoyi.common.utils.RandomStringUtil;
|
import com.ruoyi.common.utils.RandomStringUtil;
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.TencentCosUtil;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
@ -36,13 +36,19 @@ import java.util.regex.Pattern;
|
||||||
public class ByteApiController extends BaseController {
|
public class ByteApiController extends BaseController {
|
||||||
|
|
||||||
private final IByteService byteService;
|
private final IByteService byteService;
|
||||||
private final AwsS3Util awsS3Util;
|
private final TencentCosUtil tencentCosUtil;
|
||||||
private final IAiOrderService aiOrderService;
|
private final IAiOrderService aiOrderService;
|
||||||
private final IAiManagerService managerService;
|
private final IAiManagerService managerService;
|
||||||
private final IAiTagService aiTagService;
|
private final IAiTagService aiTagService;
|
||||||
@Value("${byteapi.callBackUrl}")
|
@Value("${byteapi.callBackUrl}")
|
||||||
private String url;
|
private String url;
|
||||||
|
|
||||||
|
// 火山引擎配置
|
||||||
|
@Value("${volcengine.ark.baseUrl}")
|
||||||
|
private String volcBaseUrl;
|
||||||
|
@Value("${volcengine.ark.callbackUrl}")
|
||||||
|
private String volcCallbackUrl;
|
||||||
|
|
||||||
@PostMapping("/promptToImg")
|
@PostMapping("/promptToImg")
|
||||||
@ApiOperation("文生图")
|
@ApiOperation("文生图")
|
||||||
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
|
public AjaxResult promptToImg(@RequestBody ByteApiRequest request) {
|
||||||
|
|
@ -51,9 +57,12 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.error("functionType is null");
|
return AjaxResult.error("functionType is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String mode = request.getMode() != null ? request.getMode() : "image-to-video";
|
||||||
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
AiManager aiManager = managerService.selectAiManagerByType(functionType);
|
||||||
String tags = request.getTags();
|
String tags = request.getTags();
|
||||||
String text = "";
|
String text = request.getText();
|
||||||
|
|
||||||
|
// 如果使用标签系统生成prompt
|
||||||
if (StringUtils.isNotEmpty(tags)) {
|
if (StringUtils.isNotEmpty(tags)) {
|
||||||
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
List<AiTag> aiTags = aiTagService.selectAiTagListByIds(request.getTags(), aiManager.getParentIdSort());
|
||||||
List<String> tagPrompts = new ArrayList<>();
|
List<String> tagPrompts = new ArrayList<>();
|
||||||
|
|
@ -70,22 +79,24 @@ public class ByteApiController extends BaseController {
|
||||||
tagPrompts.add(p);
|
tagPrompts.add(p);
|
||||||
}
|
}
|
||||||
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
text = StringUtils.replacePlaceholders(aiManager.getPrompt(), tagPrompts);
|
||||||
} else {
|
|
||||||
text = aiManager.getPrompt();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(text)) {
|
if (StringUtils.isEmpty(text)) {
|
||||||
return AjaxResult.error("text is null");
|
return AjaxResult.error("text is null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||||
try {
|
try {
|
||||||
if (aiOrder == null) {
|
if (aiOrder == null) {
|
||||||
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||||
}
|
}
|
||||||
aiOrder.setText(text);
|
aiOrder.setText(text);
|
||||||
|
aiOrder.setMode(mode); // 记录生成模式
|
||||||
|
|
||||||
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
||||||
byteBodyReq.setModel("ep-20251104104536-2gpgz");
|
// model由前端传入,默认为Seedance 2.0
|
||||||
|
byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ?
|
||||||
|
request.getModel() : "doubao-seedance-2.0");
|
||||||
byteBodyReq.setPrompt(text);
|
byteBodyReq.setPrompt(text);
|
||||||
byteBodyReq.setSequential_image_generation("disabled");
|
byteBodyReq.setSequential_image_generation("disabled");
|
||||||
byteBodyReq.setResponse_format("url");
|
byteBodyReq.setResponse_format("url");
|
||||||
|
|
@ -96,7 +107,7 @@ public class ByteApiController extends BaseController {
|
||||||
List<ByteDataRes> data = byteBodyRes.getData();
|
List<ByteDataRes> data = byteBodyRes.getData();
|
||||||
ByteDataRes byteDataRes = data.get(0);
|
ByteDataRes byteDataRes = data.get(0);
|
||||||
String url = byteDataRes.getUrl();
|
String url = byteDataRes.getUrl();
|
||||||
url = awsS3Util.uploadFileByUrl(url);
|
url = tencentCosUtil.uploadFileByUrl(url);
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
// 判断生成失败,退回金额逻辑
|
// 判断生成失败,退回金额逻辑
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
|
@ -163,7 +174,8 @@ public class ByteApiController extends BaseController {
|
||||||
|
|
||||||
aiOrder.setImg1(firstUrl.toString());
|
aiOrder.setImg1(firstUrl.toString());
|
||||||
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
||||||
byteBodyReq.setModel("ep-20251104104536-2gpgz");
|
byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ?
|
||||||
|
request.getModel() : "doubao-seedance-2.0");
|
||||||
byteBodyReq.setPrompt(text);
|
byteBodyReq.setPrompt(text);
|
||||||
byteBodyReq.setImage(firstUrl);
|
byteBodyReq.setImage(firstUrl);
|
||||||
byteBodyReq.setSequential_image_generation("disabled");
|
byteBodyReq.setSequential_image_generation("disabled");
|
||||||
|
|
@ -175,7 +187,7 @@ public class ByteApiController extends BaseController {
|
||||||
List<ByteDataRes> data = byteBodyRes.getData();
|
List<ByteDataRes> data = byteBodyRes.getData();
|
||||||
ByteDataRes byteDataRes = data.get(0);
|
ByteDataRes byteDataRes = data.get(0);
|
||||||
String url = byteDataRes.getUrl();
|
String url = byteDataRes.getUrl();
|
||||||
url = awsS3Util.uploadFileByUrl(url);
|
url = tencentCosUtil.uploadFileByUrl(url);
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
// 判断生成失败,退回金额逻辑
|
// 判断生成失败,退回金额逻辑
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
|
@ -191,7 +203,7 @@ public class ByteApiController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/imgToVideo")
|
@PostMapping("/imgToVideo")
|
||||||
@ApiOperation("图生视频")
|
@ApiOperation("图生视频 (Seedance 2.0)")
|
||||||
public AjaxResult imgToVideo(@RequestBody ByteApiRequest request) throws Exception {
|
public AjaxResult imgToVideo(@RequestBody ByteApiRequest request) throws Exception {
|
||||||
String functionType = request.getFunctionType();
|
String functionType = request.getFunctionType();
|
||||||
if (null == functionType) {
|
if (null == functionType) {
|
||||||
|
|
@ -235,51 +247,56 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// String text = request.getText();
|
|
||||||
// if (StringUtils.isBlank(text)) {
|
|
||||||
// return AjaxResult.error("text is null");
|
|
||||||
// }
|
|
||||||
// String tags = request.getTags();
|
|
||||||
// if (StringUtils.isNotBlank(tags)) {
|
|
||||||
// text = "(优先考虑以下关键词:" + tags + ")";
|
|
||||||
// }
|
|
||||||
aiOrder.setText(text);
|
aiOrder.setText(text);
|
||||||
|
|
||||||
aiOrder.setImg1(firstUrl.toString());
|
aiOrder.setImg1(firstUrl.toString());
|
||||||
Integer duration = request.getDuration();
|
|
||||||
|
Integer duration = request.getDuration() != null ? request.getDuration() : 4;
|
||||||
|
|
||||||
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
ByteBodyReq byteBodyReq = new ByteBodyReq();
|
||||||
byteBodyReq.setModel("ep-20251113072240-cfxlz");
|
// model由前端传入,默认为Seedance2.0
|
||||||
byteBodyReq.setCallback_url(url + "/api/ai/callBack");
|
byteBodyReq.setModel(StringUtils.isNotEmpty(request.getModel()) ?
|
||||||
List<ContentItem> content = new ArrayList<>();
|
request.getModel() : "doubao-seedance-2.0");
|
||||||
ContentItem contentItem = new ContentItem();
|
byteBodyReq.setCallback_url(volcCallbackUrl);
|
||||||
contentItem.setType("text");
|
|
||||||
contentItem.setText(text + " --dur " + duration + " --fps 24 --rs 720p --wm false --cf false");
|
|
||||||
content.add(contentItem);
|
|
||||||
|
|
||||||
ContentItem contentItem1 = new ContentItem();
|
// 构建符合火山引擎格式的content
|
||||||
contentItem1.setType("image_url");
|
List<ContentItem> contentList = new ArrayList<>();
|
||||||
contentItem1.setRole("first_frame");
|
|
||||||
ImageUrl imageUrl1 = new ImageUrl();
|
// 文本提示词
|
||||||
imageUrl1.setUrl(firstUrl.toString());
|
ContentItem textItem = new ContentItem();
|
||||||
contentItem1.setImageUrl(imageUrl1);
|
textItem.setType("text");
|
||||||
content.add(contentItem1);
|
textItem.setText(text);
|
||||||
|
contentList.add(textItem);
|
||||||
|
|
||||||
|
// 首帧图片
|
||||||
|
ContentItem firstFrameItem = new ContentItem();
|
||||||
|
firstFrameItem.setType("image_url");
|
||||||
|
firstFrameItem.setRole("first_frame");
|
||||||
|
ImageUrl firstImageUrl = new ImageUrl();
|
||||||
|
firstImageUrl.setUrl(firstUrl.toString());
|
||||||
|
firstFrameItem.setImageUrl(firstImageUrl);
|
||||||
|
contentList.add(firstFrameItem);
|
||||||
|
|
||||||
|
// 如果有尾帧
|
||||||
String lastUrl = request.getLastUrl();
|
String lastUrl = request.getLastUrl();
|
||||||
if (StringUtils.isNotBlank(lastUrl)) {
|
if (StringUtils.isNotBlank(lastUrl)) {
|
||||||
ContentItem contentItem2 = new ContentItem();
|
ContentItem lastFrameItem = new ContentItem();
|
||||||
contentItem2.setType("image_url");
|
lastFrameItem.setType("image_url");
|
||||||
contentItem2.setRole("last_frame");
|
lastFrameItem.setRole("last_frame");
|
||||||
ImageUrl imageUrl2 = new ImageUrl();
|
ImageUrl lastImageUrl = new ImageUrl();
|
||||||
imageUrl2.setUrl(lastUrl);
|
lastImageUrl.setUrl(lastUrl);
|
||||||
contentItem2.setImageUrl(imageUrl2);
|
lastFrameItem.setImageUrl(lastImageUrl);
|
||||||
content.add(contentItem2);
|
contentList.add(lastFrameItem);
|
||||||
aiOrder.setImg2(lastUrl);
|
aiOrder.setImg2(lastUrl);
|
||||||
}
|
}
|
||||||
byteBodyReq.setContent(content);
|
|
||||||
|
byteBodyReq.setContent(contentList);
|
||||||
|
byteBodyReq.setDuration(duration);
|
||||||
|
byteBodyReq.setResolution("720p");
|
||||||
|
byteBodyReq.setRatio("3:4");
|
||||||
|
|
||||||
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq);
|
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq);
|
||||||
String id = byteBodyRes.getId();
|
String id = byteBodyRes.getId();
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
// 判断生成失败,退回金额逻辑
|
|
||||||
aiOrderService.orderFailure(aiOrder);
|
aiOrderService.orderFailure(aiOrder);
|
||||||
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
}
|
}
|
||||||
|
|
@ -299,7 +316,7 @@ public class ByteApiController extends BaseController {
|
||||||
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
||||||
content content = byteBodyRes.getContent();
|
content content = byteBodyRes.getContent();
|
||||||
String videoUrl = content.getVideo_url();
|
String videoUrl = content.getVideo_url();
|
||||||
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
|
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
|
||||||
content.setVideo_url(videoUrl);
|
content.setVideo_url(videoUrl);
|
||||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
||||||
AiOrder aiOrder = new AiOrder();
|
AiOrder aiOrder = new AiOrder();
|
||||||
|
|
@ -311,24 +328,131 @@ public class ByteApiController extends BaseController {
|
||||||
return AjaxResult.success(byteBodyRes);
|
return AjaxResult.success(byteBodyRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/callBack")
|
@PostMapping(value = "/volcCallback")
|
||||||
@ApiOperation("视频下载回调")
|
@ApiOperation("火山引擎视频回调")
|
||||||
@Anonymous
|
@Anonymous
|
||||||
public AjaxResult callBack(@PathVariable("id") ByteBodyRes byteBodyRes) throws Exception {
|
public AjaxResult volcCallback(@RequestBody ByteBodyRes byteBodyRes) throws Exception {
|
||||||
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
logger.info("volcCallback 收到回调数据: {}", byteBodyRes);
|
||||||
String id = byteBodyRes.getId();
|
String id = byteBodyRes.getId();
|
||||||
content content = byteBodyRes.getContent();
|
if (StringUtils.isEmpty(id)) {
|
||||||
String videoUrl = content.getVideo_url();
|
logger.warn("volcCallback 无任务 id,跳过业务处理");
|
||||||
videoUrl = awsS3Util.uploadFileByUrl(videoUrl);
|
return AjaxResult.success("callback success");
|
||||||
content.setVideo_url(videoUrl);
|
|
||||||
AiOrder aiOrderByResult = aiOrderService.getAiOrderByResult(id);
|
|
||||||
AiOrder aiOrder = new AiOrder();
|
|
||||||
aiOrder.setId(aiOrderByResult.getId());
|
|
||||||
aiOrder.setResult(videoUrl);
|
|
||||||
// aiOrder.setUpdateBy(SecurityUtils.getLoginAiUser().getUsername());
|
|
||||||
aiOrderService.updateAiOrder(aiOrder);
|
|
||||||
}
|
}
|
||||||
return AjaxResult.success(byteBodyRes);
|
|
||||||
|
Integer code = byteBodyRes.getCode();
|
||||||
|
boolean codeError = code != null && code != 200;
|
||||||
|
String st = byteBodyRes.getStatus();
|
||||||
|
|
||||||
|
if (st != null && !st.isEmpty()) {
|
||||||
|
// 执行中状态
|
||||||
|
Integer extStatus = null;
|
||||||
|
if ("queued".equals(st)) {
|
||||||
|
extStatus = 0;
|
||||||
|
} else if ("running".equals(st)) {
|
||||||
|
extStatus = 1;
|
||||||
|
}
|
||||||
|
if (extStatus != null) {
|
||||||
|
AiOrder order = findAiOrderByVolcTaskId(id);
|
||||||
|
if (order == null) {
|
||||||
|
logger.warn("volcCallback 修改执行中状态,未找到任务对应订单, id={}, {}", id, st);
|
||||||
|
return AjaxResult.success("callback success");
|
||||||
|
}
|
||||||
|
// if (order.getStatus() != 0) {
|
||||||
|
// logger.warn("订单状态不为0, 因此不修改ext_status, id = {}, status = {}, order status = {}", id, st, order.getStatus());
|
||||||
|
// return AjaxResult.success("callback success");
|
||||||
|
// }
|
||||||
|
AiOrder upd = new AiOrder();
|
||||||
|
upd.setId(order.getId());
|
||||||
|
upd.setExtStatus(extStatus);
|
||||||
|
aiOrderService.updateAiOrder(upd);
|
||||||
|
return AjaxResult.success("callback success");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codeError) {
|
||||||
|
markVolcCallbackOrderClearResultFailed(id, "code=" + code);
|
||||||
|
return AjaxResult.success("callback success");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("succeeded".equals(st)) {
|
||||||
|
content contentObj = byteBodyRes.getContent();
|
||||||
|
if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) {
|
||||||
|
String videoUrl = contentObj.getVideo_url();
|
||||||
|
videoUrl = tencentCosUtil.uploadFileByUrl(videoUrl);
|
||||||
|
contentObj.setVideo_url(videoUrl);
|
||||||
|
|
||||||
|
AiOrder aiOrderByResult = findAiOrderByVolcTaskId(id);
|
||||||
|
if (aiOrderByResult != null) {
|
||||||
|
AiOrder aiOrder = new AiOrder();
|
||||||
|
aiOrder.setId(aiOrderByResult.getId());
|
||||||
|
aiOrder.setResult(videoUrl);
|
||||||
|
aiOrder.setStatus(1);
|
||||||
|
aiOrderService.updateAiOrder(aiOrder);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handleVolcCallbackFailure(id, "succeeded但缺少video_url");
|
||||||
|
}
|
||||||
|
return AjaxResult.success("callback success");
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed、canceled 等终态,或 status 与成功/进行中均不一致
|
||||||
|
if (StringUtils.isNotEmpty(st) || code != null) {
|
||||||
|
handleVolcCallbackFailure(id, "status=" + st);
|
||||||
|
} else {
|
||||||
|
logger.warn("volcCallback 未携带可判定的 status/code,id={}", id);
|
||||||
|
}
|
||||||
|
return AjaxResult.success("callback success");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调体 code 非 200(HTTP/业务状态码):清空 result,status=2,不退款。
|
||||||
|
*/
|
||||||
|
private void markVolcCallbackOrderClearResultFailed(String taskId, String reason) {
|
||||||
|
AiOrder order = findAiOrderByVolcTaskId(taskId);
|
||||||
|
if (order == null) {
|
||||||
|
logger.warn("volcCallback code 非 200:未找到任务对应订单, taskId={}, {}", taskId, reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AiOrder upd = new AiOrder();
|
||||||
|
upd.setId(order.getId());
|
||||||
|
upd.setResult("");
|
||||||
|
upd.setStatus(2);
|
||||||
|
aiOrderService.updateAiOrder(upd);
|
||||||
|
logger.warn("volcCallback code 非 200,已清空 result 并 status=2, orderId={}, {}", order.getId(), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AiOrder findAiOrderByVolcTaskId(String taskId) {
|
||||||
|
AiOrder order = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||||
|
if (order != null) {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
return aiOrderService.getAiOrderByResult(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调判定任务失败:订单仍持有火山任务 id、且非已失败时,标记失败并退余额(orderFailure)。
|
||||||
|
*/
|
||||||
|
private void handleVolcCallbackFailure(String taskId, String reason) {
|
||||||
|
AiOrder order = aiOrderService.getAiOrderByResult(taskId);
|
||||||
|
if (order == null) {
|
||||||
|
logger.warn("volcCallback 失败:未找到 result={} 的订单, {}", taskId, reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!taskId.equals(order.getResult())) {
|
||||||
|
logger.info("volcCallback 失败处理跳过:订单结果已更新, taskId={}, {}", taskId, reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(2).equals(order.getStatus())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
aiOrderService.orderFailure(order);
|
||||||
|
logger.warn("volcCallback 任务失败,已标记订单失败并退款, taskId={}, reason={}", taskId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/{id}/cancel")
|
||||||
|
@ApiOperation("取消视频生成任务")
|
||||||
|
public AjaxResult cancelTask(@PathVariable("id") String id) throws Exception {
|
||||||
|
return byteService.cancelVideoTask(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
package com.ruoyi.api;
|
||||||
|
|
||||||
|
import com.ruoyi.ai.service.IByteAssetService;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.request.asset.*;
|
||||||
|
import com.ruoyi.common.core.response.asset.GetAssetResponse;
|
||||||
|
import com.ruoyi.common.core.response.asset.ListAssetsResponse;
|
||||||
|
import com.ruoyi.common.core.response.asset.UpdateAssetResponse;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 火山 - 素材管理(api.docx Assets API)。CreateAsset 先上传存储桶再调三方接口。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/byteAsset")
|
||||||
|
@Api(tags = "火山 - 素材管理")
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class ByteAssetApiController extends BaseController {
|
||||||
|
|
||||||
|
private final IByteAssetService byteAssetService;
|
||||||
|
|
||||||
|
@PostMapping("/createAsset")
|
||||||
|
@ApiOperation("CreateAsset:创建素材")
|
||||||
|
public AjaxResult<ListAssetsResponse> createAsset(@RequestBody CreateAssetRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(byteAssetService.createAsset(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建素材时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/listAssets")
|
||||||
|
@ApiOperation("ListAssets:查询素材列表")
|
||||||
|
public AjaxResult<ListAssetsResponse> listAssets(@RequestBody ListAssetsRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(byteAssetService.listAssets(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询素材列表时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/getAsset")
|
||||||
|
@ApiOperation("GetAsset:查询单个素材")
|
||||||
|
public AjaxResult<GetAssetResponse> getAsset(@RequestBody GetAssetRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(byteAssetService.getAsset(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询素材详情时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateAsset")
|
||||||
|
@ApiOperation("UpdateAsset:更新素材(当前仅名称)")
|
||||||
|
public AjaxResult<UpdateAssetResponse> updateAsset(@RequestBody UpdateAssetRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(byteAssetService.updateAsset(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新素材时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/deleteAsset")
|
||||||
|
@ApiOperation("DeleteAsset:删除素材")
|
||||||
|
public AjaxResult<Void> deleteAsset(@RequestBody DeleteAssetRequest request) {
|
||||||
|
try {
|
||||||
|
byteAssetService.deleteAsset(request);
|
||||||
|
return AjaxResult.success();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("删除素材时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.ruoyi.api;
|
||||||
|
|
||||||
|
import com.ruoyi.ai.service.IByteAssetGroupService;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.request.asset.CreateAssetGroupRequest;
|
||||||
|
import com.ruoyi.common.core.request.asset.GetAssetGroupRequest;
|
||||||
|
import com.ruoyi.common.core.request.asset.ListAssetsGroupRequest;
|
||||||
|
import com.ruoyi.common.core.request.asset.UpdateAssetGroupRequest;
|
||||||
|
import com.ruoyi.common.core.response.asset.CreateAssetGroupResponse;
|
||||||
|
import com.ruoyi.common.core.response.asset.GetAssetGroupResponse;
|
||||||
|
import com.ruoyi.common.core.response.asset.ListAssetsGroupResponse;
|
||||||
|
import com.ruoyi.common.core.response.asset.UpdateAssetGroupResponse;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 火山 - 素材组管理 Controller
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/byteAssetGroup")
|
||||||
|
@Api(tags = "火山 - 素材组管理")
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class ByteAssetGroupApiController extends BaseController {
|
||||||
|
|
||||||
|
private final IByteAssetGroupService groupService;
|
||||||
|
|
||||||
|
@PostMapping("/listAssetGroups")
|
||||||
|
@ApiOperation("ListAssetGroups:查询素材资产组合列表")
|
||||||
|
public AjaxResult<ListAssetsGroupResponse> listAssetGroups(@RequestBody ListAssetsGroupRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(groupService.listAssetGroups(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询素材组列表时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/createAssetGroup")
|
||||||
|
@ApiOperation("CreateAssetGroup:创建素材资产组合")
|
||||||
|
public AjaxResult<CreateAssetGroupResponse> createAssetGroup(@RequestBody CreateAssetGroupRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(groupService.createAssetGroup(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("创建素材组时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/getAssetGroup")
|
||||||
|
@ApiOperation("GetAssetGroup:查询单个素材资产组合")
|
||||||
|
public AjaxResult<GetAssetGroupResponse> getAssetGroup(@RequestBody GetAssetGroupRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(groupService.getAssetGroup(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("查询素材组详情时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/updateAssetGroup")
|
||||||
|
@ApiOperation("UpdateAssetGroup:更新素材资产组合")
|
||||||
|
public AjaxResult<UpdateAssetGroupResponse> updateAssetGroup(@RequestBody UpdateAssetGroupRequest request) {
|
||||||
|
try {
|
||||||
|
return AjaxResult.success(groupService.updateAssetGroup(request));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("更新素材组时发生异常", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.ruoyi.api;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.TencentCosUtil;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import io.swagger.annotations.ApiParam;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COS 上传兼容接口
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/cos")
|
||||||
|
@Api(tags = "COS文件上传")
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class CosController {
|
||||||
|
|
||||||
|
private final TencentCosUtil tencentCosUtil;
|
||||||
|
|
||||||
|
@ApiOperation("COS上传接口")
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public AjaxResult upload(
|
||||||
|
@ApiParam(name = "file", value = "文件", required = true)
|
||||||
|
@RequestParam("file") MultipartFile file,
|
||||||
|
@ApiParam(name = "pathPrefix", value = "对象键前缀,如 asset,则路径为 asset/yyyy/MM/dd/...")
|
||||||
|
@RequestParam(value = "pathPrefix", required = false) String pathPrefix) throws Exception {
|
||||||
|
String prefix = StringUtils.isNotEmpty(pathPrefix) ? pathPrefix.trim() : null;
|
||||||
|
String uploadUrl = tencentCosUtil.uploadMultipartFile(file, true, prefix);
|
||||||
|
AjaxResult ajax = AjaxResult.success(uploadUrl);
|
||||||
|
ajax.put("url", uploadUrl);
|
||||||
|
ajax.put("oldName", file.getOriginalFilename());
|
||||||
|
return ajax;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.ruoyi.api;
|
package com.ruoyi.api;
|
||||||
|
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.utils.AwsS3Util;
|
import com.ruoyi.common.utils.TencentCosUtil;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import io.swagger.annotations.ApiParam;
|
import io.swagger.annotations.ApiParam;
|
||||||
|
|
@ -18,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||||
@Api(tags = "文件上传")
|
@Api(tags = "文件上传")
|
||||||
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
public class FileController {
|
public class FileController {
|
||||||
private final AwsS3Util awsS3Util;
|
private final TencentCosUtil tencentCosUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件上传
|
* 文件上传
|
||||||
|
|
@ -30,7 +30,7 @@ public class FileController {
|
||||||
@ApiParam(name = "file", value = "文件", required = true)
|
@ApiParam(name = "file", value = "文件", required = true)
|
||||||
@RequestParam("file") MultipartFile file) throws Exception {
|
@RequestParam("file") MultipartFile file) throws Exception {
|
||||||
AjaxResult ajax = AjaxResult.success();
|
AjaxResult ajax = AjaxResult.success();
|
||||||
String uploadUrl = awsS3Util.uploadMultipartFile(file, true);
|
String uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
|
||||||
ajax.put("url", uploadUrl);
|
ajax.put("url", uploadUrl);
|
||||||
return ajax;
|
return ajax;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ public class KadaPaymentJava {
|
||||||
map.put("pm", "QRPH");
|
map.put("pm", "QRPH");
|
||||||
map.put("ref", orderNo);
|
map.put("ref", orderNo);
|
||||||
map.put("payer", payer);
|
map.put("payer", payer);
|
||||||
map.put("redirect", "www.google.com/recharge");
|
map.put("redirect", "https://undressing.top/recharge");
|
||||||
map.put("callbackUrl", "www.google.com/api/pay/kada-callBack");
|
map.put("callbackUrl", "https://undressing.top/api/pay/kada-callBack");
|
||||||
|
|
||||||
return JSON.toJSONString(map);
|
return JSON.toJSONString(map);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ import com.ruoyi.ai.service.IAiRechargeService;
|
||||||
import com.ruoyi.ai.service.IJinShaService;
|
import com.ruoyi.ai.service.IJinShaService;
|
||||||
import com.ruoyi.ai.service.IKaDaService;
|
import com.ruoyi.ai.service.IKaDaService;
|
||||||
import com.ruoyi.ai.service.IYuZhouService;
|
import com.ruoyi.ai.service.IYuZhouService;
|
||||||
|
import com.ruoyi.ai.service.IVmService;
|
||||||
import com.ruoyi.common.annotation.Anonymous;
|
import com.ruoyi.common.annotation.Anonymous;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.utils.ip.IpUtils;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
@ -25,6 +27,7 @@ public class PayController {
|
||||||
private final IKaDaService kaDaService;
|
private final IKaDaService kaDaService;
|
||||||
private final IAiRechargeService aiRechargeService;
|
private final IAiRechargeService aiRechargeService;
|
||||||
private final IYuZhouService yuZhouService;
|
private final IYuZhouService yuZhouService;
|
||||||
|
private final IVmService vmService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* jinsha请求支付
|
* jinsha请求支付
|
||||||
|
|
@ -85,4 +88,109 @@ public class PayController {
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VM请求支付
|
||||||
|
*/
|
||||||
|
@PostMapping("/vm-pay")
|
||||||
|
@ApiOperation("VM请求支付")
|
||||||
|
public AjaxResult vmPay(@RequestBody VmPayReq req, HttpServletRequest request) throws Exception {
|
||||||
|
// 获取用户真实IP地址
|
||||||
|
String clientIp = IpUtils.getIpAddr(request);
|
||||||
|
PayResVO payResVO = vmService.vmPay(req.getGearId(), req.getVmCardInfo(), clientIp);
|
||||||
|
return AjaxResult.success(payResVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/vm-callBack")
|
||||||
|
@ApiOperation("VM支付回调")
|
||||||
|
@Anonymous
|
||||||
|
public String vmCallBack(HttpServletRequest request) {
|
||||||
|
// 记录请求的Content-Type
|
||||||
|
String contentType = request.getContentType();
|
||||||
|
|
||||||
|
// 从请求中解析表单参数
|
||||||
|
VmCallBackReq req = new VmCallBackReq();
|
||||||
|
|
||||||
|
// 记录所有请求参数用于调试
|
||||||
|
java.util.Enumeration<String> paramNames = request.getParameterNames();
|
||||||
|
java.util.Map<String, String> allParams = new java.util.HashMap<>();
|
||||||
|
while (paramNames.hasMoreElements()) {
|
||||||
|
String paramName = paramNames.nextElement();
|
||||||
|
String paramValue = request.getParameter(paramName);
|
||||||
|
allParams.put(paramName, paramValue);
|
||||||
|
}
|
||||||
|
org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(PayController.class);
|
||||||
|
log.info("VM支付回调请求参数: {}, Content-Type: {}", allParams, contentType);
|
||||||
|
|
||||||
|
req.setMchNo(request.getParameter("mchNo"));
|
||||||
|
req.setAppId(request.getParameter("appId"));
|
||||||
|
req.setMchOrderNo(request.getParameter("mchOrderNo"));
|
||||||
|
req.setPayOrderId(request.getParameter("payOrderId"));
|
||||||
|
req.setWayCode(request.getParameter("wayCode"));
|
||||||
|
|
||||||
|
// 解析整数类型字段
|
||||||
|
String amountStr = request.getParameter("amount");
|
||||||
|
if (amountStr != null && !amountStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
req.setAmount(Integer.parseInt(amountStr));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("VM支付回调amount解析失败: {}", amountStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.setCurrency(request.getParameter("currency"));
|
||||||
|
req.setIfCode(request.getParameter("ifCode"));
|
||||||
|
req.setSubject(request.getParameter("subject"));
|
||||||
|
req.setBody(request.getParameter("body"));
|
||||||
|
req.setClientIp(request.getParameter("clientIp"));
|
||||||
|
|
||||||
|
// 解析订单状态(文档字段名:state)
|
||||||
|
String stateStr = request.getParameter("state");
|
||||||
|
if (stateStr != null && !stateStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
req.setState(Integer.parseInt(stateStr));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("VM支付回调state解析失败: {}", stateStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.setChannelOrderNo(request.getParameter("channelOrderNo"));
|
||||||
|
req.setErrCode(request.getParameter("errCode"));
|
||||||
|
req.setErrMsg(request.getParameter("errMsg"));
|
||||||
|
req.setExtParam(request.getParameter("extParam"));
|
||||||
|
|
||||||
|
// 解析时间戳:createdAt、successTime、reqTime(文档字段名)
|
||||||
|
String createdAtStr = request.getParameter("createdAt");
|
||||||
|
if (createdAtStr != null && !createdAtStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
req.setCreatedAt(Long.parseLong(createdAtStr));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("VM支付回调createdAt解析失败: {}", createdAtStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String successTimeStr = request.getParameter("successTime");
|
||||||
|
if (successTimeStr != null && !successTimeStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
req.setSuccessTime(Long.parseLong(successTimeStr));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("VM支付回调successTime解析失败: {}", successTimeStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String reqTimeStr = request.getParameter("reqTime");
|
||||||
|
if (reqTimeStr != null && !reqTimeStr.isEmpty()) {
|
||||||
|
try {
|
||||||
|
req.setReqTime(Long.parseLong(reqTimeStr));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("VM支付回调reqTime解析失败: {}", reqTimeStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.setSign(request.getParameter("sign"));
|
||||||
|
req.setSignType(request.getParameter("signType"));
|
||||||
|
|
||||||
|
log.info("VM支付回调解析后的对象: mchOrderNo={}, state={}, sign={}",
|
||||||
|
req.getMchOrderNo(), req.getState(), req.getSign());
|
||||||
|
|
||||||
|
return vmService.vmCallBack(req);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package com.ruoyi.api;
|
||||||
|
|
||||||
|
import com.ruoyi.ai.domain.AiOrder;
|
||||||
|
import com.ruoyi.ai.service.IAiOrderService;
|
||||||
|
import com.ruoyi.ai.service.IAiUserService;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.domain.entity.AiUser;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门户-生成资产管理:展示当前用户的AI订单记录,支持收藏功能
|
||||||
|
*/
|
||||||
|
@Api(tags = "门户-生成资产")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/portal/assets")
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class PortalAssetsController extends BaseController {
|
||||||
|
|
||||||
|
private final IAiOrderService aiOrderService;
|
||||||
|
|
||||||
|
private final IAiUserService aiUserService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前用户的生成资产列表
|
||||||
|
* 支持dept=true参数查询当前用户所在部门的所有作品
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
@ApiOperation("查询生成资产列表(支持收藏筛选、时间范围、dept部门查询)")
|
||||||
|
public TableDataInfo list(
|
||||||
|
@RequestParam(required = false) String is_top,
|
||||||
|
@RequestParam(required = false) String beginTime,
|
||||||
|
@RequestParam(required = false) String endTime,
|
||||||
|
@RequestParam(required = false, defaultValue = "false") Boolean dept) {
|
||||||
|
|
||||||
|
AiOrder query = new AiOrder();
|
||||||
|
Long currentUserId = SecurityUtils.getAiUserId();
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(dept)) {
|
||||||
|
// 查询当前用户所在部门的所有作品(包括自己)
|
||||||
|
AiUser currentUser = aiUserService.selectAiUserById(currentUserId);
|
||||||
|
if (currentUser != null && currentUser.getDeptId() != null) {
|
||||||
|
// 通过部门查询所有用户(在XML中通过IN子查询实现)
|
||||||
|
query.getParams().put("deptId", currentUser.getDeptId());
|
||||||
|
} else {
|
||||||
|
// 兜底只查个人
|
||||||
|
query.setUserId(currentUserId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 默认只查询当前用户的数据
|
||||||
|
query.setUserId(currentUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 收藏状态筛选 - is_top: Y=已收藏, N=未收藏
|
||||||
|
if (is_top != null && !is_top.trim().isEmpty()) {
|
||||||
|
String trimmedIsTop = is_top.trim();
|
||||||
|
query.setIsTop(trimmedIsTop);
|
||||||
|
System.out.println("✓ 设置 isTop = [" + trimmedIsTop + "]");
|
||||||
|
System.out.println(" query.getIsTop() = " + query.getIsTop());
|
||||||
|
} else {
|
||||||
|
System.out.println("✗ is_top 参数为空或未提供,不设置筛选条件");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (beginTime != null && !beginTime.isEmpty()) {
|
||||||
|
query.getParams().put("beginTime", beginTime);
|
||||||
|
System.out.println("✓ 设置 beginTime = " + beginTime);
|
||||||
|
}
|
||||||
|
if (endTime != null && !endTime.isEmpty()) {
|
||||||
|
query.getParams().put("endTime", endTime);
|
||||||
|
System.out.println("✓ 设置 endTime = " + endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
startPage();
|
||||||
|
List<AiOrder> list = aiOrderService.selectAiOrderList(query);
|
||||||
|
System.out.println("PortalAssetsController - 查询返回 " + (list != null ? list.size() : 0) + " 条记录");
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收藏/取消收藏
|
||||||
|
*/
|
||||||
|
@PostMapping("/favorite")
|
||||||
|
@ApiOperation("收藏或取消收藏生成资产")
|
||||||
|
public AjaxResult favorite(@RequestBody FavoriteRequest request) {
|
||||||
|
Long userId = SecurityUtils.getAiUserId();
|
||||||
|
|
||||||
|
// 验证订单归属
|
||||||
|
AiOrder order = aiOrderService.selectAiOrderById(request.getId());
|
||||||
|
if (order == null) {
|
||||||
|
return AjaxResult.error("订单不存在");
|
||||||
|
}
|
||||||
|
if (!userId.equals(order.getUserId())) {
|
||||||
|
return AjaxResult.error("无权操作该订单");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新收藏状态
|
||||||
|
AiOrder update = new AiOrder();
|
||||||
|
update.setId(request.getId());
|
||||||
|
update.setIsTop(request.getIsTop());
|
||||||
|
|
||||||
|
int result = aiOrderService.updateAiOrder(update);
|
||||||
|
return toAjax(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收藏请求体
|
||||||
|
*/
|
||||||
|
public static class FavoriteRequest {
|
||||||
|
private Long id;
|
||||||
|
private String isTop;
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIsTop() {
|
||||||
|
return isTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsTop(String isTop) {
|
||||||
|
this.isTop = isTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,616 @@
|
||||||
|
package com.ruoyi.api;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import com.ruoyi.ai.domain.AiOrder;
|
||||||
|
import com.ruoyi.ai.domain.ByteBodyReq;
|
||||||
|
import com.ruoyi.ai.domain.ByteBodyRes;
|
||||||
|
import com.ruoyi.ai.domain.ContentItem;
|
||||||
|
import com.ruoyi.ai.domain.ImageUrl;
|
||||||
|
import com.ruoyi.ai.domain.content;
|
||||||
|
import com.ruoyi.ai.service.IAiOrderService;
|
||||||
|
import com.ruoyi.ai.service.IByteDeptApiKeyService;
|
||||||
|
import com.ruoyi.ai.service.IByteService;
|
||||||
|
import com.ruoyi.api.request.PortalVideoGenRequest;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.TencentCosUtil;
|
||||||
|
import com.ruoyi.config.PortalVideoProperties;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门户视频生成:按用户二级部门 byte_api_key 调用火山;任务列表含库表与火山过滤列表。
|
||||||
|
*/
|
||||||
|
@Api(tags = "门户-视频生成")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/portal/video")
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class PortalVideoController extends BaseController {
|
||||||
|
|
||||||
|
private final IByteService byteService;
|
||||||
|
private final IByteDeptApiKeyService byteDeptApiKeyService;
|
||||||
|
private final IAiOrderService aiOrderService;
|
||||||
|
private final TencentCosUtil tencentCosUtil;
|
||||||
|
private final PortalVideoProperties portalVideoProperties;
|
||||||
|
|
||||||
|
@Value("${volcengine.ark.callbackUrl:}")
|
||||||
|
private String volcCallbackUrl;
|
||||||
|
|
||||||
|
private static final ObjectMapper OM = new ObjectMapper();
|
||||||
|
|
||||||
|
private String apiKey() {
|
||||||
|
return byteDeptApiKeyService.resolveVolcApiKey(SecurityUtils.getAiUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 ai_manager.type、portal.video.function-type 对齐,用于扣费 */
|
||||||
|
private String resolveFunctionType(PortalVideoGenRequest req) {
|
||||||
|
if (StringUtils.isNotEmpty(req.getFunctionType())) {
|
||||||
|
return req.getFunctionType();
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(portalVideoProperties.getFunctionType())) {
|
||||||
|
return portalVideoProperties.getFunctionType();
|
||||||
|
}
|
||||||
|
return "21";
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PortalVideoProperties.ModelOption> loadModelOptionsForAiUser(Long aiUserId, Long secondDeptId) {
|
||||||
|
String json = byteDeptApiKeyService.resolveSecondLevelModelParm(aiUserId);
|
||||||
|
List<PortalVideoProperties.ModelOption> fromDb = parseModelParmJson(json);
|
||||||
|
if (fromDb != null && !fromDb.isEmpty()) {
|
||||||
|
return fromDb;
|
||||||
|
}
|
||||||
|
return portalVideoProperties.resolveModelsForSecondDept(secondDeptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PortalVideoProperties.ModelOption> parseModelParmJson(String json) {
|
||||||
|
if (StringUtils.isEmpty(json)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<PortalVideoProperties.ModelOption> list = OM.readValue(json.trim(),
|
||||||
|
new TypeReference<List<PortalVideoProperties.ModelOption>>() { });
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<PortalVideoProperties.ModelOption> out = new ArrayList<>();
|
||||||
|
for (PortalVideoProperties.ModelOption o : list) {
|
||||||
|
if (o != null && StringUtils.isNotEmpty(o.getLabel()) && StringUtils.isNotEmpty(o.getValue())) {
|
||||||
|
out.add(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.isEmpty() ? null : out;
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("解析二级部门 model_parm 失败: {}", e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isModelInOptions(String modelId, List<PortalVideoProperties.ModelOption> options) {
|
||||||
|
if (modelId == null || modelId.isEmpty() || options == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (PortalVideoProperties.ModelOption o : options) {
|
||||||
|
if (modelId.equals(o.getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void alignDefaultsModelToOptions(PortalVideoProperties.Defaults defs, List<PortalVideoProperties.ModelOption> models) {
|
||||||
|
if (defs == null || models == null || models.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String m = defs.getModel();
|
||||||
|
if (m == null || m.isEmpty()) {
|
||||||
|
defs.setModel(models.get(0).getValue());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isModelInOptions(m, models)) {
|
||||||
|
defs.setModel(models.get(0).getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveDefaultModelForAiUser(Long aiUserId, Long secondDeptId, List<PortalVideoProperties.ModelOption> allowed) {
|
||||||
|
PortalVideoProperties.Defaults d = portalVideoProperties.resolveEffectiveDefaults(secondDeptId);
|
||||||
|
if (d.getModel() != null && !d.getModel().isEmpty() && isModelInOptions(d.getModel(), allowed)) {
|
||||||
|
return d.getModel();
|
||||||
|
}
|
||||||
|
if (!allowed.isEmpty() && StringUtils.isNotEmpty(allowed.get(0).getValue())) {
|
||||||
|
return allowed.get(0).getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyOptionalParams(ByteBodyReq body, PortalVideoGenRequest req, Long secondDeptId) {
|
||||||
|
PortalVideoProperties.Defaults d = portalVideoProperties.resolveEffectiveDefaults(secondDeptId);
|
||||||
|
body.setDuration(req.getDuration() != null ? req.getDuration() : d.getDuration());
|
||||||
|
body.setResolution(StringUtils.isNotEmpty(req.getResolution()) ? req.getResolution() : d.getResolution());
|
||||||
|
body.setRatio(StringUtils.isNotEmpty(req.getRatio()) ? req.getRatio() : d.getRatio());
|
||||||
|
if (StringUtils.isNotEmpty(volcCallbackUrl)) {
|
||||||
|
body.setCallback_url(volcCallbackUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ByteBodyReq newVideoBody(PortalVideoGenRequest req, List<ContentItem> content) {
|
||||||
|
Long uid = SecurityUtils.getAiUserId();
|
||||||
|
Long secondDeptId = byteDeptApiKeyService.resolveSecondLevelDeptId(uid);
|
||||||
|
List<PortalVideoProperties.ModelOption> allowed = loadModelOptionsForAiUser(uid, secondDeptId);
|
||||||
|
String modelId = StringUtils.isNotEmpty(req.getModel()) ? req.getModel() : resolveDefaultModelForAiUser(uid, secondDeptId, allowed);
|
||||||
|
if (StringUtils.isEmpty(modelId)) {
|
||||||
|
throw new ServiceException("未配置门户视频模型:请在后台为该用户所属二级部门配置 model_parm,或在 portal.video.models 中配置全局列表");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(req.getModel()) && !isModelInOptions(modelId, allowed)) {
|
||||||
|
throw new ServiceException("所选模型对当前部门不可用");
|
||||||
|
}
|
||||||
|
ByteBodyReq body = new ByteBodyReq();
|
||||||
|
body.setModel(modelId);
|
||||||
|
body.setContent(content);
|
||||||
|
applyOptionalParams(body, req, secondDeptId);
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyOrderImages(AiOrder aiOrder, PortalVideoGenRequest req) {
|
||||||
|
if (StringUtils.isNotEmpty(req.getFirstUrl())) {
|
||||||
|
aiOrder.setImg1(req.getFirstUrl());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(req.getLastUrl())) {
|
||||||
|
aiOrder.setImg2(req.getLastUrl());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(req.getReferenceUrl())) {
|
||||||
|
aiOrder.setImg1(req.getReferenceUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写入订单:提示词、生成模式、图床 URL,以及模型/时长/分辨率/比例与完整 JSON 参数(便于对账与审计)。
|
||||||
|
*/
|
||||||
|
private void fillVideoOrderRecord(AiOrder aiOrder, PortalVideoGenRequest req, String mode, ByteBodyReq body, String functionTypeResolved) {
|
||||||
|
aiOrder.setText(req.getText());
|
||||||
|
aiOrder.setMode(mode);
|
||||||
|
applyOrderImages(aiOrder, req);
|
||||||
|
if (req.getDuration() != null) {
|
||||||
|
aiOrder.setDuration(req.getDuration());
|
||||||
|
} else if (body.getDuration() != null) {
|
||||||
|
aiOrder.setDuration(body.getDuration());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(body.getResolution())) {
|
||||||
|
aiOrder.setResolution(body.getResolution());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(body.getRatio())) {
|
||||||
|
aiOrder.setRatio(body.getRatio());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(body.getModel())) {
|
||||||
|
aiOrder.setModel(body.getModel());
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<String, Object> snap = new LinkedHashMap<>();
|
||||||
|
snap.put("generationMode", mode);
|
||||||
|
snap.put("prompt", req.getText());
|
||||||
|
snap.put("functionType", functionTypeResolved);
|
||||||
|
snap.put("model", body.getModel());
|
||||||
|
snap.put("duration", body.getDuration());
|
||||||
|
snap.put("resolution", body.getResolution());
|
||||||
|
snap.put("ratio", body.getRatio());
|
||||||
|
if (StringUtils.isNotEmpty(req.getFirstUrl())) {
|
||||||
|
snap.put("firstUrl", req.getFirstUrl());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(req.getLastUrl())) {
|
||||||
|
snap.put("lastUrl", req.getLastUrl());
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotEmpty(req.getReferenceUrl())) {
|
||||||
|
snap.put("referenceUrl", req.getReferenceUrl());
|
||||||
|
}
|
||||||
|
if (req.getContent() != null && !req.getContent().isEmpty()) {
|
||||||
|
snap.put("content", req.getContent());
|
||||||
|
}
|
||||||
|
aiOrder.setVideoParams(OM.writeValueAsString(snap));
|
||||||
|
} catch (Exception e) {
|
||||||
|
aiOrder.setVideoParams("{\"error\":\"video_params_serialize_failed\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 写入 video_params.volcTaskId,任务成功后 result 会改为成品 URL,仍应用此 id 校验归属与轮询 */
|
||||||
|
private void mergeVolcTaskIdIntoVideoParams(AiOrder aiOrder, String volcTaskId) {
|
||||||
|
if (StringUtils.isEmpty(volcTaskId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ObjectNode node;
|
||||||
|
if (StringUtils.isNotEmpty(aiOrder.getVideoParams())) {
|
||||||
|
JsonNode existing = OM.readTree(aiOrder.getVideoParams());
|
||||||
|
if (existing instanceof ObjectNode) {
|
||||||
|
node = (ObjectNode) existing;
|
||||||
|
} else {
|
||||||
|
node = OM.createObjectNode();
|
||||||
|
node.set("snapshotBeforeVolcId", existing);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node = OM.createObjectNode();
|
||||||
|
}
|
||||||
|
node.put("volcTaskId", volcTaskId);
|
||||||
|
aiOrder.setVideoParams(OM.writeValueAsString(node));
|
||||||
|
} catch (Exception e) {
|
||||||
|
aiOrder.setVideoParams("{\"volcTaskId\":\"" + volcTaskId.replace("\"", "") + "\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AjaxResult submitOrderAndCreate(PortalVideoGenRequest req, String mode, ByteBodyReq byteBodyReq) {
|
||||||
|
String functionType = resolveFunctionType(req);
|
||||||
|
AiOrder aiOrder = aiOrderService.getAiOrder(functionType);
|
||||||
|
if (aiOrder == null) {
|
||||||
|
return AjaxResult.error(-1, "You have a low balance, please recharge");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fillVideoOrderRecord(aiOrder, req, mode, byteBodyReq, functionType);
|
||||||
|
aiOrderService.updateAiOrder(aiOrder);
|
||||||
|
|
||||||
|
String key = apiKey();
|
||||||
|
ByteBodyRes byteBodyRes = byteService.imgToVideo(byteBodyReq, key);
|
||||||
|
String id = byteBodyRes.getId();
|
||||||
|
if (id == null) {
|
||||||
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
return AjaxResult.error(-2, "generation failed, balance has been refunded");
|
||||||
|
}
|
||||||
|
mergeVolcTaskIdIntoVideoParams(aiOrder, id);
|
||||||
|
aiOrder.setResult(id);
|
||||||
|
aiOrderService.orderSuccess(aiOrder);
|
||||||
|
return AjaxResult.success(byteBodyRes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
aiOrderService.orderFailure(aiOrder);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/text-to-video")
|
||||||
|
@ApiOperation("文生视频")
|
||||||
|
public AjaxResult textToVideo(@RequestBody PortalVideoGenRequest request) {
|
||||||
|
List<ContentItem> contentList;
|
||||||
|
if (request.getContent() != null && !request.getContent().isEmpty()) {
|
||||||
|
contentList = request.getContent();
|
||||||
|
} else {
|
||||||
|
if (StringUtils.isEmpty(request.getText())) {
|
||||||
|
return AjaxResult.error("请输入视频描述文本");
|
||||||
|
}
|
||||||
|
contentList = new ArrayList<>();
|
||||||
|
ContentItem textItem = new ContentItem();
|
||||||
|
textItem.setType("text");
|
||||||
|
textItem.setText(request.getText());
|
||||||
|
contentList.add(textItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBodyReq body = newVideoBody(request, contentList);
|
||||||
|
return submitOrderAndCreate(request, "text-to-video", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/image-first-frame")
|
||||||
|
@ApiOperation("图生视频-基于首帧")
|
||||||
|
public AjaxResult imageFirstFrame(@RequestBody PortalVideoGenRequest request) {
|
||||||
|
if (StringUtils.isEmpty(request.getFirstUrl())) {
|
||||||
|
return AjaxResult.error("请上传首帧图片");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(request.getText())) {
|
||||||
|
return AjaxResult.error("请输入视频描述文本");
|
||||||
|
}
|
||||||
|
List<ContentItem> contentList = buildTextAndFirstFrame(request.getText(), request.getFirstUrl());
|
||||||
|
ByteBodyReq body = newVideoBody(request, contentList);
|
||||||
|
return submitOrderAndCreate(request, "image-first-frame", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/image-first-last-frame")
|
||||||
|
@ApiOperation("图生视频-基于首尾帧")
|
||||||
|
public AjaxResult imageFirstLastFrame(@RequestBody PortalVideoGenRequest request) {
|
||||||
|
if (StringUtils.isEmpty(request.getFirstUrl()) || StringUtils.isEmpty(request.getLastUrl())) {
|
||||||
|
return AjaxResult.error("请同时上传首帧与尾帧图片");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(request.getText())) {
|
||||||
|
return AjaxResult.error("请输入视频描述文本");
|
||||||
|
}
|
||||||
|
List<ContentItem> contentList = buildTextAndFirstFrame(request.getText(), request.getFirstUrl());
|
||||||
|
ContentItem lastFrameItem = new ContentItem();
|
||||||
|
lastFrameItem.setType("image_url");
|
||||||
|
lastFrameItem.setRole("last_frame");
|
||||||
|
ImageUrl lastImageUrl = new ImageUrl();
|
||||||
|
lastImageUrl.setUrl(request.getLastUrl());
|
||||||
|
lastFrameItem.setImageUrl(lastImageUrl);
|
||||||
|
contentList.add(lastFrameItem);
|
||||||
|
|
||||||
|
ByteBodyReq body = newVideoBody(request, contentList);
|
||||||
|
return submitOrderAndCreate(request, "image-first-last-frame", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/image-reference")
|
||||||
|
@ApiOperation("图生视频-基于参考图")
|
||||||
|
public AjaxResult imageReference(@RequestBody PortalVideoGenRequest request) {
|
||||||
|
List<ContentItem> contentList;
|
||||||
|
if (request.getContent() != null && !request.getContent().isEmpty()) {
|
||||||
|
contentList = new ArrayList<>(request.getContent());
|
||||||
|
ContentItem head = contentList.get(0);
|
||||||
|
if (head == null || !"text".equals(head.getType()) || StringUtils.isEmpty(head.getText())) {
|
||||||
|
return AjaxResult.error("请输入视频描述文本(首条须为 type=text,可含 [图片n]/[视频n]/[音频n] 占位)");
|
||||||
|
}
|
||||||
|
// 保留 text + 合法 reference_*(图/音/视频);允许“只有 text 没有参考素材”
|
||||||
|
List<ContentItem> filtered = new ArrayList<>();
|
||||||
|
filtered.add(head);
|
||||||
|
for (int i = 1; i < contentList.size(); i++) {
|
||||||
|
ContentItem it = contentList.get(i);
|
||||||
|
if (isReferenceImageContentItem(it)
|
||||||
|
|| isReferenceAudioContentItem(it)
|
||||||
|
|| isReferenceVideoContentItem(it)) {
|
||||||
|
filtered.add(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contentList = filtered;
|
||||||
|
|
||||||
|
String firstRef = contentList.stream()
|
||||||
|
.skip(1)
|
||||||
|
.map(PortalVideoController::firstReferenceUrlFromItem)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
// 无参考图也允许,仅 text 提示词
|
||||||
|
if (StringUtils.isNotEmpty(firstRef) && StringUtils.isEmpty(request.getReferenceUrl())) {
|
||||||
|
request.setReferenceUrl(firstRef);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (StringUtils.isEmpty(request.getReferenceUrl())) {
|
||||||
|
return AjaxResult.error("请上传参考图");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(request.getText())) {
|
||||||
|
return AjaxResult.error("请输入视频描述文本");
|
||||||
|
}
|
||||||
|
contentList = new ArrayList<>();
|
||||||
|
ContentItem textItem = new ContentItem();
|
||||||
|
textItem.setType("text");
|
||||||
|
textItem.setText(request.getText());
|
||||||
|
contentList.add(textItem);
|
||||||
|
|
||||||
|
ContentItem refItem = new ContentItem();
|
||||||
|
refItem.setType("image_url");
|
||||||
|
refItem.setRole("reference_image");
|
||||||
|
ImageUrl refUrl = new ImageUrl();
|
||||||
|
refUrl.setUrl(request.getReferenceUrl());
|
||||||
|
refItem.setImageUrl(refUrl);
|
||||||
|
contentList.add(refItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBodyReq body = newVideoBody(request, contentList);
|
||||||
|
return submitOrderAndCreate(request, "image-reference", body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String firstReferenceUrlFromItem(ContentItem item) {
|
||||||
|
if (isReferenceImageContentItem(item)) {
|
||||||
|
return item.getImageUrl().getUrl();
|
||||||
|
}
|
||||||
|
if (isReferenceVideoContentItem(item)) {
|
||||||
|
return item.getVideoUrl().getUrl();
|
||||||
|
}
|
||||||
|
if (isReferenceAudioContentItem(item)) {
|
||||||
|
return item.getAudioUrl().getUrl();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValidReferenceAssetOrHttpUrl(String raw) {
|
||||||
|
if (StringUtils.isEmpty(raw)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String url = raw.trim().toLowerCase();
|
||||||
|
return url.startsWith("http://") || url.startsWith("https://") || url.startsWith("asset://");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isReferenceImageContentItem(ContentItem item) {
|
||||||
|
if (item == null || !"image_url".equals(item.getType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!"reference_image".equals(item.getRole())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ImageUrl iu = item.getImageUrl();
|
||||||
|
if (iu == null || StringUtils.isEmpty(iu.getUrl())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isValidReferenceAssetOrHttpUrl(iu.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isReferenceVideoContentItem(ContentItem item) {
|
||||||
|
if (item == null || !"video_url".equals(item.getType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!"reference_video".equals(item.getRole())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ImageUrl vu = item.getVideoUrl();
|
||||||
|
if (vu == null || StringUtils.isEmpty(vu.getUrl())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isValidReferenceAssetOrHttpUrl(vu.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isReferenceAudioContentItem(ContentItem item) {
|
||||||
|
if (item == null || !"audio_url".equals(item.getType())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!"reference_audio".equals(item.getRole())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ImageUrl au = item.getAudioUrl();
|
||||||
|
if (au == null || StringUtils.isEmpty(au.getUrl())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isValidReferenceAssetOrHttpUrl(au.getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ContentItem> buildTextAndFirstFrame(String text, String firstUrl) {
|
||||||
|
List<ContentItem> contentList = new ArrayList<>();
|
||||||
|
ContentItem textItem = new ContentItem();
|
||||||
|
textItem.setType("text");
|
||||||
|
textItem.setText(text);
|
||||||
|
contentList.add(textItem);
|
||||||
|
|
||||||
|
ContentItem firstFrameItem = new ContentItem();
|
||||||
|
firstFrameItem.setType("image_url");
|
||||||
|
firstFrameItem.setRole("first_frame");
|
||||||
|
ImageUrl firstImageUrl = new ImageUrl();
|
||||||
|
firstImageUrl.setUrl(firstUrl);
|
||||||
|
firstFrameItem.setImageUrl(firstImageUrl);
|
||||||
|
contentList.add(firstFrameItem);
|
||||||
|
return contentList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/options")
|
||||||
|
@ApiOperation("门户视频生成可选参数(模型/比例/时长等,来自配置)")
|
||||||
|
public AjaxResult videoParamOptions() {
|
||||||
|
Long uid = SecurityUtils.getAiUserId();
|
||||||
|
Long secondDeptId = byteDeptApiKeyService.resolveSecondLevelDeptId(uid);
|
||||||
|
List<PortalVideoProperties.ModelOption> models = loadModelOptionsForAiUser(uid, secondDeptId);
|
||||||
|
PortalVideoProperties.Defaults defs = portalVideoProperties.resolveEffectiveDefaults(secondDeptId);
|
||||||
|
alignDefaultsModelToOptions(defs, models);
|
||||||
|
Map<String, Object> data = new LinkedHashMap<>();
|
||||||
|
data.put("defaults", defs);
|
||||||
|
data.put("models", models);
|
||||||
|
data.put("ratios", portalVideoProperties.getRatios());
|
||||||
|
data.put("durations", portalVideoProperties.getDurations());
|
||||||
|
data.put("resolutions", portalVideoProperties.getResolutions());
|
||||||
|
return AjaxResult.success(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks")
|
||||||
|
@ApiOperation("查询视频生成任务列表(本用户库表分页)")
|
||||||
|
public TableDataInfo listMyVideoTasks(AiOrder aiOrder) {
|
||||||
|
aiOrder.setUserId(SecurityUtils.getAiUserId());
|
||||||
|
aiOrder.setType("21");
|
||||||
|
startPage();
|
||||||
|
List<AiOrder> list = aiOrderService.selectAiOrderList(aiOrder);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/volc-tasks")
|
||||||
|
@ApiOperation("查询视频生成任务列表(火山平台,按本用户在库中的任务 id 过滤)")
|
||||||
|
public AjaxResult listVolcTasks(
|
||||||
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
|
@RequestParam(defaultValue = "20") int pageSize) throws Exception {
|
||||||
|
Long uid = SecurityUtils.getAiUserId();
|
||||||
|
String key = apiKey();
|
||||||
|
String raw = byteService.listVideoGenerationTasks(pageNum, pageSize, key);
|
||||||
|
String filtered = filterVolcTasksJsonForUser(raw, uid);
|
||||||
|
return AjaxResult.success(OM.readTree(filtered));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String filterVolcTasksJsonForUser(String raw, Long userId) throws Exception {
|
||||||
|
AiOrder query = new AiOrder();
|
||||||
|
query.setUserId(userId);
|
||||||
|
query.setType("21");
|
||||||
|
List<AiOrder> mine = aiOrderService.selectAiOrderList(query);
|
||||||
|
Set<String> allowed = mine.stream()
|
||||||
|
.map(AiOrder::getResult)
|
||||||
|
.filter(StringUtils::isNotEmpty)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
JsonNode root = OM.readTree(raw);
|
||||||
|
if (!(root instanceof ObjectNode)) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
ArrayNode arr = null;
|
||||||
|
String arrayKey = null;
|
||||||
|
if (root.has("items") && root.get("items").isArray()) {
|
||||||
|
arr = (ArrayNode) root.get("items");
|
||||||
|
arrayKey = "items";
|
||||||
|
} else if (root.has("data") && root.get("data").isArray()) {
|
||||||
|
arr = (ArrayNode) root.get("data");
|
||||||
|
arrayKey = "data";
|
||||||
|
}
|
||||||
|
if (arr == null) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
ArrayNode out = OM.createArrayNode();
|
||||||
|
for (JsonNode n : arr) {
|
||||||
|
String tid = n.has("id") ? n.get("id").asText() : null;
|
||||||
|
if (tid != null && allowed.contains(tid)) {
|
||||||
|
out.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectNode result = ((ObjectNode) root).deepCopy();
|
||||||
|
result.set(arrayKey, out);
|
||||||
|
result.put("filtered_total", out.size());
|
||||||
|
return OM.writeValueAsString(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/tasks/{taskId}")
|
||||||
|
@ApiOperation("查询单个视频生成任务(火山)")
|
||||||
|
public AjaxResult getVolcTask(@PathVariable String taskId) throws Exception {
|
||||||
|
Long uid = SecurityUtils.getAiUserId();
|
||||||
|
AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||||
|
if (owned == null || !uid.equals(owned.getUserId())) {
|
||||||
|
return AjaxResult.error("无权查看该任务");
|
||||||
|
}
|
||||||
|
String key = apiKey();
|
||||||
|
ByteBodyRes byteBodyRes;
|
||||||
|
try {
|
||||||
|
byteBodyRes = byteService.uploadVideo(taskId, key);
|
||||||
|
} catch (Exception e) {
|
||||||
|
String msg = e.getMessage();
|
||||||
|
// ByteService 在 HTTP 非 2xx 时抛出以此前缀开头的异常,仅此种情况清空订单,避免网络抖动等误伤
|
||||||
|
if (msg != null && msg.startsWith("uploadVideo error")) {
|
||||||
|
logger.warn("查询火山任务 HTTP 非成功,已清空订单 result 并标记失败, orderId={}, taskId={}",
|
||||||
|
owned.getId(), taskId, e);
|
||||||
|
AiOrder failedOrder = new AiOrder();
|
||||||
|
failedOrder.setId(owned.getId());
|
||||||
|
failedOrder.setResult("");
|
||||||
|
failedOrder.setStatus(2);
|
||||||
|
aiOrderService.updateAiOrder(failedOrder);
|
||||||
|
return AjaxResult.error(msg);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if ("succeeded".equals(byteBodyRes.getStatus())) {
|
||||||
|
content contentObj = byteBodyRes.getContent();
|
||||||
|
if (contentObj != null && StringUtils.isNotEmpty(contentObj.getVideo_url())) {
|
||||||
|
String videoUrl = tencentCosUtil.uploadFileByUrl(contentObj.getVideo_url());
|
||||||
|
if (videoUrl != null) {
|
||||||
|
contentObj.setVideo_url(videoUrl);
|
||||||
|
AiOrder aiOrder = new AiOrder();
|
||||||
|
aiOrder.setId(owned.getId());
|
||||||
|
aiOrder.setResult(videoUrl);
|
||||||
|
aiOrderService.updateAiOrder(aiOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AjaxResult.success(byteBodyRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/tasks/{taskId}")
|
||||||
|
@ApiOperation("删除或取消视频生成任务")
|
||||||
|
public AjaxResult deleteOrCancelTask(@PathVariable String taskId) throws Exception {
|
||||||
|
Long uid = SecurityUtils.getAiUserId();
|
||||||
|
AiOrder owned = aiOrderService.getAiOrderByPortalVideoTask(taskId);
|
||||||
|
if (owned == null || !uid.equals(owned.getUserId())) {
|
||||||
|
return AjaxResult.error("无权操作该任务");
|
||||||
|
}
|
||||||
|
String key = apiKey();
|
||||||
|
AjaxResult cancelRes = byteService.cancelVideoTask(taskId, key);
|
||||||
|
if (cancelRes.isSuccess() && owned.getStatus() != null && owned.getStatus() == 0) {
|
||||||
|
aiOrderService.orderFailure(owned);
|
||||||
|
}
|
||||||
|
return cancelRes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.ruoyi.api;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.ruoyi.ai.domain.TosAsset;
|
||||||
|
import com.ruoyi.ai.domain.dto.TosAssetSubmitRequest;
|
||||||
|
import com.ruoyi.ai.service.IModerationImageService;
|
||||||
|
import com.ruoyi.ai.service.ITosAssetService;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TOS 图片审核:加密对接第三方 /api/moderation/image,解密结果落库 tos_asset。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Api(tags = "TOS-审核资产")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/tos")
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = @Autowired)
|
||||||
|
public class TosAssetController extends BaseController {
|
||||||
|
|
||||||
|
private final IModerationImageService moderationImageService;
|
||||||
|
private final ITosAssetService tosAssetService;
|
||||||
|
|
||||||
|
@GetMapping("/asset/list")
|
||||||
|
@ApiOperation("分页查询当前用户的 tos_asset")
|
||||||
|
public TableDataInfo list(TosAsset query) {
|
||||||
|
startPage();
|
||||||
|
return getDataTable(tosAssetService.selectTosAssetList(query, SecurityUtils.getAiUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/asset/{id}")
|
||||||
|
@ApiOperation("删除单条 tos_asset(仅本人数据)")
|
||||||
|
public AjaxResult<Void> remove(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
tosAssetService.deleteByIdForUser(id, SecurityUtils.getAiUserId());
|
||||||
|
return AjaxResult.success();
|
||||||
|
} catch (ServiceException e) {
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/asset")
|
||||||
|
@ApiOperation("提交图片审核(加密转发 + 解密落库)")
|
||||||
|
public AjaxResult<JsonNode> submitAsset(@RequestBody TosAssetSubmitRequest request) {
|
||||||
|
try {
|
||||||
|
JsonNode data = moderationImageService.submitImageModeration(request, SecurityUtils.getAiUserId());
|
||||||
|
return AjaxResult.success(data);
|
||||||
|
} catch (ServiceException e) {
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("TOS 图片审核失败", e);
|
||||||
|
return AjaxResult.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,11 @@ public class ByteApiRequest {
|
||||||
@ApiModelProperty(name = "标签字符串")
|
@ApiModelProperty(name = "标签字符串")
|
||||||
private String tags;
|
private String tags;
|
||||||
|
|
||||||
|
@ApiModelProperty(name = "使用的模型")
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
@ApiModelProperty(name = "生成模式:text-to-video 或 image-to-video")
|
||||||
|
private String mode = "text-to-video";
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.ruoyi.api.request;
|
||||||
|
|
||||||
|
import com.ruoyi.ai.domain.ContentItem;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门户视频生成(火山 Seedance)请求体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class PortalVideoGenRequest {
|
||||||
|
|
||||||
|
private String text;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文生视频时可选:多段文本 + 参考图(与火山 content 一致);不传则仅使用 text 单行
|
||||||
|
*/
|
||||||
|
private List<ContentItem> content;
|
||||||
|
|
||||||
|
/** 默认与后台配置的视频计费类型一致 */
|
||||||
|
private String functionType = "21";
|
||||||
|
|
||||||
|
private String model;
|
||||||
|
|
||||||
|
private Integer duration;
|
||||||
|
|
||||||
|
private String resolution;
|
||||||
|
|
||||||
|
private String ratio;
|
||||||
|
|
||||||
|
private String firstUrl;
|
||||||
|
|
||||||
|
private String lastUrl;
|
||||||
|
|
||||||
|
/** 图生视频-参考图模式 */
|
||||||
|
private String referenceUrl;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
package com.ruoyi.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 门户视频生成参数(模型、比例、时长、分辨率等从配置读取,不在代码中写死业务默认值)。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "portal.video")
|
||||||
|
public class PortalVideoProperties {
|
||||||
|
|
||||||
|
private Defaults defaults = new Defaults();
|
||||||
|
|
||||||
|
private List<ModelOption> models = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按二级部门 {@code dept_id}(字符串,如 {@code "101"})配置专属模型列表;未命中时使用全局 {@link #models}。
|
||||||
|
*/
|
||||||
|
private Map<String, List<ModelOption>> modelsBySecondDept = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按二级部门覆盖 {@link #defaults} 中的字段;仅配置需要覆盖的项即可。
|
||||||
|
*/
|
||||||
|
private Map<String, Defaults> defaultsBySecondDept = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
private List<String> ratios = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<Integer> durations = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<String> resolutions = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与 ai_manager.type 一致,用于扣费与订单;库中需存在对应记录且 status=0、del_flag=0
|
||||||
|
*/
|
||||||
|
private String functionType = "21";
|
||||||
|
|
||||||
|
public String getFunctionType() {
|
||||||
|
return functionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFunctionType(String functionType) {
|
||||||
|
this.functionType = functionType != null ? functionType : "21";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Defaults getDefaults() {
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaults(Defaults defaults) {
|
||||||
|
if (defaults != null) {
|
||||||
|
this.defaults = defaults;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ModelOption> getModels() {
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModels(List<ModelOption> models) {
|
||||||
|
this.models = models != null ? models : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, List<ModelOption>> getModelsBySecondDept() {
|
||||||
|
return modelsBySecondDept;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModelsBySecondDept(Map<String, List<ModelOption>> modelsBySecondDept) {
|
||||||
|
this.modelsBySecondDept = modelsBySecondDept != null ? modelsBySecondDept : new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Defaults> getDefaultsBySecondDept() {
|
||||||
|
return defaultsBySecondDept;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultsBySecondDept(Map<String, Defaults> defaultsBySecondDept) {
|
||||||
|
this.defaultsBySecondDept = defaultsBySecondDept != null ? defaultsBySecondDept : new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getRatios() {
|
||||||
|
return ratios;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRatios(List<String> ratios) {
|
||||||
|
this.ratios = ratios != null ? ratios : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getDurations() {
|
||||||
|
return durations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDurations(List<Integer> durations) {
|
||||||
|
this.durations = durations != null ? durations : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getResolutions() {
|
||||||
|
return resolutions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResolutions(List<String> resolutions) {
|
||||||
|
this.resolutions = resolutions != null ? resolutions : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* defaults.model 为空时,取 models 第一项的 value。
|
||||||
|
*/
|
||||||
|
public String resolveDefaultModelId() {
|
||||||
|
if (defaults.getModel() != null && !defaults.getModel().isEmpty()) {
|
||||||
|
return defaults.getModel();
|
||||||
|
}
|
||||||
|
if (!models.isEmpty() && models.get(0).getValue() != null) {
|
||||||
|
return models.get(0).getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ModelOption> resolveModelsForSecondDept(Long secondDeptId) {
|
||||||
|
if (secondDeptId != null) {
|
||||||
|
List<ModelOption> byDept = modelsBySecondDept.get(String.valueOf(secondDeptId));
|
||||||
|
if (byDept != null && !byDept.isEmpty()) {
|
||||||
|
return byDept;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Defaults resolveEffectiveDefaults(Long secondDeptId) {
|
||||||
|
Defaults out = new Defaults();
|
||||||
|
Defaults g = this.defaults;
|
||||||
|
out.setModel(g.getModel());
|
||||||
|
out.setDuration(g.getDuration());
|
||||||
|
out.setResolution(g.getResolution());
|
||||||
|
out.setRatio(g.getRatio());
|
||||||
|
if (secondDeptId != null) {
|
||||||
|
Defaults o = defaultsBySecondDept.get(String.valueOf(secondDeptId));
|
||||||
|
if (o != null) {
|
||||||
|
if (o.getModel() != null && !o.getModel().isEmpty()) {
|
||||||
|
out.setModel(o.getModel());
|
||||||
|
}
|
||||||
|
if (o.getDuration() != null) {
|
||||||
|
out.setDuration(o.getDuration());
|
||||||
|
}
|
||||||
|
if (o.getResolution() != null && !o.getResolution().isEmpty()) {
|
||||||
|
out.setResolution(o.getResolution());
|
||||||
|
}
|
||||||
|
if (o.getRatio() != null && !o.getRatio().isEmpty()) {
|
||||||
|
out.setRatio(o.getRatio());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在给定二级部门下,解析默认模型:先看合并后的 defaults.model,否则取该部门 models 列表首项。
|
||||||
|
*/
|
||||||
|
public String resolveDefaultModelIdForSecondDept(Long secondDeptId) {
|
||||||
|
Defaults d = resolveEffectiveDefaults(secondDeptId);
|
||||||
|
if (d.getModel() != null && !d.getModel().isEmpty()) {
|
||||||
|
return d.getModel();
|
||||||
|
}
|
||||||
|
List<ModelOption> m = resolveModelsForSecondDept(secondDeptId);
|
||||||
|
if (!m.isEmpty() && m.get(0).getValue() != null && !m.get(0).getValue().isEmpty()) {
|
||||||
|
return m.get(0).getValue();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isModelAllowedForSecondDept(String modelId, Long secondDeptId) {
|
||||||
|
if (modelId == null || modelId.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (ModelOption opt : resolveModelsForSecondDept(secondDeptId)) {
|
||||||
|
if (modelId.equals(opt.getValue())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Defaults {
|
||||||
|
private String model;
|
||||||
|
private Integer duration;
|
||||||
|
private String resolution;
|
||||||
|
private String ratio;
|
||||||
|
|
||||||
|
public String getModel() {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModel(String model) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getDuration() {
|
||||||
|
return duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDuration(Integer duration) {
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResolution() {
|
||||||
|
return resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResolution(String resolution) {
|
||||||
|
this.resolution = resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRatio() {
|
||||||
|
return ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRatio(String ratio) {
|
||||||
|
this.ratio = ratio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ModelOption {
|
||||||
|
private String label;
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLabel(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.ruoyi.web.controller.ai;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import com.ruoyi.ai.service.IAiUserService;
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.constant.UserConstants;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.domain.entity.SysDept;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.system.service.ISysDeptService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 业务侧部门管理(数据源与系统部门 sys_dept 一致,权限独立)
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/ai/dept")
|
||||||
|
public class AiDeptController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private ISysDeptService deptService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IAiUserService aiUserService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:dept:list')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public AjaxResult list(SysDept dept)
|
||||||
|
{
|
||||||
|
List<SysDept> depts = deptService.selectDeptList(dept);
|
||||||
|
return success(depts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:dept:list')")
|
||||||
|
@GetMapping("/list/exclude/{deptId}")
|
||||||
|
public AjaxResult excludeChild(@PathVariable(value = "deptId", required = false) Long deptId)
|
||||||
|
{
|
||||||
|
List<SysDept> depts = deptService.selectDeptList(new SysDept());
|
||||||
|
depts.removeIf(d -> d.getDeptId().intValue() == deptId || ArrayUtils.contains(StringUtils.split(d.getAncestors(), ","), deptId + ""));
|
||||||
|
return success(depts);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:dept:query')")
|
||||||
|
@GetMapping(value = "/{deptId}")
|
||||||
|
public AjaxResult getInfo(@PathVariable Long deptId)
|
||||||
|
{
|
||||||
|
deptService.checkDeptDataScope(deptId);
|
||||||
|
return success(deptService.selectDeptById(deptId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:dept:add')")
|
||||||
|
@Log(title = "AI部门管理", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@Validated @RequestBody SysDept dept)
|
||||||
|
{
|
||||||
|
if (!deptService.checkDeptNameUnique(dept))
|
||||||
|
{
|
||||||
|
return error("新增部门'" + dept.getDeptName() + "'失败,部门名称已存在");
|
||||||
|
}
|
||||||
|
dept.setCreateBy(getUsername());
|
||||||
|
return toAjax(deptService.insertDept(dept));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:dept:edit')")
|
||||||
|
@Log(title = "AI部门管理", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult edit(@Validated @RequestBody SysDept dept)
|
||||||
|
{
|
||||||
|
Long deptId = dept.getDeptId();
|
||||||
|
deptService.checkDeptDataScope(deptId);
|
||||||
|
if (!deptService.checkDeptNameUnique(dept))
|
||||||
|
{
|
||||||
|
return error("修改部门'" + dept.getDeptName() + "'失败,部门名称已存在");
|
||||||
|
}
|
||||||
|
else if (dept.getParentId().equals(deptId))
|
||||||
|
{
|
||||||
|
return error("修改部门'" + dept.getDeptName() + "'失败,上级部门不能是自己");
|
||||||
|
}
|
||||||
|
else if (StringUtils.equals(UserConstants.DEPT_DISABLE, dept.getStatus()) && deptService.selectNormalChildrenDeptById(deptId) > 0)
|
||||||
|
{
|
||||||
|
return error("该部门包含未停用的子部门!");
|
||||||
|
}
|
||||||
|
dept.setUpdateBy(getUsername());
|
||||||
|
return toAjax(deptService.updateDept(dept));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:dept:remove')")
|
||||||
|
@Log(title = "AI部门管理", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{deptId}")
|
||||||
|
public AjaxResult remove(@PathVariable Long deptId)
|
||||||
|
{
|
||||||
|
if (deptService.hasChildByDeptId(deptId))
|
||||||
|
{
|
||||||
|
return warn("存在下级部门,不允许删除");
|
||||||
|
}
|
||||||
|
if (deptService.checkDeptExistUser(deptId))
|
||||||
|
{
|
||||||
|
return warn("部门存在系统用户,不允许删除");
|
||||||
|
}
|
||||||
|
if (aiUserService.countAiUserByDeptId(deptId) > 0)
|
||||||
|
{
|
||||||
|
return warn("部门存在 AI 用户,不允许删除");
|
||||||
|
}
|
||||||
|
deptService.checkDeptDataScope(deptId);
|
||||||
|
return toAjax(deptService.deleteDeptById(deptId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.ruoyi.ai.controller;
|
package com.ruoyi.web.controller.ai;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,27 @@
|
||||||
package com.ruoyi.web.controller.ai;
|
package com.ruoyi.web.controller.ai;
|
||||||
|
|
||||||
import cn.hutool.core.util.NumberUtil;
|
import cn.hutool.core.util.NumberUtil;
|
||||||
import com.ruoyi.ai.service.EmailVerifyService;
|
|
||||||
import com.ruoyi.ai.service.IAiUserService;
|
import com.ruoyi.ai.service.IAiUserService;
|
||||||
import com.ruoyi.common.annotation.Anonymous;
|
|
||||||
import com.ruoyi.common.annotation.Log;
|
import com.ruoyi.common.annotation.Log;
|
||||||
import com.ruoyi.common.constant.BalanceChangerConstants;
|
import com.ruoyi.common.constant.BalanceChangerConstants;
|
||||||
import com.ruoyi.common.constant.HttpStatus;
|
|
||||||
import com.ruoyi.common.core.controller.BaseController;
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.domain.entity.AiUser;
|
import com.ruoyi.common.core.domain.entity.AiUser;
|
||||||
import com.ruoyi.common.core.domain.model.RegisterAiUserBody;
|
|
||||||
import com.ruoyi.common.core.page.TableDataInfo;
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
|
||||||
import com.ruoyi.common.utils.MessageUtils;
|
|
||||||
import com.ruoyi.common.utils.SecurityUtils;
|
|
||||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.annotations.Api;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.annotations.ApiOperation;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import javax.mail.MessagingException;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ai-用户信息Controller
|
* ai-用户信息Controller
|
||||||
|
|
@ -77,7 +71,6 @@ public class AiUserController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增ai-用户信息
|
* 新增ai-用户信息
|
||||||
*/
|
*/
|
||||||
|
|
@ -100,6 +93,20 @@ public class AiUserController extends BaseController {
|
||||||
return toAjax(aiUserService.updateAiUser(aiUser));
|
return toAjax(aiUserService.updateAiUser(aiUser));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配归属部门(deptId 为空表示不归属任何部门)
|
||||||
|
*/
|
||||||
|
@ApiOperation("分配AI用户归属部门")
|
||||||
|
@PreAuthorize("@ss.hasPermi('ai:user:edit')")
|
||||||
|
@Log(title = "ai-用户信息", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/dept")
|
||||||
|
public AjaxResult assignDept(@RequestBody AiUser aiUser) {
|
||||||
|
if (aiUser.getId() == null) {
|
||||||
|
return error("用户主键不能为空");
|
||||||
|
}
|
||||||
|
return toAjax(aiUserService.updateAiUserDept(aiUser.getId(), aiUser.getDeptId()));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 状态修改
|
* 状态修改
|
||||||
*/
|
*/
|
||||||
|
|
@ -123,9 +130,12 @@ public class AiUserController extends BaseController {
|
||||||
aiUser.setUpdateBy(getUsername());
|
aiUser.setUpdateBy(getUsername());
|
||||||
// 获取原余额
|
// 获取原余额
|
||||||
AiUser u = aiUserService.selectAiUserById(aiUser.getId());
|
AiUser u = aiUserService.selectAiUserById(aiUser.getId());
|
||||||
|
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
|
||||||
|
String dateTime = new SimpleDateFormat("yyyyMMdd").format(new Date());
|
||||||
|
String orderNo = dateTime + uuid;
|
||||||
// 计算变更余额
|
// 计算变更余额
|
||||||
BigDecimal amount = NumberUtil.sub(aiUser.getBalance(), u.getBalance());
|
BigDecimal amount = NumberUtil.sub(aiUser.getBalance(), u.getBalance());
|
||||||
aiUserService.addUserBalance(aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION);
|
aiUserService.addUserBalance(orderNo, aiUser.getId(), amount, BalanceChangerConstants.SYSTEM_OPERATION);
|
||||||
return toAjax(1);
|
return toAjax(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,8 +161,6 @@ public class AiUserController extends BaseController {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// @GetMapping("/addUserBalance")
|
// @GetMapping("/addUserBalance")
|
||||||
// public AjaxResult addUserBalance(BigDecimal amount) {
|
// public AjaxResult addUserBalance(BigDecimal amount) {
|
||||||
// aiUserService.addUserBalance(SecurityUtils.getAiUserId(), amount, BalanceChangerConstants.RECHARGE);
|
// aiUserService.addUserBalance(SecurityUtils.getAiUserId(), amount, BalanceChangerConstants.RECHARGE);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import com.ruoyi.common.annotation.Anonymous;
|
||||||
import com.ruoyi.common.config.RuoYiConfig;
|
import com.ruoyi.common.config.RuoYiConfig;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.exception.base.BaseException;
|
import com.ruoyi.common.exception.base.BaseException;
|
||||||
import com.ruoyi.common.utils.AwsS3Util;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.common.utils.TencentCosUtil;
|
import com.ruoyi.common.utils.TencentCosUtil;
|
||||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||||
|
|
@ -36,7 +35,6 @@ public class CommonController {
|
||||||
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
|
private static final Logger log = LoggerFactory.getLogger(CommonController.class);
|
||||||
private final ServerConfig serverConfig;
|
private final ServerConfig serverConfig;
|
||||||
private final TencentCosUtil tencentCosUtil;
|
private final TencentCosUtil tencentCosUtil;
|
||||||
private final AwsS3Util awsS3Util;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用下载请求
|
* 通用下载请求
|
||||||
|
|
@ -158,7 +156,7 @@ public class CommonController {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AWS上传请求(单个)
|
* 腾讯云COS上传请求(单个)
|
||||||
*/
|
*/
|
||||||
@ApiOperation("图片上传接口")
|
@ApiOperation("图片上传接口")
|
||||||
@PostMapping("/aws/upload")
|
@PostMapping("/aws/upload")
|
||||||
|
|
@ -166,7 +164,7 @@ public class CommonController {
|
||||||
AjaxResult ajax = AjaxResult.success();
|
AjaxResult ajax = AjaxResult.success();
|
||||||
String uploadUrl;
|
String uploadUrl;
|
||||||
try {
|
try {
|
||||||
uploadUrl = awsS3Util.uploadMultipartFile(file, true);
|
uploadUrl = tencentCosUtil.uploadMultipartFile(file, true);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return AjaxResult.error(e.getMessage());
|
return AjaxResult.error(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,10 @@ spring:
|
||||||
druid:
|
druid:
|
||||||
# 主库数据源
|
# 主库数据源
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://xxxxx:xxxxx/byteai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
url: jdbc:mysql://database-1.cvs822qoc391.ap-southeast-1.rds.amazonaws.com:3306/byteai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
|
||||||
username: root
|
username: root
|
||||||
password: xxxxxx
|
password: mkMReisAKl6I7rVqEY90
|
||||||
driverClassName: com.mysql.cj.jdbc.Driver
|
driverClassName: com.mysql.cj.jdbc.Driver
|
||||||
|
|
||||||
# 初始连接数
|
# 初始连接数
|
||||||
initialSize: 5
|
initialSize: 5
|
||||||
# 最小连接池数量
|
# 最小连接池数量
|
||||||
|
|
@ -41,8 +40,8 @@ spring:
|
||||||
allow:
|
allow:
|
||||||
url-pattern: /druid/*
|
url-pattern: /druid/*
|
||||||
# 控制台管理用户名和密码
|
# 控制台管理用户名和密码
|
||||||
login-username:
|
login-username: ruoyi
|
||||||
login-password:
|
login-password: cvs822qoc391
|
||||||
filter:
|
filter:
|
||||||
stat:
|
stat:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
@ -52,5 +51,4 @@ spring:
|
||||||
merge-sql: true
|
merge-sql: true
|
||||||
wall:
|
wall:
|
||||||
config:
|
config:
|
||||||
multi-statement-allow: true
|
multi-statement-allow: true
|
||||||
|
|
||||||
|
|
@ -47,12 +47,16 @@ user:
|
||||||
|
|
||||||
# Spring配置
|
# Spring配置
|
||||||
spring:
|
spring:
|
||||||
# 邮件发送配置
|
# 邮件发送配置
|
||||||
mail:
|
mail:
|
||||||
host: smtp.qq.com # QQ邮箱SMTP服务器(163邮箱:smtp.163.com;Gmail:smtp.gmail.com)
|
# host: mailcow-self.undressing.name # QQ邮箱SMTP服务器(163邮箱:smtp.163.com;Gmail:smtp.gmail.com)
|
||||||
|
# port: 465 # SMTP端口(SSL加密端口:465,非加密:587,优先用587)
|
||||||
|
# username: undressing@mail.undressing.name # 你的邮箱地址
|
||||||
|
# password: aRtHEN39 # 邮箱授权码(不是登录密码!)
|
||||||
|
host: smtpdm-ap-southeast-1.aliyun.com # QQ邮箱SMTP服务器(163邮箱:smtp.163.com;Gmail:smtp.gmail.com)
|
||||||
port: 465 # SMTP端口(SSL加密端口:465,非加密:587,优先用587)
|
port: 465 # SMTP端口(SSL加密端口:465,非加密:587,优先用587)
|
||||||
username: # 你的邮箱地址
|
username: undressing@undressing.name # 你的邮箱地址
|
||||||
password: # 邮箱授权码(不是登录密码!)
|
password: 1284GOvkho # 邮箱授权码(不是登录密码!)
|
||||||
default-encoding: UTF-8 # 编码格式
|
default-encoding: UTF-8 # 编码格式
|
||||||
protocol: smtp
|
protocol: smtp
|
||||||
# SSL/TLS配置(可选,根据邮箱要求)
|
# SSL/TLS配置(可选,根据邮箱要求)
|
||||||
|
|
@ -89,15 +93,17 @@ spring:
|
||||||
# redis 配置
|
# redis 配置
|
||||||
redis:
|
redis:
|
||||||
# 地址
|
# 地址
|
||||||
host: localhost
|
host: master.redis.chguac.apse1.cache.amazonaws.com
|
||||||
# 端口,默认为6379
|
# 端口,默认为6379
|
||||||
port: 6379
|
port: 6379
|
||||||
# 数据库索引
|
# 数据库索引
|
||||||
database: 2
|
database: 2
|
||||||
# 密码
|
# 密码
|
||||||
password:
|
user: root
|
||||||
|
password: mkMReisAKl6I7rVqEY90
|
||||||
# 连接超时时间
|
# 连接超时时间
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
ssl: true
|
||||||
lettuce:
|
lettuce:
|
||||||
pool:
|
pool:
|
||||||
# 连接池中的最小空闲连接
|
# 连接池中的最小空闲连接
|
||||||
|
|
@ -108,7 +114,6 @@ spring:
|
||||||
max-active: 8
|
max-active: 8
|
||||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||||
max-wait: -1ms
|
max-wait: -1ms
|
||||||
|
|
||||||
# 自定义验证码配置
|
# 自定义验证码配置
|
||||||
verify:
|
verify:
|
||||||
code:
|
code:
|
||||||
|
|
@ -162,7 +167,8 @@ mybatis-plus:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
cache-enabled: true
|
cache-enabled: true
|
||||||
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
|
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||||
|
|
||||||
# PageHelper分页插件
|
# PageHelper分页插件
|
||||||
pagehelper:
|
pagehelper:
|
||||||
|
|
@ -204,42 +210,129 @@ google:
|
||||||
redirect-uri:
|
redirect-uri:
|
||||||
|
|
||||||
tencentCos:
|
tencentCos:
|
||||||
accessKey:
|
accessKey: AKIDBE3dzBdLsHYfZLwKVSFArLchZDerrfHf
|
||||||
secretKey:
|
secretKey: EDyUmsnX2IJ5f0oRn1QdeQ0TmrtqgQ1c
|
||||||
endpoint:
|
endpoint: ap-guangzhou
|
||||||
bucketName:
|
bucketName: seedance-1331490964
|
||||||
domain:
|
domain: https://seedance-1331490964.cos.ap-guangzhou.myqcloud.com
|
||||||
|
|
||||||
aws:
|
# aws配置已替换为腾讯云COS,请在环境变量或配置文件中设置腾讯云凭证
|
||||||
accessKey: AKIAYVMHEVDDZQGE3HVX
|
# aws:
|
||||||
secretKey: B9nxdferMhdRuxzoKeQam/NxiVvIhI7lSru6VfwG
|
# accessKey: AKIAYVMHEVDDZQGE3HVX
|
||||||
endpoint: ap-southeast-1
|
# secretKey: B9nxdferMhdRuxzoKeQam/NxiVvIhI7lSru6VfwG
|
||||||
bucketName: di-image
|
# endpoint: ap-southeast-1
|
||||||
domain: https://images.iqyjsnwv.com/
|
# bucketName: di-image
|
||||||
|
# domain: https://images.iqyjsnwv.com/
|
||||||
|
|
||||||
byteapi:
|
byteapi:
|
||||||
|
url:
|
||||||
|
apiKey:
|
||||||
|
callBackUrl:
|
||||||
|
|
||||||
|
volcengine:
|
||||||
|
region: cn-beijing
|
||||||
|
# 素材库等 ark 控制面 OpenAPI(与 byteapi 数据面推理地址无关)
|
||||||
|
# URL 请加引号;代码会去掉 https:// 再交给 SDK(SDK 自己会拼 https://,避免双 scheme)
|
||||||
|
openApiEndpoint: "https://ark.cn-beijing.volces.com"
|
||||||
|
ark:
|
||||||
|
baseUrl: "https://ark.cn-beijing.volces.com"
|
||||||
|
openApiVersion: "2024-01-01"
|
||||||
|
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
|
||||||
|
callbackUrl: http://47.86.170.114:8011/api/ai/volcCallback
|
||||||
|
ak: AKLTNmYyN2VhZTcyMDcxNDNlNzg3OGVlMDVmZjRhNWQwY2M
|
||||||
|
sk: Tm1ZeU1UTmlORFk1WmpKa05HUmpaRGcxTWpjMFpqUmpOVE01TUdJME5URQ==
|
||||||
|
projectAesKeyBase64: "gJajABVfQJ9xA94Q9IvQi68fqqhSIkfcKlG7pjGFt2U="
|
||||||
url: https://ark.ap-southeast.bytepluses.com/api/v3
|
url: https://ark.ap-southeast.bytepluses.com/api/v3
|
||||||
apiKey: 327d2815-2516-44c2-9e32-2dc50bf7afd7
|
apiKey: 3e33e034-7e25-4228-8864-b51b2a7a8f97
|
||||||
callBackUrl: www.google.com
|
callBackUrl: http://47.86.170.114:5173/
|
||||||
|
|
||||||
|
# 门户视频生成页:模型 / 比例 / 时长 / 分辨率均由此处维护,前后端不写死业务枚举
|
||||||
|
# 第三方图片素材审核(AES-256-ECB + Base64,与对端文档一致)
|
||||||
|
moderation:
|
||||||
|
image:
|
||||||
|
url: http://118.196.112.236:3428/api/moderation/image
|
||||||
|
user_id: 72
|
||||||
|
encryption_key_id: 3de57cb256df4bd7bb52297eaf363e81
|
||||||
|
aesHexKey: 7152a23b1a2a8759726a82e14c44c1e19d65b223f2469fe518577ce73c98245c
|
||||||
|
|
||||||
|
portal:
|
||||||
|
video:
|
||||||
|
# 与库表 ai_manager.type 一致(用于扣费);若报错 functionType does not exist,请插入对应 type 或改此处与库一致
|
||||||
|
function-type: "21"
|
||||||
|
defaults:
|
||||||
|
model: ep-20260326165811-dlkth
|
||||||
|
duration: 4
|
||||||
|
resolution: 720p
|
||||||
|
ratio: "3:4"
|
||||||
|
models:
|
||||||
|
- label: Seedance 2.0
|
||||||
|
value: ep-20260326165811-dlkth
|
||||||
|
- label: Seedance 2.0 Fast
|
||||||
|
value: ep-20260326170056-dkj9m
|
||||||
|
# 可选:按二级部门 dept_id 覆盖 YAML(仅当库表 sys_dept.model_parm 为空时才会用到)
|
||||||
|
# models-by-second-dept / defaults-by-second-dept 见 PortalVideoProperties 说明
|
||||||
|
ratios:
|
||||||
|
- "16:9"
|
||||||
|
- "9:16"
|
||||||
|
- "3:4"
|
||||||
|
- "1:1"
|
||||||
|
- "4:3"
|
||||||
|
durations:
|
||||||
|
- 4
|
||||||
|
- 5
|
||||||
|
- 6
|
||||||
|
- 8
|
||||||
|
- 10
|
||||||
|
- 11
|
||||||
|
- 12
|
||||||
|
- 13
|
||||||
|
- 14
|
||||||
|
- 15
|
||||||
|
resolutions:
|
||||||
|
- "720p"
|
||||||
|
- "1080p"
|
||||||
|
|
||||||
jinsha:
|
jinsha:
|
||||||
url: https://api.jinshapay.xyz
|
url: https://api.jinshapay.xyz
|
||||||
appId: 1763617360
|
appId: 1763617360
|
||||||
secret: a201e7969af5045dcd62d203b26121ae
|
secret: a201e7969af5045dcd62d203b26121ae
|
||||||
notifyUrl: www.google.com
|
notifyUrl: https://undressing.top
|
||||||
returnUrl: www.google.com
|
returnUrl: https://undressing.top
|
||||||
|
|
||||||
kada:
|
kada:
|
||||||
url: https://rapi.openkada.xyz
|
url: https://rapi.openkada.xyz
|
||||||
appId: c70f1719017e290354017d1c101d0cc288d06ceb
|
appId: d1743d48fb8fc24f38b7268015cf800e3b49f0fd
|
||||||
secret: ME2VRe6tWH6weK/NAUJA5lhmewHkB23rA6CdWlrHrAs+/E/E3j3eG3io/GCHbQKqMMurfTNrBj/R4Yy84UziM5YJheiKFKbsWQc5xRoE46E3/0EYy4ZjbK9jhwGyHS+C
|
secret: b0CH/+tVEsz+1j2mfBzd9Kgu6UylJxr0056TwTbkKfHWw9UW/6TaQyQHv+teBnGbqWy5ObaLUMvnrs9adpymebEqjI3ipNpJa7YPQbMYm0VGuYUEgeM+fjakhWuYx2XEVzmjdIvvfhNsfr2YHTmDUzwIKPbp/OJvfG9KhSPMzpw=
|
||||||
notifyUrl: www.google.com
|
notifyUrl: https://undressing.top
|
||||||
returnUrl: www.google.com
|
returnUrl: https://undressing.top
|
||||||
|
|
||||||
yuzhou:
|
yuzhou:
|
||||||
url: https://pay.joinus6688.cc
|
url: https://api.fast-vip.store
|
||||||
appId: PM20251211091945
|
appId: PM20251211091945
|
||||||
publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiTl8fg6eM6uUJqxAjGtzskt+ESIgferomy6uUtjRx4yhu6I4cVBgaw9ErJq9KMNQpMVl44GEese6PRDmNPdvXBktI/skpCfyNvT+1LqYm69Hh+rFre2Ve+0XIVoln0H3EGNUHL/KPOCm2tYXLXlZ3r02z+AQeS3rxNhE4jr32oQIDAQAB
|
publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCiTl8fg6eM6uUJqxAjGtzskt+ESIgferomy6uUtjRx4yhu6I4cVBgaw9ErJq9KMNQpMVl44GEese6PRDmNPdvXBktI/skpCfyNvT+1LqYm69Hh+rFre2Ve+0XIVoln0H3EGNUHL/KPOCm2tYXLXlZ3r02z+AQeS3rxNhE4jr32oQIDAQAB
|
||||||
secretKey: MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL7U/f7yb9Z9j56dauCUb0B/I0ONAcZDK+TtOnAgLEjV4qrirYHYuCumxYbPFvt6qYggjbBpphFhihbWzf8IPS7iD6VXoSX1T1iAlWFL1ZaBscqQSvPxFGtgpTBiRFS3KkZV70WswcCL770OCkY8+DJpdyk9OkD6vBa0TYU1wxJFAgMBAAECgYBpF+t5iBJHUYbSl2bQn25VWq8U+IbNpRh7TposPculoQTfj052f9+NSp7liw7hF8Bdk2/0g3pNgCYIRevUU7k9MEIKqHCiOWkyavtsfqGYI37PZ4/0uMzB5eibTqKTEkcyskSJ9GxrL4uGKgTGNc213i3VOcZZ4xEfvuDQCHF8gQJBAPATuhQeFNNAIE9TkGiESHFGChSZZgzp1xfGrAt8BwidBSe+r9duAcGJSeNJatxneeu0w6NuwQ6iq9ztnqtoG90CQQDLfSHrOTloHSa+DdBc2SFQWa/P4K2Tznb8Y5ng8L/t+a9sYvGjWOln0R3Bq9TrImm0AjWnq/saaMg2nYD1wr2JAkA36XA5xTO2a0XbE6wbG0u/zb8FQyCIO2GTsPpahl0g/Wi48+kB9CXGjBHANFYF1LeJVIUHqACgRvRdtJ1ycAGlAkEAqASVWiTw2p+fWrQLRG7gS/kR6uIIUI/cvT78UrhWsYdFqof0Hz0N0/PdzwkzkEbk4oYkiWK+viqgjj/0uHfoiQJAQhPYVVLHD7xiJApc/Aga6g0OFF5O7zy8KTsq+KTXqRlJREBH5nirSponHwYalEbUvtQrVs+Z4BCBEGCU8m2GEw==
|
secretKey: MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOyqpe98vNWLL/1ja4WnUkCGiHAVnNJtlw7c9bdacCc7xgGSlXIq1bRQWXMjLZeSC31BUFcrIMk5eMr1AWUTYnVOmAJ4IsLZCUMQOBU9S6BJdqnaus4K6Kul35lrb/Zt6ju6fY1acL7q3NWJGdeRtmYaGXZbF+RH6UbA6aH4vkHBAgMBAAECgYAEh+8EbveOYZCuGdpil82w9KUVpe5Hj5JQANOMpU+RdWPLiX7xDX2jqv2kFmEeiPeNbXk7AAABJjzoKtO84lz/n0Vp8hwqIgeMdmLZBBTsJhXgOfgnvwjb/1Ev2Yjg3KGp7nKvkyPpoU9e0W8+PR5yTclylV0bCYqsP260BFLnIQJBAPmqUzQLQN739dKoO3uVpB9qU3exZOv+zlTQZj2C0cXt3/+JzJs78DBieN9+b3ZLq307namt8QAUVV+QwRoNPxECQQDyq+QoAakpi1tQoGfYUecucDcZf8GDPdEkCGhGoIq/FkzL0gGub45Rz0oXzAzBME6DcobfVVippcUB54pueTexAkBxDTtX2bCqRkW9+gfVUlFbGF5rWJyGcH8l0Kg7Oj2bDrfbkp5fvKhqgGyTZ0E5o9InhxNBfk4e5xYxi+6kyVLBAkBcPahA6LizOOxhzkcKu78jMLZQ8/XLfCDWEHgKeJWkosZYJyBKfM7dG+zu9LnYaRM+9bZ8h8Vm3sLuwWMmMN9RAkBdOSqC71e9248Bmw70zX7x6ZmszGQ64toA6mhhcdYb4wAlDtmm0OJjJRmpyXdhgdIHF3i7vNe7sXpPfNKFc1Yf
|
||||||
redirectUrl: www.google.com
|
redirectUrl: https://undressing.top
|
||||||
callbackUrl: www.google.com
|
callbackUrl: https://undressing.top
|
||||||
|
vm:
|
||||||
|
url: http://payment-api.togame.top
|
||||||
|
mchNo: M1768983012
|
||||||
|
appId: 697089e4f41a4f456f159408
|
||||||
|
secret: 120tzr4snoq11yus8la9gx7cbutw1uore4pervckvqmsswrt1hl9qkd0ug5r6twwv94jex03ajpsmsky2za4x1kghd2l54z4nn7t5fcy4gewsvwjjxrce5q1f7u2yeqj
|
||||||
|
notifyUrl: https://undressing.top
|
||||||
|
# 支付方式,固定为BUZHI_VM(国际VM卡支付)
|
||||||
|
wayCode: BUZHI_VM
|
||||||
|
# 货币代码,默认USD
|
||||||
|
currency: USD
|
||||||
|
|
||||||
|
# 汇率服务配置
|
||||||
|
exchange-rate:
|
||||||
|
enabled: true
|
||||||
|
apikey: 0bed27315c87475f8dd6a0792a632cc5
|
||||||
|
api-url: https://api.currencyfreaks.com/v2.0/rates/latest
|
||||||
|
# base 货币(API返回的基准货币,通常为USD)
|
||||||
|
base-currency: USD
|
||||||
|
# 备用汇率(当API调用失败时使用)
|
||||||
|
fallback-rate-jinsha: 90
|
||||||
|
fallback-rate-kada: 60
|
||||||
|
# PHP比索的备用汇率(当API调用失败时使用,默认使用JinSha的汇率)
|
||||||
|
fallback-rate-php: 90
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,12 @@
|
||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Apache Commons Codec for Base64 encoding/decoding -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-codec</groupId>
|
||||||
|
<artifactId>commons-codec</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- excel工具 -->
|
<!-- excel工具 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||||
|
|
@ -54,8 +54,8 @@ public class AiUser extends BaseEntity {
|
||||||
@Excel(name = "手机号码")
|
@Excel(name = "手机号码")
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
/** 密码 */
|
/** 密码(仅接受入参反序列化,响应 Json 不输出,避免与 @JsonIgnore 导致入参丢失) */
|
||||||
@JsonIgnore
|
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
|
||||||
@Excel(name = "密码")
|
@Excel(name = "密码")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
|
@ -117,12 +117,19 @@ public class AiUser extends BaseEntity {
|
||||||
*/
|
*/
|
||||||
private String country;
|
private String country;
|
||||||
|
|
||||||
|
/** 归属部门ID(sys_dept.dept_id) */
|
||||||
|
private Long deptId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上级用户昵称
|
* 上级用户昵称
|
||||||
*/
|
*/
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String superiorName;
|
private String superiorName;
|
||||||
|
|
||||||
|
/** 归属部门名称 */
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String deptName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上级用户ID
|
* 上级用户ID
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,15 @@ public class SysDept extends BaseEntity
|
||||||
|
|
||||||
/** 父部门名称 */
|
/** 父部门名称 */
|
||||||
private String parentName;
|
private String parentName;
|
||||||
|
|
||||||
|
/** Byte API Key */
|
||||||
|
private String byteApiKey;
|
||||||
|
|
||||||
|
/** 门户视频模型列表 JSON:[{"label":"…","value":"ep-…"},…] */
|
||||||
|
private String modelParm;
|
||||||
|
|
||||||
|
/** Byte project */
|
||||||
|
private String project;
|
||||||
|
|
||||||
/** 子部门 */
|
/** 子部门 */
|
||||||
private List<SysDept> children = new ArrayList<SysDept>();
|
private List<SysDept> children = new ArrayList<SysDept>();
|
||||||
|
|
@ -171,6 +180,36 @@ public class SysDept extends BaseEntity
|
||||||
this.parentName = parentName;
|
this.parentName = parentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getByteApiKey()
|
||||||
|
{
|
||||||
|
return byteApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setByteApiKey(String byteApiKey)
|
||||||
|
{
|
||||||
|
this.byteApiKey = byteApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getModelParm()
|
||||||
|
{
|
||||||
|
return modelParm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setModelParm(String modelParm)
|
||||||
|
{
|
||||||
|
this.modelParm = modelParm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProject()
|
||||||
|
{
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProject(String project)
|
||||||
|
{
|
||||||
|
this.project = project;
|
||||||
|
}
|
||||||
|
|
||||||
public List<SysDept> getChildren()
|
public List<SysDept> getChildren()
|
||||||
{
|
{
|
||||||
return children;
|
return children;
|
||||||
|
|
@ -192,6 +231,9 @@ public class SysDept extends BaseEntity
|
||||||
.append("leader", getLeader())
|
.append("leader", getLeader())
|
||||||
.append("phone", getPhone())
|
.append("phone", getPhone())
|
||||||
.append("email", getEmail())
|
.append("email", getEmail())
|
||||||
|
.append("byteApiKey", getByteApiKey())
|
||||||
|
.append("modelParm", getModelParm())
|
||||||
|
.append("project", getProject())
|
||||||
.append("status", getStatus())
|
.append("status", getStatus())
|
||||||
.append("delFlag", getDelFlag())
|
.append("delFlag", getDelFlag())
|
||||||
.append("createBy", getCreateBy())
|
.append("createBy", getCreateBy())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateAssetGroup — POST /open/CreateAssetGroup(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "创建 Asset Group(素材资产组合)。首次创建需在控制台签署授权函。")
|
||||||
|
public class CreateAssetGroupRequest {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的名称,上限为 64 字符。", required = true)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的描述,上限为 300 字符。")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "Asset Group(素材资产组合)的类型。可选值:AIGC:虚拟人像。当前仅支持 AIGC 类型。")
|
||||||
|
private String groupType = "AIGC";
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "资源所属的项目名称,默认值为 default。若资源不在默认项目中,需填写正确的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateAsset — POST /open/CreateAsset 请求体(api.docx)。由服务端在上传存储桶后组装 URL。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "CreateAsset 第三方接口请求体")
|
||||||
|
public class CreateAssetRequest {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)所属的 Asset Group(素材资产组合)的 id。", required = true)
|
||||||
|
private String groupId;
|
||||||
|
|
||||||
|
@JsonProperty("URL")
|
||||||
|
@ApiModelProperty(value = "传入的 Asset(素材资产)的公共可访问地址。", required = true)
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的名称,上限为 64 个字符。仅用于 ListAssets 模糊搜索,不参与模型推理。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(allowableValues = "Image,Video,Audio", value = "Asset(素材资产)的类型。可选值:Image、Video、Audio。", required = true)
|
||||||
|
private String assetType;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "资源所属的项目名称,默认 default。需与所属 Asset Group 的 ProjectName 一致。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DeleteAsset — POST /open/DeleteAsset(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "删除单个 Asset(素材资产)")
|
||||||
|
public class DeleteAssetRequest {
|
||||||
|
@ApiModelProperty(value = "需要删除的 Asset(素材资产)的 id。", required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "需要删除的 Asset(素材资产)所属的项目名称,默认 default。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetAssetGroup — POST /open/GetAssetGroup(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "获取单个 Asset Group(素材资产组合)信息")
|
||||||
|
public class GetAssetGroupRequest {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的 id。", required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "需要查询的 Asset Group(素材资产组合)所属的项目名称,默认值为 default。若资源不在默认项目中,需填写正确的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GetAsset — POST /open/GetAsset(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "获取单个 Asset(素材资产)信息")
|
||||||
|
public class GetAssetRequest {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的 id。", required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "需要查询的 Asset 所属的项目名称,默认 default。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.ruoyi.common.core.request.asset.dto.ListAssetsGroupFilter;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListAssetGroups — POST /open/ListAssetGroups(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "查询符合筛选条件的 Asset Groups(素材资产组合)列表")
|
||||||
|
public class ListAssetsGroupRequest {
|
||||||
|
@ApiModelProperty(value = "搜索的过滤条件。", required = true)
|
||||||
|
private ListAssetsGroupFilter filter;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "搜索页码,可用于列表分页功能,从 1 开始。", required = true)
|
||||||
|
private int pageNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "每页搜索结果的数量,上限为 100。", required = true)
|
||||||
|
private int pageSize;
|
||||||
|
|
||||||
|
@ApiModelProperty(allowableValues = "CreateTime,UpdateTime", value = "用于排序的字段名称,默认值 createTime。支持:CreateTime(创建时间)、UpdateTime(更新时间)。")
|
||||||
|
private String sortBy;
|
||||||
|
|
||||||
|
@ApiModelProperty(allowableValues = "Desc,Asc", value = "排序顺序,默认值 Desc。可选值:Desc(降序)、Asc(升序)。")
|
||||||
|
private String sortOrder;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "资源所属的项目名称,默认值为 default。若资源不在默认项目中,需填写正确的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.ruoyi.common.core.request.asset.dto.ListAssetsFilter;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListAssets — POST /open/ListAssets(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "查询符合筛选条件的 Assets(素材资产)列表")
|
||||||
|
public class ListAssetsRequest {
|
||||||
|
@ApiModelProperty(value = "搜索的过滤条件。", required = true)
|
||||||
|
private ListAssetsFilter filter;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "搜索页码,从 1 开始。", required = true)
|
||||||
|
private int pageNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "每页搜索结果的数量,上限为 100。", required = true)
|
||||||
|
private int pageSize;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "排序字段,默认 createTime。支持:CreateTime、UpdateTime、GroupId。")
|
||||||
|
private String sortBy;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "排序顺序,默认 Desc。可选:Desc、Asc。")
|
||||||
|
private String sortOrder;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "资源所属的项目名称,默认 default。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateAssetGroup — POST /open/UpdateAssetGroup(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "更新单个 Asset Group(素材资产组合)信息。当前仅支持更新 Name 和 Description。")
|
||||||
|
public class UpdateAssetGroupRequest {
|
||||||
|
@ApiModelProperty(value = "需要更新的 Asset Group(素材资产组合)的 id。", required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "需要更新的 Asset Group(素材资产组合)的新名称,上限为 64 个字符。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "需要更新的 Asset Group(素材资产组合)的新描述,上限为 300 字符。")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "需要更新的 Asset Group(素材资产组合)所属的项目名称,默认值为 default。若资源不在默认项目中,需填写正确的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.ruoyi.common.core.request.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateAsset — POST /open/UpdateAsset(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "更新单个 Asset(素材资产)信息,当前仅支持更新 Name。")
|
||||||
|
public class UpdateAssetRequest {
|
||||||
|
@ApiModelProperty(value = "需要更新的 Asset(素材资产)的 id。", required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "需要更新的 Asset(素材资产)的新名称,上限为 64 个字符。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "需要更新的 Asset(素材资产)所属的项目名称,默认 default。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.ruoyi.common.core.request.asset.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListAssets 请求中的 Filter(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "ListAssets 过滤条件")
|
||||||
|
public class ListAssetsFilter {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)所属的 Asset Group(素材资产组合)的 id 列表。")
|
||||||
|
private List<String> groupIds;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "Asset Group(素材资产组合)的类型。可选值:AIGC:虚拟人像。", required = true)
|
||||||
|
private String groupType = "AIGC";
|
||||||
|
|
||||||
|
@ApiModelProperty(allowableValues = "Active,Processing,Failed", value = "任务状态列表:Active(已处理完毕)、Processing(预处理中)、Failed(处理失败)。")
|
||||||
|
private List<String> statuses;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的名称,上限为 64 个字符(模糊搜索)。")
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.ruoyi.common.core.request.asset.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListAssetGroups 请求中的 Filter 对象(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "ListAssetGroups 过滤条件")
|
||||||
|
public class ListAssetsGroupFilter {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的名称,上限为 64 个字符(模糊搜索)。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)所属的 Asset Group(素材资产组合)的 id 列表。")
|
||||||
|
private List<String> groupIds;
|
||||||
|
|
||||||
|
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||||
|
@ApiModelProperty(hidden = true, value = "Asset Group(素材资产组合)的类型。可选值:AIGC:虚拟人像。", required = true)
|
||||||
|
private String groupType = "AIGC";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "CreateAssetGroup 返回参数")
|
||||||
|
public class CreateAssetGroupResponse {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的 id。")
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "CreateAsset 返回参数")
|
||||||
|
public class CreateAssetResponse {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的 id。")
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "GetAssetGroup 返回参数")
|
||||||
|
public class GetAssetGroupResponse {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的 id。")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的名称,上限为 64 个字符。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的标题。已废弃,请直接使用参数 Name。")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的描述,上限为 300 字符。")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的类型。AIGC:虚拟人像。")
|
||||||
|
private String groupType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "资源所属的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间。")
|
||||||
|
private String createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间。")
|
||||||
|
private String updateTime;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.ruoyi.common.core.response.asset.dto.AssetError;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "GetAsset 返回参数")
|
||||||
|
public class GetAssetResponse {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的 id。")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的名称,上限为 64 个字符。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@JsonProperty("URL")
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的访问地址。有效期为 12 小时,请及时保存。")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的类型:Image、Video、Audio。")
|
||||||
|
private String assetType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)所属的 Asset Group(素材资产组合)的 id。")
|
||||||
|
private String groupId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "任务状态:Active、Processing、Failed。")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "错误信息。")
|
||||||
|
private AssetError error;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间。")
|
||||||
|
private String createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间。")
|
||||||
|
private String updateTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "资源所属的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.response.asset.dto.ListAssetsGroup;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "ListAssetGroups 返回参数")
|
||||||
|
public class ListAssetsGroupResponse {
|
||||||
|
@ApiModelProperty(value = "返回的 Asset Group(素材资产组合)的总数。")
|
||||||
|
private int totalCount;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "返回的页数。")
|
||||||
|
private int pageNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "每页搜索结果的数量,上限为 100。")
|
||||||
|
private int pageSize;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "符合筛选条件的 Asset Group(素材资产组合)数组。")
|
||||||
|
private List<ListAssetsGroup> items;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.response.asset.dto.ListAssetItem;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "ListAssets 返回参数")
|
||||||
|
public class ListAssetsResponse {
|
||||||
|
@ApiModelProperty(value = "符合筛选条件的 Asset 数组。")
|
||||||
|
private List<ListAssetItem> items;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "返回总数。")
|
||||||
|
private int totalCount;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "返回的页数。")
|
||||||
|
private int pageNumber;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "每页搜索结果的数量,上限为 100。")
|
||||||
|
private int pageSize;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "UpdateAssetGroup 返回参数")
|
||||||
|
public class UpdateAssetGroupResponse {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的 id。")
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.ruoyi.common.core.response.asset;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "UpdateAsset 返回参数")
|
||||||
|
public class UpdateAssetResponse {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的 id。")
|
||||||
|
private String id;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.ruoyi.common.core.response.asset.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "素材处理错误信息")
|
||||||
|
public class AssetError {
|
||||||
|
@ApiModelProperty(value = "错误码。")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "错误信息。")
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
package com.ruoyi.common.core.response.asset.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListAssets 返回 Items 元素(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "素材资产(列表项)")
|
||||||
|
public class ListAssetItem {
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的 id。")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的名称,上限为 64 个字符。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的公共可访问地址。有效期为 12 小时,请及时保存。")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)所属的 Asset Group(素材资产组合)的 id。")
|
||||||
|
private String groupId;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset(素材资产)的类型:Image、Video、Audio。")
|
||||||
|
private String assetType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "任务状态:Active、Processing、Failed。")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "错误信息(处理失败时)。")
|
||||||
|
private AssetError error;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "资源所属的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间。")
|
||||||
|
private String createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间。")
|
||||||
|
private String updateTime;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.ruoyi.common.core.response.asset.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListAssetGroups 返回 Items 元素(api.docx)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@ApiModel(description = "素材资产组合(列表项)")
|
||||||
|
public class ListAssetsGroup {
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的 id。")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的名称,上限为 64 个字符。")
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的标题。已废弃,请直接使用参数 Name。")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的描述,上限为 300 字符。")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "Asset Group(素材资产组合)的类型。AIGC:虚拟人像。")
|
||||||
|
private String groupType;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "资源所属的项目名称。")
|
||||||
|
private String projectName;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "创建时间。")
|
||||||
|
private String createTime;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "更新时间。")
|
||||||
|
private String updateTime;
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,13 @@ import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|
@ -33,40 +39,93 @@ public class TencentCosUtil {
|
||||||
private String domain;
|
private String domain;
|
||||||
|
|
||||||
|
|
||||||
//文件上传
|
/**
|
||||||
public String upload(MultipartFile file) {
|
* 上传MultipartFile到腾讯云COS,返回文件访问地址
|
||||||
|
* 与AwsS3Util.uploadMultipartFile方法接口兼容
|
||||||
|
*/
|
||||||
|
public String upload(MultipartFile file) throws Exception {
|
||||||
|
return uploadMultipartFile(file, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传MultipartFile到腾讯云COS,返回文件访问地址
|
||||||
|
*
|
||||||
|
* @param file 前端上传的MultipartFile
|
||||||
|
* @param isPublic 是否公开访问(当前实现中忽略,使用domain配置)
|
||||||
|
* @return 文件访问地址(URL字符串)
|
||||||
|
*/
|
||||||
|
public String uploadMultipartFile(MultipartFile file, boolean isPublic) throws Exception {
|
||||||
|
return uploadMultipartFile(file, isPublic, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param objectKeyPrefix 对象键前缀,如 {@code asset},最终 key 为 {@code asset/yyyy/MM/dd/uuid_name};{@code null} 或空则不添加
|
||||||
|
*/
|
||||||
|
public String uploadMultipartFile(MultipartFile file, boolean isPublic, String objectKeyPrefix) throws Exception {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("上传文件不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
// 3 生成 cos 客户端。
|
|
||||||
COSClient cosClient = createCosClient();
|
COSClient cosClient = createCosClient();
|
||||||
|
|
||||||
// 存储桶的命名格式为 BucketName-APPID,此处填写的存储桶名称必须为此格式
|
// 生成唯一文件键,格式与AWS一致:yyyy/MM/dd/uuid_filename
|
||||||
// 对象键(Key)是对象在存储桶中的唯一标识。 998u-09iu-09i-333
|
String key = generateCosKey(file.getOriginalFilename());
|
||||||
//在文件名称前面添加uuid值
|
if (objectKeyPrefix != null && !objectKeyPrefix.trim().isEmpty()) {
|
||||||
String key = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8) + "_"
|
String p = objectKeyPrefix.trim().replaceAll("^/+", "").replaceAll("/+$", "");
|
||||||
+ file.getOriginalFilename();
|
key = p + "/" + key;
|
||||||
//对上传文件分组,根据当前日期 /2022/11/11
|
}
|
||||||
String dateTime = new DateTime().toString("yyyy/MM/dd");
|
|
||||||
key = dateTime + "/" + key;
|
|
||||||
try {
|
try {
|
||||||
//获取上传文件输入流
|
|
||||||
InputStream inputStream = file.getInputStream();
|
InputStream inputStream = file.getInputStream();
|
||||||
ObjectMetadata objectMetadata = new ObjectMetadata();
|
ObjectMetadata objectMetadata = new ObjectMetadata();
|
||||||
|
// 必须设置长度,否则底层 OkHttp 会告警「No content length specified…」并整段缓冲到内存,易 OOM
|
||||||
|
long contentLength = file.getSize();
|
||||||
|
if (contentLength >= 0) {
|
||||||
|
objectMetadata.setContentLength(contentLength);
|
||||||
|
}
|
||||||
|
if (file.getContentType() != null && !file.getContentType().isEmpty()) {
|
||||||
|
objectMetadata.setContentType(file.getContentType());
|
||||||
|
}
|
||||||
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
PutObjectRequest putObjectRequest = new PutObjectRequest(
|
||||||
bucketName,
|
bucketName,
|
||||||
key,
|
key,
|
||||||
inputStream,
|
inputStream,
|
||||||
objectMetadata);
|
objectMetadata);
|
||||||
// 高级接口会返回一个异步结果Upload
|
|
||||||
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest);
|
||||||
|
|
||||||
//返回上传文件路径
|
// 返回COS文件访问地址
|
||||||
//https://ggkt-atguigu-1310644373.cos.ap-beijing.myqcloud.com/01.jpg
|
String url = domain + (domain.endsWith("/") ? "" : "/") + key;
|
||||||
String url = domain + "/" + key;
|
|
||||||
return url;
|
return url;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException("上传文件到COS失败: " + e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
cosClient.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过URL下载文件并上传到COS
|
||||||
|
* 与AwsS3Util.uploadFileByUrl方法接口兼容
|
||||||
|
*/
|
||||||
|
public String uploadFileByUrl(String fileUrl) throws Exception {
|
||||||
|
return uploadFileByUrl(fileUrl, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String uploadFileByUrl(String fileUrl, boolean isPublic) throws Exception {
|
||||||
|
if (fileUrl == null || fileUrl.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("文件下载链接不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path tempPath = downloadFileToTemp(fileUrl);
|
||||||
|
try {
|
||||||
|
// 使用临时文件上传
|
||||||
|
MultipartFile multipartFile = createMultipartFileFromPath(tempPath, extractFileNameFromUrl(fileUrl));
|
||||||
|
return uploadMultipartFile(multipartFile, isPublic);
|
||||||
|
} finally {
|
||||||
|
Files.deleteIfExists(tempPath);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -78,7 +137,107 @@ public class TencentCosUtil {
|
||||||
ClientConfig clientConfig = new ClientConfig(region);
|
ClientConfig clientConfig = new ClientConfig(region);
|
||||||
// 这里建议设置使用 https 协议
|
// 这里建议设置使用 https 协议
|
||||||
clientConfig.setHttpProtocol(HttpProtocol.https);
|
clientConfig.setHttpProtocol(HttpProtocol.https);
|
||||||
|
clientConfig.setConnectionTimeout(30 * 1000); // 连接超时30秒
|
||||||
|
clientConfig.setSocketTimeout(60 * 1000); // 读取超时60秒
|
||||||
//1.3 生成cos客户端
|
//1.3 生成cos客户端
|
||||||
return new COSClient(credentials, clientConfig);
|
return new COSClient(credentials, clientConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成COS文件键,与AWS保持一致的命名格式
|
||||||
|
*/
|
||||||
|
private String generateCosKey(String originalFileName) {
|
||||||
|
String uuid = UUID.randomUUID().toString().replaceAll("-", "").substring(0, 8);
|
||||||
|
String dateTime = new DateTime().toString("yyyy/MM/dd");
|
||||||
|
return dateTime + "/" + uuid + "_" + originalFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件到临时路径
|
||||||
|
*/
|
||||||
|
private Path downloadFileToTemp(String fileUrl) throws Exception {
|
||||||
|
String suffix = getFileSuffixFromUrl(fileUrl);
|
||||||
|
Path tempPath = Files.createTempFile("url-upload-", suffix);
|
||||||
|
|
||||||
|
HttpURLConnection connection = null;
|
||||||
|
InputStream in = null;
|
||||||
|
OutputStream out = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
URL url = new URL(fileUrl);
|
||||||
|
connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(5000);
|
||||||
|
connection.setReadTimeout(10000);
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode < 200 || responseCode >= 300) {
|
||||||
|
throw new RuntimeException("文件下载失败,状态码:" + responseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
in = connection.getInputStream();
|
||||||
|
out = Files.newOutputStream(tempPath);
|
||||||
|
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (out != null) {
|
||||||
|
try { out.close(); } catch (IOException e) { e.printStackTrace(); }
|
||||||
|
}
|
||||||
|
if (in != null) {
|
||||||
|
try { in.close(); } catch (IOException e) { e.printStackTrace(); }
|
||||||
|
}
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tempPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFileNameFromUrl(String fileUrl) {
|
||||||
|
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
|
||||||
|
if (fileName.contains("?")) {
|
||||||
|
fileName = fileName.split("\\?")[0];
|
||||||
|
}
|
||||||
|
return fileName.isEmpty() ? "default_file" : fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getFileSuffixFromUrl(String fileUrl) {
|
||||||
|
String fileName = extractFileNameFromUrl(fileUrl);
|
||||||
|
if (fileName.contains(".")) {
|
||||||
|
return fileName.substring(fileName.lastIndexOf("."));
|
||||||
|
}
|
||||||
|
return ".tmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Path转换为MultipartFile(简单实现,用于uploadFileByUrl)
|
||||||
|
*/
|
||||||
|
private MultipartFile createMultipartFileFromPath(Path path, String originalFilename) throws IOException {
|
||||||
|
byte[] bytes = Files.readAllBytes(path);
|
||||||
|
return new MultipartFile() {
|
||||||
|
@Override
|
||||||
|
public String getName() { return "file"; }
|
||||||
|
@Override
|
||||||
|
public String getOriginalFilename() { return originalFilename; }
|
||||||
|
@Override
|
||||||
|
public String getContentType() { return null; }
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() { return bytes.length == 0; }
|
||||||
|
@Override
|
||||||
|
public long getSize() { return bytes.length; }
|
||||||
|
@Override
|
||||||
|
public byte[] getBytes() throws IOException { return bytes; }
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException { return Files.newInputStream(path); }
|
||||||
|
@Override
|
||||||
|
public void transferTo(java.io.File dest) throws IOException, IllegalStateException {
|
||||||
|
Files.copy(path, dest.toPath());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.ruoyi.common.utils.crypto;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-256-ECB + PKCS5Padding(与 PKCS7 在 16 字节块上等价),密钥为 32 字节。
|
||||||
|
* 密文传输形态:Base64 字符串。
|
||||||
|
*/
|
||||||
|
public final class Aes256EcbPkcs5 {
|
||||||
|
|
||||||
|
private static final int AES_KEY_BYTES = 32;
|
||||||
|
private static final String AES_ECB_PKCS5 = "AES/ECB/PKCS5Padding";
|
||||||
|
|
||||||
|
private Aes256EcbPkcs5() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param hex 64 位十六进制字符串(32 字节密钥)
|
||||||
|
*/
|
||||||
|
public static byte[] hexStringToBytes(String hex) {
|
||||||
|
if (hex == null || hex.length() != AES_KEY_BYTES * 2) {
|
||||||
|
throw new IllegalArgumentException("AES-256 hex key must be 64 hex characters");
|
||||||
|
}
|
||||||
|
int n = hex.length() / 2;
|
||||||
|
byte[] out = new byte[n];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
int hi = Character.digit(hex.charAt(i * 2), 16);
|
||||||
|
int lo = Character.digit(hex.charAt(i * 2 + 1), 16);
|
||||||
|
if (hi < 0 || lo < 0) {
|
||||||
|
throw new IllegalArgumentException("invalid hex in AES key");
|
||||||
|
}
|
||||||
|
out[i] = (byte) ((hi << 4) + lo);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String encryptToBase64(String plainUtf8, byte[] key256) throws GeneralSecurityException {
|
||||||
|
if (plainUtf8 == null) {
|
||||||
|
throw new IllegalArgumentException("plain text must not be null");
|
||||||
|
}
|
||||||
|
if (key256 == null || key256.length != AES_KEY_BYTES) {
|
||||||
|
throw new IllegalArgumentException("AES key must be 32 bytes");
|
||||||
|
}
|
||||||
|
Cipher cipher = Cipher.getInstance(AES_ECB_PKCS5);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key256, "AES"));
|
||||||
|
byte[] cipherBytes = cipher.doFinal(plainUtf8.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Base64.getEncoder().encodeToString(cipherBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decryptFromBase64(String base64Cipher, byte[] key256) throws GeneralSecurityException {
|
||||||
|
if (base64Cipher == null || base64Cipher.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("empty ciphertext");
|
||||||
|
}
|
||||||
|
if (key256 == null || key256.length != AES_KEY_BYTES) {
|
||||||
|
throw new IllegalArgumentException("AES key must be 32 bytes");
|
||||||
|
}
|
||||||
|
byte[] raw = Base64.getDecoder().decode(base64Cipher.trim());
|
||||||
|
Cipher cipher = Cipher.getInstance(AES_ECB_PKCS5);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key256, "AES"));
|
||||||
|
byte[] plain = cipher.doFinal(raw);
|
||||||
|
return new String(plain, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.ruoyi.common.utils.crypto;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES-256-CBC PKCS5 加/解密成对使用。密文存库形态:整段 Base64( IV(16字节) ‖ 密文 ),无前缀。
|
||||||
|
*/
|
||||||
|
public final class AesCbcPkcs5Decrypt {
|
||||||
|
|
||||||
|
private static final int IV_LENGTH = 16;
|
||||||
|
private static final int AES_KEY_LENGTH = 32;
|
||||||
|
private static final String AES_CBC_PKCS5 = "AES/CBC/PKCS5Padding";
|
||||||
|
|
||||||
|
private AesCbcPkcs5Decrypt() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用随机 IV 加密明文,输出与 {@link #decrypt(String, byte[])} 输入格式一致。
|
||||||
|
*
|
||||||
|
* @param plainText UTF-8 明文;{@code null} 非法
|
||||||
|
* @param aes256Key 32 字节 AES-256 密钥
|
||||||
|
* @return Base64( IV ‖ ciphertext ),无换行
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText, byte[] aes256Key) throws GeneralSecurityException {
|
||||||
|
if (plainText == null) {
|
||||||
|
throw new IllegalArgumentException("plain text must not be null");
|
||||||
|
}
|
||||||
|
if (aes256Key == null || aes256Key.length != AES_KEY_LENGTH) {
|
||||||
|
throw new IllegalArgumentException("aes key must be 32 bytes");
|
||||||
|
}
|
||||||
|
byte[] iv = new byte[IV_LENGTH];
|
||||||
|
SECURE_RANDOM.nextBytes(iv);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(aes256Key, "AES");
|
||||||
|
Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(iv));
|
||||||
|
byte[] cipherBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||||
|
byte[] combined = new byte[IV_LENGTH + cipherBytes.length];
|
||||||
|
System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
|
||||||
|
System.arraycopy(cipherBytes, 0, combined, IV_LENGTH, cipherBytes.length);
|
||||||
|
return Base64.getEncoder().encodeToString(combined);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param base64IvAndCipher Base64( IV ‖ ciphertext )
|
||||||
|
* @param aes256Key 32 字节 AES-256 密钥
|
||||||
|
* @return UTF-8 明文
|
||||||
|
*/
|
||||||
|
public static String decrypt(String base64IvAndCipher, byte[] aes256Key) throws GeneralSecurityException {
|
||||||
|
if (base64IvAndCipher == null || base64IvAndCipher.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("empty payload");
|
||||||
|
}
|
||||||
|
if (aes256Key == null || aes256Key.length != AES_KEY_LENGTH) {
|
||||||
|
throw new IllegalArgumentException("aes key must be 32 bytes");
|
||||||
|
}
|
||||||
|
byte[] combined = Base64.getDecoder().decode(base64IvAndCipher.trim());
|
||||||
|
if (combined.length <= IV_LENGTH) {
|
||||||
|
throw new GeneralSecurityException("payload too short after base64 decode");
|
||||||
|
}
|
||||||
|
byte[] iv = Arrays.copyOfRange(combined, 0, IV_LENGTH);
|
||||||
|
byte[] ciphertext = Arrays.copyOfRange(combined, IV_LENGTH, combined.length);
|
||||||
|
SecretKeySpec keySpec = new SecretKeySpec(aes256Key, "AES");
|
||||||
|
Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));
|
||||||
|
byte[] plain = cipher.doFinal(ciphertext);
|
||||||
|
return new String(plain, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,19 @@
|
||||||
package com.ruoyi.common.utils.sign;
|
package com.ruoyi.common.utils.sign;
|
||||||
|
|
||||||
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONObject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
import java.security.interfaces.RSAPublicKey;
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.security.spec.X509EncodedKeySpec;
|
import java.security.spec.X509EncodedKeySpec;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -16,25 +21,29 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class RSAUtils {
|
public class RSAUtils {
|
||||||
|
|
||||||
// RSA最⼤加密明⽂⼤⼩
|
// RSA最大加密明文大小
|
||||||
private static final int MAX_ENCRYPT_BLOCK = 117;
|
private static final int MAX_ENCRYPT_BLOCK = 117;
|
||||||
// 不仅可以使⽤DSA算法,同样也可以使⽤RSA算法做数字签名
|
// 不仅可以使用DSA算法,同样也可以使用RSA算法做数字签名
|
||||||
private static final String KEY_ALGORITHM = "RSA";
|
private static final String KEY_ALGORITHM = "RSA";
|
||||||
|
|
||||||
public static String encryptByPrivateKey(String str, String privateKey) throws Exception {
|
public static String encryptByPrivateKey(String str, String privateKey)
|
||||||
|
throws InvalidKeySpecException, NoSuchAlgorithmException, javax.crypto.NoSuchPaddingException, java.security.InvalidKeyException,
|
||||||
|
javax.crypto.IllegalBlockSizeException, javax.crypto.BadPaddingException, UnsupportedEncodingException {
|
||||||
// base64编码的公钥
|
// base64编码的公钥
|
||||||
byte[] keyBytes = decryptBASE64(privateKey);
|
byte[] keyBytes = decryptBASE64(privateKey);
|
||||||
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance(KEY_ALGORITHM)
|
||||||
|
.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
// RSA加密
|
// RSA加密
|
||||||
Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
|
Cipher cipher = Cipher.getInstance(KEY_ALGORITHM);
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, priKey);
|
cipher.init(Cipher.ENCRYPT_MODE, priKey);
|
||||||
byte[] data = str.getBytes("UTF-8");
|
byte[] data = str.getBytes("UTF-8");
|
||||||
// 加密时超过117字节就报错。为此采⽤分段加密的办法来加密
|
// 加密时超过117字节就报错。为此采用分段加密的办法来加密
|
||||||
byte[] enBytes = null;
|
byte[] enBytes = null;
|
||||||
for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) {
|
for (int i = 0; i < data.length; i += MAX_ENCRYPT_BLOCK) {
|
||||||
// 注意要使⽤2的倍数,否则会出现加密后的内容再解密时为乱码
|
// 注意要使用2的倍数,否则会出现加密后的内容再解密时为乱码
|
||||||
byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK));
|
byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(data, i, i + MAX_ENCRYPT_BLOCK));
|
||||||
enBytes = ArrayUtils.addAll(enBytes, doFinal);
|
enBytes = ArrayUtils.addAll(enBytes, doFinal);
|
||||||
}
|
}
|
||||||
|
|
@ -43,15 +52,22 @@ public class RSAUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String encryptBASE64(byte[] data) {
|
private static String encryptBASE64(byte[] data) {
|
||||||
return new String(Base64.encode(data));
|
return new String(Base64.encodeBase64(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] decryptBASE64(String data) {
|
private static byte[] decryptBASE64(String data) {
|
||||||
return Base64.decode(data);
|
return Base64.decodeBase64(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify signature
|
||||||
|
*
|
||||||
|
* @param params
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
public static boolean verifySign(JSONObject params, String publickey) {
|
public static boolean verifySign(JSONObject params, String publickey) {
|
||||||
String platSign = params.getStr("signature"); // sign
|
String platSign = params.getStr("signature"); // sign
|
||||||
|
log.info("signature:" + platSign);
|
||||||
List<String> paramNameList = new ArrayList<>();
|
List<String> paramNameList = new ArrayList<>();
|
||||||
for (String key : params.keySet()) {
|
for (String key : params.keySet()) {
|
||||||
if (!"signature".equals(key)) {
|
if (!"signature".equals(key)) {
|
||||||
|
|
@ -64,13 +80,14 @@ public class RSAUtils {
|
||||||
String name = paramNameList.get(i);
|
String name = paramNameList.get(i);
|
||||||
stringBuilder.append(params.getStr(name));
|
stringBuilder.append(params.getStr(name));
|
||||||
}
|
}
|
||||||
|
log.info("keys:" + stringBuilder);
|
||||||
String decryptSign = "";
|
String decryptSign = "";
|
||||||
try {
|
try {
|
||||||
decryptSign = publicDecrypt(platSign, getPublicKey(publickey)
|
decryptSign = publicDecrypt(platSign, getPublicKey(publickey));
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println(e.toString());
|
log.error("Error decrypting signature", e);
|
||||||
}
|
}
|
||||||
|
log.info("decryptSign:" + decryptSign);
|
||||||
if (!stringBuilder.toString().equalsIgnoreCase(decryptSign)) {
|
if (!stringBuilder.toString().equalsIgnoreCase(decryptSign)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -89,11 +106,12 @@ public class RSAUtils {
|
||||||
stringBuilder.append(createMap.get(key)); // 拼接参数
|
stringBuilder.append(createMap.get(key)); // 拼接参数
|
||||||
}
|
}
|
||||||
String keyStr = stringBuilder.toString(); // 得到待加密的字符串
|
String keyStr = stringBuilder.toString(); // 得到待加密的字符串
|
||||||
|
log.info("keyStr:" + keyStr);
|
||||||
String signedStr = "";
|
String signedStr = "";
|
||||||
try {
|
try {
|
||||||
signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密
|
signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println(e.toString());
|
log.error("Error encrypting signature", e);
|
||||||
}
|
}
|
||||||
return signedStr;
|
return signedStr;
|
||||||
}
|
}
|
||||||
|
|
@ -110,47 +128,70 @@ public class RSAUtils {
|
||||||
stringBuilder.append(createMap.get(key)); // 拼接参数
|
stringBuilder.append(createMap.get(key)); // 拼接参数
|
||||||
}
|
}
|
||||||
String keyStr = stringBuilder.toString(); // 得到待加密的字符串
|
String keyStr = stringBuilder.toString(); // 得到待加密的字符串
|
||||||
|
log.info("keyStr:" + keyStr);
|
||||||
String signedStr = "";
|
String signedStr = "";
|
||||||
try {
|
try {
|
||||||
signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密
|
signedStr = privateEncrypt(keyStr, getPrivateKey(MCH_PRIVATE_KEY)); // 私钥加密
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println(e.toString());
|
log.error("Error encrypting signature", e);
|
||||||
}
|
}
|
||||||
return signedStr;
|
return signedStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* private key encryption
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* @param privateKey
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
|
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
|
||||||
try {
|
try {
|
||||||
Cipher cipher = Cipher.getInstance("RSA");
|
Cipher cipher = Cipher.getInstance("RSA");
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
|
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
|
||||||
return Base64.encode(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes("UTF-8"), privateKey.getModulus().bitLength()));
|
return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes("UTF-8"), privateKey.getModulus().bitLength()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Exception encountered while encry pting string [" + data + "]", e);
|
throw new RuntimeException("Exception encountered while encrypting string [" + data + "]", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* public key decryption
|
||||||
|
*/
|
||||||
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
|
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
|
||||||
try {
|
try {
|
||||||
Cipher cipher = Cipher.getInstance("RSA");
|
Cipher cipher = Cipher.getInstance("RSA");
|
||||||
cipher.init(Cipher.DECRYPT_MODE, publicKey);
|
cipher.init(Cipher.DECRYPT_MODE, publicKey);
|
||||||
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decode(data), publicKey.getModulus().bitLength()), "UTF-8");
|
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), "UTF-8");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("An exception was encountered whil e decrypting the string[" + data + "]", e);
|
throw new RuntimeException("An exception was encountered while decrypting the string[" + data + "]", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RSAPrivateKey getPrivateKey(String privateKey) throws Exception {
|
/**
|
||||||
|
* get private key
|
||||||
|
*
|
||||||
|
* @param privateKey key string (base64 encoded)
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
//Get the private key object through the PKCS#8 encoded Key instruction
|
//Get the private key object through the PKCS#8 encoded Key instruction
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||||
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decode(privateKey));
|
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
|
||||||
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
|
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RSAPublicKey getPublicKey(String publicKey) throws Exception {
|
/**
|
||||||
|
* get the public key
|
||||||
|
*
|
||||||
|
* @param publicKey key string (base64 encoded)
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
//Get the public key object through the X509 encoded Key instruction
|
//Get the public key object through the X509 encoded Key instruction
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||||
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decode(publicKey));
|
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
|
||||||
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
|
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +220,7 @@ public class RSAUtils {
|
||||||
offSet = i * maxBlock;
|
offSet = i * maxBlock;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("An exception occurred when encryp ting and decrypting data whose threshold is [" + maxBlock + "]", e);
|
throw new RuntimeException("An exception occurred when encrypting and decrypting data whose threshold is [" + maxBlock + "]", e);
|
||||||
}
|
}
|
||||||
byte[] resultDatas = out.toByteArray();
|
byte[] resultDatas = out.toByteArray();
|
||||||
IOUtils.closeQuietly(out);
|
IOUtils.closeQuietly(out);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
package com.ruoyi.ai.domain;
|
package com.ruoyi.ai.domain;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
|
||||||
import com.ruoyi.common.annotation.Excel;
|
import com.ruoyi.common.annotation.Excel;
|
||||||
import com.ruoyi.common.core.domain.BaseEntity;
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 余额使用记录对象 ai_balance_change_record
|
* 余额使用记录对象 ai_balance_change_record
|
||||||
|
|
@ -24,13 +22,23 @@ public class AiBalanceChangeRecord extends BaseEntity {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
/** 主键ID */
|
/**
|
||||||
|
* 主键ID
|
||||||
|
*/
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
/** 删除标志(0代表存在 2代表删除) */
|
/**
|
||||||
|
* 删除标志(0代表存在 2代表删除)
|
||||||
|
*/
|
||||||
private String delFlag;
|
private String delFlag;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关联订单号
|
||||||
|
*/
|
||||||
|
@Excel(name = "关联订单号")
|
||||||
|
private String orderNo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户ID
|
* 用户ID
|
||||||
*/
|
*/
|
||||||
|
|
@ -42,19 +50,27 @@ public class AiBalanceChangeRecord extends BaseEntity {
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String uuid;
|
private String uuid;
|
||||||
|
|
||||||
/** 操作类型:0-充值 1-返佣 2-充值赠送 3-体验金赠送 4-体验金回收 5-图生图 6-一键换脸 7-快捷生图 8-快捷生视频 9-退款 10-系统操作 */
|
/**
|
||||||
|
* 操作类型:0-充值 1-返佣 2-充值赠送 3-体验金赠送 4-体验金回收 5-图生图 6-一键换脸 7-快捷生图 8-快捷生视频 9-退款 10-系统操作
|
||||||
|
*/
|
||||||
@Excel(name = "操作类型")
|
@Excel(name = "操作类型")
|
||||||
private Integer type;
|
private Integer type;
|
||||||
|
|
||||||
/** 变更金额 */
|
/**
|
||||||
|
* 变更金额
|
||||||
|
*/
|
||||||
@Excel(name = "变更金额")
|
@Excel(name = "变更金额")
|
||||||
private BigDecimal changeAmount;
|
private BigDecimal changeAmount;
|
||||||
|
|
||||||
/** 变更后金额 */
|
/**
|
||||||
|
* 变更后金额
|
||||||
|
*/
|
||||||
@Excel(name = "变更后金额")
|
@Excel(name = "变更后金额")
|
||||||
private BigDecimal resultAmount;
|
private BigDecimal resultAmount;
|
||||||
|
|
||||||
/** 用户昵称 */
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
*/
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
@Excel(name = "用户昵称")
|
@Excel(name = "用户昵称")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue