Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
S
saas-dml
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
saas-dml
Commits
8ce62951
提交
8ce62951
authored
7月 14, 2025
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat: 新增数字人管理
上级
6521fc27
显示空白字符变更
内嵌
并排
正在显示
7 个修改的文件
包含
4800 行增加
和
1 行删除
+4800
-1
text-creation-common.css
src/assets/css/text-creation-common.css
+1834
-0
index.ts
src/modules/material/digital-human/index.ts
+12
-0
BaiduDigitalHuman.vue
...odules/material/digital-human/views/BaiduDigitalHuman.vue
+1963
-0
menu.ts
src/stores/menu.ts
+8
-1
bosDirectUpload.js
src/utils/bosDirectUpload.js
+645
-0
digitalHumanAPI.js
src/utils/digitalHumanAPI.js
+333
-0
vite.config.ts
vite.config.ts
+5
-0
没有找到文件。
src/assets/css/text-creation-common.css
0 → 100644
浏览文件 @
8ce62951
/* 文本创作中心通用样式 */
/* ========== 页面布局 ========== */
.text-creation-page
{
padding
:
0
;
margin-top
:
0
;
border
:
none
;
border-top
:
none
!important
;
border-bottom
:
none
!important
;
box-shadow
:
none
!important
;
}
.page-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
5px
;
padding
:
0
;
border
:
none
!important
;
border-top
:
none
!important
;
border-bottom
:
none
!important
;
box-shadow
:
none
!important
;
background-image
:
none
!important
;
}
.page-header
::after
,
.page-header
::before
,
.page-nav
::after
,
.page-nav
::before
,
.page-nav
h2
::after
,
.page-nav
h2
::before
,
.text-creation-page
::after
,
.text-creation-page
::before
{
display
:
none
!important
;
content
:
none
!important
;
border
:
none
!important
;
border-top
:
none
!important
;
border-bottom
:
none
!important
;
background
:
none
!important
;
background-image
:
none
!important
;
height
:
0
!important
;
width
:
0
!important
;
opacity
:
0
!important
;
visibility
:
hidden
!important
;
}
.page-nav
{
border
:
none
!important
;
border-top
:
none
!important
;
border-bottom
:
none
!important
;
background-image
:
none
!important
;
}
.page-nav
h2
{
font-size
:
22px
;
color
:
#333
;
margin
:
0
;
border
:
none
!important
;
border-top
:
none
!important
;
border-bottom
:
none
!important
;
padding-bottom
:
0
;
position
:
relative
;
background-image
:
none
!important
;
text-decoration
:
none
!important
;
}
.page-actions
{
display
:
flex
;
gap
:
8px
;
}
/* ========== 主容器和布局 ========== */
.main-container
{
display
:
flex
;
gap
:
15px
;
margin-top
:
5px
;
}
.input-section
{
width
:
45%
;
background-color
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
10px
rgba
(
0
,
0
,
0
,
0.05
);
padding
:
12px
;
}
.right-column
{
width
:
55%
;
display
:
flex
;
flex-direction
:
column
;
gap
:
12px
;
}
/* ========== 通用区块 ========== */
.section-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
10px
16px
;
background-color
:
#fff
;
border-top-left-radius
:
8px
;
border-top-right-radius
:
8px
;
}
.section-title
{
display
:
flex
;
align-items
:
center
;
color
:
var
(
--primary-color
,
#ba003f
);
font-size
:
16px
;
margin
:
0
0
10px
0
;
font-weight
:
600
;
}
.section-title
i
{
margin-right
:
8px
;
font-size
:
20px
;
color
:
var
(
--primary-color
,
#ba003f
);
}
/* ========== 表单元素样式 ========== */
.form-row
{
display
:
flex
;
gap
:
12px
;
margin-bottom
:
12px
;
}
.form-group
{
flex
:
1
;
margin-bottom
:
12px
;
border-bottom
:
none
!important
;
position
:
relative
;
transition
:
all
0.3s
ease
;
}
/* 修改这条规则,它会导致表单标签文字在悬停时变色 */
/* .form-group:hover label {
color: var(--primary-color, #ba003f);
} */
/* 添加修改后的规则 */
.form-group
:hover
label
.form-control-label
{
color
:
var
(
--primary-color
,
#ba003f
);
}
.form-group
label
{
display
:
block
;
margin-bottom
:
8px
;
font-weight
:
500
;
color
:
#444
;
font-size
:
14px
;
transition
:
color
0.3s
ease
;
}
.required
:after
{
content
:
' *'
;
color
:
var
(
--primary-color
,
#ba003f
);
}
.form-control
{
width
:
100%
;
padding
:
10px
12px
;
border
:
1px
solid
#ddd
;
border-radius
:
6px
;
font-size
:
14px
;
transition
:
border-color
0.3s
;
}
.form-control
:focus
{
border-color
:
var
(
--primary-color
,
#ba003f
);
outline
:
none
;
box-shadow
:
0
0
0
3px
rgba
(
186
,
0
,
63
,
0.1
);
}
textarea
.form-control
{
min-height
:
100px
;
resize
:
vertical
;
line-height
:
1.5
;
background-color
:
#fafafa
;
}
textarea
.form-control
:focus
{
background-color
:
#fff
;
}
/* ========== 下拉菜单样式 ========== */
select
.form-control
{
appearance
:
none
;
-webkit-appearance
:
none
;
-moz-appearance
:
none
;
background-image
:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23666' viewBox='0 0 16 16'><path d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/></svg>")
;
background-repeat
:
no-repeat
;
background-position
:
calc
(
100%
-
12px
)
center
;
background-size
:
12px
;
padding-right
:
32px
;
cursor
:
pointer
;
transition
:
all
0.3s
;
border-radius
:
6px
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.05
);
}
select
.form-control
:hover
{
border-color
:
#bbb
;
background-color
:
#f9f9f9
;
transform
:
translateY
(
-1px
);
box-shadow
:
0
2px
5px
rgba
(
0
,
0
,
0
,
0.08
);
}
select
.form-control
:focus
{
border-color
:
var
(
--primary-color
,
#ba003f
);
box-shadow
:
0
0
0
3px
rgba
(
186
,
0
,
63
,
0.1
);
background-image
:
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23ba003f' viewBox='0 0 16 16'><path d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/></svg>")
;
}
select
.form-control
option
{
font-weight
:
normal
;
background-color
:
white
;
color
:
#333
;
padding
:
8px
;
}
/* ========== 复选框样式 ========== */
.checkbox-group
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
12px
;
margin-top
:
10px
;
}
.checkbox-item
{
position
:
relative
;
background
:
#fff
;
border-radius
:
30px
;
border
:
1px
solid
#eee
;
transition
:
all
0.2s
ease
;
overflow
:
hidden
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.05
);
display
:
flex
;
align-items
:
center
;
}
.checkbox-item
:hover
{
border-color
:
#ddd
;
transform
:
translateY
(
-2px
);
box-shadow
:
0
2px
5px
rgba
(
0
,
0
,
0
,
0.1
);
}
.checkbox-active
{
background-color
:
var
(
--primary-color
,
#ba003f
);
border-color
:
var
(
--primary-color
,
#ba003f
);
}
.checkbox-active
.checkbox-label
{
color
:
#fff
;
}
.checkbox-label
{
display
:
block
;
padding
:
4px
18px
;
margin
:
0
;
font-weight
:
normal
;
cursor
:
pointer
;
color
:
#444
;
font-size
:
14px
;
transition
:
color
0.2s
;
text-align
:
center
;
line-height
:
1.5
;
}
/* 移除这两条规则,以防止它们被激活或干扰其他规则 */
/* .checkbox-item:hover .checkbox-label {
color: #444;
} */
/* .checkbox-active:hover .checkbox-label {
color: #fff;
} */
/* 添加明确的规则来阻止任何颜色变化 */
.checkbox-item
:hover:not
(
.checkbox-active
)
.checkbox-label
{
color
:
#444
!important
;
/* 使用!important确保优先级 */
}
.checkbox-active
:hover
.checkbox-label
{
color
:
#ffffff
!important
;
/* 使用!important确保优先级 */
}
.checkbox-item
input
[
type
=
'checkbox'
]
{
position
:
absolute
;
opacity
:
0
;
cursor
:
pointer
;
height
:
0
;
width
:
0
;
}
/* ========== 单选框样式 ========== */
.radio-group
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
10px
;
margin-top
:
8px
;
}
.radio-item
{
position
:
relative
;
background-color
:
#fcf2f5
;
border-radius
:
20px
;
border
:
1px
solid
#f9e0e7
;
transition
:
all
0.3s
ease
;
overflow
:
hidden
;
display
:
flex
;
align-items
:
center
;
cursor
:
pointer
;
box-shadow
:
0
1px
3px
rgba
(
186
,
0
,
63
,
0.05
);
}
.radio-item
:hover
{
background-color
:
#f9e0e7
;
transform
:
translateY
(
-2px
);
box-shadow
:
0
3px
6px
rgba
(
186
,
0
,
63
,
0.1
);
}
.radio-active
{
background-color
:
var
(
--primary-color
,
#ba003f
);
border-color
:
var
(
--primary-color
,
#ba003f
);
}
.radio-active
.radio-label
{
color
:
#fff
;
}
.radio-label
{
display
:
block
;
padding
:
8px
14px
;
margin
:
0
;
font-weight
:
500
;
cursor
:
pointer
;
color
:
#555
;
font-size
:
14px
;
transition
:
color
0.3s
;
position
:
relative
;
line-height
:
1.4
;
text-align
:
center
;
}
.radio-item
input
[
type
=
'radio'
]
{
position
:
absolute
;
opacity
:
0
;
cursor
:
pointer
;
height
:
0
;
width
:
0
;
}
/* ========== 按钮样式 ========== */
.action-buttons
{
display
:
flex
;
gap
:
8px
;
margin-top
:
15px
;
}
.btn
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
height
:
54px
;
padding
:
0
16px
;
font-size
:
14px
;
font-weight
:
500
;
border-radius
:
6px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
border
:
none
;
}
.btn
i
{
font-size
:
16px
;
}
/* 紧凑型按钮样式 */
.btn-compact
{
height
:
27px
;
padding
:
0
24px
;
font-size
:
12px
;
}
.btn-compact
i
{
font-size
:
14px
;
}
/* 紧凑型小按钮样式 */
.btn-compact-sm
{
height
:
24px
;
padding
:
0
12px
;
font-size
:
12px
;
min-width
:
60px
;
}
.btn-compact-sm
i
{
font-size
:
13px
;
}
.btn-primary
{
background-color
:
var
(
--primary-color
,
#ba003f
);
color
:
white
;
flex
:
1
;
}
.btn-primary
:hover
{
background-color
:
#980034
;
box-shadow
:
0
4px
12px
rgba
(
186
,
0
,
63
,
0.2
);
transform
:
translateY
(
-2px
);
}
.btn-primary
:active
{
transform
:
scale
(
0.96
);
}
.btn-primary
:disabled
{
background-color
:
#ddd
;
cursor
:
not-allowed
;
}
.btn-secondary
{
background-color
:
#f5f5f5
;
color
:
#333
;
border
:
1px
solid
#eee
;
}
.btn-secondary
:hover
{
background-color
:
#e5e5e5
;
box-shadow
:
0
4px
12px
rgba
(
108
,
117
,
125
,
0.15
);
transform
:
translateY
(
-2px
);
}
.btn-secondary
:active
{
transform
:
scale
(
0.96
);
}
/* ========== 功能按钮样式 ========== */
.learn-button
{
background-color
:
var
(
--primary-color
,
#ba003f
);
color
:
white
;
border
:
none
;
padding
:
6px
14px
;
border-radius
:
4px
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
font-size
:
14px
;
height
:
auto
;
}
.learn-button
:hover
{
background-color
:
#980034
;
}
.learn-button
i
{
font-size
:
16px
;
margin-right
:
0
;
}
.action-button
{
background
:
none
;
border
:
1px
solid
#eee
;
color
:
#666
;
padding
:
6px
10px
;
border-radius
:
4px
;
cursor
:
pointer
;
font-size
:
14px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
transition
:
all
0.2s
ease
;
}
.action-button
:hover
{
color
:
var
(
--primary-color
,
#ba003f
);
background-color
:
#f8f8f8
;
border-color
:
#ddd
;
transform
:
translateY
(
-1px
);
}
.action-button
i
{
font-size
:
16px
;
}
.primary-button
{
background-color
:
var
(
--primary-color
,
#ba003f
);
color
:
white
;
border
:
none
;
padding
:
6px
14px
;
border-radius
:
4px
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
font-size
:
14px
;
}
.primary-button
:hover
{
background-color
:
#980034
;
}
.secondary-button
{
background-color
:
#f5f5f5
;
color
:
#333
;
border
:
1px
solid
#eee
;
padding
:
6px
14px
;
border-radius
:
4px
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
gap
:
4px
;
font-size
:
14px
;
}
.secondary-button
:hover
{
background-color
:
#e5e5e5
;
}
/* 禁用状态 */
.primary-button
:disabled
,
.secondary-button
:disabled
{
opacity
:
0.6
;
cursor
:
not-allowed
;
background-color
:
#ccc
;
color
:
#666
;
border-color
:
#ccc
;
}
.primary-button
:disabled:hover
{
background-color
:
#ccc
;
transform
:
none
;
box-shadow
:
none
;
}
.secondary-button
:disabled:hover
{
background-color
:
#f5f5f5
;
transform
:
none
;
}
/* ========== 内容展示区域 ========== */
.result-section
{
flex
:
1
;
background-color
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
10px
rgba
(
0
,
0
,
0
,
0.05
);
overflow-y
:
auto
;
display
:
flex
;
flex-direction
:
column
;
padding
:
0
;
position
:
relative
;
overflow
:
hidden
;
}
.result-content-wrapper
{
position
:
relative
;
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
min-height
:
300px
;
}
/* ========== 数据表格样式 ========== */
.data-table
{
width
:
100%
;
border-collapse
:
collapse
;
margin-bottom
:
20px
;
background
:
#fff
;
border-radius
:
8px
;
overflow
:
hidden
;
box-shadow
:
0
2px
12px
rgba
(
0
,
0
,
0
,
0.1
);
}
.data-table
th
,
.data-table
td
{
padding
:
12px
16px
;
text-align
:
center
;
border-bottom
:
1px
solid
#f0f0f0
;
}
.data-table
th
{
background-color
:
#f9f9f9
;
color
:
#333
;
font-weight
:
500
;
white-space
:
nowrap
;
}
.data-table
tr
:hover
{
background-color
:
#f9f9f9
;
}
.data-table
td
.tag
{
text-align
:
center
;
}
.operations
{
white-space
:
nowrap
;
min-width
:
200px
;
}
.operation-buttons
{
display
:
flex
;
justify-content
:
flex-start
;
gap
:
8px
;
}
.table-button
{
margin-right
:
8px
;
padding
:
4px
8px
;
border
:
none
;
border-radius
:
4px
;
cursor
:
pointer
;
font-size
:
13px
;
color
:
#fff
;
background-color
:
#ba003f
;
}
.table-button.edit
{
background-color
:
#409eff
;
}
.table-button.view
{
background-color
:
#67c23a
;
}
.table-button.delete
{
background-color
:
#f56c6c
;
}
.tag
{
display
:
inline-block
;
padding
:
2px
6px
;
margin-right
:
6px
;
background-color
:
#f0f2f5
;
color
:
#606266
;
font-size
:
12px
;
border-radius
:
4px
;
}
.empty-data
{
text-align
:
center
;
padding
:
30px
;
color
:
#909399
;
}
.section-content
{
margin-bottom
:
20px
;
}
/* ========== 分页样式 ========== */
.pagination-container
{
display
:
flex
;
justify-content
:
flex-end
;
margin-top
:
20px
;
}
.pagination
{
display
:
flex
;
align-items
:
center
;
}
.pagination-button
{
width
:
32px
;
height
:
32px
;
border
:
1px
solid
#dcdfe6
;
background
:
#fff
;
color
:
#606266
;
border-radius
:
4px
;
cursor
:
pointer
;
margin
:
0
5px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
}
.pagination-button
:disabled
{
cursor
:
not-allowed
;
color
:
#c0c4cc
;
}
.page-info
{
margin
:
0
10px
;
font-size
:
14px
;
color
:
#606266
;
}
.page-size-selector
{
margin-left
:
15px
;
display
:
flex
;
align-items
:
center
;
font-size
:
14px
;
color
:
#606266
;
}
.page-size-selector
select
{
margin
:
0
5px
;
padding
:
2px
8px
;
border
:
1px
solid
#dcdfe6
;
border-radius
:
4px
;
}
/* ========== 表单宽度变体 ========== */
.form-group-wide
{
flex
:
2
;
}
.form-group-medium
{
flex
:
1.5
;
}
/* ========== 按钮类型变体 ========== */
.btn-danger
{
background-color
:
#f56c6c
;
color
:
white
;
border
:
none
;
}
.btn-danger
:hover
{
background-color
:
#f78989
;
transform
:
translateY
(
-1px
);
}
/* ========== 空状态样式 ========== */
.empty-result
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
height
:
100%
;
min-height
:
350px
;
background-color
:
#fff
;
border-radius
:
8px
;
color
:
#666
;
text-align
:
center
;
}
.empty-content
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
padding
:
30px
;
}
.empty-image
{
width
:
120px
;
height
:
120px
;
margin-bottom
:
20px
;
}
.empty-message
{
margin
:
0
0
20px
;
font-size
:
16px
;
color
:
#666
;
}
/* ========== 加载状态样式 ========== */
.loading-overlay
{
position
:
absolute
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
rgba
(
255
,
255
,
255
,
0.8
);
z-index
:
5
;
border-radius
:
0
0
8px
8px
;
}
.loading-spinner
{
width
:
50px
;
height
:
50px
;
border
:
5px
solid
rgba
(
186
,
0
,
63
,
0.1
);
border-radius
:
50%
;
border-top-color
:
var
(
--primary-color
,
#ba003f
);
animation
:
spin
1s
ease-in-out
infinite
;
margin
:
0
auto
20px
;
}
.loading-text
{
font-size
:
16px
;
color
:
var
(
--primary-color
,
#ba003f
);
font-weight
:
500
;
}
@keyframes
spin
{
0
%
{
transform
:
rotate
(
0deg
);
}
100
%
{
transform
:
rotate
(
360deg
);
}
}
.spinning
{
animation
:
spin
1.5s
linear
infinite
;
display
:
inline-block
;
}
.blur-content
{
filter
:
blur
(
1px
);
opacity
:
0.6
;
pointer-events
:
none
;
}
/* ========== 模态框样式 ========== */
.modal
{
position
:
fixed
;
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
background-color
:
rgba
(
0
,
0
,
0
,
0.5
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
z-index
:
1000
;
}
.modal-content
{
background-color
:
#fff
;
border-radius
:
8px
;
width
:
80%
;
max-width
:
800px
;
max-height
:
80vh
;
display
:
flex
;
flex-direction
:
column
;
box-shadow
:
0
8px
30px
rgba
(
0
,
0
,
0
,
0.25
);
animation
:
modal-pop
0.3s
ease-out
;
}
@keyframes
modal-pop
{
0
%
{
transform
:
scale
(
0.9
);
opacity
:
0
;
}
100
%
{
transform
:
scale
(
1
);
opacity
:
1
;
}
}
.modal-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
15px
20px
;
border-bottom
:
1px
solid
#eee
;
}
.modal-header
h3
{
margin
:
0
;
font-size
:
18px
;
font-weight
:
600
;
color
:
#333
;
display
:
flex
;
align-items
:
center
;
gap
:
8px
;
}
.modal-header
h3
i
{
color
:
var
(
--primary-color
,
#ba003f
);
}
.close-btn
{
background
:
none
;
border
:
none
;
color
:
#666
;
font-size
:
20px
;
cursor
:
pointer
;
}
.modal-body
{
padding
:
20px
;
overflow-y
:
auto
;
max-height
:
calc
(
80vh
-
60px
);
}
/* ========== 知识学习侧边栏 ========== */
.knowledge-drawer
:deep
(
.el-drawer__header
)
{
margin-bottom
:
0
;
padding
:
15px
20px
;
border-bottom
:
1px
solid
#eaeaea
;
color
:
#ba003f
;
font-weight
:
bold
;
}
.knowledge-drawer
:deep
(
.el-drawer__title
)
{
font-size
:
18px
;
color
:
#ba003f
;
}
.knowledge-content
{
padding
:
20px
;
height
:
100%
;
overflow-y
:
auto
;
}
.knowledge-section
{
margin-bottom
:
25px
;
}
.knowledge-subtitle
{
color
:
#ba003f
;
margin-top
:
0
;
margin-bottom
:
12px
;
font-size
:
18px
;
font-weight
:
600
;
border-bottom
:
1px
solid
rgba
(
186
,
0
,
63
,
0.2
);
padding-bottom
:
8px
;
display
:
flex
;
align-items
:
center
;
}
.knowledge-text
{
font-size
:
15px
;
line-height
:
1.6
;
color
:
#333
;
}
.knowledge-text
strong
{
color
:
#ba003f
;
font-weight
:
600
;
}
.knowledge-text
ul
{
padding-left
:
20px
;
margin
:
10px
0
;
}
.knowledge-text
li
{
margin-bottom
:
8px
;
line-height
:
1.5
;
}
.knowledge-icon
{
display
:
inline-block
;
margin-right
:
8px
;
width
:
28px
;
height
:
28px
;
line-height
:
28px
;
text-align
:
center
;
border-radius
:
50%
;
background-color
:
rgba
(
186
,
0
,
63
,
0.1
);
color
:
#ba003f
;
font-size
:
16px
;
vertical-align
:
middle
;
}
/* ========== 响应式布局 ========== */
@media
(
max-width
:
992px
)
{
.main-container
{
flex-direction
:
column
;
}
.input-section
,
.right-column
{
width
:
100%
;
}
}
@media
(
max-width
:
768px
)
{
.knowledge-drawer
:deep
(
.el-drawer
)
{
width
:
90%
!important
;
}
.form-row
{
flex-direction
:
column
;
}
}
/* ========== 参考案例样式 ========== */
.examples-section
{
background-color
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
10px
rgba
(
0
,
0
,
0
,
0.05
);
padding
:
15px
;
margin-bottom
:
0
;
/* 移除底部边距 */
}
.examples-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
10px
;
}
.example-carousel
{
position
:
relative
;
overflow
:
hidden
;
width
:
100%
;
padding-bottom
:
5px
;
/* 添加底部间距避免阴影被裁剪 */
}
.example-cards
{
display
:
flex
;
gap
:
20px
;
/* 增加卡片间距 */
transition
:
transform
0.3s
ease
;
will-change
:
transform
;
padding
:
5px
0
;
width
:
max-content
;
/* 确保足够宽以容纳所有内容 */
}
.example-card
{
flex
:
0
0
220px
;
/* 增加卡片宽度 */
height
:
auto
;
/* 改为自适应高度 */
min-height
:
120px
;
/* 设置最小高度 */
margin-right
:
10px
;
cursor
:
pointer
;
position
:
relative
;
border-radius
:
12px
;
background
:
white
;
transition
:
all
0.3s
;
box-shadow
:
0
3px
10px
rgba
(
0
,
0
,
0
,
0.08
);
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
;
border
:
1px
solid
#eee
;
}
.example-card
:hover
{
transform
:
translateY
(
-5px
);
box-shadow
:
0
10px
20px
rgba
(
0
,
0
,
0
,
0.12
);
border-color
:
var
(
--primary-color
,
#ba003f
);
}
/* 卡片头部区域 - 顶部固定部分 */
.example-card-header
{
position
:
relative
;
background
:
linear-gradient
(
to
right
,
rgba
(
186
,
0
,
63
,
0.02
),
rgba
(
186
,
0
,
63
,
0.08
));
padding
:
16px
;
border-bottom
:
1px
solid
#f0f0f0
;
display
:
flex
;
align-items
:
flex-start
;
/* 改为顶部对齐,适合多行标题 */
gap
:
15px
;
min-height
:
85px
;
}
/* 图标样式 */
.example-icon
{
width
:
55px
;
height
:
55px
;
min-width
:
55px
;
background-color
:
rgba
(
186
,
0
,
63
,
0.1
);
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
12px
;
box-shadow
:
0
3px
6px
rgba
(
186
,
0
,
63
,
0.1
);
}
.example-icon
i
,
.example-icon
svg
{
font-size
:
26px
;
color
:
var
(
--primary-color
,
#ba003f
);
display
:
inline-block
;
/* 确保图标可见 */
line-height
:
1
;
/* 修正行高,防止图标被裁剪 */
}
/* 标题区域 */
.example-info
{
display
:
flex
;
flex-direction
:
column
;
overflow
:
hidden
;
}
.example-title
{
font-weight
:
600
;
font-size
:
16px
;
color
:
#222
;
line-height
:
1.3
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
display
:
-webkit-box
;
-webkit-line-clamp
:
2
;
-webkit-box-orient
:
vertical
;
transition
:
all
0.3s
;
flex
:
1
;
min-height
:
42px
;
/* 确保有足够的高度容纳两行文字 */
}
.example-desc
{
font-size
:
14px
;
color
:
#666
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
display
:
-webkit-box
;
-webkit-line-clamp
:
3
;
-webkit-box-orient
:
vertical
;
white-space
:
normal
;
line-height
:
1.5
;
}
/* 卡片内容区域 - 底部描述部分,无内容时隐藏 */
.example-content
{
padding
:
16px
;
display
:
flex
;
flex-direction
:
column
;
background-color
:
white
;
flex
:
1
;
min-height
:
60px
;
transition
:
all
0.3s
ease
;
}
.example-content
:empty
{
display
:
none
;
}
.example-detail
{
font-size
:
14px
;
line-height
:
1.6
;
color
:
#444
;
display
:
-webkit-box
;
-webkit-line-clamp
:
3
;
/* 默认显示3行 */
-webkit-box-orient
:
vertical
;
overflow
:
hidden
;
position
:
relative
;
margin-bottom
:
5px
;
/* 添加底部边距 */
transition
:
all
0.3s
ease
;
}
/* 移除渐变遮罩,在文本末尾添加省略号 */
.example-detail
::after
{
display
:
none
;
/* 移除渐变遮罩 */
}
/* 卡片悬停效果优化 */
.example-card
:hover
.example-detail
{
-webkit-line-clamp
:
8
;
/* 悬停时显示更多行 */
color
:
#333
;
/* 文字颜色加深 */
}
/* 添加更好的内容展开交互 */
.example-card
:hover
.example-content
{
background-color
:
#fafafa
;
/* 轻微背景色变化 */
}
/* 当描述为空时隐藏卡片内容部分 */
.example-detail
:empty
{
display
:
none
;
}
/* 标签区域 */
.example-tags
{
padding
:
0
16px
16px
;
display
:
flex
;
gap
:
6px
;
flex-wrap
:
wrap
;
}
.example-tag
{
padding
:
4px
10px
;
background-color
:
rgba
(
186
,
0
,
63
,
0.08
);
color
:
var
(
--primary-color
,
#ba003f
);
border-radius
:
20px
;
font-size
:
12px
;
white-space
:
nowrap
;
}
/* 标签为空时隐藏 */
.example-tags
:empty
{
display
:
none
;
padding
:
0
;
}
/* 鼠标悬停时的动画效果 */
.example-card
:hover
.example-title
{
color
:
var
(
--primary-color
,
#ba003f
);
}
.example-card
:hover
.example-detail
::after
{
opacity
:
0.5
;
}
/* 轮播控制样式 */
.example-carousel
{
position
:
relative
;
overflow
:
hidden
;
width
:
100%
;
padding-bottom
:
15px
;
/* 增加底部边距以适应更大的卡片阴影 */
}
.example-cards
{
display
:
flex
;
gap
:
25px
;
/* 增加间距 */
transition
:
transform
0.4s
ease
;
will-change
:
transform
;
padding
:
10px
0
;
width
:
max-content
;
}
.carousel-controls
{
display
:
flex
;
gap
:
8px
;
}
.carousel-control
{
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
background-color
:
white
;
border
:
1px
solid
#eee
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
transition
:
all
0.3s
;
}
.carousel-control
i
{
font-size
:
20px
;
color
:
#666
;
display
:
inline-block
;
/* 确保图标可见 */
line-height
:
1
;
/* 修正行高 */
}
.carousel-control
:hover
{
background-color
:
rgba
(
186
,
0
,
63
,
0.05
);
border-color
:
var
(
--primary-color
,
#ba003f
);
}
.carousel-control
:hover
i
{
color
:
var
(
--primary-color
,
#ba003f
);
}
.carousel-control.disabled
{
opacity
:
0.5
;
cursor
:
not-allowed
;
}
/* 大尺寸示例卡片 */
.example-card-large
{
display
:
flex
;
flex-direction
:
column
;
height
:
400px
;
/* 比标准卡片高度高一倍 */
min-width
:
300px
;
/* 更宽的最小宽度 */
max-width
:
400px
;
background-color
:
white
;
border-radius
:
8px
;
border
:
1px
solid
#eaeaea
;
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.05
);
transition
:
all
0.3s
;
overflow
:
hidden
;
cursor
:
pointer
;
}
.example-card-large
:hover
{
border-color
:
var
(
--primary-color
,
#ba003f
);
box-shadow
:
0
4px
12px
rgba
(
186
,
0
,
63
,
0.15
);
height
:
auto
;
/* 悬停时可以自动调整高度 */
}
.example-card-large
.example-icon
{
width
:
60px
;
height
:
60px
;
border-radius
:
30px
;
background-color
:
rgba
(
186
,
0
,
63
,
0.1
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
15px
;
}
.example-card-large
.example-icon
i
,
.example-card-large
.example-icon
svg
{
font-size
:
32px
;
}
.example-card-large
.example-title
{
font-size
:
18px
;
margin-bottom
:
10px
;
-webkit-line-clamp
:
2
;
display
:
-webkit-box
;
-webkit-box-orient
:
vertical
;
white-space
:
normal
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
.example-card-large
.example-desc
{
font-size
:
15px
;
white-space
:
normal
;
-webkit-line-clamp
:
2
;
display
:
-webkit-box
;
-webkit-box-orient
:
vertical
;
}
.example-card-large
.example-content
{
padding
:
20px
;
max-height
:
250px
;
/* 增加内容区域的高度 */
}
.example-card-large
.example-detail
{
-webkit-line-clamp
:
8
;
/* 显示更多行 */
font-size
:
15px
;
}
.example-card-large
.example-tags
{
padding
:
0
20px
20px
;
}
.example-card-large
.example-tag
{
padding
:
5px
12px
;
font-size
:
13px
;
}
/* 大尺寸卡片的轮播样式调整 */
.example-cards-large
{
display
:
flex
;
gap
:
30px
;
transition
:
transform
0.4s
ease
;
will-change
:
transform
;
padding
:
15px
0
;
width
:
max-content
;
}
/* ========== AI应用案例编辑特有样式 ========== */
/* 标签预览区域 */
.tags-preview
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
8px
;
margin-top
:
10px
;
margin-bottom
:
20px
;
}
/* 编辑表单容器 */
.edit-form-container
{
background-color
:
#fff
;
border-radius
:
8px
;
padding
:
20px
;
box-shadow
:
0
2px
12px
rgba
(
0
,
0
,
0
,
0.1
);
margin-bottom
:
20px
;
}
/* 错误信息提示 */
.error-message
{
color
:
#f56c6c
;
font-size
:
12px
;
margin-top
:
5px
;
}
/* 按钮与输入框的容器样式 */
.form-group
>
div
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
}
/* 案例封面上传样式 */
.cover-upload-container
{
margin-top
:
10px
;
width
:
100%
;
}
.cover-preview
{
position
:
relative
;
width
:
320px
;
height
:
180px
;
border-radius
:
8px
;
overflow
:
hidden
;
box-shadow
:
0
2px
12px
rgba
(
0
,
0
,
0
,
0.1
);
}
.cover-preview
img
{
width
:
100%
;
height
:
100%
;
object-fit
:
cover
;
display
:
block
;
}
.cover-remove-btn
{
position
:
absolute
;
top
:
10px
;
right
:
10px
;
width
:
32px
;
height
:
32px
;
border-radius
:
50%
;
background-color
:
rgba
(
0
,
0
,
0
,
0.6
);
border
:
none
;
color
:
white
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
font-size
:
16px
;
transition
:
all
0.2s
;
}
.cover-remove-btn
:hover
{
background-color
:
rgba
(
0
,
0
,
0
,
0.8
);
}
.cover-upload
{
width
:
320px
;
height
:
180px
;
border
:
2px
dashed
#dcdfe6
;
border-radius
:
8px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
}
.cover-upload-button
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
gap
:
10px
;
color
:
#909399
;
cursor
:
pointer
;
}
.cover-upload-button
i
{
font-size
:
32px
;
}
/* 文件上传样式 */
.file-upload-container
{
display
:
flex
;
align-items
:
center
;
margin-bottom
:
8px
;
}
.file-input
{
display
:
none
;
}
.file-upload-button
{
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
5px
;
padding
:
8px
16px
;
background-color
:
#f5f5f5
;
color
:
#333
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
cursor
:
pointer
;
font-size
:
14px
;
transition
:
all
0.2s
;
}
.file-upload-button
:hover
{
background-color
:
#e8e8e8
;
border-color
:
#ccc
;
}
.file-name
{
margin-left
:
12px
;
color
:
#606266
;
font-size
:
14px
;
flex
:
1
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.upload-tip
{
font-size
:
12px
;
color
:
#909399
;
margin-top
:
5px
;
}
.uploaded-files
{
margin-top
:
15px
;
}
.uploaded-file
{
display
:
flex
;
align-items
:
center
;
background-color
:
#f9f9f9
;
padding
:
8px
12px
;
border-radius
:
4px
;
margin-bottom
:
8px
;
}
.file-icon
{
margin-right
:
10px
;
color
:
#409eff
;
font-size
:
18px
;
}
.file-info
{
flex
:
1
;
font-size
:
14px
;
color
:
#606266
;
}
/* 红色危险按钮样式 */
.btn-danger
{
background-color
:
#f56c6c
;
color
:
white
;
border
:
none
;
}
.btn-danger
:hover
{
background-color
:
#f78989
;
}
/* 绿色成功按钮样式 */
.btn-success
{
background-color
:
#67c23a
;
color
:
white
;
border
:
none
;
}
.btn-success
:hover
{
background-color
:
#73d13d
;
}
/* AI设计案例提示词工程样式 */
.prompt-section
{
margin-bottom
:
20px
;
}
.prompt-header
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin-bottom
:
10px
;
}
.radio-group
{
display
:
flex
;
gap
:
10px
;
}
.radio-label
{
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
}
/* 紫荆红色单选框样式 */
.radio-label
input
[
type
=
'radio'
]
{
accent-color
:
#c10055
;
width
:
16px
;
height
:
16px
;
}
.radio-label
input
[
type
=
'radio'
]
:checked
+
span
{
color
:
#c10055
;
font-weight
:
500
;
}
.prompt-content
{
margin-bottom
:
10px
;
}
.prompt-content.file-content
{
padding
:
10px
;
background-color
:
#f9f9f9
;
border-radius
:
4px
;
}
.file-placeholder
{
color
:
#909399
;
font-size
:
12px
;
}
.prompt-actions
{
margin-top
:
10px
;
text-align
:
center
;
}
.generated-prompt
{
margin-top
:
20px
;
padding
:
20px
;
background-color
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
12px
rgba
(
0
,
0
,
0
,
0.1
);
}
.prompt-controls
{
display
:
flex
;
gap
:
10px
;
}
.prompt-editor
{
margin-top
:
10px
;
}
.prompt-editor
textarea
{
height
:
auto
!important
;
min-height
:
300px
;
}
/* 一键创建案例按钮样式 */
.create-case-action
{
margin-top
:
20px
;
text-align
:
center
;
}
.create-case-action
.btn-lg
{
padding
:
12px
30px
;
font-size
:
16px
;
transition
:
all
0.3s
ease
;
}
.create-case-action
.btn-lg
:hover
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
4px
12px
rgba
(
0
,
0
,
0
,
0.1
);
}
.create-case-action
.btn-lg
:active
{
transform
:
translateY
(
0
);
}
/* 案例创建进度样式 */
.case-creation-progress
{
margin-top
:
20px
;
padding
:
20px
;
background-color
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
12px
rgba
(
0
,
0
,
0
,
0.1
);
}
.progress-header
{
display
:
flex
;
align-items
:
center
;
margin-bottom
:
20px
;
}
.progress-header
h4
{
margin-right
:
15px
;
min-width
:
120px
;
}
.progress-bar-container
{
flex
:
1
;
height
:
10px
;
background-color
:
#f0f0f0
;
border-radius
:
5px
;
overflow
:
hidden
;
margin-right
:
10px
;
}
.progress-bar
{
height
:
100%
;
background-color
:
#c10055
;
/* 紫荆红色进度条 */
transition
:
width
0.3s
ease
;
}
.progress-percentage
{
font-size
:
14px
;
font-weight
:
500
;
color
:
#c10055
;
min-width
:
45px
;
text-align
:
right
;
}
.creation-steps
{
margin-top
:
20px
;
margin-bottom
:
20px
;
border-left
:
2px
solid
#f0f0f0
;
padding-left
:
20px
;
}
.creation-step
{
position
:
relative
;
padding
:
15px
0
;
transition
:
all
0.3s
ease
;
}
.creation-step
:not
(
:last-child
)
{
border-bottom
:
1px
dashed
#f0f0f0
;
}
.step-indicator
{
position
:
absolute
;
left
:
-31px
;
top
:
15px
;
width
:
20px
;
height
:
20px
;
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
#fff
;
border
:
2px
solid
#f0f0f0
;
transition
:
all
0.3s
ease
;
}
.step-indicator
i
{
font-size
:
12px
;
}
.step-current
.step-indicator
{
border-color
:
#c10055
;
background-color
:
#fff
;
}
.step-current
.step-indicator
i
.fa-spinner
{
color
:
#c10055
;
animation
:
rotate
1s
infinite
linear
;
}
.step-completed
.step-indicator
{
border-color
:
#c10055
;
background-color
:
#c10055
;
}
.step-completed
.step-indicator
i
{
color
:
#fff
;
}
.step-content
{
padding-left
:
10px
;
}
.step-name
{
font-size
:
16px
;
font-weight
:
500
;
margin-bottom
:
5px
;
color
:
#303133
;
transition
:
all
0.3s
ease
;
}
.step-current
.step-name
{
color
:
#c10055
;
font-weight
:
600
;
}
.step-description
{
font-size
:
14px
;
color
:
#606266
;
margin-bottom
:
10px
;
}
.creation-complete-actions
{
margin-top
:
20px
;
text-align
:
center
;
padding-top
:
20px
;
border-top
:
1px
solid
#f0f0f0
;
}
.creation-complete-actions
.action-btn
{
margin
:
0
10px
;
padding
:
12px
30px
;
font-size
:
16px
;
transition
:
all
0.3s
ease
;
min-width
:
180px
;
display
:
inline-flex
;
align-items
:
center
;
justify-content
:
center
;
}
.creation-complete-actions
.action-btn
:hover
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
4px
12px
rgba
(
0
,
0
,
0
,
0.1
);
}
.creation-complete-actions
.action-btn
:active
{
transform
:
translateY
(
0
);
}
.creation-complete-actions
.action-btn
i
{
margin-right
:
8px
;
font-size
:
16px
;
}
/* 图标样式补充 */
.icon-left
{
margin-right
:
5px
;
}
.btn
.icon-left
{
margin-right
:
8px
;
}
/* 旋转动画 */
@keyframes
rotate
{
from
{
transform
:
rotate
(
0deg
);
}
to
{
transform
:
rotate
(
360deg
);
}
}
/* 确保优化按钮高度匹配输入框 */
.form-group
button
.btn
{
height
:
40px
!important
;
box-sizing
:
border-box
!important
;
line-height
:
38px
!important
;
padding
:
0
15px
!important
;
vertical-align
:
middle
!important
;
font-size
:
14px
!important
;
border-radius
:
4px
!important
;
}
/* 统一表单控件高度 */
.form-control
{
height
:
40px
!important
;
box-sizing
:
border-box
!important
;
}
/* 宽按钮样式 */
.wide-btn
{
width
:
300px
!important
;
/* 宽度约为普通按钮的两倍 */
margin
:
0
auto
!important
;
display
:
block
!important
;
/* 确保按钮是块级元素 */
font-size
:
16px
!important
;
padding
:
10px
30px
!important
;
}
.wide-btn
:hover
{
transform
:
translateY
(
-2px
)
!important
;
box-shadow
:
0
4px
12px
rgba
(
193
,
0
,
85
,
0.2
)
!important
;
}
/* 生成提示词按钮容器样式 */
.prompt-actions
{
margin-top
:
20px
!important
;
text-align
:
center
!important
;
display
:
flex
!important
;
justify-content
:
center
!important
;
}
/* 一键创建案例按钮样式 */
.create-case-action
{
margin-top
:
20px
!important
;
text-align
:
center
!important
;
display
:
flex
!important
;
justify-content
:
center
!important
;
}
src/modules/material/digital-human/index.ts
0 → 100644
浏览文件 @
8ce62951
import
type
{
RouteRecordRaw
}
from
'vue-router'
import
Layout
from
'@/components/layout/Index.vue'
const
routes
:
RouteRecordRaw
[]
=
[
{
path
:
'/material/digital-human'
,
component
:
Layout
,
children
:
[{
path
:
''
,
component
:
()
=>
import
(
'./views/BaiduDigitalHuman.vue'
)
}],
},
]
export
{
routes
}
src/modules/material/digital-human/views/BaiduDigitalHuman.vue
0 → 100644
浏览文件 @
8ce62951
<
template
>
<AppCard>
<div
class=
"text-creation-page"
>
<!-- 分步骤视频合成流程 -->
<div
class=
"main-container"
>
<!-- 左侧步骤条 -->
<div
class=
"steps-container slim-steps"
style=
"margin-left: 10px"
>
<!-- 步骤指示器 -->
<div
class=
"creation-steps"
>
<!-- 步骤1 -->
<div
class=
"creation-step"
:class=
"
{ 'step-completed': currentStep > 1, 'step-current': currentStep === 1 }">
<div
class=
"step-indicator"
>
<i
v-if=
"currentStep > 1"
class=
"ri-check-line"
></i>
<i
v-else-if=
"currentStep === 1"
class=
"ri-edit-line"
></i>
<span
v-else
>
1
</span>
</div>
<div
class=
"step-content"
>
<div
class=
"step-name"
>
输入文本
</div>
<div
class=
"step-description"
>
添加内容
</div>
</div>
</div>
<!-- 步骤2 -->
<div
class=
"creation-step"
:class=
"
{ 'step-completed': currentStep > 2, 'step-current': currentStep === 2 }">
<div
class=
"step-indicator"
>
<i
v-if=
"currentStep > 2"
class=
"ri-check-line"
></i>
<i
v-else-if=
"currentStep === 2"
class=
"ri-user-voice-line"
></i>
<span
v-else
>
2
</span>
</div>
<div
class=
"step-content"
>
<div
class=
"step-name"
>
形象音色
</div>
<div
class=
"step-description"
>
选择配置
</div>
</div>
</div>
<!-- 步骤3 -->
<div
class=
"creation-step"
:class=
"
{ 'step-completed': currentStep > 3, 'step-current': currentStep === 3 }">
<div
class=
"step-indicator"
>
<i
v-if=
"currentStep > 3"
class=
"ri-check-line"
></i>
<i
v-else-if=
"currentStep === 3"
class=
"ri-settings-3-line"
></i>
<span
v-else
>
3
</span>
</div>
<div
class=
"step-content"
>
<div
class=
"step-name"
>
视频参数
</div>
<div
class=
"step-description"
>
调整设置
</div>
</div>
</div>
<!-- 步骤4 -->
<div
class=
"creation-step"
:class=
"
{ 'step-completed': currentStep > 4, 'step-current': currentStep === 4 }">
<div
class=
"step-indicator"
>
<i
v-if=
"currentStep > 4"
class=
"ri-check-line"
></i>
<i
v-else-if=
"currentStep === 4"
class=
"ri-upload-cloud-line"
></i>
<span
v-else
>
4
</span>
</div>
<div
class=
"step-content"
>
<div
class=
"step-name"
>
提交任务
</div>
<div
class=
"step-description"
>
确认信息
</div>
</div>
</div>
<!-- 步骤5 -->
<div
class=
"creation-step"
:class=
"
{ 'step-completed': currentStep > 5, 'step-current': currentStep === 5 }">
<div
class=
"step-indicator"
>
<i
v-if=
"currentStep > 5"
class=
"ri-check-line"
></i>
<i
v-else-if=
"currentStep === 5"
class=
"ri-video-line"
></i>
<span
v-else
>
5
</span>
</div>
<div
class=
"step-content"
>
<div
class=
"step-name"
>
查看结果
</div>
<div
class=
"step-description"
>
获取视频
</div>
</div>
</div>
</div>
</div>
<!-- 右侧内容区 -->
<div
class=
"step-content-container wide-content"
>
<!-- 步骤1:输入文本内容 -->
<div
v-if=
"currentStep === 1"
class=
"step-content-panel"
>
<div
class=
"section-title"
>
<i
class=
"ri-chat-1-line"
></i>
<span>
输入文本内容
</span>
</div>
<div
class=
"form-group"
>
<label
for=
"textContent"
class=
"form-control-label required"
>
文本内容
</label>
<div
class=
"text-input-wrapper"
>
<textarea
id=
"textContent"
v-model=
"formData.text"
class=
"form-control"
placeholder=
"请输入要数字人播报的文本内容,例如:欢迎使用紫荆AI图文创作工具,我可以为您生成各种风格的数字人视频。"
rows=
"10"
></textarea>
<div
class=
"input-actions"
>
<button
class=
"text-action-button"
@
click=
"useExampleText"
title=
"使用示例文本"
>
<i
class=
"ri-file-list-line"
></i>
使用示例文本
</button>
</div>
</div>
</div>
<div
class=
"action-buttons"
>
<button
class=
"primary-button"
@
click=
"nextStep"
:disabled=
"!formData.text"
>
<span>
下一步
</span>
<i
class=
"ri-arrow-right-line"
></i>
</button>
</div>
</div>
<!-- 步骤2:选择数字人和音色 -->
<div
v-if=
"currentStep === 2"
class=
"step-content-panel"
>
<div
class=
"section-title"
>
<i
class=
"ri-user-voice-line"
></i>
<span>
选择数字人和音色
</span>
</div>
<div
class=
"form-group"
>
<label
class=
"form-control-label required"
>
数字人选择
</label>
<div
class=
"digital-human-grid inline-grid"
>
<div
v-for=
"human in digitalHumans"
:key=
"human.id"
class=
"digital-human-card compact-card"
:class=
"
{ selected: formData.figureId === human.id }"
@click="selectDigitalHuman(human.id)">
<div
class=
"human-avatar"
>
<img
:src=
"human.avatar"
:alt=
"human.name"
/>
</div>
<div
class=
"human-name"
>
{{
human
.
name
}}
</div>
</div>
</div>
</div>
<div
class=
"form-group"
>
<label
class=
"form-control-label required"
>
音色选择
<span
class=
"voice-filter-info"
>
(
{{
getSelectedHumanGender
()
===
'女'
?
'女'
:
'男'
}}
性音色)
</span
></label
>
<div
class=
"voice-filter"
>
<input
type=
"text"
v-model=
"voiceSearchText"
placeholder=
"搜索音色名称或风格"
class=
"form-control voice-search-input"
/>
</div>
<div
class=
"voice-list multi-column-list"
>
<div
v-for=
"voice in filteredActiveVoices"
:key=
"voice.id"
class=
"voice-item compact-voice-item"
:class=
"
{ selected: formData.ttsParams.person === voice.id }"
@click="selectVoice(voice.id)">
<div
class=
"voice-icon"
>
<i
class=
"ri-voice-recognition-line"
></i>
</div>
<div
class=
"voice-info"
>
<div
class=
"voice-name"
>
{{
voice
.
name
}}
</div>
<div
class=
"voice-desc"
>
{{
voice
.
gender
}}
·
{{
voice
.
style
}}
</div>
</div>
<div
class=
"voice-preview"
>
<button
class=
"preview-button"
@
click
.
stop=
"previewVoice(voice.id)"
title=
"试听音色"
:disabled=
"!getVoicePreviewUrl(voice.id)"
>
<i
class=
"ri-play-circle-line"
></i>
</button>
</div>
</div>
<!-- 添加音频播放元素 -->
<audio
ref=
"audioPlayer"
v-if=
"previewAudio"
:src=
"previewAudio"
controls
style=
"display: none"
></audio>
</div>
<div
v-if=
"filteredActiveVoices.length === 0"
class=
"no-voices-found"
>
<div
class=
"empty-content"
>
<div
class=
"empty-image"
>
<i
class=
"ri-error-warning-line"
></i>
</div>
<div
class=
"empty-message"
>
没有找到匹配的音色
</div>
</div>
</div>
</div>
<div
class=
"action-buttons"
>
<button
class=
"secondary-button"
@
click=
"prevStep"
>
<i
class=
"ri-arrow-left-line"
></i>
<span>
上一步
</span>
</button>
<button
class=
"primary-button"
@
click=
"nextStep"
>
<span>
下一步
</span>
<i
class=
"ri-arrow-right-line"
></i>
</button>
</div>
</div>
<!-- 步骤3:设置视频参数 -->
<div
v-if=
"currentStep === 3"
class=
"step-content-panel"
>
<div
class=
"section-title"
>
<i
class=
"ri-settings-3-line"
></i>
<span>
设置视频参数
</span>
</div>
<div
class=
"form-group"
>
<label
class=
"form-control-label"
>
视频分辨率
</label>
<div
class=
"resolution-options"
>
<div
v-for=
"option in resolutionOptions"
:key=
"option.value"
class=
"resolution-option"
:class=
"
{ active: isResolutionSelected(option.value) }"
@click="selectResolution(option.value)">
{{
option
.
label
}}
</div>
</div>
</div>
<div
class=
"form-group"
style=
"display: none"
>
<label
class=
"form-control-label"
>
视频选项
</label>
<div
class=
"checkbox-group"
>
<div
class=
"checkbox-item"
:class=
"
{ 'checkbox-active': formData.videoParams.transparent }"
@click="formData.videoParams.transparent = !formData.videoParams.transparent">
<input
type=
"checkbox"
id=
"transparent"
v-model=
"formData.videoParams.transparent"
style=
"display: none"
/>
<label
class=
"checkbox-label"
for=
"transparent"
>
透明背景
</label>
</div>
<div
class=
"checkbox-item"
:class=
"
{ 'checkbox-active': formData.autoAnimoji }"
@click="formData.autoAnimoji = !formData.autoAnimoji">
<input
type=
"checkbox"
id=
"autoAnimoji"
v-model=
"formData.autoAnimoji"
style=
"display: none"
/>
<label
class=
"checkbox-label"
for=
"autoAnimoji"
>
自动添加动作
</label>
</div>
<div
class=
"checkbox-item"
:class=
"
{ 'checkbox-active': formData.subtitleParams.enabled }"
@click="formData.subtitleParams.enabled = !formData.subtitleParams.enabled">
<input
type=
"checkbox"
id=
"subtitleEnabled"
v-model=
"formData.subtitleParams.enabled"
style=
"display: none"
/>
<label
class=
"checkbox-label"
for=
"subtitleEnabled"
>
显示字幕
</label>
</div>
</div>
</div>
<div
class=
"action-buttons"
>
<button
class=
"secondary-button"
@
click=
"prevStep"
>
<i
class=
"ri-arrow-left-line"
></i>
<span>
上一步
</span>
</button>
<button
class=
"primary-button"
@
click=
"nextStep"
>
<span>
下一步
</span>
<i
class=
"ri-arrow-right-line"
></i>
</button>
</div>
</div>
<!-- 步骤4:提交任务 -->
<div
v-if=
"currentStep === 4"
class=
"step-content-panel"
>
<div
class=
"section-title"
>
<i
class=
"ri-upload-cloud-line"
></i>
<span>
提交任务
</span>
</div>
<div
class=
"task-summary-container"
>
<h3
class=
"summary-heading"
>
<i
class=
"ri-file-list-3-line"
></i>
任务信息确认
</h3>
<div
class=
"task-summary"
>
<div
class=
"summary-item"
>
<div
class=
"summary-label"
>
<i
class=
"ri-text"
></i>
文本内容:
</div>
<div
class=
"summary-value text-value"
>
{{
formData
.
text
.
length
>
100
?
formData
.
text
.
substring
(
0
,
100
)
+
'...'
:
formData
.
text
}}
</div>
</div>
<div
class=
"summary-item"
>
<div
class=
"summary-label"
>
<i
class=
"ri-user-line"
></i>
数字人形象:
</div>
<div
class=
"summary-value"
>
{{
getHumanName
(
formData
.
figureId
)
}}
</div>
</div>
<div
class=
"summary-item"
>
<div
class=
"summary-label"
>
<i
class=
"ri-volume-up-line"
></i>
音色:
</div>
<div
class=
"summary-value"
>
{{
getVoiceName
(
formData
.
ttsParams
.
person
)
}}
</div>
</div>
<div
class=
"summary-item"
>
<div
class=
"summary-label"
>
<i
class=
"ri-aspect-ratio-line"
></i>
视频分辨率:
</div>
<div
class=
"summary-value"
>
{{
getResolutionLabel
(
formData
.
videoParams
.
width
,
formData
.
videoParams
.
height
)
}}
</div>
</div>
<div
class=
"summary-item"
>
<div
class=
"summary-label"
>
<i
class=
"ri-settings-3-line"
></i>
高级选项:
</div>
<div
class=
"summary-value"
>
<span
v-if=
"formData.videoParams.transparent"
class=
"summary-tag"
>
<i
class=
"ri-contrast-drop-line"
></i>
透明背景
</span>
<span
v-if=
"formData.autoAnimoji"
class=
"summary-tag"
>
<i
class=
"ri-emotion-line"
></i>
自动添加动作
</span>
<span
v-if=
"formData.subtitleParams.enabled"
class=
"summary-tag"
>
<i
class=
"ri-subtitle"
></i>
显示字幕
</span>
</div>
</div>
</div>
</div>
<div
class=
"action-buttons"
>
<button
class=
"secondary-button"
@
click=
"prevStep"
>
<i
class=
"ri-arrow-left-line"
></i>
<span>
上一步
</span>
</button>
<button
class=
"primary-button submit-button"
@
click=
"submitTask"
:disabled=
"isSubmitting"
>
<i
v-if=
"isSubmitting"
class=
"ri-loader-4-line spinning"
></i>
<span>
{{
isSubmitting
?
'提交中...'
:
'确认提交'
}}
</span>
</button>
</div>
</div>
<!-- 步骤5:查看结果 -->
<div
v-if=
"currentStep === 5"
class=
"step-content-panel"
>
<div
class=
"section-title"
>
<i
class=
"ri-video-line"
></i>
<span>
查看结果
</span>
</div>
<div
class=
"task-result-container"
>
<!-- 任务状态卡片 -->
<div
class=
"task-status-card"
>
<div
class=
"status-header"
>
<div
class=
"task-id"
>
<span
class=
"label"
>
任务ID:
</span>
<span
class=
"value"
>
{{
taskResult
?.
taskId
||
'未知'
}}
</span>
</div>
<div
:class=
"['status-badge', getStatusClass(taskStatus?.status)]"
>
{{
getStatusText
(
taskStatus
?.
status
)
}}
</div>
</div>
<!-- 处理中状态显示 -->
<div
v-if=
"taskStatus?.status === 'PROCESSING' || taskStatus?.status === 'GENERATING'"
class=
"processing-section"
>
<div
class=
"processing-indicator"
>
<i
class=
"ri-loader-4-line animate-spin"
></i>
<span>
视频生成中,请稍候...
</span>
</div>
<div
class=
"processing-info"
>
<p>
百度数字人视频生成通常需要10-60秒。生成完成后您将看到视频播放界面。
</p>
</div>
</div>
<!-- 失败状态显示 -->
<div
v-if=
"taskStatus?.status === 'FAILED'"
class=
"failure-section"
>
<div
class=
"failure-icon"
>
<i
class=
"ri-error-warning-line"
></i>
</div>
<div
class=
"failure-message"
>
<h3>
生成失败
</h3>
<p>
{{
taskStatus
?.
failedMessage
||
'未知错误'
}}
</p>
</div>
</div>
<!-- 成功状态显示 -->
<div
v-if=
"taskStatus?.status === 'SUCCESS'"
class=
"success-section"
>
<div
class=
"video-container"
>
<!-- 视频播放器 -->
<div
class=
"video-player-wrapper"
>
<video
ref=
"videoPlayer"
class=
"video-player"
:src=
"taskStatus.videoUrl"
controls
autoplay
controlsList=
"nodownload"
:poster=
"taskStatus.previewUrl"
></video>
</div>
<div
class=
"video-info"
>
<p>
创建时间:
{{
formatDate
(
taskStatus
.
createTime
)
}}
</p>
</div>
<div
class=
"video-controls"
>
<div
class=
"video-actions"
>
<button
class=
"action-button"
@
click=
"downloadVideo"
>
<i
class=
"ri-download-cloud-line"
></i>
<span>
下载视频
</span>
</button>
<button
class=
"action-button"
@
click=
"copyVideoLink"
>
<i
class=
"ri-link"
></i>
<span>
复制链接
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 更新按钮(当视频未生成完成时显示) -->
<div
v-if=
"taskStatus?.status !== 'SUCCESS'"
class=
"update-actions"
>
<div
class=
"auto-update-info"
v-if=
"autoQueryInterval"
>
<i
class=
"ri-time-line"
></i>
<span>
系统正在自动刷新状态 (每5秒)
</span>
</div>
<button
class=
"update-button"
@
click=
"queryTask"
:disabled=
"isQuerying"
>
<i
class=
"ri-refresh-line"
:class=
"
{ 'animate-spin': isQuerying }">
</i>
<span>
{{
isQuerying
?
'刷新中...'
:
'手动刷新'
}}
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</AppCard>
</
template
>
<
script
>
import
digitalHumanAPI
from
'@/utils/digitalHumanAPI'
import
{
ref
,
computed
,
onMounted
,
nextTick
}
from
'vue'
import
axios
from
'axios'
import
{
saveAs
}
from
'file-saver'
export
default
{
name
:
'BaiduDigitalHuman'
,
setup
()
{
// 状态定义
const
currentStep
=
ref
(
1
)
const
formData
=
ref
({
text
:
''
,
figureId
:
'A2A_V2-xinxin'
,
driveType
:
'TEXT'
,
ttsParams
:
{
person
:
'CAP_4146'
,
speed
:
'5'
,
volume
:
'5'
,
pitch
:
'5'
,
},
videoParams
:
{
width
:
1080
,
height
:
1920
,
transparent
:
false
,
},
autoAnimoji
:
true
,
subtitleParams
:
{
enabled
:
false
,
subtitlePolicy
:
'SRT'
,
},
backgroundImageUrl
:
''
,
callbackUrl
:
''
,
})
const
digitalHumans
=
ref
([
{
id
:
'A2A_V2-xinxin'
,
name
:
'梓欣'
,
gender
:
'female'
,
avatar
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%2089_8dc1165.png'
,
},
{
id
:
'A2A_V2-xixi'
,
name
:
'筱萱'
,
gender
:
'female'
,
avatar
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%2090_2cae36d.png'
,
},
{
id
:
'A2A_V2-xiaomeng2'
,
name
:
'乔雅'
,
gender
:
'female'
,
avatar
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%2091_70a3d4d.png'
,
},
{
id
:
'A2A_V2-aning'
,
name
:
'嘉睿'
,
gender
:
'male'
,
avatar
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/%E5%98%89%E7%9D%BF-2_34e59dc.png'
,
},
{
id
:
'A2A_V2-aning_red'
,
name
:
'嘉霖'
,
gender
:
'male'
,
avatar
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%2094_10f09cc.png'
,
},
{
id
:
'A2A_V2-gaoming'
,
name
:
'纪楚'
,
gender
:
'male'
,
avatar
:
'https://bce.bdstatic.com/doc/bce-doc/AI_DH/image%2095_ed96bc7.png'
,
},
])
const
femaleVoices
=
ref
([
{
id
:
'CAP_4146'
,
name
:
'度禧禧'
,
gender
:
'女声'
,
style
:
'温柔甜美'
,
previewUrl
:
'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/1e9d042c-f9d7-417f-88d3-4209f5516338/4146.wav'
,
},
{
id
:
'BV502_streaming'
,
name
:
'度小夏'
,
gender
:
'女声'
,
style
:
'标准音'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/075ca6ce61d49629f62c520734b9e70e.wav'
,
},
{
id
:
'7011_moxingxiaoxiao_16k'
,
name
:
'专业靠谱爽朗女'
,
gender
:
'女声'
,
style
:
'专业娴熟/沉稳冷静/激情饱满'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/314a0e551c40beaead431bb4a30c7f43.wav'
,
},
{
id
:
'7011_moxingkangxi_16k'
,
name
:
'热情悦耳女主播'
,
gender
:
'女声'
,
style
:
'元气活力/权威靠谱/激情饱满'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/6cd5a9da434866bf285ddf6fe0411bbc.wav'
,
},
{
id
:
'7011_moxinghuanhuan_16k'
,
name
:
'自信活泼小姐姐'
,
gender
:
'女声'
,
style
:
'元气活力/权威靠谱/沉稳冷静'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/e7e878850475aaf2f34f299d4850ff90.wav'
,
},
{
id
:
'7011_vc0020_16k'
,
name
:
'自然朴实小妹妹'
,
gender
:
'女声'
,
style
:
'专业娴熟/亲和力强/权威靠谱'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/ec2da60778a9dad8f973c976876e50a4.wav'
,
},
{
id
:
'7011_vc0053_16k'
,
name
:
'专注真诚大姐姐'
,
gender
:
'女声'
,
style
:
'专业娴熟/亲和力强/权威靠谱'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/73ffa1c551a7f1b4b252f89e48909036.wav'
,
},
{
id
:
'7011_vc0033_16k'
,
name
:
'职业霸气御姐'
,
gender
:
'女声'
,
style
:
'专业娴熟/权威靠谱/沉稳冷静'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/0c4a0336a2c3f0f77cad7b841a8fa15e.wav'
,
},
{
id
:
'7011_vc0019_16k'
,
name
:
'知性优雅叙事女声'
,
gender
:
'女声'
,
style
:
'专业娴熟/亲和力强/沉稳冷静'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/5a4d90dad835e2347e99ec4746f265ca.wav'
,
},
{
id
:
'7011_vc0048_16k'
,
name
:
'幽默东北大妹子'
,
gender
:
'女声'
,
style
:
'亲和力强/权威靠谱/激情饱满'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/86d9023df2473565cd6a0b3ee6a11e59.wav'
,
},
{
id
:
'7011_vc0114_16k'
,
name
:
'温柔亲和女主播'
,
gender
:
'女声'
,
style
:
'元气活力/权威靠谱/激情饱满'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/b2ef65039c63cad57ffed13dd8867dcd.wav'
,
},
{
id
:
'7011_vc0100_16k'
,
name
:
'北京口音女声'
,
gender
:
'女声'
,
style
:
'亲和力强/权威靠谱/沉稳冷静'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/b1ea9bae92b4b003b239527594a20ef8.wav'
,
},
])
const
maleVoices
=
ref
([
{
id
:
'CAP_4193'
,
name
:
'度泽言'
,
gender
:
'男声'
,
style
:
'温柔青年'
,
previewUrl
:
'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/a545f018-54a1-4a89-a279-2c56a901bd5b/4193.wav'
,
},
{
id
:
'CAP_4195'
,
name
:
'度怀安'
,
gender
:
'男声'
,
style
:
'磁性深情'
,
previewUrl
:
'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/029dd3eb-1bd9-455b-a5fe-3cc3d32f85c3/4195.wav'
,
},
{
id
:
'4001'
,
name
:
'度小科'
,
gender
:
'男声'
,
style
:
'权威靠谱'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/6765196a1b6b66e8357fb833dad02404.wav'
,
},
{
id
:
'7011_moxingchuyi_16k'
,
name
:
'专业自信男主播'
,
gender
:
'男声'
,
style
:
'专业娴熟/亲和力强/沉稳冷静'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/91affdc041d5b0dc6665408cf3b33a27.wav'
,
},
{
id
:
'7011_vc0104_16k'
,
name
:
'自信坦诚大男孩'
,
gender
:
'男声'
,
style
:
'专业娴熟/元气活力/幽默有趣'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/f2af2ed9c71b950c753a6db96ad92c1c.wav'
,
},
{
id
:
'7011_vc0041_16k'
,
name
:
'直接果断男主播'
,
gender
:
'男声'
,
style
:
'亲和力强/元气活力/幽默有趣'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/6c94172414c967a06d5a14030e2790da.wav'
,
},
{
id
:
'7011_vc0049_16k'
,
name
:
'硬朗自信小哥哥'
,
gender
:
'男声'
,
style
:
'元气活力/幽默有趣/激情饱满'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/7027e7bf631cb34fa5b079ba71709b3b.wav'
,
},
{
id
:
'7011_vc0147_16k'
,
name
:
'雄浑宽广男主播'
,
gender
:
'男声'
,
style
:
'专业娴熟/权威靠谱/沉稳冷静'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/cd71c7fb34ccfe009082d8b2ac3eee4a.wav'
,
},
{
id
:
'7011_vc0079_16k'
,
name
:
'头头是道讲解员'
,
gender
:
'男声'
,
style
:
'亲和力强/元气活力/激情饱满'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/faf238c938ec771252217912c50d96ad.wav'
,
},
{
id
:
'7011_vc0067_16k'
,
name
:
'东北磁性男声'
,
gender
:
'男声'
,
style
:
'专业商务'
,
previewUrl
:
'https://digital-human-pipeline-output.cdn.bcebos.com/de3759ffdedde37d69bbcaab8662c37b.wav'
,
},
])
const
activeVoices
=
ref
([])
const
isSubmitting
=
ref
(
false
)
const
isQuerying
=
ref
(
false
)
const
taskResult
=
ref
(
null
)
const
taskStatus
=
ref
(
null
)
const
errorMessage
=
ref
(
''
)
const
autoQueryInterval
=
ref
(
null
)
const
voiceSearchText
=
ref
(
''
)
const
audioPlayer
=
ref
(
null
)
const
videoPlayer
=
ref
(
null
)
const
previewAudio
=
ref
(
null
)
// 分辨率选项
const
resolutionOptions
=
ref
([
{
label
:
'720p (1280x720)'
,
value
:
'1280x720'
},
{
label
:
'1080p (1920x1080)'
,
value
:
'1920x1080'
},
{
label
:
'竖屏 (720x1280)'
,
value
:
'720x1280'
},
{
label
:
'竖屏 (1080x1920)'
,
value
:
'1080x1920'
},
])
// 计算属性
const
filteredActiveVoices
=
computed
(()
=>
{
if
(
!
voiceSearchText
.
value
)
{
return
activeVoices
.
value
}
const
searchText
=
voiceSearchText
.
value
.
toLowerCase
()
return
activeVoices
.
value
.
filter
(
(
voice
)
=>
voice
.
name
.
toLowerCase
().
includes
(
searchText
)
||
voice
.
style
.
toLowerCase
().
includes
(
searchText
)
)
})
// 将计算属性改为方法
const
getSelectedHumanGender
=
()
=>
{
const
selectedHuman
=
digitalHumans
.
value
.
find
((
h
)
=>
h
.
id
===
formData
.
value
.
figureId
)
return
selectedHuman
?.
gender
===
'female'
?
'女'
:
'男'
}
// 方法定义
const
initVoices
=
()
=>
{
const
selectedHuman
=
digitalHumans
.
value
.
find
((
h
)
=>
h
.
id
===
formData
.
value
.
figureId
)
if
(
selectedHuman
)
{
if
(
selectedHuman
.
gender
===
'female'
)
{
activeVoices
.
value
=
femaleVoices
.
value
}
else
{
activeVoices
.
value
=
maleVoices
.
value
}
}
else
{
activeVoices
.
value
=
femaleVoices
.
value
}
}
const
nextStep
=
()
=>
{
if
(
currentStep
.
value
<
5
)
{
currentStep
.
value
++
}
}
const
prevStep
=
()
=>
{
if
(
currentStep
.
value
>
1
)
{
currentStep
.
value
--
}
}
const
selectDigitalHuman
=
(
humanId
)
=>
{
formData
.
value
.
figureId
=
humanId
const
selectedHuman
=
digitalHumans
.
value
.
find
((
h
)
=>
h
.
id
===
humanId
)
if
(
selectedHuman
)
{
activeVoices
.
value
=
selectedHuman
.
gender
===
'female'
?
femaleVoices
.
value
:
maleVoices
.
value
const
currentVoiceExists
=
activeVoices
.
value
.
some
((
v
)
=>
v
.
id
===
formData
.
value
.
ttsParams
.
person
)
if
(
!
currentVoiceExists
&&
activeVoices
.
value
.
length
>
0
)
{
formData
.
value
.
ttsParams
.
person
=
activeVoices
.
value
[
0
].
id
}
}
else
{
activeVoices
.
value
=
femaleVoices
.
value
if
(
activeVoices
.
value
.
length
>
0
)
{
formData
.
value
.
ttsParams
.
person
=
activeVoices
.
value
[
0
].
id
}
}
}
const
selectVoice
=
(
voiceId
)
=>
{
formData
.
value
.
ttsParams
.
person
=
voiceId
}
// 关键方法:获取语音预览URL
const
getVoicePreviewUrl
=
(
voiceId
)
=>
{
const
allVoices
=
[...
femaleVoices
.
value
,
...
maleVoices
.
value
]
const
voice
=
allVoices
.
find
((
v
)
=>
v
.
id
===
voiceId
)
return
voice
&&
voice
.
previewUrl
?
voice
.
previewUrl
:
''
}
const
previewVoice
=
(
voiceId
)
=>
{
console
.
log
(
'预览音色:'
,
voiceId
)
// 停止之前正在播放的音频
if
(
audioPlayer
.
value
)
{
audioPlayer
.
value
.
pause
()
audioPlayer
.
value
=
null
}
const
previewUrl
=
getVoicePreviewUrl
(
voiceId
)
console
.
log
(
'音频URL:'
,
previewUrl
)
if
(
previewUrl
)
{
try
{
// 判断是否为本地开发环境
const
isLocalDev
=
window
.
location
.
hostname
===
'localhost'
||
window
.
location
.
hostname
===
'127.0.0.1'
if
(
isLocalDev
)
{
// 本地开发环境使用代理API
console
.
log
(
'本地开发环境,通过代理API获取音频...'
)
axios
.
get
(
`/api/duan/api/v1/digital_human/proxy-resource?url=
${
encodeURIComponent
(
previewUrl
)}
&type=audio`
)
.
then
((
response
)
=>
{
if
(
response
.
data
&&
response
.
data
.
success
)
{
if
(
response
.
data
.
data
&&
response
.
data
.
data
.
base64Data
)
{
const
audioSrc
=
response
.
data
.
data
.
base64Data
previewAudio
.
value
=
audioSrc
nextTick
(()
=>
{
if
(
audioPlayer
.
value
)
{
audioPlayer
.
value
.
play
().
catch
((
e
)
=>
{
console
.
error
(
'代理音频播放失败:'
,
e
)
alert
(
'音频播放失败,请检查您的浏览器设置是否允许自动播放'
)
})
}
})
}
else
{
console
.
error
(
'通过代理获取音频数据成功,但 base64Data 字段缺失。查看响应详情:'
,
'Response Data:'
,
JSON
.
stringify
(
response
.
data
),
'Response Data.data:'
,
JSON
.
stringify
(
response
.
data
.
data
)
)
alert
(
'通过代理获取音频失败(数据字段缺失),请稍后再试'
)
}
}
else
{
console
.
error
(
'通过代理获取音频数据返回非成功状态:'
,
response
.
data
)
alert
(
'通过代理获取音频失败(非成功状态),请稍后再试'
)
}
})
.
catch
((
e
)
=>
{
console
.
error
(
'音频代理API请求异常:'
,
e
)
alert
(
'音频加载失败,无法播放'
)
})
}
else
{
// 生产环境直接播放
console
.
log
(
'生产环境,直接播放音频...'
)
audioPlayer
.
value
=
new
Audio
(
previewUrl
)
audioPlayer
.
value
.
onerror
=
(
e
)
=>
{
console
.
error
(
'直接播放音频失败:'
,
e
,
previewUrl
)
alert
(
`音频加载失败:
${
previewUrl
}
`
)
audioPlayer
.
value
=
null
}
audioPlayer
.
value
.
onloadeddata
=
()
=>
console
.
log
(
'音频已加载,准备播放'
)
audioPlayer
.
value
.
onended
=
()
=>
console
.
log
(
'音频播放完成'
)
console
.
log
(
'开始直接播放音频:'
,
previewUrl
)
const
playPromise
=
audioPlayer
.
value
.
play
()
if
(
playPromise
!==
undefined
)
{
playPromise
.
then
(()
=>
console
.
log
(
'音频开始直接播放成功'
))
.
catch
((
e
)
=>
{
console
.
error
(
'直接播放音频失败:'
,
e
)
alert
(
'无法播放试听音频,请检查音频链接或浏览器设置。'
)
audioPlayer
.
value
=
null
})
}
}
}
catch
(
err
)
{
console
.
error
(
'创建音频播放器逻辑出错:'
,
err
)
alert
(
'音频播放器初始化失败,请稍后再试。'
)
}
}
else
{
const
allVoices
=
[...
femaleVoices
.
value
,
...
maleVoices
.
value
]
const
voice
=
allVoices
.
find
((
v
)
=>
v
.
id
===
voiceId
)
alert
(
`音色
${
voice
?
voice
.
name
:
voiceId
}
暂无试听音频链接。`
)
}
}
const
getHumanName
=
(
humanId
)
=>
{
const
human
=
digitalHumans
.
value
.
find
((
h
)
=>
h
.
id
===
humanId
)
return
human
?
human
.
name
:
humanId
}
const
getVoiceName
=
(
voiceId
)
=>
{
const
allVoices
=
[...
femaleVoices
.
value
,
...
maleVoices
.
value
]
const
voice
=
allVoices
.
find
((
v
)
=>
v
.
id
===
voiceId
)
return
voice
?
voice
.
name
:
voiceId
}
const
submitTask
=
async
()
=>
{
if
(
!
formData
.
value
.
text
)
{
alert
(
'请输入文本内容'
)
return
}
isSubmitting
.
value
=
true
errorMessage
.
value
=
''
// 构建基础数字人视频所需的参数格式
const
params
=
{
text
:
formData
.
value
.
text
,
figureId
:
formData
.
value
.
figureId
,
driveType
:
formData
.
value
.
driveType
,
ttsParams
:
{
person
:
formData
.
value
.
ttsParams
.
person
,
speed
:
formData
.
value
.
ttsParams
.
speed
,
volume
:
formData
.
value
.
ttsParams
.
volume
,
pitch
:
formData
.
value
.
ttsParams
.
pitch
,
},
videoParams
:
{
width
:
formData
.
value
.
videoParams
.
width
,
height
:
formData
.
value
.
videoParams
.
height
,
transparent
:
formData
.
value
.
videoParams
.
transparent
,
},
autoAnimoji
:
formData
.
value
.
autoAnimoji
,
}
// 仅当字幕启用时添加字幕参数
if
(
formData
.
value
.
subtitleParams
.
enabled
)
{
params
.
subtitleParams
=
formData
.
value
.
subtitleParams
}
console
.
log
(
'提交基础数字人视频任务参数:'
,
JSON
.
stringify
(
params
))
try
{
const
response
=
await
digitalHumanAPI
.
submitBasicVideoTask
(
params
)
console
.
log
(
'API响应:'
,
response
.
data
)
if
(
response
.
data
.
success
)
{
taskResult
.
value
=
response
.
data
.
data
console
.
log
(
'任务提交成功:'
,
taskResult
.
value
)
startAutoQuery
()
currentStep
.
value
=
5
}
else
{
errorMessage
.
value
=
response
.
data
.
message
||
'任务提交失败'
alert
(
errorMessage
.
value
)
}
}
catch
(
error
)
{
console
.
error
(
'提交任务出错:'
,
error
)
errorMessage
.
value
=
error
.
response
?.
data
?.
message
||
'网络错误,请稍后重试'
alert
(
errorMessage
.
value
)
}
finally
{
isSubmitting
.
value
=
false
}
}
const
startAutoQuery
=
()
=>
{
stopAutoQuery
()
autoQueryInterval
.
value
=
setInterval
(()
=>
{
if
(
taskResult
.
value
&&
taskResult
.
value
.
taskId
)
{
queryTask
(
false
)
if
(
taskStatus
.
value
&&
(
taskStatus
.
value
.
status
===
'SUCCESS'
||
taskStatus
.
value
.
status
===
'FAILED'
))
{
stopAutoQuery
()
}
}
else
{
stopAutoQuery
()
}
},
3000
)
}
const
stopAutoQuery
=
()
=>
{
if
(
autoQueryInterval
.
value
)
{
clearInterval
(
autoQueryInterval
.
value
)
autoQueryInterval
.
value
=
null
}
}
const
queryTask
=
async
(
showLoading
=
true
)
=>
{
if
(
!
taskResult
.
value
||
!
taskResult
.
value
.
taskId
)
{
alert
(
'请先提交任务'
)
return
}
if
(
showLoading
)
{
isQuerying
.
value
=
true
}
try
{
const
response
=
await
digitalHumanAPI
.
queryBasicVideoTask
(
taskResult
.
value
.
taskId
)
console
.
log
(
'查询响应:'
,
response
.
data
)
if
(
response
.
data
.
success
)
{
taskStatus
.
value
=
response
.
data
.
data
console
.
log
(
'任务状态:'
,
taskStatus
.
value
)
// 本地开发环境处理跨域视频和封面图
const
isLocalDev
=
window
.
location
.
hostname
===
'localhost'
||
window
.
location
.
hostname
===
'127.0.0.1'
if
(
isLocalDev
&&
taskStatus
.
value
)
{
// 处理视频URL
if
(
taskStatus
.
value
.
videoUrl
&&
taskStatus
.
value
.
videoUrl
.
startsWith
(
'http'
))
{
const
originalVideoUrl
=
taskStatus
.
value
.
videoUrl
taskStatus
.
value
.
videoUrl
=
''
// 临时清空,避免直接加载
console
.
log
(
`本地开发环境,代理视频URL:
${
originalVideoUrl
}
`
)
axios
.
get
(
`/api/duan/api/v1/digital_human/proxy-resource?url=
${
encodeURIComponent
(
originalVideoUrl
)}
&type=video`
)
.
then
((
proxyResponse
)
=>
{
if
(
proxyResponse
.
data
&&
proxyResponse
.
data
.
success
&&
proxyResponse
.
data
.
data
&&
proxyResponse
.
data
.
data
.
base64Data
)
{
taskStatus
.
value
.
videoUrl
=
proxyResponse
.
data
.
data
.
base64Data
console
.
log
(
'视频URL已通过代理更新为Base64'
)
}
else
{
console
.
error
(
'通过代理获取视频URL失败或base64Data字段缺失:'
,
proxyResponse
.
data
)
taskStatus
.
value
.
videoUrl
=
originalVideoUrl
// 代理失败,尝试用回原始URL(可能依然失败)
}
})
.
catch
((
error
)
=>
{
console
.
error
(
'视频URL代理请求失败:'
,
error
)
taskStatus
.
value
.
videoUrl
=
originalVideoUrl
// 代理失败,尝试用回原始URL
})
}
// 处理封面图URL (poster)
if
(
taskStatus
.
value
.
previewUrl
&&
taskStatus
.
value
.
previewUrl
.
startsWith
(
'http'
))
{
const
originalPreviewUrl
=
taskStatus
.
value
.
previewUrl
taskStatus
.
value
.
previewUrl
=
'/img/default-avatar.png'
// 临时占位图
console
.
log
(
`本地开发环境,代理封面图URL:
${
originalPreviewUrl
}
`
)
axios
.
get
(
`/api/duan/api/v1/digital_human/proxy-resource?url=
${
encodeURIComponent
(
originalPreviewUrl
)}
&type=image`
)
.
then
((
proxyResponse
)
=>
{
if
(
proxyResponse
.
data
&&
proxyResponse
.
data
.
success
&&
proxyResponse
.
data
.
data
&&
proxyResponse
.
data
.
data
.
base64Data
)
{
taskStatus
.
value
.
previewUrl
=
proxyResponse
.
data
.
data
.
base64Data
console
.
log
(
'封面图URL已通过代理更新为Base64'
)
}
else
{
console
.
error
(
'通过代理获取封面图URL失败或base64Data字段缺失:'
,
proxyResponse
.
data
)
// 代理失败,保留占位图或尝试用回原始URL
// taskStatus.value.previewUrl = originalPreviewUrl;
}
})
.
catch
((
error
)
=>
{
console
.
error
(
'封面图URL代理请求失败:'
,
error
)
// taskStatus.value.previewUrl = originalPreviewUrl;
})
}
}
}
else
{
errorMessage
.
value
=
response
.
data
.
message
||
'查询任务失败'
if
(
showLoading
)
{
alert
(
errorMessage
.
value
)
}
else
{
console
.
error
(
errorMessage
.
value
)
}
}
}
catch
(
error
)
{
console
.
error
(
'查询任务出错:'
,
error
)
errorMessage
.
value
=
error
.
response
?.
data
?.
message
||
'网络错误,请稍后重试'
if
(
showLoading
)
{
alert
(
errorMessage
.
value
)
}
else
{
console
.
error
(
errorMessage
.
value
)
}
}
finally
{
if
(
showLoading
)
{
isQuerying
.
value
=
false
}
}
}
const
resetForm
=
()
=>
{
formData
.
value
=
{
text
:
''
,
figureId
:
'A2A_V2-xinxin'
,
driveType
:
'TEXT'
,
ttsParams
:
{
person
:
'CAP_4146'
,
speed
:
'5'
,
volume
:
'5'
,
pitch
:
'5'
,
},
videoParams
:
{
width
:
1080
,
height
:
1920
,
transparent
:
false
,
},
autoAnimoji
:
true
,
subtitleParams
:
{
enabled
:
false
,
subtitlePolicy
:
'SRT'
,
},
backgroundImageUrl
:
''
,
callbackUrl
:
''
,
}
taskResult
.
value
=
null
taskStatus
.
value
=
null
currentStep
.
value
=
1
}
const
getStatusText
=
(
status
)
=>
{
const
statusMap
=
{
PROCESSING
:
'处理中'
,
SUCCESS
:
'成功'
,
FAILED
:
'失败'
,
null
:
'未知'
,
}
return
statusMap
[
status
]
||
status
}
const
getStatusClass
=
(
status
)
=>
{
if
(
!
status
)
return
'status-unknown'
const
statusLower
=
status
.
toLowerCase
()
if
(
statusLower
===
'processing'
)
return
'status-processing'
if
(
statusLower
===
'success'
)
return
'status-success'
if
(
statusLower
===
'failed'
)
return
'status-failed'
return
'status-unknown'
}
const
formatTime
=
(
seconds
)
=>
{
if
(
!
seconds
||
isNaN
(
seconds
))
return
''
try
{
const
totalSeconds
=
parseInt
(
seconds
)
const
minutes
=
Math
.
floor
(
totalSeconds
/
60
)
const
remainingSeconds
=
totalSeconds
%
60
return
`
${
padZero
(
minutes
)}
:
${
padZero
(
remainingSeconds
)}
`
}
catch
(
e
)
{
console
.
error
(
'formatTime error:'
,
e
)
return
seconds
}
}
const
padZero
=
(
num
)
=>
{
return
num
<
10
?
`0
${
num
}
`
:
`
${
num
}
`
}
// 添加formatDate函数,处理日期格式化
const
formatDate
=
(
timeString
)
=>
{
if
(
!
timeString
)
return
''
try
{
const
date
=
new
Date
(
timeString
)
return
`
${
date
.
getFullYear
()}
-
${
padZero
(
date
.
getMonth
()
+
1
)}
-
${
padZero
(
date
.
getDate
())}
${
padZero
(
date
.
getHours
()
)}
:
${
padZero
(
date
.
getMinutes
())}
:
${
padZero
(
date
.
getSeconds
())}
`
}
catch
(
e
)
{
console
.
error
(
'formatDate error:'
,
e
)
return
timeString
}
}
const
downloadVideo
=
()
=>
{
if
(
taskStatus
.
value
&&
taskStatus
.
value
.
videoUrl
)
{
saveAs
(
taskStatus
.
value
.
videoUrl
,
`数字人视频_
${
new
Date
().
getTime
()}
.mp4`
)
}
else
{
alert
(
'视频链接无效,无法下载'
)
}
}
const
copyVideoLink
=
()
=>
{
if
(
taskStatus
.
value
&&
taskStatus
.
value
.
videoUrl
)
{
const
textarea
=
document
.
createElement
(
'textarea'
)
textarea
.
value
=
taskStatus
.
value
.
videoUrl
document
.
body
.
appendChild
(
textarea
)
textarea
.
select
()
try
{
const
successful
=
document
.
execCommand
(
'copy'
)
if
(
successful
)
{
alert
(
'视频链接已复制到剪贴板'
)
}
else
{
alert
(
'复制失败,请手动复制'
)
}
}
catch
(
err
)
{
alert
(
'复制失败: '
+
err
)
}
document
.
body
.
removeChild
(
textarea
)
}
else
{
alert
(
'视频链接无效,无法复制'
)
}
}
const
getResolutionLabel
=
(
width
,
height
)
=>
{
const
resolution
=
`
${
width
}
x
${
height
}
`
const
found
=
resolutionOptions
.
value
.
find
((
option
)
=>
option
.
value
===
resolution
)
return
found
?
found
.
label
:
`
${
width
}
x
${
height
}
`
}
const
selectResolution
=
(
resolution
)
=>
{
const
[
width
,
height
]
=
resolution
.
split
(
'x'
)
formData
.
value
.
videoParams
.
width
=
parseInt
(
width
)
formData
.
value
.
videoParams
.
height
=
parseInt
(
height
)
}
const
isResolutionSelected
=
(
resolution
)
=>
{
return
`
${
formData
.
value
.
videoParams
.
width
}
x
${
formData
.
value
.
videoParams
.
height
}
`
===
resolution
}
const
useExampleText
=
()
=>
{
formData
.
value
.
text
=
'欢迎使用紫荆AI图文创作工具,我可以为您生成各种风格的数字人视频。'
}
const
adjustVideoSize
=
()
=>
{
// 获取视频播放器包装元素
const
playerWrapper
=
document
.
querySelector
(
'.video-player-wrapper'
)
if
(
playerWrapper
)
{
playerWrapper
.
style
.
maxWidth
=
'640px'
}
}
// 生命周期钩子
onMounted
(()
=>
{
initVoices
()
// 判断是否为本地开发环境
const
isLocalDev
=
window
.
location
.
hostname
===
'localhost'
||
window
.
location
.
hostname
===
'127.0.0.1'
if
(
isLocalDev
)
{
// 本地环境处理图片跨域问题
console
.
log
(
'本地开发环境,处理图片跨域问题...'
)
digitalHumans
.
value
.
forEach
((
human
)
=>
{
if
(
human
.
avatar
&&
human
.
avatar
.
startsWith
(
'http'
))
{
const
originalAvatar
=
human
.
avatar
human
.
avatar
=
'/img/default-avatar.png'
// 确保这个占位图片存在于 public/img/ 目录下
axios
.
get
(
`/api/duan/api/v1/digital_human/proxy-resource?url=
${
encodeURIComponent
(
originalAvatar
)}
&type=image`
)
.
then
((
response
)
=>
{
if
(
response
.
data
&&
response
.
data
.
success
)
{
if
(
response
.
data
.
data
&&
response
.
data
.
data
.
base64Data
)
{
human
.
avatar
=
response
.
data
.
data
.
base64Data
console
.
log
(
`数字人
${
human
.
name
}
头像已转换为Base64格式`
)
}
else
{
console
.
warn
(
`加载数字人
${
human
.
name
}
头像通过代理返回成功,但 base64Data 字段缺失。查看响应详情:`
,
'Response Data:'
,
JSON
.
stringify
(
response
.
data
),
'Response Data.data:'
,
JSON
.
stringify
(
response
.
data
.
data
)
)
}
}
else
{
console
.
warn
(
`加载数字人
${
human
.
name
}
头像通过代理返回非成功状态:`
,
response
.
data
)
}
})
.
catch
((
error
)
=>
{
console
.
error
(
`加载数字人
${
human
.
name
}
头像代理请求失败:`
,
error
)
})
}
})
}
})
// 返回模板中需要使用的内容
return
{
currentStep
,
formData
,
digitalHumans
,
femaleVoices
,
maleVoices
,
activeVoices
,
filteredActiveVoices
,
isSubmitting
,
isQuerying
,
taskResult
,
taskStatus
,
errorMessage
,
voiceSearchText
,
videoPlayer
,
audioPlayer
,
previewAudio
,
// 方法
nextStep
,
prevStep
,
selectDigitalHuman
,
selectVoice
,
previewVoice
,
getVoicePreviewUrl
,
getHumanName
,
getVoiceName
,
getSelectedHumanGender
,
submitTask
,
queryTask
,
resetForm
,
getStatusText
,
getStatusClass
,
formatTime
,
formatDate
,
downloadVideo
,
copyVideoLink
,
getResolutionLabel
,
selectResolution
,
isResolutionSelected
,
resolutionOptions
,
useExampleText
,
adjustVideoSize
,
}
},
}
</
script
>
<
style
scoped
>
@import
'@/assets/css/text-creation-common.css'
;
/* 主容器布局调整 */
.main-container
{
display
:
flex
;
gap
:
15px
;
height
:
100%
;
}
/* 文本输入区域样式增强 */
.text-input-wrapper
{
position
:
relative
;
}
.input-actions
{
margin-top
:
8px
;
display
:
flex
;
justify-content
:
flex-end
;
}
.text-action-button
{
background-color
:
#f5f5f5
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
padding
:
5px
10px
;
font-size
:
12px
;
display
:
flex
;
align-items
:
center
;
gap
:
5px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.text-action-button
:hover
{
background-color
:
#e9e9e9
;
border-color
:
#ccc
;
}
.text-action-button
i
{
font-size
:
14px
;
}
/* 左侧步骤条变窄 */
.steps-container.slim-steps
{
width
:
200px
;
/* 调小左侧宽度 */
flex-shrink
:
0
;
margin-bottom
:
0
;
/* 移除底部间距,因为右侧会滚动 */
}
/* 右侧内容区变宽 */
.step-content-container.wide-content
{
flex-grow
:
1
;
/* 占据剩余宽度 */
max-height
:
calc
(
100vh
-
120px
);
/* 限制高度,留出顶部导航空间 */
overflow-y
:
auto
;
/* 内容超出时允许滚动 */
background
:
#fff
;
border-radius
:
8px
;
box-shadow
:
0
2px
10px
rgba
(
0
,
0
,
0
,
0.05
);
}
/* 步骤内容面板 */
.step-content-panel
{
padding
:
20px
;
height
:
auto
;
display
:
flex
;
flex-direction
:
column
;
}
/* 表单组样式 */
.form-group
{
margin-bottom
:
20px
;
flex-shrink
:
0
;
}
/* 优化数字人选择布局 */
.digital-human-grid.inline-grid
{
display
:
grid
;
grid-template-columns
:
repeat
(
6
,
1
fr
);
/* 六列布局 */
gap
:
15px
;
/* 增加间距 */
max-height
:
220px
;
/* 提高高度,原来是150px */
overflow-y
:
auto
;
/* 超出时添加滚动 */
padding
:
10px
;
/* 增加内边距 */
border
:
1px
solid
#eee
;
border-radius
:
8px
;
}
.digital-human-card.compact-card
{
width
:
auto
;
margin
:
0
;
padding
:
8px
;
/* 增加内边距 */
border
:
2px
solid
transparent
;
border-radius
:
8px
;
transition
:
all
0.2s
;
}
.digital-human-card.compact-card
:hover
{
background-color
:
#f9f9f9
;
transform
:
translateY
(
-2px
);
}
.digital-human-card.compact-card.selected
{
border-color
:
var
(
--primary-color
,
#ba003f
);
background-color
:
rgba
(
186
,
0
,
63
,
0.05
);
}
.compact-card
.human-avatar
{
width
:
70px
;
/* 增加头像尺寸,原来是50px */
height
:
70px
;
/* 增加头像尺寸,原来是50px */
margin
:
0
auto
8px
;
/* 增加底部间距 */
border-radius
:
50%
;
overflow
:
hidden
;
}
.compact-card
.human-avatar
img
{
width
:
100%
;
height
:
100%
;
object-fit
:
cover
;
}
.compact-card
.human-name
{
font-size
:
13px
;
/* 增大字体 */
text-align
:
center
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
/* 优化音色选择列表布局,适应更多音色 */
.voice-list.multi-column-list
{
display
:
grid
;
grid-template-columns
:
repeat
(
3
,
1
fr
);
/* 三列 */
gap
:
10px
;
max-height
:
300px
;
/* 增加高度,显示更多音色 */
overflow-y
:
auto
;
padding
:
5px
;
border
:
1px
solid
#eee
;
border-radius
:
8px
;
}
.voice-item.compact-voice-item
{
padding
:
8px
10px
;
border
:
2px
solid
#eee
;
border-radius
:
8px
;
transition
:
all
0.2s
;
display
:
flex
;
align-items
:
center
;
}
.voice-item.compact-voice-item
:hover
{
border-color
:
var
(
--primary-color
,
#ba003f
);
background-color
:
#f9f9f9
;
transform
:
translateY
(
-2px
);
}
.voice-item.compact-voice-item.selected
{
border-color
:
var
(
--primary-color
,
#ba003f
);
background-color
:
rgba
(
186
,
0
,
63
,
0.05
);
}
.voice-icon
{
font-size
:
20px
;
color
:
#666
;
margin-right
:
10px
;
flex-shrink
:
0
;
}
.voice-info
{
flex
:
1
;
min-width
:
0
;
/* 防止flex子项溢出 */
}
.voice-name
{
font-weight
:
bold
;
color
:
#333
;
font-size
:
14px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
.voice-desc
{
color
:
#666
;
font-size
:
12px
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
.voice-preview
{
margin-left
:
8px
;
flex-shrink
:
0
;
}
/* 试听按钮强调 */
.preview-button
{
background
:
none
;
border
:
none
;
color
:
var
(
--primary-color
,
#ba003f
);
font-size
:
20px
;
cursor
:
pointer
;
padding
:
4px
;
transition
:
transform
0.2s
;
line-height
:
1
;
}
.preview-button
:hover
{
transform
:
scale
(
1.2
);
}
.preview-button
:disabled
{
opacity
:
0.5
;
cursor
:
not-allowed
;
}
/* 音色过滤器 */
.voice-filter
{
margin-bottom
:
10px
;
}
.voice-search-input
{
padding
:
8px
12px
;
border-radius
:
20px
;
border
:
1px
solid
#ddd
;
width
:
100%
;
font-size
:
14px
;
background-image
:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23999' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0z'/%3E%3C/svg%3E")
;
background-repeat
:
no-repeat
;
background-position
:
10px
center
;
background-size
:
16px
;
padding-left
:
32px
;
}
.voice-filter-info
{
font-size
:
12px
;
color
:
#666
;
background-color
:
#f0f0f0
;
padding
:
2px
8px
;
border-radius
:
10px
;
margin-left
:
8px
;
}
/* 动作按钮 */
.action-buttons
{
margin-top
:
auto
;
/* 推到底部 */
display
:
flex
;
justify-content
:
space-between
;
padding-top
:
20px
;
}
/* 响应式调整 */
@media
(
max-width
:
1200px
)
{
.digital-human-grid.inline-grid
{
grid-template-columns
:
repeat
(
4
,
1
fr
);
/* 四列 */
max-height
:
280px
;
/* 响应式调整高度 */
}
.voice-list.multi-column-list
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
/* 两列 */
}
}
@media
(
max-width
:
768px
)
{
.main-container
{
flex-direction
:
column
;
}
.steps-container.slim-steps
{
width
:
100%
;
margin-bottom
:
15px
;
}
.step-content-container.wide-content
{
max-height
:
none
;
}
.digital-human-grid.inline-grid
{
grid-template-columns
:
repeat
(
3
,
1
fr
);
/* 三列 */
max-height
:
330px
;
/* 在移动端增加更多高度 */
}
.voice-list.multi-column-list
{
grid-template-columns
:
1
fr
;
/* 单列 */
}
}
/* 机位选择样式 */
.camera-options
{
display
:
flex
;
gap
:
15px
;
margin-top
:
10px
;
}
.camera-option
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
justify-content
:
center
;
padding
:
15px
;
border
:
1px
solid
#ddd
;
border-radius
:
8px
;
cursor
:
pointer
;
transition
:
all
0.3s
;
width
:
100px
;
height
:
100px
;
}
.camera-option
i
{
font-size
:
2rem
;
margin-bottom
:
10px
;
}
.camera-option.active
{
border-color
:
#007bff
;
background-color
:
rgba
(
0
,
123
,
255
,
0.1
);
color
:
#007bff
;
}
.resolution-options
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
10px
;
margin-top
:
10px
;
}
.resolution-option
{
padding
:
10px
15px
;
border
:
1px
solid
#ddd
;
border-radius
:
6px
;
cursor
:
pointer
;
transition
:
all
0.3s
;
}
.resolution-option.active
{
border-color
:
#007bff
;
background-color
:
rgba
(
0
,
123
,
255
,
0.1
);
color
:
#007bff
;
}
/* 提交任务和查看结果步骤样式优化 */
.task-summary-container
{
background-color
:
#f9f9f9
;
border-radius
:
12px
;
box-shadow
:
0
2px
8px
rgba
(
0
,
0
,
0
,
0.05
);
padding
:
20px
;
margin-bottom
:
20px
;
}
.summary-heading
{
font-size
:
20px
;
color
:
#333
;
margin-bottom
:
16px
;
display
:
flex
;
align-items
:
center
;
border-bottom
:
1px
solid
#eee
;
padding-bottom
:
12px
;
}
.summary-heading
i
{
margin-right
:
8px
;
color
:
var
(
--primary-color
,
#ba003f
);
}
.task-summary
{
display
:
grid
;
grid-template-columns
:
1
fr
;
gap
:
12px
;
}
.summary-item
{
padding
:
12px
;
background-color
:
white
;
border-radius
:
8px
;
box-shadow
:
0
1px
3px
rgba
(
0
,
0
,
0
,
0.05
);
transition
:
transform
0.2s
;
}
.summary-item
:hover
{
transform
:
translateY
(
-2px
);
box-shadow
:
0
3px
6px
rgba
(
0
,
0
,
0
,
0.1
);
}
.summary-label
{
font-weight
:
bold
;
color
:
#555
;
margin-bottom
:
6px
;
display
:
flex
;
align-items
:
center
;
}
.summary-label
i
{
margin-right
:
6px
;
color
:
var
(
--primary-color
,
#ba003f
);
font-size
:
16px
;
}
.summary-value
{
font-size
:
15px
;
color
:
#333
;
line-height
:
1.5
;
}
.text-value
{
max-height
:
80px
;
overflow-y
:
auto
;
white-space
:
pre-line
;
padding
:
8px
;
background-color
:
#f5f5f5
;
border-radius
:
4px
;
font-family
:
monospace
;
font-size
:
14px
;
}
.summary-tag
{
display
:
inline-flex
;
align-items
:
center
;
background-color
:
rgba
(
186
,
0
,
63
,
0.1
);
color
:
var
(
--primary-color
,
#ba003f
);
border-radius
:
16px
;
padding
:
4px
10px
;
margin-right
:
8px
;
margin-bottom
:
6px
;
font-size
:
13px
;
}
.summary-tag
i
{
margin-right
:
4px
;
}
.submit-button
{
background-color
:
var
(
--primary-color
,
#ba003f
);
font-size
:
16px
;
padding
:
10px
20px
;
transition
:
all
0.3s
;
}
.submit-button
:hover
{
background-color
:
darken
(
var
(
--primary-color
,
#ba003f
),
10%
);
transform
:
translateY
(
-3px
);
box-shadow
:
0
4px
8px
rgba
(
186
,
0
,
63
,
0.3
);
}
/* 查看结果样式 */
.task-result-container
{
display
:
flex
;
flex-direction
:
column
;
gap
:
20px
;
}
.task-status-card
{
background-color
:
white
;
border-radius
:
12px
;
box-shadow
:
0
4px
12px
rgba
(
0
,
0
,
0
,
0.08
);
padding
:
20px
;
transition
:
transform
0.3s
;
}
.task-status-card
:hover
{
transform
:
translateY
(
-4px
);
}
.status-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
16px
;
border-bottom
:
1px
solid
#eee
;
padding-bottom
:
12px
;
}
.task-id
{
font-weight
:
bold
;
color
:
#333
;
font-size
:
18px
;
}
.status-badge
{
padding
:
6px
12px
;
border-radius
:
20px
;
font-size
:
14px
;
font-weight
:
bold
;
display
:
flex
;
align-items
:
center
;
}
.status-processing
{
background-color
:
#e3f2fd
;
color
:
#1976d2
;
}
.status-success
{
background-color
:
#e8f5e9
;
color
:
#2e7d32
;
}
.status-failed
{
background-color
:
#ffebee
;
color
:
#c62828
;
}
.status-unknown
{
background-color
:
#f5f5f5
;
color
:
#757575
;
}
.processing-section
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
text-align
:
center
;
}
.processing-indicator
{
font-size
:
24px
;
margin-bottom
:
10px
;
}
.processing-info
{
font-size
:
14px
;
color
:
#666
;
}
.failure-section
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
text-align
:
center
;
}
.failure-icon
{
font-size
:
48px
;
color
:
#c62828
;
margin-bottom
:
10px
;
}
.failure-message
{
font-size
:
18px
;
color
:
#c62828
;
}
.success-section
{
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
text-align
:
center
;
}
.video-container
{
width
:
100%
;
margin
:
20px
auto
;
transition
:
all
0.3s
ease
;
}
/* 视频播放器样式 */
.video-player-wrapper
{
width
:
100%
;
margin-bottom
:
20px
;
border-radius
:
8px
;
overflow
:
hidden
;
box-shadow
:
0
4px
12px
rgba
(
0
,
0
,
0
,
0.15
);
transition
:
all
0.3s
ease
;
}
.video-player
{
width
:
100%
;
height
:
auto
;
max-height
:
600px
;
border-radius
:
8px
;
background-color
:
#000
;
}
/* 视频尺寸控制 */
.video-size-controls
{
display
:
none
;
}
.size-label
{
display
:
none
;
}
.size-btn
{
display
:
none
;
}
.video-info
{
background-color
:
#f9f9f9
;
padding
:
12px
;
border-radius
:
8px
;
margin-bottom
:
20px
;
}
.video-info
p
{
margin
:
6px
0
;
color
:
#555
;
font-size
:
14px
;
}
.video-actions
{
display
:
flex
;
flex-wrap
:
wrap
;
gap
:
10px
;
margin-top
:
15px
;
justify-content
:
center
;
}
.action-button
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
6px
;
background-color
:
var
(
--primary-color
,
#ba003f
);
color
:
white
;
border
:
none
;
border-radius
:
6px
;
padding
:
8px
16px
;
font-size
:
14px
;
cursor
:
pointer
;
transition
:
all
0.2s
;
}
.action-button
:hover
{
background-color
:
#d4004c
;
transform
:
translateY
(
-2px
);
}
.action-button
i
{
font-size
:
16px
;
}
.update-actions
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
}
.auto-update-info
{
font-size
:
14px
;
color
:
#666
;
}
.update-button
{
background-color
:
#e3f2fd
;
color
:
#1976d2
;
}
.update-button
:hover
{
background-color
:
#bbdefb
;
}
@keyframes
spin
{
0
%
{
transform
:
rotate
(
0deg
);
}
100
%
{
transform
:
rotate
(
360deg
);
}
}
.animate-spin
{
animation
:
spin
1s
linear
infinite
;
}
</
style
>
src/stores/menu.ts
浏览文件 @
8ce62951
...
@@ -297,6 +297,13 @@ const adminMenus: IMenuItem[] = [
...
@@ -297,6 +297,13 @@ const adminMenus: IMenuItem[] = [
icon
:
markRaw
(
IconAudio
),
icon
:
markRaw
(
IconAudio
),
// tag: 'v1-experiment-marketing-material-list',
// tag: 'v1-experiment-marketing-material-list',
},
},
// {
// id: 14,
// name: '数字人管理',
// path: '/material/digital-human',
// icon: markRaw(IconVideo),
// // tag: 'v1-experiment-marketing-material-list',
// },
{
{
id
:
14
,
id
:
14
,
name
:
'视频资料管理'
,
name
:
'视频资料管理'
,
...
@@ -392,7 +399,7 @@ export const useMenuStore = defineStore({
...
@@ -392,7 +399,7 @@ export const useMenuStore = defineStore({
const
filterMenus
=
(
menus
:
IMenuItem
[]):
IMenuItem
[]
=>
{
const
filterMenus
=
(
menus
:
IMenuItem
[]):
IMenuItem
[]
=>
{
return
menus
return
menus
.
filter
((
menu
)
=>
userPermissions
.
some
((
perm
)
=>
perm
.
id
===
menu
.
id
))
.
filter
((
menu
)
=>
userPermissions
.
some
((
perm
)
=>
perm
.
id
===
menu
.
id
))
.
filter
((
menu
)
=>
menu
.
role
?
menu
.
role
?.
includes
(
userRole
)
:
true
)
.
filter
((
menu
)
=>
(
menu
.
role
?
menu
.
role
?.
includes
(
userRole
)
:
true
)
)
.
map
((
menu
)
=>
{
.
map
((
menu
)
=>
{
const
filteredMenu
:
IMenuItem
=
{
const
filteredMenu
:
IMenuItem
=
{
...
menu
,
...
menu
,
...
...
src/utils/bosDirectUpload.js
0 → 100644
浏览文件 @
8ce62951
/**
* 百度云BOS直接上传工具
* 实现前端直接上传图片到百度云BOS对象存储
*/
import
axios
from
'axios'
import
CryptoJS
from
'crypto-js'
// 百度云BOS配置
const
BOS_CONFIG
=
{
accessKeyId
:
''
,
// 将在使用时从后端获取
secretAccessKey
:
''
,
// 将在使用时从后端获取
endpoint
:
'bj.bcebos.com'
,
bucketName
:
'ezijing'
,
domain
:
'https://ezijing.bj.bcebos.com'
,
}
/**
* 生成ISO8601格式的时间戳
* @returns {string} ISO8601格式的时间戳
*/
function
getISODateString
()
{
return
new
Date
().
toISOString
().
replace
(
/
\.\d
+Z$/
,
'Z'
)
}
/**
* 计算文件的MD5值
* @param {File} file - 文件对象
* @returns {Promise<string>} - 返回base64编码的MD5值
*/
function
calculateFileMD5
(
file
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
reader
=
new
FileReader
()
reader
.
onload
=
(
e
)
=>
{
try
{
const
wordArray
=
CryptoJS
.
lib
.
WordArray
.
create
(
e
.
target
.
result
)
const
md5
=
CryptoJS
.
MD5
(
wordArray
)
const
base64Md5
=
CryptoJS
.
enc
.
Base64
.
stringify
(
md5
)
resolve
(
base64Md5
)
}
catch
(
err
)
{
reject
(
err
)
}
}
reader
.
onerror
=
(
err
)
=>
reject
(
err
)
reader
.
readAsArrayBuffer
(
file
)
})
}
/**
* 生成上传策略和签名
* @param {Object} options - 配置选项
* @returns {Object} - 包含policy和signature的对象
*/
function
generatePolicyAndSignature
(
options
=
{})
{
const
{
expiration
=
new
Date
(
Date
.
now
()
+
30
*
60
*
1000
).
toISOString
(),
// 默认30分钟过期
contentLengthRange
=
[
0
,
10
*
1024
*
1024
],
// 默认最大10MB
keyPattern
=
null
,
// 对象键匹配模式
bucket
=
BOS_CONFIG
.
bucketName
,
}
=
options
// 构建policy对象 - 按照百度云BOS要求的格式
const
policy
=
{
expiration
,
conditions
:
[{
bucket
}],
}
// 添加content-length-range
policy
.
conditions
.
push
([
'content-length-range'
,
...
contentLengthRange
])
// 如果指定了keyPattern,添加到conditions中
if
(
keyPattern
)
{
policy
.
conditions
.
push
({
key
:
keyPattern
})
}
// 将policy转为JSON
const
policyJson
=
JSON
.
stringify
(
policy
)
console
.
log
(
'Policy JSON:'
,
policyJson
)
// 对policy进行base64编码
// 确保使用UTF-8编码,完全模拟Python中的base64.b64encode(policy)
const
policyBase64
=
btoa
(
unescape
(
encodeURIComponent
(
policyJson
)))
// 根据Python示例代码精确实现签名方法
// Python: hmac.new(sk, base64.b64encode(policy), hashlib.sha256).hexdigest()
// 使用CryptoJS的HMAC-SHA256算法计算签名
const
signature
=
CryptoJS
.
HmacSHA256
(
policyBase64
,
// 使用base64编码后的policy作为消息
BOS_CONFIG
.
secretAccessKey
// 使用SK作为密钥
).
toString
(
CryptoJS
.
enc
.
Hex
)
// 输出为十六进制字符串
console
.
log
(
'Generated policy:'
,
policyBase64
)
console
.
log
(
'Generated signature:'
,
signature
)
return
{
policy
:
policyBase64
,
signature
,
}
}
/**
* 生成百度云BOS的V1签名
* @param {Object} options - 签名选项
* @returns {Object} - 包含authorization和headers的对象
*/
function
generateBosV1Signature
(
options
)
{
const
{
method
,
uri
,
params
=
{},
headers
=
{},
timestamp
=
getISODateString
()
}
=
options
// 1. 生成CanonicalRequest
// 1.1 添加HTTP方法
let
canonicalRequest
=
`
${
method
.
toUpperCase
()}
\n`
// 1.2 添加URI
canonicalRequest
+=
`
${
uri
}
\n`
// 1.3 添加查询参数
const
queryParams
=
[]
Object
.
keys
(
params
)
.
sort
()
.
forEach
((
key
)
=>
{
queryParams
.
push
(
`
${
encodeURIComponent
(
key
)}
=
${
encodeURIComponent
(
params
[
key
])}
`
)
})
canonicalRequest
+=
`
${
queryParams
.
join
(
'&'
)}
\n`
// 1.4 添加规范化头信息
const
canonicalHeaders
=
{}
// 添加host头
canonicalHeaders
[
'host'
]
=
`
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
`
// 添加指定的头
Object
.
keys
(
headers
).
forEach
((
key
)
=>
{
const
lowerKey
=
key
.
toLowerCase
()
// 只保留必要的headers
if
(
lowerKey
===
'content-type'
||
lowerKey
===
'content-md5'
||
lowerKey
===
'date'
||
lowerKey
.
startsWith
(
'x-bce-'
)
)
{
canonicalHeaders
[
lowerKey
]
=
headers
[
key
]
}
})
// 如果没有Date头,添加x-bce-date头
if
(
!
canonicalHeaders
[
'date'
])
{
canonicalHeaders
[
'x-bce-date'
]
=
timestamp
}
// 构建规范化头部字符串
const
canonicalHeadersArray
=
[]
Object
.
keys
(
canonicalHeaders
)
.
sort
()
.
forEach
((
key
)
=>
{
canonicalHeadersArray
.
push
(
`
${
key
}
:
${
canonicalHeaders
[
key
]}
`
)
})
canonicalRequest
+=
canonicalHeadersArray
.
join
(
'
\
n'
)
console
.
log
(
'规范化请求字符串:'
,
canonicalRequest
)
// 2. 生成签名密钥
const
authStringPrefix
=
`bce-auth-v1/
${
BOS_CONFIG
.
accessKeyId
}
/
${
timestamp
}
/1800`
const
signingKey
=
CryptoJS
.
HmacSHA256
(
authStringPrefix
,
BOS_CONFIG
.
secretAccessKey
).
toString
(
CryptoJS
.
enc
.
Hex
)
console
.
log
(
'签名密钥:'
,
signingKey
)
// 3. 计算签名
const
signature
=
CryptoJS
.
HmacSHA256
(
canonicalRequest
,
signingKey
).
toString
(
CryptoJS
.
enc
.
Hex
)
console
.
log
(
'签名结果:'
,
signature
)
// 4. 生成Authorization字符串
const
headersToSign
=
Object
.
keys
(
canonicalHeaders
).
sort
().
join
(
';'
)
const
authorization
=
`
${
authStringPrefix
}
/host;
${
headersToSign
}
/
${
signature
}
`
return
{
authorization
,
headers
:
canonicalHeaders
,
}
}
/**
* 生成上传表单数据
* @param {File} file - 要上传的文件
* @param {Object} options - 上传选项
* @returns {FormData} - 填充好的FormData对象
*/
function
prepareUploadFormData
(
file
,
options
=
{})
{
const
{
key
=
`uploads/
${
new
Date
().
toISOString
().
slice
(
0
,
10
).
replace
(
/-/g
,
''
)}
/
${
Date
.
now
()}
_
${
file
.
name
}
`
,
contentType
=
file
.
type
||
'application/octet-stream'
,
successRedirectUrl
=
null
,
contentDisposition
=
null
,
metadata
=
{},
}
=
options
// 生成policy和signature
const
{
policy
,
signature
}
=
generatePolicyAndSignature
({
contentLengthRange
:
[
0
,
file
.
size
+
1024
],
// 允许的文件大小范围
keyPattern
:
key
,
bucket
:
BOS_CONFIG
.
bucketName
,
})
// 创建FormData
const
formData
=
new
FormData
()
// BOS API要求使用accessKey而不是accessKeyId
formData
.
append
(
'accessKey'
,
BOS_CONFIG
.
accessKeyId
)
formData
.
append
(
'policy'
,
policy
)
formData
.
append
(
'signature'
,
signature
)
formData
.
append
(
'key'
,
key
)
// 添加Content-Type
if
(
contentType
)
{
formData
.
append
(
'Content-Type'
,
contentType
)
}
// 添加Content-Disposition (如果指定)
if
(
contentDisposition
)
{
formData
.
append
(
'Content-Disposition'
,
contentDisposition
)
}
// 添加自定义元数据
Object
.
entries
(
metadata
).
forEach
(([
key
,
value
])
=>
{
formData
.
append
(
`x-bce-meta-
${
key
}
`
,
value
)
})
// 添加成功重定向URL (如果指定)
if
(
successRedirectUrl
)
{
formData
.
append
(
'success-redirect-url'
,
successRedirectUrl
)
}
// 添加文件数据 (必须放在最后)
formData
.
append
(
'file'
,
file
)
return
formData
}
/**
* 获取安全凭证
* 从后端获取临时上传凭证,避免在前端暴露永久凭证
* @returns {Promise<Object>} - 包含accessKeyId和secretAccessKey的对象
*/
async
function
getCredentials
()
{
try
{
// 直接使用硬编码的访问凭证(临时解决方案)
BOS_CONFIG
.
accessKeyId
=
'05c5a1cf304f45c6b17f55967449bc9b'
BOS_CONFIG
.
secretAccessKey
=
'42d4dea8b0614fc09488c9386bad9909'
return
{
accessKeyId
:
'05c5a1cf304f45c6b17f55967449bc9b'
,
secretAccessKey
:
'42d4dea8b0614fc09488c9386bad9909'
,
}
/* 注释掉原始的API调用代码
const response = await axios.get('/api/duan/api/v1/upload/bos_credentials');
if (response.data && response.data.success) {
BOS_CONFIG.accessKeyId = response.data.accessKeyId;
BOS_CONFIG.secretAccessKey = response.data.secretAccessKey;
return {
accessKeyId: response.data.accessKeyId,
secretAccessKey: response.data.secretAccessKey
};
} else {
throw new Error('获取上传凭证失败');
}
*/
}
catch
(
error
)
{
console
.
error
(
'获取上传凭证异常:'
,
error
)
throw
error
}
}
/**
* 使用表单方式直接上传文件到百度云BOS
* @param {File} file - 要上传的文件对象
* @param {Object} options - 上传选项
* @param {Function} onProgress - 上传进度回调
* @returns {Promise<string>} - 上传成功的文件URL
*/
export
async
function
uploadFileByForm
(
file
,
options
=
{},
onProgress
=
null
)
{
try
{
// 首先获取上传凭证
await
getCredentials
()
// 准备上传参数
const
{
prefix
=
'uploads'
,
fileType
=
'general'
,
key
=
`
${
prefix
}
/
${
fileType
}
/
${
new
Date
().
toISOString
().
slice
(
0
,
10
).
replace
(
/-/g
,
''
)}
/
${
Date
.
now
()}
_
${
file
.
name
}
`
,
}
=
options
// 准备表单数据
const
formData
=
prepareUploadFormData
(
file
,
{
key
,
contentType
:
file
.
type
||
'application/octet-stream'
,
})
// 设置上传URL
const
uploadUrl
=
`https://
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
/`
console
.
log
(
'上传请求URL:'
,
uploadUrl
)
// 执行上传请求
const
response
=
await
axios
.
post
(
uploadUrl
,
formData
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
},
onUploadProgress
:
(
progressEvent
)
=>
{
if
(
onProgress
&&
progressEvent
.
total
)
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
,
progressEvent
)
}
},
})
console
.
log
(
'表单上传请求响应:'
,
response
)
// 构建文件URL
const
fileUrl
=
`
${
BOS_CONFIG
.
domain
}
/
${
key
}
`
return
fileUrl
}
catch
(
error
)
{
console
.
error
(
'表单上传到百度云BOS失败:'
,
error
)
if
(
error
.
response
)
{
console
.
error
(
'响应状态:'
,
error
.
response
.
status
)
console
.
error
(
'响应数据:'
,
error
.
response
.
data
)
console
.
error
(
'响应头:'
,
error
.
response
.
headers
)
}
throw
error
}
}
/**
* 生成预签名PUT上传URL
* @param {string} key - 文件的对象键
* @param {string} contentType - 文件内容类型
* @param {number} contentLength - 文件大小
* @param {string} contentMD5 - 文件MD5值(可选)
* @param {number} expireInSeconds - 链接有效期(秒)
* @returns {string} - 预签名URL
*/
function
generatePresignedPutUrl
(
key
,
contentType
,
contentLength
,
contentMD5
=
''
,
expireInSeconds
=
1800
)
{
// 获取当前时间戳
const
timestamp
=
getISODateString
()
// 准备查询参数
const
params
=
{
'X-Bce-Date'
:
timestamp
,
'Content-Type'
:
contentType
,
'Content-Length'
:
contentLength
.
toString
(),
Host
:
`
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
`
,
}
// 如果有Content-MD5,添加到参数
if
(
contentMD5
)
{
params
[
'Content-MD5'
]
=
contentMD5
}
// 计算签名有效期截止时间
const
expirationTimestamp
=
Math
.
floor
(
Date
.
now
()
/
1000
)
+
expireInSeconds
params
[
'X-Bce-Expires'
]
=
expirationTimestamp
.
toString
()
// 准备签名所需的参数
const
signOptions
=
{
method
:
'PUT'
,
uri
:
`/
${
key
}
`
,
params
,
headers
:
{
'Content-Type'
:
contentType
,
'Content-Length'
:
contentLength
.
toString
(),
Host
:
`
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
`
,
},
timestamp
,
}
// 如果有Content-MD5,添加到请求头
if
(
contentMD5
)
{
signOptions
.
headers
[
'Content-MD5'
]
=
contentMD5
}
// 生成签名
const
{
authorization
}
=
generateBosV1Signature
(
signOptions
)
// 构建查询参数字符串
const
queryParams
=
{
authorization
:
authorization
,
'x-bce-date'
:
timestamp
,
expires
:
expirationTimestamp
.
toString
(),
'content-type'
:
contentType
,
}
if
(
contentMD5
)
{
queryParams
[
'content-md5'
]
=
contentMD5
}
// 拼接查询参数
const
queryString
=
Object
.
entries
(
queryParams
)
.
map
(([
key
,
value
])
=>
`
${
encodeURIComponent
(
key
)}
=
${
encodeURIComponent
(
value
)}
`
)
.
join
(
'&'
)
// 生成预签名URL
return
`https://
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
/
${
key
}
?
${
queryString
}
`
}
/**
* 使用PutObject方式直接上传文件到百度云BOS
* @param {File} file - 要上传的文件对象
* @param {Object} options - 上传选项
* @param {Function} onProgress - 上传进度回调
* @returns {Promise<string>} - 上传成功的文件URL
*/
export
async
function
uploadFileByPut
(
file
,
options
=
{},
onProgress
=
null
)
{
try
{
// 首先获取上传凭证
await
getCredentials
()
// 准备上传参数
const
{
prefix
=
'uploads'
,
fileType
=
'general'
,
key
=
`
${
prefix
}
/
${
fileType
}
/
${
new
Date
().
toISOString
().
slice
(
0
,
10
).
replace
(
/-/g
,
''
)}
/
${
Date
.
now
()}
_
${
file
.
name
}
`
,
}
=
options
// 计算文件的Content-MD5
let
contentMD5
=
''
try
{
contentMD5
=
await
calculateFileMD5
(
file
)
console
.
log
(
'计算的Content-MD5:'
,
contentMD5
)
}
catch
(
error
)
{
console
.
warn
(
'MD5计算失败,继续上传:'
,
error
)
}
// 检测是否在浏览器环境中
const
isInBrowser
=
typeof
window
!==
'undefined'
&&
typeof
window
.
document
!==
'undefined'
if
(
isInBrowser
)
{
console
.
log
(
'在浏览器环境中使用预签名URL方式上传...'
)
// 生成预签名URL
const
presignedUrl
=
generatePresignedPutUrl
(
key
,
file
.
type
||
'application/octet-stream'
,
file
.
size
,
contentMD5
)
console
.
log
(
'生成的预签名URL:'
,
presignedUrl
)
// 执行PUT上传请求(使用预签名URL,不需要额外的授权头)
try
{
const
response
=
await
axios
.
put
(
presignedUrl
,
file
,
{
// 使用最简化的头,避免CORS和浏览器安全限制问题
headers
:
{
'Content-Type'
:
file
.
type
||
'application/octet-stream'
,
},
onUploadProgress
:
(
progressEvent
)
=>
{
if
(
onProgress
&&
progressEvent
.
total
)
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
,
progressEvent
)
}
},
})
console
.
log
(
'预签名URL PUT上传成功:'
,
response
.
status
)
// 生成访问URL
const
fileUrl
=
`
${
BOS_CONFIG
.
domain
}
/
${
key
}
`
return
fileUrl
}
catch
(
putError
)
{
console
.
error
(
'预签名URL PUT上传失败,错误详情:'
,
putError
)
if
(
putError
.
response
)
{
console
.
error
(
' - 响应状态:'
,
putError
.
response
.
status
)
console
.
error
(
' - 响应数据:'
,
putError
.
response
.
data
)
console
.
error
(
' - 响应头:'
,
putError
.
response
.
headers
)
}
console
.
log
(
'尝试使用标准PUT方法上传...'
)
// 获取当前时间戳
const
timestamp
=
getISODateString
()
// 准备签名所需的参数(使用最简化的头信息)
const
signOptions
=
{
method
:
'PUT'
,
uri
:
`/
${
key
}
`
,
params
:
{},
headers
:
{
'Content-Type'
:
file
.
type
||
'application/octet-stream'
,
'x-bce-date'
:
timestamp
,
},
timestamp
,
}
// 如果有Content-MD5,添加到请求头
if
(
contentMD5
)
{
signOptions
.
headers
[
'Content-MD5'
]
=
contentMD5
}
// 生成签名
const
{
authorization
}
=
generateBosV1Signature
(
signOptions
)
// 设置上传URL
const
uploadUrl
=
`https://
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
/
${
key
}
`
try
{
const
response
=
await
axios
.
put
(
uploadUrl
,
file
,
{
headers
:
{
Authorization
:
authorization
,
'Content-Type'
:
file
.
type
||
'application/octet-stream'
,
'x-bce-date'
:
timestamp
,
...(
contentMD5
?
{
'Content-MD5'
:
contentMD5
}
:
{}),
},
onUploadProgress
:
(
progressEvent
)
=>
{
if
(
onProgress
&&
progressEvent
.
total
)
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
,
progressEvent
)
}
},
})
console
.
log
(
'标准PUT上传成功:'
,
response
.
status
)
// 生成访问URL
const
fileUrl
=
`
${
BOS_CONFIG
.
domain
}
/
${
key
}
`
return
fileUrl
}
catch
(
standardPutError
)
{
console
.
error
(
'标准PUT上传也失败,尝试使用表单上传:'
,
standardPutError
)
// 如果标准PUT上传失败,尝试使用表单上传(作为备选方案)
return
uploadFileByForm
(
file
,
options
,
onProgress
)
}
}
}
else
{
// 非浏览器环境(如Node.js),可以直接使用PUT
console
.
log
(
'非浏览器环境使用PUT方法上传'
)
// 获取当前时间戳
const
timestamp
=
getISODateString
()
// 生成签名
const
{
authorization
,
headers
:
signedHeaders
}
=
generateBosV1Signature
({
method
:
'PUT'
,
uri
:
`/
${
key
}
`
,
params
:
{},
headers
:
{
'Content-Type'
:
file
.
type
||
'application/octet-stream'
,
'Content-Length'
:
file
.
size
.
toString
(),
...(
contentMD5
?
{
'Content-MD5'
:
contentMD5
}
:
{}),
},
timestamp
,
})
console
.
log
(
'非浏览器环境PUT上传请求头:'
,
signedHeaders
)
// 设置上传URL
const
uploadUrl
=
`https://
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
/
${
key
}
`
// 构建请求头
const
requestHeaders
=
{
Host
:
`
${
BOS_CONFIG
.
bucketName
}
.
${
BOS_CONFIG
.
endpoint
}
`
,
Authorization
:
authorization
,
'Content-Type'
:
file
.
type
||
'application/octet-stream'
,
'Content-Length'
:
file
.
size
.
toString
(),
'x-bce-date'
:
timestamp
,
}
// 如果有Content-MD5,添加到请求头
if
(
contentMD5
)
{
requestHeaders
[
'Content-MD5'
]
=
contentMD5
}
// 执行上传请求
const
response
=
await
axios
.
put
(
uploadUrl
,
file
,
{
headers
:
requestHeaders
,
onUploadProgress
:
(
progressEvent
)
=>
{
if
(
onProgress
&&
progressEvent
.
total
)
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
,
progressEvent
)
}
},
})
console
.
log
(
'非浏览器环境PUT上传请求响应:'
,
response
)
// 生成访问URL
const
fileUrl
=
`
${
BOS_CONFIG
.
domain
}
/
${
key
}
`
return
fileUrl
}
}
catch
(
error
)
{
console
.
error
(
'PUT上传到百度云BOS失败:'
,
error
)
if
(
error
.
response
)
{
console
.
error
(
'响应状态:'
,
error
.
response
.
status
)
console
.
error
(
'响应数据:'
,
error
.
response
.
data
)
console
.
error
(
'响应头:'
,
error
.
response
.
headers
)
}
throw
error
}
}
/**
* 直接上传文件到百度云BOS (优先使用PUT方法)
* @param {File} file - 要上传的文件对象
* @param {Object} options - 上传选项
* @param {Function} onProgress - 上传进度回调
* @returns {Promise<string>} - 上传成功的文件URL
*/
export
async
function
uploadFileToBos
(
file
,
options
=
{},
onProgress
=
null
)
{
// 检测是否在浏览器环境中
const
isInBrowser
=
typeof
window
!==
'undefined'
&&
typeof
window
.
document
!==
'undefined'
console
.
log
(
'isInBrowser:'
,
isInBrowser
)
// 使用指定的上传方法或默认使用PUT
const
{
method
=
'PUT'
}
=
options
if
(
method
.
toUpperCase
()
===
'POST'
||
method
.
toUpperCase
()
===
'FORM'
)
{
// 用户明确要求使用表单上传
return
uploadFileByForm
(
file
,
options
,
onProgress
)
}
else
{
// 优先使用PUT上传,失败时自动回退到表单上传
try
{
return
await
uploadFileByPut
(
file
,
options
,
onProgress
)
}
catch
(
error
)
{
console
.
warn
(
'PUT上传失败,自动回退到表单上传:'
,
error
)
console
.
log
(
'使用表单方式重试上传...'
)
return
uploadFileByForm
(
file
,
options
,
onProgress
)
}
}
}
/**
* 上传图片到百度云BOS
* @param {File} file - 图片文件
* @param {string} type - 图片类型 (background|logo)
* @param {Function} onProgress - 上传进度回调
* @returns {Promise<string>} - 上传成功的图片URL
*/
export
async
function
uploadImageToBos
(
file
,
type
=
'general'
,
onProgress
=
null
)
{
return
uploadFileToBos
(
file
,
{
prefix
:
'images'
,
fileType
:
type
,
},
onProgress
)
}
export
default
{
uploadFileToBos
,
uploadImageToBos
,
uploadFileByForm
,
uploadFileByPut
,
}
src/utils/digitalHumanAPI.js
0 → 100644
浏览文件 @
8ce62951
import
axios
from
'axios'
import
{
uploadImageToBos
}
from
'./bosDirectUpload'
const
baseURL
=
'/api/duan/api/v1'
/**
* 百度数字人视频合成服务API
*/
const
digitalHumanAPI
=
{
/**
* 提交百度数字人视频合成任务
* @param {Object} params - 任务参数
* @returns {Promise} - axios请求Promise
*/
submitVideoTask
(
params
)
{
return
axios
.
post
(
`
${
baseURL
}
/digital_human/video/advanced/submit`
,
params
)
},
/**
* 提交基础数字人视频任务
* @param {Object} params - 任务参数
* @returns {Promise} - axios请求Promise
*/
submitBasicVideoTask
(
params
)
{
return
axios
.
post
(
`
${
baseURL
}
/digital_human/video/submit`
,
params
)
},
/**
* 查询百度数字人视频合成任务状态
* @param {string} taskId - 任务ID
* @returns {Promise} - axios请求Promise
*/
queryVideoTask
(
taskId
)
{
return
axios
.
get
(
`
${
baseURL
}
/digital_human/video/advanced/query`
,
{
params
:
{
taskId
},
})
},
/**
* 查询基础数字人视频任务状态
* @param {string} taskId - 任务ID
* @returns {Promise} - axios请求Promise
*/
queryBasicVideoTask
(
taskId
)
{
return
axios
.
get
(
`
${
baseURL
}
/digital_human/video/query`
,
{
params
:
{
taskId
},
})
},
/**
* 上传图片到阿里云OSS
* @param {File|string} imageData - 图片文件对象或Base64编码的图片数据
* @param {string} type - 图片类型 (background|logo)
* @param {Object} options - 上传选项
* @param {Function} options.onProgress - 上传进度回调函数
* @param {Number} options.retryCount - 失败重试次数,默认为2
* @param {Number} options.retryDelay - 重试间隔(毫秒),默认为1000
* @returns {Promise} - axios请求Promise
*/
uploadImageToAliyunOSS
(
imageData
,
type
,
options
=
{})
{
const
{
onProgress
=
null
,
retryCount
=
2
,
retryDelay
=
1000
}
=
options
let
currentRetry
=
0
const
executeUpload
=
()
=>
{
// 使用FormData封装数据
const
formData
=
new
FormData
()
if
(
imageData
instanceof
File
)
{
formData
.
append
(
'file'
,
imageData
)
}
else
{
// 如果是Base64,使用Blob转换
try
{
// 提取Base64数据
const
base64Data
=
imageData
.
split
(
','
)[
1
]
const
blob
=
this
.
_base64ToBlob
(
base64Data
,
'image/png'
)
formData
.
append
(
'file'
,
blob
,
`image_
${
Date
.
now
()}
.png`
)
}
catch
(
error
)
{
console
.
error
(
'Base64转换失败:'
,
error
)
formData
.
append
(
'image'
,
imageData
)
}
}
formData
.
append
(
'type'
,
type
)
// 构建上传请求
const
uploadEndpoint
=
`
${
baseURL
}
/uploads/image`
return
axios
.
post
(
uploadEndpoint
,
formData
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
},
// 添加上传进度回调
onUploadProgress
:
onProgress
?
(
progressEvent
)
=>
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
,
progressEvent
)
}
:
undefined
,
})
.
catch
((
error
)
=>
{
// 实现重试逻辑
if
(
currentRetry
<
retryCount
)
{
currentRetry
++
console
.
log
(
`上传失败,第
${
currentRetry
}
次重试...`
)
// 延迟后重试
return
new
Promise
((
resolve
)
=>
{
setTimeout
(()
=>
{
resolve
(
executeUpload
())
},
retryDelay
)
})
}
// 所有重试都失败,抛出错误
throw
error
})
}
return
executeUpload
()
},
/**
* 上传视频到阿里云OSS
* @param {File} videoFile - 视频文件对象
* @param {string} type - 视频类型 (opening-video|ending-video)
* @param {Object} options - 上传选项
* @param {Function} options.onProgress - 上传进度回调函数
* @returns {Promise} - axios请求Promise
*/
uploadVideoToAliyunOSS
(
videoFile
,
type
,
options
=
{})
{
const
{
onProgress
=
null
}
=
options
const
formData
=
new
FormData
()
formData
.
append
(
'file'
,
videoFile
)
formData
.
append
(
'type'
,
type
)
formData
.
append
(
'is_video'
,
'true'
)
const
uploadEndpoint
=
`
${
baseURL
}
/uploads/video`
return
axios
.
post
(
uploadEndpoint
,
formData
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
},
onUploadProgress
:
onProgress
?
(
progressEvent
)
=>
{
const
percentCompleted
=
Math
.
round
((
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
onProgress
(
percentCompleted
,
progressEvent
)
}
:
undefined
,
})
},
/**
* 将Base64数据转换为Blob对象
* @private
* @param {string} base64Data - Base64编码的数据
* @param {string} contentType - 内容类型
* @returns {Blob} - Blob对象
*/
_base64ToBlob
(
base64Data
,
contentType
)
{
const
byteCharacters
=
atob
(
base64Data
)
const
byteArrays
=
[]
for
(
let
offset
=
0
;
offset
<
byteCharacters
.
length
;
offset
+=
512
)
{
const
slice
=
byteCharacters
.
slice
(
offset
,
offset
+
512
)
const
byteNumbers
=
new
Array
(
slice
.
length
)
for
(
let
i
=
0
;
i
<
slice
.
length
;
i
++
)
{
byteNumbers
[
i
]
=
slice
.
charCodeAt
(
i
)
}
const
byteArray
=
new
Uint8Array
(
byteNumbers
)
byteArrays
.
push
(
byteArray
)
}
return
new
Blob
(
byteArrays
,
{
type
:
contentType
})
},
/**
* 提交高级视频合成任务
* @param {Object} data 请求参数
* @returns {Promise} 请求结果
*/
submitAdvancedVideoTask
(
data
)
{
return
axios
.
post
(
`
${
baseURL
}
/digital_human/video/advanced/submit`
,
data
)
},
/**
* 查询高级视频合成任务状态
* @param {String} taskId 任务ID
* @returns {Promise} 请求结果
*/
queryAdvancedVideoTask
(
taskId
)
{
return
axios
.
get
(
`
${
baseURL
}
/digital_human/video/advanced/query?taskId=
${
taskId
}
`
)
},
/**
* 获取模板列表
* @returns {Array} 模板列表
*/
getTemplateList
()
{
return
[
{
id
:
't-pf4kqasspwzwyexyte121'
,
name
:
'模板1'
,
ratio
:
'9:16'
,
isVertical
:
true
},
{
id
:
't-af4keqsspfzwyexyte123'
,
name
:
'模板2'
,
ratio
:
'9:16'
,
isVertical
:
true
},
{
id
:
't-ad4eeqsspfzwyqxyte125'
,
name
:
'模板3'
,
ratio
:
'9:16'
,
isVertical
:
true
},
{
id
:
't-cd4eeqsspfzwyqxyte127'
,
name
:
'模板4'
,
ratio
:
'9:16'
,
isVertical
:
true
},
{
id
:
't-hb4eeqsspfzwyqxyte129'
,
name
:
'模板5'
,
ratio
:
'16:9'
,
isVertical
:
false
},
{
id
:
't-cd4eeqsspfzwyqxyteditu3'
,
name
:
'模板6'
,
ratio
:
'9:16'
,
isVertical
:
true
},
{
id
:
't-cd4eeqsspfzwyqxyteditu4'
,
name
:
'模板7'
,
ratio
:
'9:16'
,
isVertical
:
true
},
{
id
:
't-cd4eeqsspfzwyqxyteditu5'
,
name
:
'无数字人模板'
,
ratio
:
'9:16'
,
isVertical
:
true
,
noDigitalHuman
:
true
},
]
},
/**
* 上传图片到存储服务 (兼容方法)
* @param {File|string} imageData - 图片文件对象或Base64编码的图片数据
* @param {string} type - 图片类型 (background|logo)
* @param {boolean} useBaiduCloud - 是否使用百度云存储,默认false使用阿里云OSS
* @returns {Promise} - axios请求Promise
*/
uploadImageToOSS
(
imageData
,
type
,
useBaiduCloud
=
false
)
{
console
.
log
(
'uploadImageToOSS'
,
imageData
,
type
,
useBaiduCloud
)
// 统一使用阿里云OSS上传,忽略useBaiduCloud参数
console
.
log
(
'使用阿里云OSS上传图片'
)
// 如果传入的是File对象,使用FormData
if
(
imageData
instanceof
File
)
{
return
this
.
uploadImageToAliyunOSS
(
imageData
,
type
,
{})
}
// 否则按原来的方式处理
const
formData
=
new
FormData
()
const
blob
=
this
.
_base64ToBlob
(
imageData
.
split
(
','
)[
1
],
'image/png'
)
formData
.
append
(
'file'
,
blob
,
`image_
${
Date
.
now
()}
.png`
)
formData
.
append
(
'type'
,
type
)
const
uploadEndpoint
=
`
${
baseURL
}
/uploads/image`
return
axios
.
post
(
uploadEndpoint
,
formData
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
},
})
},
/**
* 上传视频到存储服务 (兼容方法)
* @param {File} videoFile - 视频文件对象
* @param {string} type - 视频类型 (opening-video|ending-video)
* @param {boolean} useBaiduCloud - 是否使用百度云存储,默认false使用阿里云OSS
* @returns {Promise} - axios请求Promise
*/
uploadVideoToOSS
(
videoFile
,
type
,
useBaiduCloud
=
false
)
{
console
.
log
(
'uploadVideoToOSS'
,
videoFile
,
type
,
useBaiduCloud
)
// 统一使用阿里云OSS上传,忽略useBaiduCloud参数
console
.
log
(
'使用阿里云OSS上传视频'
)
return
this
.
uploadVideoToAliyunOSS
(
videoFile
,
type
,
{})
},
/**
* 上传图片到百度云BOS
* @param {File|string} imageData - 图片文件对象或Base64编码的图片数据
* @param {string} type - 图片类型 (background|logo)
* @param {Object} options - 上传选项
* @returns {Promise} - 包含上传结果的Promise
*/
uploadImageToBaiduBOS
(
imageData
,
type
,
options
=
{})
{
console
.
log
(
'使用百度云BOS PUT方式上传图片'
)
// 处理Base64图片数据
if
(
typeof
imageData
===
'string'
&&
imageData
.
startsWith
(
'data:'
))
{
// 转换Base64到Blob对象
const
blob
=
this
.
_base64ToBlob
(
imageData
.
split
(
','
)[
1
],
'image/png'
)
const
file
=
new
File
([
blob
],
`image_
${
Date
.
now
()}
.png`
,
{
type
:
'image/png'
})
// 使用直接上传到百度云BOS的方法,明确指定PUT方法
return
uploadImageToBos
(
file
,
{
fileType
:
type
,
method
:
'PUT'
,
// 明确指定使用PUT上传
},
options
.
onProgress
).
then
((
url
)
=>
{
console
.
log
(
'百度云BOS上传成功,文件URL:'
,
url
)
return
{
data
:
{
success
:
true
,
data
:
{
url
},
},
}
})
}
// 处理File对象,直接上传到百度云BOS
if
(
imageData
instanceof
File
)
{
return
uploadImageToBos
(
imageData
,
{
fileType
:
type
,
method
:
'PUT'
,
// 明确指定使用PUT上传
},
options
.
onProgress
).
then
((
url
)
=>
{
console
.
log
(
'百度云BOS上传成功,文件URL:'
,
url
)
return
{
data
:
{
success
:
true
,
data
:
{
url
},
},
}
})
}
// 如果不是File或Base64,返回错误
return
Promise
.
reject
(
new
Error
(
'不支持的图片数据格式,请提供File对象或Base64编码的图片数据'
))
},
/**
* 上传视频到百度云BOS (兼容方法,实际使用阿里云OSS)
* @param {File} videoFile - 视频文件对象
* @param {string} type - 视频类型 (opening-video|ending-video)
* @param {Object} options - 上传选项
* @returns {Promise} - axios请求Promise
*/
uploadVideoToBaiduBOS
(
videoFile
,
type
,
options
=
{})
{
console
.
warn
(
'uploadVideoToBaiduBOS已废弃,请使用uploadVideoToAliyunOSS'
)
return
this
.
uploadVideoToAliyunOSS
(
videoFile
,
type
,
options
)
},
}
export
default
digitalHumanAPI
vite.config.ts
浏览文件 @
8ce62951
...
@@ -50,6 +50,11 @@ export default defineConfig(() => ({
...
@@ -50,6 +50,11 @@ export default defineConfig(() => ({
changeOrigin
:
true
,
changeOrigin
:
true
,
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
qianfan/
,
''
),
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
api
\/
qianfan/
,
''
),
},
},
'/api/duan'
:
{
target
:
'https://saas-ai.ezijing.com'
,
changeOrigin
:
true
,
// rewrite: (path) => path.replace(/^\/api\/duan/, ''),
},
// '/api/lab': {
// '/api/lab': {
// target: 'http://local-com-resource-api.frontend.ezijing.com',
// target: 'http://local-com-resource-api.frontend.ezijing.com',
// changeOrigin: true,
// changeOrigin: true,
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论