Schedule.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. <template>
  2. <div class="schedule-layout">
  3. <div class="schedule-left">
  4. <el-row style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
  5. <h4>项目概览</h4>
  6. </el-row>
  7. <el-card shadow="never" class='description-card'>
  8. <el-row>
  9. <el-col :span="12">
  10. <div class="description-card-text" style="font-size: 18px;font-weight: bold">{{selectedProject.name}}</div>
  11. <div class="description-card-text">项目地址:
  12. <span>
  13. <el-link
  14. target="_blank"
  15. type="primary"
  16. @click.prevent="copy(selectedProject.gitRemoteUrl)"
  17. >
  18. <a :href="selectedProject.gitRemoteUrl" class="url">{{selectedProject.gitRemoteUrl}}</a>
  19. </el-link>
  20. </span>
  21. </div>
  22. <div class="description-card-text">项目介绍:
  23. <span>{{selectedProject.description}}</span>
  24. </div>
  25. <div class="description-card-text">项目成员:
  26. <span>
  27. <el-tag v-for="item in projectMembers" :key="item.id" style="margin: 0 5px;">
  28. {{item.username}}
  29. </el-tag>
  30. </span>
  31. </div>
  32. </el-col>
  33. <el-col :span="6">
  34. <div style="height: 140px;width: 140px" id="requirement_pie"></div>
  35. </el-col>
  36. <el-col :span="6">
  37. <div style="height: 140px;width: 140px" id="bug_pie"></div>
  38. </el-col>
  39. </el-row>
  40. </el-card>
  41. <el-row style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
  42. <h4>项目规划</h4>
  43. <ElTooltip
  44. class="tooltip"
  45. effect="dark"
  46. :content="'点击查看需求规划四层说明'"
  47. placement="right-end"
  48. >
  49. <i class="el-icon-question" @click="getDetails()" style="cursor: pointer"></i>
  50. </ElTooltip>
  51. </el-row>
  52. <Tree
  53. :data="requirements"
  54. v-loading="loading"
  55. @change="handleTreeChange"
  56. @nodeClick="handleNodeClick"
  57. ref="tree"
  58. ></Tree>
  59. <RequirementDrawer
  60. ref="drawer"
  61. v-bind="requirementInDrawer"
  62. @change="handleDrawerContentChange"
  63. @requirement-update="getTreeData"
  64. ></RequirementDrawer>
  65. <ElDrawer
  66. v-model="detailDrawer"
  67. :size="600">
  68. <template #title>
  69. <el-row>
  70. <el-col :span="8">
  71. <span style="font-weight: bold; font-size: 18px; color: black">需求规划四层说明</span>
  72. </el-col>
  73. </el-row>
  74. </template>
  75. <ElTable :data="details" width="100%">
  76. <ElTableColumn
  77. prop="type"
  78. label="类型"
  79. align="center"
  80. width="150">
  81. </ElTableColumn>
  82. <ElTableColumn
  83. prop="explain"
  84. label="说明"
  85. align="left"
  86. header-align="center">
  87. </ElTableColumn>
  88. </ElTable>
  89. <el-row>
  90. <el-col :span="8">
  91. <span style="font-weight: bold; font-size: 18px; color: black; padding: 20px 20px 20px; margin-top: 20px">举例</span>
  92. </el-col>
  93. </el-row>
  94. <img src="@/assets/example.png" style="height: 400px; width: 100%">
  95. </ElDrawer>
  96. </div>
  97. <div class="schedule-right">
  98. <div class="chat-header">AI 需求助手</div>
  99. <iframe
  100. class="chat-iframe"
  101. :src="chatUrl"
  102. frameborder="0"
  103. allow="microphone"
  104. ></iframe>
  105. </div>
  106. </div>
  107. </template>
  108. <script>
  109. import Tree from '@/components/Tree.vue';
  110. import * as echarts from 'echarts';
  111. import { BugListAPI, RequirementAPI } from '@/api';
  112. import RequirementDrawer from '@/components/RequirementDrawer.vue';
  113. import { copy } from '@/utils/clipboard';
  114. import { useProjectList } from '../../composable/useProject';
  115. export default {
  116. name: 'Schedule',
  117. components: { RequirementDrawer, Tree },
  118. setup() {
  119. const { selectedProject } = useProjectList();
  120. return {
  121. selectedProject,
  122. };
  123. },
  124. data() {
  125. return {
  126. chatUrl: 'https://dify.seec.seecoder.cn/chatbot/h8mBtz4IQMSuT3xm',
  127. loading: false,
  128. requirements: [],
  129. showRequirementDrawer: false,
  130. addRequirementDrawer: false,
  131. parentOfAddingRequirement: null,
  132. requirementInDrawer: {},
  133. detailDrawer: false,
  134. details: [
  135. {
  136. type: '战略举措(Epic)',
  137. explain: '● 项目中的关键战略举措,可以是重大的业务方向,也可以是重大的技术演进\n'
  138. + '● 这里需求的粒度较大,需要继续分解为Feature;并通过Feature继续细化为Story来完成开发和迭代。',
  139. },
  140. {
  141. type: '特性(Feature)',
  142. explain: '● 特性是可以给用户带来价值的产品功能或者特性。\n'
  143. + '● 特性向上承接Feature,向下分解为Story。相比需求,Feature更加具体形象,用户更容易理解。\n',
  144. },
  145. {
  146. type: '用户故事(Story)',
  147. explain: '● 用户故事是从用户角度对产品需求的详细描述,具有更小的粒度。\n'
  148. + '● 一般是从不同用户出发,对同一个大粒度需求的不同希望。\n',
  149. },
  150. {
  151. type: '任务(Task)',
  152. explain: '● 在书写完用户故事后,需要将Story指派给具体成员,并分解为一个或多个任务(Task)。\n'
  153. + '● 需要填写预计工时',
  154. },
  155. ],
  156. };
  157. },
  158. computed: {
  159. projectId() {
  160. return this.$route.params.projectId;
  161. },
  162. projectMembers() {
  163. return this.$store.state.workspace.currentProjectMembers;
  164. },
  165. },
  166. watch: {
  167. $route: {
  168. async handler() {
  169. // 当从schedule页点击Devcloud Logo直接返回主页时,
  170. // 该handler也会触发,因此进行判断,不进入下面逻辑
  171. if (this.$route.name === 'Home') {
  172. return;
  173. }
  174. await this.getTreeData();
  175. this.drawChart();
  176. },
  177. immediate: true,
  178. },
  179. },
  180. methods: {
  181. copy,
  182. async getTreeData() {
  183. this.loading = true;
  184. try {
  185. this.requirements = this.addChild2Node(await RequirementAPI.getTreeNodesByProjectId(this.projectId));
  186. } catch (e) {
  187. console.error(e);
  188. }
  189. this.loading = false;
  190. },
  191. addChild2Node(array) {
  192. const nodeMap = new Map();
  193. array.forEach((item) => nodeMap.set(item.id, item));
  194. array.forEach((item) => {
  195. if (item.parentId) {
  196. const tempParent = nodeMap.get(item.parentId);
  197. if (!tempParent.childs) {
  198. tempParent.childs = [];
  199. }
  200. tempParent.childs.push(item);
  201. }
  202. });
  203. // 返回前序遍历的列表
  204. const newArr = [];
  205. const preOrder = (node) => {
  206. newArr.push(node);
  207. if (!node.childs || node.childs.length === 0) return;
  208. node.childs.sort((node1, node2) => node1.id - node2.id);
  209. newArr.concat(node.childs);
  210. node.childs.forEach((_node) => preOrder(_node));
  211. };
  212. const root = array.find((item) => !item.parentId);
  213. preOrder(root);
  214. return newArr;
  215. },
  216. async handleTreeChange(msg) {
  217. const { type, id } = msg;
  218. await RequirementAPI[type](id);
  219. await this.getTreeData();
  220. },
  221. handleNodeClick(id) {
  222. this.$refs.drawer.show();
  223. this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
  224. },
  225. handleDrawerContentChange(id) {
  226. this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
  227. },
  228. async drawChart() {
  229. const bugList = await BugListAPI.getBugListByProjectId(this.projectId);
  230. const requirementsValuesMap = new Map();
  231. const requirementsValues = [];
  232. const bugListValuesMap = new Map();
  233. const bugListValues = [];
  234. this.requirements.forEach((item) => {
  235. if (item.type === '任务') {
  236. if (!requirementsValuesMap.get(item.state)) requirementsValuesMap.set(item.state, 0);
  237. requirementsValuesMap.set(item.state, requirementsValuesMap.get(item.state) + 1);
  238. }
  239. });
  240. requirementsValuesMap.forEach((value, key) => {
  241. requirementsValues.push(
  242. { value, name: key },
  243. );
  244. });
  245. bugList.forEach((item) => {
  246. if (!bugListValuesMap.get(item.state)) bugListValuesMap.set(item.state, 0);
  247. bugListValuesMap.set(item.state, bugListValuesMap.get(item.state) + 1);
  248. });
  249. bugListValuesMap.forEach((value, key) => {
  250. bugListValues.push(
  251. { value, name: key },
  252. );
  253. });
  254. if (bugListValues.length === 0) {
  255. bugListValues.push({
  256. name: '无记录', value: 0,
  257. });
  258. }
  259. if (requirementsValues.length === 0) {
  260. requirementsValues.push({
  261. name: '无记录', value: 0,
  262. });
  263. }
  264. const requirementPie = echarts.init(document.getElementById('requirement_pie'));
  265. requirementPie.setOption({
  266. title: {
  267. text: '任务完成情况',
  268. left: 'center',
  269. textStyle: {
  270. fontSize: 12,
  271. },
  272. },
  273. tooltip: {
  274. trigger: 'item',
  275. },
  276. series: [
  277. {
  278. name: '任务情况',
  279. type: 'pie',
  280. radius: ['40%', '70%'],
  281. avoidLabelOverlap: false,
  282. label: {
  283. show: false,
  284. position: 'center',
  285. },
  286. emphasis: {
  287. label: {
  288. show: true,
  289. fontSize: '10',
  290. fontWeight: 'bold',
  291. },
  292. },
  293. labelLine: {
  294. show: false,
  295. },
  296. data: requirementsValues,
  297. },
  298. ],
  299. });
  300. const bugPie = echarts.init(document.getElementById('bug_pie'));
  301. bugPie.setOption({
  302. title: {
  303. text: 'Bug处理情况',
  304. left: 'center',
  305. textStyle: {
  306. fontSize: 12,
  307. },
  308. },
  309. tooltip: {
  310. trigger: 'item',
  311. },
  312. series: [
  313. {
  314. name: 'Bug情况',
  315. type: 'pie',
  316. radius: ['40%', '70%'],
  317. avoidLabelOverlap: false,
  318. label: {
  319. show: false,
  320. position: 'center',
  321. },
  322. emphasis: {
  323. label: {
  324. show: true,
  325. fontSize: '10',
  326. fontWeight: 'bold',
  327. },
  328. },
  329. labelLine: {
  330. show: false,
  331. },
  332. data: bugListValues,
  333. },
  334. ],
  335. });
  336. },
  337. getDetails() {
  338. this.detailDrawer = true;
  339. },
  340. },
  341. };
  342. </script>
  343. <style>
  344. .description-card{
  345. margin: 10px 0;
  346. }
  347. .description-card-text{
  348. margin: 5px 0;
  349. }
  350. .url {
  351. color: dodgerblue;
  352. text-decoration: none;
  353. }
  354. .el-drawer__body{
  355. overflow: auto;
  356. }
  357. .el-table .cell{
  358. white-space: pre-line;
  359. }
  360. .schedule-layout {
  361. display: flex;
  362. gap: 16px;
  363. height: calc(100vh - 120px);
  364. }
  365. .schedule-left {
  366. flex: 1;
  367. min-width: 0;
  368. overflow-y: auto;
  369. }
  370. .schedule-right {
  371. width: 400px;
  372. min-width: 400px;
  373. display: flex;
  374. flex-direction: column;
  375. border: 1px solid #e4e7ed;
  376. border-radius: 8px;
  377. background: #fff;
  378. overflow: hidden;
  379. height: 100%;
  380. }
  381. .chat-header {
  382. padding: 12px 16px;
  383. font-size: 16px;
  384. font-weight: bold;
  385. color: #333;
  386. border-bottom: 1px solid #e4e7ed;
  387. background: #f8f9fa;
  388. flex-shrink: 0;
  389. }
  390. .chat-iframe {
  391. flex: 1;
  392. width: 100%;
  393. height: 100%;
  394. border: none;
  395. }
  396. </style>