| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- <template>
- <div class="schedule-layout">
- <div class="schedule-left">
- <el-row style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
- <h4>项目概览</h4>
- </el-row>
- <el-card shadow="never" class='description-card'>
- <el-row>
- <el-col :span="12">
- <div class="description-card-text" style="font-size: 18px;font-weight: bold">{{selectedProject.name}}</div>
- <div class="description-card-text">项目地址:
- <span>
- <el-link
- target="_blank"
- type="primary"
- @click.prevent="copy(selectedProject.gitRemoteUrl)"
- >
- <a :href="selectedProject.gitRemoteUrl" class="url">{{selectedProject.gitRemoteUrl}}</a>
- </el-link>
- </span>
- </div>
- <div class="description-card-text">项目介绍:
- <span>{{selectedProject.description}}</span>
- </div>
- <div class="description-card-text">项目成员:
- <span>
- <el-tag v-for="item in projectMembers" :key="item.id" style="margin: 0 5px;">
- {{item.username}}
- </el-tag>
- </span>
- </div>
- </el-col>
- <el-col :span="6">
- <div style="height: 140px;width: 140px" id="requirement_pie"></div>
- </el-col>
- <el-col :span="6">
- <div style="height: 140px;width: 140px" id="bug_pie"></div>
- </el-col>
- </el-row>
- </el-card>
- <el-row style="display: flex;flex-direction: row;align-items: center;margin-bottom: 10px;color: #333333">
- <h4>项目规划</h4>
- <ElTooltip
- class="tooltip"
- effect="dark"
- :content="'点击查看需求规划四层说明'"
- placement="right-end"
- >
- <i class="el-icon-question" @click="getDetails()" style="cursor: pointer"></i>
- </ElTooltip>
- </el-row>
- <Tree
- :data="requirements"
- v-loading="loading"
- @change="handleTreeChange"
- @nodeClick="handleNodeClick"
- ref="tree"
- ></Tree>
- <RequirementDrawer
- ref="drawer"
- v-bind="requirementInDrawer"
- @change="handleDrawerContentChange"
- @requirement-update="getTreeData"
- ></RequirementDrawer>
- <ElDrawer
- v-model="detailDrawer"
- :size="600">
- <template #title>
- <el-row>
- <el-col :span="8">
- <span style="font-weight: bold; font-size: 18px; color: black">需求规划四层说明</span>
- </el-col>
- </el-row>
- </template>
- <ElTable :data="details" width="100%">
- <ElTableColumn
- prop="type"
- label="类型"
- align="center"
- width="150">
- </ElTableColumn>
- <ElTableColumn
- prop="explain"
- label="说明"
- align="left"
- header-align="center">
- </ElTableColumn>
- </ElTable>
- <el-row>
- <el-col :span="8">
- <span style="font-weight: bold; font-size: 18px; color: black; padding: 20px 20px 20px; margin-top: 20px">举例</span>
- </el-col>
- </el-row>
- <img src="@/assets/example.png" style="height: 400px; width: 100%">
- </ElDrawer>
- </div>
- <div class="schedule-right">
- <div class="chat-header">AI 需求助手</div>
- <iframe
- class="chat-iframe"
- :src="chatUrl"
- frameborder="0"
- allow="microphone"
- ></iframe>
- </div>
- </div>
- </template>
- <script>
- import Tree from '@/components/Tree.vue';
- import * as echarts from 'echarts';
- import { BugListAPI, RequirementAPI } from '@/api';
- import RequirementDrawer from '@/components/RequirementDrawer.vue';
- import { copy } from '@/utils/clipboard';
- import { useProjectList } from '../../composable/useProject';
- export default {
- name: 'Schedule',
- components: { RequirementDrawer, Tree },
- setup() {
- const { selectedProject } = useProjectList();
- return {
- selectedProject,
- };
- },
- data() {
- return {
- chatUrl: 'https://dify.seec.seecoder.cn/chatbot/h8mBtz4IQMSuT3xm',
- loading: false,
- requirements: [],
- showRequirementDrawer: false,
- addRequirementDrawer: false,
- parentOfAddingRequirement: null,
- requirementInDrawer: {},
- detailDrawer: false,
- details: [
- {
- type: '战略举措(Epic)',
- explain: '● 项目中的关键战略举措,可以是重大的业务方向,也可以是重大的技术演进\n'
- + '● 这里需求的粒度较大,需要继续分解为Feature;并通过Feature继续细化为Story来完成开发和迭代。',
- },
- {
- type: '特性(Feature)',
- explain: '● 特性是可以给用户带来价值的产品功能或者特性。\n'
- + '● 特性向上承接Feature,向下分解为Story。相比需求,Feature更加具体形象,用户更容易理解。\n',
- },
- {
- type: '用户故事(Story)',
- explain: '● 用户故事是从用户角度对产品需求的详细描述,具有更小的粒度。\n'
- + '● 一般是从不同用户出发,对同一个大粒度需求的不同希望。\n',
- },
- {
- type: '任务(Task)',
- explain: '● 在书写完用户故事后,需要将Story指派给具体成员,并分解为一个或多个任务(Task)。\n'
- + '● 需要填写预计工时',
- },
- ],
- };
- },
- computed: {
- projectId() {
- return this.$route.params.projectId;
- },
- projectMembers() {
- return this.$store.state.workspace.currentProjectMembers;
- },
- },
- watch: {
- $route: {
- async handler() {
- // 当从schedule页点击Devcloud Logo直接返回主页时,
- // 该handler也会触发,因此进行判断,不进入下面逻辑
- if (this.$route.name === 'Home') {
- return;
- }
- await this.getTreeData();
- this.drawChart();
- },
- immediate: true,
- },
- },
- methods: {
- copy,
- async getTreeData() {
- this.loading = true;
- try {
- this.requirements = this.addChild2Node(await RequirementAPI.getTreeNodesByProjectId(this.projectId));
- } catch (e) {
- console.error(e);
- }
- this.loading = false;
- },
- addChild2Node(array) {
- const nodeMap = new Map();
- array.forEach((item) => nodeMap.set(item.id, item));
- array.forEach((item) => {
- if (item.parentId) {
- const tempParent = nodeMap.get(item.parentId);
- if (!tempParent.childs) {
- tempParent.childs = [];
- }
- tempParent.childs.push(item);
- }
- });
- // 返回前序遍历的列表
- const newArr = [];
- const preOrder = (node) => {
- newArr.push(node);
- if (!node.childs || node.childs.length === 0) return;
- node.childs.sort((node1, node2) => node1.id - node2.id);
- newArr.concat(node.childs);
- node.childs.forEach((_node) => preOrder(_node));
- };
- const root = array.find((item) => !item.parentId);
- preOrder(root);
- return newArr;
- },
- async handleTreeChange(msg) {
- const { type, id } = msg;
- await RequirementAPI[type](id);
- await this.getTreeData();
- },
- handleNodeClick(id) {
- this.$refs.drawer.show();
- this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
- },
- handleDrawerContentChange(id) {
- this.requirementInDrawer = Array.prototype.find.call(this.requirements, (requirement) => requirement.id === id);
- },
- async drawChart() {
- const bugList = await BugListAPI.getBugListByProjectId(this.projectId);
- const requirementsValuesMap = new Map();
- const requirementsValues = [];
- const bugListValuesMap = new Map();
- const bugListValues = [];
- this.requirements.forEach((item) => {
- if (item.type === '任务') {
- if (!requirementsValuesMap.get(item.state)) requirementsValuesMap.set(item.state, 0);
- requirementsValuesMap.set(item.state, requirementsValuesMap.get(item.state) + 1);
- }
- });
- requirementsValuesMap.forEach((value, key) => {
- requirementsValues.push(
- { value, name: key },
- );
- });
- bugList.forEach((item) => {
- if (!bugListValuesMap.get(item.state)) bugListValuesMap.set(item.state, 0);
- bugListValuesMap.set(item.state, bugListValuesMap.get(item.state) + 1);
- });
- bugListValuesMap.forEach((value, key) => {
- bugListValues.push(
- { value, name: key },
- );
- });
- if (bugListValues.length === 0) {
- bugListValues.push({
- name: '无记录', value: 0,
- });
- }
- if (requirementsValues.length === 0) {
- requirementsValues.push({
- name: '无记录', value: 0,
- });
- }
- const requirementPie = echarts.init(document.getElementById('requirement_pie'));
- requirementPie.setOption({
- title: {
- text: '任务完成情况',
- left: 'center',
- textStyle: {
- fontSize: 12,
- },
- },
- tooltip: {
- trigger: 'item',
- },
- series: [
- {
- name: '任务情况',
- type: 'pie',
- radius: ['40%', '70%'],
- avoidLabelOverlap: false,
- label: {
- show: false,
- position: 'center',
- },
- emphasis: {
- label: {
- show: true,
- fontSize: '10',
- fontWeight: 'bold',
- },
- },
- labelLine: {
- show: false,
- },
- data: requirementsValues,
- },
- ],
- });
- const bugPie = echarts.init(document.getElementById('bug_pie'));
- bugPie.setOption({
- title: {
- text: 'Bug处理情况',
- left: 'center',
- textStyle: {
- fontSize: 12,
- },
- },
- tooltip: {
- trigger: 'item',
- },
- series: [
- {
- name: 'Bug情况',
- type: 'pie',
- radius: ['40%', '70%'],
- avoidLabelOverlap: false,
- label: {
- show: false,
- position: 'center',
- },
- emphasis: {
- label: {
- show: true,
- fontSize: '10',
- fontWeight: 'bold',
- },
- },
- labelLine: {
- show: false,
- },
- data: bugListValues,
- },
- ],
- });
- },
- getDetails() {
- this.detailDrawer = true;
- },
- },
- };
- </script>
- <style>
- .description-card{
- margin: 10px 0;
- }
- .description-card-text{
- margin: 5px 0;
- }
- .url {
- color: dodgerblue;
- text-decoration: none;
- }
- .el-drawer__body{
- overflow: auto;
- }
- .el-table .cell{
- white-space: pre-line;
- }
- .schedule-layout {
- display: flex;
- gap: 16px;
- height: calc(100vh - 120px);
- }
- .schedule-left {
- flex: 1;
- min-width: 0;
- overflow-y: auto;
- }
- .schedule-right {
- width: 400px;
- min-width: 400px;
- display: flex;
- flex-direction: column;
- border: 1px solid #e4e7ed;
- border-radius: 8px;
- background: #fff;
- overflow: hidden;
- height: 100%;
- }
- .chat-header {
- padding: 12px 16px;
- font-size: 16px;
- font-weight: bold;
- color: #333;
- border-bottom: 1px solid #e4e7ed;
- background: #f8f9fa;
- flex-shrink: 0;
- }
- .chat-iframe {
- flex: 1;
- width: 100%;
- height: 100%;
- border: none;
- }
- </style>
|