提交 66acce26 authored 作者: vipcxj's avatar vipcxj

1.服务端接口重构,故客户端对应api大改

2.新增路由相关api,位于services/route
上级 1986f61e
<component name="ProjectDictionaryState">
<dictionary name="yaohx_169">
<words>
<w>activex</w>
<w>anticon</w>
<w>arcfour</w>
<w>dropdown</w>
......
const proxy = {
"target": "http://192.168.1.116:1180",
"target": "http://192.168.1.121:8180",
"changeOrigin": true,
"pathRewrite": {
"^/api": "/houseServer/restful-services"
"^/api": "/big-machine/restful-services"
}
};
......@@ -24,7 +24,8 @@ module.exports = {
["import", { "libraryName": "antd", "style": true }]
],
"proxy": {
"/api/user/login2": proxy,
"/api/auth": proxy,
"/api/user": proxy,
"/api/domain": proxy,
"/api/datasource": proxy,
"/api/modules": proxy,
......
......@@ -100,23 +100,26 @@ const dealWithData = (req) => {
let currentDomainId = null;
const wrapResponse = (response) => {
return {
errorCode: 0,
data: response,
};
};
module.exports = {
'/api/user/login2': (req, res) => {
currentUser = users[0];
res.send(currentUser);
},
'/api/user/logout': (req, res) => {
res.status(204).end();
},
'/api/domain/all': domains,
'/api/domain/all': wrapResponse(domains),
'/api/domain/switch': (req, res) => {
const domainId = req.query.domainId;
const intDomainId = parseInt(domainId, 10);
const domainIds = domains.map(domain => domain.id);
if (domainIds.indexOf(intDomainId) !== -1) {
if (currentDomainId) {
res.send(getDomain(currentDomainId));
res.send(wrapResponse(getDomain(currentDomainId)));
} else {
res.status(204).end();
}
......@@ -129,7 +132,7 @@ module.exports = {
}
},
'/api/domain/current': (req, res) => {
res.send(getDomain(currentDomainId));
res.send(wrapResponse(getDomain(currentDomainId)));
},
'/api/module/all/info': (req, res) => {
console.log('/api/module/all/info');
......@@ -151,17 +154,17 @@ module.exports = {
parents.add(p);
});
});
res.send([
res.send(wrapResponse([
...publics,
...parents,
]);
]));
},
'/api/bpm/task/all/count': (req, res) => {
res.send(`${dealWithData(req).length}`);
res.send(wrapResponse(dealWithData(req).length));
},
'/api/bpm/task/all/info': (req, res) => {
const pst = Number.parseInt(req.query.pst, 10);
const psz = Number.parseInt(req.query.psz, 10);
res.send(dealWithData(req).slice(pst, pst + psz));
res.send(wrapResponse(dealWithData(req).slice(pst, pst + psz)));
},
};
......@@ -21,6 +21,7 @@
"dva": "^1.2.1",
"dva-loading": "^0.2.1",
"fastjson_ref_resolver": "latest",
"fingerprintjs": "^0.5.3",
"jsencrypt": "^2.3.1",
"lodash": "^4.17.4",
"lodash-deep": "^2.0.0",
......
import dva from 'dva';
import createLoading from 'dva-loading';
import { browserHistory } from 'dva/router';
import moment from 'moment';
import { history } from './services/route';
import { initApp } from './data/app';
import appModel from './models/app';
import routerConfig from './router';
import { showError } from './utils/error';
import { processError } from './utils/error';
import './index.css';
moment.locale('zh-cn');
// 1. Initialize
const app = dva({
history: browserHistory,
history,
onError(error) {
// eslint-disable-next-line no-console
console.log(error);
showError(error);
processError(error);
},
});
......
import { routerRedux } from 'dva/router';
import { fetchDomains, switchDomain, currentDomain } from '../services/domain';
import { setCookie, setLocalStorge, fullPath } from '../utils/helper';
import { cookie, storage } from '../utils/config';
import { setCookie, fullPath } from '../utils/helper';
import { cookie } from '../utils/config';
export default {
......@@ -18,14 +18,14 @@ export default {
effects: {
*fetch(action, { put, call }) {
const list = yield call(fetchDomains);
const list = yield call(fetchDomains, '/', true);
yield put({ type: 'querySuccess', payload: list });
},
*switch({ payload: domainId }, { put, call }) {
yield call(switchDomain, domainId);
const { id, name } = yield call(currentDomain);
setCookie(cookie.domain, id);
setLocalStorge(storage.domainName, name);
*switch({ payload: domainPath }, { put, call }) {
yield call(switchDomain, domainPath);
const { path, name } = yield call(currentDomain);
setCookie(cookie.domainPath, path);
setCookie(cookie.domainName, name);
yield put(routerRedux.push(fullPath('/main')));
},
},
......
import { routerRedux } from 'dva/router';
import { login } from '../services/login';
import { setCookie, setLocalStorge, fullPath } from '../utils/helper';
import { cookie, storage } from '../utils/config';
import { login, userInfo } from '../services/login';
import { setCookie, fullPath } from '../utils/helper';
import { cookie } from '../utils/config';
export default {
namespace: 'login',
......@@ -10,10 +10,11 @@ export default {
effects: {
*login({ payload }, { call, put }) {
const result = yield call(login, payload);
const { tokenId, userId, userName } = result;
const { tokenId } = result;
setCookie(cookie.token, tokenId);
setCookie(cookie.user, userId);
setLocalStorge(storage.userName, userName);
const uInfo = yield call(userInfo);
setCookie(cookie.userId, uInfo.id);
setCookie(cookie.userName, uInfo.name);
yield put(routerRedux.push(fullPath('/domain')));
},
},
......
import { routerRedux } from 'dva/router';
import { logout, fetchModuleInfos } from '../../services/main';
import { fetchDomains, switchDomain, currentDomain } from '../../services/domain';
import { cookie, storage } from '../../utils/config';
import { cookie } from '../../utils/config';
import { setModules, getModules, getModule, getPath, foreachModule, isInited } from '../../data/modules';
import { setCookie, delCookie, getLocalStorge, setLocalStorge, delLocalStorge, fullPath } from '../../utils/helper';
import { setCookie, delCookie, fullPath, getCookie } from '../../utils/helper';
const createMenus = () => {
const menus = [];
......@@ -72,29 +72,29 @@ export default {
effects: {
*fetchDomain(action, { put, call }) {
let domain = getLocalStorge(storage.domainName);
let domain = getCookie(cookie.domainName);
if (!domain) {
const { id, name } = yield call(currentDomain);
setCookie(cookie.domain, id);
setLocalStorge(storage.domainName, name);
const { path, name } = yield call(currentDomain);
setCookie(cookie.domainPath, path);
setCookie(cookie.domainName, name);
domain = name;
}
yield put({ type: 'switchDomainSuccess', payload: domain });
},
*fetchDomains(action, { put, call }) {
const list = yield call(fetchDomains);
const list = yield call(fetchDomains, '/', true);
yield put({ type: 'queryDomainsSuccess', payload: list });
},
*switchDomain({ payload: domainId }, { put, call }) {
yield call(switchDomain, domainId);
const { id, name } = yield call(currentDomain);
setCookie(cookie.domain, id);
setLocalStorge(storage.domainName, name);
const { path, name } = yield call(currentDomain);
setCookie(cookie.domainPath, path);
setCookie(cookie.domainName, name);
yield put({ type: 'switchDomainSuccess', payload: name });
yield put(routerRedux.push(fullPath('/main')));
},
*fetchUser(action, { put }) {
const user = getLocalStorge(storage.userName);
const user = getCookie(cookie.userName);
if (!user) {
yield put(routerRedux.push(fullPath('/login')));
}
......@@ -109,10 +109,10 @@ export default {
},
*logout(action, { put, call }) {
yield call(logout);
delLocalStorge(storage.domainName);
delLocalStorge(storage.userName);
delCookie(cookie.domain);
delCookie(cookie.user);
delCookie(cookie.domainName);
delCookie(cookie.userName);
delCookie(cookie.domainPath);
delCookie(cookie.userId);
delCookie(cookie.token);
yield put(routerRedux.push(fullPath('/login')));
},
......
......@@ -34,9 +34,13 @@ class Domain extends React.Component {
render() {
const { form, domain, loading } = this.props;
const currentDomain = getCookie(cookie.domain);
const currentDomain = getCookie(cookie.domainPath);
const selectOptions = {};
const decoratorOptions = {};
const decoratorOptions = {
rules: [
{ required: true },
],
};
if (!currentDomain) {
selectOptions.placeholder = '请选择项目...';
} else {
......@@ -50,8 +54,8 @@ class Domain extends React.Component {
{
form.getFieldDecorator('domain', decoratorOptions)(
<Select {...selectOptions}>
{domain.list.map(({ id, name }) => {
return <Option value={id} key={id}>{name}</Option>;
{domain.list.map(({ path, name }) => {
return <Option value={path} key={path}>{name}</Option>;
})}
</Select>,
)
......
......@@ -24,7 +24,11 @@ class LoginForm extends React.Component {
if (!err) {
this.props.dispatch({
type: 'login/login',
payload: values,
payload: {
type: 'userName',
authType: 'password',
...values,
},
});
}
});
......
......@@ -5,7 +5,7 @@ import { Button } from 'antd';
import { connect } from 'dva';
import TableEx from '../../../../components/table/index';
import config from '../../../../utils/config';
import { push } from '../../../../utils/navigate';
import { thisPush } from '../../../../services/route';
import styles from './list.less';
const columns = [{
......@@ -137,7 +137,7 @@ class List extends React.Component {
return (
<div className={styles.wrapper}>
<div className={styles.container}>
<Button onClick={() => { push(this, { pathname: '../detail', state: { a: 1, b: 2, c: 3 } }); }}>detail</Button>
<Button onClick={() => { thisPush(this, { pathname: '../detail', state: { a: 1, b: 2, c: 3 } }); }}>detail</Button>
<TableEx {...tableProps} />
</div>
</div>
......
/* eslint-disable no-param-reassign */
import { endsWith } from 'lodash';
import request from '../utils/request';
import post from '../utils/post';
import { cookie } from '../utils/config';
import { getCookie } from '../utils/helper';
export async function fetchDomains() {
return request('/api/domain/all');
const processDomainInfo = (basePath, info) => {
if (!endsWith(basePath, '/')) {
basePath = `${basePath}/`;
}
if (info.id) {
info.path = `${basePath}${info.id}`;
} else {
info.path = basePath;
}
return info;
};
export async function fetchDomains(basePath, withRoot = false) {
if (!basePath) {
basePath = getCookie(cookie.domainPath);
}
const rootDomainInfo = withRoot ? await request('/api/domain/user/info', { dmPath: basePath }) : null;
const childrenDomainInfoList = await request('/api/domain/user/children-info', { dmPath: basePath });
let infoList;
if (rootDomainInfo && childrenDomainInfoList) {
infoList = [
rootDomainInfo,
...childrenDomainInfoList,
];
} else if (rootDomainInfo) {
infoList = [rootDomainInfo];
} else if (childrenDomainInfoList) {
infoList = childrenDomainInfoList;
} else {
infoList = [];
}
return infoList.map(info => processDomainInfo(basePath, info));
}
export async function switchDomain(domainId) {
return request('/api/domain/switch', { domainId });
export async function switchDomain(path) {
return post('/api/domain/user/path', { path });
}
export async function currentDomain() {
return request('/api/domain/current');
const domainInfo = await request('/api/domain/user/info');
if (domainInfo) {
domainInfo.path = await request('/api/domain/user/path');
}
return domainInfo;
}
import Fingerprint from 'fingerprintjs';
import post from '../utils/post';
import request from '../utils/request';
import { encrypt } from '../utils/helper';
export async function login(payload) {
const sign = encrypt(JSON.stringify({
un: payload.userName,
ps: payload.password,
type: 1,
productId: 1,
}));
return request(`/api/user/login2?sign=${sign}&method=password`, {}, {}, false);
const data = {};
data.type = payload.type;
if (data.type === 'userName') {
data.data = payload.userName;
}
if (payload.authType === 'password') {
data.authRequest = {
type: payload.authType,
parameters: {
cipher: payload.password,
},
};
}
data.tokenInfo = {
productId: 'big-machine-web-front',
deviceId: `${new Fingerprint({ ie_activex: true }).get()}`,
};
return post('/api/auth/login', data, {}, {}, false);
}
export async function userInfo() {
return request('/api/user/info');
}
import request from '../utils/request';
import post from '../utils/post';
export async function logout() {
return request('/api/user/logout');
return post('/api/auth/logout');
}
export async function fetchModuleInfos() {
......
import _ from 'lodash';
import { browserHistory } from 'dva/router';
import resolvePathname from 'resolve-pathname';
import { isString } from 'lodash';
import config from '../utils/config';
export function makePath(base, path) {
const { contextPath } = config;
const makePath = (base, path) => {
if (path.startsWith('/')) {
return path;
return `${contextPath}${path}`;
}
const basePath = base.endsWith('/') ? base : `${base}/`;
return resolvePathname(path, basePath);
}
};
const processPath = (base, path) => {
if (isString(path)) {
return makePath(base, path);
}
return {
...path,
pathname: makePath(base, path.pathname),
};
};
const getHistoryBase = (history) => {
if (history.location) {
return history.location.pathname;
} else {
return location.pathname;
}
};
export const history = browserHistory;
export const location = {};
history.listen((loc, action) => {
console.log(action, loc);
location.pathname = loc.pathname;
location.state = loc.state;
location.search = loc.search;
location.hash = loc.hash;
});
export const push = (path, state) => {
return history.push(processPath(getHistoryBase(history), path), state);
};
export const replace = (path, state) => {
return history.replace(processPath(getHistoryBase(history), path), state);
};
export const go = (n) => {
return history.go(n);
};
export const goBack = () => {
return history.goBack();
};
export const goForward = () => {
return history.goForward();
};
const checkThis = (theThis) => {
if (!theThis || !theThis.props) {
......@@ -18,7 +72,7 @@ const checkThis = (theThis) => {
}
};
const getBase = (theThis) => {
const getThisBase = (theThis) => {
if (theThis.props.location) {
return theThis.props.location.pathname;
} else if (typeof window !== 'undefined') {
......@@ -28,43 +82,29 @@ const getBase = (theThis) => {
}
};
const dealWithRoute = (theThis, route) => {
const base = getBase(theThis);
if (_.isString(route)) {
return makePath(base, route);
} else if (route && route.pathname) {
return {
...route,
pathname: makePath(base, route.pathname),
};
} else {
throw new Error('invalid route');
}
};
export function push(theThis, pathOrLoc) {
export const thisPush = (theThis, pathOrLoc) => {
checkThis(theThis);
const route = dealWithRoute(theThis, pathOrLoc);
const route = processPath(getThisBase(theThis), pathOrLoc);
return theThis.props.router.push(route);
}
};
export function replace(theThis, pathOrLoc) {
export const thisReplace = (theThis, pathOrLoc) => {
checkThis(theThis);
const route = dealWithRoute(theThis, pathOrLoc);
const route = processPath(getThisBase(theThis), pathOrLoc);
return theThis.props.router.replace(route);
}
};
export function go(theThis, n) {
export const thisGo = (theThis, n) => {
checkThis(theThis);
return theThis.props.router.go(n);
}
};
export function goBack(theThis) {
export const thisGoBack = (theThis) => {
checkThis(theThis);
return theThis.props.router.goBack();
}
};
export function goForward(theThis) {
export const thisGoForward = (theThis) => {
checkThis(theThis);
return theThis.props.router.goForward();
}
};
......@@ -9,7 +9,7 @@ export function isAuthed() {
}
export function hasDomain() {
return !!getCookie(cookie.domain);
return !!getCookie(cookie.domainPath);
}
export function redirectLogin() {
......
......@@ -4,15 +4,17 @@
export const cookie = {
token: 'token',
user: 'user',
domain: 'domain',
};
export const storage = {
userId: 'userId',
userName: 'userName',
domainPath: 'domainPath',
domainName: 'domainName',
};
export const errors = {
exception: 0x00010000,
invalid_token: 0x00010003,
};
export const api = {
userLogin: '/user/login',
userLogout: '/user/logout',
......
import React from 'react';
import { message, Modal } from 'antd';
import { push } from '../services/route';
import { errors as errorCodes } from './config';
const errStyle = {
overflow: 'auto',
......@@ -10,7 +12,20 @@ const errStyle = {
marginTop: '24px',
};
export function showError(err) {
export function processError(err) {
if (err && err.data) {
const data = err.data;
switch (data.errorCode) {
case errorCodes.invalid_token:
push('/login');
break;
default:
showError(err);
}
}
}
function showError(err) {
if (err) {
let msg;
if (err.data) {
......
import _ from 'lodash';
import { Resolver } from 'fastjson_ref_resolver';
export function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
return response.json().then((data) => {
const error = new Error(data.message ? data.message : response.statusText);
error.data = data;
throw error;
});
}
export function normParams(unnormed) {
if (_.isPlainObject(unnormed)) {
return _(unnormed).toPairs().flatMap(([k, v]) => (_.isArray(v) ? v.map(vv => [k, vv]) : [[k, v]])).value();
} else {
return unnormed;
}
}
// eslint-disable-next-line max-len
export function parseObject(response, { num2str = false, bool2str = false, nul2str = false, ud2str = false, nil2str = false } = {}) {
if (response.status === 204) { // no-content
return null;
} else {
const contentType = response.headers.get('content-type');
if (contentType) {
const needMap = num2str || bool2str || nul2str || ud2str || nil2str;
const mapStr = (value) => {
if (num2str && _.isNumber(value)) {
return value.toString();
}
if (bool2str && _.isBoolean(value)) {
return value.toString();
}
if (nul2str && _.isNull(value)) {
return '';
}
if (ud2str && _.isUndefined(value)) {
return '';
}
if (nil2str && _.isNil(value)) {
return '';
}
return value;
};
const mapObj = (obj, mapArrFunc) => {
if (_.isArray(obj)) {
return mapArrFunc(obj, mapObj);
}
if (_.isPlainObject(obj)) {
return _.mapValues(obj, (val) => {
const ret = mapStr(val);
return mapObj(ret, mapArrFunc);
});
}
return obj;
};
const mapArr = (arr, mapObjFunc) => {
if (_.isPlainObject(arr)) {
return mapObjFunc(arr, mapArr);
}
if (_.isArray(arr)) {
return _.map(arr, (val) => {
const ret = mapStr(val);
return mapArr(ret, mapObjFunc);
});
}
return arr;
};
const mapValue = _.curry(mapObj)(_, mapArr);
if (contentType.indexOf('json') !== -1) {
return response.json()
.then((json) => {
if (json.errorCode === 0) {
let data = json.data;
if (_.isObjectLike(data)) {
data = new Resolver(json.data).resolve();
}
return needMap ? mapValue(data) : data;
} else {
throw new Error(json.message);
}
});
} else if (contentType.indexOf('xml') !== -1) {
return response.text().then((text) => {
return require.ensure([], (require) => {
const { parseString } = require('xml2js');
const options = {};
const json = JSON.parse(parseString(text, options));
return needMap ? mapValue(json) : json;
});
});
} else if (contentType.indexOf('text') !== -1) {
return response.text();
} else {
throw new Error(`Unsupported response content type: ${contentType}`);
}
} else {
throw new Error('No response content type.');
}
}
}
/* eslint-disable no-param-reassign */
import fetch from 'dva/fetch';
import _ from 'lodash';
import { getCookie, encrypt } from './helper';
import { checkStatus, normParams, parseObject } from './http-helper';
import { cookie } from './config';
import { errors } from './error';
const defaultOptions = {
headers: {
Accept: 'application/json',
},
};
export default function post(url, data, params = {}, options = {}, auth = true) {
if (!data) {
data = {};
}
if (auth) {
const token = getCookie(cookie.token);
if (!token) {
return Promise.reject(errors.unLogin);
}
data.token = encrypt(token);
}
let queryParams = normParams(params);
queryParams = queryParams.map(([k, v]) => (_.isNil(v) ? k : `${k}=${encodeURIComponent(v)}`));
queryParams = queryParams.join('&');
let realUrl = url;
if (queryParams) {
realUrl = `${url}?${queryParams}`;
}
const realOptions = _.defaults(options, defaultOptions);
if (!realOptions.headers) {
realOptions.headers = {
Accept: 'application/json',
};
}
realOptions.headers['Content-Type'] = 'application/json';
realOptions.method = 'POST';
realOptions.body = JSON.stringify(data);
return fetch(realUrl, realOptions)
.then(checkStatus)
.then(parseObject);
}
import fetch from 'dva/fetch';
import _ from 'lodash';
import { Resolver } from 'fastjson_ref_resolver';
import { getCookie, encrypt } from '../utils/helper';
import { cookie } from '../utils/config';
import { errors } from '../utils/error';
// eslint-disable-next-line max-len
function parseObject(response, { num2str = false, bool2str = false, nul2str = false, ud2str = false, nil2str = false } = {}) {
if (response.status === 204) { // no-content
return null;
} else {
const contentType = response.headers.get('content-type');
if (contentType) {
const needMap = num2str || bool2str || nul2str || ud2str || nil2str;
const mapStr = (value) => {
if (num2str && _.isNumber(value)) {
return value.toString();
}
if (bool2str && _.isBoolean(value)) {
return value.toString();
}
if (nul2str && _.isNull(value)) {
return '';
}
if (ud2str && _.isUndefined(value)) {
return '';
}
if (nil2str && _.isNil(value)) {
return '';
}
return value;
};
const mapObj = (obj, mapArrFunc) => {
if (_.isArray(obj)) {
return mapArrFunc(obj, mapObj);
}
if (_.isPlainObject(obj)) {
return _.mapValues(obj, (val) => {
const ret = mapStr(val);
return mapObj(ret, mapArrFunc);
});
}
return obj;
};
const mapArr = (arr, mapObjFunc) => {
if (_.isPlainObject(arr)) {
return mapObjFunc(arr, mapArr);
}
if (_.isArray(arr)) {
return _.map(arr, (val) => {
const ret = mapStr(val);
return mapArr(ret, mapObjFunc);
});
}
return arr;
};
const mapValue = _.curry(mapObj)(_, mapArr);
if (contentType.indexOf('json') !== -1) {
const json = needMap ? response.json().then(mapValue) : response.json();
return new Resolver(json).resolve();
} else if (contentType.indexOf('xml') !== -1) {
return response.text().then((text) => {
return require.ensure([], (require) => {
const { parseString } = require('xml2js');
const options = {};
const json = JSON.parse(parseString(text, options));
return needMap ? mapValue(json) : json;
});
});
} else if (contentType.indexOf('text') !== -1) {
return response.text();
} else {
throw new Error(`Unsupported response content type: ${contentType}`);
}
} else {
throw new Error('No response content type.');
}
}
}
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
return response.json().then((data) => {
const error = new Error(data.message ? data.message : response.statusText);
error.data = data;
throw error;
});
}
import { getCookie, encrypt } from './helper';
import { cookie } from './config';
import { errors } from './error';
import { checkStatus, normParams, parseObject } from './http-helper';
const defaultOptions = {
headers: { Accept: 'application/json' },
......@@ -124,10 +39,3 @@ export default function request(url, params = {}, options = {}, auth = true) {
.then(parseObject);
}
export function normParams(unnormed) {
if (_.isPlainObject(unnormed)) {
return _(unnormed).toPairs().flatMap(([k, v]) => (_.isArray(v) ? v.map(vv => [k, vv]) : [[k, v]])).value();
} else {
return unnormed;
}
}
......@@ -2,8 +2,8 @@ import React from 'react';
import { partial } from 'lodash';
import { storiesOf } from '@storybook/react';
import ReactJson from 'react-json-view'
import { DatePicker, Form, Input, Modal } from 'antd';
import ReactJson from 'react-json-view';
import { DatePicker, Form, Input, Modal, Button } from 'antd';
import dva from 'dva';
import createLoading from 'dva-loading';
......@@ -14,16 +14,15 @@ import { makeCancelable } from '../src/utils/promise';
import { login } from '../src/services/login';
import { switchDomain } from '../src/services/domain';
import { getModuleConfigure } from '../src/services/modules';
import { setCookie, setLocalStorge } from '../src/utils/helper';
import { cookie, storage } from '../src/utils/config';
import Button from "antd/es/button/button";
import { setCookie } from '../src/utils/helper';
import { cookie } from '../src/utils/config';
const loginIt = async (userName, password, domainId) => {
const result = await login({ userName, password });
const { tokenId, userId } = result;
setCookie(cookie.token, tokenId);
setCookie(cookie.user, userId);
setLocalStorge(storage.userName, userName);
setCookie(cookie.userId, userId);
setCookie(cookie.userName, userName);
await switchDomain(domainId);
const config = await getModuleConfigure('test');
console.log(config);
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论