mirror of https://gitee.com/anolis/sysom.git
commit
09e8b61bc7
|
@ -48,6 +48,22 @@ export default [
|
|||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/migrate',
|
||||
name: 'migrate',
|
||||
routes: [
|
||||
{
|
||||
path: '/migrate',
|
||||
redirect: '/migrate/implement',
|
||||
},
|
||||
{
|
||||
path: '/migrate/implement',
|
||||
name: 'implement',
|
||||
hideInBreadcrumb: true,
|
||||
component: './migrate/implement',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'monitor',
|
||||
|
|
|
@ -4,6 +4,8 @@ export default {
|
|||
'menu.host.list': '主机列表',
|
||||
'menu.host.cluster': '集群列表',
|
||||
'menu.host.terminal': '主机终端',
|
||||
'menu.migrate': '操作系统迁移',
|
||||
'menu.migrate.implement': '迁移实施',
|
||||
'menu.monitor': '监控中心',
|
||||
'menu.monitor.': '监控中心',
|
||||
'menu.monitor.dashboard': '系统监控',
|
||||
|
|
|
@ -30,6 +30,9 @@ const Welcome = () => {
|
|||
<Menu.Item key="hostlist"><a href="/host/list">主机列表</a></Menu.Item>
|
||||
<Menu.Item key="cluster"><a href="/host/cluster">集群列表</a></Menu.Item>
|
||||
</SubMenu>
|
||||
<SubMenu key="migrate" title="操作系统迁移">
|
||||
<Menu.Item key="implement"><a href="/migrate/implement">迁移实施</a></Menu.Item>
|
||||
</SubMenu>
|
||||
<SubMenu key="monitor" title="监控中心">
|
||||
<Menu.Item key="dashboard"><a href="/monitor/dashboard">系统监控</a></Menu.Item>
|
||||
</SubMenu>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
.menuCenter{
|
||||
position: absolute;
|
||||
width: 40%;
|
||||
width: 46%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-5%);
|
||||
line-height: 48px!important;
|
||||
|
|
|
@ -0,0 +1,200 @@
|
|||
/* eslint-disable prefer-promise-reject-errors */
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { Form, Input, Modal, Select, Button, Radio, message } from 'antd';
|
||||
import { withRouter } from 'umi';
|
||||
|
||||
import { WrapperContext } from '../../containers';
|
||||
import { SET_DATA } from '../../containers/constants';
|
||||
import { getNodesList,BulkMigration } from '../../../service';
|
||||
|
||||
const { TextArea } = Input;
|
||||
const { Option } = Select;
|
||||
const { Item: FormItem } = Form;
|
||||
|
||||
export default withRouter(
|
||||
() => {
|
||||
const {
|
||||
dispatch,
|
||||
state: { allMoveVisible, machineList, startMigrateIp, activeMachineGroupId },
|
||||
} = useContext(WrapperContext);
|
||||
// Repo配置选择项
|
||||
const [repoType, setRepoType] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const initialValues = {
|
||||
kernel: 'ANCK',
|
||||
repo_type: 'public',
|
||||
verson: 'Anolis OS 8',
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
form.validateFields().then(async (values) => {
|
||||
if (typeof values === 'undefined') return true;
|
||||
const hide = message.loading('开始迁移中...', 0);
|
||||
let params = {...values}
|
||||
if(startMigrateIp !== ""){
|
||||
params.ip = [startMigrateIp];
|
||||
}
|
||||
try {
|
||||
const { code } = await BulkMigration(params);
|
||||
if (code === 200) {
|
||||
try {
|
||||
const {
|
||||
code: queryCode,
|
||||
data: nodeList,
|
||||
} = await getNodesList({ id: activeMachineGroupId });
|
||||
if (queryCode === 200) {
|
||||
message.success('开始迁移成功');
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineList: nodeList && nodeList.length !== 0 ? nodeList : [],
|
||||
nodeTotal: nodeList ? nodeList.length : 0,
|
||||
allMoveVisible: false,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log(`更新数据获取失败,错误信息:${e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
message.error('开始迁移失败,请重试!');
|
||||
return false;
|
||||
} catch (error) {
|
||||
message.error(error);
|
||||
return false;
|
||||
} finally {
|
||||
hide();
|
||||
form.resetFields();
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
const handleRepo = (e) => {
|
||||
if(e.target.value === "public"){
|
||||
setRepoType(false);
|
||||
}else if(e.target.value === "private"){
|
||||
setRepoType(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
wrapClassName="addModal"
|
||||
centered
|
||||
width={960}
|
||||
destroyOnClose
|
||||
afterClose={() => form.resetFields()}
|
||||
title="批量迁移机器"
|
||||
visible={allMoveVisible}
|
||||
onCancel={() => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: { allMoveVisible: false },
|
||||
});
|
||||
}}
|
||||
footer={[
|
||||
<Button
|
||||
key="back"
|
||||
onClick={() =>{
|
||||
form.resetFields();
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: { allMoveVisible: false },
|
||||
})
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleAdd}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form
|
||||
layout="horizontal"
|
||||
form={form}
|
||||
initialValues={initialValues}
|
||||
labelCol={{
|
||||
span: 4,
|
||||
}}
|
||||
wrapperCol={{
|
||||
span: 18,
|
||||
}}
|
||||
>
|
||||
{
|
||||
startMigrateIp === '' ?
|
||||
<FormItem
|
||||
name="ip"
|
||||
label="选择机器"
|
||||
rules={[{ required: true, message: "机器不能为空" }]}
|
||||
>
|
||||
<Select placeholder="请选择机器" mode="multiple">
|
||||
{
|
||||
machineList && machineList.map((item)=>{
|
||||
return(
|
||||
<Option key={item.ip}>{item.ip}</Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
</FormItem>
|
||||
:
|
||||
<FormItem
|
||||
name="ip"
|
||||
label="选择机器"
|
||||
required
|
||||
>
|
||||
<Input value={startMigrateIp} defaultValue={startMigrateIp} disabled/>
|
||||
</FormItem>
|
||||
}
|
||||
<FormItem
|
||||
name="verson"
|
||||
label="迁移版本"
|
||||
rules={[{ required: true, message: "迁移版本不能为空" }]}
|
||||
>
|
||||
<Select placeholder="请选择要迁移的版本">
|
||||
{/* <Option key="Anolis OS 7">Anolis OS 7</Option> */}
|
||||
<Option key="Anolis OS 8">Anolis OS 8</Option>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="kernel"
|
||||
label="选择内核"
|
||||
rules={[{ required: true, message: "内核不能为空" }]}
|
||||
>
|
||||
<Radio.Group >
|
||||
<Radio value="ANCK">ANCK</Radio>
|
||||
{/* <Radio value="RHCK">RHCK</Radio> */}
|
||||
</Radio.Group>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="repo_type"
|
||||
label="Repo配置"
|
||||
rules={[{ required: true, message: "Repo配置不能为空" }]}
|
||||
>
|
||||
<Radio.Group onChange={handleRepo}>
|
||||
<Radio value="public">公网地址</Radio>
|
||||
<Radio value="private">内网地址</Radio>
|
||||
</Radio.Group>
|
||||
</FormItem>
|
||||
{
|
||||
repoType &&
|
||||
<FormItem name="repo_url" label colon={false}>
|
||||
<TextArea rows={3} placeholder="请输入内网地址" maxLength={150} />
|
||||
</FormItem>
|
||||
}
|
||||
<FormItem name="backup_pwd" label="备份路径">
|
||||
<TextArea rows={3} placeholder="请输入备份路径" maxLength={150} />
|
||||
</FormItem>
|
||||
<FormItem name="backup_dir" label="备份目录">
|
||||
<TextArea rows={3} placeholder="请输入备份目录" maxLength={150} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -0,0 +1,287 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Statistic, Row, Col, Skeleton, message, Card, Select } from 'antd';
|
||||
import { WrapperContext } from '../../containers';
|
||||
import { SET_DATA } from '../../containers/constants';
|
||||
import {
|
||||
getBannerList,
|
||||
getNodesList,
|
||||
qyeryMachineInfo,
|
||||
qyeryLog,
|
||||
qyeryReport,
|
||||
} from '../../../service';
|
||||
import styles from './style.less';
|
||||
|
||||
|
||||
const machineGroup = (props) => {
|
||||
const {
|
||||
dispatch,
|
||||
state: { loadingVisible, nodeTotal, abnormalNodeTotal, machineGroupsList, activeMachineGroupId },
|
||||
} = useContext(WrapperContext);
|
||||
|
||||
useEffect(()=>{
|
||||
getGroupsList()
|
||||
},[])
|
||||
|
||||
// 获取机器组
|
||||
const getGroupsList = async () => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
loadingVisible: true,
|
||||
},
|
||||
});
|
||||
const {code, data} = await getBannerList();
|
||||
if (code === 200) {
|
||||
if(data && data.length !== 0){
|
||||
getMachineList(data[0].id);
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineGroupsList: data,
|
||||
activeMachineGroupId: data[0].id,
|
||||
},
|
||||
});
|
||||
}else{
|
||||
getNoData()
|
||||
}
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
loadingVisible: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取机器列表
|
||||
const getMachineList = async (myid) => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineTableLoading: true,
|
||||
},
|
||||
});
|
||||
const {code, data} = await getNodesList({id:myid});
|
||||
if (code === 200) {
|
||||
if(data && data.length !== 0){
|
||||
handleMachineName(data[0])
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineList: data,
|
||||
nodeTotal: data.length,
|
||||
abnormalNodeTotal: data.filter((i)=>i.status === "waiting").length,
|
||||
},
|
||||
});
|
||||
}else{
|
||||
getNoData()
|
||||
}
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineTableLoading: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 数据置空
|
||||
const getNoData = () => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineList: [],
|
||||
nodeTotal: 0,
|
||||
abnormalNodeTotal: 0,
|
||||
systemMessage: {},
|
||||
logtMessage: '',
|
||||
reportMessage: {},
|
||||
tableIp: '',
|
||||
tableIpVersion: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const handleMachineName = (r) => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineDetailLoading: true,
|
||||
},
|
||||
});
|
||||
const hide = message.loading('loading...', 0);
|
||||
Promise.all([
|
||||
getMachineInfo(r.ip),
|
||||
getLog(r.ip),
|
||||
getReport(r.ip),
|
||||
]).then((res) => {
|
||||
console.log(res,'jaja')
|
||||
const [
|
||||
{ data: systemMessage },
|
||||
{ data: logtMessage },
|
||||
{ data: reportMessage },
|
||||
] = res;
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
tableIp: r.ip,
|
||||
tableIpVersion: r.version ? `${r.ip} (${r.version})` : `${r.ip}`,
|
||||
machineDetailLoading: false,
|
||||
},
|
||||
});
|
||||
hide();
|
||||
}).catch((error)=>{
|
||||
hide();
|
||||
console.log(error,'error')
|
||||
})
|
||||
// Promise.all([
|
||||
// qyeryMachineInfo({ ip: r.ip}),
|
||||
// qyeryLog({ ip: r.ip }),
|
||||
// qyeryReport({ ip: r.ip }),
|
||||
// ]).then((res) => {
|
||||
// hide();
|
||||
// const [
|
||||
// { data: systemMessage },
|
||||
// { data: logtMessage },
|
||||
// { data: reportMessage },
|
||||
// ] = res;
|
||||
// dispatch({
|
||||
// type: SET_DATA,
|
||||
// payload: {
|
||||
// systemMessage,
|
||||
// logtMessage,
|
||||
// reportMessage,
|
||||
// tableIp: r.ip,
|
||||
// tableIpVersion: r.version ? `${r.ip} (${r.version})` : `${r.ip}`,
|
||||
// machineDetailLoading: false,
|
||||
// },
|
||||
// });
|
||||
// })
|
||||
}
|
||||
|
||||
const getMachineInfo = async (ip) => {
|
||||
try {
|
||||
const { code,data } = await qyeryMachineInfo({ ip });
|
||||
if (code === 200) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
systemMessage: data,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
systemMessage: {},
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const getLog = async (ip) => {
|
||||
try {
|
||||
const { code,data } = await qyeryLog({ ip });
|
||||
if (code === 200) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
logtMessage: data,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
logtMessage: '',
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const getReport = async (ip) => {
|
||||
try {
|
||||
const { code,data } = await qyeryReport({ ip });
|
||||
if (code === 200) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
reportMessage: data,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
reportMessage: {},
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.banner}>
|
||||
<Skeleton loading={loadingVisible}>
|
||||
<Card
|
||||
title={
|
||||
<div>
|
||||
{
|
||||
machineGroupsList && machineGroupsList.length !== 0 ?
|
||||
<Select
|
||||
key="banner-select"
|
||||
defaultValue={activeMachineGroupId}
|
||||
style={{ width: 150 }}
|
||||
dropdownStyle={{background:"#333"}}
|
||||
bordered={false}
|
||||
// open={true}
|
||||
onChange={(key)=>getMachineList(key)}
|
||||
>
|
||||
{ machineGroupsList.map((item)=>{
|
||||
return(
|
||||
<Option key={item.id} value={item.id}>{item.cluster_name}</Option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Select>
|
||||
:
|
||||
<div>暂无机器组</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Row>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="机器总数:"
|
||||
value={nodeTotal}
|
||||
valueStyle={{ color: 'rgba(255,255,255,0.85)', fontSize: 30 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic
|
||||
title="待迁移机器数:"
|
||||
value={abnormalNodeTotal}
|
||||
valueStyle={{ color: '#FF4D4F', fontSize: 30 }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default machineGroup;
|
|
@ -0,0 +1,54 @@
|
|||
.banner {
|
||||
background-color: #141414;
|
||||
border-radius: 4px;
|
||||
|
||||
:global {
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-row {
|
||||
padding: 24px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
.ant-statistic {
|
||||
.ant-statistic-title {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
font-size: 16px;
|
||||
}
|
||||
.ant-statistic-content {
|
||||
text-indent: 2px;
|
||||
}
|
||||
}
|
||||
// .ant-card-head{
|
||||
// padding: 0;
|
||||
// }
|
||||
.operate-btn {
|
||||
text-align: right;
|
||||
.ant-statistic-title {
|
||||
.ant-btn {
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.dashboard-operate-cluster.ant-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
background-color: #333;
|
||||
.ant-dropdown-menu-item {
|
||||
color: #fff;
|
||||
}
|
||||
.ant-dropdown-menu-item:hover,
|
||||
.ant-dropdown-menu-submenu-title:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-select-item-option-selected:not(.ant-select-item-option-disabled){
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Skeleton, Empty } from 'antd';
|
||||
import { WrapperContext } from '../../../../containers';
|
||||
|
||||
export default () => {
|
||||
const {
|
||||
state: { machineDetailLoading, logtMessage },
|
||||
} = useContext(WrapperContext);
|
||||
return (
|
||||
<div style={{ height: 'calc(100vh - 48px)', overflow: 'scroll' }}>
|
||||
<Skeleton loading={machineDetailLoading}>
|
||||
{logtMessage ? (
|
||||
<pre style={{ color: 'rgba(255,255,255,0.65)' }}>{logtMessage}</pre>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} style={{ marginTop: 100 }} />
|
||||
)}
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,119 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { Card, Col, Row, Empty, Skeleton } from 'antd';
|
||||
import { AppstoreOutlined, HddOutlined } from '@ant-design/icons';
|
||||
import { WrapperContext } from '../../../../containers';
|
||||
import { ReactComponent as SoftIcon } from '@/pages/migrate/static/softIcon.svg';
|
||||
import styles from './style.less';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export default function MachineMessage() {
|
||||
const {
|
||||
state: { systemMessage, machineDetailLoading },
|
||||
} = useContext(WrapperContext);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Skeleton loading={machineDetailLoading}>
|
||||
<Card
|
||||
className={styles.card}
|
||||
title={
|
||||
<>
|
||||
<AppstoreOutlined style={{ marginRight: 12, color: '#1890ff' }} />
|
||||
基本信息
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Row>
|
||||
{systemMessage.base_info && systemMessage.base_info.length !== 0 ? (
|
||||
<>
|
||||
{systemMessage.base_info.map((item) => (
|
||||
<Col span={12} key={nanoid()}>
|
||||
{item.name}:{item.value}
|
||||
</Col>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Col span={24} key={nanoid()}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
<Card
|
||||
className={styles.card}
|
||||
title={
|
||||
<>
|
||||
<HddOutlined style={{ marginRight: 12, color: '#1890ff' }} />
|
||||
硬件信息
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Row gutter={[0, 20]}>
|
||||
{systemMessage.hard_info && systemMessage.hard_info.length !== 0 ? (
|
||||
<>
|
||||
{systemMessage.hard_info.map((item) => (
|
||||
<Col span={12} key={nanoid()}>
|
||||
{item.name}:{item.value}
|
||||
</Col>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Col span={24} key={nanoid()}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
<Card
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<SoftIcon style={{ marginRight: 12 }} />
|
||||
软件信息
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Row gutter={[0, 20]}>
|
||||
{systemMessage.soft_info && systemMessage.soft_info.length !== 0 ? (
|
||||
<>
|
||||
{systemMessage.soft_info.map((item) => (
|
||||
<Col span={12} key={nanoid()}>
|
||||
{item.name}:{item.value}
|
||||
</Col>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Col span={24} key={nanoid()}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
<Card
|
||||
className={styles.card}
|
||||
title={
|
||||
<>
|
||||
<HddOutlined style={{ marginRight: 12, color: '#1890ff' }} />
|
||||
迁移信息
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Row gutter={[0, 20]}>
|
||||
{systemMessage.migration_info && systemMessage.migration_info.length !== 0 ? (
|
||||
<>
|
||||
{systemMessage.migration_info.map((item) => (
|
||||
<Col span={12} key={nanoid()}>
|
||||
{item.name}:{item.value}
|
||||
</Col>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<Col span={24} key={nanoid()}>
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
.content {
|
||||
.card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
:global {
|
||||
.ant-card-body {
|
||||
padding: 30px 50px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import { Collapse, Card, Row, Col, Empty, Skeleton } from 'antd';
|
||||
import {
|
||||
BankOutlined,
|
||||
RotateLeftOutlined,
|
||||
RotateRightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import styles from './style.less';
|
||||
import { WrapperContext } from '../../../../containers';
|
||||
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
const { Panel } = Collapse;
|
||||
export default function Report() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { reportMessage, machineDetailLoading },
|
||||
} = useContext(WrapperContext);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Skeleton loading={machineDetailLoading}>
|
||||
{reportMessage && reportMessage.before === undefined ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} style={{ marginTop: 100 }} />
|
||||
) : (
|
||||
<>
|
||||
<Collapse defaultActiveKey={['1']} style={{ marginBottom: 24 }}>
|
||||
<Panel
|
||||
header={
|
||||
<div style={{ fontSize: 16 }}>
|
||||
<BankOutlined style={{ color: '#1890ff', marginRight: '13px' }} />
|
||||
<span>系统信息迁移对比</span>
|
||||
</div>
|
||||
}
|
||||
key="1"
|
||||
>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<RotateLeftOutlined style={{ color: '#FCC00B', marginRight: '9px' }} />
|
||||
迁移前
|
||||
</>
|
||||
}
|
||||
>
|
||||
{reportMessage && reportMessage.before && reportMessage.before.length !== 0 && reportMessage.before.map((item) => (
|
||||
<div className={styles.rowLine} key={nanoid()}>
|
||||
{item.name}:{item.value}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card
|
||||
title={
|
||||
<>
|
||||
<RotateRightOutlined style={{ color: '#FCC00B', marginRight: '9px' }} />
|
||||
迁移后
|
||||
</>
|
||||
}
|
||||
>
|
||||
{reportMessage && reportMessage.after && reportMessage.after.length !== 0 && reportMessage.after.map((item) => (
|
||||
<div className={styles.rowLine} key={nanoid()}>
|
||||
{item.name}:{item.value}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Panel>
|
||||
</Collapse>
|
||||
</>
|
||||
)}
|
||||
</Skeleton>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
.content {
|
||||
.rowLine {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.contrast {
|
||||
:global {
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.ant-table-tbody :hover {
|
||||
background-color: #141414 !important;
|
||||
}
|
||||
.ant-card-head {
|
||||
font-size: 15px;
|
||||
}
|
||||
.ant-table table {
|
||||
padding: 0px 26px;
|
||||
}
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
.ant-collapse-content > .ant-collapse-content-box {
|
||||
padding: 24px 30px;
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 30px 0px 30px 60px;
|
||||
}
|
||||
.ant-card {
|
||||
background-color: #141414;
|
||||
}
|
||||
.ant-table-tbody > tr > td.ant-table-cell-row-hover {
|
||||
background-color: #141414;
|
||||
}
|
||||
.ant-pagination-disabled .ant-pagination-item-link,
|
||||
.ant-pagination-disabled:hover .ant-pagination-item-link {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background: #141414;
|
||||
border: 1px solid #2a2a2a;
|
||||
}
|
||||
.ant-pagination-item-active {
|
||||
background: #141414;
|
||||
// border: 1px solid #389e0d;
|
||||
// a {
|
||||
// color: #389e0d;
|
||||
// }
|
||||
}
|
||||
.ant-collapse-expand-icon {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
}
|
||||
.ant-collapse-header-text {
|
||||
padding-left: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.ant-table-filter-dropdown-btns {
|
||||
.ant-btn-primary {
|
||||
// color: #389e0d;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Tabs, Card } from 'antd';
|
||||
import styles from './style.less';
|
||||
import { ReactComponent as TabIcon } from '@/pages/migrate/static/tab.svg';
|
||||
import { ReactComponent as LiveTabIcon } from '@/pages/migrate/static/liveTab.svg';
|
||||
import MachineMessage from './components/MachineMessage';
|
||||
import LogMessage from './components/Log';
|
||||
import { WrapperContext } from '../../containers';
|
||||
import Report from './components/Report';
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
export default function Message() {
|
||||
const {
|
||||
dispatch,
|
||||
state: { tableIpVersion },
|
||||
} = useContext(WrapperContext);
|
||||
const [activeKey, setActiveKey] = useState('1');
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<Card title={tableIpVersion}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
activeKey={activeKey}
|
||||
onChange={(key) => {
|
||||
setActiveKey(key);
|
||||
}}
|
||||
>
|
||||
<TabPane
|
||||
key="1"
|
||||
tab={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{activeKey === '1' ? (
|
||||
<LiveTabIcon style={{ marginRight: 6 }} />
|
||||
) : (
|
||||
<TabIcon style={{ marginRight: 6 }} />
|
||||
)}
|
||||
机器信息
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<MachineMessage />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
key="3"
|
||||
tab={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{activeKey === '3' ? (
|
||||
<LiveTabIcon style={{ marginRight: 6 }} />
|
||||
) : (
|
||||
<TabIcon style={{ marginRight: 6 }} />
|
||||
)}
|
||||
迁移日志
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LogMessage />
|
||||
</TabPane>
|
||||
<TabPane
|
||||
key="4"
|
||||
tab={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{activeKey === '4' ? (
|
||||
<LiveTabIcon style={{ marginRight: 6 }} />
|
||||
) : (
|
||||
<TabIcon style={{ marginRight: 6 }} />
|
||||
)}
|
||||
迁移报告
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Report />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.content {
|
||||
min-height: calc(100vh - 90px);
|
||||
background-color: #141414;
|
||||
border-radius: 4px;
|
||||
}
|
|
@ -0,0 +1,422 @@
|
|||
/* eslint-disable no-case-declarations */
|
||||
import React, { Fragment, useContext, useEffect } from 'react';
|
||||
import { Card, Tooltip, Skeleton, Modal, message, Menu, Progress, Button } from 'antd';
|
||||
import ProTable, { TableDropdown } from '@ant-design/pro-table';
|
||||
import { withRouter } from 'umi';
|
||||
|
||||
import { WrapperContext } from '../../containers';
|
||||
import { SET_DATA } from '../../containers/constants';
|
||||
import {
|
||||
qyeryMachineInfo,
|
||||
qyeryLog,
|
||||
qyeryReport,
|
||||
rebootMachine,
|
||||
stopMigration,
|
||||
getNodesList,
|
||||
} from '../../../service';
|
||||
import styles from './style.less';
|
||||
|
||||
|
||||
export default withRouter(
|
||||
() => {
|
||||
const {
|
||||
dispatch,
|
||||
state: { loadingVisible, machineList, nodeTotal, activeMachineGroupId, machineTableLoading },
|
||||
} = useContext(WrapperContext);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '机器 IP(系统版本)',
|
||||
dataIndex: 'ip',
|
||||
ellipsis: true,
|
||||
render: (t, r) => (
|
||||
// <Tooltip trigger="hover" placement="topLeft" title={r.ip+' '+(r.version && `(${r.version})`)}>
|
||||
<span style={{cursor:"pointer"}} title={r.ip+' '+(r.version && `(${r.version})`)} onClick={()=>handleMachineName(r)}>{r.ip} {r.version ? `(${r.version})` : ''}</span>
|
||||
// </Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '迁移状态',
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
filters: false,
|
||||
valueEnum: {
|
||||
waiting: {
|
||||
text: <span style={{ fontSize: 13, color: 'rgba(255,255,255,0.4)' }}>未迁移</span>,
|
||||
},
|
||||
running: {
|
||||
text: <span style={{ fontSize: 13, color: '#FCC00B' }}>迁移中</span>,
|
||||
},
|
||||
success: {
|
||||
text: <span style={{ fontSize: 13, color: '#52C41A' }}>迁移完</span>,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
dataIndex: 'rate',
|
||||
width: 110,
|
||||
filters: false,
|
||||
renderText: (_, r) => (
|
||||
<Progress
|
||||
percent={r.rate || 0}
|
||||
className={
|
||||
r.rate === 100
|
||||
? styles.numGreen
|
||||
: r.rate > 0 && r.rate < 99
|
||||
? styles.numRed
|
||||
: styles.numZero
|
||||
}
|
||||
size="small"
|
||||
steps={10}
|
||||
format={(percent) => `${percent}%`}
|
||||
strokeColor={r.rate === 100 ? '#52C41A' : '#FF4D4F'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'option',
|
||||
valueType: 'option',
|
||||
width: 30,
|
||||
fixed: 'right',
|
||||
render: (t, r) => {
|
||||
const menus = [
|
||||
{
|
||||
key: 'start',
|
||||
name: '开始迁移',
|
||||
disabled: r.status === "success"?true:false,
|
||||
},
|
||||
{
|
||||
key: 'destroy',
|
||||
name: '停止迁移',
|
||||
disabled: r.status === "waiting"?false:true,
|
||||
},
|
||||
{
|
||||
key: 'reboot',
|
||||
name: '重启机器',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<TableDropdown
|
||||
onSelect={async (key, e) => {
|
||||
switch (key) {
|
||||
case 'start':
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
startMigrateIp: r.ip,
|
||||
allMoveVisible: true,
|
||||
},
|
||||
})
|
||||
break;
|
||||
case 'destroy':
|
||||
Modal.confirm({
|
||||
title: (
|
||||
<span style={{ fontWeight: 'normal', fontSize: 14 }}>
|
||||
确定要停止迁移吗?
|
||||
</span>
|
||||
),
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const destroyHide = message.loading('正在停止迁移...', 0);
|
||||
try {
|
||||
const { code } = await stopMigration({
|
||||
ip: r.ip,
|
||||
});
|
||||
if (code === 200) {
|
||||
try {
|
||||
const {
|
||||
code: queryCode,
|
||||
data: nodeList,
|
||||
} = await getNodesList({ id: activeMachineGroupId });
|
||||
if (queryCode === 200) {
|
||||
message.success('停止迁移成功');
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineList: nodeList,
|
||||
nodeTotal: nodeList ? nodeList.length : 0,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log(`更新数据获取失败,错误信息:${e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
message.error('停止迁移失败');
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
destroyHide();
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
case 'reboot':
|
||||
Modal.confirm({
|
||||
title: (
|
||||
<span style={{ fontWeight: 'normal', fontSize: 14 }}>
|
||||
确定要重启机器吗?
|
||||
</span>
|
||||
),
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const rebootHide = message.loading('正在重启机器...', 0);
|
||||
try {
|
||||
const { code } = await rebootMachine({
|
||||
ip: r.ip,
|
||||
});
|
||||
if (code === 200) {
|
||||
try {
|
||||
const {
|
||||
code: queryCode,
|
||||
data: nodeList,
|
||||
} = await getNodesList({ id: activeMachineGroupId });
|
||||
if (queryCode === 200) {
|
||||
message.success('重启机器成功');
|
||||
console.log(nodeList,"nodeList")
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineList: nodeList,
|
||||
nodeTotal: nodeList ? nodeList.length : 0,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log(`更新数据获取失败,错误信息:${e}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
message.error('重启机器失败');
|
||||
return false;
|
||||
} catch (error) {
|
||||
return false;
|
||||
} finally {
|
||||
rebootHide();
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
menus={menus}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleMachineName = async (r) => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
machineDetailLoading: true,
|
||||
},
|
||||
});
|
||||
const hide = message.loading('loading...', 0);
|
||||
|
||||
Promise.all([
|
||||
getMachineInfo(r.ip),
|
||||
getLog(r.ip),
|
||||
getReport(r.ip),
|
||||
]).then((res) => {
|
||||
console.log(res,'jaja')
|
||||
const [
|
||||
{ data: systemMessage },
|
||||
{ data: logtMessage },
|
||||
{ data: reportMessage },
|
||||
] = res;
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
tableIp: r.ip,
|
||||
tableIpVersion: r.version ? `${r.ip} (${r.version})` : `${r.ip}`,
|
||||
machineDetailLoading: false,
|
||||
},
|
||||
});
|
||||
hide();
|
||||
}).catch((error)=>{
|
||||
hide();
|
||||
console.log(error,'error')
|
||||
})
|
||||
// Promise.all([
|
||||
// qyeryMachineInfo({ ip: r.ip}),
|
||||
// qyeryLog({ ip: r.ip }),
|
||||
// qyeryReport({ ip: r.ip }),
|
||||
// ]).then((res) => {
|
||||
// console.log(res,'jaja')
|
||||
// const [
|
||||
// { data: systemMessage },
|
||||
// { data: logtMessage },
|
||||
// { data: reportMessage },
|
||||
// ] = res;
|
||||
// dispatch({
|
||||
// type: SET_DATA,
|
||||
// payload: {
|
||||
// systemMessage,
|
||||
// logtMessage,
|
||||
// reportMessage,
|
||||
// tableIp: r.ip,
|
||||
// tableIpVersion: r.version ? `${r.ip} (${r.version})` : `${r.ip}`,
|
||||
// machineDetailLoading: false,
|
||||
// },
|
||||
// });
|
||||
// hide();
|
||||
// }).catch((error)=>{
|
||||
// hide();
|
||||
// console.log(error,'error')
|
||||
// })
|
||||
|
||||
// dispatch({
|
||||
// type: SET_DATA,
|
||||
// payload: {
|
||||
// tableIp: r.ip,
|
||||
// tableIpVersion: r.version ? `${r.ip} (${r.version})` : `${r.ip}`,
|
||||
// machineDetailLoading: false,
|
||||
// },
|
||||
// });
|
||||
// hide();
|
||||
}
|
||||
|
||||
const getMachineInfo = async (ip) => {
|
||||
try {
|
||||
const { code,data } = await qyeryMachineInfo({ ip });
|
||||
if (code === 200) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
systemMessage: data,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
systemMessage: {},
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const getLog = async (ip) => {
|
||||
try {
|
||||
const { code,data } = await qyeryLog({ ip });
|
||||
if (code === 200) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
logtMessage: data,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
logtMessage: '',
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const getReport = async (ip) => {
|
||||
try {
|
||||
const { code,data } = await qyeryReport({ ip });
|
||||
if (code === 200) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
reportMessage: data,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
reportMessage: {},
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.nodes}>
|
||||
<Card
|
||||
bordered={false}
|
||||
title={
|
||||
<span>
|
||||
<em>机器列表</em>
|
||||
<i>( {nodeTotal}台 )</i>
|
||||
</span>
|
||||
}
|
||||
extra={
|
||||
<>
|
||||
<Button
|
||||
style={{ marginRight: 20 }}
|
||||
type="primary"
|
||||
ghost
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: SET_DATA,
|
||||
payload: {
|
||||
startMigrateIp: '',
|
||||
allMoveVisible: true,
|
||||
},
|
||||
})
|
||||
}}
|
||||
>
|
||||
批量迁移
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Skeleton loading={machineTableLoading}>
|
||||
<ProTable
|
||||
rowKey="ip"
|
||||
tableLayout="fixed"
|
||||
size="small"
|
||||
rowClassName={() => 'node-row'}
|
||||
search={false}
|
||||
pagination={false}
|
||||
tableAlertRender={false}
|
||||
toolBarRender={false}
|
||||
options={false}
|
||||
columns={columns}
|
||||
dataSource={machineList}
|
||||
onRow={(r) => ({
|
||||
})}
|
||||
/>
|
||||
</Skeleton>
|
||||
{/* <Skeleton loading={loadingVisible} />
|
||||
<Skeleton loading={loadingVisible} />
|
||||
<Skeleton loading={loadingVisible} /> */}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
|
@ -0,0 +1,143 @@
|
|||
|
||||
.nodes {
|
||||
background-color: #141414;
|
||||
border-radius: 4px;
|
||||
.numGreen {
|
||||
:global {
|
||||
.ant-progress-text {
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
}
|
||||
.numRed {
|
||||
:global {
|
||||
.ant-progress-text {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
}
|
||||
.numZero {
|
||||
:global {
|
||||
.ant-progress-text {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.ant-card-extra {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ant-card {
|
||||
background-color: #141414;
|
||||
border-radius: 4px;
|
||||
.ant-card-head {
|
||||
// padding: 0 15px;
|
||||
padding-right: 24px;
|
||||
color: #fff;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
.ant-card-head-title {
|
||||
padding: 19px 0;
|
||||
em {
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
}
|
||||
i {
|
||||
font-weight: normal;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
.ant-pro-table {
|
||||
margin: 10px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 30px;
|
||||
background-color: #141414;
|
||||
.ant-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
background-color: #333 !important;
|
||||
.ant-dropdown-menu-item {
|
||||
color: #fff;
|
||||
}
|
||||
.ant-dropdown-menu-item:last-child {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.ant-dropdown-menu-item:hover,
|
||||
.ant-dropdown-menu-submenu-title:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-table-row .ant-table-cell:first-child {
|
||||
padding-right: 0;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.ant-table-content {
|
||||
background-color: #141414;
|
||||
tr:first-child > th:first-child {
|
||||
padding-right: 0;
|
||||
padding-left: 5px;
|
||||
}
|
||||
.ant-table-thead > tr > th {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background-color: #141414;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
}
|
||||
.ant-table-tbody > tr > td {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: #141414;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
.ant-typography {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
// .ant-pro-table-dropdown {
|
||||
// .anticon {
|
||||
// color: #389e0d;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.node-row:hover > td {
|
||||
background-color: #2a2a2a !important;
|
||||
}
|
||||
.dashboard-operate-node.ant-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
background-color: #333;
|
||||
.ant-dropdown-menu-item {
|
||||
color: #fff;
|
||||
}
|
||||
.ant-dropdown-menu-item:hover,
|
||||
.ant-dropdown-menu-submenu-title:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
// overlayClassName 失效
|
||||
.ant-dropdown-menu {
|
||||
background-color: #333;
|
||||
.ant-dropdown-menu-item {
|
||||
color: #fff;
|
||||
}
|
||||
.ant-dropdown-menu-item-disabled {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.ant-dropdown-menu-item:hover,
|
||||
.ant-dropdown-menu-submenu-title:hover {
|
||||
background-color: #1a1a1a;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
// 常量: actionTypes
|
||||
export const SET_DATA = 'SET_DATA';
|
|
@ -0,0 +1,58 @@
|
|||
import React, { useEffect, useReducer, createContext } from 'react';
|
||||
import { withRouter } from 'umi';
|
||||
import rootReducer from './reducers';
|
||||
// import { SET_DATA } from './constants';
|
||||
// import { qyeryMachineInfo, qyeryLog, qyeryReport,getBannerList } from '../../service';
|
||||
|
||||
// 初始化全量数据(全部的分类、应用数据)
|
||||
const initState = {
|
||||
loadingVisible: true,
|
||||
machineTableLoading: true,
|
||||
machineDetailLoading: true,
|
||||
// 机器总数
|
||||
nodeTotal: 0,
|
||||
// 待迁移机器数
|
||||
abnormalNodeTotal: 0,
|
||||
isFocused: false,
|
||||
isDefault: false,
|
||||
// 机器信息
|
||||
systemMessage: {},
|
||||
// 迁移日志
|
||||
logtMessage: '',
|
||||
// 迁移报告
|
||||
reportMessage: {},
|
||||
// 当前展示的机器ip(系统版本)
|
||||
tableIpVersion: '',
|
||||
// 当前展示的机器ip
|
||||
tableIp: '',
|
||||
// 批量迁移
|
||||
allMoveVisible: false,
|
||||
// 机器组列表
|
||||
machineGroupsList: [],
|
||||
// 机器列表
|
||||
machineList:[],
|
||||
// 选中机器组id
|
||||
activeMachineGroupId: '',
|
||||
// 开始迁移弹窗机器ip
|
||||
startMigrateIp: '',
|
||||
};
|
||||
|
||||
// 创建需要共享全量数据的 Context
|
||||
export const WrapperContext = createContext();
|
||||
|
||||
export default withRouter((props) => {
|
||||
|
||||
// 使用 useReducer 取代 useState 集中管理数据
|
||||
const [state, dispatch] = useReducer(rootReducer, initState);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// 使用 Provider 提供 Context 的值,Provider所包含的子树都可以直接访问 Context 的值
|
||||
return (
|
||||
<WrapperContext.Provider value={{ state, dispatch }}>{props.children}</WrapperContext.Provider>
|
||||
);
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import * as types from './constants';
|
||||
|
||||
// 全量数据变化的处理逻辑 reducer函数
|
||||
export default (state, { type, payload }) => {
|
||||
switch (type) {
|
||||
case types.SET_DATA:
|
||||
return {
|
||||
...state,
|
||||
...payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Row, Col } from "antd";
|
||||
// import { connect } from 'dva';
|
||||
import "./index.less";
|
||||
import Wrapper from './containers';
|
||||
import Banner from './components/Banner';
|
||||
import Nodes from './components/Nodes';
|
||||
import Message from './components/Message';
|
||||
import AllMoveModel from './components/AllMove';
|
||||
|
||||
const migrate = (props) => {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Wrapper>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 15 }}>
|
||||
<Col span={24}>
|
||||
<Banner />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Nodes />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={16} style={{ overflow: 'auto' }}>
|
||||
<Message />
|
||||
</Col>
|
||||
</Row>
|
||||
<AllMoveModel />
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default migrate;
|
|
@ -0,0 +1,79 @@
|
|||
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
min-height: calc(100vh - 48px);
|
||||
padding: 20px 25px 10px 25px;
|
||||
background-color: #000;
|
||||
:global {
|
||||
.ant-skeleton-content {
|
||||
padding: 20px;
|
||||
padding-bottom: 6px;
|
||||
.ant-skeleton-title {
|
||||
height: 12px;
|
||||
margin-top: 0;
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
.ant-skeleton-paragraph {
|
||||
margin-top: 12px;
|
||||
li {
|
||||
height: 10px;
|
||||
margin-top: 10px;
|
||||
background-color: #2a2a2a;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ant-empty {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.updateModal {
|
||||
.ant-modal-body {
|
||||
padding: 20px 30px;
|
||||
background-color: #fff;
|
||||
.ant-descriptions {
|
||||
margin-bottom: 24px;
|
||||
.ant-descriptions-item-label {
|
||||
width: 20%;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
.ant-descriptions-item-content {
|
||||
padding: 4px 16px;
|
||||
.ant-input-disabled {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
background-color: #fff;
|
||||
border: none;
|
||||
cursor: text;
|
||||
}
|
||||
.ant-select-disabled {
|
||||
.ant-select-selection-item {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
.ant-select-selector {
|
||||
cursor: text;
|
||||
.ant-select-selection-search-input {
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.switch-is-default {
|
||||
width: 300px;
|
||||
.ant-popover-title {
|
||||
padding: 20px 20px 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.ant-popover-inner-content {
|
||||
padding: 0 20px 20px 40px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { request } from 'umi';
|
||||
import { stringify } from 'qs';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
/* *************** 操作系统迁移--迁移实施 *************** */
|
||||
|
||||
// 迁移机器组列表
|
||||
export async function getBannerList(params) {
|
||||
return request(`/api/v1/migration/group/`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 机器列表
|
||||
export async function getNodesList(params) { //id=1
|
||||
return request(`/api/v1/implementation/list/?${stringify(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 机器信息
|
||||
export async function qyeryMachineInfo(params) {
|
||||
return request(`/api/v1/implementation/info/?${stringify(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 迁移日志
|
||||
export async function qyeryLog(params) {
|
||||
return request(`/api/v1/implementation/log/?${stringify(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 迁移报告
|
||||
export async function qyeryReport(params) {
|
||||
return request(`/api/v1/implementation/report/?${stringify(params)}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
}
|
||||
|
||||
// 停止迁移
|
||||
export async function stopMigration(params) {
|
||||
return request(`/api/v1/implementation/stop/`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
// 批量迁移 & 开始迁移
|
||||
export async function BulkMigration(params) {
|
||||
return request(`/api/v1/implementation/migrate/`, {
|
||||
method: 'POST',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
// 重启机器
|
||||
export async function rebootMachine(params) {
|
||||
return request(`/api/v1/implementation/reboot/`, {
|
||||
method: 'post',
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>
|
||||
图标
|
||||
</title>
|
||||
<g id="OS迁移平台-UI-Beta1.0备份" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="0机器组详情-弹窗位置备份" transform="translate(-487.000000, -95.000000)">
|
||||
<g id="展示-带图标" transform="translate(486.000000, 92.000000)">
|
||||
<g id="图标" transform="translate(1.000000, 3.000000)">
|
||||
<rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="0" y="0" width="16" height="16">
|
||||
</rect>
|
||||
<rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="2" y="2" width="11.9882927" height="11.9882927">
|
||||
</rect>
|
||||
<path d="M10.7276376,5.34227177 C10.9818537,5.08939372 10.9818537,4.67729616 10.7276376,4.42441812 L9.79239024,3.48917073 L9.79372823,3.49050871 C9.5462021,3.24298258 9.12072475,2.81750523 8.51863415,2.21675262 C8.23498258,1.93979094 7.77605575,1.94246691 7.49508014,2.22344251 L2.22611847,7.48839024 C2.08996122,7.62362369 2.01339073,7.80759494 2.01339073,7.99949826 C2.01339073,8.19140157 2.08996122,8.37537283 2.22611847,8.51060628 L7.49374216,13.774216 C7.77653032,14.0565208 8.23450801,14.0565208 8.51729616,13.774216 L10.7262997,11.5665505 C10.9805157,11.3136725 10.9805157,10.9015749 10.7262997,10.6486969 C10.4721014,10.395765 10.0613062,10.395765 9.80710802,10.6486969 L8.13463415,12.3238467 C8.06505924,12.3934216 7.95668293,12.3934216 7.88710802,12.3238467 L3.67916376,8.11991637 C3.60958885,8.05034146 3.60958885,7.94196516 3.67916376,7.87239024 L7.88577003,3.66845993 C7.89112195,3.66310802 7.89781184,3.65909408 7.90316376,3.65374216 C7.97273867,3.59888502 8.06907317,3.60423694 8.13329616,3.66845993 L9.808446,5.34227177 C10.062662,5.5964878 10.4747596,5.5964878 10.7276376,5.34227177 Z M6.61067595,8.02625784 C6.61067595,8.5339086 6.88175941,9.00299725 7.32181183,9.25682264 C7.76186426,9.51064802 8.30403121,9.51064802 8.74408363,9.25682264 C9.18413606,9.00299725 9.45521951,8.5339086 9.45521951,8.02625784 C9.45521951,7.24149776 8.81844675,6.60532403 8.03294773,6.60532403 C7.24744872,6.60532403 6.61067595,7.24149776 6.61067595,8.02625784 L6.61067595,8.02625784 Z M13.7849199,7.5097979 L12.1405436,5.87344948 C11.8863275,5.62057143 11.47423,5.62057143 11.2213519,5.87478746 C11.0992832,5.99631438 11.03066,6.16146576 11.03066,6.33371429 C11.03066,6.50596282 11.0992832,6.6711142 11.2213519,6.79264112 L12.3318746,7.90182579 C12.4014495,7.9714007 12.4014495,8.07977701 12.3318746,8.14935192 L11.2374077,9.24248083 C11.115339,9.36400775 11.0467158,9.52915913 11.0467158,9.70140766 C11.0467158,9.87365619 11.115339,10.0388076 11.2374077,10.1603345 C11.4916059,10.4132663 11.9024011,10.4132663 12.1565993,10.1603345 L13.7862578,8.53201394 C13.9219406,8.3964443 13.9980647,8.21243242 13.9978143,8.02062814 C13.9975626,7.82882386 13.9209571,7.64501189 13.7849199,7.5097979 L13.7849199,7.5097979 Z" id="形状" fill-opacity="1" fill="#1890ff">
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>待迁移机器图标</title>
|
||||
<g id="OS迁移平台-UI-Beta1.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="OS迁移平台-Dashboard" transform="translate(-1030.000000, -224.000000)" fill="#FF4D4F" fill-rule="nonzero">
|
||||
<path d="M1042,232.678571 L1034,232.678571 C1033.84286,232.678571 1033.71429,232.807143 1033.71429,232.964286 L1033.71429,236.107143 C1033.71429,236.264286 1033.84286,236.392857 1034,236.392857 L1042,236.392857 C1042.15714,236.392857 1042.28571,236.264286 1042.28571,236.107143 L1042.28571,232.964286 C1042.28571,232.807143 1042.15714,232.678571 1042,232.678571 Z M1041,235.107143 L1035,235.107143 L1035,233.964286 L1041,233.964286 L1041,235.107143 Z M1044.71429,237.428571 C1044.00357,237.428571 1043.42857,238.003571 1043.42857,238.714286 C1043.42857,239.425 1044.00357,240 1044.71429,240 C1045.425,240 1046,239.425 1046,238.714286 C1046,238.003571 1045.425,237.428571 1044.71429,237.428571 Z M1044.71429,239.142857 C1044.47679,239.142857 1044.28571,238.951786 1044.28571,238.714286 C1044.28571,238.476786 1044.47679,238.285714 1044.71429,238.285714 C1044.95179,238.285714 1045.14286,238.476786 1045.14286,238.714286 C1045.14286,238.951786 1044.95179,239.142857 1044.71429,239.142857 Z M1042,227.607143 L1034,227.607143 C1033.84286,227.607143 1033.71429,227.735714 1033.71429,227.892857 L1033.71429,231.035714 C1033.71429,231.192857 1033.84286,231.321429 1034,231.321429 L1042,231.321429 C1042.15714,231.321429 1042.28571,231.192857 1042.28571,231.035714 L1042.28571,227.892857 C1042.28571,227.735714 1042.15714,227.607143 1042,227.607143 Z M1041,230.035714 L1035,230.035714 L1035,228.892857 L1041,228.892857 L1041,230.035714 Z M1044.71429,226.571429 C1045.425,226.571429 1046,225.996429 1046,225.285714 C1046,224.575 1045.425,224 1044.71429,224 C1044.00357,224 1043.42857,224.575 1043.42857,225.285714 C1043.42857,225.996429 1044.00357,226.571429 1044.71429,226.571429 Z M1044.71429,224.857143 C1044.95179,224.857143 1045.14286,225.048214 1045.14286,225.285714 C1045.14286,225.523214 1044.95179,225.714286 1044.71429,225.714286 C1044.47679,225.714286 1044.28571,225.523214 1044.28571,225.285714 C1044.28571,225.048214 1044.47679,224.857143 1044.71429,224.857143 Z M1031.28571,224 C1030.575,224 1030,224.575 1030,225.285714 C1030,225.996429 1030.575,226.571429 1031.28571,226.571429 C1031.99643,226.571429 1032.57143,225.996429 1032.57143,225.285714 C1032.57143,224.575 1031.99643,224 1031.28571,224 Z M1031.28571,225.714286 C1031.04821,225.714286 1030.85714,225.523214 1030.85714,225.285714 C1030.85714,225.048214 1031.04821,224.857143 1031.28571,224.857143 C1031.52321,224.857143 1031.71429,225.048214 1031.71429,225.285714 C1031.71429,225.523214 1031.52321,225.714286 1031.28571,225.714286 Z M1031.28571,237.428571 C1030.575,237.428571 1030,238.003571 1030,238.714286 C1030,239.425 1030.575,240 1031.28571,240 C1031.99643,240 1032.57143,239.425 1032.57143,238.714286 C1032.57143,238.003571 1031.99643,237.428571 1031.28571,237.428571 Z M1031.28571,239.142857 C1031.04821,239.142857 1030.85714,238.951786 1030.85714,238.714286 C1030.85714,238.476786 1031.04821,238.285714 1031.28571,238.285714 C1031.52321,238.285714 1031.71429,238.476786 1031.71429,238.714286 C1031.71429,238.951786 1031.52321,239.142857 1031.28571,239.142857 Z" id="待迁移机器图标"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg t="1659606311721" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="25834" width="20" height="20">
|
||||
<path d="M169 169v686h686V169H169z m640 46v146H215V215h594zM215 407h145.8v402H215V407z m191.8 402V407H809v402H406.8z" p-id="25835" fill="#1890ff">
|
||||
</path>
|
||||
</svg>
|
After Width: | Height: | Size: 310 B |
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>图标</title>
|
||||
<g id="OS迁移平台-UI-Beta1.0备份" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="0机器组详情-弹窗位置备份" transform="translate(-487.000000, -95.000000)">
|
||||
<g id="展示-带图标" transform="translate(486.000000, 92.000000)">
|
||||
<g id="图标" transform="translate(1.000000, 3.000000)">
|
||||
<rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="0" y="0" width="16" height="16"></rect>
|
||||
<rect id="矩形" fill="#000000" fill-rule="nonzero" opacity="0" x="2" y="2" width="11.9882927" height="11.9882927"></rect>
|
||||
<path d="M10.7276376,5.34227177 C10.9818537,5.08939372 10.9818537,4.67729616 10.7276376,4.42441812 L9.79239024,3.48917073 L9.79372823,3.49050871 C9.5462021,3.24298258 9.12072475,2.81750523 8.51863415,2.21675262 C8.23498258,1.93979094 7.77605575,1.94246691 7.49508014,2.22344251 L2.22611847,7.48839024 C2.08996122,7.62362369 2.01339073,7.80759494 2.01339073,7.99949826 C2.01339073,8.19140157 2.08996122,8.37537283 2.22611847,8.51060628 L7.49374216,13.774216 C7.77653032,14.0565208 8.23450801,14.0565208 8.51729616,13.774216 L10.7262997,11.5665505 C10.9805157,11.3136725 10.9805157,10.9015749 10.7262997,10.6486969 C10.4721014,10.395765 10.0613062,10.395765 9.80710802,10.6486969 L8.13463415,12.3238467 C8.06505924,12.3934216 7.95668293,12.3934216 7.88710802,12.3238467 L3.67916376,8.11991637 C3.60958885,8.05034146 3.60958885,7.94196516 3.67916376,7.87239024 L7.88577003,3.66845993 C7.89112195,3.66310802 7.89781184,3.65909408 7.90316376,3.65374216 C7.97273867,3.59888502 8.06907317,3.60423694 8.13329616,3.66845993 L9.808446,5.34227177 C10.062662,5.5964878 10.4747596,5.5964878 10.7276376,5.34227177 Z M6.61067595,8.02625784 C6.61067595,8.5339086 6.88175941,9.00299725 7.32181183,9.25682264 C7.76186426,9.51064802 8.30403121,9.51064802 8.74408363,9.25682264 C9.18413606,9.00299725 9.45521951,8.5339086 9.45521951,8.02625784 C9.45521951,7.24149776 8.81844675,6.60532403 8.03294773,6.60532403 C7.24744872,6.60532403 6.61067595,7.24149776 6.61067595,8.02625784 L6.61067595,8.02625784 Z M13.7849199,7.5097979 L12.1405436,5.87344948 C11.8863275,5.62057143 11.47423,5.62057143 11.2213519,5.87478746 C11.0992832,5.99631438 11.03066,6.16146576 11.03066,6.33371429 C11.03066,6.50596282 11.0992832,6.6711142 11.2213519,6.79264112 L12.3318746,7.90182579 C12.4014495,7.9714007 12.4014495,8.07977701 12.3318746,8.14935192 L11.2374077,9.24248083 C11.115339,9.36400775 11.0467158,9.52915913 11.0467158,9.70140766 C11.0467158,9.87365619 11.115339,10.0388076 11.2374077,10.1603345 C11.4916059,10.4132663 11.9024011,10.4132663 12.1565993,10.1603345 L13.7862578,8.53201394 C13.9219406,8.3964443 13.9980647,8.21243242 13.9978143,8.02062814 C13.9975626,7.82882386 13.9209571,7.64501189 13.7849199,7.5097979 L13.7849199,7.5097979 Z" id="形状" fill-opacity="0.65" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
Loading…
Reference in New Issue