提交 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 })
}
差异被折叠。
// 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()
}
}
.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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论