Browse Source

萤石 ifream 播放组件

dev
巴林闲侠 2 years ago
parent
commit
c7ffd6a730
  1. 2
      api/.vscode/launch.json
  2. 59
      api/app/lib/controllers/data/videoCenter.js
  3. 5
      api/app/lib/index.js
  4. 50
      api/app/lib/middlewares/business-rest.js
  5. 6
      api/app/lib/routes/data/index.js
  6. 67
      api/app/lib/service/paasRequest.js
  7. 20
      api/config.js
  8. 4
      web/client/src/components/index.js
  9. 52
      web/client/src/components/ysPlayerIframe.js
  10. 4
      web/client/src/layout/actions/global.js
  11. 15
      web/client/src/layout/reducers/global.js
  12. 3
      web/client/src/sections/fillion/containers/index.js
  13. 30
      web/client/src/sections/fillion/containers/videoCenter.js
  14. 3
      web/client/src/sections/fillion/nav-item.js
  15. 9
      web/client/src/sections/fillion/routes.js
  16. 11
      web/client/src/sections/quanju/actions/example.js
  17. 17
      web/client/src/sections/quanju/containers/footer/leadership/index.js
  18. 47
      web/client/src/sections/quanju/containers/footer/leadership/left/left-center.js
  19. 3
      web/client/src/utils/webapi.js
  20. 13
      web/config.js
  21. 2
      web/package.json
  22. 4
      web/routes/attachment/index.js

2
api/.vscode/launch.json

@ -22,6 +22,8 @@
"--qnbkt dev-highways4good",
// "--qndmn http://resources.anxinyun.cn",
"--qndmn http://rfkimpwbb.hn-bkt.clouddn.com",
"--yingshiKey d0704fb9d5d14a6682c1c1d592c12512",
"--yingshiSecret 93d023269495b86be62cdfdcf34a6cd1"
]
},
{

59
api/app/lib/controllers/data/videoCenter.js

@ -0,0 +1,59 @@
'use strict';
const moment = require('moment')
const request = require('superagent')
function videoList (opts) {
return async function (ctx) {
try {
const { models, } = ctx.fs.dc;
const { app, yingshiTokenRes } = ctx
let yingshiToken = ''
if (yingshiTokenRes && yingshiTokenRes.token && yingshiTokenRes.expire && moment().isBefore(moment(yingshiTokenRes.expire))) {
yingshiToken = yingshiTokenRes.token
} else {
const tokenRes = await app.fs.yingshiRequest.post(`lapp/token/get`, {
query: {
appKey: opts.yingshiKey,
appSecret: opts.yingshiSecret
}
})
if (tokenRes.code == 200 && tokenRes.data) {
const { accessToken, expireTime } = tokenRes.data
ctx.yingshiTokenRes = {
token: accessToken,
expire: expireTime
}
yingshiToken = accessToken
} else {
throw '未能获取进行萤石鉴权'
}
}
const deviceRes = await app.fs.yingshiRequest.post(`lapp/device/list`, {
query: {
accessToken: yingshiToken,
}
})
ctx.status = 200;
ctx.body = (deviceRes.data || []).map(item => {
return {
...item,
token: yingshiToken,
}
})
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: error`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
}
module.exports = {
videoList
};

5
api/app/lib/index.js

@ -5,7 +5,7 @@ const fs = require('fs');
const path = require('path');
const authenticator = require('./middlewares/authenticator');
// const apiLog = require('./middlewares/api-log');
// const businessRest = require('./middlewares/business-rest');
const paasRequest = require('./service/paasRequest');
module.exports.entry = function (app, router, opts) {
app.fs.logger.log('info', '[FS-AUTH]', 'Inject auth and api mv into router.');
@ -15,8 +15,9 @@ module.exports.entry = function (app, router, opts) {
app.fs.api.logAttr = app.fs.api.logAttr || {};
router.use(authenticator(app, opts));
// router.use(businessRest(app, router, opts));
// router.use(apiLog(app, opts));
// 实例其他平台请求方法
paasRequest(app, opts)
router = routes(app, router, opts);
};

50
api/app/lib/middlewares/business-rest.js

@ -1,50 +0,0 @@
'use strict';
const request = require('superagent');
const buildUrl = (url,token) => {
let connector = url.indexOf('?') === -1 ? '?' : '&';
return `${url}${connector}token=${token}`;
};
function factory(app, router, opts) {
return async function (ctx, next) {
const token = ctx.fs.api.token;
//console.log(username,password)
const req = {
get: (url, query) => {
return request
.get(buildUrl(url,token))
.query(query)
},
post: (url, data, query) => {
return request
.post(buildUrl(url,token))
.query(query)
//.set('Content-Type', 'application/json')
.send(data);
},
put: (url, data) => {
return request
.put(buildUrl(url,token))
//.set('Content-Type', 'application/json')
.send(data);
},
delete: (url) => {
return request
.del(buildUrl(url,token))
},
};
app.business = app.business || {};
app.business.request = req;
await next();
};
}
module.exports = factory;

6
api/app/lib/routes/data/index.js

@ -10,6 +10,7 @@ const publicity = require('../../controllers/data/publicity');
const dataIndex = require('../../controllers/data/index');
const task = require('../../controllers/data/task')
const assess = require('../../controllers/data/assess')
const videoCenter = require('../../controllers/data/videoCenter')
module.exports = function (app, router, opts) {
@ -196,4 +197,9 @@ module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/assess/nearest'] = { content: '获取评分考核最近的月份的所有考核单位的数据', visible: true };
router.get('/assess/nearest', assess.nearestSourceData);
// 评分考核 END
// 视频中心
app.fs.api.logAttr['GET/videoCenter/list'] = { content: '获取萤石设备列表', visible: true };
router.get('/videoCenter/list', videoCenter.videoList(opts));
// 视频中心 END
};

67
api/app/lib/service/paasRequest.js

@ -0,0 +1,67 @@
'use strict';
const request = require('superagent')
class paasRequest {
constructor(root, { query = {} } = {}, option) {
this.root = root;
this.query = query
this.option = option
}
#buildUrl = (url) => {
return `${this.root}/${url}`;
}
#resultHandler = (resolve, reject) => {
return (err, res) => {
if (err) {
reject(err);
} else {
resolve(res[this.option.dataWord]);
}
};
}
get = (url, { query = {}, header = {} } = {}) => {
return new Promise((resolve, reject) => {
request.get(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).end(this.#resultHandler(resolve, reject));
})
}
post = (url, { data = {}, query = {}, header = {} } = {}) => {
return new Promise((resolve, reject) => {
request.post(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).send(data).end(this.#resultHandler(resolve, reject));
})
}
put = (url, { data = {}, header = {}, query = {}, } = {}) => {
return new Promise((resolve, reject) => {
request.put(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).send(data).end(this.#resultHandler(resolve, reject));
})
}
delete = (url, { header = {}, query = {} } = {}) => {
return new Promise((resolve, reject) => {
request.delete(this.#buildUrl(url)).set(header).query(Object.assign(query, this.query)).end(this.#resultHandler(resolve, reject));
})
}
}
function factory (app, opts) {
if (opts.pssaRequest) {
try {
for (let r of opts.pssaRequest) {
if (r.name && r.root) {
app.fs[r.name] = new paasRequest(r.root, { ...(r.params || {}) }, { dataWord: r.dataWord || 'body' })
} else {
throw 'opts.pssaRequest 参数错误!'
}
}
} catch (error) {
console.error(error)
process.exit(-1);
}
}
}
module.exports = factory;

20
api/config.js

@ -17,6 +17,8 @@ args.option('qnsk', 'qiniuSecretKey');
args.option('qnbkt', 'qiniuBucket');
args.option('qndmn', 'qiniuDomain');
//
args.option('yingshiKey', '萤石 KEY')
args.option('yingshiSecret', '萤石 SECRET')
const flags = args.parse(process.argv);
@ -27,8 +29,15 @@ const QINIU_DOMAIN_QNDMN_RESOURCE = process.env.ANXINCLOUD_QINIU_DOMAIN_QNDMN_RE
const QINIU_BUCKET_RESOURCE = process.env.ANXINCLOUD_QINIU_BUCKET_RESOURCE || flags.qnbkt;
const QINIU_AK = process.env.ANXINCLOUD_QINIU_ACCESSKEY || flags.qnak;
const QINIU_SK = process.env.ANXINCLOUD_QINIU_SECRETKEY || flags.qnsk;
//
const YINGSHI_KEY = process.env.YINGSHI_KEY || flags.yingshiKey;
const YINGSHI_SECRET = process.env.YINGSHI_SECRET || flags.yingshiSecret;
// 萤石服务的地址
const YINGSHI_URL = process.env.YINGSHI_URL || flags.yingshiUrl || 'https://open.ys7.com/api';
if (!FS_UNIAPP_DB) {
if (
!FS_UNIAPP_DB ||
!YINGSHI_KEY || !YINGSHI_SECRET) {
console.log('缺少启动参数,异常退出');
args.showHelp();
process.exit(-1);
@ -57,7 +66,16 @@ const product = {
}, {
entry: require('./app').entry,
opts: {
yingshiKey: YINGSHI_KEY,
yingshiSecret: YINGSHI_SECRET,
exclude: [], // 不做认证的路由,也可以使用 exclude: ["*"] 跳过所有路由
pssaRequest: [
{
name: 'yingshiRequest',
root: YINGSHI_URL,
params: {}
}
]
}
}
],

4
web/client/src/components/index.js

@ -10,6 +10,7 @@ import Table from './table'
import Search from './search'
import SketchColor from './sketchColor'
import SimpleFileDownButton from './simpleFileDownButton'
import YSIframePlayer from './ysPlayerIframe'
export {
Upload,
@ -20,5 +21,6 @@ export {
Table,
Search,
SketchColor,
SimpleFileDownButton
SimpleFileDownButton,
YSIframePlayer
};

52
web/client/src/components/ysPlayerIframe.js

@ -0,0 +1,52 @@
/**
* 萤石视频直播基于萤石云iframe模式使用方式简单
* 官方参考https://open.ys7.com/console/ezopenIframe.html
*/
'use strict';
import React from 'react';
import { connect } from 'react-redux';
const YSIframePlayer = props => {
const { containerId, height, width, url, autoplay, audio, videoState, ysToken } = props;
const at = ysToken
if (!url || !at) return null;
const src = `https://open.ys7.com/ezopen/h5/iframe?audio=${audio ? '1' : '0'}&url=${url}&autoplay=${autoplay || 1}&accessToken=${at}`
// const src = `https://open.ys7.com/ezopen/h5/iframe?audio=1&url=${url}&autoplay=${autoplay || 1}&accessToken=${at}`
return (
<div
style={{ position: 'relative', height: '100%', width: '100%' }}>
<iframe
frameBorder="0"
id={containerId || 'myPlayer'}
src={src}
// https://open.ys7.com/doc/zh/book/index/live_proto.html 单个播放器的长宽比例限制最小为{width: 400px;height: 300px;}
width={width || 400}
height={height || 300}
allowFullScreen
wmode="transparent"
>
</iframe>
{
videoState && videoState.status == 0 ?
<div style={{
height: width || 300, width: width || 400, position: 'absolute', top: 0, background: '#000',
display: 'flex', justifyContent: 'center', alignItems: 'center', color: '#fff'
}}>
设备中断正在处理中...
</div>
: ''
}
</div>
)
}
function mapStateToProps (state) {
const { auth, } = state;
return {
user: auth.user,
};
}
export default connect(mapStateToProps)(YSIframePlayer);

4
web/client/src/layout/actions/global.js

@ -35,7 +35,9 @@ export function initApiRoot () {
dispatch({
type: INIT_API_ROOT,
payload: {
apiRoot: res.root
apiRoot: res.root,
vcmpWebUrl: res.vcmpWebUrl,
vcmpMirrorId: res.vcmpMirrorId,
}
})
});

15
web/client/src/layout/reducers/global.js

@ -1,15 +1,18 @@
'use strict';
import Immutable from 'immutable';
import { INIT_LAYOUT, RESIZE } from '../actions/global';
import { INIT_LAYOUT, RESIZE, INIT_API_ROOT } from '../actions/global';
function global(state = {
function global (state = {
title: '',
copyright: '',
sections: [],
actions: {},
plugins: {},
clientHeight: 768,
clientWidth: 1024
clientWidth: 1024,
apiRoot: '',
vcmpWebUrl: '',
vcmpMirrorId: '',
}, action) {
const payload = action.payload;
switch (action.type) {
@ -34,6 +37,12 @@ function global(state = {
// return Immutable.fromJS(state).merge({
// detailsComponent: payload.component
// }).toJS();
case INIT_API_ROOT:
return Immutable.fromJS(state).merge({
apiRoot: payload.apiRoot,
vcmpWebUrl: payload.vcmpWebUrl,
vcmpMirrorId: payload.vcmpMirrorId,
}).toJS();
default:
return state;
}

3
web/client/src/sections/fillion/containers/index.js

@ -15,4 +15,5 @@ import File from './file';
import Jiekouguanli from './jiekouguanli';
import Task from './task'
import Assess from './assess'
export { Infor, transportation, BridgeTable, HigHways, OperaTional, Enforce, Public, Videois, PromoTional, Maintenance, Patrol, File, Jiekouguanli, Task, Assess };
import VideoCenter from './videoCenter';
export { Infor, transportation, BridgeTable, HigHways, OperaTional, Enforce, Public, Videois, PromoTional, Maintenance, Patrol, File, Jiekouguanli, Task, Assess, VideoCenter };

30
web/client/src/sections/fillion/containers/videoCenter.js

@ -0,0 +1,30 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { getAssess, delAssess, editAssess } from '../actions/assess';
import ProTable from '@ant-design/pro-table';
import AssessModal from '../components/assessModal';
import { Form, Space, DatePicker, Button, Select, Popconfirm } from 'antd'
import moment from 'moment';
function VideoCenter (props) {
const { dispatch, vcmpWebUrl, vcmpMirrorId } = props;
useEffect(() => {
return () => { };
}, []);
return (
<div>
<iframe src={`${vcmpWebUrl}/callService?mid=${vcmpMirrorId}`} style={{ height: 'calc(100vh - 142px)', width: '100%', display: 'block' }} frameBorder={0}></iframe>
</div>
);
}
function mapStateToProps (state) {
const { auth, global } = state
return {
user: auth.user,
vcmpWebUrl: global.vcmpWebUrl,
vcmpMirrorId: global.vcmpMirrorId,
}
}
export default connect(mapStateToProps)(VideoCenter);

3
web/client/src/sections/fillion/nav-item.js

@ -88,6 +88,9 @@ export function getNavItem (user, dispatch) {
<Menu.Item key="fillionassess">
<Link to="/fillion/assess">考核评分</Link>
</Menu.Item>
<Menu.Item key="fillionvideoCenter">
<Link to="/fillion/videoCenter">视频中心</Link>
</Menu.Item>
</SubMenu> : null
);
}

9
web/client/src/sections/fillion/routes.js

@ -12,7 +12,7 @@ import { Maintenance } from './containers'
import { Patrol } from './containers'
import { File } from './containers';
import { Jiekouguanli } from './containers'
import { Task, Assess } from './containers'
import { Task, Assess, VideoCenter } from './containers'
export default [{
type: 'inner',
@ -153,7 +153,12 @@ export default [{
menuSelectKeys: ['fillionassess'],
component: Assess,
breadcrumb: '考核评分',
}, {
path: '/videoCenter',
key: 'fillionvideoCenter',
menuSelectKeys: ['fillionvideoCenter'],
component: VideoCenter,
breadcrumb: '视频中心',
}
]
}

11
web/client/src/sections/quanju/actions/example.js

@ -168,3 +168,14 @@ export function getNearestAssessData (query = {}) {
msg: { error: '获取各乡镇考核得分情况失败' },
});
}
export function getVideoCenterList () {
return dispatch => basicAction({
type: 'get',
dispatch: dispatch,
actionType: 'GET_VIDEO_CENTER_LIST',
url: ApiTable.videoCenterList,
msg: { error: '获取视频中心列表失败' },
reducer: { name: 'videoCenterList' }
});
}

17
web/client/src/sections/quanju/containers/footer/leadership/index.js

@ -1,19 +1,30 @@
import React from 'react'
import React, { useEffect, useState } from 'react'
import Left from './left'
import Right from './right'
import CenterLeft from "./centerLeft"
import Centerright from "./centerRight"
import { connect } from 'react-redux'
import { getNearestAssessData, getVideoCenterList } from "../../../actions/example"
const Leadership = (props) => {
const { dispatch } = props
useEffect(() => {
dispatch(getVideoCenterList())
}, [])
return (
<>
<Left dispatch={dispatch} />
<CenterLeft dispatch={dispatch} />
<Right dispatch={dispatch} />
{/* <Centerright /> */}
</>
)
}
export default Leadership
function mapStateToProps (state) {
return {
}
}
export default connect(mapStateToProps)(Leadership)

47
web/client/src/sections/quanju/containers/footer/leadership/left/left-center.js

@ -1,22 +1,32 @@
import React, { useEffect, useState } from 'react'
import Module from '../../../public/module'
import Lunbo from "../right/lunbo"
import { connect } from 'react-redux'
// import "./left.less"
const Leftcenter = () => {
const Leftcenter = ({ videoCenterList }) => {
console.log(videoCenterList);
const style = { height: "30%", marginTop: "5%" }
// const hualun = "auto"
const [num, setNum] = useState(1);
const [tu, setTu] = useState("");
const [name, setName] = useState("");
const [list, setList] = useState([
{ name: '沙潭至五星', img: "/assets/images/leadership/fake/1.jpg" },
{ name: '滁槎至协城', img: "/assets/images/leadership/fake/2.jpg" },
{ name: '瓜山至广福', img: "/assets/images/leadership/fake/3.jpg" },
{ name: '罗舍至泗洪', img: "/assets/images/leadership/fake/4.jpg" },
{ name: '渔业至万州', img: "/assets/images/leadership/fake/5.jpg" },
// { name: '沙潭至五星', img: "/assets/images/leadership/fake/1.jpg" },
// { name: '滁槎至协城', img: "/assets/images/leadership/fake/2.jpg" },
// { name: '瓜山至广福', img: "/assets/images/leadership/fake/3.jpg" },
// { name: '罗舍至泗洪', img: "/assets/images/leadership/fake/4.jpg" },
// { name: '渔业至万州', img: "/assets/images/leadership/fake/5.jpg" },
// { name: '小蓝至东新', img: "/assets/images/leadership/shiyantu.png" },
])
useEffect(() => {
if (videoCenterList.length) {
setList(videoCenterList.slice(0, 5))
}
}, [videoCenterList])
useEffect(() => {
const timer = setInterval(() => {
if (num == list.length) {
@ -26,9 +36,10 @@ const Leftcenter = () => {
setNum(num + 1);
setTu(list[num].img);
}
}, 2000);
}, 2000 * 10);
return () => clearInterval(timer);
}, [num]);
const renderBody = () => {
return (
<div style={{ width: "100%", height: "55%" }}>{
@ -39,10 +50,10 @@ const Leftcenter = () => {
<li style={{ height: "20px", position: "relative", width: "100%", marginTop: index == 0 ? "4px" : "5px", listStyle: "none", borderLeft: num - 1 == index ? "2px solid #1C60FE" : "2px solid #113892", backgroundColor: "linear-gradient(to right, rgba(0,70,200,0.3000) , rgba(0,124,230,0))" }} onMouseEnter={() => {
setTu(item.img);
setNum(index + 1);
setName(item.name)
setName(item.deviceName)
// console.log(list);
}}>
<p style={{ position: "absolute", color: num - 1 == index ? "#fff" : "rgba(216,240,255,0.8)", left: "10%" }}>{item.name}</p>
<p style={{ position: "absolute", color: num - 1 == index ? "#fff" : "rgba(216,240,255,0.8)", left: "10%" }}>{item.deviceName}</p>
<img src='/assets/images/leadership/juxing.png' style={{ width: "100%", height: "100%", position: "absolute" }} />
</li>
// {/* </div> */}
@ -69,15 +80,18 @@ const Leftcenter = () => {
return index + 1 == num ?
<div style={{ width: "100%", height: "100%", position: "relative" }}>
<img style={{ width: "100%",height:'100%', position: "absolute", bottom: "5%" }} src={item.img} />
<img style={{ width: "100%", height: '100%', position: "absolute", bottom: "5%" }} src={item.img} />
<p style={{
width: "100%", height: "3vh", position: "absolute", bottom: "3%",
backgroundColor: "rgba(0,0,0,0.26)", lineHeight: "3vh", textAlign: ""
}}>
<img src='/assets/images/leadership/weizhis.png' style={{ width: "5%", height: "60%", marginLeft: "3%" }} />
<span style={{ marginLeft: "3%", color: "#FFFFFF", fontSize: "12px", fontFamily: "PingFangSC-Regular, PingFang SC", fontWeight: 400 }}>{item.name}</span></p>
</div> : ""
<span style={{ marginLeft: "3%", color: "#FFFFFF", fontSize: "12px", fontFamily: "PingFangSC-Regular, PingFang SC", fontWeight: 400 }}>
{item.deviceName}
</span>
</p>
</div> : ""
})
}
</div>
@ -92,4 +106,11 @@ const Leftcenter = () => {
</>
)
}
export default Leftcenter
function mapStateToProps (state) {
const { videoCenterList } = state
return {
videoCenterList: videoCenterList.data || []
}
}
export default connect(mapStateToProps)(Leftcenter)

3
web/client/src/utils/webapi.js

@ -172,6 +172,9 @@ export const ApiTable = {
delAssess: 'assess/{assessId}',
nearestAssessData:'assess/nearest',
// 视频中心
videoCenterList:'videoCenter/list',
//工程数据
getProject: 'project',
putProject: 'project',

13
web/config.js

@ -18,13 +18,22 @@ args.option(['u', 'api-url'], 'webapi的URL');
args.option('apiUrl', '可外网访问的 webapi 的URL');
args.option(['r', 'report-node'], '报表进程地址');
args.option('qndmn', '七牛');
args.option('vcmpWebUrl', '视频平台web可访问地址');
args.option('vcmpMirrorId', '视频平台镜像服务id')
const flags = args.parse(process.argv);
const FS_UNIAPP_API = process.env.FS_UNIAPP_API || flags.apiUrl;
const QINIU_DOMAIN_QNDMN_RESOURCE = process.env.ANXINCLOUD_QINIU_DOMAIN_QNDMN_RESOURCE || flags.qndmn;
const API_URL = process.env.API_URL || flags.apiUrl;
//
const VCMP_WEB_URL = process.env.VCMP_WEB_URL || flags.vcmpWebUrl;
const VCMP_MIRROR_ID = process.env.VCMP_MIRROR_ID || flags.vcmpMirrorId;
if (!FS_UNIAPP_API) {
if (
!FS_UNIAPP_API ||
!VCMP_WEB_URL || !VCMP_MIRROR_ID
) {
console.log('缺少启动参数,异常退出');
args.showHelp();
process.exit(-1);
@ -56,6 +65,8 @@ const product = {
opts: {
apiUrl: API_URL,
qndmn: QINIU_DOMAIN_QNDMN_RESOURCE,
vcmpWebUrl: VCMP_WEB_URL,
vcmpMirrorId: VCMP_MIRROR_ID,
staticRoot: './client',
}
}, {

2
web/package.json

@ -6,7 +6,7 @@
"scripts": {
"test": "mocha",
"start": "cross-env NODE_ENV=development npm run start-params",
"start-params": "node server -p 5000 -u http://localhost:13400 --qndmn http://rfkimpwbb.hn-bkt.clouddn.com",
"start-params": "node server -p 5000 -u http://localhost:13400 --qndmn http://rfkimpwbb.hn-bkt.clouddn.com --vcmpWebUrl https://mediaconsole.ngaiot.com --vcmpMirrorId 24461524032354",
"deploy": "export NODE_ENV=production&&npm run color && npm run build && node server",
"build-dev": "export NODE_ENV=development&&webpack --config webpack.config.js",
"build": "export NODE_ENV=production&&webpack --config webpack.config.prod.js",

4
web/routes/attachment/index.js

@ -19,9 +19,9 @@ module.exports = {
entry: function (app, router, opts) {
const getApiRoot = async function (ctx) {
try {
const { apiUrl, qndmn } = opts;
const { apiUrl, qndmn, vcmpWebUrl, vcmpMirrorId } = opts;
ctx.status = 200;
ctx.body = { root: apiUrl, qndmn };
ctx.body = { root: apiUrl, qndmn, vcmpWebUrl, vcmpMirrorId };
} catch (error) {
console.error('getApiRoot', error)
}

Loading…
Cancel
Save