快速开始

在项目根目录下打开终端(包含package.json的目录)

  1. # 导入项目所需依赖
  2. npm install
  3. # 访问项目
  4. npm run dev
  5. # 打包项目
  6. 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(自己创建的):全局样式

image.png

开发流程

1. 创建页面组件

src/views 下创建目录及页面组件
image.png

2. 更改路由

首先打开 src/router 下的 index.js 文件
根据二次开发的需求,我们把最初默认跳转到 running 目录改成默认跳转到我们之前定义的联邦训练页面,并且暂时注释了默认的路由守卫(用于鉴权,默认返回到登陆页面)

  1. export const constantRouterMap = [
  2. {
  3. path: '/',
  4. component: Layout,
  5. redirect: '/federalTrain',
  6. name: 'Dashboard',
  7. hidden: true,
  8. children: [
  9. {
  10. path: '/federalTrain',
  11. redirect: '/federalTrain/chosePartner',
  12. component: () => import('@/views/federal-train'),
  13. children: [{
  14. path: '/federalTrain/chosePartner',
  15. component: () => import('@/views/federal-train/chose-partner')
  16. }, {
  17. path: '/federalTrain/choseParams-normal',
  18. component: () => import('@/views/federal-train/chose-params-normal')
  19. }, {
  20. path: '/federalTrain/choseParams-advance',
  21. component: () => import('@/views/federal-train/chose-params-advance')
  22. }
  23. ]
  24. },
  25. ........
  26. ]
  27. },
  28. // { path: '/login', component: () => import('@/views/job-login/index'), hidden: true },
  29. { path: '/404', component: () => import('@/views/404'), hidden: true },
  30. { path: '*', redirect: '/404', hidden: true }
  31. ]
  32. const router = new Router({
  33. // mode: 'history',
  34. scrollBehavior: () => ({ y: 0 }),
  35. routes: constantRouterMap
  36. })
  37. // let preUrl = null
  38. // router.beforeEach((to, from, next) => {
  39. // if (getLocal('CurrentUser')) {
  40. // if (preUrl) {
  41. // const mid = preUrl
  42. // preUrl = null
  43. // next(mid)
  44. // } else if (to.path === '/history') {
  45. // if (from.query.page || from.params.page) {
  46. // to.params.page = from.query.page
  47. // to.params.search_job_id = from.query.search_job_id
  48. // to.params.search_party_id = from.query.search_party_id
  49. // to.params.search_role = from.query.search_role
  50. // to.params.search_status = from.query.search_status
  51. // }
  52. // // console.log(to, from)
  53. // next()
  54. // } else if (to.name === 'login') {
  55. // next({
  56. // name: 'RUNNINNG'
  57. // })
  58. // } else {
  59. // next()
  60. // }
  61. // } else if (to.name !== 'login') {
  62. // if (!preUrl) {
  63. // preUrl = Object.assign({}, to)
  64. // }
  65. // next({
  66. // name: 'login'
  67. // })
  68. // } else {
  69. // if (from.name !== 'login') {
  70. // if (!preUrl) {
  71. // preUrl = Object.assign({}, from)
  72. // }
  73. // }
  74. // next()
  75. // }
  76. // })
  77. export default router

3. 相关api开发

打开 src/api 目录,新建一个 federalTrain.js 用于封装我们自定义的ajax请求
image.png
具体代码如下,由于request是axios的一个实例,其中封装了bashurl,我们在指定访问路径时只需要指定path部分

  1. import request from '@/utils/request'
  2. export function submitAssignment(data) {
  3. return request({
  4. url: '/v1/client/submit/job/general',
  5. method: 'post',
  6. data
  7. })
  8. }
  9. export function submitAssignmentByFile(data) {
  10. return request({
  11. url: '/v1/client/submit/job/high',
  12. method: 'post',
  13. data
  14. })
  15. }

分别封装了通过参数提交训练任务和通过文件提交训练任务

4. 导航栏二次开发

打开 src/views/layout/components 目录下的 Navbar.vue

更改导航
image.png
根据 style 标签下的导入找到样式文件,src/styles/common/layout/navbar.scss
image.png
根据 scss 规则,为刚刚添加的导航定义样式

  1. .middle-router-btns {
  2. align-self: flex-start;
  3. justify-self: start;
  4. margin-left: 28px;
  5. > span {
  6. margin: 0 8px;
  7. padding: 0 15px;
  8. $h: 28px;
  9. color: rgba(255, 255, 255, 0.6);
  10. height: $h;
  11. line-height: $h;
  12. border-radius: $h;
  13. cursor: pointer;
  14. &:hover {
  15. color: #fff;
  16. }
  17. }
  18. .active {
  19. color: #fff;
  20. }
  21. }

最终效果,可以根据需求添加更过样式
image.png

5. 联邦训练组件开发

根据第一步和第二步,我们已经可以访问到我们自定义的页面组件了,并且根据第4步,我们更是可以通过导航栏访问到我们自定义的页面
接下来我们要具体开发联邦训练组件

  1. 定义框架

首先我们要根据之前定义的二级路由,创建相应的 steps 组件和添加用于显示具体内容的 route -view ,用于配置联邦训练参与者和相关参数的页面组件都会显示在route -view

最终效果:
image.png
其中的卡片效果在全局样式 App.css

代码实现:
index.vue

  1. <template>
  2. <div class="site-layout-content">
  3. <el-steps :active="stepActive" style="height: 10%">
  4. <el-step icon="el-icon-upload" title="任务方选择"/>
  5. <el-step icon="el-icon-download" title="参数配置"/>
  6. </el-steps>
  7. <div class="content-container" style="height: 90%">
  8. <router-view @stepActive="changeStep"/>
  9. </div>
  10. </div>
  11. </template>
  12. <script>
  13. export default {
  14. data() {
  15. return {
  16. stepActive: 1
  17. }
  18. },
  19. methods: {
  20. changeStep(step) {
  21. this.stepActive = step
  22. }
  23. }
  24. }
  25. </script>
  26. <style scoped>
  27. .content-container {
  28. width: 100%;
  29. overflow-y: scroll;
  30. }
  31. </style>

全局样式

  1. /* 全局卡片样式 */
  2. .site-layout-content {
  3. padding: 24px;
  4. border-radius: 7px;
  5. -moz-border-radius: 7px;
  6. -webkit-box-shadow: 0 20px 15px #9B7468;
  7. -moz-box-shadow: 0 20px 15px #9B7468;
  8. box-shadow: 0 8px 8px rgba(96, 185, 234, .24), 0 0 8px rgba(96, 185, 234, .12);
  9. text-decoration: none;
  10. background: #fff;
  11. height: 88vh;
  12. width: 90vw;
  13. margin-top: 3vh;
  14. }
  1. 编写参与方组件

我们需要指定联邦训练的参与方,以及用于训练的数据源和命名空间
image.png

代码实现:

  1. <template>
  2. <div>
  3. <el-form ref="form" :inline="true" :model="form">
  4. <!-- 单行数据 -->
  5. <el-row v-for="(item, index) in form.items" :key="index" type="flex" justify="space-around">
  6. <el-form-item
  7. :prop="'items.'+index+'.role'"
  8. :rules="[{ required: true, message: '请输入角色类型'}]"
  9. label="角色类型:">
  10. <el-input v-model="item.role" readonly/>
  11. </el-form-item>
  12. <el-form-item
  13. :prop="'items.'+index+'.id'"
  14. :rules="[{ required: true, message: '请输入成员ID' }]"
  15. label="成员ID:">
  16. <el-input v-model="item.id"/>
  17. </el-form-item>
  18. <el-form-item
  19. :prop="'items.'+index+'.tableName'"
  20. :rules="[{ required: true, message: '请输入数据表名称' }]"
  21. label="数据表名称:">
  22. <el-input v-model="item.tableName"/>
  23. </el-form-item>
  24. <el-form-item
  25. :prop="'items.'+index+'.nameSpace'"
  26. :rules="[{ required: true, message: '请输入命名空间' }]"
  27. label="命名空间:">
  28. <el-input v-model="item.nameSpace"/>
  29. </el-form-item>
  30. </el-row>
  31. <el-row type="flex" justify="center">
  32. <el-form-item>
  33. <el-button
  34. type="primary"
  35. style="width: 83vw"
  36. @click="addPartner"
  37. >新增参与方
  38. </el-button
  39. >
  40. </el-form-item>
  41. </el-row>
  42. </el-form>
  43. <el-row style="margin: 5vh 10vw;" type="flex" justify="space-around">
  44. <el-button type="info" plain round @click="resetForm">重置</el-button>
  45. <el-button type="primary" plain round @click="toNext">继续</el-button>
  46. </el-row>
  47. </div>
  48. </template>
  49. <script>
  50. import { mapMutations } from '_vuex@3.0.1@vuex'
  51. export default {
  52. data() {
  53. return {
  54. form: {
  55. items: [
  56. { role: '主导方', id: '', tableName: '', nameSpace: '' },
  57. { role: '参与方', id: '', tableName: '', nameSpace: '' }
  58. ]
  59. }
  60. }
  61. },
  62. mounted() {
  63. this.$emit('stepActive', 1)
  64. },
  65. methods: {
  66. addPartner() {
  67. this.form.items.push({
  68. role: '参与方',
  69. id: '',
  70. tableName: '',
  71. nameSpace: ''
  72. })
  73. },
  74. resetForm() {
  75. this.$refs.form.resetFields()
  76. this.form.items = this.form.items.slice(0, 2)
  77. },
  78. toNext() {
  79. this.$refs.form.validate((valid) => {
  80. if (valid) {
  81. const guest = {
  82. party_id: this.form.items[0].id,
  83. table_name: this.form.items[0].tableName,
  84. namespace: this.form.items[0].nameSpace
  85. }
  86. const host = this.form.items.slice(1).map(item => ({
  87. party_id: item.id,
  88. table_name: item.tableName,
  89. namespace: item.nameSpace
  90. }))
  91. this.UPDATE_PARTNER({ host, guest })
  92. this.$router.push('/federalTrain/choseParams-normal')
  93. } else {
  94. console.log('error submit!!')
  95. return false
  96. }
  97. })
  98. },
  99. ...mapMutations('federalTrain', ['UPDATE_PARTNER'])
  100. }
  101. }
  102. </script>
  1. 编写根据参数训练和根据配置文件训练的组件

3.1 根据参数训练

image.png
代码实现

  1. <template>
  2. <div>
  3. <div class="form-container">
  4. <el-form
  5. ref="form"
  6. :rules="rules"
  7. :model="form"
  8. label-position="right"
  9. size="small"
  10. style="width: 50vw"
  11. label-width="15vw">
  12. <el-form-item prop="assignmentName" label="任务名称:">
  13. <el-input v-model="form.assignmentName" class="form-input"/>
  14. </el-form-item>
  15. <el-form-item :rules="[{ required: true, message: '请输入任务类型'}]" prop="assignmentType" label="任务类型:">
  16. <el-radio-group v-model="form.assignmentType">
  17. <el-radio :label="0">单机</el-radio>
  18. <el-radio :label="1">多机</el-radio>
  19. </el-radio-group>
  20. </el-form-item>
  21. <el-form-item prop="algorithmName" label="采用算法:">
  22. <el-input v-model="form.algorithmName" class="form-input" readonly/>
  23. </el-form-item>
  24. <el-form-item prop="algorithmParams" label="算法参数:">
  25. <el-input v-model="form.algorithmParams" class="form-input" type="textarea"/>
  26. </el-form-item>
  27. <el-form-item prop="isScale" label="isScale:">
  28. <el-radio-group v-model="form.isScale">
  29. <el-radio :label="true"></el-radio>
  30. <el-radio :label="false"></el-radio>
  31. </el-radio-group>
  32. </el-form-item>
  33. <el-form-item prop="percent" label="测试数据集百分比(%):">
  34. <el-input v-model.number="form.percent" class="form-input" max="1" min="0" type="number" step="0.1"/>
  35. </el-form-item>
  36. <el-form-item prop="postScript" label="备注信息:">
  37. <el-input v-model="form.postScript" class="form-input" type="textarea"/>
  38. </el-form-item>
  39. <el-form-item>
  40. <el-link style="color:blue" @click="$router.push('/federalTrain/choseParams-advance')">高级</el-link>
  41. </el-form-item>
  42. </el-form>
  43. </div>
  44. <el-row style="margin: 5vh 10vw;" type="flex" justify="space-around">
  45. <el-button type="info" plain round @click="toLast">返回</el-button>
  46. <el-button :loading="submitLoading" type="primary" plain round @click="handleFinish">提交</el-button>
  47. </el-row>
  48. </div>
  49. </template>
  50. <script>
  51. import { mapState } from 'vuex'
  52. import { submitAssignment } from '../../../api/federalTrain'
  53. export default {
  54. data() {
  55. const algorithmValidator = (rules, value, callback) => {
  56. if (!value || value === '') {
  57. callback()
  58. }
  59. const jsonVal = '{' + value + '}'
  60. try {
  61. if (typeof JSON.parse(jsonVal) === 'object') {
  62. this.paramType = 0
  63. callback()
  64. }
  65. } catch (e) {
  66. try {
  67. if (typeof JSON.parse(value) === 'object') {
  68. this.paramType = 1
  69. callback()
  70. }
  71. } catch (e2) {
  72. console.log(2222)
  73. callback(new Error())
  74. }
  75. }
  76. callback(new Error())
  77. }
  78. return {
  79. form: {
  80. assignmentName: '',
  81. assignmentType: 0,
  82. algorithmName: 'hetero_lr',
  83. algorithmParams: '',
  84. isScale: true,
  85. percent: null,
  86. postScript: '',
  87. paramType: 0
  88. },
  89. submitLoading: false,
  90. rules: {
  91. assignmentName: [{ required: true, message: '请输入任务名称' }],
  92. assignmentType: [{ required: true, message: '请输入任务类型' }],
  93. algorithmParams: [{ validator: algorithmValidator, message: '算法参数格式不正确' }],
  94. percent: [{ required: true, message: '请输入测试数据百分比' }, { type: 'number', max: 1, min: 0, message: '必须在0和1之间' }]
  95. }
  96. }
  97. },
  98. computed: {
  99. ...mapState('federalTrain', ['guest', 'host'])
  100. },
  101. mounted() {
  102. this.$emit('stepActive', 2)
  103. if (this.guest === null || this.host === null) {
  104. this.$router.push('/federalTrain/chosePartner')
  105. }
  106. },
  107. methods: {
  108. toLast() {
  109. this.$router.push('/federalTrain/chosePartner')
  110. },
  111. handleFinish() {
  112. this.$refs.form.validate((valid) => {
  113. if (valid) {
  114. this.submitLoading = true
  115. let algorithmParams
  116. if (this.paramType === 0) {
  117. algorithmParams = this.form.algorithmParams
  118. ? JSON.parse('{' + this.form.algorithmParams + '}')
  119. : {}
  120. } else {
  121. algorithmParams = this.form.algorithmParams
  122. ? JSON.parse(this.form.algorithmParams)
  123. : {}
  124. }
  125. const { form } = this
  126. const data = {
  127. algorithm_parameters: algorithmParams,
  128. guest: this.guest,
  129. host: this.host,
  130. isScale: form.isScale,
  131. job_description: form.postScript,
  132. job_name: form.assignmentName,
  133. test_size: form.percent,
  134. train_algorithm_name: form.algorithmName,
  135. work_mode: form.assignmentType,
  136. config_type: 0
  137. }
  138. console.log(data)
  139. submitAssignment(data).then((res) => {
  140. console.log(res)
  141. }, (err) => {
  142. console.log(err)
  143. }).finally(() => {
  144. this.submitLoading = false
  145. })
  146. // this.$router.push('/running')
  147. } else {
  148. console.log('error submit!!')
  149. return false
  150. }
  151. })
  152. }
  153. }
  154. }
  155. </script>
  156. <style scoped>
  157. .form-container {
  158. width: 100%;
  159. display: flex;
  160. flex-direction: column;
  161. align-items: center;
  162. }
  163. .form-input {
  164. width: 35vw;
  165. }
  166. </style>

3.2 根据配置文件训练
image.png
代码实现

  1. <template>
  2. <div>
  3. <div class="form-container">
  4. <el-form
  5. ref="form"
  6. :rules="rules"
  7. :model="form"
  8. label-position="right"
  9. size="small"
  10. style="width: 50vw"
  11. label-width="15vw">
  12. <el-form-item prop="train_name" label="任务名称:">
  13. <el-input v-model="form.train_name" class="form-input" readonly/>
  14. </el-form-item>
  15. <el-form-item prop="config_file" label="config_file">
  16. <el-upload
  17. :limit="1"
  18. :auto-upload="true"
  19. :before-upload="resetConfigFile"
  20. :multiple="false"
  21. :file-list="form.config_file"
  22. action="#">
  23. <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  24. <el-button size="small" type="primary">下载模版</el-button>
  25. </el-upload>
  26. </el-form-item>
  27. <el-form-item prop="dsl_file" label="dsl_file">
  28. <el-upload
  29. :limit="1"
  30. :auto-upload="true"
  31. :before-upload="resetDslFile"
  32. :multiple="false"
  33. :file-list="form.dsl_file"
  34. action="#">
  35. <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
  36. <el-button size="small" type="primary">下载模版</el-button>
  37. </el-upload>
  38. </el-form-item>
  39. <el-form-item>
  40. <el-link style="color:blue" @click="$router.push('/federalTrain/choseParams-normal')">普通</el-link>
  41. </el-form-item>
  42. </el-form>
  43. <el-row style="position: absolute;bottom: 35vh;width: 50vw" type="flex" justify="space-around">
  44. <el-button type="info" plain round @click="toLast">返回</el-button>
  45. <el-button :loading="submitLoading" type="primary" plain round @click="handleFinish">提交</el-button>
  46. </el-row>
  47. </div>
  48. </div>
  49. </template>
  50. <script>
  51. import { mapState } from 'vuex'
  52. import { submitAssignmentByFile } from '../../../api/federalTrain'
  53. export default {
  54. data() {
  55. return {
  56. form: {
  57. train_name: 'homo_logistic_regression',
  58. config_file: [],
  59. dsl_file: []
  60. },
  61. submitLoading: false,
  62. rules: {
  63. train_name: [{ required: true, message: '请输入任务名称' }]
  64. }
  65. }
  66. },
  67. computed: {
  68. ...mapState('federalTrain', ['guest', 'host'])
  69. },
  70. mounted() {
  71. this.$emit('stepActive', 2)
  72. if (this.guest === null || this.host === null) {
  73. this.$router.push('/federalTrain/chosePartner')
  74. }
  75. },
  76. methods: {
  77. toLast() {
  78. this.$router.push('/federalTrain/chosePartner')
  79. },
  80. resetConfigFile(file) {
  81. this.form.config_file = [file]
  82. return false
  83. },
  84. resetDslFile(file) {
  85. this.form.dsl_file = [file]
  86. return false
  87. },
  88. handleFinish() {
  89. console.log(this.form)
  90. if (this.form.config_file.length === 0 || this.form.dsl_file.length === 0) {
  91. this.$message({ message: '请选择config文件和dsl文件', type: 'warning' })
  92. return
  93. }
  94. this.$refs.form.validate((valid) => {
  95. if (valid) {
  96. this.submitLoading = true
  97. const { form } = this
  98. console.log(form)
  99. const formData = new FormData()
  100. formData.append('train_algorithm_name', form.train_name)
  101. formData.append('config_type', 1)
  102. formData.append('config_file', form.config_file[0])
  103. formData.append('dsl_file', form.dsl_file[0])
  104. submitAssignmentByFile(formData).then((res) => {
  105. console.log(res)
  106. }, (err) => {
  107. console.log(err)
  108. }).finally(() => {
  109. this.submitLoading = false
  110. })
  111. // this.$router.push('/running')
  112. } else {
  113. console.log('error submit!!')
  114. return false
  115. }
  116. })
  117. }
  118. }
  119. }
  120. </script>
  121. <style scoped>
  122. .form-container {
  123. width: 100%;
  124. display: flex;
  125. flex-direction: column;
  126. align-items: center;
  127. }
  128. .form-input {
  129. width: 20vw;
  130. }
  131. </style>