feat: 新增新手任务以及新手引导功能

This commit is contained in:
lan-yonghui 2023-04-03 20:01:54 +08:00 committed by 刘瑞斌
parent c08ce30a15
commit 021ac0b711
63 changed files with 2718 additions and 10 deletions

View File

@ -0,0 +1,23 @@
package io.metersphere.base.domain;
import java.io.Serializable;
import lombok.Data;
@Data
public class NoviceStatistics implements Serializable {
private String id;
private String userId;
private Integer guideStep;
private Integer guideNum;
private Long createTime;
private Long updateTime;
private String dataOption;
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,580 @@
package io.metersphere.base.domain;
import java.util.ArrayList;
import java.util.List;
public class NoviceStatisticsExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public NoviceStatisticsExample() {
oredCriteria = new ArrayList<Criteria>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<Criterion>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(String value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(String value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(String value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(String value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(String value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(String value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdLike(String value) {
addCriterion("id like", value, "id");
return (Criteria) this;
}
public Criteria andIdNotLike(String value) {
addCriterion("id not like", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<String> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<String> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(String value1, String value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(String value1, String value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andUserIdIsNull() {
addCriterion("user_id is null");
return (Criteria) this;
}
public Criteria andUserIdIsNotNull() {
addCriterion("user_id is not null");
return (Criteria) this;
}
public Criteria andUserIdEqualTo(String value) {
addCriterion("user_id =", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdNotEqualTo(String value) {
addCriterion("user_id <>", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdGreaterThan(String value) {
addCriterion("user_id >", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdGreaterThanOrEqualTo(String value) {
addCriterion("user_id >=", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdLessThan(String value) {
addCriterion("user_id <", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdLessThanOrEqualTo(String value) {
addCriterion("user_id <=", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdLike(String value) {
addCriterion("user_id like", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdNotLike(String value) {
addCriterion("user_id not like", value, "userId");
return (Criteria) this;
}
public Criteria andUserIdIn(List<String> values) {
addCriterion("user_id in", values, "userId");
return (Criteria) this;
}
public Criteria andUserIdNotIn(List<String> values) {
addCriterion("user_id not in", values, "userId");
return (Criteria) this;
}
public Criteria andUserIdBetween(String value1, String value2) {
addCriterion("user_id between", value1, value2, "userId");
return (Criteria) this;
}
public Criteria andUserIdNotBetween(String value1, String value2) {
addCriterion("user_id not between", value1, value2, "userId");
return (Criteria) this;
}
public Criteria andGuideStepIsNull() {
addCriterion("guide_step is null");
return (Criteria) this;
}
public Criteria andGuideStepIsNotNull() {
addCriterion("guide_step is not null");
return (Criteria) this;
}
public Criteria andGuideStepEqualTo(Integer value) {
addCriterion("guide_step =", value, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepNotEqualTo(Integer value) {
addCriterion("guide_step <>", value, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepGreaterThan(Integer value) {
addCriterion("guide_step >", value, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepGreaterThanOrEqualTo(Integer value) {
addCriterion("guide_step >=", value, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepLessThan(Integer value) {
addCriterion("guide_step <", value, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepLessThanOrEqualTo(Integer value) {
addCriterion("guide_step <=", value, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepIn(List<Integer> values) {
addCriterion("guide_step in", values, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepNotIn(List<Integer> values) {
addCriterion("guide_step not in", values, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepBetween(Integer value1, Integer value2) {
addCriterion("guide_step between", value1, value2, "guideStep");
return (Criteria) this;
}
public Criteria andGuideStepNotBetween(Integer value1, Integer value2) {
addCriterion("guide_step not between", value1, value2, "guideStep");
return (Criteria) this;
}
public Criteria andGuideNumIsNull() {
addCriterion("guide_num is null");
return (Criteria) this;
}
public Criteria andGuideNumIsNotNull() {
addCriterion("guide_num is not null");
return (Criteria) this;
}
public Criteria andGuideNumEqualTo(Integer value) {
addCriterion("guide_num =", value, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumNotEqualTo(Integer value) {
addCriterion("guide_num <>", value, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumGreaterThan(Integer value) {
addCriterion("guide_num >", value, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumGreaterThanOrEqualTo(Integer value) {
addCriterion("guide_num >=", value, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumLessThan(Integer value) {
addCriterion("guide_num <", value, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumLessThanOrEqualTo(Integer value) {
addCriterion("guide_num <=", value, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumIn(List<Integer> values) {
addCriterion("guide_num in", values, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumNotIn(List<Integer> values) {
addCriterion("guide_num not in", values, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumBetween(Integer value1, Integer value2) {
addCriterion("guide_num between", value1, value2, "guideNum");
return (Criteria) this;
}
public Criteria andGuideNumNotBetween(Integer value1, Integer value2) {
addCriterion("guide_num not between", value1, value2, "guideNum");
return (Criteria) this;
}
public Criteria andCreateTimeIsNull() {
addCriterion("create_time is null");
return (Criteria) this;
}
public Criteria andCreateTimeIsNotNull() {
addCriterion("create_time is not null");
return (Criteria) this;
}
public Criteria andCreateTimeEqualTo(Long value) {
addCriterion("create_time =", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeNotEqualTo(Long value) {
addCriterion("create_time <>", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeGreaterThan(Long value) {
addCriterion("create_time >", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeGreaterThanOrEqualTo(Long value) {
addCriterion("create_time >=", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeLessThan(Long value) {
addCriterion("create_time <", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeLessThanOrEqualTo(Long value) {
addCriterion("create_time <=", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeIn(List<Long> values) {
addCriterion("create_time in", values, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeNotIn(List<Long> values) {
addCriterion("create_time not in", values, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeBetween(Long value1, Long value2) {
addCriterion("create_time between", value1, value2, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeNotBetween(Long value1, Long value2) {
addCriterion("create_time not between", value1, value2, "createTime");
return (Criteria) this;
}
public Criteria andUpdateTimeIsNull() {
addCriterion("update_time is null");
return (Criteria) this;
}
public Criteria andUpdateTimeIsNotNull() {
addCriterion("update_time is not null");
return (Criteria) this;
}
public Criteria andUpdateTimeEqualTo(Long value) {
addCriterion("update_time =", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeNotEqualTo(Long value) {
addCriterion("update_time <>", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeGreaterThan(Long value) {
addCriterion("update_time >", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeGreaterThanOrEqualTo(Long value) {
addCriterion("update_time >=", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeLessThan(Long value) {
addCriterion("update_time <", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeLessThanOrEqualTo(Long value) {
addCriterion("update_time <=", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeIn(List<Long> values) {
addCriterion("update_time in", values, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeNotIn(List<Long> values) {
addCriterion("update_time not in", values, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeBetween(Long value1, Long value2) {
addCriterion("update_time between", value1, value2, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeNotBetween(Long value1, Long value2) {
addCriterion("update_time not between", value1, value2, "updateTime");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}

View File

@ -31,8 +31,10 @@
"pinia": "^2.0.14",
"pinia-plugin-persistedstate": "^1.6.3",
"qiankun": ">2.10.1 || 2.9.3",
"shepherd.js": "^10.0.0",
"vue": "^2.7.3",
"vue-i18n": "^8.22.4",
"vue-shepherd": "^0.3.0",
"vuedraggable": "^2.24.3"
},
"devDependencies": {

View File

@ -0,0 +1,55 @@
import {get, post} from "../plugins/request"
import {getCurrentUserId} from "../utils/token";
import {TASK_DATA} from "../utils/constants";
export function getSideTask() {
return post(`/novice/info`);
}
export function saveStep() {
return post(`/novice/save/step`,{'guideStep': localStorage.getItem('step')});
}
export function saveTask(data) {
return post(`/novice/save/task`,{'dataOption': JSON.stringify(data)});
}
export function updateUserByResourceId(resourceId) {
let userId = getCurrentUserId();
return get(`/user/update/current-by-resource/${resourceId}`);
}
export function initTaskData(url){
getSideTask().then(res=>{
let taskData = TASK_DATA
if(res.data.length > 0 && res.data[0].dataOption){
taskData = JSON.parse(res.data[0].dataOption)
}
if(taskData.length > 0){
taskData.forEach(item=>{
let index = item.taskData.findIndex(function(res) {
return res.status === 0 && res.api.includes(url);
});
if(index > -1){
item.taskData[index].status = 1
item.rate += 1
item.percentage = Math.floor(item.rate / item.taskData.length * 100)
if(item.percentage === 100){
item.status = 1
}else if(100 > item.percentage && item.percentage > 0){
item.status = 2
}
}
})
// 入库
saveTask(taskData).then(res => {
}).catch(error => {
// 错误的信息
this.$error({
message: error.response.data.message
})
})
}
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@ -0,0 +1,152 @@
@import '~shepherd.js/dist/css/shepherd.css';
.shepherd-has-title.shepherd-element {
width: 220px !important;
}
.custom-width {
width: 320px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom"] {
margin-top: 15px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom-start"] {
margin-top: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom-end"] {
margin-top: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="left"] {
margin-right: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="left-start"] {
margin-right: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="left-end"] {
margin-right: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="right"] {
margin-left: 20px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="right-start"] {
margin-left: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="right-end"] {
margin-left: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="top"] {
margin-bottom: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="top-start"] {
margin-bottom: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="top-end"] {
margin-bottom: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom"]>.shepherd-arrow {
left: 10px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom-end"]>.shepherd-arrow {
left: -50px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom-start"]>.shepherd-arrow {
left: -20px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="top-end"]>.shepherd-arrow {
left: 150px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="top-start"]>.shepherd-arrow {
left: -150px !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="right"]>.shepherd-arrow {
left: 0 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="right-start"]>.shepherd-arrow {
left: 0 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="right-end"]>.shepherd-arrow {
left: 0 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="left"]>.shepherd-arrow {
right: 0 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="left-start"]>.shepherd-arrow {
right: 0 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="left-end"]>.shepherd-arrow {
right: 0 !important;
}
.shepherd-has-title.shepherd-element .shepherd-arrow:before {
top: 4px;
left: -5px;
height: 10px;
width: 10px;
background-color: #fff !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom"]>.shepherd-arrow:before {
background-color: #783787 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom-end"]>.shepherd-arrow:before {
background-color: #783787 !important;
}
.shepherd-has-title.shepherd-element[data-popper-placement="bottom-start"]>.shepherd-arrow:before {
background-color: #783787 !important;
}
.shepherd-has-title.shepherd-element .shepherd-header {
height: 40px;
line-height: 40px;
padding: 1em;
background-color: #fff;
}
.shepherd-has-title.shepherd-element .shepherd-header .shepherd-title {
/*margin-top: 1.2em;*/
font-size: 16px !important;
font-weight: 500;
}
.shepherd-has-title.shepherd-element .shepherd-text {
font-size: 13px;
font-weight: 300;
padding: 10px 14px;
line-height: 1.6;
}
.shepherd-has-title.shepherd-element .shepherd-footer {
padding: 0 1em 1em;
}
.shepherd-has-title.shepherd-element .shepherd-footer .shep-btn {
height: 32px;
padding: 0 1px;
line-height: 32px;
background-color: #fff;
font-size: 13px;
}
.shepherd-has-title.shepherd-element .shepherd-footer .close-btn {
height: 32px;
padding: 0 1px;
line-height: 32px;
color: #868bac !important;
background-color: #fff;
font-size: 13px;
}
.shepherd-has-title.shepherd-element .close-btn:hover {
color: #505266 !important;
}
.shepherd-has-title.shepherd-element .shep-btn {
border: none;
color: #783787;
}
.shepherd-has-title.shepherd-element .shep-btn:hover {
transform: scale(1.1) !important;
}
.shepherd-title {
color: #fff;
}
.shepherd-has-title.shepherd-element .shepherd-header {
height: 40px;
line-height: 40px;
padding: 1em;
background-color: #783787;
}

View File

@ -18,7 +18,7 @@
<el-container>
<el-aside
:class="[isCollapse ? 'ms-aside': 'ms-aside-collapse-open', isFullScreen ? 'is-fullscreen' : '']"
class="ms-left-aside"
class="ms-left-aside shepherd-menu"
:style="isFixed ? 'opacity:100%; position: relative;z-index: 666;': 'opacity: 95%;position: fixed'"
@mouseenter.native="collapseOpen"
@mouseleave.native="collapseClose">

View File

@ -0,0 +1,256 @@
<template>
<el-dropdown size="medium" @command="handleCommand" class="ms-header-menu align-right">
<span class="dropdown-link">
<font-awesome-icon class="icon global focusing" :icon="['fas', 'question-circle']" />
</span>
<template v-slot:dropdown>
<el-dropdown-menu>
<el-dropdown-item command="guidance">{{ $t('commons.page_guidance') }}</el-dropdown-item>
<el-dropdown-item command="introduction">{{ $t('commons.function_introduction') }}</el-dropdown-item>
<el-dropdown-item command="novice">{{ $t('commons.novice_journey') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
<ms-introduction ref="introduction" @skipOpen="skipOpen"/>
<ms-side-menus ref="sideMenu"/>
</el-dropdown>
</template>
<script>
import MsIntroduction from "../../components/guide/components/Introduction";
import MsSideMenus from "../../components/sidemenu/SideMenus";
import {getSideTask} from "../../api/novice";
export default {
name: "MsGuidance",
components: {MsIntroduction,MsSideMenus},
data() {
return {
};
},
mounted() {
this.$refs.introduction.resVisible = localStorage.getItem("introduction") !== 'false' &&
(localStorage.getItem("guide") === '1' || localStorage.getItem("step") > 1);
this.checkStep()
},
methods: {
handleCommand(command) {
switch (command) {
case "introduction":
this.$refs.introduction.openNext();
break;
case "guidance":
localStorage.setItem("guide", 0)
localStorage.removeItem('step')
if(this.$route.path.includes('project')){
this.$router.push('/project/home')
this.$router.go(0);
}else{
this.$router.push('/project/home')
}
break;
case "novice":
this.$refs.sideMenu.toggle();
break;
default:
break;
}
},
checkStep(){
getSideTask().then(res=> {
if (res.data.length > 0 && res.data[0].guideStep) {
localStorage.setItem('step', res.data[0].guideStep)
} else {
localStorage.setItem('guide','0')
localStorage.removeItem('step')
}
let microApps = JSON.parse(sessionStorage.getItem("micro_apps"));
if(localStorage.getItem("guide") === '0' && microApps && microApps['project']) {
localStorage.setItem("step", '1')
this.initStep()
}
})
},
initStep() {
const _this = this
_this.$nextTick(() => {
const tour = _this.$shepherd({
useModalOverlay: true,
exitOnEsc: false,
keyboardNavigation: false,
defaultStepOptions: {
scrollTo: {
behavior: 'smooth',
block: 'center'
},
canClickTarget: false,
//
modalOverlayOpeningPadding: 0,
//
modalOverlayOpeningRadius: 4
}
})
tour.addSteps([
{
attachTo: {
element: document.querySelector('.shepherd-workspace'),
on: 'bottom-start'
},
buttons: [
{
action: function() {
_this.$refs.introduction.resVisible = localStorage.getItem("step") > 1
return _this.gotoCancel(this, true)
},
classes: 'close-btn',
text: _this.$t("shepherd.exit")
},
{
action: function() {
return _this.gotoNext(this, null, 2)
},
classes: 'shep-btn',
text: _this.$t("shepherd.next")
}
],
title: _this.$t("shepherd.step1.title"),
text: _this.$t("shepherd.step1.text")
},
{
attachTo: {
element: document.querySelector('.shepherd-menu'),
on: 'right'
},
buttons: [
{
action: function() {
_this.$refs.introduction.resVisible = localStorage.getItem("step") > 1
return _this.gotoCancel(this, true)
},
classes: 'close-btn',
text: _this.$t("shepherd.exit")
},
{
action: function() {
return _this.gotoNext(this, '/project/home', 3)
},
classes: 'shep-btn',
text: _this.$t("shepherd.next")
}
],
title: _this.$t("shepherd.step2.title"),
text: _this.$t("shepherd.step2.text")
},
{
attachTo: {
element: document.querySelector('.shepherd-project'),
on: 'bottom-end'
},
classes: "custom-width",
buttons: [
{
action: function() {
_this.$refs.introduction.resVisible = localStorage.getItem("step") > 1
return _this.gotoCancel(this, true)
},
classes: 'close-btn',
text: _this.$t("shepherd.exit")
},
{
action: function() {
return _this.gotoNext(this, null, 4)
},
classes: 'shep-btn',
text: _this.$t("shepherd.next")
}
],
title: _this.$t("shepherd.step3.title"),
text: _this.$t("shepherd.step3.text")
},
{
attachTo: {
element: document.querySelector('.shepherd-project-menu'),
on: 'bottom-start'
},
buttons: [
{
action: function() {
_this.$refs.introduction.resVisible = localStorage.getItem("step") > 1
return _this.gotoCancel(this, true)
},
classes: 'close-btn',
text: _this.$t("shepherd.exit")
},
{
action: function() {
return _this.gotoNext(this, '/project/home', 5)
},
classes: 'shep-btn',
text: _this.$t("shepherd.next")
}
],
title: _this.$t("shepherd.step4.title"),
text: _this.$t("shepherd.step4.text")
},
{
arrow:true,
modalOverlayOpeningPadding: 8,
attachTo: {
element: document.querySelector('.shepherd-project-name'),
on: 'right'
},
buttons: [
{
action: function() {
_this.$refs.introduction.resVisible = localStorage.getItem("step") > 1
return _this.gotoCancel(this, false)
},
classes: 'close-btn',
text: _this.$t("shepherd.know")
}
],
title: _this.$t("shepherd.step5.title"),
text: _this.$t("shepherd.step5.text")
}
])
tour.start()
})
},
skipOpen(path){
if(path){
this.$refs.sideMenu.skipOpen(path);
}
}
}
};
</script>
<style scoped>
.dropdown-link {
cursor: pointer;
font-size: 14px;
color: rgb(146, 147, 150);
}
.align-right {
float: right;
}
.icon {
width: 24px;
}
.ms-header-menu {
padding-top: 12px;
width: 24px;
}
.ms-header-menu:hover {
cursor: pointer;
border-color: var(--color);
}
</style>

View File

@ -0,0 +1,239 @@
<template>
<div>
<el-dialog :close-on-click-modal="false" z-index="1000" :width="nextVisible ? '50%' : '40%'"
:visible.sync="resVisible" :class="nextVisible ? 'api-import-next' : 'api-import'" destroy-on-close @close="closeDialog">
<div class="card" v-if="!nextVisible">
<img src="/assets/guide/visual-collaboration.png" class="image" alt="MS">
<div class="content">
<p class="title" ><img src="../../../assets/guide/hard.png" alt="MS">{{ $t("guide.home.title") }}</p>
<div class="bottom clearfix">
<p class="desc">{{ $t("guide.home.desc") }}</p>
<el-button type="primary" round size="small" class="button" @click="openNext">
{{ $t("guide.home.button") }}
</el-button>
</div>
</div>
</div>
<div class="card-next" v-else>
<el-carousel trigger="click" :autoplay="false" :loop="false" indicator-position="outside" arrow="never"
height="350px" ref="carousel" @change="toggleCarousel">
<el-carousel-item v-for="item in carouseData" :key="item.id" >
<img :src="'/assets/guide/' + item.url" class="image-next" alt="MS">
</el-carousel-item>
</el-carousel>
<template v-for="item in carouseData">
<div class="bottom clearfix" :key="item.id" v-if="item.id === carouselId">
<p class="title-next">{{ $t(item.title) }}</p>
<p class="desc-next" v-html="$t(item.desc)" />
<el-row>
<el-col v-if="carouselId === 3" :span="24">
<el-button type="primary" round size="small" class="button" @click="gotoTurn()">
{{ $t(item.button) }}
</el-button>
</el-col>
<template v-else>
<el-col :span="8">
<br v-if="carouselId === 0">
<el-button type="primary" v-else round size="small" class="is-plain"
@click="toggleCarousel(carouselId - 1)">
{{ $t("guide.go_prev") }}
</el-button>
</el-col>
<el-col :span="8">
<br>
</el-col>
<el-col :span="8">
<el-button type="primary" round size="small" class="button" @click="toggleCarousel(carouselId + 1)">
{{ $t(item.button) }}
</el-button>
</el-col>
</template>
</el-row>
</div>
</template>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: "MsIntroduction",
data() {
return {
carouselId: 0,
resVisible: false,
nextVisible: false,
carouseData: [
{
url: 'track.png',
title: 'guide.test.title',
desc: 'guide.test.desc',
button: 'guide.test.button',
id: 0
},
{
url: 'api.png',
title: 'guide.api.title',
desc: 'guide.api.desc',
button: 'guide.api.button',
id: 1
},
{
url: 'ui.png',
title: 'guide.ui.title',
desc: 'guide.ui.desc',
button: 'guide.ui.button',
id: 2
},
{
url: 'performance.png',
title: 'guide.performance.title',
desc: 'guide.performance.desc',
button: 'guide.performance.button',
id: 3
}
]
}
},
created() {
},
methods: {
openNext() {
this.carouselId = 0
this.resVisible = true;
this.nextVisible = true;
},
closeDialog() {
localStorage.setItem("introduction", 'false');
this.resVisible = false;
this.nextVisible = false;
},
toggleCarousel(index){
this.carouselId = index
this.$refs.carousel.setActiveItem(index);
},
gotoTurn(){
this.closeDialog()
let redirectUrl = sessionStorage.getItem("redirectUrl")
if(redirectUrl.includes("track")){
this.$emit("skipOpen", "/track/case/all")
this.$router.push("/track/case/all")
// this.$router.go(0);
}else{
this.$router.push({
path: '/track/case/all',
query: { status: true },
})
}
}
}
}
</script>
<style scoped>
::v-deep .api-import .el-dialog {
border-radius: 8px;
background-image: linear-gradient(to bottom, #f4f4f4 45%, #FFF 0);
}
::v-deep .api-import .el-dialog__body {
padding: 1px 5px 0 5px;
}
::v-deep .api-import-next .el-dialog__body {
padding: 12px 5px 0 5px;
}
::v-deep .api-import-next .el-dialog {
margin-top: 10vh !important;
border-radius: 8px;
background-image: linear-gradient(to bottom, #783887 65%, #FFF 0);
}
::v-deep .api-import-next .el-dialog__headerbtn:focus .el-dialog__close {
color: #fff !important;
}
::v-deep .api-import-next .el-dialog__headerbtn:hover .el-dialog__close {
color: #fff !important;
}
.card {
height: 350px;
text-align: center;
}
.image {
width: 217px;
height: 140px;
}
.title {
margin: 0;
font-size: 220%;
font-weight: 700;
}
.title img {
width: 53px;
height: 50px;
}
.desc {
font-size: 18px;
font-weight: 300;
margin-bottom: 40px;
}
.card-next {
height: 560px;
text-align: center;
}
.image-next {
width: 600px;
height: 300px;
padding-top: 5px;
border-radius: 4px;
border: 1px solid #fff;
box-shadow: 0px 0px 10px hsl(0deg 0% 100%);
background-clip: padding-box;
}
.title-next {
margin: 0;
font-size: x-large;
font-weight: 700;
}
.desc-next {
line-height: initial;
font-size: 14px;
font-weight: 300;
padding: 0 15px;
margin-bottom: 20px;
}
</style>
<style scoped>
::v-deep .el-carousel__indicator--horizontal .el-carousel__button {
width: 10px;
height: 10px;
background: #8c8c8c;
border: 1px solid #ffffff;
border-radius: 50%;
opacity: 0.5;
}
::v-deep .el-carousel__indicator--horizontal.is-active .el-carousel__button {
width: 10px;
height: 10px;
background: #783887;
border-radius: 50%;
opacity: 1;
}
::v-deep .el-dialog__headerbtn {
top: 10px;
right: 10px;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<el-dropdown size="medium" @command="changeWs" placement="bottom" class="align-right"
<el-dropdown size="medium" @command="changeWs" placement="bottom" class="align-right shepherd-workspace"
v-permission="['PROJECT_TRACK_CASE:READ','PROJECT_TRACK_PLAN:READ','PROJECT_TRACK_REVIEW:READ',
'PROJECT_API_DEFINITION:READ','PROJECT_API_SCENARIO:READ','PROJECT_API_REPORT:READ',
'PROJECT_USER:READ', 'PROJECT_ENVIRONMENT:READ', 'PROJECT_FILE:READ+JAR', 'PROJECT_FILE:READ+FILE', 'PROJECT_OPERATING_LOG:READ', 'PROJECT_CUSTOM_CODE:READ',

View File

@ -1,5 +1,5 @@
<template>
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" default-active="1" router>
<el-menu class="header-menu shepherd-project" :unique-opened="true" mode="horizontal" default-active="1" router>
<!-- 不激活项目路由-->
<el-menu-item index="1" v-show="false">Placeholder</el-menu-item>
<el-submenu index="2" popper-class="submenu">
@ -24,6 +24,7 @@
<span style="padding-left: 7px;">{{ $t('commons.show_all') }}</span>
</el-menu-item>
</el-submenu>
<ms-introduction ref="introduction"/>
</el-menu>
</template>
@ -31,13 +32,15 @@
import ProjectSearchList from "./ProjectSearchList";
import {PROJECT_NAME} from "../../utils/constants";
import {getCurrentProjectID} from "../../utils/token";
import MsIntroduction from "../../components/guide/components/Introduction";
export default {
name: "ProjectSwitch",
props: {
projectName: String
},
components: {ProjectSearchList},
components: {ProjectSearchList,MsIntroduction},
watch: {
currentProject() {
sessionStorage.setItem(PROJECT_NAME, this.currentProject);

View File

@ -5,6 +5,7 @@
<ms-header-ws/>
<ms-task-center/>
<ms-notification/>
<ms-guidance/>
</div>
</template>
@ -14,6 +15,7 @@ import MsHeaderWs from "../../components/head/HeaderWs";
import MsLanguageSwitch from "../../components/head/LanguageSwitch";
import MsTaskCenter from "../../components/task/TaskCenter";
import MsNotification from "../../components/notice/Notification";
import MsGuidance from "../../components/guide/Guidance";
export default {
@ -24,7 +26,8 @@ export default {
MsTaskCenter,
MsLanguageSwitch,
MsUser,
MsHeaderWs
MsHeaderWs,
MsGuidance
},
data() {
return {}

View File

@ -0,0 +1,144 @@
<template>
<div v-if="taskStatus">
<!-- 侧边任务按钮-->
<div class="parentBox" @click="toggle()">
<div class="contentsBox">
<div :style="openBox ? 'right: 0;width:120px;cursor: auto;' : ''">
<font-awesome-icon class="icon global focusing" :icon="['fas', 'compass']" spin style="color: #ffffff;" />
<span :style="openBox ? 'display: block;color: #fff;cursor: pointer;' : ''">{{$t('side_task.novice_task')}}</span>
</div>
</div>
</div>
<ms-site-task ref="siteTask" :taskData="taskData" @closeBox="closeBox"/>
</div>
</template>
<script>
import MsSiteTask from "../../components/sidemenu/components/SiteTask";
import {getSideTask} from "../../api/novice";
import {TASK_DATA} from "../../utils/constants";
export default {
name: "MsSideTask",
components: { MsSiteTask },
data() {
return {
taskStatus: false,
openBox:false,
totalTask: 0,
taskData:[]
};
},
created() {
this.initTaskData()
},
methods: {
initTaskData(){
getSideTask().then(res=>{
if(res.data.length > 0 && res.data[0].dataOption){
this.taskData = JSON.parse(res.data[0].dataOption)
}else{
this.taskData = TASK_DATA
}
let microApp = JSON.parse(sessionStorage.getItem("micro_apps"));
let num = 0
let total = 0
this.taskData.forEach(item =>{
if(!(microApp && microApp[item.name])){
item.status = -1
total++
} else {
item.percentage = Math.floor(item.rate / item.taskData.length * 100)
if(item.percentage === 100){
item.status = 1
}else if(100 > item.percentage && item.percentage > 0){
item.status = 2
}
num += item.rate
}
})
if(total < this.taskData.length){
this.taskStatus = true
}
this.totalTask = num
})
},
toggle(){
this.openBox = true
this.initTaskData()
this.$refs.siteTask.open();
},
closeBox(status){
this.openBox = status
},
skipOpen(path){
this.initTaskData()
this.$refs.siteTask.skipOpen(path);
}
}
}
</script>
<style scoped>
.parentBox {
height: 100%;
background: gainsboro;
overflow: hidden;
overflow-y: auto;
z-index: 1000;
position: fixed;
}
.parentBox .contentsBox div {
transition: all 1s;
position: fixed;
right: 0;
width: 27px;
border-radius: 50px;
background-color: #783787;
color: #fff;
padding: 3px;
cursor: pointer;
display: flex;
align-items: center;
}
.parentBox .contentsBox div span {
display: none;
color: #fff;
}
.parentBox .contentsBox div span:last-child {
margin-left: 10px;
}
.parentBox .contentsBox div:nth-child(1) {
bottom: 100px;
}
.parentBox .contentsBox div:hover {
right: 0;
height: 28px;
width: 120px;
cursor: auto;
}
.parentBox .contentsBox div:hover span {
display: block;
color: #fff;
cursor: pointer;
}
.parentBox .contentsBox div:not(:last-child) {
border-bottom: 1px solid #fff;
}
/*隐藏浏览器滚动条*/
::-webkit-scrollbar {
display: none;
}
.icon {
width: 2em !important;
height: 2em;
}
</style>

View File

@ -0,0 +1,320 @@
<template>
<div>
<div class="csat-popup" v-if="cardVisible">
<template v-for="(item,index) in taskInfo" >
<el-card :key="item.id" v-if="(index + 1) === taskIndex" class="box-card" >
<div slot="header" class="clearfix">
<span class="text-header">{{$t(item.title)}}</span>
<el-button style="float: right; padding: 5px 0;" class="moon" type="text" @click="open()">
<font-awesome-icon :icon="['fa', 'chevron-down']" class="icon"/>
</el-button>
<el-button style="float: right; padding: 3px 0;margin-right: 25px" type="text" >
<img v-for="num in completeNum" src="../../../assets/guide/moon-dark.png"
class="moon" alt="MS" :key="num + 'd'">
<img v-for="num in ongoingNum" src="../../../assets/guide/moon-light.png"
class="moon" alt="MS" :key="num + 'l'">
<img v-for="num in incompleteNum" src="../../../assets/guide/moon.png"
class="moon" alt="MS" :key="num">
</el-button>
<el-progress :percentage="item.percentage" color="#783787" class="progress-card"></el-progress>
</div>
<div style="height: 220px">
<template v-for="(val,i) in item.taskData">
<div class="text item" v-permission="val.permission" :key="i">
<p v-if="val.status === 1">
<font-awesome-icon :icon="['far', 'check-circle']" style="color:#783887" />
<label> {{$t(val.name)}}</label>
</p>
<p v-else @click="openGif(val)">
<font-awesome-icon :icon="['far', 'circle']" class="title-icon" />
<span> {{$t(val.name)}}
</span>
</p>
</div>
</template>
</div>
<div class="footer">
<el-button v-if="taskIndex < totalNum" style="float: right; margin-left: 10px; padding: 15px 0" type="text" @click="next()">
{{$t('side_task.next')}}
</el-button>
<el-button v-if="taskIndex > 1" style="float: right;margin-left: 10px; padding: 15px 0" type="text" @click="prev()">
{{$t('side_task.prev')}}
</el-button>
<el-button style="float: right; padding: 15px 0;color:#8C8C8C" type="text" @click="skip()">
{{$t('side_task.skip')}}
</el-button>
</div>
</el-card>
</template>
</div>
<div class="csat-popup-gif" v-if="gifVisible">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span style="float: right; padding: 5px 0;" class="moon" @click="closeGif()">
<font-awesome-icon :icon="['fa', 'times']" class="icon"/>
</span>
</div>
<div class="text" style="text-align: center;height: 210px">
<el-image
style="width: 340px;border-radius: 4px;"
:src="gifData.url"
:preview-src-list="[gifData.url]" lazy>
<div slot="placeholder" class="image-slot">
加载中<span class="dot">...</span>
</div>
</el-image>
</div>
<div class="gif-footer">
<el-button type="primary" round size="small" class="is-plain" @click="gotoPath(gifData.path)">
{{$t(gifData.name)}}
</el-button>
</div>
</el-card>
</div>
</div>
</template>
<script>
import {hasLicense} from "../../../utils/permission";
import {TASK_DATA, TASK_MODULE} from "../../../utils/constants";
import {getSideTask} from "../../../api/novice";
export default {
name: "SiteTask",
data() {
return {
cardVisible: false,
gifVisible: false,
gifData:'',
taskIndex: 1,
taskInfo: [],
interfaceData: '',
performanceData: '',
projectData: '',
uiData: '',
completeNum: 0,
ongoingNum: 0,
incompleteNum: 0,
totalNum:0,
status: this.$route.query.status
}
},
props: {
taskData: Array
},
created() {
console.log("this.status")
console.log(this.status)
if(this.status){
this.skipOpen("/track/case/all")
}
},
methods: {
taskNum() {
let totalNum = 0
let completeNum = 0
let ongoingNum = 0
this.taskInfo = []
// status -1 0 1 2
this.taskData.forEach(item=>{
if(item.status === 1){
completeNum++;
}else if(item.status === 2){
ongoingNum++;
}
if(item.status !== -1) {
totalNum++
this.taskInfo.push(item)
}
})
let incompleteNum = totalNum - completeNum - ongoingNum
this.completeNum = completeNum
this.ongoingNum = ongoingNum
this.incompleteNum = incompleteNum
this.totalNum = totalNum
},
open() {
this.taskIndex = 1
this.taskNum()
this.cardVisible = !this.cardVisible;
this.$emit("closeBox", this.cardVisible)
this.gifVisible = this.cardVisible ? this.gifVisible : this.cardVisible;
},
openGif(gif) {
this.gifVisible = true
this.gifData = gif
},
closeGif(){
this.gifVisible = false
},
prev() {
this.taskIndex = this.taskIndex - 1
this.taskIndex = this.taskIndex < 1 ? 1 : this.taskIndex
},
next() {
this.taskIndex = this.taskIndex + 1
this.taskIndex = this.taskIndex > this.taskData.length ? this.taskData.length : this.taskIndex
},
skip() {
this.open()
},
gotoPath(path){
this.$router.push(path)
},
skipOpen(path){
if(path){
this.taskData.forEach(val=>{
val.taskData.forEach(item=>{
if(item.path === path){
this.taskIndex = 1
this.taskNum()
this.cardVisible = true
this.$emit("closeBox", this.cardVisible)
this.openGif(item)
}
})
})
}
}
}
}
</script>
<style scoped>
.text {
font-size: 16px;
font-weight: 300;
}
.item {
margin-bottom: 10px;
margin-left: 35px;
}
.item p {
margin-top: 0;
margin-bottom: 10px;
}
.item p label {
color:#783887;
padding: 3px 6px;
}
.item p span {
padding: 3px 6px;
}
.title-icon {
color: #303133
}
.item p:hover {
color:#783887;
cursor:pointer;
}
.icon {
color: #0a0a0a;
}
.icon:hover {
color: #783787;
}
.item p:hover .title-icon {
color:#783887;
}
.progress-card {
margin-top: 10px;
margin-right: 40px;
}
.footer {
width: 95%;
margin-bottom: 20px;
}
.gif-footer {
width: 100%;
margin: 10px 0 30px;
text-align: center;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
}
.clearfix:after {
clear: both
}
.box-card {
width: 400px;
}
.csat-popup {
position: fixed;
right: 16px;
bottom: 160px;
width: 400px;
border-radius: 8px;
overflow: hidden;
z-index: 1000;
-webkit-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #EBEEF5;
background-color: #fff;
-webkit-transition: .3s;
transition: .3s;
}
.csat-popup-gif {
position: fixed;
right: 426px;
bottom: 160px;
width: 400px;
border-radius: 8px;
overflow: hidden;
z-index: 1000;
-webkit-box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #EBEEF5;
background-color: #fff;
-webkit-transition: .3s;
transition: .3s;
}
.text-header {
font-weight: 700;
font-size: 24px;
}
.moon {
width: 20px;
height: 20px;
}
.circle {
width: 12px;
height: 12px;
margin-right: 5px;
}
::v-deep .csat-popup .el-card__header {
border-bottom: none;
padding: 20px 24px 10px 24px;
}
::v-deep .csat-popup-gif .el-card__header {
border-bottom: none;
padding: 20px 20px 10px 24px;
}
::v-deep .el-progress__text{
color: #783787 !important;
float: left;
margin-left: 0;
margin-right: 10px
}
</style>

View File

@ -519,6 +519,9 @@ const message = {
ui_module: "default",
},
other: "Other",
function_introduction: 'Function introduction',
page_guidance: 'Page guidance',
novice_journey: 'Novice Journey'
},
login: {
normal_Login: "Normal Login",
@ -3529,6 +3532,102 @@ const message = {
ui_title: "UI testing tasks",
perf_title: "Perf testing tasks",
},
shepherd: {
step1: {
title: 'A Workspaces and Projects',
text: 'MeterSphere uses "workspaces" and "projects" to organize test data and members. You can switch positions as you like in the top menu.'
},
step2: {
title: 'Side navigation menu',
text: 'The navigation menu shows which function module you are in.'
},
step3: {
title: 'One workspace holds multiple projects',
text: 'A "project" is a collection of use cases and members. Various types of tests on MeterSphere are viewed and managed through projects.'
},
step4: {
title: 'Top function menu',
text: 'The topl function menu supports switching subdivision functions under the current first-class module.'
},
step5: {
title: "Where are you?",
text: "Now, you have joined our preset workspace by default and become a member of the demo project. Please start your testing journey from here."
},
exit:'skip',
next: 'Next',
know:'know',
},
guide: {
home: {
title: 'Welcome to MeterSphere!',
desc: 'A quickstart guide to see what MeterSphere can do for you.',
button: 'Lets get started'
},
test: {
title: 'Test cases are the cornerstone of testing',
desc: '<span>Maintain your test cases through online editing/file import/URL synchronization/multi-person review,</span><br> <span>add them to your test plan,quantitatively manage test progress, record results, synchronize issues, </span><br> retain/share test reports, and cover the entire software testing life cycle. ',
button: 'Next: Interface Test'
},
api: {
title: 'Simulate real scenarios to automate API testing',
desc: '<span>API testing is triggered by manual/scheduled tasks/plug-ins, supporting multiple communication protocols; </span><br> <span> scenario case-sets are arranged based on real business processes, </span><br> <span> and multi-type controllers/custom scripts/assertions are supported to meet various user needs. ',
button: 'Next: UI Test'
},
ui: {
title: 'Portable UI element library and instruction set',
desc: '<span>Arrange scenario cases based on reusable element libraries and instructions;</span><br> combine your commonly used test steps into new instructions, <br> <span> which can be flexibly called in automation scenarios. </span>',
button: 'Next: Performance Test'
},
performance: {
title: 'One-click launch performance testing',
desc: '<span>Provides a distributed performance testing solution, supporting multiple types of testing resource pools </span><br> <span>such as physical machines/virtual machines/k8s container clusters; </span><br> <span>One-click to initiate API scenario case to performance testing, and view real-time reports;</span><br> Provides report comparison under different configurations to control performance bottlenecks and optimize them. ',
button: 'To create your first test case'
},
go_prev: 'Return to previous'
},
side_task: {
test_tracking: {
title: "Challenging Test Track",
task_1: "Join a project",
task_2: "Create a functional test case",
task_3: "Create a review plan",
task_4: "Post a comment in a review",
task_5: "Create a test plan",
task_6: "Add test cases to test plan",
task_7: "Add ralated issue to test case",
},
api_test: {
title: "Challenging API Test",
task_1: "Create an API definition",
task_2: "Import local API definition or API cases",
task_3: "Execute an API testing",
task_4: "Create a new test case based on API testing",
task_5: "Share API documents",
task_6: "Create an automation scenario case",
task_7: "Execute automated API testing with scheduled task ",
},
performance_test: {
title: "Challenging Performance Test",
task_1: "Convert API scenario case into performance testing",
task_2: "Share performance testing report",
},
project_setting: {
title: "Challenging Project Settings",
task_1: "create a new project",
task_2: "Add a project member",
task_3: "Create a project environment",
},
ui_test: {
title: "Challenging UI Test",
task_1: "Create an element",
task_2: "Create an automated UI scenario case",
task_3: "Execute an automated UI scenario case",
},
next: "Next",
prev: "Previous",
skip: "Skip",
novice_task: "Novice Task"
}
};
export default {

View File

@ -511,6 +511,9 @@ const message = {
template_delete: "模版删除",
scope: "应用场景",
other: "其他",
function_introduction: '功能介绍',
page_guidance: '页面指引',
novice_journey: '新手旅程'
},
login: {
normal_Login: "普通登录",
@ -3400,6 +3403,102 @@ const message = {
envrionment: {
export_variable_tip: "导出接口测试变量",
},
shepherd: {
step1: {
title: '工作空间和项目',
text: 'MeterSphere 使用「工作空间」和「项目」来组织测试数据和人力资源。您可以在顶部菜单任意切换位置。'
},
step2: {
title: '功能主菜单',
text: '主菜单显示您所在的功能模块。'
},
step3: {
title: '一个空间可以创建多个项目',
text: '「项目」是一组用例和成员的集合。MeterSphere 上各种类型的测试均通过项目进行分权分域查看和管理。'
},
step4: {
title: '一级功能菜单',
text: '顶部一级功能菜单栏,支持在当前功能模块下切换细分功能。'
},
step5: {
title: "你在哪?",
text: "现在,您已默认加入了我们预置的演示空间,并成为演示项目的一员。就从这里开始你的测试之旅吧。"
},
exit:'跳过',
next:'下一步',
know:'知道啦',
},
guide: {
home: {
title: '欢迎来到 MeterSphere',
desc: '通过一个快捷指引来了解 MeterSphere 究竟能为你做哪些事。',
button: '让我们开始吧'
},
test: {
title: '测试用例是测试任务的基石',
desc: '<span>使用在线编辑/文件导入/URL同步/多人评审的方式维护你的用例,</span><br> <span>将它们加入你的测试计划中,量化管理测试进度,记录结果,同步缺陷,</span><br> 留存/分享测试报告,覆盖整个测试生命周期。',
button: '下一个:接口测试'
},
api: {
title: '模拟真实场景 让接口自动化',
desc: '<span>通过手动/定时任务/插件触发接口测试,支持多种通信协议;</span><br> <span> 基于真实业务流程编排场景化用例集,支持添加多类型控制器/自定义脚本/断言,</span><br> 满足各种复杂场景所需。',
button: '下一个UI测试'
},
ui: {
title: '可移植的 UI 元素库与指令集',
desc: '<span>基于可复用的元素库及指令快速编排场景化用例;</span><br> 将你常用的测试步骤组合成新的自定义指令,在自动化场景中灵活调用。<span>&nbsp;<br><br></span>',
button: '下一个:性能测试'
},
performance: {
title: '性能测试 一键就可以',
desc: '<span>提供分布式压测解决方案,支持物理机/虚拟机/k8s容器集群等多类型压测资源池</span><br> <span>使用接口测试转性能一键发起,实时查看报告;</span><br> 提供差异配置下的报告对比,掌控性能瓶颈及调优。',
button: '完成!去创建你的第 1 条测试用例'
},
go_prev: '返回上一个'
},
side_task: {
test_tracking: {
title: "通关测试跟踪",
task_1: "加入一个项目",
task_2: "创建一条功能用例",
task_3: "发起一个评审计划",
task_4: "在评审中发布评论",
task_5: "发布一个测试计划",
task_6: "为测试计划添加用例",
task_7: "为测试用例关联缺陷",
},
api_test: {
title: "通关接口测试",
task_1: "创建一条接口定义",
task_2: "导入本地接口或接口用例",
task_3: "进行一次接口快捷调试",
task_4: "基于接口调试创建新用例",
task_5: "分享接口文档",
task_6: "创建一条场景自动化用例",
task_7: "以定时任务执行接口自动化测试",
},
performance_test: {
title: "通关性能测试",
task_1: "将接口场景用例转为性能测试",
task_2: "分享性能测试报告",
},
project_setting: {
title: "通关项目设置",
task_1: "创建一个新项目",
task_2: "添加一位项目成员",
task_3: "创建项目环境",
},
ui_test: {
title: "通关 UI 测试",
task_1: "创建一个元素",
task_2: "创建一个 UI 自动化场景",
task_3: "执行一个 UI 自动化场景",
},
next: "下一章",
prev: "上一章",
skip: "跳过",
novice_task: "新手旅程"
}
};
export default {

View File

@ -510,6 +510,9 @@ const message = {
},
template_delete: "模版刪除",
other: "其他",
function_introduction: '功能介紹',
page_guidance: '頁面指引',
novice_journey: '新手旅程'
},
login: {
normal_Login: "普通登錄",
@ -3400,6 +3403,102 @@ const message = {
ui_title: "UI測試任務",
perf_title: "性能測試任務",
},
shepherd: {
step1: {
title: '工作空間和項目',
text: 'MeterSphere 使用「工作空間」和「項目」來組織測試數據和人力資源。您可以在頂部菜單任意切換位置。'
},
step2: {
title: '功能主菜單',
text: '主菜單顯示您所在的功能模塊。'
},
step3: {
title: '一個空間可以創建多個項目',
text: '「項目」是一組用例和成員的集合。 MeterSphere 上各種類型的測試均通過項目進行分權分域查看和管理。'
},
step4: {
title: '一級功能菜單',
text: '頂部一級功能菜單欄,支持在當前功能模塊下切換細分功能。'
},
step5: {
title: "你在哪?",
text: "現在,您已默認加入了我們預置的演示空間,並成為演示項目的一員。就從這裡開始你的測試之旅吧。"
},
exit:'跳過',
next:'下一步',
know:'知道啦',
},
guide: {
home: {
title: '歡迎來到 MeterSphere',
desc: '通過一個快捷指引來了解 MeterSphere 究竟能為你做哪些事。',
button: '讓我們開始吧'
},
test: {
title: '測試用例是測試任務的基石',
desc: '<span>使用在線編輯/文件導入/URL同步/多人評審的方式維護你的用例,</span><br> <span>將它們加入你的測試計劃中,量化管理測試進度,記錄結果,同步缺陷,</span><br> 留存/分享測試報告,覆蓋整個測試生命週期。 ',
button: '下一個:接口測試'
},
api: {
title: '模擬真實場景 讓接口自動化',
desc: '<span>通過手動/定時任務/插件觸發接口測試,支持多種通信協議;</span><br> <span> 基於真實業務流程編排場景化用例集,支持添加多類型控制器/自定義腳本/斷言,</span><br> 滿足各種複雜場景所需。 ',
button: '下一個UI測試'
},
ui: {
title: '可移植的 UI 元素庫與指令集',
desc: '<span>基於可複用的元素庫及指令快速編排場景化用例;</span><br> 將你常用的測試步驟組合成新的自定義指令,在自動化場景中靈活調用。 <span>&nbsp;<br><br></span>',
button: '下一個:性能測試'
},
performance: {
title: '性能測試 一鍵就可以',
desc: '<span>提供分佈式壓測解決方案,支持物理機/虛擬機/k8s容器集群等多類型壓測資源池</span><br> <span>使用接口測試轉性能一鍵發起,實時查看報告;</span><br> 提供差異配置下的報告對比,掌控性能瓶頸及調優。 ',
button: '完成!去創建你的第 1 條測試用例'
},
go_prev: '返回上一個'
},
side_task: {
test_tracking: {
title: "通關測試跟踪",
task_1: "加入一個項目",
task_2: "創建一條功能用例",
task_3: "發起一個評審計畫",
task_4: "在評審中發佈評論",
task_5: "發佈一個測試計畫",
task_6: "為測試計畫添加用例",
task_7: "為測試用例關聯缺陷",
},
api_test: {
title: "通關介面測試",
task_1: "創建一條介面定義",
task_2: "導入本地介面或介面用例",
task_3: "進行一次介面快捷調試",
task_4: "基於介面調試創建新用例",
task_5: "分享介面檔案",
task_6: "創建一條場景自動化用例",
task_7: "以定時任務執行介面自動化測試",
},
performance_test: {
title: "通關性能測試",
task_1: "將介面場景用例轉為性能測試",
task_2: "分享性能測試報告",
},
project_setting: {
title: "通關項目設定",
task_1: "創建一個新項目",
task_2: "添加一比特項目成員",
task_3: "創建項目環境",
},
ui_test: {
title: "通關 UI 測試",
task_1: "創建一個元素",
task_2: "創建一個 UI 自動化場景",
task_3: "執行一個 UI 自動化場景",
},
next: "下一章",
prev: "上一章",
skip: "跳過",
novice_task: "新手旅程"
}
};
export default {

View File

@ -15,6 +15,9 @@ import "./router/permission";
import "./micro-app";
import mavonEditor from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';
import VueShepherd from 'vue-shepherd' // 新手引导
import './assets/shepherd/shepherd-theme.css'
import { gotoCancel, gotoNext } from "./utils";
Vue.config.productionTip = false
@ -33,6 +36,10 @@ Vue.use(directives);
Vue.use(filters);
Vue.use(PiniaVuePlugin);
Vue.use(mavonEditor);
Vue.use(VueShepherd)
Vue.prototype.gotoCancel = gotoCancel
Vue.prototype.gotoNext = gotoNext
new Vue({
el: "#app",

View File

@ -1,9 +1,10 @@
import axios from 'axios'
import {$error} from "./message"
import {getCurrentProjectID, getCurrentWorkspaceId} from "../utils/token";
import {PROJECT_ID, TokenKey, WORKSPACE_ID} from "../utils/constants";
import {PROJECT_ID, TokenKey, WORKSPACE_ID, TASK_PATH, TASK_DATA} from "../utils/constants";
import packageJSON from '@/../package.json'
import {getUrlParams, getUUID} from "../utils";
import {initTaskData} from "../api/novice";
import {Base64} from "js-base64";
// baseURL 根据是否是独立运行修改
@ -94,9 +95,17 @@ const checkPermission = response => {
}
}
const checkTask = response => {
// 请根据实际需求修改
if (TASK_PATH.includes(response.config["url"]) && response.status === 200) {
initTaskData(response.config["url"]);
}
}
// 请根据实际需求修改
instance.interceptors.response.use(response => {
checkAuth(response);
checkTask(response);
return response;
}, error => {
let msg;

View File

@ -263,3 +263,103 @@ export const SECOND_LEVEL_ROUTE_PERMISSION_MAP = {
}
]
}
export const TASK_PATH = [
"/test/case/add",
"/test/case/review/save",
"/test/case/review/comment/save",
"/test/plan/add",
"/test/plan/relevance",
"/issues/add",
"/api/definition/create",
"/api/definition/run/debug",
"/api/testcase/create",
"/share/info/generateApiDocumentShareInfo",
"/api/definition/import",
"/api/automation/create",
"/api/automation/schedule/update",
"/performance/save",
"/share/info/generateShareInfoWithExpired",
"/project/add",
"/project/member/add",
"/user/project/member/add",
"/api/environment/add",
"/ui/element/add",
"/ui/automation/create",
"/ui/automation/run/debug",
];
export const TASK_DATA = [
{
id: 1,
name: "track",
title: "side_task.test_tracking.title",
percentage: 14,
taskData: [
{ id: 1, name: "side_task.test_tracking.task_1", status: 1, permission: ['PROJECT_MANAGER:READ', 'WORKSPACE_PROJECT_MANAGER:READ'], api: [''], path: '/setting/project/:type', url: "" },
{ id: 2, name: "side_task.test_tracking.task_2", status: 0, permission: ['PROJECT_TRACK_CASE:READ+CREATE'], api: ["/test/case/add"], path: '/track/case/all', url: "/assets/guide/track/task-2.gif" },
{ id: 3, name: "side_task.test_tracking.task_3", status: 0, permission: ['PROJECT_TRACK_REVIEW:READ+CREATE'], api: ["/test/case/review/save"], path: '/track/review/all', url: "/assets/guide/track/task-3.gif" },
{ id: 4, name: "side_task.test_tracking.task_4", status: 0, permission: ['PROJECT_TRACK_REVIEW:READ+COMMENT'], api: ["/test/case/review/comment/save"], path: '/track/review/all', url: "/assets/guide/track/task-4.gif" },
{ id: 5, name: "side_task.test_tracking.task_5", status: 0, permission: ['PROJECT_TRACK_PLAN:READ+CREATE'], api: ["/test/plan/add"], path: '/track/plan/all', url: "/assets/guide/track/task-5.gif" },
{ id: 6, name: "side_task.test_tracking.task_6", status: 0, permission: ['PROJECT_TRACK_PLAN:READ+RELEVANCE_OR_CANCEL'], api: ["/test/plan/relevance"], path: '/track/plan/all', url: "/assets/guide/track/task-6.gif" },
{ id: 7, name: "side_task.test_tracking.task_7", status: 0, permission: ['PROJECT_TRACK_ISSUE:READ+CREATE'], api: ["/issues/add"], path: '/track/issue', url: "/assets/guide/track/task-7.gif" },
],
rate: 1,
status: 0
},
{
id: 2,
name: "api",
title: 'side_task.api_test.title',
percentage: 0,
taskData: [
{id: 1, name: "side_task.api_test.task_1", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+CREATE_API'], api: ["/api/definition/create"], url: "/assets/guide/api/task-1.gif" },
{id: 2, name: "side_task.api_test.task_2", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+IMPORT_API'], api: ["/api/definition/import"], url: "/assets/guide/api/task-2.gif" },
{id: 3, name: "side_task.api_test.task_3", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+DEBUG'], api: ["/api/definition/run/debug"], url: "/assets/guide/api/task-3.gif" },
{id: 4, name: "side_task.api_test.task_4", status: 0, path: '/api/definition', permission: ['PROJECT_API_DEFINITION:READ+CREATE_CASE'], api: ["/api/testcase/create"], url: "/assets/guide/api/task-4.gif" },
{id: 5, name: "side_task.api_test.task_5", status: 0, path: '/api/automation', permission: ['PROJECT_API_DEFINITION:READ'], api: ["/share/info/generateApiDocumentShareInfo"], url: "/assets/guide/api/task-5.gif" },
{id: 6, name: "side_task.api_test.task_6", status: 0, path: '/api/automation', permission: ['PROJECT_API_SCENARIO:READ+CREATE'], api: ["/api/automation/create"], url: "/assets/guide/api/task-6.gif" },
{id: 7, name: "side_task.api_test.task_7", status: 0, path: '/api/automation', permission: ['PROJECT_API_SCENARIO:READ+SCHEDULE'], api: ["/api/automation/schedule/update"], url: "/assets/guide/api/task-7.gif" },
],
rate: 0,
status: 0
},
{
id: 3,
name: "performance",
title: 'side_task.performance_test.title',
percentage: 0,
taskData: [
{id: 1, name: 'side_task.performance_test.task_1', status: 0, path: '/performance/test/all', permission: ['PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE',"PROJECT_API_SCENARIO:READ+CREATE_PERFORMANCE_BATCH"], api: ["/performance/save"], url: "/assets/guide/performance/task-1.gif" },
{id: 2, name: 'side_task.performance_test.task_2', status: 0, path: '/performance/report/all', permission: ['PROJECT_PERFORMANCE_REPORT:READ'], api: ["/share/info/generateShareInfoWithExpired"], url: "/assets/guide/performance/task-2.gif" },
],
rate: 0,
status: 0
},
{
id: 4,
name: "project",
title: 'side_task.project_setting.title',
percentage: 0,
taskData: [
{id: 1, name: 'side_task.project_setting.task_1', status: 0, permission: ['WORKSPACE_PROJECT_MANAGER:READ+CREATE'], api: ["/project/add"], path: '/setting/project/:type', url: "/assets/guide/project/task-1.gif" },
{id: 2, name: 'side_task.project_setting.task_2', status: 0, permission: ['PROJECT_USER:READ+CREATE'], api: ["/project/member/add","/user/project/member/add"], path: '/project/member', url: "/assets/guide/project/task-2.gif" },
{id: 3, name: 'side_task.project_setting.task_3', status: 0, permission: ['PROJECT_ENVIRONMENT:READ+CREATE'], api: ["/api/environment/add"], path: '/project/env', url: "/assets/guide/project/task-3.gif" },
],
rate: 0,
status: 0
},
{
id: 5,
name: "ui",
title: 'side_task.ui_test.title',
percentage: 0,
taskData: [
{id: 1, name: 'side_task.ui_test.task_1', status: 0, permission: ['PROJECT_UI_ELEMENT:READ+CREATE'], api: ["/ui/element/add"], path: '/ui/element', url: "/assets/guide/ui/task-1.gif" },
{id: 2, name: 'side_task.ui_test.task_2', status: 0, permission: ['PROJECT_UI_ELEMENT:READ+CREATE'], api: ["/ui/automation/create"], path: '/ui/automation', url: "/assets/guide/ui/task-2.gif" },
{id: 2, name: 'side_task.ui_test.task_3', status: 0, permission: ['PROJECT_UI_SCENARIO:READ+RUN'], api: ["/ui/automation/run/debug"], path: '/ui/report', url: "/assets/guide/ui/task-3.gif" },
],
rate: 0,
status: 0
},
]

View File

@ -16,6 +16,7 @@ import JsPDF from "jspdf";
* 如果编辑某一行则只调整某一行提升效率
*/
import calcTextareaHeight from "element-ui/packages/input/src/calcTextareaHeight";
import {saveStep} from "../api/novice";
export function setCustomizeColor(color) {
// 自定义主题风格
@ -450,3 +451,32 @@ export function downloadPDF(ele, pdfName) {
//可动态生成
});
}
export function goSkip(_this) {
_this.cancel()
}
export function gotoCancel(_this, cancel) {
if (cancel) {
_this.cancel()
} else {
_this.complete()
}
saveStep().then(res => {
localStorage.setItem('guide', '1')
}).catch(error => {
// 错误的信息
this.$error({
message: error.response.data.message
})
})
}
// 上一步,下一步
export function gotoNext(_this, path, step) {
_this.next()
localStorage.setItem('step', step)
if (path) {
this.$router.push(path)
}
}

View File

@ -0,0 +1,36 @@
package io.metersphere.base.mapper;
import io.metersphere.base.domain.NoviceStatistics;
import io.metersphere.base.domain.NoviceStatisticsExample;
import java.util.List;
import org.apache.ibatis.annotations.Param;
public interface NoviceStatisticsMapper {
long countByExample(NoviceStatisticsExample example);
int deleteByExample(NoviceStatisticsExample example);
int deleteByPrimaryKey(String id);
int insert(NoviceStatistics record);
int insertSelective(NoviceStatistics record);
List<NoviceStatistics> selectByExampleWithBLOBs(NoviceStatisticsExample example);
List<NoviceStatistics> selectByExample(NoviceStatisticsExample example);
NoviceStatistics selectByPrimaryKey(String id);
int updateByExampleSelective(@Param("record") NoviceStatistics record, @Param("example") NoviceStatisticsExample example);
int updateByExampleWithBLOBs(@Param("record") NoviceStatistics record, @Param("example") NoviceStatisticsExample example);
int updateByExample(@Param("record") NoviceStatistics record, @Param("example") NoviceStatisticsExample example);
int updateByPrimaryKeySelective(NoviceStatistics record);
int updateByPrimaryKeyWithBLOBs(NoviceStatistics record);
int updateByPrimaryKey(NoviceStatistics record);
}

View File

@ -0,0 +1,286 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.base.mapper.NoviceStatisticsMapper">
<resultMap id="BaseResultMap" type="io.metersphere.base.domain.NoviceStatistics">
<id column="id" jdbcType="VARCHAR" property="id" />
<result column="user_id" jdbcType="VARCHAR" property="userId" />
<result column="guide_step" jdbcType="TINYINT" property="guideStep" />
<result column="guide_num" jdbcType="TINYINT" property="guideNum" />
<result column="create_time" jdbcType="BIGINT" property="createTime" />
<result column="update_time" jdbcType="BIGINT" property="updateTime" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.NoviceStatistics">
<result column="data_option" jdbcType="LONGVARCHAR" property="dataOption" />
</resultMap>
<sql id="Example_Where_Clause">
<where>
<foreach collection="oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<sql id="Update_By_Example_Where_Clause">
<where>
<foreach collection="example.oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="(" separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<sql id="Base_Column_List">
id, user_id, guide_step, guide_num, create_time, update_time
</sql>
<sql id="Blob_Column_List">
data_option
</sql>
<select id="selectByExampleWithBLOBs" parameterType="io.metersphere.base.domain.NoviceStatisticsExample" resultMap="ResultMapWithBLOBs">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from novice_statistics
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<select id="selectByExample" parameterType="io.metersphere.base.domain.NoviceStatisticsExample" resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List" />
from novice_statistics
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="ResultMapWithBLOBs">
select
<include refid="Base_Column_List" />
,
<include refid="Blob_Column_List" />
from novice_statistics
where id = #{id,jdbcType=VARCHAR}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.String">
delete from novice_statistics
where id = #{id,jdbcType=VARCHAR}
</delete>
<delete id="deleteByExample" parameterType="io.metersphere.base.domain.NoviceStatisticsExample">
delete from novice_statistics
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</delete>
<insert id="insert" parameterType="io.metersphere.base.domain.NoviceStatistics">
insert into novice_statistics (id, user_id, guide_step, guide_num,
create_time, update_time, data_option)
values (#{id,jdbcType=VARCHAR}, #{userId,jdbcType=VARCHAR}, #{guideStep,jdbcType=TINYINT},
#{guideNum,jdbcType=INTEGER},#{createTime,jdbcType=BIGINT}, #{updateTime,jdbcType=BIGINT},
#{dataOption,jdbcType=LONGVARCHAR})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.NoviceStatistics">
insert into novice_statistics
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="userId != null">
user_id,
</if>
<if test="guideStep != null">
guide_step,
</if>
<if test="guideNum != null">
guide_num,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updateTime != null">
update_time,
</if>
<if test="dataOption != null">
data_option,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=VARCHAR},
</if>
<if test="userId != null">
#{userId,jdbcType=VARCHAR},
</if>
<if test="guideStep != null">
#{guideStep,jdbcType=TINYINT},
</if>
<if test="guideNum != null">
#{guideNum,jdbcType=INTEGER},
</if>
<if test="createTime != null">
#{createTime,jdbcType=BIGINT},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=BIGINT},
</if>
<if test="dataOption != null">
#{dataOption,jdbcType=LONGVARCHAR},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.NoviceStatisticsExample" resultType="java.lang.Long">
select count(*) from novice_statistics
<if test="_parameter != null">
<include refid="Example_Where_Clause" />
</if>
</select>
<update id="updateByExampleSelective" parameterType="map">
update novice_statistics
<set>
<if test="record.id != null">
id = #{record.id,jdbcType=VARCHAR},
</if>
<if test="record.userId != null">
user_id = #{record.userId,jdbcType=VARCHAR},
</if>
<if test="record.guideStep != null">
guide_step = #{record.guideStep,jdbcType=TINYINT},
</if>
<if test="record.guideNum != null">
guide_num = #{record.guideNum,jdbcType=INTEGER},
</if>
<if test="record.createTime != null">
create_time = #{record.createTime,jdbcType=BIGINT},
</if>
<if test="record.updateTime != null">
update_time = #{record.updateTime,jdbcType=BIGINT},
</if>
<if test="record.dataOption != null">
data_option = #{record.dataOption,jdbcType=LONGVARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExampleWithBLOBs" parameterType="map">
update novice_statistics
set id = #{record.id,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
guide_step = #{record.guideStep,jdbcType=TINYINT},
guide_num = #{record.guideNum,jdbcType=INTEGER},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT},
data_option = #{record.dataOption,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByExample" parameterType="map">
update novice_statistics
set id = #{record.id,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
guide_step = #{record.guideStep,jdbcType=TINYINT},
guide_num = #{record.guideNum,jdbcType=INTEGER},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause" />
</if>
</update>
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.base.domain.NoviceStatistics">
update novice_statistics
<set>
<if test="userId != null">
user_id = #{userId,jdbcType=VARCHAR},
</if>
<if test="guideStep != null">
guide_step = #{guideStep,jdbcType=TINYINT},
</if>
<if test="record.guideNum != null">
guide_num = #{record.guideNum,jdbcType=INTEGER},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=BIGINT},
</if>
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=BIGINT},
</if>
<if test="dataOption != null">
data_option = #{dataOption,jdbcType=LONGVARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKeyWithBLOBs" parameterType="io.metersphere.base.domain.NoviceStatistics">
update novice_statistics
set user_id = #{userId,jdbcType=VARCHAR},
guide_step = #{guideStep,jdbcType=TINYINT},
guide_num = #{record.guideNum,jdbcType=INTEGER},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT},
data_option = #{dataOption,jdbcType=LONGVARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.NoviceStatistics">
update novice_statistics
set user_id = #{userId,jdbcType=VARCHAR},
guide_step = #{guideStep,jdbcType=TINYINT},
guide_num = #{record.guideNum,jdbcType=INTEGER},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -0,0 +1,45 @@
package io.metersphere.novice.controller;
import io.metersphere.base.domain.NoviceStatistics;
import io.metersphere.commons.constants.OperLogConstants;
import io.metersphere.commons.constants.OperLogModule;
import io.metersphere.log.annotation.MsAuditLog;
import io.metersphere.notice.domain.MessageDetail;
import io.metersphere.notice.service.NoticeService;
import io.metersphere.novice.request.StepRequest;
import io.metersphere.novice.service.NoviceService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author: LAN
* @date: 2023/3/17 14:07
* @version: 1.0
*/
@RestController
@RequestMapping(value = "novice")
public class NoviceController {
@Resource
private NoviceService noviceService;
@PostMapping("/info")
public List<NoviceStatistics> getNoviceInfo() {
return noviceService.getNoviceInfo();
}
@PostMapping("/save/step")
public void saveStep(@RequestBody StepRequest stepRequest) {
noviceService.saveStep(stepRequest);
}
@PostMapping("/save/task")
public void saveTask(@RequestBody NoviceStatistics noviceStatistics) {
noviceService.saveNoviceInfo(noviceStatistics);
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.novice.request;
import lombok.Data;
/**
* @author: LAN
* @date: 2023/3/18 17:34
* @version: 1.0
*/
@Data
public class StepRequest {
/**
* 新手引导截止步骤
*/
private Integer guideStep;
}

View File

@ -0,0 +1,78 @@
package io.metersphere.novice.service;
import io.metersphere.base.domain.NoviceStatistics;
import io.metersphere.base.domain.NoviceStatisticsExample;
import io.metersphere.base.mapper.NoviceStatisticsMapper;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.novice.request.StepRequest;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
/**
* @author: LAN
* @date: 2023/3/17 14:18
* @version: 1.0
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class NoviceService {
@Resource
private NoviceStatisticsMapper noviceStatisticsMapper;
public List<NoviceStatistics> getNoviceInfo() {
NoviceStatisticsExample example = new NoviceStatisticsExample();
example.createCriteria().andUserIdEqualTo(SessionUtils.getUserId());
return noviceStatisticsMapper.selectByExampleWithBLOBs(example);
}
public void saveNoviceInfo(NoviceStatistics noviceStatistics) {
List<NoviceStatistics> noviceInfo = getNoviceInfo();
long systemTime = System.currentTimeMillis();
if(noviceInfo != null && noviceInfo.size() > 0){
NoviceStatistics record = noviceInfo.get(0);
NoviceStatisticsExample example = new NoviceStatisticsExample();
example.createCriteria().andUserIdEqualTo(SessionUtils.getUserId()).andIdEqualTo(record.getId());
noviceStatistics.setUpdateTime(systemTime);
noviceStatisticsMapper.updateByExampleSelective(noviceStatistics, example);
}else{
noviceStatistics.setId(UUID.randomUUID().toString());
noviceStatistics.setUserId(SessionUtils.getUserId());
noviceStatistics.setCreateTime(systemTime);
noviceStatistics.setUpdateTime(systemTime);
noviceStatisticsMapper.insertSelective(noviceStatistics);
}
}
public void saveStep(StepRequest stepRequest){
List<NoviceStatistics> noviceInfo = getNoviceInfo();
long systemTime = System.currentTimeMillis();
if(noviceInfo != null && noviceInfo.size() > 0){
NoviceStatistics noviceStatistics = noviceInfo.get(0);
noviceStatistics.setGuideStep(stepRequest.getGuideStep());
noviceStatistics.setGuideNum(noviceStatistics.getGuideNum() + 1);
noviceStatistics.setUpdateTime(systemTime);
NoviceStatisticsExample example = new NoviceStatisticsExample();
example.createCriteria().andUserIdEqualTo(SessionUtils.getUserId()).andIdEqualTo(noviceStatistics.getId());
noviceStatisticsMapper.updateByExampleSelective(noviceStatistics, example);
} else{
NoviceStatistics noviceStatistics = new NoviceStatistics();
noviceStatistics.setId(UUID.randomUUID().toString());
noviceStatistics.setUserId(SessionUtils.getUserId());
noviceStatistics.setGuideStep(stepRequest.getGuideStep());
noviceStatistics.setGuideStep(1);
noviceStatistics.setCreateTime(systemTime);
noviceStatistics.setUpdateTime(systemTime);
noviceStatisticsMapper.insertSelective(noviceStatistics);
}
}
}

View File

@ -66,7 +66,9 @@
"vue2-ace-editor": "0.0.15",
"vuedraggable": "^2.24.3",
"xml-js": "^1.6.11",
"yan-progress": "^1.0.3"
"yan-progress": "^1.0.3",
"vue-shepherd": "^0.3.0",
"shepherd.js": "^10.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",

View File

@ -3,7 +3,7 @@
<el-row type="flex">
<project-switch :project-name="currentProject"/>
<el-col :span="14">
<el-menu class="header-menu" :unique-opened="true" mode="horizontal" router
<el-menu class="header-menu shepherd-project-menu" :unique-opened="true" mode="horizontal" router
:default-active="pathName">
<el-menu-item :index="'/project/home'">
{{ $t('project.info') }}

View File

@ -5,7 +5,7 @@
<el-row type="flex" justify="space-around" :gutter="10">
<el-col :xs="12" :sm="12" :md="11" :lg="11" :xl="10" class="card-col">
<el-card class="project-info-card">
<div class="project-info-card-div">
<div class="project-info-card-div shepherd-project-name">
<span class="project-name">{{ project.name }}</span>
<i class="el-icon-edit project-edit" @click="edit" v-permission="['PROJECT_MANAGER:READ+EDIT']"></i>
<el-row class="project-item">

View File

@ -16,6 +16,9 @@ import "metersphere-frontend/src/router/permission";
import mavonEditor from "mavon-editor";
import "mavon-editor/dist/css/index.css";
import VuePapaParse from "vue-papa-parse";
import VueShepherd from 'vue-shepherd' // 新手引导
import 'metersphere-frontend/src/assets/shepherd/shepherd-theme.css'
import { gotoCancel, gotoNext } from "metersphere-frontend/src/utils";
Vue.config.productionTip = false;
const pinia = createPinia();
@ -33,6 +36,10 @@ Vue.use(directives);
Vue.use(filters);
Vue.use(PiniaVuePlugin);
Vue.use(VuePapaParse);
Vue.use(VueShepherd);
Vue.prototype.gotoCancel = gotoCancel
Vue.prototype.gotoNext = gotoNext
let instance = null;

View File

@ -0,0 +1,10 @@
CREATE TABLE `novice_statistics` (
`id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`user_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户id',
`guide_step` tinyint NOT NULL DEFAULT '0' COMMENT '新手引导完成的步骤',
`guide_num` int NOT NULL DEFAULT '1' COMMENT '新手引导的次数',
`data_option` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'data option (JSON format)',
`create_time` bigint DEFAULT NULL,
`update_time` bigint DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

View File

@ -67,7 +67,8 @@
"vuedraggable": "^2.24.3",
"xml-js": "^1.6.11",
"yan-progress": "^1.0.3",
"js-file-download": "^0.4.12"
"vue-shepherd": "^0.3.0",
"shepherd.js": "^10.0.0"
},
"devDependencies": {
"@babel/core": "^7.12.16",

View File

@ -13,6 +13,9 @@ import plugins from "metersphere-frontend/src/plugins";
import directives from "metersphere-frontend/src/directive";
import filters from "metersphere-frontend/src/filters";
import "metersphere-frontend/src/router/permission";
import VueShepherd from 'vue-shepherd' // 新手引导
import 'metersphere-frontend/src/assets/shepherd/shepherd-theme.css'
import { gotoCancel, gotoNext } from "metersphere-frontend/src/utils";
Vue.config.productionTip = false
@ -29,6 +32,10 @@ Vue.use(plugins);
Vue.use(directives);
Vue.use(filters);
Vue.use(PiniaVuePlugin);
Vue.use(VueShepherd);
Vue.prototype.gotoCancel = gotoCancel
Vue.prototype.gotoNext = gotoNext
let instance = null;