快速开始
在项目根目录下打开终端(包含package.json的目录)
# 导入项目所需依赖
npm install
# 访问项目
npm run dev
# 打包项目
npm run build
目录结构分析
fateboard基于vue2,使用了vue生态中vuex、vue-router、element-ui等插件
src目录下,核心目录:
- views:包括了不同的页面组件
- components:页面组件中用到的重复的组件
- utils:封装了一些公共的方法,其中request.js导出了一个axios实例,访问服务器的具体路径需要在此处修改
- api:通过Promise封装了所需要的api请求函数,函数内都用到了utils工具包下的request.js,为保持项目代码风格的一致性,后续开发的所有ajax请求也需要在此处定义
- store:vuex目录,用于管理不同组件的公共状态,后续开发也需要使用
- router:定义了路由,需要在开发时根据需要更改
- style:使用sass定义了一些样式
src目录下,核心文件:
- main.js:项目的入口文件,导入相关插件
- App.js:根组件
- App.css(自己创建的):全局样式
开发流程
1. 创建页面组件
2. 更改路由
首先打开 src/router
下的 index.js
文件
根据二次开发的需求,我们把最初默认跳转到 running 目录改成默认跳转到我们之前定义的联邦训练页面,并且暂时注释了默认的路由守卫(用于鉴权,默认返回到登陆页面)
export const constantRouterMap = [
{
path: '/',
component: Layout,
redirect: '/federalTrain',
name: 'Dashboard',
hidden: true,
children: [
{
path: '/federalTrain',
redirect: '/federalTrain/chosePartner',
component: () => import('@/views/federal-train'),
children: [{
path: '/federalTrain/chosePartner',
component: () => import('@/views/federal-train/chose-partner')
}, {
path: '/federalTrain/choseParams-normal',
component: () => import('@/views/federal-train/chose-params-normal')
}, {
path: '/federalTrain/choseParams-advance',
component: () => import('@/views/federal-train/chose-params-advance')
}
]
},
........
]
},
// { path: '/login', component: () => import('@/views/job-login/index'), hidden: true },
{ path: '/404', component: () => import('@/views/404'), hidden: true },
{ path: '*', redirect: '/404', hidden: true }
]
const router = new Router({
// mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap
})
// let preUrl = null
// router.beforeEach((to, from, next) => {
// if (getLocal('CurrentUser')) {
// if (preUrl) {
// const mid = preUrl
// preUrl = null
// next(mid)
// } else if (to.path === '/history') {
// if (from.query.page || from.params.page) {
// to.params.page = from.query.page
// to.params.search_job_id = from.query.search_job_id
// to.params.search_party_id = from.query.search_party_id
// to.params.search_role = from.query.search_role
// to.params.search_status = from.query.search_status
// }
// // console.log(to, from)
// next()
// } else if (to.name === 'login') {
// next({
// name: 'RUNNINNG'
// })
// } else {
// next()
// }
// } else if (to.name !== 'login') {
// if (!preUrl) {
// preUrl = Object.assign({}, to)
// }
// next({
// name: 'login'
// })
// } else {
// if (from.name !== 'login') {
// if (!preUrl) {
// preUrl = Object.assign({}, from)
// }
// }
// next()
// }
// })
export default router
3. 相关api开发
打开 src/api
目录,新建一个 federalTrain.js
用于封装我们自定义的ajax请求
具体代码如下,由于request是axios的一个实例,其中封装了bashurl,我们在指定访问路径时只需要指定path部分
import request from '@/utils/request'
export function submitAssignment(data) {
return request({
url: '/v1/client/submit/job/general',
method: 'post',
data
})
}
export function submitAssignmentByFile(data) {
return request({
url: '/v1/client/submit/job/high',
method: 'post',
data
})
}
4. 导航栏二次开发
打开 src/views/layout/components
目录下的 Navbar.vue
更改导航
根据 style
标签下的导入找到样式文件,src/styles/common/layout/navbar.scss
根据 scss 规则,为刚刚添加的导航定义样式
.middle-router-btns {
align-self: flex-start;
justify-self: start;
margin-left: 28px;
> span {
margin: 0 8px;
padding: 0 15px;
$h: 28px;
color: rgba(255, 255, 255, 0.6);
height: $h;
line-height: $h;
border-radius: $h;
cursor: pointer;
&:hover {
color: #fff;
}
}
.active {
color: #fff;
}
}
5. 联邦训练组件开发
根据第一步和第二步,我们已经可以访问到我们自定义的页面组件了,并且根据第4步,我们更是可以通过导航栏访问到我们自定义的页面
接下来我们要具体开发联邦训练组件
- 定义框架
首先我们要根据之前定义的二级路由,创建相应的 steps
组件和添加用于显示具体内容的 route -view
,用于配置联邦训练参与者和相关参数的页面组件都会显示在route -view
中
最终效果:
其中的卡片效果在全局样式 App.css
下
代码实现:
index.vue
<template>
<div class="site-layout-content">
<el-steps :active="stepActive" style="height: 10%">
<el-step icon="el-icon-upload" title="任务方选择"/>
<el-step icon="el-icon-download" title="参数配置"/>
</el-steps>
<div class="content-container" style="height: 90%">
<router-view @stepActive="changeStep"/>
</div>
</div>
</template>
<script>
export default {
data() {
return {
stepActive: 1
}
},
methods: {
changeStep(step) {
this.stepActive = step
}
}
}
</script>
<style scoped>
.content-container {
width: 100%;
overflow-y: scroll;
}
</style>
全局样式
/* 全局卡片样式 */
.site-layout-content {
padding: 24px;
border-radius: 7px;
-moz-border-radius: 7px;
-webkit-box-shadow: 0 20px 15px #9B7468;
-moz-box-shadow: 0 20px 15px #9B7468;
box-shadow: 0 8px 8px rgba(96, 185, 234, .24), 0 0 8px rgba(96, 185, 234, .12);
text-decoration: none;
background: #fff;
height: 88vh;
width: 90vw;
margin-top: 3vh;
}
- 编写参与方组件
我们需要指定联邦训练的参与方,以及用于训练的数据源和命名空间
代码实现:
<template>
<div>
<el-form ref="form" :inline="true" :model="form">
<!-- 单行数据 -->
<el-row v-for="(item, index) in form.items" :key="index" type="flex" justify="space-around">
<el-form-item
:prop="'items.'+index+'.role'"
:rules="[{ required: true, message: '请输入角色类型'}]"
label="角色类型:">
<el-input v-model="item.role" readonly/>
</el-form-item>
<el-form-item
:prop="'items.'+index+'.id'"
:rules="[{ required: true, message: '请输入成员ID' }]"
label="成员ID:">
<el-input v-model="item.id"/>
</el-form-item>
<el-form-item
:prop="'items.'+index+'.tableName'"
:rules="[{ required: true, message: '请输入数据表名称' }]"
label="数据表名称:">
<el-input v-model="item.tableName"/>
</el-form-item>
<el-form-item
:prop="'items.'+index+'.nameSpace'"
:rules="[{ required: true, message: '请输入命名空间' }]"
label="命名空间:">
<el-input v-model="item.nameSpace"/>
</el-form-item>
</el-row>
<el-row type="flex" justify="center">
<el-form-item>
<el-button
type="primary"
style="width: 83vw"
@click="addPartner"
>新增参与方
</el-button
>
</el-form-item>
</el-row>
</el-form>
<el-row style="margin: 5vh 10vw;" type="flex" justify="space-around">
<el-button type="info" plain round @click="resetForm">重置</el-button>
<el-button type="primary" plain round @click="toNext">继续</el-button>
</el-row>
</div>
</template>
<script>
import { mapMutations } from '_vuex@3.0.1@vuex'
export default {
data() {
return {
form: {
items: [
{ role: '主导方', id: '', tableName: '', nameSpace: '' },
{ role: '参与方', id: '', tableName: '', nameSpace: '' }
]
}
}
},
mounted() {
this.$emit('stepActive', 1)
},
methods: {
addPartner() {
this.form.items.push({
role: '参与方',
id: '',
tableName: '',
nameSpace: ''
})
},
resetForm() {
this.$refs.form.resetFields()
this.form.items = this.form.items.slice(0, 2)
},
toNext() {
this.$refs.form.validate((valid) => {
if (valid) {
const guest = {
party_id: this.form.items[0].id,
table_name: this.form.items[0].tableName,
namespace: this.form.items[0].nameSpace
}
const host = this.form.items.slice(1).map(item => ({
party_id: item.id,
table_name: item.tableName,
namespace: item.nameSpace
}))
this.UPDATE_PARTNER({ host, guest })
this.$router.push('/federalTrain/choseParams-normal')
} else {
console.log('error submit!!')
return false
}
})
},
...mapMutations('federalTrain', ['UPDATE_PARTNER'])
}
}
</script>
- 编写根据参数训练和根据配置文件训练的组件
3.1 根据参数训练
代码实现
<template>
<div>
<div class="form-container">
<el-form
ref="form"
:rules="rules"
:model="form"
label-position="right"
size="small"
style="width: 50vw"
label-width="15vw">
<el-form-item prop="assignmentName" label="任务名称:">
<el-input v-model="form.assignmentName" class="form-input"/>
</el-form-item>
<el-form-item :rules="[{ required: true, message: '请输入任务类型'}]" prop="assignmentType" label="任务类型:">
<el-radio-group v-model="form.assignmentType">
<el-radio :label="0">单机</el-radio>
<el-radio :label="1">多机</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="algorithmName" label="采用算法:">
<el-input v-model="form.algorithmName" class="form-input" readonly/>
</el-form-item>
<el-form-item prop="algorithmParams" label="算法参数:">
<el-input v-model="form.algorithmParams" class="form-input" type="textarea"/>
</el-form-item>
<el-form-item prop="isScale" label="isScale:">
<el-radio-group v-model="form.isScale">
<el-radio :label="true">是</el-radio>
<el-radio :label="false">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item prop="percent" label="测试数据集百分比(%):">
<el-input v-model.number="form.percent" class="form-input" max="1" min="0" type="number" step="0.1"/>
</el-form-item>
<el-form-item prop="postScript" label="备注信息:">
<el-input v-model="form.postScript" class="form-input" type="textarea"/>
</el-form-item>
<el-form-item>
<el-link style="color:blue" @click="$router.push('/federalTrain/choseParams-advance')">高级</el-link>
</el-form-item>
</el-form>
</div>
<el-row style="margin: 5vh 10vw;" type="flex" justify="space-around">
<el-button type="info" plain round @click="toLast">返回</el-button>
<el-button :loading="submitLoading" type="primary" plain round @click="handleFinish">提交</el-button>
</el-row>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { submitAssignment } from '../../../api/federalTrain'
export default {
data() {
const algorithmValidator = (rules, value, callback) => {
if (!value || value === '') {
callback()
}
const jsonVal = '{' + value + '}'
try {
if (typeof JSON.parse(jsonVal) === 'object') {
this.paramType = 0
callback()
}
} catch (e) {
try {
if (typeof JSON.parse(value) === 'object') {
this.paramType = 1
callback()
}
} catch (e2) {
console.log(2222)
callback(new Error())
}
}
callback(new Error())
}
return {
form: {
assignmentName: '',
assignmentType: 0,
algorithmName: 'hetero_lr',
algorithmParams: '',
isScale: true,
percent: null,
postScript: '',
paramType: 0
},
submitLoading: false,
rules: {
assignmentName: [{ required: true, message: '请输入任务名称' }],
assignmentType: [{ required: true, message: '请输入任务类型' }],
algorithmParams: [{ validator: algorithmValidator, message: '算法参数格式不正确' }],
percent: [{ required: true, message: '请输入测试数据百分比' }, { type: 'number', max: 1, min: 0, message: '必须在0和1之间' }]
}
}
},
computed: {
...mapState('federalTrain', ['guest', 'host'])
},
mounted() {
this.$emit('stepActive', 2)
if (this.guest === null || this.host === null) {
this.$router.push('/federalTrain/chosePartner')
}
},
methods: {
toLast() {
this.$router.push('/federalTrain/chosePartner')
},
handleFinish() {
this.$refs.form.validate((valid) => {
if (valid) {
this.submitLoading = true
let algorithmParams
if (this.paramType === 0) {
algorithmParams = this.form.algorithmParams
? JSON.parse('{' + this.form.algorithmParams + '}')
: {}
} else {
algorithmParams = this.form.algorithmParams
? JSON.parse(this.form.algorithmParams)
: {}
}
const { form } = this
const data = {
algorithm_parameters: algorithmParams,
guest: this.guest,
host: this.host,
isScale: form.isScale,
job_description: form.postScript,
job_name: form.assignmentName,
test_size: form.percent,
train_algorithm_name: form.algorithmName,
work_mode: form.assignmentType,
config_type: 0
}
console.log(data)
submitAssignment(data).then((res) => {
console.log(res)
}, (err) => {
console.log(err)
}).finally(() => {
this.submitLoading = false
})
// this.$router.push('/running')
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
<style scoped>
.form-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.form-input {
width: 35vw;
}
</style>
3.2 根据配置文件训练
代码实现
<template>
<div>
<div class="form-container">
<el-form
ref="form"
:rules="rules"
:model="form"
label-position="right"
size="small"
style="width: 50vw"
label-width="15vw">
<el-form-item prop="train_name" label="任务名称:">
<el-input v-model="form.train_name" class="form-input" readonly/>
</el-form-item>
<el-form-item prop="config_file" label="config_file">
<el-upload
:limit="1"
:auto-upload="true"
:before-upload="resetConfigFile"
:multiple="false"
:file-list="form.config_file"
action="#">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button size="small" type="primary">下载模版</el-button>
</el-upload>
</el-form-item>
<el-form-item prop="dsl_file" label="dsl_file">
<el-upload
:limit="1"
:auto-upload="true"
:before-upload="resetDslFile"
:multiple="false"
:file-list="form.dsl_file"
action="#">
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button size="small" type="primary">下载模版</el-button>
</el-upload>
</el-form-item>
<el-form-item>
<el-link style="color:blue" @click="$router.push('/federalTrain/choseParams-normal')">普通</el-link>
</el-form-item>
</el-form>
<el-row style="position: absolute;bottom: 35vh;width: 50vw" type="flex" justify="space-around">
<el-button type="info" plain round @click="toLast">返回</el-button>
<el-button :loading="submitLoading" type="primary" plain round @click="handleFinish">提交</el-button>
</el-row>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { submitAssignmentByFile } from '../../../api/federalTrain'
export default {
data() {
return {
form: {
train_name: 'homo_logistic_regression',
config_file: [],
dsl_file: []
},
submitLoading: false,
rules: {
train_name: [{ required: true, message: '请输入任务名称' }]
}
}
},
computed: {
...mapState('federalTrain', ['guest', 'host'])
},
mounted() {
this.$emit('stepActive', 2)
if (this.guest === null || this.host === null) {
this.$router.push('/federalTrain/chosePartner')
}
},
methods: {
toLast() {
this.$router.push('/federalTrain/chosePartner')
},
resetConfigFile(file) {
this.form.config_file = [file]
return false
},
resetDslFile(file) {
this.form.dsl_file = [file]
return false
},
handleFinish() {
console.log(this.form)
if (this.form.config_file.length === 0 || this.form.dsl_file.length === 0) {
this.$message({ message: '请选择config文件和dsl文件', type: 'warning' })
return
}
this.$refs.form.validate((valid) => {
if (valid) {
this.submitLoading = true
const { form } = this
console.log(form)
const formData = new FormData()
formData.append('train_algorithm_name', form.train_name)
formData.append('config_type', 1)
formData.append('config_file', form.config_file[0])
formData.append('dsl_file', form.dsl_file[0])
submitAssignmentByFile(formData).then((res) => {
console.log(res)
}, (err) => {
console.log(err)
}).finally(() => {
this.submitLoading = false
})
// this.$router.push('/running')
} else {
console.log('error submit!!')
return false
}
})
}
}
}
</script>
<style scoped>
.form-container {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.form-input {
width: 20vw;
}
</style>