提交 3c16b961 authored 作者: 王鹏飞's avatar 王鹏飞

feat: 添加数字人视频生成功能和相关组件

- 新增 AIDigitalHumanModal 组件用于生成数字人视频 - 引入数字人和配音数据 - 实现视频生成和插入功能 - 更新 API 代理路径为 /api/volcano_file - 优化相关样式和逻辑
上级 e793befe
import axios from 'axios'
import { message } from 'antd'
const appId = 'i-ri1jxut3edfmz'
const appKey = 'z3d69f164i698e3nph68'
/**
* 使用 Web Crypto API 实现 HMAC-SHA256
* 由于 HmacSHA256 通常是同步的,但 Web Crypto 是异步的,
* 我们需要调整拦截器以支持异步签名生成。
*/
const hmacSHA256 = async (data, key) => {
const encoder = new TextEncoder()
const keyData = encoder.encode(key)
const msgData = encoder.encode(data)
const cryptoKey = await window.crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await window.crypto.subtle.sign('HMAC', cryptoKey, msgData)
return Array.from(new Uint8Array(signature))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
const buildSign = async () => {
// 生成当前时间 + 1小时后的时间戳
const time = new Date(new Date().getTime() + 60 * 60 * 3000).toISOString()
// 使用 Web Crypto API 生成签名
const signature = await hmacSHA256(appId + time, appKey)
// 返回Authorization头格式: appId/signature/time
return `${appId}/${signature}/${time}`
}
const request = axios.create({ baseURL: '/api/xiling' })
request.interceptors.request.use(async (config) => {
config.headers.Authorization = await buildSign()
return config
})
request.interceptors.response.use((response) => {
if (response.data.code == 0) {
return response.data
} else {
const errorMsg = response.data.message?.global || '请求失败'
message.error(errorMsg)
return Promise.reject(response)
}
}, (error) => {
message.error('网络请求错误')
return Promise.reject(error)
})
/**
* 文件上传
* https://cloud.baidu.com/doc/AI_DH/s/5m11r7clu
*/
export const upload = async (data, options = {}) => {
const response = await request.post('/api/digitalhuman/open/v1/file/upload', data, {
headers: { 'Content-Type': 'multipart/form-data' },
...options,
})
return response.result
}
// 获取数字人列表
export const getDigitalHumanList = async (params, options) => {
const { systemFigure, item = 'VIDEO-DH_2D', pageSize = 1000 } = params
const response = await request.get('/api/digitalhuman/open/v1/figure/query', {
params: { systemFigure, item, pageSize },
...options,
})
return response.result?.result || []
}
/**
* 提交生成式数字人形象定制任务
*/
export const submitGenerativeFigure = async (params, options = {}) => {
return request.post('/api/digitalhuman/open/v1/figure/generative/train', params, options)
}
/**
* 查询生成式数字人形象定制状态
*/
export const queryGenerativeFigure = async (params, options = {}) => {
return request.get('/api/digitalhuman/open/v1/figure/lite2d/query', { params, ...options })
}
/**
* 提交123数字人视频任务
*/
export const submitVideoFast = async (params, options = {}) => {
return request.post('/api/digitalhuman/open/v1/video/submit/fast', params, options)
}
/**
* 提交基础视频合成任务
*/
export const submitVideo = async (params, options = {}) => {
return request.post('/api/digitalhuman/open/v1/video/submit', params, options)
}
/**
* 查询视频任务状态
*/
export const getVideoTask = async (params, options = {}) => {
return request.get('/api/digitalhuman/open/v1/video/task', { params, ...options })
}
/**
* 提交高级视频合成任务
*/
export const submitVideoAdvanced = async (params, options = {}) => {
return request.post('/api/digitalhuman/open/v1/video/advanced/submit', params, options)
}
/**
* 查询高级视频任务状态
*/
export const getVideoAdvancedTask = async (params, options = {}) => {
return request.get('/api/digitalhuman/open/v1/video/advanced/task', { params, ...options })
}
@image-link-url: '/src/assets/images/editor/icon_link_editor.png'; @image-link-url: '/src/assets/images/editor/icon_link_editor.png';
.wrap-phone-privew { .wrap-phone-privew {
.ant-modal { .ant-modal {
.ant-modal-close { .ant-modal-close {
inset-inline-end: 17px; inset-inline-end: 17px;
top: 0px; top: 0px;
right: 0; right: 0;
} }
.ant-modal-content { .ant-modal-content {
background: url('@/assets/images/phone_6.7.png') no-repeat; background: url('@/assets/images/phone_6.7.png') no-repeat;
background-size: 100% 100%; background-size: 100% 100%;
box-shadow: none; box-shadow: none;
height: 1003px; height: 1003px;
border-radius: 48px; border-radius: 48px;
padding: 36px 32px 34px 32px; padding: 36px 32px 34px 32px;
} }
.priview-modal { .priview-modal {
padding-top: 40px; padding-top: 40px;
width: 430px; width: 430px;
height: 932px; height: 932px;
position: relative; position: relative;
} }
} }
.phone-body { .phone-body {
flex: 1; flex: 1;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
width: 430px; width: 430px;
border-radius: 30px; border-radius: 30px;
} }
.previee-container { .previee-container {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 0 0 40px; border-radius: 0 0 40px;
.chapter-head-title { .chapter-head-title {
height: 44px; height: 44px;
line-height: 44px; line-height: 44px;
border-bottom: 1px solid #efefef; border-bottom: 1px solid #efefef;
h2 { h2 {
font-size: 18px; font-size: 18px;
text-align: left; text-align: left;
height: 44px; height: 44px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: 600; font-weight: 600;
overflow: hidden; overflow: hidden;
padding-right: 10px; padding-right: 10px;
align-items: center; align-items: center;
&.tree { &.tree {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
.no { .no {
display: block; display: block;
flex: 1; flex: 1;
max-width: calc(100% - 50px); max-width: calc(100% - 50px);
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.menu { .menu {
flex: 0 0 40px; flex: 0 0 40px;
display: block; display: block;
text-align: right; text-align: right;
} }
} }
.ant-btn { .ant-btn {
padding-right: 0; padding-right: 0;
padding: 0 5px; padding: 0 5px;
margin-left: 5px; margin-left: 5px;
} }
.ant-space { .ant-space {
width: 98%; width: 98%;
overflow: hidden; overflow: hidden;
.ant-space-item:nth-child(2) { .ant-space-item:nth-child(2) {
width: 98%; width: 98%;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.in { }
} .no {
} padding-left: 20px;
.no { }
padding-left: 20px; .menu {
} cursor: pointer;
.menu { }
cursor: pointer; }
} }
} .preview-content-it {
} flex: 1;
.preview-content-it { position: relative;
flex: 1; height: calc(100% - 45px);
position: relative; overflow: hidden;
height: calc(100% - 45px); .preview-content-show {
overflow: hidden; overflow-x: hidden;
.preview-content-show { overflow-y: auto;
overflow-x: hidden; height: calc(100% - 85px);
overflow-y: auto; }
height: calc(100% - 85px); .preview-page-number {
} height: 40px;
.preview-page-number { line-height: 40px;
height: 40px; text-align: center;
line-height: 40px; font-size: 14px;
text-align: center; color: #999;
font-size: 14px; border-top: 1px solid #efefef;
color: #999; background: #fff;
border-top: 1px solid #efefef; flex-shrink: 0;
background: #fff; }
flex-shrink: 0; * {
} line-height: 1.5;
* { font-family: '黑体';
line-height: 1.5; }
font-family: '黑体'; .preview-content-html {
} padding-bottom: 50px;
.preview-content-html {
padding-bottom: 50px; .chapter-practice,
.chapter-expand,
.chapter-practice, .chapter-item-section,
.chapter-expand, .chapter-item-header,
.chapter-item-section, .chapter-gallery-container {
.chapter-item-header, margin-top: 15px;
.chapter-gallery-container { margin-bottom: 15px;
margin-top: 15px; &.chapter-expand-inline,
margin-bottom: 15px; &.chapter-gallery-inline {
&.chapter-expand-inline, margin-top: 0;
&.chapter-gallery-inline { margin-bottom: 0;
margin-top: 0; margin-top: -3px !important;
margin-bottom: 0; }
margin-top: -3px !important; }
} .tooltip-icon {
} margin-top: -3px !important;
.tooltip-icon { }
margin-top: -3px !important; }
} .table-container {
} padding: 10px;
.table-container { }
padding: 10px; span {
} vertical-align: middle;
span { }
vertical-align: middle; table {
} border-collapse: collapse;
table { margin-left: 10px;
border-collapse: collapse; margin-right: 10px;
margin-left: 10px; width: calc(100% - 20px) !important;
margin-right: 10px; }
width: calc(100% - 20px) !important; table th {
} font-weight: 700;
table th { background-color: #f5f2f0;
font-weight: 700; }
background-color: #f5f2f0; table th,
} table td {
table th, border: 1px solid #ddd;
table td { padding: 5px;
border: 1px solid #ddd; font-size: 18px;
padding: 5px; }
font-size: 18px; div[data-w-e-type='video'] {
} margin: 10px;
div[data-w-e-type='video'] { }
margin: 10px; p[data-slate-node]:not(:empty) {
} margin: 15px 10px;
p[data-slate-node]:not(:empty) { }
margin: 15px 10px; p {
} line-height: 1.5;
p { font-size: 18px;
line-height: 1.5; font-family: '黑体';
font-size: 18px; margin: 15px 10px;
font-family: '黑体'; img {
margin: 15px 10px; height: auto;
img { max-width: 100%;
height: auto; }
max-width: 100%; > img {
} vertical-align: middle;
> img { + span {
vertical-align: middle; vertical-align: middle;
+ span { padding-top: 4px;
vertical-align: middle; line-height: 1.5;
padding-top: 4px; }
line-height: 1.5; }
} }
}
} // 插入图标(img + 文本节点)需要整体按行居中
p:has(> img[alt='icon-inline']) {
// 预览题库 display: flex;
.chapter-practice { align-items: center;
margin: 0 10px; gap: 6px;
position: relative; }
cursor: pointer;
} p:has(> img[alt='icon-inline']) > img[alt='icon-inline'] {
.practice-insert-topic { flex: 0 0 auto;
padding: 10px 10px 30px; }
.topic_style {
h3 { // 预览题库
font-size: 18px; .chapter-practice {
color: #333; margin: 0 10px;
line-height: 20px; position: relative;
margin-bottom: 10px; cursor: pointer;
} }
} .practice-insert-topic {
.practice-item { padding: 10px 10px 30px;
padding: 10px 0; .topic_style {
.topic-choose-title { h3 {
display: flex; font-size: 18px;
align-items: first baseline; color: #333;
margin-bottom: 10px; line-height: 20px;
.index { margin-bottom: 10px;
display: block; }
flex: 0 0 18px; }
line-height: 24px; .practice-item {
} padding: 10px 0;
p { .topic-choose-title {
display: inline; display: flex;
margin: 0; align-items: first baseline;
} margin-bottom: 10px;
img { .index {
max-height: 60px !important; display: block;
max-width: 100% !important; flex: 0 0 18px;
width: auto !important; line-height: 24px;
height: auto !important; }
vertical-align: middle; p {
} display: inline;
} margin: 0;
.topic_choose-list { }
.topic_choose-item { img {
padding: 3px 3px 3px 10px; max-height: 60px !important;
display: flex; max-width: 100% !important;
.choose-ico { width: auto !important;
flex: 0 0 20px; height: auto !important;
align-items: baseline; vertical-align: middle;
padding-top: 2px; }
.ico { }
display: inline-block; .topic_choose-list {
width: 18px; .topic_choose-item {
height: 18px; padding: 3px 3px 3px 10px;
vertical-align: text-top; display: flex;
background-size: cover; .choose-ico {
background-repeat: no-repeat; flex: 0 0 20px;
&.checkbox { align-items: baseline;
background-image: url('@/assets/images/editor/icon_checkbox_normal.png'); padding-top: 2px;
} .ico {
&.radio { display: inline-block;
background-image: url('@/assets/images/editor/icon_radio_normal.png'); width: 18px;
} height: 18px;
} vertical-align: text-top;
} background-size: cover;
} background-repeat: no-repeat;
.topic_content { &.checkbox {
margin-left: 5px; background-image: url('@/assets/images/editor/icon_checkbox_normal.png');
display: flex; }
align-items: first baseline; &.radio {
flex: 1; background-image: url('@/assets/images/editor/icon_radio_normal.png');
.correct { }
flex: 0 0 18px; }
display: block; }
} }
p { .topic_content {
margin: 0; margin-left: 5px;
display: inline; display: flex;
vertical-align: top; align-items: first baseline;
img { flex: 1;
max-width: 50% !important; .correct {
height: auto; flex: 0 0 18px;
} display: block;
} }
} p {
} margin: 0;
} display: inline;
} vertical-align: top;
pre { img {
white-space: pre-wrap; max-width: 50% !important;
word-break: break-all; height: auto;
word-wrap: break-word; }
line-height: 20px; }
font-size: 14px; }
padding: 10px; }
color: #333; }
background-color: #f5f2f0; }
margin: 15px 0; pre {
} white-space: pre-wrap;
word-break: break-all;
blockquote { word-wrap: break-word;
padding: 10px; line-height: 20px;
line-height: 20px; font-size: 14px;
font-size: 14px; padding: 10px;
background-color: #f5f2f0; color: #333;
border-left: 5px solid #b4d5ff; background-color: #f5f2f0;
margin: 15px 0; margin: 15px 0;
} }
a { blockquote {
color: #aa1941; padding: 10px;
&:link, line-height: 20px;
&:visited, font-size: 14px;
&:hover { background-color: #f5f2f0;
color: #aa1941; border-left: 5px solid #b4d5ff;
} margin: 15px 0;
span { }
color: #aa1941 !important;
} a {
&::after { color: #aa1941;
content: ''; &:link,
background-size: cover; &:visited,
background-image: url(@image-link-url); // 使用 Less 变量 &:hover {
width: 14px; color: #aa1941;
height: 14px; }
display: inline-block; span {
margin: 0 5px; color: #aa1941 !important;
vertical-align: text-bottom; }
} &::after {
} content: '';
background-size: cover;
h1 { background-image: url(@image-link-url); // 使用 Less 变量
line-height: 40px; width: 14px;
font-size: 24px; height: 14px;
} display: inline-block;
h2 { margin: 0 5px;
line-height: 34px; vertical-align: text-bottom;
font-size: 20px; }
} }
h3 {
line-height: 28px; h1 {
font-size: 18px; line-height: 40px;
} font-size: 24px;
h4 { }
line-height: 22px; h2 {
font-size: 16px; line-height: 34px;
} font-size: 20px;
h5 { }
line-height: 18px; h3 {
font-size: 14px; line-height: 28px;
} font-size: 18px;
h6 { }
line-height: 16px; h4 {
font-size: 12px; line-height: 22px;
} font-size: 16px;
video, }
audio { h5 {
width: 100%; line-height: 18px;
} font-size: 14px;
.chapter-image-pic { }
img { h6 {
width: 100%; line-height: 16px;
height: auto; font-size: 12px;
} }
} video,
audio {
.w-e-image-container { width: 100%;
img { }
&:not([style]) { .chapter-image-pic {
width: auto; img {
} width: 100%;
width: 100%; height: auto;
} }
} }
// 画廊 .w-e-image-container {
.chapter-gallery-container { img {
cursor: pointer; &:not([style]) {
.chapter-gallery-item { width: auto;
width: 50%; }
padding: 10px; width: 100%;
box-sizing: border-box; }
text-align: center; }
&.one {
width: 100%; // 画廊
} .chapter-gallery-container {
p { cursor: pointer;
} .chapter-gallery-item {
img { width: 50%;
height: auto; padding: 10px;
width: 100%; box-sizing: border-box;
} text-align: center;
&.one { &.one {
img { width: 100%;
max-width: 100%; }
height: auto; img {
width: 100%; height: auto;
} width: 100%;
} }
} &.one {
img {
&.chapter-gallery-inline { max-width: 100%;
display: inline-block; height: auto;
vertical-align: middle; width: 100%;
cursor: pointer; }
} }
} }
.chapter-item-tooltip, &.chapter-gallery-inline {
.chapter-item-link, display: inline-block;
.chapter-gallery-inline, vertical-align: middle;
.chapter-expand-inline { cursor: pointer;
text-indent: 0; }
svg { }
text-indent: 0;
} .chapter-item-tooltip,
} .chapter-item-link,
.chapter-gallery-inline,
.chapter-item-link { .chapter-expand-inline {
display: inline !important; text-indent: 0;
} svg {
text-indent: 0;
// 画廊展示 }
.gallery-prview-container { }
.gallery-img {
max-height: 300px; .chapter-item-link {
height: 300px; display: inline !important;
width: 100%; }
text-align: center;
display: flex; // 画廊展示
align-items: center; .gallery-prview-container {
position: relative; .gallery-img {
text-align: center; max-height: 300px;
justify-content: center; height: 300px;
&.noData { width: 100%;
text-align: center; text-align: center;
padding-top: 40px; display: flex;
color: #999; align-items: center;
font-size: 18px; position: relative;
} text-align: center;
.opa { justify-content: center;
position: absolute; &.noData {
top: 50%; text-align: center;
background-repeat: no-repeat; padding-top: 40px;
background-size: cover; color: #999;
width: 32px; font-size: 18px;
height: 32px; }
margin-top: -16px; .opa {
cursor: pointer; position: absolute;
&.prev { top: 50%;
left: 10px; background-repeat: no-repeat;
background-image: url('@/assets/images/editor/icon_preview_prev.png'); background-size: cover;
} width: 32px;
&.next { height: 32px;
right: 10px; margin-top: -16px;
background-image: url('@/assets/images/editor/icon_preview_next.png'); cursor: pointer;
} &.prev {
} left: 10px;
img { background-image: url('@/assets/images/editor/icon_preview_prev.png');
max-width: 100%; }
max-height: 300px; &.next {
// width: 100%; right: 10px;
// height: auto; background-image: url('@/assets/images/editor/icon_preview_next.png');
} }
} }
img {
.controll { max-width: 100%;
padding: 20px; max-height: 300px;
display: flex; // width: 100%;
justify-content: space-between; // height: auto;
} }
.steps { }
padding: 0 10px;
text-align: right; .controll {
color: #999; padding: 20px;
} display: flex;
.title { justify-content: space-between;
padding: 12px 10px; }
font-size: 20px; .steps {
line-height: 28px; padding: 0 10px;
margin-bottom: 5px; text-align: right;
border-bottom: 1px solid #f1f1f1; color: #999;
white-space: pre-wrap; }
word-wrap: break-word; .title {
word-break: break-all; padding: 12px 10px;
} font-size: 20px;
.desc { line-height: 28px;
padding: 0 10px; margin-bottom: 5px;
font-size: 16px; border-bottom: 1px solid #f1f1f1;
line-height: 28px; white-space: pre-wrap;
color: #999; word-wrap: break-word;
white-space: pre-wrap; word-break: break-all;
word-wrap: break-word; }
word-break: break-all; .desc {
} padding: 0 10px;
} font-size: 16px;
line-height: 28px;
.chapter-expand { color: #999;
margin: 0 10px; white-space: pre-wrap;
} word-wrap: break-word;
word-break: break-all;
// 扩展内容 }
.expand-content { }
padding-bottom: 25px;
} .chapter-expand {
margin: 0 10px;
.img-preview { }
text-align: center;
padding-top: 50px; // 扩展内容
img { .expand-content {
width: 100%; padding-bottom: 25px;
} }
}
.img-preview {
li, text-align: center;
dd, padding-top: 50px;
dt, img {
blockquote { width: 100%;
font-size: 18px; }
font-family: '黑体'; }
line-height: 1.5;
margin: 15px 0; li,
} dd,
ol { dt,
margin: 0 10px 0 28px; blockquote {
li { font-size: 18px;
padding-left: 5px; font-family: '黑体';
span { line-height: 1.5;
vertical-align: initial; margin: 15px 0;
} }
} ol {
} margin: 0 10px 0 28px;
ul { li {
margin: 0px 10px 0 30px; padding-left: 5px;
li { span {
span { vertical-align: initial;
vertical-align: initial; }
} }
} }
} ul {
margin: 0px 10px 0 30px;
.preview-content-other { li {
position: absolute; span {
left: 0; vertical-align: initial;
top: 0; }
width: 100%; }
height: 100%; }
overflow-x: hidden;
overflow-y: auto; .preview-content-other {
background-color: #fff; position: absolute;
} left: 0;
} top: 0;
width: 100%;
// 气泡 height: 100%;
.tooltip { overflow-x: hidden;
position: absolute; overflow-y: auto;
width: 70%; background-color: #fff;
max-width: 80%; }
// min-width: 50%; }
top: 10%;
left: 00%; // 气泡
background-color: #fff; .tooltip {
border: 1px solid #d2d2d2; position: absolute;
border-radius: 5px; width: 70%;
// opacity: 0; max-width: 80%;
transition: 0.2s opacity linear; // min-width: 50%;
z-index: -1; top: 10%;
box-shadow: 0 0 15px 1px rgba(122, 122, 122, 0.8); left: 00%;
.square { background-color: #fff;
width: 0px; border: 1px solid #d2d2d2;
height: 0px; border-radius: 5px;
position: absolute; // opacity: 0;
left: 0; transition: 0.2s opacity linear;
z-index: 10002; z-index: -1;
&::before { box-shadow: 0 0 15px 1px rgba(122, 122, 122, 0.8);
width: 0px; .square {
height: 0px; width: 0px;
position: absolute; height: 0px;
border: 9px solid transparent; position: absolute;
content: ''; left: 0;
padding: 0; z-index: 10002;
} &::before {
&::after { width: 0px;
width: 0px; height: 0px;
height: 0px; position: absolute;
position: absolute; border: 9px solid transparent;
content: ''; content: '';
border: 10px solid transparent; padding: 0;
} }
&.square_top { &::after {
top: 0px; width: 0px;
&::before { height: 0px;
top: -17px; position: absolute;
left: 3px; content: '';
border-color: transparent transparent #fff transparent; border: 10px solid transparent;
z-index: 12; }
} &.square_top {
&::after { top: 0px;
top: -20px; &::before {
left: 2px; top: -17px;
border-color: transparent transparent #d2d2d2 transparent; left: 3px;
z-index: 10; border-color: transparent transparent #fff transparent;
} z-index: 12;
} }
&.square_bottom { &::after {
bottom: 0px; top: -20px;
&::before { left: 2px;
top: -1px; border-color: transparent transparent #d2d2d2 transparent;
left: -19px; z-index: 10;
border-color: #fff transparent transparent transparent; }
z-index: 12; }
} &.square_bottom {
&::after { bottom: 0px;
top: 0px; &::before {
left: -20px; top: -1px;
border-color: #d2d2d2 transparent transparent transparent; left: -19px;
z-index: 10; border-color: #fff transparent transparent transparent;
} z-index: 12;
} }
} &::after {
.tooltip-content-container { top: 0px;
padding: 5px; left: -20px;
} border-color: #d2d2d2 transparent transparent transparent;
.tooltip-content { z-index: 10;
.content { }
line-height: 22px; }
min-height: 22px; }
overflow-x: hidden; .tooltip-content-container {
overflow-y: auto; padding: 5px;
max-height: 140px; }
padding: 5px; .tooltip-content {
p { .content {
white-space: pre-wrap; line-height: 22px;
word-wrap: break-word; min-height: 22px;
word-break: break-all; overflow-x: hidden;
margin: 0; overflow-y: auto;
} max-height: 140px;
} padding: 5px;
.content-opa { p {
border-top: 1px solid #ebebeb; white-space: pre-wrap;
padding-top: 5px; word-wrap: break-word;
display: flex; word-break: break-all;
justify-content: flex-end; margin: 0;
align-items: center; }
.c-link { }
max-width: 60px; .content-opa {
text-overflow: ellipsis; border-top: 1px solid #ebebeb;
white-space: nowrap; padding-top: 5px;
overflow: hidden; display: flex;
height: 18px; justify-content: flex-end;
line-height: 18px; align-items: center;
} .c-link {
a:link, max-width: 60px;
a:visited, text-overflow: ellipsis;
a:hover { white-space: nowrap;
font-size: 14px; overflow: hidden;
display: inline-block; height: 18px;
margin-left: 10px; line-height: 18px;
color: #aa1941; }
} a:link,
a { a:visited,
img { a:hover {
width: 18px; font-size: 14px;
height: 18px; display: inline-block;
} margin-left: 10px;
} color: #aa1941;
} }
} a {
} img {
} width: 18px;
} height: 18px;
}
.priview-drawer-container { }
height: 932px; }
top: 42px; }
left: 0px; }
width: 430px; }
border-radius: 15px 0 40px 40px; }
overflow: hidden;
.priview-drawer-header { .priview-drawer-container {
padding: 10px; height: 932px;
} top: 42px;
.priview-drawer-body { left: 0px;
padding: 10px; width: 430px;
height: 100%; border-radius: 15px 0 40px 40px;
overflow-x: hidden; overflow: hidden;
overflow-y: auto; .priview-drawer-header {
.ant-tree .ant-tree-treenode { padding: 10px;
position: relative; }
// padding-right: 40px; .priview-drawer-body {
.ant-tree-node-content-wrapper { padding: 10px;
overflow: hidden; height: 100%;
} overflow-x: hidden;
} overflow-y: auto;
.tree-node-title { .ant-tree .ant-tree-treenode {
display: inline-flex; position: relative;
align-items: center; // padding-right: 40px;
width: 100%; .ant-tree-node-content-wrapper {
.tree-node-name { overflow: hidden;
flex: 1; }
min-width: 0; }
overflow: hidden; .tree-node-title {
text-overflow: ellipsis; display: inline-flex;
white-space: nowrap; align-items: center;
} width: 100%;
.tree-node-page { .tree-node-name {
// position: absolute; flex: 1;
// right: 0; min-width: 0;
font-size: 12px; overflow: hidden;
color: #999; text-overflow: ellipsis;
// min-width: 32px; white-space: nowrap;
text-align: right; }
background: #f0f0f0; .tree-node-page {
border-radius: 4px; // position: absolute;
padding: 0 6px; // right: 0;
line-height: 20px; font-size: 12px;
} color: #999;
} // min-width: 32px;
} text-align: right;
} background: #f0f0f0;
border-radius: 4px;
padding: 0 6px;
line-height: 20px;
}
}
}
}
@image-url: '/src/assets/images/editor/icon_link_editor.png'; @image-url: '/src/assets/images/editor/icon_link_editor.png';
.wangeditor-customer-container { .wangeditor-customer-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
height: 100%; height: 100%;
.editor-content-container { .editor-content-container {
height: 100%; height: 100%;
overflow-y: hidden; overflow-y: hidden;
flex: 3; flex: 3;
padding: 10px; padding: 10px;
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
border-radius: 6px; border-radius: 6px;
.w-e-scroll { .w-e-scroll {
line-height: 1.5; line-height: 1.5;
font-family: '黑体'; font-family: '黑体';
} }
.w-e-scroll div[role='textarea'] > div { .w-e-scroll div[role='textarea'] > div {
margin: 15px 0 !important; margin: 15px 0 !important;
font-size: 18px; font-size: 18px;
line-height: 1.65; line-height: 1.65;
font-family: '黑体'; font-family: '黑体';
span { span {
font-size: 18px; font-size: 18px;
} }
} }
.w-e-text-container [data-slate-editor] h1.chapter-item-header, .w-e-text-container [data-slate-editor] h1.chapter-item-header,
.w-e-text-container [data-slate-editor] h2.chapter-item-section { .w-e-text-container [data-slate-editor] h2.chapter-item-section {
margin: 0 !important; margin: 0 !important;
} }
.ant-spin-nested-loading, .ant-spin-nested-loading,
.ant-spin-container, .ant-spin-container,
.w-e-text-container { .w-e-text-container {
height: 100%; height: 100%;
} }
.ant-spin-container > div, .ant-spin-container > div,
#w-e-textarea-1 { #w-e-textarea-1 {
min-height: 99%; min-height: 99%;
} }
.w-e-text-container [data-slate-editor] p:not(:empty) { .w-e-text-container [data-slate-editor] p:not(:empty) {
margin: 15px 0; margin: 15px 0;
span[data-slate-node='text'] { span[data-slate-node='text'] {
padding-top: 0px; padding-top: 0px;
} }
} }
.w-e-text-container [data-slate-editor] p { .w-e-text-container [data-slate-editor] p {
line-height: 1.5; line-height: 1.5;
font-size: 18px; font-size: 18px;
font-family: '黑体'; font-family: '黑体';
margin-block: 0; margin-block: 0;
span[data-slate-node='text'], span[data-slate-node='text'],
span[data-slate-node='element'] { span[data-slate-node='element'] {
vertical-align: middle; vertical-align: middle;
height: 100%; height: 100%;
line-height: 100%; line-height: 100%;
} }
span[data-slate-node='text'] { span[data-slate-node='text'] {
padding-top: 3px; padding-top: 3px;
} }
} }
.w-e-text-container [data-slate-editor] .w-e-image-container { .w-e-text-container [data-slate-editor] .w-e-image-container {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
+ span { + span {
padding-top: 8px; padding-top: 8px;
} }
} }
p { .w-e-text-container [data-slate-editor] img[alt='icon-inline'] {
span[data-slate-zero-width] { vertical-align: middle;
display: inline-block; display: inline-block;
width: 2px; }
}
> span:last-child { // 插入图标后(图片节点 + 文本节点)强制整行居中
span[data-slate-zero-width] { .w-e-text-container [data-slate-editor] p:has(.w-e-image-container img[alt='icon-inline']) {
display: inline-block; display: flex;
width: 2px; align-items: center;
// &:not(:empty) { gap: 6px;
// display: inline; }
// }
} .w-e-text-container [data-slate-editor] p:has(.w-e-image-container img[alt='icon-inline'])
} .w-e-image-container {
> span:first-child { flex: 0 0 auto;
span[data-slate-zero-width] { }
display: inline;
} .w-e-text-container [data-slate-editor] p:has(.w-e-image-container img[alt='icon-inline'])
} .w-e-image-container
} + span {
padding-top: 0;
.w-e-text-container [data-slate-editor] a, }
.w-e-text-container [data-slate-editor] .chapter-item-link {
position: relative; p {
&:link, span[data-slate-zero-width] {
&:hover, display: inline-block;
&:visited { width: 2px;
color: #aa1941; }
span { > span:last-child {
color: #aa1941 !important; span[data-slate-zero-width] {
} display: inline-block;
} width: 2px;
&::after { // &:not(:empty) {
content: ''; // display: inline;
background-size: cover; // }
background-image: url(@image-url); // 使用 Less 变量 }
width: 14px; }
height: 14px; > span:first-child {
display: inline-block; span[data-slate-zero-width] {
margin: 0 5px; display: inline;
vertical-align: text-bottom; }
} }
img { }
max-width: 14px;
max-height: 14px; .w-e-text-container [data-slate-editor] a,
min-width: 14px; .w-e-text-container [data-slate-editor] .chapter-item-link {
min-height: 14px; position: relative;
width: 14px; &:link,
height: 14px; &:hover,
} &:visited {
} color: #aa1941;
span {
.w-e-text-container [data-slate-editor] .chapter-item-link { color: #aa1941 !important;
&::after { }
content: ''; }
background-size: cover; &::after {
background-image: none; // 使用 Less 变量 content: '';
width: 0px; background-size: cover;
height: 0px; background-image: url(@image-url); // 使用 Less 变量
display: inline-block; width: 14px;
margin: 0; height: 14px;
vertical-align: text-bottom; display: inline-block;
} margin: 0 5px;
} vertical-align: text-bottom;
}
video { img {
max-width: 100%; max-width: 14px;
} max-height: 14px;
min-width: 14px;
li, min-height: 14px;
dd, width: 14px;
dt, height: 14px;
blockquote { }
font-size: 18px; }
font-family: '黑体';
line-height: 1.5; .w-e-text-container [data-slate-editor] .chapter-item-link {
margin: 15px 0; &::after {
} content: '';
table { background-size: cover;
td, background-image: none; // 使用 Less 变量
th { width: 0px;
font-size: 18px; height: 0px;
line-height: 1.5; display: inline-block;
} margin: 0;
} vertical-align: text-bottom;
}
.w-e-text-container [data-slate-editor] .chapter-gallery-inline { }
margin: 0 4px;
height: 18px; video {
cursor: pointer; max-width: 100%;
img { }
min-width: 18px;
min-height: 18px; li,
} dd,
} dt,
blockquote {
.title-head { font-size: 18px;
text-align: center; font-family: '黑体';
font-size: 18px; line-height: 1.5;
color: #333333; margin: 15px 0;
line-height: 40px; }
display: flex; table {
justify-content: space-between; td,
height: 52px; th {
padding-bottom: 10px; font-size: 18px;
.ant-divider { line-height: 1.5;
margin-top: 0; }
} }
.left {
flex: 1; .w-e-text-container [data-slate-editor] .chapter-gallery-inline {
width: calc(100% - 480px); margin: 0 4px;
h4 { height: 18px;
font-size: 16px; cursor: pointer;
white-space: nowrap; img {
text-overflow: ellipsis; min-width: 18px;
overflow: hidden; min-height: 18px;
text-align: left; }
padding-right: 10px; }
}
} .title-head {
.right { text-align: center;
display: flex; font-size: 18px;
right: 0; color: #333333;
top: 0; line-height: 40px;
align-items: flex-start; display: flex;
flex: 0 0 480px; justify-content: space-between;
justify-content: flex-end; height: 52px;
.save-time { padding-bottom: 10px;
display: flex; .ant-divider {
flex-direction: column; margin-top: 0;
justify-content: flex-end; }
.img { .left {
color: #ab1941; flex: 1;
font-size: 12px; width: calc(100% - 480px);
display: flex; h4 {
align-items: center; font-size: 16px;
height: 22px; white-space: nowrap;
line-height: 22px; text-overflow: ellipsis;
text-align: right; overflow: hidden;
display: block; text-align: left;
img { padding-right: 10px;
width: 20px; }
margin-right: 8px; }
vertical-align: middle; .right {
} display: flex;
} right: 0;
.time { top: 0;
font-size: 12px; align-items: flex-start;
color: #999; flex: 0 0 480px;
line-height: 14px; justify-content: flex-end;
} .save-time {
} display: flex;
.view { flex-direction: column;
&:disabled { justify-content: flex-end;
color: #666666 !important; .img {
} color: #ab1941;
} font-size: 12px;
.history { display: flex;
color: #666666; align-items: center;
&:hover { height: 22px;
.ant-btn-icon { line-height: 22px;
color: #ab1941; text-align: right;
} display: block;
} img {
} width: 20px;
} margin-right: 8px;
} vertical-align: middle;
} }
.editor-toolbar-container { }
margin: 10px; .time {
.w-e-bar { font-size: 12px;
padding: 0; color: #999;
svg { line-height: 14px;
width: 18px; }
height: 18px; }
} .view {
} &:disabled {
.w-e-bar-divider { color: #666666 !important;
margin: 0; }
padding-top: 10px; }
width: 100%; .history {
height: auto; color: #666666;
background: #fafafa; &:hover {
font-size: 14px; .ant-btn-icon {
font-weight: 600; color: #ab1941;
line-height: 40px; }
} }
.w-e-bar-item { }
height: auto; }
button { }
svg:nth-child(2) { }
display: none; .editor-toolbar-container {
} margin: 10px;
&.has-title { .w-e-bar {
width: 100%; padding: 0;
height: 100%; svg {
padding: 10px; width: 18px;
flex-direction: column; height: 18px;
align-items: center; }
.title { }
margin: 5px 0 0 0; .w-e-bar-divider {
} margin: 0;
} padding-top: 10px;
} width: 100%;
} height: auto;
} background: #fafafa;
.ant-spin-nested-loading, font-size: 14px;
.ant-spin-container { font-weight: 600;
width: 100%; line-height: 40px;
} }
.toolbar-customer { .w-e-bar-item {
flex: 1; height: auto;
} button {
svg:nth-child(2) {
.menu-tabs-key { display: none;
flex: 0 0 306px; }
background: #fafafa; &.has-title {
border: 1px solid #e5e5e5; width: 100%;
min-width: 300px; height: 100%;
margin-left: 10px; padding: 10px;
border-radius: 6px; flex-direction: column;
align-items: center;
height: 100%; .title {
overflow: hidden; margin: 5px 0 0 0;
.tabs { }
height: 50px; }
display: flex; }
width: 100%; }
justify-content: center; }
.tabs-item { .ant-spin-nested-loading,
flex: 1; .ant-spin-container {
text-align: center; width: 100%;
border-bottom: 1px solid #e5e5e5; }
color: #333; .toolbar-customer {
font-size: 16px; flex: 1;
line-height: 50px; }
box-sizing: border-box;
cursor: pointer; .menu-tabs-key {
position: relative; flex: 0 0 306px;
&.active { background: #fafafa;
span { border: 1px solid #e5e5e5;
width: 60px; min-width: 300px;
display: block; margin-left: 10px;
height: 2px; border-radius: 6px;
background-color: #ab1941;
position: absolute; height: 100%;
left: 50%; overflow: hidden;
margin-left: -30px; .tabs {
bottom: -1px; height: 50px;
} display: flex;
} width: 100%;
} justify-content: center;
} .tabs-item {
flex: 1;
.menu-tabs-content { text-align: center;
overflow-y: auto; border-bottom: 1px solid #e5e5e5;
overflow-x: hidden; color: #333;
max-height: calc(100% - 50px); font-size: 16px;
.styletem { line-height: 50px;
padding: 0 11px; box-sizing: border-box;
p { cursor: pointer;
font-weight: 600; position: relative;
color: #333333; &.active {
line-height: 22px; span {
font-size: 16px; width: 60px;
margin-top: 24px; display: block;
margin-bottom: 34px; height: 2px;
} background-color: #ab1941;
ul { position: absolute;
padding-left: 0; left: 50%;
} margin-left: -30px;
ul li { bottom: -1px;
display: flex; }
border-bottom: 1px solid #cccccc; }
padding: 10px 0; }
justify-content: space-between; }
align-items: center;
.left { .menu-tabs-content {
flex: 1; overflow-y: auto;
display: flex; overflow-x: hidden;
} max-height: calc(100% - 50px);
img { .styletem {
width: 23px; padding: 0 11px;
margin-right: 20px; p {
vertical-align: middle; font-weight: 600;
} color: #333333;
.color { line-height: 22px;
display: inline-block; font-size: 16px;
width: 22px; margin-top: 24px;
height: 22px; margin-bottom: 34px;
border-radius: 3px; }
&.color1 { ul {
background-color: #ab1941; padding-left: 0;
} }
&.color2 { ul li {
background-color: #2970f6; display: flex;
} border-bottom: 1px solid #cccccc;
&.color3 { padding: 10px 0;
background-color: #2ad882; justify-content: space-between;
} align-items: center;
&.color4 { .left {
background-color: #eb3351; flex: 1;
} display: flex;
} }
.type { img {
font-size: 14px; width: 23px;
color: #333333; margin-right: 20px;
line-height: 20px; vertical-align: middle;
vertical-align: middle; }
} .color {
b { display: inline-block;
font-weight: 600; width: 22px;
font-size: 14px !important; height: 22px;
margin-left: 15px; border-radius: 3px;
} &.color1 {
.use { background-color: #ab1941;
flex: 0 0 60px; }
text-align: right; &.color2 {
color: #1672ec; background-color: #2970f6;
} }
} &.color3 {
} background-color: #2ad882;
.toolbox-parent { }
position: relative; &.color4 {
background-color: #eb3351;
.custom-bar-box-input { }
position: absolute; }
z-index: 101; .type {
height: 48px; font-size: 14px;
padding: 10px 15px; color: #333333;
width: 298px; line-height: 20px;
top: 165px; vertical-align: middle;
box-sizing: border-box; }
.box { b {
justify-content: space-around; font-weight: 600;
display: flex; font-size: 14px !important;
width: 268px; margin-left: 15px;
} }
.customer-box-input-item { .use {
box-sizing: border-box; flex: 0 0 60px;
text-align: center; text-align: right;
width: 60px; color: #1672ec;
p { }
width: 60px; }
text-align: center; }
} .toolbox-parent {
.ant-input-number-outlined { position: relative;
border: none;
outline: none; .custom-bar-box-input {
width: 60px; position: absolute;
&:hover, z-index: 101;
&:focus { height: 48px;
border: none; padding: 10px 15px;
outline: none; width: 298px;
} top: 165px;
} box-sizing: border-box;
input { .box {
width: 60px; justify-content: space-around;
color: #333; display: flex;
text-align: center; width: 268px;
border: 1px solid #d9d9d9; }
border-radius: 5px; .customer-box-input-item {
height: 30px; box-sizing: border-box;
line-height: 30px; text-align: center;
background-color: #fff; width: 60px;
&:hover, p {
&:focus { width: 60px;
border-color: #b83956; text-align: center;
} }
} .ant-input-number-outlined {
.text { border: none;
color: #999; outline: none;
font-size: 12px; width: 60px;
margin-top: 4px; &:hover,
} &:focus {
} border: none;
} outline: none;
} }
} }
} input {
} width: 60px;
color: #333;
/* text-align: center;
['bold', 'underline', 'italic', 'through', 'code', 'sub', 'sup', 'clearStyle', 'color', 'bgColor', 'fontSize', 'fontFamily', 'indent', 'delIndent', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'lineHeight', 'insertImage', 'deleteImage', 'editImage', 'viewImageLink', 'imageWidth30', 'imageWidth50', 'imageWidth100', 'divider', 'emotion', 'insertLink', 'editLink', 'unLink', 'viewLink', 'codeBlock', 'blockquote', 'headerSelect', 'header1', 'header2', 'header3', 'header4', 'header5', 'todo', 'redo', 'undo', 'fullScreen', 'enter', 'bulletedList', 'numberedList', 'insertTable', 'deleteTable', 'insertTableRow', 'deleteTableRow', 'insertTableCol', 'deleteTableCol', 'tableHeader', 'tableFullWidth', 'insertVideo', 'uploadVideo', 'editVideoSize', 'uploadImage', 'codeSelectLang'] border: 1px solid #d9d9d9;
border-radius: 5px;
height: 30px;
['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'deleteImage', 'editImage', 'viewImageLink', 'imageWidth30', 'imageWidth50', 'imageWidth100', '', '', '', 'editLink', 'unLink', 'viewLink', '', '', 'headerSelect', 'header1', 'header2', 'header3', 'header4', 'header5', '', '', '', '', '', '', '', '', 'deleteTable', 'insertTableRow', 'deleteTableRow', 'insertTableCol', 'deleteTableCol', 'tableHeader', 'tableFullWidth', '', 'codeSelectLang', 'editVideoSize', '', ''] line-height: 30px;
*/ background-color: #fff;
&:hover,
&:focus {
border-color: #b83956;
}
}
.text {
color: #999;
font-size: 12px;
margin-top: 4px;
}
}
}
}
}
}
}
/*
['bold', 'underline', 'italic', 'through', 'code', 'sub', 'sup', 'clearStyle', 'color', 'bgColor', 'fontSize', 'fontFamily', 'indent', 'delIndent', 'justifyLeft', 'justifyRight', 'justifyCenter', 'justifyJustify', 'lineHeight', 'insertImage', 'deleteImage', 'editImage', 'viewImageLink', 'imageWidth30', 'imageWidth50', 'imageWidth100', 'divider', 'emotion', 'insertLink', 'editLink', 'unLink', 'viewLink', 'codeBlock', 'blockquote', 'headerSelect', 'header1', 'header2', 'header3', 'header4', 'header5', 'todo', 'redo', 'undo', 'fullScreen', 'enter', 'bulletedList', 'numberedList', 'insertTable', 'deleteTable', 'insertTableRow', 'deleteTableRow', 'insertTableCol', 'deleteTableCol', 'tableHeader', 'tableFullWidth', 'insertVideo', 'uploadVideo', 'editVideoSize', 'uploadImage', 'codeSelectLang']
['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'deleteImage', 'editImage', 'viewImageLink', 'imageWidth30', 'imageWidth50', 'imageWidth100', '', '', '', 'editLink', 'unLink', 'viewLink', '', '', 'headerSelect', 'header1', 'header2', 'header3', 'header4', 'header5', '', '', '', '', '', '', '', '', 'deleteTable', 'insertTableRow', 'deleteTableRow', 'insertTableCol', 'deleteTableCol', 'tableHeader', 'tableFullWidth', '', 'codeSelectLang', 'editVideoSize', '', '']
*/
// Extend menu import BaseModalMenu from './common/BaseModalMenu'
class AIDigitalHuman { import AIDigitalHumanModal from './common/AIDigitalHumanModal'
constructor() {
this.title = '数字人' class AIDigitalHuman extends BaseModalMenu {
this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 28 28"><path fill="currentColor" d="M12 5.5a2 2 0 0 0 1.491 1.935c.337.053.68.053 1.018 0A2 2 0 1 0 12 5.5m-1.337 1.058a3.5 3.5 0 1 1 6.675 0l4.419-1.436a2.477 2.477 0 1 1 1.53 4.712L18 11.552v3.822c0 .16.03.32.091.468l2.728 6.752a2.477 2.477 0 0 1-4.594 1.856l-2.243-5.553l-2.232 5.56a2.46 2.46 0 0 1-3.21 1.362a2.477 2.477 0 0 1-1.364-3.215l2.734-6.812q.09-.224.09-.466v-3.774L4.712 9.834a2.477 2.477 0 0 1 1.531-4.712zm2.518 2.346a5 5 0 0 1-.649-.162L5.78 6.548a.977.977 0 0 0-.604 1.859l5.46 1.774c.515.168.864.648.864 1.189v3.957c0 .35-.067.698-.198 1.024l-2.734 6.811a.977.977 0 0 0 .538 1.267a.96.96 0 0 0 1.252-.531l2.463-6.136c.42-1.045 1.897-1.047 2.319-.003l2.476 6.129a.977.977 0 1 0 1.812-.732L16.7 16.404a2.8 2.8 0 0 1-.2-1.03V11.37c0-.541.349-1.021.864-1.189l5.46-1.774a.977.977 0 1 0-.604-1.859l-6.752 2.194q-.32.104-.649.162a3.5 3.5 0 0 1-1.639 0"/></svg>` constructor() {
this.tag = 'button' super()
}
getValue() { this.title = '数字人'
return 'hello, 音频' this.iconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 28 28"><path fill="currentColor" d="M12 5.5a2 2 0 0 0 1.491 1.935c.337.053.68.053 1.018 0A2 2 0 1 0 12 5.5m-1.337 1.058a3.5 3.5 0 1 1 6.675 0l4.419-1.436a2.477 2.477 0 1 1 1.53 4.712L18 11.552v3.822c0 .16.03.32.091.468l2.728 6.752a2.477 2.477 0 0 1-4.594 1.856l-2.243-5.553l-2.232 5.56a2.46 2.46 0 0 1-3.21 1.362a2.477 2.477 0 0 1-1.364-3.215l2.734-6.812q.09-.224.09-.466v-3.774L4.712 9.834a2.477 2.477 0 0 1 1.531-4.712zm2.518 2.346a5 5 0 0 1-.649-.162L5.78 6.548a.977.977 0 0 0-.604 1.859l5.46 1.774c.515.168.864.648.864 1.189v3.957c0 .35-.067.698-.198 1.024l-2.734 6.811a.977.977 0 0 0 .538 1.267a.96.96 0 0 0 1.252-.531l2.463-6.136c.42-1.045 1.897-1.047 2.319-.003l2.476 6.129a.977.977 0 1 0 1.812-.732L16.7 16.404a2.8 2.8 0 0 1-.2-1.03V11.37c0-.541.349-1.021.864-1.189l5.46-1.774a.977.977 0 1 0-.604-1.859l-6.752 2.194q-.32.104-.649.162a3.5 3.5 0 0 1-1.639 0"/></svg>`
} }
isActive() { getValue(editor) {
return false return <AIDigitalHumanModal key={Date.now()} editor={editor}></AIDigitalHumanModal>
} }
isDisabled() { }
return true
} export default {
exec() { key: 'AIDigitalHuman',
return factory() {
} return new AIDigitalHuman()
} },
}
export default {
key: 'AIDigitalHuman',
factory() {
return new AIDigitalHuman()
}
}
import { useState, useRef, useEffect } from 'react'
import { Modal, Input, Button, message, Spin, Row, Col, Space, Divider, Checkbox, Tabs, Empty } from 'antd'
import {
VideoCameraOutlined,
CheckOutlined,
DownloadOutlined,
PlayCircleOutlined,
PauseCircleOutlined,
UserOutlined,
} from '@ant-design/icons'
import { SlateTransforms } from '@wangeditor/editor'
import { saveAs } from 'file-saver'
import { uploadFileByUrl } from '@/utils/oss'
import useDigitalHuman from '@/hooks/useDigitalHuman'
import './AIDigitalHumanModal.less'
const { TextArea } = Input
export default function AIDigitalHumanModal(props) {
const { editor } = props
const [isModalOpen, setIsModalOpen] = useState(true)
// 核心选择状态
const [selectedFigure, setSelectedFigure] = useState(null)
const [selectedVoice, setSelectedVoice] = useState(null)
// 已移除字幕、透明背景和背景图片 URL 相关配置
const [textValue, setTextValue] = useState('')
const [playingVoiceId, setPlayingVoiceId] = useState(null)
const { figures, femaleVoices, maleVoices, loadingFigures, isGenerating, generatedVideo, generateVideo } =
useDigitalHuman()
// 初始化默认选择
useEffect(() => {
if (!selectedFigure && figures.length > 0) {
setSelectedFigure(figures[0])
}
if (!selectedVoice && femaleVoices.length > 0) {
setSelectedVoice(femaleVoices[0])
}
}, [figures, femaleVoices, selectedFigure, selectedVoice])
const handleVoicePlay = (voice) => {
if (playingVoiceId === voice.id) {
audioRef.current.pause()
setPlayingVoiceId(null)
} else {
audioRef.current.src = voice.previewUrl
audioRef.current.play()
setPlayingVoiceId(voice.id)
audioRef.current.onended = () => setPlayingVoiceId(null)
}
}
const handleGenerate = async () => {
if (!selectedFigure) return message.warning('请选择数字人形象')
if (!textValue.trim()) return message.warning('请输入播报文本')
if (!selectedVoice) return message.warning('请选择音色')
try {
await generateVideo({
figureId: selectedFigure.id,
figureName: selectedFigure.name,
driveType: 'TEXT',
text: textValue.trim(),
voiceId: selectedVoice?.id,
width: 720,
height: 1280,
})
} catch (error) {}
}
const handleInsert = async () => {
if (!generatedVideo) return
try {
message.loading({ content: '处理中...', key: 'inserting' })
const ossUrl = await uploadFileByUrl(generatedVideo.url)
if (editor) {
editor.restoreSelection()
const nodes = [
{ type: 'video', src: ossUrl, children: [{ text: '' }] },
{
type: 'paragraph',
textAlign: 'center',
fontSize: '14px',
children: [{ text: `${generatedVideo.figureName} - AI播报` }],
},
]
SlateTransforms.insertNodes(editor, nodes)
message.success({ content: '已插入文章', key: 'inserting' })
setIsModalOpen(false)
}
} catch (error) {
message.error({ content: '插入失败', key: 'inserting' })
}
}
return (
<Modal
title="数字人 AI 视频生成"
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
footer={null}
width={1000}
centered
destroyOnClose>
<div className="ai-digital-human">
<Row gutter={[24, 16]}>
<Col span={24}>
{/* 形象选择 */}
<div className="section-box">
<div className="section-title">1. 选择数字人形象</div>
{loadingFigures && figures.length === 0 ? (
<div style={{ textAlign: 'center', padding: '20px' }}>
<Spin tip="同步云端形象中..." />
</div>
) : figures.length > 0 ? (
<div className="figure-list">
{figures.map((fig) => (
<div
key={fig.id}
className={`figure-item ${selectedFigure?.id === fig.id ? 'active' : ''}`}
onClick={() => setSelectedFigure(fig)}>
<div className="avatar-wrapper">
{fig.avatar ? (
<img
src={fig.avatar}
className="figure-avatar"
onError={(e) => {
e.target.style.display = 'none'
e.target.nextSibling.style.display = 'flex'
}}
/>
) : null}
<div className="avatar-fallback" style={{ display: fig.avatar ? 'none' : 'flex' }}>
<UserOutlined />
</div>
</div>
<div className="figure-name">{fig.name}</div>
</div>
))}
</div>
) : (
<Empty description="暂无可用形象" />
)}
</div>
{/* 驱动配置 */}
<div className="section-box">
<div className="section-title">2. 配置播报内容与音色</div>
<div className="drive-content">
<TextArea
placeholder="请输入要播报的文本内容..."
rows={4}
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
style={{ marginBottom: 12 }}
/>
<div className="voice-selection">
<div className="sub-title">精选配音:</div>
<Tabs
size="small"
items={[
{
key: 'female',
label: '女声',
children: (
<div className="voice-grid">
{femaleVoices.map((v) => (
<div
key={v.id}
className={`voice-item ${selectedVoice?.id === v.id ? 'active' : ''}`}
onClick={() => setSelectedVoice(v)}>
<div className="v-info">
<span className="v-name">{v.name}</span>
<span className="v-style">{v.style}</span>
</div>
<Button
type="text"
shape="circle"
icon={playingVoiceId === v.id ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
onClick={(e) => {
e.stopPropagation()
handleVoicePlay(v)
}}
/>
</div>
))}
</div>
),
},
{
key: 'male',
label: '男声',
children: (
<div className="voice-grid">
{maleVoices.map((v) => (
<div
key={v.id}
className={`voice-item ${selectedVoice?.id === v.id ? 'active' : ''}`}
onClick={() => setSelectedVoice(v)}>
<div className="v-info">
<span className="v-name">{v.name}</span>
<span className="v-style">{v.style}</span>
</div>
<Button
type="text"
shape="circle"
icon={playingVoiceId === v.id ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
onClick={(e) => {
e.stopPropagation()
handleVoicePlay(v)
}}
/>
</div>
))}
</div>
),
},
]}
/>
</div>
</div>
{/* 已彻底移除背景图片 URL 相关输入 */}
</div>
</Col>
<Col span={24}>
<div className="preview-panel">
<div className="section-title">生成状态</div>
<div className="results-container">
{isGenerating ? (
<div className="generating-box">
<Spin size="large" />
<div className="gen-text">视频正在录制中...</div>
<div className="gen-hint">预计 1 分钟左右完成</div>
</div>
) : generatedVideo ? (
<div className="final-video-box">
<video src={generatedVideo.url} controls autoPlay />
<div className="video-actions">
<Button
block
size="large"
icon={<DownloadOutlined />}
onClick={() => saveAs(generatedVideo.url, 'ai_video.mp4')}>
下载视频
</Button>
<Button block type="primary" size="large" icon={<CheckOutlined />} onClick={handleInsert}>
插入编辑器
</Button>
</div>
</div>
) : (
<div className="empty-preview">
<VideoCameraOutlined style={{ fontSize: 64, opacity: 0.1 }} />
<p>等待配置完成后点击开始</p>
</div>
)}
</div>
<Divider style={{ margin: '12px 0' }} />
<Button
type="primary"
size="large"
block
icon={<VideoCameraOutlined />}
onClick={handleGenerate}
loading={isGenerating}
className="submit-btn">
开始生成 AI 视频
</Button>
</div>
</Col>
</Row>
</div>
</Modal>
)
}
.ai-digital-human {
.section-box {
margin-bottom: 14px;
padding: 12px;
background: #fff;
border-radius: 8px;
border: 1px solid #f0f0f0;
.section-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 10px;
border-left: 4px solid #ab1941;
padding-left: 10px;
}
}
.figure-list {
display: flex;
overflow-x: auto;
gap: 12px;
padding: 6px 0;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #e8e8e8;
border-radius: 3px;
}
.figure-item {
flex: 0 0 92px;
cursor: pointer;
text-align: center;
padding: 8px;
border: 2px solid transparent;
border-radius: 12px;
transition: all 0.3s;
&.active {
border-color: #ab1941;
background: rgba(171, 25, 65, 0.08);
}
.avatar-wrapper {
width: 72px;
height: 72px;
margin: 0 auto 6px;
position: relative;
.figure-avatar {
width: 72px;
height: 72px;
border-radius: 50%;
object-fit: cover;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background: #f0f0f0;
}
.avatar-fallback {
width: 72px;
height: 72px;
border-radius: 50%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: #bfbfbf;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
.figure-name {
font-size: 13px;
color: #555;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.voice-selection {
.sub-title {
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
color: #666;
}
.voice-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 6px;
max-height: 176px;
overflow-y: auto;
padding-right: 4px;
}
.voice-item {
display: flex;
align-items: center;
padding: 6px 10px;
border: 1px solid #e8e8e8;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
&.active {
border-color: #ab1941;
background: rgba(171, 25, 65, 0.08);
}
&:hover {
background: #fafafa;
}
.v-info {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
.v-name {
font-size: 13px;
font-weight: 500;
}
.v-style {
font-size: 11px;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.upload-status {
margin-top: 10px;
color: #52c41a;
font-size: 13px;
}
.preview-panel {
.results-container {
min-height: 320px;
background: #f7f8fa;
border-radius: 12px;
border: 2px dashed #e1e4e8;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
.empty-preview {
text-align: center;
color: #abb2bb;
p {
font-size: 14px;
margin-top: 16px;
}
}
.generating-box {
text-align: center;
.gen-text {
font-weight: 600;
margin: 16px 0 8px;
color: #333;
}
.gen-hint {
font-size: 13px;
color: #999;
}
}
.final-video-box {
width: 100%;
video {
width: 100%;
border-radius: 8px;
background: #000;
max-height: 300px;
}
.video-actions {
margin-top: 14px;
display: flex;
flex-direction: column;
gap: 10px;
}
}
}
.submit-btn {
height: 44px;
font-size: 15px;
font-weight: 600;
border-radius: 8px;
margin-top: 16px;
}
}
}
...@@ -80,8 +80,8 @@ export default function IconModal(props) { ...@@ -80,8 +80,8 @@ export default function IconModal(props) {
const iconNode = { const iconNode = {
type: 'image', type: 'image',
src: selectedIcon.url, src: selectedIcon.url,
alt: 'icon', alt: 'icon-inline',
style: { width: '32px', height: '32px' }, style: { width: '32px', height: '32px', verticalAlign: 'middle' },
width: '32', // 双重保障,部分渲染器读这个 width: '32', // 双重保障,部分渲染器读这个
height: '32', height: '32',
children: [{ text: '' }], children: [{ text: '' }],
......
export const digitalHumans = [
{
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',
},
]
export const femaleVoices = [
{
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: 'CAP_6567',
name: '度小柔',
gender: '女声',
style: '知性大方',
previewUrl: 'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/d00df619-70d5-458b-98e2-4d0e14595ace/6567.wav',
},
{
id: 'CAP_4189',
name: '度涵竹',
gender: '女声',
style: '自然生动',
previewUrl: 'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/c3414454-fe66-4980-b653-b806da9616ed/4189.wav',
},
{
id: 'CAP_4194',
name: '度嫣然',
gender: '女声',
style: '温柔可爱',
previewUrl: 'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/503224a8-98f3-4703-b953-795725546686/4194.wav',
},
{
id: 'CAP_4196',
name: '度清影',
gender: '女声',
style: '甜美可爱',
previewUrl: 'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/d5e13abb-7a7e-4264-896c-4f9022fb6b78/4196.wav',
},
{
id: 'CAP_4197',
name: '度沁遥',
gender: '女声',
style: '温柔知性',
previewUrl: 'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/e7e7274e-02df-4caa-9741-7d73b25d8ddd/4197.wav',
},
{
id: '5132',
name: '度小夏',
gender: '女声',
style: '知性大方',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/075ca6ce61d49629f62c520734b9e70e.wav',
},
{
id: '4100',
name: '度小雯',
gender: '女声',
style: '元气活力',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/b444cd975c8e5458ab0ab49644514a1a.wav',
},
{
id: '5116',
name: '度小希',
gender: '女声',
style: '元气活力',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/7015792b10a16e60753918c06869da2b.wav',
},
{
id: '5147',
name: '度常盈',
gender: '女声',
style: '亲和力强',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/1e6368f45761d7d65724bfc109cc5d4d.wav',
},
]
export const maleVoices = [
{
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: 'CAP_4179',
name: '度泽言-温暖',
gender: '男声',
style: '温柔青年',
previewUrl: 'https://meta-human-editor-prd.cdn.bcebos.com/1a71e60c-bbe0-482b-81fb-4889524acbc3/ed2cc48e-db88-4a4d-91ba-e7f86935df03/4179.wav',
},
{
id: '4140',
name: '度小新',
gender: '男声',
style: '元气活力',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/a6bf500f7156b7e762ee2760288de98c.wav',
},
{
id: '5135',
name: '度星河',
gender: '男声',
style: '沉稳冷静',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/337482d4a3299ac70a4d01283ce9de9f.wav',
},
{
id: '4123',
name: '度小凯',
gender: '男声',
style: '激情饱满',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/17c68fd8042a35b667cd6a92ceef4dbf.wav',
},
{
id: '4003',
name: '度逍遥',
gender: '男声',
style: '权威靠谱/专业娴熟',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/f26b3d0c97543381f7a2ec3508eb96e8.wav',
},
{
id: '4129',
name: '度小彦',
gender: '男声',
style: '元气活力',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/f7e067b169ff8262f91ff69aa64a6be3.wav',
},
{
id: '4115',
name: '度小贤',
gender: '男声',
style: '权威靠谱/沉稳冷静',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/31aebec53b10a474019ba63e687c21f8.wav',
},
{
id: '4106',
name: '度博文',
gender: '男声',
style: '沉稳冷静',
previewUrl: 'https://digital-human-pipeline-output.cdn.bcebos.com/e192515285df0423911b14ef916cafbb.wav',
},
]
import { useState, useEffect, useRef, useCallback } from 'react'
import { message } from 'antd'
import { getDigitalHumanList, submitVideo, getVideoTask } from '@/api/xiling'
import { digitalHumans as staticFigures, femaleVoices, maleVoices } from '@/common/wangeditor-customer/menu/common/digital-human-data'
/**
* 数字人功能 Hook (简化版:仅文本驱动,移除底图)
*/
export default function useDigitalHuman() {
const [figures, setFigures] = useState(staticFigures)
const [loadingFigures, setLoadingFigures] = useState(false)
const [isGenerating, setIsGenerating] = useState(false)
const [generatedVideo, setGeneratedVideo] = useState(null)
const [error, setError] = useState(null)
const pollTimerRef = useRef(null)
const stopPolling = useCallback(() => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current)
pollTimerRef.current = null
}
}, [])
const startPolling = useCallback((taskId, figureName) => {
stopPolling()
pollTimerRef.current = setInterval(async () => {
try {
const response = await getVideoTask({ taskId })
const { status, videoUrl } = response.result
if (status === 'SUCCESS' && videoUrl) {
stopPolling()
setGeneratedVideo({
url: videoUrl,
taskId: taskId,
figureName: figureName,
})
setIsGenerating(false)
message.success('数字人视频生成成功!')
} else if (status === 'FAILED') {
stopPolling()
setIsGenerating(false)
setError(response.result.failedMessage || '生成失败')
message.error(`视频生成失败: ${response.result.failedMessage || '未知错误'}`)
}
} catch (err) {
console.error('Polling Error:', err)
}
}, 5000)
}, [stopPolling])
// 处理获取列表
const fetchFigures = useCallback(async () => {
setLoadingFigures(true)
try {
const list = await getDigitalHumanList({ systemFigure: true })
if (list && list.length > 0) {
setFigures(prev => {
// 创建新数组,保留静态配置,并用 API 的数据补充或更新关键信息(如 avatar)
const combined = [...prev]
list.forEach(item => {
const existingIndex = combined.findIndex(c => String(c.id) === String(item.figureId))
const avatarUrl = item.figureImageUrl || item.figureVideoThumbnailUrl
if (existingIndex > -1) {
// 如果静态配置已存在,且 API 返回了有效的预览图,则更新它
if (avatarUrl) {
combined[existingIndex] = {
...combined[existingIndex],
avatar: avatarUrl
}
}
} else {
// 如果是全新的数字人,则添加
combined.push({
id: item.figureId,
name: item.name,
avatar: avatarUrl,
})
}
})
return combined
})
}
} catch (err) {
console.error('Fetch Figures Error:', err)
} finally {
setLoadingFigures(false)
}
}, [])
const generateVideo = useCallback(async (options) => {
const {
figureId,
figureName,
text,
voiceId,
width = 720,
height = 1280,
transparent = true,
enableSubtitle = false,
backgroundImageUrl
} = options
setIsGenerating(true)
setGeneratedVideo(null)
setError(null)
try {
const params = {
figureId,
driveType: 'TEXT',
text,
...(backgroundImageUrl ? { backgroundImageUrl } : {}),
ttsParams: {
person: voiceId || '5116',
speed: '5',
pitch: '5',
volume: '5'
},
videoParams: {
width: parseInt(width),
height: parseInt(height),
transparent: !!transparent
},
subtitleParams: {
enabled: !!enableSubtitle,
subtitlePolicy: 'SRT'
}
}
const response = await submitVideo(params)
if (response.result && response.result.taskId) {
startPolling(response.result.taskId, figureName)
return response.result.taskId
} else {
throw new Error('TaskId missing')
}
} catch (err) {
setIsGenerating(false)
const msg = err.response?.data?.message?.global || '提交失败'
setError(msg)
message.error(msg)
throw err
}
}, [startPolling])
useEffect(() => {
fetchFigures()
return () => stopPolling()
}, [fetchFigures, stopPolling])
return {
figures,
femaleVoices,
maleVoices,
loadingFigures,
isGenerating,
generatedVideo,
error,
generateVideo,
resetGeneratedVideo: () => setGeneratedVideo(null)
}
}
...@@ -77,7 +77,7 @@ export async function uploadFile(file) { ...@@ -77,7 +77,7 @@ export async function uploadFile(file) {
// 上传通过URL获取的文件 // 上传通过URL获取的文件
export async function uploadFileByUrl(url) { export async function uploadFileByUrl(url) {
try { try {
url = url.replace('https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com', '/api/ai_file') url = url.replace('https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com', '/api/volcano_file')
const res = await axios.get(url, { responseType: 'blob' }) const res = await axios.get(url, { responseType: 'blob' })
return await uploadFile(res.data) return await uploadFile(res.data)
} catch (error) { } catch (error) {
......
...@@ -52,10 +52,10 @@ export default defineConfig(() => { ...@@ -52,10 +52,10 @@ export default defineConfig(() => {
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/xiling/, ''), rewrite: (path) => path.replace(/^\/api\/xiling/, ''),
}, },
'/api/ai_file': { '/api/volcano_file': {
target: 'https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com', target: 'https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/ai_file/, ''), rewrite: (path) => path.replace(/^\/api\/volcano_file/, ''),
}, },
'/api': { '/api': {
target: 'https://zijingebook.ezijing.com', target: 'https://zijingebook.ezijing.com',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论