This commit is contained in:
sx1989827 2023-11-06 22:15:31 +08:00
parent 1aadc3173d
commit 07b9fd7a7a
88 changed files with 5493 additions and 1187 deletions

View File

@ -8,6 +8,7 @@
,日历calendarvideo conferenceteamlinkercollaboration">
<link rel="icon" href="public/favicon.ico"/>
<title>TeamLinker</title>
</head>
<body>
<div id="app"></div>

View File

@ -4,7 +4,7 @@
overflowY:'visible',
height:rect.height+(gap??0)*2+15+'px'
})}">
<div v-if="props.type==='fixed' && !multiRow && $slots.pinHeader" style="position: sticky;z-index: 1;left: 0px;background-color: white;" :style="{height:rect.height+'px',width:(rect.width+(gap??0))+'px'}">
<div v-if="props.type==='fixed' && !multiRow && $slots.pinHeader" style="position: sticky;z-index: 10;left: 0px;background-color: white;" :style="{height:rect.height+'px',width:(rect.width+(gap??0))+'px'}">
<div style="height: 100%;background-color: rgb(244, 245, 247);border-radius: 5px;;position: relative;z-index: 1" :style="{marginLeft:(gap??0)+'px',width:rect.width+'px',top:(gap??0)+'px'}">
<div style="width: 100%;height: 30px;position: sticky;top: 0px;background-color: rgb(244, 245, 247);z-index: 1">
<slot name="pinHeader"></slot>

View File

@ -31,7 +31,8 @@ export class Dialog {
events,
onOk,
onClose,
loading
loading,
parentNode:el
});
el.appendChild(ele);
async function onOk(){
@ -64,7 +65,8 @@ export class Dialog {
let destroyFunc=renderComponent(ele,DialogView,appContext,{
onOk,
onClose,
title:content
title:content,
parentNode:el
});
el.appendChild(ele);
async function onOk(){
@ -90,7 +92,8 @@ export class Dialog {
onOk,
onClose,
title:title,
input
input,
parentNode:el
});
el.appendChild(ele);
async function onOk(){
@ -123,7 +126,8 @@ export class Dialog {
events,
onOk,
onClose,
loading
loading,
parentNode:el
});
el.appendChild(ele);
async function onOk(){

View File

@ -1,6 +1,8 @@
<template>
<div style="position: absolute;left: 0;top: 0;width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;background-color: rgba(29,33,41,0.6);z-index: 1000" ref="root">
<div style="background-color: white;width: 60%;height:auto;max-height: 80%;border-radius: 5px;display: flex;flex-direction: column">
<div style="background-color: white;height:auto;max-height: 80%;border-radius: 5px;display: flex;flex-direction: column" :style="{
width:(parentNode.tagName==='BODY' || parentNode.id==='teamOS')?'40%':'60%'
}">
<div style="height: 35px;line-height: 35px;width: 100%;text-align: center;color: rgb(93,93,93);border-bottom: 1px solid gainsboro;flex: 1 1 auto">
<b>{{component?title:input?$t("util.input"):$t("util.alert")}}</b>
</div>
@ -27,6 +29,7 @@
import {provide, ref} from "vue";
const props=defineProps<{
parentNode:HTMLElement,
title:string,
component?:any,
props?:object,

View File

@ -0,0 +1,69 @@
<template>
<div>
<template v-if="!isEdit">
{{showValue}}
</template>
<template v-else>
<a-space size="mini">
<a-input-number v-model="editValue" :min="1" :max="100" :precision="0"></a-input-number>
<a-button type="text" @click="onClick">
<template #icon>
<icon-check></icon-check>
</template>
</a-button>
<a-button type="text" @click="onBlur">
<template #icon>
<icon-close style="color: red"></icon-close>
</template>
</a-button>
</a-space>
</template>
</div>
</template>
<script setup lang="ts">
import {ref, watch} from "vue";
import {apiIssue} from "../../../request/request";
const props=defineProps<{
isEdit:boolean,
showValue?:number,
projectIssueId:string
}>()
const emit=defineEmits<{
cancel:[],
update:[value:number]
}>()
const editValue=ref<number>()
const assignValue=()=>{
editValue.value=props.showValue
}
watch(()=>props.showValue,()=>{
assignValue()
},{
immediate:true,
deep:true
})
const onClick=async ()=>{
let res=await apiIssue.editBasicField({
projectIssueId:props.projectIssueId,
manDay:editValue.value
})
if(res?.code==0) {
emit("update",editValue.value)
}
}
const onBlur=()=>{
emit('cancel')
assignValue()
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,149 @@
<template>
<div>
<template v-if="!isEdit">
<a-space wrap size="mini" v-if="(showValue as ICommon_Model_Plan[]).length>0">
<a-link href="javascript:void(0)" v-for="item in (showValue as ICommon_Model_Plan[])" :key="item.id" @click="onOpenPlan(item.id)">{{item.name}}</a-link>
</a-space>
<span v-else style="line-height: 30px;width: 100%;color: grey">{{$t("util.none")}}</span>
</template>
<a-row style="padding-right: 10px" v-else>
<a-space size="mini" wrap>
<a-tag v-for="(item,index) in (editValue as {id:string,name:string}[])" :closable="true" @close="onCloseLabelTag(index)" :key="item.id">
{{item.name}}
</a-tag>
<a-select v-model="addValue" allow-search @search="onSearchPlan" v-if="showInput" @change="onAddChange">
<a-option v-for="item in labelList" :label="item.name" :value="item.id"></a-option>
</a-select>
<a-tag v-else :style="{backgroundColor: 'var(--color-fill-2)',border: '1px dashed var(--color-fill-3)',cursor: 'pointer',}" @click="showInput=true">
<template #icon>
<icon-plus />
</template>
{{$t("util.add")}}
</a-tag>
<a-button type="text" @click="onClick">
<template #icon>
<icon-check></icon-check>
</template>
</a-button>
<a-button type="text" @click="onBlur">
<template #icon>
<icon-close style="color: red"></icon-close>
</template>
</a-button>
</a-space>
</a-row>
</div>
</template>
<script setup lang="ts">
import {inject, ref, watch} from "vue";
import {injectProjectInfo} from "../../../util/symbol";
import {apiPlan} from "../../../request/request";
import {DCSType} from "../../../../../../../common/types";
import {ICommon_Model_Plan} from "../../../../../../../common/model/plan";
import {EClient_EVENTBUS_TYPE, eventBus} from "@/business/common/event/event";
const props=defineProps<{
isEdit:boolean,
showValue?:DCSType<ICommon_Model_Plan>[],
projectIssueId:string
}>()
const emit=defineEmits<{
cancel:[],
update:[value:DCSType<ICommon_Model_Plan>[]]
}>()
const labelList=ref<{
id:string,
name:string
}[]>([])
const editValue=ref<{
id:string,
name:string
}[]>()
const showInput=ref(false)
const addValue=ref("")
const projectId=inject(injectProjectInfo).id;
const assignValue=()=>{
editValue.value=props.showValue.length>0?props.showValue.map(item=>{
return {
id:item.id,
name:item.name
}
}):[]
showInput.value=false
addValue.value=""
}
watch(()=>props.showValue,()=>{
assignValue()
},{
immediate:true,
deep:true
})
const onCloseLabelTag=(index:number)=>{
editValue.value.splice(index,1)
}
const onAddChange=()=>{
let arr=editValue.value as {
id:string,
name:string
}[]
let index=labelList.value.findIndex((item)=>{
if(item.id==addValue.value) {
return true;
}
})
arr.push({
id:labelList.value[index].id,
name:labelList.value[index].name
})
addValue.value=""
showInput.value=false
}
const onSearchPlan=async (keyword:string)=>{
let res=await apiPlan.listPlan({
projectId,
keyword:keyword,
page:0,
size:10
})
if(res?.code==0) {
labelList.value=res.data.data
}
}
const onClick=async ()=>{
let arr=editValue.value as {
id:string,
name:string
}[]
let arrId=Array.from(new Set(arr.map(item=>item.id)));
let res=await apiPlan.issuePlanEdit({
projectIssueId:props.projectIssueId,
planList:arrId
})
if(res?.code==0) {
emit("update",res.data)
}
}
const onOpenPlan=(planId:string)=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_PROJECT_PLAN_PROFILE,projectId,planId)
}
const onBlur=()=>{
emit('cancel')
assignValue()
}
</script>
<style scoped>
</style>

View File

@ -7,5 +7,7 @@ export enum EClient_Field_Basic_Type {
MODULE,
PRIORITY,
FIXVERSIONS,
SPRINT
SPRINT,
MANDAY,
PLANS
}

View File

@ -1,17 +1,41 @@
<template>
<span style="display: inline-block;width: 100%;cursor: text" @mouseenter="onEnter" @mouseleave="onLeave" tabindex="-1" @focus="onFocus" ref="element">
<FieldEditBasicName :is-edit="isEdit" :show-value="showValue as string" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.NAME" @update="onUpdate" @cancel="onBlur"></FieldEditBasicName>
<FieldEditBasicDescription :is-edit="isEdit" :show-value="showValue as string" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.DESCRIPTION" @update="onUpdate" @cancel="onBlur"></FieldEditBasicDescription>
<FieldEditBasicAssigner :is-edit="isEdit" :show-value="showValue as User" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.ASSIGNER" @update="onUpdate" @cancel="onBlur"></FieldEditBasicAssigner>
<FieldEditBasicReporter :is-edit="isEdit" :show-value="showValue as User" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.REPORTER" @update="onUpdate" @cancel="onBlur"></FieldEditBasicReporter>
<FieldEditBasicPriority :is-edit="isEdit" :show-value="showValue as ECommon_Model_Project_Issue_Priority" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.PRIORITY" @update="onUpdate" @cancel="onBlur"></FieldEditBasicPriority>
<FieldEditBasicLabel :is-edit="isEdit" :show-value="showValue as ICommon_Model_Project_Label[]" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.LABEL" @update="onUpdate" @cancel="onBlur"></FieldEditBasicLabel>
<FieldEditBasicModule :is-edit="isEdit" :show-value="showValue as ICommon_Model_Project_Module[]" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.MODULE" @update="onUpdate" @cancel="onBlur"></FieldEditBasicModule>
<FieldEditBasicFixVersion :is-edit="isEdit" :show-value="showValue as DCSType<ICommon_Model_Project_Release>[]" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.FIXVERSIONS" @update="onUpdate" @cancel="onBlur"></FieldEditBasicFixVersion>
<span style="display: inline-block;width: 100%;cursor: text" @mouseenter="onEnter" @mouseleave="onLeave" tabindex="-1"
@focus="onFocus" ref="element">
<FieldEditBasicName :is-edit="isEdit" :show-value="showValue as string" :project-issue-id="projectIssueId"
v-if="type==EClient_Field_Basic_Type.NAME" @update="onUpdate"
@cancel="onBlur"></FieldEditBasicName>
<FieldEditBasicDescription :is-edit="isEdit" :show-value="showValue as string" :project-issue-id="projectIssueId"
v-if="type==EClient_Field_Basic_Type.DESCRIPTION" @update="onUpdate"
@cancel="onBlur"></FieldEditBasicDescription>
<FieldEditBasicAssigner :is-edit="isEdit" :show-value="showValue as User" :project-issue-id="projectIssueId"
v-if="type==EClient_Field_Basic_Type.ASSIGNER" @update="onUpdate"
@cancel="onBlur"></FieldEditBasicAssigner>
<FieldEditBasicReporter :is-edit="isEdit" :show-value="showValue as User" :project-issue-id="projectIssueId"
v-if="type==EClient_Field_Basic_Type.REPORTER" @update="onUpdate"
@cancel="onBlur"></FieldEditBasicReporter>
<FieldEditBasicPriority :is-edit="isEdit" :show-value="showValue as ECommon_Model_Project_Issue_Priority"
:project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.PRIORITY"
@update="onUpdate" @cancel="onBlur"></FieldEditBasicPriority>
<FieldEditBasicLabel :is-edit="isEdit" :show-value="showValue as ICommon_Model_Project_Label[]"
:project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.LABEL"
@update="onUpdate" @cancel="onBlur"></FieldEditBasicLabel>
<FieldEditBasicModule :is-edit="isEdit" :show-value="showValue as ICommon_Model_Project_Module[]"
:project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.MODULE"
@update="onUpdate" @cancel="onBlur"></FieldEditBasicModule>
<FieldEditBasicFixVersion :is-edit="isEdit" :show-value="showValue as DCSType<ICommon_Model_Project_Release>[]"
:project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.FIXVERSIONS"
@update="onUpdate" @cancel="onBlur"></FieldEditBasicFixVersion>
<FieldEditBasicSprint :is-edit="isEdit" :show-value="showValue as {
id:string,
name:string
}" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.SPRINT" @update="onUpdate" @cancel="onBlur"></FieldEditBasicSprint>
}" :project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.SPRINT" @update="onUpdate"
@cancel="onBlur"></FieldEditBasicSprint>
<FieldEditBasicManDay :is-edit="isEdit" :show-value="showValue as number" :project-issue-id="projectIssueId"
v-if="type==EClient_Field_Basic_Type.MANDAY" @update="onUpdate"
@cancel="onBlur"></FieldEditBasicManDay>
<FieldEditBasicPlan :is-edit="isEdit" :show-value="showValue as DCSType<ICommon_Model_Plan>[]"
:project-issue-id="projectIssueId" v-if="type==EClient_Field_Basic_Type.PLANS"
@update="onUpdate" @cancel="onBlur"></FieldEditBasicPlan>
</span>
</template>
@ -35,78 +59,93 @@ import FieldEditBasicModule from "./basic/fieldEditBasicModule.vue";
import FieldEditBasicFixVersion from "./basic/fieldEditBasicFixVersion.vue";
import FieldEditBasicSprint from "@/business/common/component/field/basic/fieldEditBasicSprint.vue";
import {DCSType} from "../../../../../../common/types";
import FieldEditBasicManDay from "@/business/common/component/field/basic/fieldEditBasicManDay.vue";
import {ICommon_Model_Plan} from "../../../../../../common/model/plan";
import FieldEditBasicPlan from "@/business/common/component/field/basic/fieldEditBasicPlan.vue";
type User={
id:string,
organizationUserId?:string,
photo?:string,
nickname?:string
type User = {
id: string,
organizationUserId?: string,
photo?: string,
nickname?: string
}
type Value=string|ECommon_Model_Project_Issue_Priority|User|ICommon_Model_Project_Label[]|ICommon_Model_Project_Module[]|number|ICommon_Model_Project_Release[]
type Props={
projectIssueId:string
type Value =
string
| ECommon_Model_Project_Issue_Priority
| User
| ICommon_Model_Project_Label[]
| ICommon_Model_Project_Module[]
| number
| ICommon_Model_Project_Release[]
type Props = {
projectIssueId: string
} & (
{
type:EClient_Field_Basic_Type.NAME | EClient_Field_Basic_Type.DESCRIPTION,
value?:string
type: EClient_Field_Basic_Type.NAME | EClient_Field_Basic_Type.DESCRIPTION,
value?: string
} | {
type:EClient_Field_Basic_Type.PRIORITY,
value?:ECommon_Model_Project_Issue_Priority
} | {
type:EClient_Field_Basic_Type.ASSIGNER | EClient_Field_Basic_Type.REPORTER,
value?:User
} | {
type:EClient_Field_Basic_Type.LABEL,
value?:ICommon_Model_Project_Label[]
} | {
type:EClient_Field_Basic_Type.MODULE,
value?:ICommon_Model_Project_Module[]
} | {
type:EClient_Field_Basic_Type.FIXVERSIONS,
value?:ICommon_Model_Project_Release[]
} | {
type:EClient_Field_Basic_Type.SPRINT,
value?:{
id:string,
name:string
type: EClient_Field_Basic_Type.PRIORITY,
value?: ECommon_Model_Project_Issue_Priority
} | {
type: EClient_Field_Basic_Type.ASSIGNER | EClient_Field_Basic_Type.REPORTER,
value?: User
} | {
type: EClient_Field_Basic_Type.LABEL,
value?: ICommon_Model_Project_Label[]
} | {
type: EClient_Field_Basic_Type.MODULE,
value?: ICommon_Model_Project_Module[]
} | {
type: EClient_Field_Basic_Type.FIXVERSIONS,
value?: ICommon_Model_Project_Release[]
} | {
type: EClient_Field_Basic_Type.SPRINT,
value?: {
id: string,
name: string
}
} | {
type: EClient_Field_Basic_Type.MANDAY,
value?: number
} | {
type: EClient_Field_Basic_Type.PLANS,
value?: ICommon_Model_Plan[]
})
const props = defineProps<Props>()
const element = ref<HTMLSpanElement>(null)
const showValue = ref(props.value)
const permission = inject(injectProjectInfo).permission
const isEdit = ref(false)
watch(() => props.value, (newValue, oldValue) => {
showValue.value = props.value
}, {
immediate: true,
deep: true
})
const onEnter = (event: MouseEvent) => {
if (!isEdit.value) {
const ele = event.currentTarget as HTMLElement
ele.style.backgroundColor = "rgb(230,231,237)"
}
}
)
const props=defineProps<Props>()
const element=ref<HTMLSpanElement>(null)
const showValue=ref(props.value)
const permission=inject(injectProjectInfo).permission
const isEdit=ref(false)
watch(()=>props.value,(newValue,oldValue)=>{
showValue.value=props.value
},{
immediate:true,
deep:true
})
const onEnter=(event:MouseEvent)=>{
if(!isEdit.value) {
const ele=event.currentTarget as HTMLElement
ele.style.backgroundColor="rgb(230,231,237)"
}
const onLeave = (event: MouseEvent) => {
const ele = event.currentTarget as HTMLElement
ele.style.backgroundColor = ""
}
const onLeave=(event:MouseEvent)=>{
const ele=event.currentTarget as HTMLElement
ele.style.backgroundColor=""
const onFocus = async (event: MouseEvent) => {
if (checkPermission(permission.value, Permission_Types.Project.EDIT)) {
isEdit.value = true
const ele = event.currentTarget as HTMLElement
ele.style.backgroundColor = ""
}
}
const onFocus=async (event:MouseEvent)=>{
if(checkPermission(permission.value,Permission_Types.Project.EDIT)) {
isEdit.value=true
const ele=event.currentTarget as HTMLElement
ele.style.backgroundColor=""
}
}
const onBlur=async ()=>{
isEdit.value=false
const onBlur = async () => {
isEdit.value = false
}
const onUpdate=(value)=>{
showValue.value=value
isEdit.value=false
const onUpdate = (value) => {
showValue.value = value
isEdit.value = false
}
</script>

View File

@ -0,0 +1,792 @@
<template>
<a-split min="700px" v-model:size="size" style="width: 100%;height: 100%;border: 1px solid lightgrey;box-sizing: border-box">
<template #first>
<div style="width: 100%;height: 100%;overflow: auto" ref="tableEle">
<a-table :columns="column" v-model:expanded-keys="expandedKeys" show-empty-tree hide-expand-button-on-empty :data="data" row-class="ganttRow" ref="dataEle" :bordered="{
headerCell:true,
bodyCell:true
}" :pagination="false" :scroll="{
y:'100%'
}" column-resizable stripe @cell-dblclick="onCellDbClick">
<template #type="{record}">
<slot name="type" :record="record as GanttDataItem"></slot>
</template>
<template #name="{record}">
<slot name="name" :record="record as GanttDataItem"></slot>
</template>
<template #manDay="{record}">
<slot name="manDay" :record="record as GanttDataItem"></slot>
</template>
<template #progress="{record}">
<slot name="progress" :record="record as GanttDataItem"></slot>
</template>
<template #depend="{record}">
<slot name="depend" :record="record as GanttDataItem"></slot>
</template>
<template #delay="{record}">
<slot name="delay" :record="record as GanttDataItem"></slot>
</template>
<template #startDate="{record}">
<slot name="startDate" :record="record as GanttDataItem"></slot>
</template>
<template #endDate="{record}">
<slot name="endDate" :record="record as GanttDataItem"></slot>
</template>
<template #operation="{record}">
<slot name="operation" :record="record as GanttDataItem"></slot>
</template>
</a-table>
</div>
</template>
<template #second>
<a-scrollbar style="width: 100%;height: 100%;overflow-x:auto;" :outer-style="{
height:'100%'
}" ref="scrollEle">
<div style="height: 82px;display: flex;">
<template v-if="type==='day'">
<div style="height: 100%;" v-for="item in monthList">
<div style="height: 41px;position: sticky;left: 0px;z-index: 1;display: flex;align-items: center;justify-content: center;width: 500px">
{{item.year()}}.{{item.month()+1}}
</div>
<div style="height: 41px;display: flex;box-sizing: border-box;border-top: 1px lightgrey solid;">
<div v-for="item1 in item.daysInMonth()" style="display: flex;justify-content: center;align-items: center;box-sizing: border-box" :style="{
borderRight:item1==item.daysInMonth()?'1px lightgray solid':'',
width:dayWidth+'px'
}">
{{item1}}
</div>
</div>
</div>
</template>
<template v-else-if="type==='month'">
<div style="height: 100%;" v-for="item in yearList">
<div style="height: 41px;position: sticky;left: 0px;z-index: 1;display: flex;align-items: center;justify-content: center;width: 500px">
{{item.year()}}
</div>
<div style="height: 41px;display: flex;box-sizing: border-box;border-top: 1px lightgrey solid;">
<div v-for="n in 12" style="display: flex;justify-content: center;align-items: center;box-sizing: border-box" :style="{
borderRight:n<12?'1px lightgray solid':'',
width:dayWidth*item.clone().set('month',n-1).daysInMonth()+'px'
}">
{{n}}
</div>
</div>
</div>
</template>
</div>
<div style="height: 1px;background-color: lightgrey" :style="{
width:width+'px'
}"></div>
<div style="height: calc(100% - 83px);overflow-y:auto;position: relative" :style="{
width:width+'px'
}" ref="linesEle" @scroll="onScroll" @mousemove="onMouseMove" @mouseup="onLineMouseUp">
<div v-for="(line,index) in lines" style="width: 100%;position: relative;box-sizing: border-box;border-bottom: 1px lightgrey solid;height: 41px" :style="{
backgroundColor:index%2==0?'':'rgb(247,248,250)'
}" class="line">
<template v-if="line.type===ECommon_Model_Plan_Table.STAGE || line.type===ECommon_Model_Plan_Table.ISSUE">
<a-popover position="bottom">
<div style="height: 15px;top:13px;position: absolute;border-radius: 3px;z-index: 1;overflow: hidden;" :id="line.key" :style="{
left:line.left+'px',
width:line.width+'px',
resize: (line.type===ECommon_Model_Plan_Table.ISSUE && !line.hasChild)?'horizontal':'none',
cursor:'move',
...(line.showProgress!=null?{
background:`linear-gradient(to right,${line.color} 0,${line.color} ${line.showProgress.toFixed(0)+'%'},${line.colorUndone} ${line.showProgress.toFixed(0)+'%'},${line.colorUndone} 100%)`
}:{
backgroundColor:line.color
})
}" @mousedown="onLineMouseDown(line,$event)" ></div>
<template #content>
<slot name="shortView" :data="findObj(data,line.key)"></slot>
</template>
</a-popover>
</template>
<template v-else-if="line.type===ECommon_Model_Plan_Table.MILESTONE">
<div style="top:13px;position: absolute;border-radius: 3px;transform: rotate(45deg)" :style="{
left:line.left+(type==='day'?30:10)/4+'px',
width:type==='day'?'15px':'10px',
backgroundColor:line.color,
height: type==='day'?'15px':'10px'
}" :id="line.key"></div>
</template>
</div>
<div v-for="item in lines.filter(item=>item.type===ECommon_Model_Plan_Table.MILESTONE)" style="position: absolute;width: 3px;" :style="{
top:(item.parentKey?((lines.findIndex(obj=>obj.key===item.parentKey)+1)*41):0)+'px',
backgroundColor:item.color,
left:item.left+(type==='day'?30:10)-3+'px',
height:((lines.findIndex(obj=>obj.key===item.key)-(item.parentKey?lines.findIndex(obj=>obj.key===item.parentKey):-1))*41)+'px'
}"></div>
<div style="position: absolute;width: 1px;top: 0px;background-color: purple" :style="{
left:moment(startDate).startOf('day').diff(startDay,'day')*dayWidth+'px',
height:(lines.length*41)+'px'
}"></div>
</div>
</a-scrollbar>
</template>
</a-split>
</template>
<script setup lang="ts">
import {computed, nextTick, onBeforeUnmount, onMounted, ref, watch} from "vue";
import {GanttDataItem, GanttLine} from "./types";
import moment from "moment";
import {TableData} from "@arco-design/web-vue";
import {ECommon_Model_Plan_Table} from "../../../../../../common/model/plan_table";
import {useI18n} from "vue-i18n";
const emit=defineEmits<{
change:[item:GanttDataItem,originalStartDate:number,originalEndDate:number,originalDalay:number],
move:[key:string,destKey:string,type:"in"|"top"|"bottom"]
}>()
const props=defineProps<{
data:GanttDataItem[]
startDate:number,
type:"day"|"month"
}>()
const tableEle=ref<HTMLElement>()
const tableScrollEle=ref<HTMLElement>()
const scrollEle=ref()
const linesEle=ref<HTMLElement>()
const isRightScrollByCode=ref(true)
const isLeftScrollByCode=ref(true)
let selectedItem:GanttDataItem=null
let selectType:"move"|"adjust"
let selectedElement:HTMLElement
let selectedStartDate:number,selectedDependEndDate:number
let originalStartDate:number,originalEndDate:number,originalDalay:number
let offsetX=0
const dayWidth=ref(0)
let tipElement:HTMLElement=null
const {t}=useI18n()
let dragInfo:{
dropAction:"in"|"top"|"bottom",
containerElement:HTMLElement,
key:string,
type:ECommon_Model_Plan_Table,
parentKey:string
}=null
const startMonth=computed(()=>{
return moment(props.startDate).startOf("month")
})
const startYear = computed(()=>{
return startMonth.value.clone().startOf("year")
})
const startDay=computed(()=>{
if(props.type==="day") {
return startMonth.value
} else if(props.type==="month") {
return startYear.value
}
})
const endYear=computed(()=>{
return endMonth.value.clone().endOf("year")
})
const yearList=computed(()=>{
let ret=[startYear.value]
while (1) {
let obj=ret.at(-1).clone().add(1,"year")
if(obj.year()<=endYear.value.year()) {
ret.push(obj)
} else {
break
}
}
return ret;
})
const endMonth=computed(()=>{
let max=0;
function _max(data:GanttDataItem[]) {
for(let obj of data) {
if(obj.endDate>max) {
max=obj.endDate
}
if(obj.children?.length>0) {
_max(obj.children)
}
}
}
if(props.data?.length>0) {
_max(props.data)
return moment(max).add(1,"month").endOf("month")
} else {
return moment(props.startDate).endOf("date")
}
})
const width=computed(()=>{
if(props.type==="day") {
return monthList.value.reduce((previousValue, currentValue) => {
return previousValue+currentValue.daysInMonth()*dayWidth.value
},0)
} else if(props.type==="month") {
return yearList.value.reduce((previousValue, currentValue) => {
return previousValue+currentValue.clone().endOf("year").dayOfYear()*dayWidth.value
},0)
}
})
const monthList=computed(()=>{
let ret=[startMonth.value]
let obj=ret.at(-1)
while(true) {
obj=obj.clone().add(1,"month")
if(obj.year()<endMonth.value.year() || (obj.year()==endMonth.value.year() && obj.month()<=endMonth.value.month())) {
ret.push(obj)
} else {
break
}
}
return ret;
})
const expandedKeys=ref([])
const column=[
{
title:t("util.name"),
slotName:"name",
headerCellStyle:{
height:"82px",
width:"200px",
overflow:"hidden",
textOverflow: "ellipsis"
},
bodyCellStyle:{
overflow:"hidden",
width:"200px",
textOverflow:"ellipsis"
}
},
{
title:t("util.type"),
slotName:"type",
headerCellStyle:{
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
},
},
{
title:t("util.manDay"),
slotName: "manDay",
headerCellStyle:{
width:"60px",
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
},
bodyCellStyle:{
width:"60px",
}
},
{
title:t("util.progress"),
slotName: "progress",
headerCellStyle:{
height:"82px",
width:"80px",
wordBreak:"break-all",
overflow:"hidden",
textOverflow: "ellipsis"
},
bodyCellStyle:{
width:"80px",
overflow:"hidden",
textOverflow:"ellipsis"
}
},
{
title:t("util.depend"),
slotName: "depend",
headerCellStyle:{
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
},
bodyCellStyle:{
overflow:"hidden",
textOverflow:"ellipsis",
}
},
{
title:t("util.delay"),
slotName: "delay",
headerCellStyle:{
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
}
},
{
title:t("util.startDate"),
slotName: "startDate",
headerCellStyle:{
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
}
},
{
title: t("util.endDate"),
slotName: "endDate",
headerCellStyle:{
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
}
},
{
title: t("util.operation"),
slotName: "operation",
headerCellStyle:{
height:"82px",
overflow:"hidden",
textOverflow: "ellipsis"
}
}
]
const size=ref(0.3)
const lines=ref<GanttLine[]>([])
watch(()=>props.type,()=>{
nextTick(()=>{
setTimeout(()=>{
if(lines.value.length>0) {
let ele=document.getElementById(lines.value[0].key)
if(ele) {
ele.scrollIntoView({
behavior:"smooth",
inline:"center"
})
}
}
},100)
})
})
watch(()=>[expandedKeys,props.data,props.type],()=>{
function _handle(data:GanttDataItem[],parentKey:string,startDate:number) {
for(let i=0;i<data.length;i++) {
let obj=data[i]
if(obj.type===ECommon_Model_Plan_Table.STAGE || obj.type===ECommon_Model_Plan_Table.ISSUE) {
lines.value.push({
key:obj.key,
left:moment(obj.startDate).endOf("day").diff(startDay.value,"day")*dayWidth.value,
width:(moment(obj.endDate).endOf("day").diff(obj.startDate,"day")+1)*dayWidth.value,
color:obj.type===ECommon_Model_Plan_Table.ISSUE?"rgb(85,171,251)":"green",
colorUndone:obj.type===ECommon_Model_Plan_Table.ISSUE?"rgba(85,171,251,0.5)":"rgb(156,215,176)",
type:obj.type,
depend:obj.depend,
parentKey,
progress:obj.progress,
showProgress:obj.showProgress,
hasChild:obj.children?.length>0,
})
if(obj.children?.length>0) {
if(expandedKeys.value.includes(obj.key)) {
_handle(obj.children,obj.key,obj.startDate)
}
}
} else if(obj.type===ECommon_Model_Plan_Table.MILESTONE) {
let maxEndDate=0;
for(let j=0;j<i;j++) {
maxEndDate=Math.max(data[j].endDate,maxEndDate)
}
lines.value.push({
key:obj.key,
left:moment(maxEndDate==0?startDate:maxEndDate).endOf("day").diff(startDay.value,"day")*dayWidth.value,
color:obj.completed?"#03ad03":"orange",
type:obj.type,
parentKey,
})
}
}
}
if(props.type==="day") {
dayWidth.value=30
} else if(props.type==="month") {
dayWidth.value=10
}
lines.value=[]
_handle(props.data,null,props.startDate)
nextTick(()=>{
if(tableEle.value) {
let rows=document.querySelectorAll(".ganttRow")
if(rows.length>0) {
rows.forEach((value, key, parent) => {
let ele=value as HTMLElement
ele.draggable=true
let id=lines.value[key].key
let type=lines.value[key].type
let parentKey=lines.value[key].parentKey
ele.setAttribute("type",String(type))
ele.ondragstart=ev => {
dragInfo={} as any
dragInfo.containerElement=document.createElement("div")
dragInfo.containerElement.style.position="absolute"
dragInfo.containerElement.style.pointerEvents="none"
dragInfo.containerElement.style.zIndex="1000"
document.body.appendChild(dragInfo.containerElement)
dragInfo.key=id
dragInfo.type=type
dragInfo.parentKey=parentKey
ev.dataTransfer.setDragImage(ele,0,0)
}
ele.ondragover=ev => {
dragInfo.dropAction=null
dragInfo.containerElement.style.display="none"
if(id===dragInfo.key) {
return
}
let tempKey=parentKey
while (tempKey) {
if(tempKey===dragInfo.key) {
return;
}
tempKey=lines.value.find(item=>item.key===tempKey)?.parentKey
}
ev.preventDefault()
ev.stopPropagation()
let currentEle=ev.currentTarget as HTMLElement
let rect=currentEle.getBoundingClientRect()
let tempEle=currentEle.querySelector(".arco-table-td-content") as HTMLElement
let firstTdRect=tempEle.getBoundingClientRect()
dragInfo.containerElement.style.backgroundColor=""
dragInfo.containerElement.style.border=""
if(dragInfo.type===ECommon_Model_Plan_Table.ISSUE) {
let isDragChildIssue=lines.value.find(item=>item.key===dragInfo.parentKey)?.type===ECommon_Model_Plan_Table.ISSUE
if(isDragChildIssue) {
if(type===ECommon_Model_Plan_Table.ISSUE && parentKey===dragInfo.parentKey) {
dragInfo.containerElement.style.backgroundColor="dodgerblue"
dragInfo.containerElement.style.height="2px"
dragInfo.containerElement.style.width=rect.width-(firstTdRect.left-rect.left)+"px"
dragInfo.containerElement.style.left=firstTdRect.left+"px"
dragInfo.containerElement.style.display="block"
if(ev.y-rect.top<10) {
dragInfo.dropAction="top"
dragInfo.containerElement.style.top=rect.top+"px"
} else if(rect.top+rect.height-ev.y<10) {
dragInfo.dropAction="bottom"
dragInfo.containerElement.style.top=rect.top+rect.height+"px"
} else {
return;
}
}
return;
}
}
dragInfo.containerElement.style.display="block"
if(type===ECommon_Model_Plan_Table.STAGE) {
if(ev.y-rect.top<10) {
dragInfo.dropAction="top"
dragInfo.containerElement.style.top=rect.top+"px"
dragInfo.containerElement.style.backgroundColor="dodgerblue"
dragInfo.containerElement.style.height="2px"
dragInfo.containerElement.style.width=rect.width-(firstTdRect.left-rect.left)+"px"
dragInfo.containerElement.style.left=firstTdRect.left+"px"
} else if(rect.top+rect.height-ev.y<10) {
dragInfo.dropAction="bottom"
dragInfo.containerElement.style.top=rect.top+rect.height+"px"
dragInfo.containerElement.style.backgroundColor="dodgerblue"
dragInfo.containerElement.style.height="2px"
dragInfo.containerElement.style.width=rect.width-(firstTdRect.left-rect.left)+"px"
dragInfo.containerElement.style.left=firstTdRect.left+"px"
} else {
let tempKey=dragInfo.parentKey
while (tempKey) {
if(tempKey===id) {
return;
}
tempKey=lines.value.find(item=>item.key===tempKey)?.parentKey
}
dragInfo.dropAction="in"
dragInfo.containerElement.style.top=rect.top+"px"
dragInfo.containerElement.style.border="2px solid dodgerblue"
dragInfo.containerElement.style.height=rect.height+"px"
dragInfo.containerElement.style.width=rect.width+"px"
dragInfo.containerElement.style.left=rect.left+"px"
}
} else if(type===ECommon_Model_Plan_Table.ISSUE) {
let isChildIssue=lines.value.find(item=>item.key===parentKey)?.type===ECommon_Model_Plan_Table.ISSUE
if(isChildIssue) {
return;
}
dragInfo.containerElement.style.backgroundColor="dodgerblue"
dragInfo.containerElement.style.height="2px"
dragInfo.containerElement.style.width=rect.width-(firstTdRect.left-rect.left)+"px"
dragInfo.containerElement.style.left=firstTdRect.left+"px"
if(ev.y-rect.top<10) {
dragInfo.dropAction="top"
dragInfo.containerElement.style.top=rect.top+"px"
} else if(rect.top+rect.height-ev.y<10) {
dragInfo.dropAction="bottom"
dragInfo.containerElement.style.top=rect.top+rect.height+"px"
} else {
dragInfo.containerElement.style.display="none"
}
} else if(type===ECommon_Model_Plan_Table.MILESTONE) {
dragInfo.containerElement.style.backgroundColor="dodgerblue"
dragInfo.containerElement.style.height="2px"
dragInfo.containerElement.style.width=rect.width-(firstTdRect.left-rect.left)+"px"
dragInfo.containerElement.style.left=firstTdRect.left+"px"
if(ev.y-rect.top<10) {
dragInfo.dropAction="top"
dragInfo.containerElement.style.top=rect.top+"px"
} else if(rect.top+rect.height-ev.y<10) {
dragInfo.dropAction="bottom"
dragInfo.containerElement.style.top=rect.top+rect.height+"px"
} else {
dragInfo.containerElement.style.display="none"
}
}
}
ele.ondrop=ev => {
if(dragInfo.dropAction) {
emit("move",dragInfo.key,id,dragInfo.dropAction)
}
if(dragInfo?.containerElement) {
dragInfo.containerElement.remove()
dragInfo.containerElement=null
}
dragInfo=null;
}
ele.ondragend=ev => {
if(dragInfo?.containerElement) {
dragInfo.containerElement.remove()
dragInfo.containerElement=null
}
dragInfo=null;
}
})
}
}
})
},{
immediate:true,
deep:true
})
const findObj=(data:GanttDataItem[],key:string):GanttDataItem=>{
for(let obj of data) {
if(obj.key===key) {
return obj
}
if(obj.children?.length>0) {
let ret=findObj(obj.children,key)
if(ret) {
return ret;
}
}
}
}
const onScroll=(ev:MouseEvent)=>{
if(isRightScrollByCode.value) {
isRightScrollByCode.value=false
return
}
let ele=ev.currentTarget as HTMLElement
if(tableScrollEle.value) {
isLeftScrollByCode.value=true
tableScrollEle.value.scrollTo({
top:ele.scrollTop,
behavior:"smooth"
})
}
}
const onCellDbClick=(record:TableData)=>{
if(lines.value.length>0) {
let ele=document.getElementById(record.key)
if(ele) {
ele.scrollIntoView({
behavior:"smooth",
inline:"start"
})
}
}
}
const onLineMouseDown=(line:GanttLine,event:MouseEvent)=>{
let ele=event.currentTarget as HTMLElement
let obj=findObj(props.data,line.key)
selectedItem=obj
selectedElement=ele
if(ele.offsetWidth-event.offsetX<20 && ele.style.resize!=="none") {
selectType="adjust"
} else {
selectType="move"
offsetX=event.offsetX
}
originalStartDate=selectedItem.startDate
originalEndDate=selectedItem.endDate
originalDalay=selectedItem.delay
if(line.parentKey) {
let obj=findObj(props.data,line.parentKey)
selectedStartDate=obj.startDate
if(line.depend) {
let obj=findObj(props.data,line.depend)
selectedDependEndDate=obj.endDate
} else {
selectedDependEndDate=obj.startDate
}
} else {
selectedStartDate=props.startDate
selectedDependEndDate=props.startDate
}
}
const onLineMouseUp=(event:MouseEvent)=>{
if(selectedItem) {
if(selectType==="adjust") {
let rect=selectedElement.getBoundingClientRect()
let days=Math.floor(rect.width/dayWidth.value)
let endDate=moment(selectedItem.startDate).clone().add(days,"day").toDate().getTime()
selectedItem.endDate=endDate
selectedItem.manDay=moment(endDate).endOf("day").diff(selectedItem.startDate,"day")+1
}
selectedElement=null
offsetX=0
emit("change",selectedItem,originalStartDate,originalEndDate,originalDalay)
selectedItem=null
originalStartDate=null
originalEndDate=null
if(tipElement) {
tipElement.remove()
tipElement=null
}
}
}
const onMouseMove=(event:MouseEvent)=>{
if(selectedItem) {
if(selectType==="move") {
let ele=event.currentTarget as HTMLElement
let rect=ele.getBoundingClientRect()
let left=event.x-offsetX-rect.x
let realLeft=left+ele.offsetLeft
let days=Math.floor(realLeft/dayWidth.value)
let startDate=startDay.value.clone().add(days,"day").startOf("day")
days=moment(selectedItem.endDate).endOf("day").diff(selectedItem.startDate,"day")
if(startDate.diff(selectedStartDate,"days")>=0) {
selectedItem.startDate=startDate.toDate().getTime()
}
selectedItem.endDate=moment(selectedItem.startDate).add(days,"days").toDate().getTime()
selectedItem.delay=moment(selectedItem.startDate).endOf("day").diff(selectedDependEndDate,"day")
let rectScroll=scrollEle.value.$el.getBoundingClientRect()
left=event.x-rectScroll.left
if(left<20) {
scrollEle.value.$el.children[0].scrollBy(-10,0)
} else if(left>rectScroll.width-20) {
scrollEle.value.$el.children[0].scrollBy(10,0)
}
}
if(props.type=="month") {
if(!tipElement) {
tipElement=document.createElement("div")
tipElement.style.position="absolute"
tipElement.style.background="rgba(0,0,255,0.1)"
tipElement.style.height=Math.floor(selectedElement.parentElement.offsetTop/41+1)*41+"px"
tipElement.style.top="0px"
linesEle.value.appendChild(tipElement)
let left=document.createElement("div")
left.style.paddingRight="10px"
left.setAttribute("start_date","")
left.style.boxSizing="border-box"
left.style.position="absolute"
left.style.height="20px"
left.style.left="-70px"
left.style.bottom="10px"
left.style.width="70px"
left.style.zIndex="100"
left.style.overflow="visible"
left.style.color="blue"
left.style.textAlign="right"
left.style.zIndex="1000"
tipElement.appendChild(left)
let right=document.createElement("div")
right.style.boxSizing="border-box"
right.style.paddingLeft="10px"
right.setAttribute("end_date","")
right.style.position="absolute"
right.style.height="20px"
right.style.right="-70px"
right.style.bottom="10px"
right.style.width="70px"
right.style.zIndex="100"
right.style.overflow="visible"
right.style.color="blue"
right.style.textAlign="left"
tipElement.appendChild(right)
}
nextTick(()=>{
tipElement.style.left=selectedElement.offsetLeft+"px";
tipElement.style.width=selectedElement.offsetWidth+"px";
(tipElement.querySelector("[start_date]") as HTMLElement).innerText=moment(selectedItem.startDate).format("MM-DD");
if(selectType==="move") {
(tipElement.querySelector("[end_date]") as HTMLElement).innerText=moment(selectedItem.endDate).format("MM-DD");
} else if(selectType==="adjust") {
(tipElement.querySelector("[end_date]") as HTMLElement).innerText=moment(selectedItem.startDate).add(Math.floor(selectedElement.offsetWidth/dayWidth.value),"day").format("MM-DD")
}
})
}
}
}
const tableScrollFunc=(ev:MouseEvent) => {
if(isLeftScrollByCode.value) {
isLeftScrollByCode.value=false
return
}
let ele = ev.currentTarget as HTMLElement
isRightScrollByCode.value=true
linesEle.value.scrollTo({
top: ele.scrollTop,
behavior: "smooth"
})
}
onMounted(()=>{
tableScrollEle.value=tableEle.value.querySelectorAll(".arco-scrollbar-container.arco-table-body").item(0) as HTMLElement
if(tableScrollEle.value) {
tableScrollEle.value.addEventListener("scroll",tableScrollFunc)
}
if(lines.value.length>0) {
let ele=document.getElementById(lines.value[0].key)
if(ele) {
setTimeout(()=>{
ele.scrollIntoView({
behavior:"smooth",
inline:"center"
})
},100)
}
}
})
onBeforeUnmount(()=>{
})
</script>
<style scoped>
:global(.ganttRow) {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: move;
height: 41px;
}
.line:hover {
background-color: #f8f8f8;
}
:deep thead span {
padding: 0px 5px!important;
}
</style>

View File

@ -0,0 +1,34 @@
import {ECommon_Model_Plan_Table} from "../../../../../../common/model/plan_table";
export type GanttDataItem={
key:string,
type:ECommon_Model_Plan_Table,
uniqueId?:string, // issue
projectIssueId?:string // issue
name:string,
manDay?:number, //set :issue,calc:plan
depend?:string,
delay?:number,
progress?:number, //issue,plan
showProgress?:number
completed?:boolean, // milestone
startDate?:number, //plan,issue
endDate?:number,
children?:GanttDataItem[] //issue,plan
parentId:string
}
export type GanttLine={
key:string,
left:number,
width?:number,
color:string,
colorUndone?:string,
type:ECommon_Model_Plan_Table,
parentKey:string,
progress?:number,
showProgress?:number
depend?:string,
delay?:number,
hasChild?:boolean,
}

View File

@ -26,6 +26,7 @@ export class MeetingClient {
private defaultVideo=true
private defaultAudio=true
private defaultCameraId:string
private defaultAudioId:string
onProducerStateChange:(state:"new"|"close"|"pause"|"resume", kind: mediaSoup.types.MediaKind, businessId:string,type:"data"|"camera"|"screen",stream?: MediaStream,producerId?:string)=>void
onLocalProducerInit:(stream:MediaStream)=>void
onLocalProducerStart:(kind:MediaKind)=>void
@ -96,6 +97,13 @@ export class MeetingClient {
name:item.label
}))
}
static async enumAudioDevice() {
let ret=await navigator.mediaDevices.enumerateDevices()
return ret.filter(item=>item.kind==="audioinput").map(item=>({
id:item.deviceId,
name:item.label
}))
}
static async checkVideoStream(id:string) {
let stream=await navigator.mediaDevices.getUserMedia({
video:{
@ -110,7 +118,7 @@ export class MeetingClient {
getRoomInfo() {
return this.roomInfo
}
async join(roomId:string,extraData:any,isVideo=true,isAudio=true,cameraId?:string):Promise<{
async join(roomId:string,extraData:any,isVideo=true,isAudio=true,cameraId?:string,audioId?:string):Promise<{
success:boolean,
msg?:string
}> {
@ -123,6 +131,7 @@ export class MeetingClient {
this.defaultVideo=isVideo
this.defaultAudio=isAudio
this.defaultCameraId=cameraId
this.defaultAudioId=audioId
let ret=await this.socket.emitWithAck("joinRoom",roomId,extraData)
if(ret) {
this.roomInfo=ret;
@ -502,7 +511,10 @@ export class MeetingClient {
const mediaConstraints:MediaStreamConstraints = {
audio: {
echoCancellation:true,
noiseSuppression:true
noiseSuppression:true,
...(this.defaultAudioId && {
deviceId:this.defaultAudioId
})
},
video: (isVideo && this.defaultCameraId)?{
deviceId:this.defaultCameraId

View File

@ -17,6 +17,7 @@ export enum EClient_EVENTBUS_TYPE {
OPEN_PROJECT_BOARD_PROFILE="open_project_sprint_board",
OPEN_PROJECT_SPRINT_KANBAN_PROFILE="open_project_sprint_kanban_profile",
OPEN_PROJECT_RELEASE_PROFILE="open_project_release_profile",
OPEN_PROJECT_PLAN_PROFILE="open_project_plan_profile",
OPEN_WIKI_PROFILE="open_wiki_profile",
OPEN_WIKI_ITEM="open_wiki_item",
UPDATE_USER_INFO="update_user_info",
@ -84,6 +85,7 @@ interface IClient_EventBus_Func {
[EClient_EVENTBUS_TYPE.USER_LOGIN_EXPIRED]:()=>void
[EClient_EVENTBUS_TYPE.GUIDE]:()=>void
[EClient_EVENTBUS_TYPE.ORGANIZATION_REMOVE]:(organizationId:string)=>void
[EClient_EVENTBUS_TYPE.OPEN_PROJECT_PLAN_PROFILE]:(projectId:string,planId:string)=>void
}
interface IClient_EventBus_Emit_Func {

View File

@ -17,6 +17,7 @@ import finder from "../../../../../common/routes/finder"
import notification from "../../../../../common/routes/notification"
import board from "../../../../../common/routes/board"
import tool from "../../../../../common/routes/tool"
import plan from "../../../../../common/routes/plan"
import {Ref} from "vue";
import {SessionStorage} from "../storage/session";
import {DCSType} from "../../../../../common/types";
@ -171,3 +172,4 @@ export const apiFinder=generatorApi(finder)
export const apiNotification=generatorApi(notification)
export const apiBoard=generatorApi(board)
export const apiTool=generatorApi(tool)
export const apiPlan=generatorApi(plan)

View File

@ -12,9 +12,10 @@ export interface IClient_SessionStorage_Type {
}
organizationUserId:string,
firstShow:string,
wechatOpenId:string
}
let keys:(keyof IClient_SessionStorage_Type)[]=["organizationId","userId","userToken","imRecentList","organizationUserId","firstShow"]
let keys:(keyof IClient_SessionStorage_Type)[]=["organizationId","userId","userToken","imRecentList","organizationUserId","firstShow","wechatOpenId"]
export class SessionStorage {
static get<T extends keyof IClient_SessionStorage_Type>(name:T):IClient_SessionStorage_Type[T] {

View File

@ -63,7 +63,7 @@
<a-form :model="{}" layout="vertical">
<a-form-item :label="$t('util.calendar')">
<a-select size="small" v-model="searchForm.calendarId">
<a-option value="all">{{$t("util.all")}}</a-option>
<a-option :value="''">{{$t("util.all")}}</a-option>
<a-option v-for="item in calendarList" :value="item.id">{{item.name}}</a-option>
</a-select>
</a-form-item>
@ -115,7 +115,7 @@
</a-form-item>
<a-form-item :label="$t('controller.app.calendar.calendar.startWeekDay')">
<a-select v-model="settingEdit.start_week_day">
<a-option v-for="item in ECommon_Calendar_WeekDay" :value="item">{{calendarWeekDayName[item]}}</a-option>
<a-option v-for="item in ECommon_Calendar_WeekDay" :value="item">{{calendarWeekDayName[item]?$t("util."+calendarWeekDayName[item].toLowerCase()):undefined}}</a-option>
</a-select>
</a-form-item>
</a-form>
@ -170,6 +170,7 @@ import {DCSType} from "../../../../../../common/types";
const props=defineProps<{
calendarEventId?:string
}>()
const {t}=useI18n()
const appContext=getCurrentInstance().appContext
const root=getRootNavigatorRef()
const isCalendarEventAddSimple=ref(false)
@ -193,14 +194,14 @@ const calendarEventList=ref<IClient_Calendar_Info[]>([])
const organizationUserId=SessionStorage.get("organizationUserId")
const searchForm=reactive({
keyword:"",
calendarId:"All",
calendarId:"",
startDate:null,
endDate:null,
location:""
})
const searchResultList=ref<DCSType<ICommon_Route_Res_Calendar_ListEvent_Item>[]>([])
let searchDebounce=null
const {t}=useI18n()
provide(injectCalendarSetting,setting)
const startDay=computed(()=>{
let start_week_day=setting.value?.start_week_day
@ -548,7 +549,7 @@ const onSearch=async ()=>{
let res=await apiCalendar.searchCalendarEvent({
location:searchForm.location,
keyword:searchForm.keyword,
...(searchForm.calendarId!=="All" && {
...(searchForm.calendarId!=="" && {
calendarId:searchForm.calendarId
}),
...(searchForm.startDate && {

View File

@ -27,7 +27,7 @@
<template v-if="data.recurring===ECommon_Calendar_Recurring_Type.WEEK">
{{$t("controller.app.calendar.calendarEventDateEdit.selectWeekday")}}:
<a-select size="small" style="width: 130px" v-model="recurryingWeekDay" @change="onChangeRecurry">
<a-option v-for="item in ECommon_Calendar_WeekDay" :value="item">{{calendarWeekDayName[item]}}</a-option>
<a-option v-for="item in ECommon_Calendar_WeekDay" :value="item">{{calendarWeekDayName[item]?$t("util."+calendarWeekDayName[item].toLowerCase()):undefined}}</a-option>
</a-select>
</template>
<template v-else-if="data.recurring===ECommon_Calendar_Recurring_Type.MONTH">

View File

@ -91,7 +91,7 @@
<RichEditor v-model="content" v-if="type===ECommon_IM_Message_EntityType.USER" key="user" style="width: 100%;min-height: 50px" @upload-file="onUploadFile" :pop-menu-list="popMenuList" @pop-menu-click="onPopMenuClick" @custom-anchor-click="onCustomAnchorClick" ref="objEditorUser" @meta-enter="onSend"></RichEditor>
<RichEditor v-model="content" v-else key="team" style="width: 100%" :pop-menu-list="popMenuList" @pop-menu-click="onPopMenuClick" @custom-anchor-click="onCustomAnchorClick" @quote-list="onQuoteList" ref="objEditorTeam" @meta-enter="onSend"></RichEditor>
</div>
<a-button type="primary" style="margin-top: 2px;height: auto;flex: 0 0 60px;border-radius: 5px"
<a-button type="primary" style="margin-top: 2px;height: 60px;flex: 0 0 60px;border-radius: 5px;align-self: end"
tabindex="-1" @click="onSend">
<template #icon>
<icon-send style="color: white;font-size: x-large"></icon-send>

View File

@ -10,6 +10,13 @@
</div>
</div>
</a-form-item>
<a-form-item field="audioId" :label="$t('util.selectAudio')">
<div style="width: 80%">
<a-select v-model="form.audioId" :placeholder="$t('placeholder.meetingPreview')">
<a-option v-for="item in audioList" :value="item.id">{{item.name}}</a-option>
</a-select>
</div>
</a-form-item>
<a-form-item field="enableVideo" :label="$t('util.videoOn')">
<a-switch v-model="form.enableVideo"></a-switch>
</a-form-item>
@ -32,12 +39,17 @@ const formEle=ref()
const form=reactive({
cameraId:"",
enableVideo:true,
enableAudio:true
enableAudio:true,
audioId:""
})
const cameraList=ref<{
id:string,
name:string
}[]>([])
const audioList=ref<{
id:string,
name:string
}[]>([])
const stream=ref<MediaStream>()
watch(()=>form.cameraId,async ()=>{
if(form.cameraId) {
@ -47,8 +59,15 @@ watch(()=>form.cameraId,async ()=>{
const getCameraList=async ()=>{
cameraList.value=(await MeetingClient.enumVideoDevice()).filter(item=>item.id!=="")
}
const getAudioList =async () => {
audioList.value=(await MeetingClient.enumAudioDevice()).filter(item=>item.id!=="")
}
onBeforeMount(()=>{
getCameraList()
getAudioList()
})
onBeforeUnmount(()=>{
stream.value?.getVideoTracks()[0]?.stop()

View File

@ -250,7 +250,7 @@ const initMeeting=async ()=>{
}
let preview:any=await Dialog.open(root.value,appContext,t("controller.app.meeting.meetingProfile.meetingPreview"),markRaw(MeetingPreview))
if(preview) {
let ret=await meetingClient.join(props.meetingId,password,preview.enableVideo,preview.enableAudio,preview.cameraId)
let ret=await meetingClient.join(props.meetingId,password,preview.enableVideo,preview.enableAudio,preview.cameraId,preview.audioId)
if(!ret?.success) {
Message.error(ret.msg)
navigator.pop()

View File

@ -13,11 +13,11 @@
</a-button>
</a-space>
</a-row>
<div style="width: 100%;margin-top: 20px;overflow: auto;height: calc(100% - 90px)">
<div style="width: 100%;margin-top: 20px;overflow: auto;height: calc(100% - 140px)">
<Card v-if="sprintInfo" :data="cardData" type="fixed" :gap="10" :rect="{
width:230,
height:swimLaneList.length*250+30
}" readonly>
}" readonly style="overflow: visible">
<template #header="props">
<a-row style="height: 100%;display: flex;align-items: center;justify-content: center;padding: 0 5px;box-sizing: border-box">
<span style="color: grey">{{props.item.name}}</span>

View File

@ -32,7 +32,7 @@
<a-space>
<a-dropdown trigger="hover">
<a-button size="mini" type="primary" :status="item.status===ECommon_Model_Board_Sprint_Status.COMPLETED?'success':'normal'" @click="$event.stopPropagation(),$event.preventDefault()">
{{item.status===ECommon_Model_Board_Sprint_Status.NOTSTART?"Not Start":item.status===ECommon_Model_Board_Sprint_Status.STARTING?"Starting":"Completed"}}
{{item.status===ECommon_Model_Board_Sprint_Status.NOTSTART?$t("util.notStart"):item.status===ECommon_Model_Board_Sprint_Status.STARTING?$t("util.starting"):$t("util.complete")}}
</a-button>
<template #content>
<a-doption size="mini" type="primary" status="success" @click="onStartSprint(item,$event)" v-if="item.status!==ECommon_Model_Board_Sprint_Status.STARTING">

View File

@ -38,6 +38,9 @@
</a-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('util.manDay')" :extra="$t('help.manDay')">
<a-input-number v-model="form.manDay" :min="1" :max="100" :precision="0"></a-input-number>
</a-form-item>
</a-form>
<a-divider orientation="left" v-if="valueList.length>0">Custom Fields</a-divider>
<a-form v-if="valueList.length>0" :model="formCustom" ref="eleFormCustom" layout="vertical">
@ -88,7 +91,8 @@ const form=reactive({
description:[],
priority:ECommon_Model_Project_Issue_Priority.MEDIUM,
assigner:"",
reporter:""
reporter:"",
manDay:1
})
const loading=ref(false)
const objEditor=ref<InstanceType<typeof RichEditor>>()
@ -186,7 +190,8 @@ onDialogOk(dialogFuncGenerator({
priority :form.priority,
assignerId:form.assigner ,
reporterId:form.reporter ,
values:valueList.value.map(item=>item.fieldValue)
values:valueList.value.map(item=>item.fieldValue),
manDay:form.manDay
})
if(res?.code==0) {
if(form.description.length>0) {

View File

@ -1,7 +1,7 @@
<template>
<div class="issueProfileDetail">
<a-collapse :default-active-key="['detail']">
<a-collapse-item key="detail" :header="$t('util.detail')">
<div>
<a-tabs type="rounded" size="small" class="issueProfileDetail">
<a-tab-pane key="detail" :title="$t('util.detail')">
<a-form layout="vertical" :model="{}">
<a-form-item :label="$t('util.issueType')">
{{info.issueType.name}}
@ -15,6 +15,9 @@
<a-form-item :label="$t('util.priority')">
<FieldEditBasic :project-issue-id="info.id" :type="EClient_Field_Basic_Type.PRIORITY" :value="info.priority as ECommon_Model_Project_Issue_Priority"></FieldEditBasic>
</a-form-item>
<a-form-item :label="$t('util.manDay')">
<FieldEditBasic :project-issue-id="info.id" :type="EClient_Field_Basic_Type.MANDAY" :value="info.man_day"></FieldEditBasic>
</a-form-item>
<a-form-item :label="$t('util.module')">
<FieldEditBasic :project-issue-id="info.id" :type="EClient_Field_Basic_Type.MODULE" :value="moduleList"></FieldEditBasic>
</a-form-item>
@ -27,16 +30,19 @@
<a-form-item :label="$t('util.sprint')">
<FieldEditBasic :project-issue-id="info.id" :type="EClient_Field_Basic_Type.SPRINT" :value="sprintInfo"></FieldEditBasic>
</a-form-item>
<a-form-item :label="$t('util.plan')">
<FieldEditBasic :project-issue-id="info.id" :type="EClient_Field_Basic_Type.PLANS" :value="planList"></FieldEditBasic>
</a-form-item>
</a-form>
</a-collapse-item>
<a-collapse-item key="more" :header="$t('util.more')" v-if="fieldList?.length>0">
</a-tab-pane>
<a-tab-pane key="more" :title="$t('util.more')" v-if="fieldList?.length>0">
<a-form layout="vertical" :model="{}" v-if="fieldList.length>0">
<a-form-item v-for="item in fieldList" :label="item.nodeField.field.name" :key="item.issueFieldValue.id">
<FieldEdit :item="item"></FieldEdit>
</a-form-item>
</a-form>
</a-collapse-item>
</a-collapse>
</a-tab-pane>
</a-tabs>
<a-row style="margin-top: 10px;color: grey;font-size: 12px;line-height: 1.3;padding-left: 5px;margin-bottom: 10px">
{{$t("util.created")}} {{moment(info.created_time).format('YYYY-MM-DD HH:mm:ss')}}
</a-row>
@ -49,7 +55,7 @@ import {EClient_Field_Basic_Type} from "../../../../common/component/field/field
import moment from "moment/moment";
import FieldEdit from "../../../../common/component/field/fieldEdit.vue";
import FieldEditBasic from "../../../../common/component/field/fieldEditBasic.vue";
import {apiBoard, apiIssue} from "../../../../common/request/request";
import {apiBoard, apiIssue, apiPlan} from "../../../../common/request/request";
import {
ICommon_Route_Res_ProjectIssue_BasicInfo,
ICommon_Route_Res_ProjectIssue_fieldsInfo
@ -60,6 +66,7 @@ import {ICommon_Model_Project_Module} from "../../../../../../../common/model/pr
import {ICommon_Model_Project_Release} from "../../../../../../../common/model/project_release";
import {DCSType} from "../../../../../../../common/types";
import {ECommon_Model_Project_Issue_Priority} from "../../../../../../../common/model/project_issue";
import {ICommon_Model_Plan} from "../../../../../../../common/model/plan";
const props=defineProps<{
info:DCSType<ICommon_Route_Res_ProjectIssue_BasicInfo>,
@ -72,6 +79,7 @@ const sprintInfo=ref<{
id:string,
name:string
}>()
const planList=ref<DCSType<ICommon_Model_Plan>[]>([])
const getReleaseList=async ()=>{
let res=await apiIssue.releaseList({
projectIssueId:props.info.id
@ -95,9 +103,19 @@ const getSprintInfo=async ()=>{
}
}
const getPlanList=async ()=>{
let res=await apiPlan.issuePlanList({
projectIssueId:props.info.id
})
if(res?.code==0) {
planList.value=res.data
}
}
onBeforeMount(()=>{
getReleaseList()
getSprintInfo()
getPlanList()
})
</script>
@ -114,4 +132,8 @@ onBeforeMount(()=>{
.issueProfileDetail :deep .arco-form-item-label {
font-weight: bold;
}
.issueProfileDetail {
padding-left: 5px;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<a-form :model="form" ref="eleForm">
<a-form-item :label="$t('util.name')" field="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item :label="$t('util.startDate')" field="startTime">
<a-date-picker v-model="form.startTime"></a-date-picker>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {apiPlan} from "../../../../common/request/request";
import {reactive, ref} from "vue";
import {onDialogOk} from "../../../../common/component/dialog/dialog";
import {dialogFuncGenerator} from "../../../../common/util/helper";
import {DCSType} from "../../../../../../../common/types";
import {ICommon_Model_Plan} from "../../../../../../../common/model/plan";
import moment from "moment";
const props=defineProps<{
type:"add"|"edit",
projectId?:string,
item?:DCSType<ICommon_Model_Plan>
}>()
const form=reactive({
name:props.type=="edit"?props.item.name:"",
startTime:props.type==="edit"?moment(props.item.start_time).toDate().getTime():moment().startOf("day").toDate().getTime()
})
const eleForm=ref(null)
onDialogOk(dialogFuncGenerator({
func:()=>{
return props.type=="add"?apiPlan.createPlan({
projectId:props.projectId,
name:form.name,
startTime:moment(form.startTime).startOf("day").toDate().getTime()
}):apiPlan.editPlan({
planId:props.item.id,
name:form.name,
startTime:moment(form.startTime).startOf("day").toDate().getTime()
})
},
form:()=>{
return eleForm.value
}
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,86 @@
<template>
<a-form auto-label-width :model="form" ref="eleForm">
<a-form-item field="projectIssueId" :label="$t('util.issue')" required v-if="type==='add'">
<a-select v-model="form.projectIssueId" @search="onSearch" allow-search>
<a-option v-for="item in issueList" :value="item.id">
{{item.project.keyword}}-{{item.unique_id}}&nbsp;{{item.name}}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="manDay" :label="$t('util.manDay')" v-if="type==='edit'">
<a-input-number v-model="form.manDay" :min="1" :max="100" :precision="0"></a-input-number>
</a-form-item>
<a-form-item field="dependId" :label="$t('util.depend')">
<a-select v-model="form.dependId" allow-clear>
<a-option v-for="item in dependList" :label="item.name" :value="item.key"></a-option>
</a-select>
</a-form-item>
<a-form-item field="delay" :label="$t('util.delay')">
<a-input-number :precision="0" :min="-100" :max="100" v-model="form.delay"></a-input-number>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {GanttDataItem} from "@/business/common/component/gantt/types";
import {reactive, ref} from "vue";
import {onDialogOk} from "@/business/common/component/dialog/dialog";
import {dialogFuncGenerator} from "@/business/common/util/helper";
import {apiIssue, apiPlan} from "@/business/common/request/request";
import {DCSType} from "../../../../../../../common/types";
import {ICommon_Route_Res_Project_Issue_filter_Item} from "../../../../../../../common/routes/response";
const props=defineProps<{
projectId:string,
type:"add"|"edit",
item?:GanttDataItem,
planId:string,
dependList:GanttDataItem[],
parentId?:string
}>()
const form=reactive({
id:props.type==="edit"?props.item.key:"",
delay:props.type==="edit"?props.item.delay:0,
dependId:props.type==="edit"?props.item.depend:"",
manDay:props.type==="edit"?props.item.manDay:1,
projectIssueId:props.type==="edit"?props.item.projectIssueId:"",
})
const eleForm=ref()
const issueList=ref<DCSType<ICommon_Route_Res_Project_Issue_filter_Item>[]>([])
const onSearch=async (keyword:string)=>{
let res=await apiIssue.filter({
projectId:props.projectId,
name:keyword,
page:0,
size:20
})
if(res?.code==0) {
issueList.value=res.data.data
}
}
onDialogOk(dialogFuncGenerator({
func:()=>{
return props.type=="add"?apiPlan.addIssue({
planId:props.planId,
projectIssueId:form.projectIssueId,
dependId:form.dependId,
delay:form.delay,
parentId:props.parentId
}):apiPlan.editIssue({
planItemId:form.id,
manDay:form.manDay,
delay:form.delay,
dependId:form.dependId
})
},
form:()=>{
return eleForm.value
}
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,147 @@
<template>
<div>
<a-alert closable style="margin-bottom: 10px">
{{$t("help.plan")}}
</a-alert>
<a-row>
<a-space wrap>
<a-input-search v-model="keyword" :placeholder="$t('placeholder.typePlanName')" style="width: 250px" @search="onSearch" v-if="checkPermission(permission,Permission_Types.Project.READ)"></a-input-search>
<a-button type="primary" @click="onCreate" v-if="checkPermission(permission,Permission_Types.Project.CREATE)">{{$t("util.create")}}</a-button>
</a-space>
</a-row>
<a-table style="margin-top: 10px" :columns="columns" :data="planList" :pagination="pagination" @pageChange="onPageChange">
<template #name="{record}">
<a-link href="javascript:void(0)" @click="onProfile(record,$event)">{{record.name}}</a-link>
</template>
<template #startDate="{record}">
{{moment(record.start_time).format("YYYY-MM-DD")}}
</template>
<template #operation="{record}">
<a-space v-if="checkPermission(permission,Permission_Types.Project.EDIT)" wrap>
<a-button size="small" @click="onEdit(record)">{{$t("util.edit")}}</a-button>
<a-button size="small" status="danger" @click="onDelete(record)" v-if="checkPermission(permission,Permission_Types.Project.DELETE) || record?.created_by.id===myOrganizationUserId">{{$t("util.delete")}}</a-button>
</a-space>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {getCurrentInstance, inject, markRaw, onBeforeMount, reactive, ref} from "vue";
import {injectProjectInfo} from "../../../../common/util/symbol";
import {apiPlan} from "../../../../common/request/request";
import {checkPermission, Permission_Types} from "../../../../../../../common/permission/permission";
import {Dialog} from "../../../../common/component/dialog/dialog";
import {
ETeamOS_Navigator_Action,
getCurrentNavigator,
getRootNavigatorRef,
onNavigatorShow
} from "../../../../../teamOS/common/component/navigator/navigator";
import {Message} from "@arco-design/web-vue";
import {useI18n} from "vue-i18n";
import {SessionStorage} from "@/business/common/storage/session";
import {DCSType} from "../../../../../../../common/types";
import {ICommon_Model_Plan} from "../../../../../../../common/model/plan";
import PlanEdit from "@/business/controller/app/project/plan/planEdit.vue";
import moment from "moment";
const objInject=inject(injectProjectInfo)
const projectId=objInject.id
const permission=objInject.permission
const key=objInject.key
const myOrganizationUserId=SessionStorage.get("organizationUserId")
const {t}=useI18n()
const columns=[
{
title:t("util.name"),
slotName:"name"
},
{
title:t("util.startDate"),
slotName:"startDate"
},
{
title:t("util.operation"),
slotName: "operation"
}
]
const pagination=reactive({
total:0,
current:1,
pageSize:10
})
const root=getRootNavigatorRef()
const appContext=getCurrentInstance().appContext
const keyword=ref("")
const planList=ref<DCSType<ICommon_Model_Plan[]>>([])
const navigator=getCurrentNavigator()
const search=async (page:number)=>{
let res=await apiPlan.listPlan({
projectId:projectId,
page:page-1,
size:10,
keyword:keyword.value
})
if(res?.code==0) {
planList.value=res.data.data
pagination.total=res.data.count;
pagination.current=page
}
}
const onSearch=()=>{
search(1)
}
const onPageChange=(page:number)=>{
search(page)
}
const onCreate=async ()=>{
let ret=await Dialog.open(root.value,appContext,t("util.add"),markRaw(PlanEdit),{
type:"add",
projectId:projectId
})
if(ret) {
search(pagination.current)
}
}
const onEdit=async (item:DCSType<ICommon_Model_Plan>)=>{
let ret=await Dialog.open(root.value,appContext,t("util.edit"),markRaw(PlanEdit),{
type:"edit",
projectId:projectId,
item:item
})
if(ret) {
search(pagination.current)
}
}
const onDelete=async (item:DCSType<ICommon_Model_Plan>)=>{
let ret=await Dialog.confirm(root.value,appContext,t("tip.deletePlan"))
if(ret) {
let res=await apiPlan.removePlan({
planId:item.id
})
if(res?.code==0) {
Message.success(t("tip.deleteSuccess"))
search(pagination.current)
}
}
}
const onProfile=async (item:DCSType<ICommon_Model_Plan>,event:MouseEvent)=>{
navigator.push("planProfile",{
planId:item.id
})
}
onNavigatorShow(action => {
if(action===ETeamOS_Navigator_Action.POP || action===ETeamOS_Navigator_Action.BACK) {
search(pagination.current)
}
})
onBeforeMount(()=>{
search(pagination.current)
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,47 @@
<template>
<a-form auto-label-width :model="form" ref="eleForm">
<a-form-item field="name" :label="$t('util.name')" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {GanttDataItem} from "@/business/common/component/gantt/types";
import {reactive, ref} from "vue";
import {onDialogOk} from "@/business/common/component/dialog/dialog";
import {dialogFuncGenerator} from "@/business/common/util/helper";
import {apiPlan} from "@/business/common/request/request";
const props=defineProps<{
type:"add"|"edit",
item?:GanttDataItem,
planId:string,
parentId?:string
}>()
const form=reactive({
id:props.type==="edit"?props.item.key:"",
name:props.type==="edit"?props.item.name:"",
})
const eleForm=ref()
onDialogOk(dialogFuncGenerator({
func:()=>{
return props.type=="add"?apiPlan.createMileStone({
planId:props.planId,
name:form.name,
parentId:props.parentId
}):apiPlan.editMileStone({
planItemId:form.id,
name:form.name,
})
},
form:()=>{
return eleForm.value
}
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,584 @@
<template>
<div style="width: 100%;height: 100%;">
<a-row style="width: 100%;font-size: 30px;align-items: center;justify-content: space-between;padding: 0px 10px;box-sizing: border-box;height: 40px">
<a-space>
{{info?.name}}
<span style="color: grey;font-size: small;height:40px;display: flex;align-items: end;padding-bottom: 5px;box-sizing: border-box">
{{$t("util.startDate")}}:{{moment(info?.start_time).format("YYYY-MM-DD")}}
</span>
</a-space>
<a-space>
<a-select size="small" v-model="type">
<a-option value="day">{{$t("util.day")}}</a-option>
<a-option value="month">{{$t("util.month")}}</a-option>
</a-select>
<a-button size="small" type="primary" @click="onEditProfile">
{{$t("util.edit")}}
</a-button>
<a-dropdown-button type="primary" size="small" status="success">
{{$t("util.add")}}
<template #icon>
<icon-down></icon-down>
</template>
<template #content>
<a-doption @click="onAddStage(null)">{{$t("util.stage")}}</a-doption>
<a-doption @click="onAddMilestone(null)">{{$t("util.milestone")}}</a-doption>
<a-doption @click="onAddIssue(null)">{{$t("util.issue")}}</a-doption>
</template>
</a-dropdown-button>
<a-popover trigger="hover" position="right">
<icon-question-circle-fill style="color: rgb(35,110,184);font-size: large"></icon-question-circle-fill>
<template #content>
<span style="color: dodgerblue;white-space: pre-line">
{{$t("help.planItemType")}}
</span>
</template>
</a-popover>
</a-space>
</a-row>
<div style="height: 70px;margin-top: 20px;overflow-x: auto" v-if="milestoneList.length>0">
<a-steps label-placement="vertical" :current="-1" small>
<a-step v-for="item in milestoneList" :description="moment(item.startDate).format('YYYY-MM-DD')">
{{item.name}}
<template #icon>
<icon-check style="color: #03ad03" v-if="item.completed"></icon-check>
<icon-close style="color: orange" v-else></icon-close>
</template>
</a-step>
</a-steps>
</div>
<div style="width: 100%;overflow: auto;margin-top: 20px" :style="{
height:milestoneList.length>0?'calc(100% - 150px)':'calc(100% - 60px)'
}">
<Gantt :data="data" :type="type" :start-date="moment(info.start_time).startOf('day').toDate().getTime()" v-if="info && plan" @change="onChange" @move="onMove">
<template #type="{record}">
<span :style="{
color:record.type===ECommon_Model_Plan_Table.MILESTONE?record.completed?typeMap[record.type].doneColor:typeMap[record.type].undoneColor:typeMap[record.type].color
}">{{typeMap[record.type].text}}</span>
</template>
<template #name="{record}">
<a-link style="box-sizing: border-box;height: 22px" @click="onOpenIssue(record.projectIssueId)" v-if="record.type===ECommon_Model_Plan_Table.ISSUE">
{{record.name}}
</a-link>
<template v-else>
{{record.name}}
</template>
</template>
<template #manDay="{record}">
<template v-if="record.type===ECommon_Model_Plan_Table.ISSUE || record.type===ECommon_Model_Plan_Table.STAGE">
{{moment(record.endDate).startOf("day").diff(record.startDate,"day")+1}}
<span v-if="record.type===ECommon_Model_Plan_Table.ISSUE && record.children?.length>0" style="color: dodgerblue">
({{record.manDay}})
</span>
</template>
</template>
<template #progress="{record}">
<template v-if="record.type===ECommon_Model_Plan_Table.MILESTONE">
<icon-check style="color: #03ad03" v-if="record.completed"></icon-check>
<icon-close style="color: orange" v-else></icon-close>
</template>
<template v-else>
{{record.showProgress!=null?(record.showProgress.toFixed(0)+'%'):""}}
<span v-if="record.type===ECommon_Model_Plan_Table.ISSUE && record.children?.length>0" style="color: dodgerblue">
({{record.progress.toFixed(0)+"%"}})
</span>
</template>
</template>
<template #depend="{record}">
<template v-if="record.type===ECommon_Model_Plan_Table.ISSUE || record.type===ECommon_Model_Plan_Table.STAGE">
{{record.depend?getNameWithKey(record.depend):""}}
</template>
</template>
<template #delay="{record}">
<template v-if="record.type===ECommon_Model_Plan_Table.ISSUE || record.type===ECommon_Model_Plan_Table.STAGE">
{{record.delay??""}}
</template>
</template>
<template #startDate="{record}">
{{moment(record.startDate).format("MM-DD")}}
</template>
<template #endDate="{record}">
{{moment(record.endDate).format("MM-DD")}}
</template>
<template #operation="{record}">
<a-dropdown trigger="hover">
<icon-more></icon-more>
<template #content>
<a-dsubmenu trigger="hover" v-if="record.type===ECommon_Model_Plan_Table.STAGE">
{{$t("util.add")}}
<template #content>
<a-doption @click="onAddStage(record)">{{$t("util.stage")}}</a-doption>
<a-doption @click="onAddMilestone(record)">{{$t("util.milestone")}}</a-doption>
<a-doption @click="onAddIssue(record)">{{$t("util.issue")}}</a-doption>
</template>
</a-dsubmenu>
<a-doption @click="onEdit(record)">{{$t("util.edit")}}</a-doption>
<a-doption @click="onEditProgress(record)" v-if="record.type===ECommon_Model_Plan_Table.ISSUE && record.progress!==0 && record.progress!==100">{{$t("util.progress")}}</a-doption>
<a-doption @click="onDelete(record)" v-if="findObj(data,record.parentId)?.type!==ECommon_Model_Plan_Table.ISSUE">{{$t("util.delete")}}</a-doption>
</template>
</a-dropdown>
</template>
<template #shortView="{data}">
<a-descriptions :data="shortViewInfo(data)" size="small" :title="data.name" :column="1"></a-descriptions>
</template>
</Gantt>
</div>
</div>
</template>
<script setup lang="ts">
import {computed, getCurrentInstance, inject, markRaw, onBeforeMount, ref} from "vue";
import {DCSType} from "../../../../../../../common/types";
import {
ICommon_Route_Res_Plan_Info,
ICommon_Route_Res_Plan_Info_Item
} from "../../../../../../../common/routes/response";
import {apiPlan} from "@/business/common/request/request";
import Gantt from "@/business/common/component/gantt/gantt.vue";
import {GanttDataItem} from "@/business/common/component/gantt/types";
import {type} from "os";
import moment from "moment";
import {ECommon_Model_Plan_Table} from "../../../../../../../common/model/plan_table";
import {injectProjectInfo} from "@/business/common/util/symbol";
import {ECommon_Model_Workflow_Node_Status} from "../../../../../../../common/model/workflow_node";
import plan from "../../../../../../../common/routes/plan";
import {Message} from "@arco-design/web-vue";
import {Dialog} from "@/business/common/component/dialog/dialog";
import {getRootNavigatorRef} from "@/teamOS/common/component/navigator/navigator";
import {useI18n} from "vue-i18n";
import PlanStageEdit from "@/business/controller/app/project/plan/planStageEdit.vue";
import PlanMilestoneEdit from "@/business/controller/app/project/plan/planMilestoneEdit.vue";
import PlanIssueEdit from "@/business/controller/app/project/plan/planIssueEdit.vue";
import {EClient_EVENTBUS_TYPE, eventBus} from "@/business/common/event/event";
import PlanProgressEdit from "@/business/controller/app/project/plan/planProgressEdit.vue";
import PlanEdit from "@/business/controller/app/project/plan/planEdit.vue";
const props=defineProps<{
planId:string
}>()
const info=ref<DCSType<ICommon_Route_Res_Plan_Info>>()
const data=ref<GanttDataItem[]>([])
const type=ref<"day"|"month">("month")
const projectKey=inject(injectProjectInfo).key
const root=getRootNavigatorRef()
const appContext=getCurrentInstance().appContext
const {t}=useI18n()
const typeMap={
[ECommon_Model_Plan_Table.ISSUE]:{
text:t("util.issue"),
color:"rgb(85, 171, 251)"
},
[ECommon_Model_Plan_Table.STAGE]:{
text:t("util.stage"),
color:"green"
},
[ECommon_Model_Plan_Table.MILESTONE]:{
text:t("util.milestone"),
undoneColor:"orange",
doneColor:"#03ad03"
}
}
const getInfo=async ()=>{
let res=await apiPlan.info({
planId:props.planId
})
if(res?.code==0) {
info.value=res.data
refreshData()
}
}
const milestoneList=computed(()=>{
return getMilestoneList(data.value)
})
const getMilestoneList=(data:GanttDataItem[])=>{
let ret:GanttDataItem[]=[]
for(let obj of data) {
if(obj.type===ECommon_Model_Plan_Table.MILESTONE) {
ret.push(obj)
}
if(obj.children?.length>0) {
ret=ret.concat(getMilestoneList(obj.children))
}
}
return ret;
}
const refreshData=()=>{
data.value=[]
handleInfo(info.value.data,data.value)
calcItem(data.value,moment(info.value.start_time).startOf("day").toDate().getTime())
}
const handleInfo=(list:DCSType<ICommon_Route_Res_Plan_Info_Item>[],ganttData:GanttDataItem[])=>{
for(let obj of list) {
let ganttItem:GanttDataItem={
type:obj.type,
name:obj.type===ECommon_Model_Plan_Table.ISSUE?(projectKey.value+"-"+obj.issue.unique_id+" "+obj.issue.name):obj.name,
progress:(()=>{
if(obj.type===ECommon_Model_Plan_Table.ISSUE) {
if(obj.workflow.status===ECommon_Model_Workflow_Node_Status.NOTSTART) {
return 0
} else if(obj.workflow.status===ECommon_Model_Workflow_Node_Status.DONE) {
return 100
} else {
return obj.progress??50
}
} else {
return 100
}
})(),
delay:obj.delay??0,
key:obj.id,
depend:obj.depend_id,
manDay:obj.type===ECommon_Model_Plan_Table.ISSUE?obj.issue.man_day:1,
...(obj.type===ECommon_Model_Plan_Table.ISSUE && {
uniqueId:String(obj.issue.unique_id),
projectIssueId:obj.ref_id
}),
parentId:obj.parent_id
}
ganttData.push(ganttItem)
if(obj.children) {
ganttItem.children=[]
handleInfo(obj.children,ganttItem.children)
}
}
}
const findObj=(data:GanttDataItem[],key:string):GanttDataItem=>{
for(let obj of data) {
if(obj.key===key) {
return obj
}
if(obj.children?.length>0) {
let ret=findObj(obj.children,key)
if(ret) {
return ret;
}
}
}
}
const getNameWithKey=(key:string)=>{
let obj=findObj(data.value,key)
if(obj) {
if(obj.type===ECommon_Model_Plan_Table.ISSUE) {
return projectKey.value+"-"+obj.uniqueId
} else if(obj.type===ECommon_Model_Plan_Table.STAGE) {
return obj.name
} else {
return ""
}
} else {
return ""
}
}
const getChildrenWithKey=(key:string):GanttDataItem[]=>{
function _find(data:GanttDataItem[]) {
for(let obj of data) {
if(obj.key===key) {
return obj.children
}
if(obj.children?.length>0) {
let ret=_find(obj.children)
if(ret) {
return ret;
}
}
}
}
if(key===null) {
return data.value
} else {
return _find(data.value)
}
}
const calcItem=(data:GanttDataItem[],startDate:number):{
maxEndDate:number,
progressTotal:number
progressCount:number
}=>{
let maxEndDate=0,progress=0,progressCount=0
for(let i=0;i<data.length;i++) {
let obj=data[i]
if(obj.type===ECommon_Model_Plan_Table.ISSUE || obj.type===ECommon_Model_Plan_Table.STAGE) {
if(obj.depend) {
let objDepend:GanttDataItem
for(let j=0;j<data.length;j++) {
if(data[j].key===obj.depend) {
objDepend=data[j]
break
}
}
if(objDepend) {
obj.startDate=moment(objDepend.endDate).add(obj.delay??1,"day").toDate().getTime()
}
} else {
obj.startDate=moment(startDate).add(obj.delay??0,"day").toDate().getTime()
}
if(obj.children?.length>0) {
let temp=calcItem(obj.children,obj.startDate)
let endDate=moment(obj.startDate).add(obj.manDay-1,"day").toDate().getTime()
obj.endDate=Math.max(temp.maxEndDate,endDate)
if(obj.type===ECommon_Model_Plan_Table.ISSUE) {
obj.showProgress=(temp.progressTotal+obj.progress)/(temp.progressCount+1)
} else {
obj.showProgress=temp.progressTotal/temp.progressCount
obj.manDay=moment(obj.endDate).diff(obj.startDate,"day")+1
}
} else {
obj.showProgress=obj.progress
obj.endDate=moment(obj.startDate).add(obj.manDay-1,"day").toDate().getTime()
}
progressCount++;
progress+=obj.showProgress
} else {
let maxEndDate:number=0,isCompleted=true
for(let j=0;j<i;j++) {
maxEndDate=Math.max(data[j].endDate,maxEndDate)
if(data[j].type===ECommon_Model_Plan_Table.STAGE || data[j].type===ECommon_Model_Plan_Table.ISSUE) {
if(data[j].showProgress!==100) {
isCompleted=false
}
}
}
obj.completed=isCompleted
if(maxEndDate===0) {
obj.startDate=startDate
obj.endDate=startDate
} else {
obj.startDate=maxEndDate
obj.endDate=maxEndDate
}
}
maxEndDate=Math.max(obj.endDate,maxEndDate)
}
return {
maxEndDate,
progressTotal:progress,
progressCount
}
}
const onChange=async (item:GanttDataItem,originalStartDate:number,originalEndDate:number,originalDalay:number) => {
if(item.type===ECommon_Model_Plan_Table.ISSUE) {
let res=await apiPlan.editIssue({
planItemId:item.key,
dependId:item.depend,
delay:item.delay,
manDay:item.manDay
})
if(res?.code==0) {
info.value.data=res.data
refreshData()
} else {
Message.error(res.msg)
item.delay=originalDalay
item.manDay=moment(originalEndDate).endOf("day").diff(originalStartDate,"day")
}
} else if(item.type===ECommon_Model_Plan_Table.STAGE) {
let res=await apiPlan.editStage({
planItemId:item.key,
delay:item.delay,
dependId:item.depend,
name:item.name
})
if(res?.code==0) {
info.value.data=res.data
refreshData()
} else {
Message.error(res.msg)
item.delay=originalDalay
}
} else if(item.type===ECommon_Model_Plan_Table.MILESTONE) {
let res=await apiPlan.editMileStone({
planItemId:item.key,
name:item.name
})
if(res?.code==0) {
info.value.data=res.data
refreshData()
}
}
}
const onMove=async (key:string,destKey:string,type:"in"|"top"|"bottom") =>{
let res=await apiPlan.moveItem({
planItemId:key,
targetId:destKey,
action:type
})
if(res?.code==0) {
info.value.data=res.data
refreshData()
} else {
Message.error(res.msg)
}
}
const onAddStage=async (item:GanttDataItem)=>{
if(item===null || item.type===ECommon_Model_Plan_Table.STAGE) {
let list=getChildrenWithKey(item?item.key:null)?.filter(obj=>obj.type!==ECommon_Model_Plan_Table.MILESTONE && obj.key!=item?.key)
let ret:any=await Dialog.open(root.value,appContext,t("util.add"),markRaw(PlanStageEdit),{
type:"add",
planId:props.planId,
dependList:list??[],
parentId:item?item.key:null
})
if(ret) {
info.value.data=ret.data
refreshData()
}
}
}
const onAddMilestone=async (item:GanttDataItem)=>{
if(item===null || item.type===ECommon_Model_Plan_Table.STAGE) {
let ret:any=await Dialog.open(root.value,appContext,t("util.add"),markRaw(PlanMilestoneEdit),{
type:"add",
planId:props.planId,
parentId:item?item.key:null
})
if(ret) {
info.value.data=ret.data
refreshData()
}
}
}
const onAddIssue=async (item:GanttDataItem)=>{
if(item===null || item.type===ECommon_Model_Plan_Table.STAGE) {
let list=getChildrenWithKey(item?item.key:null)?.filter(obj=>obj.type!==ECommon_Model_Plan_Table.MILESTONE && obj.key!=item?.key)
let ret:any=await Dialog.open(root.value,appContext,t("util.add"),markRaw(PlanIssueEdit),{
type:"add",
projectId:info.value.project_id,
planId:props.planId,
dependList:list??[],
parentId:item?item.key:null
})
if(ret) {
info.value.data=ret.data
refreshData()
}
}
}
const onOpenIssue=(projectIssueId:string)=>{
eventBus.emit(EClient_EVENTBUS_TYPE.OPEN_PROJECT_ISSUE_PROFILE,info.value.project_id,projectIssueId)
}
const onEdit=async (item:GanttDataItem)=>{
let ret:any
let list=getChildrenWithKey(item?item.parentId:null)?.filter(obj=>obj.type!==ECommon_Model_Plan_Table.MILESTONE && obj.key!=item?.key)
if(item.type===ECommon_Model_Plan_Table.ISSUE) {
ret=await Dialog.open(root.value,appContext,t("util.edit"),markRaw(PlanIssueEdit),{
type:"edit",
projectId:info.value.project_id,
planId:props.planId,
dependList:list??[],
parentId:item?item.key:null,
item
})
} else if(item.type===ECommon_Model_Plan_Table.STAGE) {
ret=await Dialog.open(root.value,appContext,t("util.edit"),markRaw(PlanStageEdit),{
type:"edit",
planId:props.planId,
dependList:list??[],
parentId:item?item.key:null,
item
})
}else if(item.type===ECommon_Model_Plan_Table.MILESTONE) {
ret=await Dialog.open(root.value,appContext,t("util.edit"),markRaw(PlanMilestoneEdit),{
type:"edit",
planId:props.planId,
parentId:item?item.key:null,
item
})
}
if(ret) {
info.value.data=ret.data
refreshData()
}
}
const onEditProgress=async (item:GanttDataItem)=>{
let ret:any=await Dialog.open(root.value,appContext,t("util.edit"),markRaw(PlanProgressEdit),{
planItemId:item.key,
progress:item.progress
})
if(ret) {
info.value.data=ret.data
refreshData()
}
}
const onDelete=async (item:GanttDataItem)=>{
let ret=await Dialog.confirm(root.value,appContext,t("tip.deleteItem"))
if(ret) {
let res=await apiPlan.removeItem({
planItemId:item.key
})
if(res?.code==0) {
info.value.data=res.data
refreshData()
}
}
}
const shortViewInfo=(item:GanttDataItem):{
label:string,
value:string
}[]=>{
if(item.type===ECommon_Model_Plan_Table.ISSUE || item.type===ECommon_Model_Plan_Table.STAGE) {
return [
{
label:t("util.type"),
value:typeMap[ECommon_Model_Plan_Table.ISSUE].text
},
{
label: t("util.progress"),
value:item.showProgress.toFixed(0)+"%"
},
{
label: t("util.delay"),
value:String(item.delay)
},
{
label: t("util.startDate"),
value:moment(item.startDate).format("YYYY-MM-DD")
},
{
label: t("util.endDate"),
value:moment(item.endDate).format("YYYY-MM-DD")
},
]
}
}
const onEditProfile=async ()=>{
let ret=await Dialog.open(root.value,appContext,t("util.edit"),markRaw(PlanEdit),{
type:"edit",
projectId:info.value.project_id,
item:info.value
})
if(ret) {
getInfo()
}
}
onBeforeMount(()=>{
getInfo()
})
</script>
<style scoped>
:deep td a {
padding: 1px 0px;
}
</style>

View File

@ -0,0 +1,38 @@
<template>
<a-form :model="form" ref="eleForm">
<a-form-item field="progress" :label="$t('util.progress')">
<a-slider v-model="form.progress" :min="1" :max="99"></a-slider>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue";
import {onDialogOk} from "@/business/common/component/dialog/dialog";
import {dialogFuncGenerator} from "@/business/common/util/helper";
import {apiPlan} from "@/business/common/request/request";
const props=defineProps<{
planItemId:string,
progress:number
}>()
const form=reactive({
progress:props.progress
})
const eleForm=ref(null)
onDialogOk(dialogFuncGenerator({
func:()=>{
return apiPlan.editProgress({
progress:form.progress,
planItemId:props.planItemId
})
},
form:()=>{
return eleForm.value
}
}))
</script>
<style scoped>
</style>

View File

@ -0,0 +1,62 @@
<template>
<a-form auto-label-width :model="form" ref="eleForm">
<a-form-item field="name" :label="$t('util.name')" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="dependId" :label="$t('util.depend')">
<a-select allow-clear allow-search v-model="form.dependId">
<a-option v-for="item in dependList" :label="item.name" :value="item.key"></a-option>
</a-select>
</a-form-item>
<a-form-item field="delay" :label="$t('util.delay')">
<a-input-number :precision="0" :min="-100" :max="100" v-model="form.delay"></a-input-number>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {GanttDataItem} from "@/business/common/component/gantt/types";
import {reactive, ref} from "vue";
import {onDialogOk} from "@/business/common/component/dialog/dialog";
import {dialogFuncGenerator} from "@/business/common/util/helper";
import {apiPlan} from "@/business/common/request/request";
const props=defineProps<{
type:"add"|"edit",
item?:GanttDataItem,
planId:string,
dependList:GanttDataItem[],
parentId?:string
}>()
const form=reactive({
id:props.type==="edit"?props.item.key:"",
name:props.type==="edit"?props.item.name:"",
delay:props.type==="edit"?props.item.delay:0,
dependId:props.type==="edit"?props.item.depend:""
})
const eleForm=ref()
onDialogOk(dialogFuncGenerator({
func:()=>{
return props.type=="add"?apiPlan.createStage({
planId:props.planId,
name:form.name,
dependId:form.dependId,
delay:form.delay,
parentId:props.parentId
}):apiPlan.editStage({
planItemId:form.id,
name:form.name,
delay:form.delay,
dependId:form.dependId
})
},
form:()=>{
return eleForm.value
}
}))
</script>
<style scoped>
</style>

View File

@ -6,6 +6,7 @@
<a-menu-item key="issue">{{$t("util.issue")}}</a-menu-item>
<a-menu-item key="board">{{$t("util.board")}}</a-menu-item>
<a-menu-item key="release">{{$t("util.release")}}</a-menu-item>
<a-menu-item key="plan">{{$t("util.plan")}}</a-menu-item>
<a-menu-item key="setting" v-if="checkPermission(permission,Permission_Types.Project.ADMIN)">{{$t("util.setting")}}</a-menu-item>
</a-menu>
</a-layout-sider>
@ -33,6 +34,8 @@ import {Message} from "@arco-design/web-vue";
import BoardList from "@/business/controller/app/project/board/boardList.vue";
import BoardProfile from "@/business/controller/app/project/board/boardProfile.vue";
import ProjectHome from "@/business/controller/app/project/home/projectHome.vue";
import PlanList from "@/business/controller/app/project/plan/planList.vue";
import PlanProfile from "@/business/controller/app/project/plan/planProfile.vue";
const props=defineProps<{
projectId:string
@ -50,7 +53,9 @@ let objComponent={
releaseProfile:markRaw(ProjectReleaseProfile),
board:markRaw(BoardList),
boardProfile:markRaw(BoardProfile),
home:markRaw(ProjectHome)
home:markRaw(ProjectHome),
plan:markRaw(PlanList),
planProfile:markRaw(PlanProfile)
}
const objProvide={
id:props.projectId,
@ -62,7 +67,8 @@ let objMeta=getCurrentNavigatorMeta()?.data as {
projectIssueId?:string
projectReleaseId?:string,
boardSprintId?:string,
boardId?:string
boardId?:string,
planId?:string
}
if(objMeta?.projectReleaseId) {
menuKey.value=["release"]
@ -70,6 +76,8 @@ if(objMeta?.projectReleaseId) {
menuKey.value=["board"]
} else if(objMeta?.projectIssueId) {
menuKey.value=["issue"]
} else if(objMeta?.planId) {
menuKey.value=["plan"]
}
onMounted(()=>{
if(objMeta?.projectIssueId) {
@ -94,6 +102,11 @@ onMounted(()=>{
boardId:objMeta?.boardId,
});
delete objMeta.boardId
} else if(objMeta?.planId) {
eleNavigator.value.navigator.replaceRoot("planProfile",{
planId:objMeta?.planId,
});
delete objMeta.planId
} else {
eleNavigator.value.navigator.replaceRoot(type.value,null);
}

View File

@ -31,7 +31,7 @@
<span v-if="record.notstart+record.inprogress+record.done==0">
{{$t("controller.app.project.release.projectReleaseList.noIssues")}}
</span>
<a-progress v-else :percent="record.done/(record.notstart+record.inprogress+record.done)" style="width: 180px" color="green"></a-progress>
<a-progress v-else :percent="Number((record.done/(record.notstart+record.inprogress+record.done)).toFixed(2))" style="width: 180px" color="green"></a-progress>
</template>
<template #startDate="{record}">
{{record.start_time}}

View File

@ -27,7 +27,7 @@
<span v-if="info?.notstart+info?.inprogress+info?.done==0">
{{$t("controller.app.project.release.projectReleaseList.noIssues")}}
</span>
<a-progress v-else :percent="info?.done/(info?.notstart+info?.inprogress+info?.done)" style="width: 100%" color="green"></a-progress>
<a-progress v-else :percent="Number((info?.done/(info?.notstart+info?.inprogress+info?.done)).toFixed(2))" style="width: 100%" color="green"></a-progress>
</a-col>
<a-col :span="8" style="text-align: center">
<a-button type="outline" size="mini" @click="onProfile">{{$t("util.profile")}}</a-button>

View File

@ -28,7 +28,7 @@ const columns=[
dataIndex:"description"
},
{
title:"keyword",
title:t("util.keyword"),
dataIndex:"keyword"
},
{

View File

@ -173,7 +173,7 @@ const pagination=reactive({
const action=(item:ICommon_Route_Res_Organization_User_Item)=>{
return [
{
name:"role",
name:t("util.role"),
func:async ()=>{
let ret=await Dialog.open(root.value,appContext,t("util.add"),markRaw(EditTeamMemberRole),{
type:"edit",
@ -186,7 +186,7 @@ const action=(item:ICommon_Route_Res_Organization_User_Item)=>{
}
},
{
name:"remove",
name:t("util.remove"),
func:async ()=>{
let ret=await Dialog.confirm(root.value,appContext,t("tip.removeMember"))
if(ret) {

View File

@ -19,14 +19,11 @@
<UserAvatar :organization-user-id="info.modified_by.organizationUserId" :name="info.modified_by.nickname" :photo="info.modified_by.photo" v-if="info"></UserAvatar>
</a-row>
</a-row>
<a-switch v-model="isWrite" v-if="checkPermission(permission,Permission_Types.Wiki.EDIT)">
<template #checked>
{{$t("util.edit")}}
</template>
<template #unchecked>
{{$t("util.preview")}}
</template>
</a-switch>
<a-space>
<span style="color: green">{{$t("util.preview")}}</span>
<a-switch type="line" v-model="isWrite" v-if="checkPermission(permission,Permission_Types.Wiki.EDIT)" unchecked-color="green" checked-color="dodgerblue"></a-switch>
<span style="color: dodgerblue">{{$t("util.edit")}}</span>
</a-space>
</a-row>
<a-row style="width: 100%;height: calc(100% - 45px)">
<a-spin :loading="loading" style="width: 100%;height: 100%">
@ -57,10 +54,11 @@ import {ECommon_Model_Finder_Shortcut_Type} from "../../../../../../common/model
import {vDrag} from "../../../../teamOS/common/directive/drag";
import {getRootNavigatorRef} from "../../../../teamOS/common/component/navigator/navigator";
import {DropParam, vDrop} from "../../../../teamOS/common/directive/drop";
import moment from "moment";
import {RichEditorEventHandle} from "../../../common/component/richEditorEventHandle";
import {DCSType} from "../../../../../../common/types";
import moment from "moment";
moment;
const props=defineProps<{
wikiItemId:string,
path:string[],

View File

@ -3,7 +3,7 @@
<template #barLeft>
<a-dropdown trigger="hover" id="dropdownEle">
<a-avatar id="myProfile" :size="32" :image-url="avatar" :trigger-icon-style="{height:'12px',width:'12px',lineHeight:'12px',right:'-2px',bottom:'-2px',...(store.status===ECommon_User_Online_Status.MEETING && {backgroundColor:'transparent'})}">
T
{{imgName}}
<template #trigger-icon>
<div style="height: 100%;width: 100%;background-color: #03ad03;border-radius: 6px" v-if="store.status===ECommon_User_Online_Status.ONLINE"></div>
<icon-stop :stroke-width="5" style="color: darkred" v-else-if="store.status===ECommon_User_Online_Status.BUSY"></icon-stop>
@ -131,7 +131,7 @@
import TeamOS from "../../../teamOS/index.vue";
import {getDesktopInstance} from "../../../teamOS/teamOS";
import {getCurrentInstance, markRaw, nextTick, onBeforeMount, onBeforeUnmount, ref, watch} from "vue";
import {computed, getCurrentInstance, markRaw, nextTick, onBeforeMount, onBeforeUnmount, ref, watch} from "vue";
import {useDesktopStore} from "./store/desktop";
import {useRouter} from "vue-router";
import {Message} from "@arco-design/web-vue";
@ -191,9 +191,20 @@ const router=useRouter();
let iconManager=getDesktopInstance().iconManager
let windowManager=getDesktopInstance().windowManager
const appContext=getCurrentInstance().appContext
store.appContext=appContext
const loading=ref(true)
let objDriver:Driver
let avatar=ref("")
let avatar=computed(()=>{
return store.userInfo?.photo
})
const imgName=computed(()=>{
if(store.userInfo?.username?.includes(" ")) {
let arr=store.userInfo.username.split(" ")
return arr[0][0].toUpperCase()+arr[1][0].toUpperCase()
} else {
return store.userInfo?.username?store.userInfo?.username[0].toUpperCase():""
}
})
const objFinderHandle=new FinderHandle(root,appContext,"")
const noteList=ref<DCSType<ICommon_Route_Res_Sticky_Note_Item & {
isReadOnly:boolean
@ -232,9 +243,6 @@ watch(()=>store.organizationList,()=>{
})
})
}
if(store.userInfo?.photo) {
avatar.value=store.userInfo?.photo
}
if(SessionStorage.get("organizationId")) {
setMenu(arr,SessionStorage.get("organizationId"))
} else {
@ -514,7 +522,11 @@ onBeforeMount(async ()=>{
if(SessionStorage.get("organizationId")) {
await store.initOrganization(SessionStorage.get("organizationId"))
}
loading.value=false
loading.value=false;
window.onbeforeunload = function(event){
event.preventDefault()
event.returnValue=t("tip.leavePage");
};
})
onBeforeUnmount(()=>{
@ -524,6 +536,7 @@ onBeforeUnmount(()=>{
eventBus.off(EClient_EVENTBUS_TYPE.USER_LOGIN_EXPIRED,onLogout.bind(null,true))
eventBus.off(EClient_EVENTBUS_TYPE.GUIDE,onGuide)
eventBus.off(EClient_EVENTBUS_TYPE.ORGANIZATION_REMOVE, onRemoveOrganization)
window.onbeforeunload=null;
})

View File

@ -9,9 +9,24 @@ import Meeting from "../../app/meeting/meeting.vue";
import MeetingProfile from "../../app/meeting/meetingProfile.vue";
import {EClient_EVENTBUS_TYPE, eventBus} from "../../../common/event/event";
import i18n from "@/business/common/i18n/i18n";
import {useDesktopStore} from "@/business/controller/desktop/store/desktop";
import {ECommon_User_Online_Status} from "../../../../../../common/types";
import {Dialog} from "@/business/common/component/dialog/dialog";
const {t}=i18n.global
export const iconMeeting=new Icon(t("util.meeting"),iconGroupMap["meeting"],"meeting")
const closeFunc=async item1 => {
const store=useDesktopStore()
if(store.status===ECommon_User_Online_Status.MEETING) {
let ret=await Dialog.confirm(document.body,store.appContext,t("tip.leaveMeeting"))
if(ret) {
return true
} else {
return false
}
}
return true
}
const func=item => {
if(!SessionStorage.get("organizationId")) {
Message.error(t("tip.switchToSpecificOrganization"))
@ -33,6 +48,7 @@ const func=item => {
}
],t("util.meeting"));
windowManager.open(win);
win.addEventListener("close",closeFunc)
}
iconMeeting.addEventListener("dbClick",item => {
if(!document.getElementById("application")) {
@ -68,4 +84,5 @@ eventBus.on(EClient_EVENTBUS_TYPE.OPEN_MEETING,(meetingId, password,inviteBusine
}
],t("util.meeting"));
windowManager.open(win);
win.addEventListener("close",closeFunc)
})

View File

@ -45,7 +45,7 @@ const func=item => {
},
default:{
name:"project",
title: "project"
title: t("util.project")
}
}
})
@ -264,3 +264,45 @@ eventBus.on(EClient_EVENTBUS_TYPE.OPEN_PROJECT_BOARD_PROFILE,(projectId, boardId
})
windowManager.open(win);
})
eventBus.on(EClient_EVENTBUS_TYPE.OPEN_PROJECT_PLAN_PROFILE,(projectId, planId) => {
const win=new Window(t("util.project"),ETeamOS_Window_Type.TAB, "project",true,[
{
id:v4(),
meta:{
title:"project",
data:{
planId:planId
}
},
components:{
project:markRaw(Project),
profile:markRaw(ProjectProfile)
},
default:{
name:"profile",
title: t("util.profile"),
props:{
projectId:projectId
}
}
}
],t("util.project"));
win.addEventListener("newTab", async item => {
return {
id:v4(),
meta:{
title:"project"
},
components:{
project:markRaw(Project),
profile:markRaw(ProjectProfile)
},
default:{
name:"project",
title: t("util.project")
}
}
})
windowManager.open(win);
})

View File

@ -39,7 +39,8 @@ export const useDesktopStore=defineStore("desktop",{
userInfo:{} as DCSType<Omit<ICommon_Model_User,"password">>,
status:ECommon_User_Online_Status.OFFLINE,
heartbeatInterval:null,
copyItemList:[] as string[]
copyItemList:[] as string[],
appContext:null
}),
actions:{
async initNotificationSocket() {

View File

@ -0,0 +1,66 @@
<template>
<a-row align="center" justify="center" style="height: 100%;position: relative;background: radial-gradient(circle at center, rgb(228,232,249), #ffffff);">
<a-space style="position: absolute;left: 20px;top: 20px">
<img :src="logo" style="width: 20px;aspect-ratio: 1/1" />
<span style="font-size: 24px;font-weight: bolder">
<router-link :to="{name:'index'}" style="text-decoration: none;color:rgb(35,110,184)">
Teamlinker
</router-link>
</span>
</a-space>
<div style="width:40%;box-shadow: rgba(22, 14, 45, 0.02) 0px 0px 40px, rgba(22, 14, 45, 0.06) 0px 0px 104px;padding: 20px;box-sizing: border-box;border: 1px solid rgb(234, 236, 240);border-radius: 10px;background-color: white">
<a-form :model="form" auto-label-width @submit="onSubmit">
<h1 style="text-align: center;margin-bottom: 50px;color: rgba(16, 24, 40, 0.8)">{{$t("controller.login.bindAccount.bindExistedAccount")}}</h1>
<a-form-item field="username" :label="$t('util.username')" required>
<a-input v-model="form.username" :placeholder="$t('placeholder.enterUsername')"></a-input>
</a-form-item>
<a-form-item field="password" :label="$t('util.password')" required>
<a-input v-model="form.password" type="password" :placeholder="$t('placeholder.typePassword')"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit" type="primary" style="background-color: rgb(147, 115, 238)">{{ $t("util.bind") }}</a-button>
</a-form-item>
</a-form>
</div>
</a-row>
</template>
<script setup lang="ts">
import {getCurrentInstance, reactive} from "vue";
import {useRouter} from "vue-router";
import logo from "@/assert/logo.png"
import {useI18n} from "vue-i18n";
import {useDesktopStore} from "@/business/controller/desktop/store/desktop";
import {apiUser} from "@/business/common/request/request";
import {SessionStorage} from "@/business/common/storage/session";
import md5 from "blueimp-md5";
import {Message} from "@arco-design/web-vue";
let form = reactive({
username: "",
password: ""
})
const appContext=getCurrentInstance().appContext
const store=useDesktopStore()
const {t}=useI18n()
let router = useRouter();
const onSubmit = async () => {
let res=await apiUser.bindWechat({
openId:SessionStorage.get("wechatOpenId"),
username:form.username,
password:md5(form.password)
})
if(res?.code==0) {
Message.success(t("tip.operationSuccess"))
SessionStorage.remove("wechatOpenId")
await router.replace("login")
} else {
Message.error(res.msg)
}
}
</script>
<style scoped>
</style>

View File

@ -10,7 +10,7 @@
</a-space>
<div style="width:40%;box-shadow: rgba(22, 14, 45, 0.02) 0px 0px 40px, rgba(22, 14, 45, 0.06) 0px 0px 104px;padding: 20px;box-sizing: border-box;border: 1px solid rgb(234, 236, 240);border-radius: 10px;background-color: white">
<a-form :model="form" auto-label-width @submit="onSubmit" v-if="!organizationList">
<h1 style="text-align: center;margin-bottom: 50px">{{$t("util.login")}}</h1>
<h1 style="text-align: center;margin-bottom: 50px;color: rgba(16, 24, 40, 0.8)">{{$t("util.login")}}</h1>
<a-form-item field="username" :label="$t('util.username')" required>
<a-input v-model="form.username" :placeholder="$t('placeholder.enterUsername')"></a-input>
</a-form-item>
@ -19,7 +19,14 @@
</a-form-item>
<a-form-item>
<a-row style="justify-content: space-between;width: 100%">
<a-button html-type="submit" type="primary">{{ $t("util.submit") }}</a-button>
<a-space size="large">
<a-button html-type="submit" type="primary" style="background-color: rgb(147, 115, 238)">{{ $t("util.submit") }}</a-button>
<a-button v-if="$deployMode.value===ECommon_Application_Mode.ONLINE" type="text" @click="onWechat">
<template #icon>
<icon-wechat style="color: green;font-size: x-large"></icon-wechat>
</template>
</a-button>
</a-space>
<a-space v-if="$deployMode.value===ECommon_Application_Mode.ONLINE">
<a-button html-type="button" type="primary" status="success" @click="onRegister">{{ $t("util.register") }}
</a-button>
@ -59,7 +66,7 @@
</a-list-item>
</a-list>
</template>
<a-button type="primary" status="success" @click="onCreate" size="large" style="width: 200px;" v-else>
<a-button type="primary" status="success" @click="onCreate" size="large" style="width: 200px;" v-else-if="$deployMode.value===ECommon_Application_Mode.ONLINE">
{{$t("controller.desktop.desktop.createOrganization")}}
</a-button>
<a-button type="text" style="color: grey;margin-top: 20px" @click="onDesktop()">{{$t("util.skip")}}</a-button>
@ -71,7 +78,7 @@
<script setup lang="ts">
import {getCurrentInstance, markRaw, reactive, ref} from "vue";
import {apiOrganization, apiUser} from "../../common/request/request";
import {apiGateway, apiOrganization, apiUser} from "../../common/request/request";
import {useRouter} from "vue-router";
import {Message} from "@arco-design/web-vue";
import {NotificationWrapper} from "../../common/component/notification/notification";
@ -95,6 +102,14 @@ const appContext=getCurrentInstance().appContext
const store=useDesktopStore()
const {t}=useI18n()
let router = useRouter();
const login=(userId:string)=>{
SessionStorage.remove("organizationId")
SessionStorage.set("userId",userId);
getOrganizationList()
store.initNotificationSocket()
}
const onSubmit = async () => {
let ret = await apiUser.login({
username: form.username,
@ -102,10 +117,7 @@ const onSubmit = async () => {
lang:localStorage.getItem("lang")??(navigator.language || "en").toLowerCase().split("-")[0]
})
if (ret.code == 0) {
SessionStorage.remove("organizationId")
SessionStorage.set("userId",ret.data.id);
getOrganizationList()
store.initNotificationSocket()
login(ret.data.id)
} else {
Message.error(ret.msg);
}
@ -140,6 +152,34 @@ const onCreate=async ()=>{
}
}
const onWechat=async ()=>{
SessionStorage.remove("wechatOpenId")
let res=await apiGateway.wechatAppId()
if(res?.code==0) {
window.open(`https://open.weixin.qq.com/connect/qrconnect?appid=${res.data.appId}&redirect_uri=${encodeURIComponent(location.protocol+"//"+location.host+"/#/wechat")}&response_type=code&scope=snsapi_login#wechat_redirect`,"_blank","popup=yes,width=600,height=600,top=100,left=300")
window.onmessage= async ev => {
let openId=ev.data
let res=await apiUser.wechatLogin({
openId,
lang:localStorage.getItem("lang")??(navigator.language || "en").toLowerCase().split("-")[0]
})
if(res?.code==0) {
if(res.data) {
login(res.data.id)
} else {
SessionStorage.set("wechatOpenId",openId)
let ret=await Dialog.confirm(document.body,appContext,t("tip.bindExistedAccount"))
if(ret) {
await router.replace("bindAccount")
} else {
await router.replace("register")
}
}
}
}
}
}
const getOrganizationList=async ()=>{
let ret=await apiOrganization.list()
if(ret && ret.code==0) {

View File

@ -34,6 +34,7 @@ import {Message} from "@arco-design/web-vue";
import {useRouter} from "vue-router";
import {useI18n} from "vue-i18n";
import logo from "@/assert/logo.png";
import {SessionStorage} from "@/business/common/storage/session";
const props=defineProps<{
username:string
@ -59,12 +60,15 @@ const onSubmit=async()=>{
if(!form.code) {
return
}
let openId=SessionStorage.get("wechatOpenId")
let res=await apiUser.confirmRegister({
username:props.username,
code:form.code
code:form.code,
openId
})
if(res?.code==0) {
Message.info(t("tip.registerSuccess"))
SessionStorage.remove("wechatOpenId")
await router.replace("login")
} else {
Message.error(res.msg)

View File

@ -0,0 +1,30 @@
<template>
</template>
<script setup lang="ts">
import {apiUser} from "@/business/common/request/request";
const request=async (code:string)=>{
let res=await apiUser.wechatCode({
code:code
})
if(res?.code==0) {
window.opener.postMessage(res.data.openId)
window.close()
}
}
let query=location.hash.substring(location.hash.indexOf("?")+1)
let map:{
[key:string]:string
}={}
for(let obj of query.split("&")) {
let arr=obj.split("=")
map[arr[0]]=arr[1]
}
request(map["code"])
</script>
<style scoped>
</style>

View File

@ -52,7 +52,17 @@ const routes=[
path:"/resetCode",
component: ()=>import("./business/controller/login/resetCode.vue"),
props:route=>({username:route.query.username})
}
},
{
name:"wechat",
path:"/wechat",
component: ()=>import("./business/controller/login/wechat.vue"),
},
{
name:"bindAccount",
path:"/bindAccount",
component: ()=>import("./business/controller/login/bindAccount.vue"),
},
]
const router=createRouter({
history:createWebHashHistory(),
@ -78,6 +88,18 @@ apiGateway.deployInfo().then(async value => {
if(!(hash.startsWith("#/login") || hash.startsWith("#/desktop"))) {
await router.replace("login")
}
} else {
loadStaticScript()
}
}
})
function loadStaticScript() {
let script = document.createElement('script');
script.type = 'text/javascript';
script.onload = function() {
eval('LA.init({id:"KEwmiZNvHr5leFev",ck:"KEwmiZNvHr5leFev"})')
}
script.src = 'https://sdk.51.la/js-sdk-pro.min.js';
document.body.appendChild(script);
}

View File

@ -39,8 +39,9 @@ export interface ITeamOS_Window_Node {
export interface ITeamOS_Window_Event {
"open":(item:Window)=>void,
"move":(item:Window)=>void,
"newTab":(item:Window)=>Promise<ITeamOS_Window_Node>
"removeTab":(item:Window)=>void
"newTab":(item:Window)=>Promise<ITeamOS_Window_Node>,
"removeTab":(item:Window)=>void,
"close":(item:Window)=>Promise<boolean>
}
export function getCurrentWindow():Window {
@ -59,6 +60,8 @@ export class Window extends Base{
width:"80%",
height:"80%"
}
originalRect:ITeamOS_Rect
originalStatus:ETeamOS_Window_Status
group:string
nodes:ITeamOS_Window_Node[]
status=ETeamOS_Window_Status.NORMAL
@ -68,6 +71,7 @@ export class Window extends Base{
onOpen:(item:Window)=>void
onNewTab:(item:Window)=>Promise<ITeamOS_Window_Node>
onRemoveTab:(item:Window)=>void
onClose:(item:Window)=>Promise<boolean>
constructor(name:string,type:ETeamOS_Window_Type,group:string,isControl:boolean,nodes:ITeamOS_Window_Node[],title?:string) {
super()
this.name=name;
@ -87,13 +91,15 @@ export class Window extends Base{
}
addEventListener<T extends keyof ITeamOS_Window_Event>(eventType:T,func:ITeamOS_Window_Event[T]) {
if(eventType=="move") {
this.onMove=func.bind(null,this);
this.onMove=func
} else if(eventType=="open") {
this.onOpen=func
} else if(eventType=="newTab") {
this.onNewTab=func as ITeamOS_Window_Event["newTab"]
} else if(eventType=="removeTab") {
this.onRemoveTab=func.bind(null,this);
this.onRemoveTab=func
} else if(eventType=="close") {
this.onClose=func as ITeamOS_Window_Event["close"]
}
}
}

View File

@ -23,7 +23,7 @@
</a-col>
</a-row>
</a-col>
<a-col flex="auto" style="height: 100%;text-align: center;line-height: 35px;color: rgb(93,93,93);font-size: medium;cursor: move">
<a-col flex="auto" style="height: 100%;text-align: center;line-height: 35px;color: rgb(93,93,93);font-size: medium;cursor: move" @dblclick="onDbClick">
</a-col>
<a-col flex="80px" style="height: 100%">
@ -102,6 +102,15 @@ const onMin=(id:string)=>{
const onClose=(id:string)=>{
windowManager.close(id)
}
const onDbClick=()=>{
if(props.item.status===ETeamOS_Window_Status.NORMAL) {
windowManager.max(props.item.id)
} else {
windowManager.normal(props.item.id)
}
}
const navigator=ref<InstanceType<typeof NavigatorContainer>>(null)
const navigatorList=ref<InstanceType<typeof NavigatorContainer>[]>([])
const back=()=>{

View File

@ -47,9 +47,16 @@ export class WindowManager extends Base{
}
}
}
removeById(id:string) {
async removeById(id:string) {
for(let i=0;i<this.windowList.length;i++) {
if(this.windowList[i].id==id) {
let obj=this.windowList[i]
if(obj.onClose) {
let ret=await obj.onClose(obj)
if(ret===false) {
return
}
}
this.windowList.splice(i,1)
break
}
@ -91,6 +98,7 @@ export class WindowManager extends Base{
max(id:string){
let obj=this.setFocus(id);
if(obj) {
obj.originalRect={...obj.rect}
obj.rect.left="0%"
obj.rect.top="0%"
obj.rect.width="100%"
@ -111,6 +119,8 @@ export class WindowManager extends Base{
hide(id:string) {
let obj=this.getById(id);
if(obj) {
obj.originalRect={...obj.rect}
obj.originalStatus=obj.status
obj.status=ETeamOS_Window_Status.MIN
if(obj.isFocus) {
obj.isFocus=false
@ -123,10 +133,15 @@ export class WindowManager extends Base{
if(isNew) {
obj.rect.top=`${10+Math.random()*8-4}%`
obj.rect.left=`${10+Math.random()*8-4}%`
obj.status=ETeamOS_Window_Status.NORMAL
obj.rect.width="80%"
obj.rect.height="80%"
} else {
if(obj.status===ETeamOS_Window_Status.MIN) {
Object.assign(obj.rect,obj.originalRect)
obj.status=obj.originalStatus
}
}
obj.status=ETeamOS_Window_Status.NORMAL
obj.rect.width="80%"
obj.rect.height="80%"
}
this.setFocus(id);
}

View File

@ -6,51 +6,51 @@ import viteCompression from 'vite-plugin-compression'
import {createSvgIconsPlugin} from "vite-plugin-svg-icons";
export default defineConfig({
resolve:{
alias:{
"@":path.join(__dirname,"src")
}
resolve: {
alias: {
"@": path.join(__dirname, "src")
}
},
plugins: [
vue(),
viteCompression({
threshold: 102400
}),
createSvgIconsPlugin({
iconDirs: [path.join(__dirname, "./src/assert/custom")],
symbolId: '[name]'
})
],
server: {
host: "0.0.0.0",
https: {
key: fs.readFileSync(path.join(__dirname, './certs/key.pem')),
cert: fs.readFileSync(path.join(__dirname, './certs/cert.pem'))
},
port: 3000,
hmr: true,
open: false, //自动打开
base: "./ ", //生产环境路径
proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
// 正则表达式写法
'^/api': {
secure: false,
target: 'https://localhost:14000/api', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/api/, '')
},
'^/file': {
secure: false,
target: 'https://localhost:14000/file', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/file/, '')
},
"/socket.io": {
target: "https://localhost:14000",
secure: false,
ws: true,
changeOrigin: true
}
}
},
plugins: [
vue(),
viteCompression({
threshold:102400
}),
createSvgIconsPlugin({
iconDirs:[path.join(__dirname,"./src/assert/custom")],
symbolId:'[name]'
})
],
server:{
host:"0.0.0.0",
https:{
key:fs.readFileSync(path.join(__dirname,'./certs/key.pem')),
cert: fs.readFileSync(path.join(__dirname,'./certs/cert.pem'))
},
port: 3000,
hmr:true,
open: false, //自动打开
base: "./ ", //生产环境路径
proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
// 正则表达式写法
'^/api': {
secure:false,
target: 'https://localhost:14000/api', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/api/, '')
},
'^/file': {
secure:false,
target: 'https://localhost:14000/file', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/file/, '')
},
"/socket.io":{
target:"https://localhost:14000",
secure:false,
ws:true,
changeOrigin: true
}
}
},
})

View File

@ -42,7 +42,12 @@ export default {
column:"column is kanban status for issues,you can add,edit or delete the column",
sprint:"sprint is a short version that includes many issues for agile project",
kanban:"kanban can show issues' status clearly in the sprint",
swimLane:"swim lane is a issues' pool that needs to complete a specific task"
swimLane:"swim lane is a issues' pool that needs to complete a specific task",
manDay:"the number of days it takes to complete the issue",
plan:"we can plan the issues,arrange the dependence relationship,and make a gantt chart",
planItemType:`stage:it's a collection that includes sub stages,issues,and milestones.
milestone:it shows the completion of issues and stages above in a level.
`
},
notification:{
teamUserAdd:{
@ -141,6 +146,7 @@ export default {
deleteEvent:"Do you want to delete this event?",
deleteFolder:"do you want to delete this folder?",
deleteItems:"Do you want to delete these items?",
deleteItem:"Do you want to delete this item?",
deleteFavorite:"Do you want delete this favorite?",
deleteMeeting:"Do you want to delete this meeting?",
leaveMeeting:"Do you leave this meeting?",
@ -237,7 +243,10 @@ export default {
deleteOrganization:"Do you want to delete this organization?",
notMatch:"input not match",
quitOrganization:"do you want to quit this organization?",
switchToOrganization:"you should switch to organization "
switchToOrganization:"you should switch to organization ",
deletePlan:"Do you want to delete this plan?",
bindExistedAccount:"Do you want to bind an existed account?",
leavePage:"Are you sure you want to leave?"
},
placeholder:{
pleaseSelect:"Please Select",
@ -277,7 +286,8 @@ export default {
typeOrganizationName:"Type Organization Name",
typeProjectReleaseName:"Type Project Release Name",
typeCalendarEventName:"type calendar event name",
typeMeetingRoomName:"type meeting room name"
typeMeetingRoomName:"type meeting room name",
typePlanName:"Type Plan Name",
},
"util":{
"ok":"Ok",
@ -426,6 +436,7 @@ export default {
meetingPassword:"Meeting Password",
users:"Users",
selectCamera:"Select Camera",
selectAudio:"Select Audio Input",
videoOn:"Video On",
audioOn:"Audio On",
created:"Created",
@ -502,7 +513,23 @@ export default {
createdBy:"Created By",
application:"Application",
todo:"To Do",
unNamed:"UnNamed"
unNamed:"UnNamed",
manDay:"Man Day",
plan:"Plan",
gantt:"Gantt",
stage:"Stage",
milestone:"Milestone",
depend:"Depend",
delay:"Delay(Day)",
day:"Day",
month:"Month",
sunday:"Sunday",
monday:"Monday",
tuesday:"Tuesday",
wednesday:"Wednesday",
thursday:"Thursday",
friday:"Friday",
saturday:"Saturday"
},
"common":{
"component":{
@ -925,6 +952,9 @@ export default {
"login":{
registerCode:{
receiveCode:"if you don't receive verification code email,you can"
},
bindAccount:{
bindExistedAccount: "Bind Account"
}
}
}

View File

@ -42,7 +42,12 @@ export default {
column:"列是代表了工作项的看板状态,你可以编辑,新增,删除列",
sprint:"对于敏捷项目来说,冲刺就是一个包含工作项的小版本",
kanban:"看板可以直观地反映出冲刺中工作项的状态",
swimLane:"泳道可以看做是一个为了完成特定任务的需求池"
swimLane:"泳道可以看做是一个为了完成特定任务的需求池",
manDay: "完成工作项需要花费的天数",
plan:"我们可以对工作项进行排期,规划前后依赖关系,形成甘特图",
planItemType:`阶段:阶段包含了子阶段,里程碑和工作项,可以看做是一段工作的集合.
里程碑:它反应了同层级下在其上面的工作项和阶段的完成情况.
`
},
notification:{
teamUserAdd:{
@ -92,7 +97,7 @@ export default {
issueReporterAssign:{
0:"工作项:",
1:"将",
2:"的reporter移交给你了"
2:"的汇报人移交给你了"
},
issueWorkflowChange:{
0:"工作项:",
@ -112,7 +117,7 @@ export default {
issueAssignerAssign:{
0:"工作项:",
1:"将",
2:"的assigner移交给你了"
2:"的执行人移交给你了"
},
issueAssignRelease:{
0:"工作项:",
@ -141,6 +146,7 @@ export default {
deleteEvent:"你要删除这个日历事件吗?",
deleteFolder:"你要删除这个文件夹吗?",
deleteItems:"你要删除这些项目吗?",
deleteItem:"你要删除这个项目吗?",
deleteFavorite:"你要删除这个会议吗?",
leaveMeeting:"你要离开这个会议吗?",
endMeeting:"你要结束这个会议吗?",
@ -236,7 +242,10 @@ export default {
deleteOrganization:"你是否想删除该组织?",
notMatch:"输入不匹配",
quitOrganization:"你确定要退出当前组织吗?",
switchToOrganization:"你应该切换到组织"
switchToOrganization:"你应该切换到组织",
deletePlan:"你要删除这个规划吗?",
bindExistedAccount:"你是否要绑定到一个已有账号?",
leavePage:"你确定要离开当前页面吗?"
},
placeholder:{
pleaseSelect:"请选择",
@ -275,7 +284,8 @@ export default {
typeOrganizationName:"输入组织名称",
typeProjectReleaseName:"输入发布名称",
typeCalendarEventName:"输入日历事件名称",
typeMeetingRoomName:"输入会议名称"
typeMeetingRoomName:"输入会议名称",
typePlanName:"输入规划名称"
},
"util":{
"ok":"确认",
@ -388,7 +398,7 @@ export default {
resend:"重发",
wallpaper:"壁纸",
members:"成员",
detail:"描述",
detail:"详情",
more:"更多",
basic:"基本信息",
projectBoard:"项目面板",
@ -424,6 +434,7 @@ export default {
meetingPassword:"会议密码",
users:"用户",
selectCamera:"选择相机",
selectAudio:"选择音频输入",
videoOn:"视频开启",
audioOn:"声音开启",
created:"已创建",
@ -431,7 +442,7 @@ export default {
issueTypes:"工作项类型",
dateRange:"日期范围",
fixVersions:"修复版本",
keyword:"项目Id",
keyword:"项目标识",
issueKey:"工作项Id",
logo:"Logo",
fieldType:"字段类型",
@ -500,7 +511,23 @@ export default {
createdBy:"创建人",
application:"应用",
todo:"待办中",
unNamed:"未命名"
unNamed:"未命名",
manDay:"工天",
plan:"规划",
gantt:"甘特图",
stage:"阶段",
milestone:"里程碑",
depend:"前置依赖",
delay:"延迟(天)",
day:"天",
month:"月",
sunday:"周日",
monday:"周一",
tuesday:"周二",
wednesday:"周三",
thursday:"周四",
friday:"周五",
saturday:"周六"
},
"common":{
"component":{
@ -923,6 +950,9 @@ export default {
"login":{
registerCode:{
receiveCode:"如果你没有收到验证码邮件,你可以"
},
bindAccount:{
bindExistedAccount: "绑定已有账号"
}
}
}

18
code/common/model/plan.ts Normal file
View File

@ -0,0 +1,18 @@
import {BaseModel} from "./base"
export interface ICommon_Model_Plan {
id :string ,
name:string,
start_time:Date,
organization_user_id:string,
project_id:string,
organization_id:string
}
export const Table_Plan="plan"
class PlanModel extends BaseModel {
table=Table_Plan
model=<ICommon_Model_Plan>{}
}
export let planModel=new PlanModel

View File

@ -0,0 +1,28 @@
import {BaseModel} from "./base"
export enum ECommon_Model_Plan_Table {
STAGE,
MILESTONE,
ISSUE
}
export interface ICommon_Model_Plan_Table {
id :string,
sort:number,
type:ECommon_Model_Plan_Table
name:string,
ref_id:string,
progress:number,
depend_id:string,
delay:number,
parent_id:string,
plan_id:string,
project_id:string
}
export const Table_Plan_Table="plan_table"
class PlanTableModel extends BaseModel {
table=Table_Plan_Table
model=<ICommon_Model_Plan_Table>{}
}
export let planTableModel=new PlanTableModel

View File

@ -19,7 +19,8 @@ export interface ICommon_Model_Project_Issue {
priority: ECommon_Model_Project_Issue_Priority,
assigner_id: string,
reporter_id: string,
workflow_node_id: string
workflow_node_id: string,
man_day:number
}
export const Table_Project_Issue = "project_issue"

View File

@ -5,6 +5,12 @@ export enum ECommon_User_Type {
ADMIN,
DELETED
}
export enum ECommon_User_From_Type {
LOCAL,
WECHAT
}
export interface ICommon_Model_User {
id :string,
username :string,
@ -15,7 +21,9 @@ export interface ICommon_Model_User {
sign :string,
active:number,
role:ECommon_User_Type,
count:number
count:number,
from_type:ECommon_User_From_Type,
from_id:string
}
export const Table_User="user"

View File

@ -35,6 +35,15 @@ const api={
type:ECommon_Application_Mode
}>{},
ignoreValidate:true
},
wechatAppId:{
method:ECommon_HttpApi_Method.GET,
path:"/wechat/appid",
req:{},
res:<{
appId:string
}>{},
ignoreValidate:true
}
}
}

View File

@ -45,6 +45,7 @@ const api={
priority :number,
assignerId? :string ,
reporterId? :string ,
manDay:number,
values?:ICommon_Route_Req_ProjectIssue_Field[]
}>{},
res:<ICommon_Model_Project_Issue>{},
@ -89,7 +90,8 @@ const api={
name? :string,
priority? :number,
assignerId? :string ,
reporterId? :string
reporterId? :string ,
manDay?:number
}>{},
res:<ICommon_Model_Project_Issue>{},
permission:[Permission_Types.Project.EDIT]

196
code/common/routes/plan.ts Normal file
View File

@ -0,0 +1,196 @@
import {ECommon_Services} from "../types";
import {ECommon_HttpApi_Method} from "./types";
import {Permission_Types} from "../permission/permission";
import {ICommon_Model_Plan} from "../model/plan";
import {ICommon_Route_Res_Plan_Info, ICommon_Route_Res_Plan_Info_Item, ICommon_Route_Res_Plan_List} from "./response";
const api= {
baseUrl: "/plan",
service: ECommon_Services.Cooperation,
routes: {
listPlan: {
method: ECommon_HttpApi_Method.GET,
path: "/list",
req: <{
projectId: string,
page: number,
size: number,
keyword?: string
}>{},
res: <ICommon_Route_Res_Plan_List>{},
permission: [Permission_Types.Project.READ]
},
plan: {
method: ECommon_HttpApi_Method.GET,
path: "/item",
req: <{
planId:string
}>{},
res: <ICommon_Model_Plan>{},
permission: [Permission_Types.Project.READ]
},
createPlan: {
method: ECommon_HttpApi_Method.POST,
path: "/item",
req: <{
projectId:string,
name:string,
startTime:number
}>{},
res: <ICommon_Model_Plan>{},
permission: [Permission_Types.Project.EDIT]
},
editPlan:{
method: ECommon_HttpApi_Method.PUT,
path: "/item",
req: <{
planId:string
name?:string,
startTime?:number
}>{},
res: <ICommon_Model_Plan>{},
permission: [Permission_Types.Project.EDIT]
},
removePlan:{
method: ECommon_HttpApi_Method.DELETE,
path: "/item",
req: <{
planId:string
}>{},
res: {},
permission: [Permission_Types.Project.DELETE,Permission_Types.Common.SELF],
permissionOr:true
},
info:{
method: ECommon_HttpApi_Method.GET,
path: "/info",
req: <{
planId:string
}>{},
res: <ICommon_Route_Res_Plan_Info>{},
permission: [Permission_Types.Project.READ]
},
createStage:{
method: ECommon_HttpApi_Method.POST,
path: "/stage",
req: <{
planId:string,
name:string,
parentId?:string,
dependId?:string,
delay?:number
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
editStage:{
method: ECommon_HttpApi_Method.PUT,
path: "/stage",
req: <{
planItemId:string,
name?:string,
dependId?:string,
delay?:number
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
createMileStone:{
method: ECommon_HttpApi_Method.POST,
path: "/milestone",
req: <{
planId:string,
name:string,
parentId?:string
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
editMileStone:{
method: ECommon_HttpApi_Method.PUT,
path: "/milestone",
req: <{
planItemId:string,
name?:string,
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
addIssue:{
method: ECommon_HttpApi_Method.POST,
path: "/issue",
req: <{
planId:string,
parentId?:string,
projectIssueId:string,
dependId?:string,
delay?:number
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
editIssue:{
method: ECommon_HttpApi_Method.PUT,
path: "/issue",
req: <{
planItemId:string,
dependId?:string,
delay?:number,
manDay?:number
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
removeItem:{
method: ECommon_HttpApi_Method.DELETE,
path: "/list/item",
req: <{
planItemId:string
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
editProgress:{
method: ECommon_HttpApi_Method.PUT,
path: "/issue/progress",
req: <{
planItemId:string,
progress:number
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
moveItem:{
method: ECommon_HttpApi_Method.PUT,
path: "/move",
req: <{
planItemId:string,
targetId:string,
action:"in"|"top"|"bottom"
}>{},
res: <ICommon_Route_Res_Plan_Info_Item[]>{},
permission: [Permission_Types.Project.EDIT]
},
issuePlanList:{
method: ECommon_HttpApi_Method.GET,
path: "/issueplan/list",
req: <{
projectIssueId:string
}>{},
res: <ICommon_Model_Plan[]>{},
permission: [Permission_Types.Project.READ]
},
issuePlanEdit:{
method: ECommon_HttpApi_Method.POST,
path: "/issueplan",
req: <{
projectIssueId:string,
planList:string[]
}>{},
res: <ICommon_Model_Plan[]>{},
permission: [Permission_Types.Project.EDIT]
}
}
}
export default api

View File

@ -35,6 +35,8 @@ import {ICommon_Model_Board_Column} from "../model/board_column";
import {ICommon_Model_Sticky_Note} from "../model/sticky_note";
import {ICommon_Model_Content} from "../model/content";
import {ICommon_Model_Photo} from "../model/photo";
import {ICommon_Model_Plan_Table} from "../model/plan_table";
import {ICommon_Model_Plan} from "../model/plan";
export interface ICommon_Route_Res_Project_CreateModule_Data {
id:string,
@ -560,3 +562,20 @@ export type ICommon_Route_Res_Photo_item =ICommon_Model_Photo & {
export type ICommon_Route_Res_RecentIssue_Item= ICommon_Model_Project_Issue & {
project:ICommon_Model_Project
}
export type ICommon_Route_Res_Plan_Info_Item=ICommon_Model_Plan_Table & {
issue?:ICommon_Model_Project_Issue,
workflow?:ICommon_Model_Workflow_Node,
children?:ICommon_Route_Res_Plan_Info_Item[]
}
export type ICommon_Route_Res_Plan_Info = ICommon_Model_Plan & {
data:ICommon_Route_Res_Plan_Info_Item[]
}
export type ICommon_Route_Res_Plan_List={
count:number,
totalPage:number,
page:number,
data:ICommon_Model_Plan[]
}

View File

@ -162,7 +162,8 @@ const api={
path:"/register/confirm",
req:<{
username:string,
code:string
code:string,
openId?:string
}>{},
res:{},
ignoreValidate:true
@ -219,6 +220,38 @@ const api={
lang:string
}>{},
res:{},
},
wechatCode:{
method:ECommon_HttpApi_Method.GET,
path:"/wechat/code",
req:<{
code:string
}>{},
res:<{
openId:string
}>{},
ignoreValidate:true
},
wechatLogin:{
method:ECommon_HttpApi_Method.POST,
path:"/wechat/login",
req:<{
openId:string,
lang:string
}>{},
res:<Omit<ICommon_Model_User,"password">>{},
ignoreValidate:true
},
bindWechat:{
method:ECommon_HttpApi_Method.POST,
path:"/wechat/bind",
req:<{
openId:string,
username:string,
password:string
}>{},
res:{},
ignoreValidate:true
}
}
}

View File

@ -582,6 +582,43 @@ export namespace Err {
zh: "面板为包含该工作项类型"
}
}
},
Plan:{
planNotFound:{
code:3920,
msg:{
en:"plan not found",
zh:"计划不存在"
}
},
planItemNotFound:{
code:3921,
msg:{
en:"plan item not found",
zh:"计划项目不存在"
}
},
planTypeNotMatched:{
code:3922,
msg:{
en:"plan type not matched",
zh:"计划的类型不匹配"
}
},
dependItemNotMatchedParentItem:{
code:3923,
msg:{
en:"depend item not matched parent item",
zh:"依赖项需要和是父项目的子项目"
}
},
operationForbidden:{
code:3924,
msg:{
en:"operation is forbidden",
zh:"禁止该操作"
}
}
}
}
export const Team = {

Binary file not shown.

View File

@ -9,7 +9,7 @@ import rpcCooperationApi from "../../cooperation/rpc/cooperation"
@DComponent
class PermissionProject extends PermissionBase {
fieldName="projectId"
async translateToField({commentId,projectIssueId,labelId,moduleId,projectReleaseId,roleId,boardId,boardSprintId,boardSprintSwimLaneId,boardColumnId}: { [param: string]: any; isAdmin?: boolean;commentId:string,projectIssueId:string,labelId:string,moduleId:string,projectReleaseId:string ,roleId:string}): Promise<string> {
async translateToField({commentId,projectIssueId,labelId,moduleId,projectReleaseId,roleId,boardId,boardSprintId,boardSprintSwimLaneId,boardColumnId,planId,planItemId}: { [param: string]: any; isAdmin?: boolean;commentId:string,projectIssueId:string,labelId:string,moduleId:string,projectReleaseId:string ,roleId:string}): Promise<string> {
let projectId:string
if(commentId) {
let obj=new REDIS_AUTH.Permission.Project.ProjectIdFromProjectIssueComment(commentId);
@ -53,6 +53,14 @@ class PermissionProject extends PermissionBase {
let obj=new REDIS_AUTH.Permission.Project.ProjectIdFromBoardColumn(boardColumnId);
projectId=await obj.getValue();
return projectId;
} else if(planId) {
let obj=new REDIS_AUTH.Permission.Project.ProjectIdFromPlan(planId);
projectId=await obj.getValue();
return projectId;
} else if(planItemId) {
let obj=new REDIS_AUTH.Permission.Project.ProjectIdFromPlanItem(planItemId);
projectId=await obj.getValue();
return projectId;
}
}

View File

@ -77,6 +77,10 @@ export default abstract class Application{
"port": number,
"user":string,
"pass":string
},
wechat:{
appId:string,
appSecret:string
}
}
Application(){

View File

@ -30,6 +30,9 @@ import {boardModel} from "../../../../common/model/board";
import {boardSprintModel} from "../../../../common/model/board_sprint";
import {boardSprintSwimLaneModel} from "../../../../common/model/board_sprint_swimlane";
import {boardColumnModel} from "../../../../common/model/board_column";
import {planModel} from "../../../../common/model/plan";
import {projectModel} from "../../../../common/model/project";
import {planTableModel} from "../../../../common/model/plan_table";
export namespace REDIS_AUTH {
export namespace Resource {
@ -489,6 +492,88 @@ export namespace REDIS_AUTH {
return projectId;
}
}
export class ProjectIdFromPlan extends BaseRedisStringCache<string> {
key="permission:projectPlan:{0}"
constructor(private planId:string) {
super()
this.objRedis=new RedisStringKey(StringUtil.format(this.key,planId),cacheRedisType<string>().String,1000*10);
}
async getValue():Promise<string> {
let mysql=getMysqlInstance()
let projectId:string
let exists=await this.objRedis.exists()
if(exists) {
projectId=await this.objRedis.get()
} else {
let sql=generateLeftJoinSql({
model:planModel
},{
model:projectModel,
columns:["id"],
expression:{
id:{
model:planModel,
field:"project_id"
}
}
},{
id:{
model:planModel,
value:this.planId
}
})
let ret=await mysql.executeOne(sql)
if(ret) {
projectId=ret.id
await this.objRedis.set(projectId)
} else {
throw Err.Project.Plan.planNotFound
}
}
return projectId;
}
}
export class ProjectIdFromPlanItem extends BaseRedisStringCache<string> {
key="permission:projectPlanItem:{0}"
constructor(private planItemId:string) {
super()
this.objRedis=new RedisStringKey(StringUtil.format(this.key,planItemId),cacheRedisType<string>().String,1000*10);
}
async getValue():Promise<string> {
let mysql=getMysqlInstance()
let projectId:string
let exists=await this.objRedis.exists()
if(exists) {
projectId=await this.objRedis.get()
} else {
let sql=generateLeftJoinSql({
model:planTableModel
},{
model:projectModel,
columns:["id"],
expression:{
id:{
model:planTableModel,
field:"project_id"
}
}
},{
id:{
model:planTableModel,
value:this.planItemId
}
})
let ret=await mysql.executeOne(sql)
if(ret) {
projectId=ret.id
await this.objRedis.set(projectId)
} else {
throw Err.Project.Plan.planItemNotFound
}
}
return projectId;
}
}
export class ProjectOrganizationUsers extends BaseRedisHashCache<string> {
key="permission:project:{0}:organizationUser"
constructor(private projectId:string) {

View File

@ -19,6 +19,7 @@ export namespace REDIS_USER {
let USER_ORGANIZATION_KEY=`${ECommon_Services.User}:user:{0}:organization`
let USER_REGISTER_KEY=`${ECommon_Services.User}:username:{0}:register`
let USER_RESET_CODE_KEY=`${ECommon_Services.User}:user:{0}:reset`
let WECHAT_OPEN_ID_KEY=`${ECommon_Services.User}:wechat:openid`
export function token(userId:string)
{
let obj=new RedisStringKey(StringUtil.format(USER_TOKEN_KEY,userId),cacheRedisType<string>().String,300)
@ -51,4 +52,11 @@ export namespace REDIS_USER {
let obj=new RedisStringKey(StringUtil.format(USER_RESET_CODE_KEY,userId),cacheRedisType<ICommon_Register_Cache_Info>().Object,600)
return obj
}
export function wechatOpenId(openId:string)
{
let obj=new RedisStringKey(StringUtil.format(WECHAT_OPEN_ID_KEY,openId),cacheRedisType<{
img:string
}>().Object,600)
return obj
}
}

View File

@ -1,5 +1,10 @@
import Application from "../app/app";
import {IServer_Common_Config_Mail, IServer_Common_Config_Mysql, IServer_Common_Config_Redis} from "../types/config";
import {
IServer_Common_Config_Mail,
IServer_Common_Config_Mysql,
IServer_Common_Config_Redis,
IServer_Common_Config_Wechat
} from "../types/config";
class Config {
constructor() {
@ -27,6 +32,9 @@ class Config {
get mailInfo():IServer_Common_Config_Mail {
return Application.privateConfig.mail
}
get wechatInfo():IServer_Common_Config_Wechat {
return Application.privateConfig.wechat
}
}
var g_config:InstanceType<typeof Config>
export function getConfigInstance() {

View File

@ -0,0 +1,64 @@
import * as https from 'https'
import {IncomingHttpHeaders} from "http"
import * as fs from "fs-extra";
import * as crypto from "crypto";
export const request=(urlOptions: string | https.RequestOptions | URL, data = ''):Promise<{
statusCode:number,
headers:IncomingHttpHeaders,
body:string
}> => new Promise((resolve, reject) => {
const req = https.request(urlOptions, res => {
const chunks = []
res.on('data', chunk => chunks.push(chunk))
res.on('error', reject)
res.on('end', () => {
const { statusCode, headers } = res
const validResponse = statusCode >= 200 && statusCode <= 299
const body = chunks.join('')
if (validResponse) resolve({ statusCode, headers, body })
else reject(new Error(`Request failed. status: ${statusCode}, body: ${body}`))
})
})
req.on('error', reject)
req.write(data, 'binary')
req.end()
})
export const download=(uri:string,dest:string):Promise<{
md5:string,
size:number
}>=>{
return new Promise(async (resolve, reject)=>{
await fs.ensureFile(dest)
const file = fs.createWriteStream(dest);
https.get(uri, (res)=>{
if(res.statusCode !== 200){
reject(res.statusCode);
return;
}
const hash = crypto.createHash("md5");
let size=0;
res.on("data",chunk => {
hash.update(chunk, "utf8");
size+=chunk.length
});
file.on('finish', ()=>{
file.close(err => {
const md5 = hash.digest("hex");
resolve({
md5,
size
})
});
}).on('error', (err)=>{
fs.unlink(dest);
reject(err.message);
})
res.pipe(file);
});
});
}

View File

@ -24,6 +24,11 @@ export interface IServer_Common_Config_Mail {
"pass":string
}
export interface IServer_Common_Config_Wechat {
appId:string,
appSecret:string
}
export interface IServer_Common_Config {
services:{
[name:string]:any

View File

@ -7,4 +7,5 @@ import "../http/release";
import "../http/label";
import "../http/workflow";
import "../http/board"
import "../http/plan"
import "../rpc/cooperation";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,441 @@
import {DComponent} from "../../common/decorate/component";
import {DHttpApi, DHttpController, DHttpReqParam, DHttpReqParamRequired, DHttpUser} from "../../common/http/http";
import planApi from "../../../common/routes/plan";
import {PlanService, PlanTableService} from "../service/plan";
import {Err} from "../../../common/status/error";
import {IUserSession} from "../../user/types/config";
import {ECommon_Model_Plan_Table} from "../../../common/model/plan_table";
import {ProjectIssueService} from "../service/issue";
import {ECommon_Model_Workflow_Node_Status} from "../../../common/model/workflow_node";
@DComponent
@DHttpController(planApi)
class PlanController {
@DHttpApi(planApi.routes.listPlan)
async listPlan(@DHttpReqParamRequired("projectId") projectId: string, @DHttpReqParamRequired("page") page: number, @DHttpReqParamRequired("size") size: number, @DHttpReqParam("keyword") keyword: string): Promise<typeof planApi.routes.listPlan.res> {
let list=await PlanService.list(projectId,page,size,keyword)
return list
}
@DHttpApi(planApi.routes.plan)
async plan(@DHttpReqParamRequired("planId") planId: string): Promise<typeof planApi.routes.plan.res> {
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
return obj.getItem()
}
@DHttpApi(planApi.routes.createPlan)
async createPlan(@DHttpReqParamRequired("projectId") projectId: string,@DHttpReqParamRequired("name") name: string,@DHttpReqParamRequired("startTime") startTime: number,@DHttpUser user:IUserSession): Promise<typeof planApi.routes.createPlan.res> {
let obj=new PlanService()
obj.assignItem({
project_id:projectId,
name,
start_time:new Date(startTime),
organization_id:user.organizationInfo.organizationId,
organization_user_id:user.organizationInfo.organizationUserId
})
let ret=await obj.create()
return ret;
}
@DHttpApi(planApi.routes.editPlan)
async editPlan(@DHttpReqParamRequired("planId") planId: string,@DHttpReqParam("name") name: string,@DHttpReqParam("startTime") startTime: number): Promise<typeof planApi.routes.editPlan.res> {
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
obj.assignItem({
name,
...(startTime && {
start_time:new Date(startTime)
})
})
let ret=await obj.update()
return ret;
}
@DHttpApi(planApi.routes.removePlan)
async removePlan(@DHttpReqParamRequired("planId") planId: string): Promise<typeof planApi.routes.removePlan.res> {
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
await obj.delete()
return
}
@DHttpApi(planApi.routes.info)
async info(@DHttpReqParamRequired("planId") planId: string): Promise<typeof planApi.routes.info.res> {
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
let ret=await obj.info()
return {
...obj.getItem(),
data:ret
};
}
@DHttpApi(planApi.routes.createStage)
async createStage(@DHttpReqParamRequired("planId") planId: string,@DHttpReqParam("name") name: string,@DHttpReqParam("parentId") parentId: string,@DHttpReqParam("dependId") dependId: string,@DHttpReqParam("delay") delay: number): Promise<typeof planApi.routes.createStage.res> {
let [objPlan,objParentItem,objDependItem,newSort]=await Promise.all([
(async ()=>{
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
return obj;
})(),
parentId?(async ()=>{
let objParent=await PlanTableService.getItemById(parentId)
if(!objParent) {
throw Err.Project.Plan.planItemNotFound
} else if(objParent.getItem().type!==ECommon_Model_Plan_Table.STAGE) {
throw Err.Project.Plan.planTypeNotMatched
}
return objParent.getItem()
})():null,
dependId?(async ()=>{
let objDepend=await PlanTableService.getItemById(dependId)
if(!objDepend) {
throw Err.Project.Plan.planItemNotFound
} else if(objDepend.getItem().type===ECommon_Model_Plan_Table.MILESTONE) {
throw Err.Project.Plan.planTypeNotMatched
}
return objDepend.getItem()
})():null,
(async ()=>{
let sort=await PlanTableService.getNewSort(parentId??null,planId)
return sort
})()
])
if(objDependItem && objDependItem.parent_id!=parentId) {
throw Err.Project.Plan.dependItemNotMatchedParentItem
}
let obj=new PlanTableService()
obj.assignItem({
plan_id:planId,
type:ECommon_Model_Plan_Table.STAGE,
sort:newSort,
name,
parent_id:parentId,
delay,
depend_id:dependId,
project_id:objPlan.getItem().project_id,
})
await obj.create()
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.editStage)
async editStage(@DHttpReqParamRequired("planItemId") planItemId: string,@DHttpReqParamRequired("name") name: string,@DHttpReqParam("dependId") dependId: string,@DHttpReqParam("delay") delay: number): Promise<typeof planApi.routes.editStage.res> {
let obj=await PlanTableService.getItemById(planItemId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
}
if(dependId) {
let objDepend=await PlanTableService.getItemById(dependId)
if(!objDepend) {
throw Err.Project.Plan.planItemNotFound
} else if(objDepend.getItem().type===ECommon_Model_Plan_Table.MILESTONE) {
throw Err.Project.Plan.planTypeNotMatched
} else if(objDepend.getItem().parent_id!=obj.getItem().parent_id) {
throw Err.Project.Plan.dependItemNotMatchedParentItem
}
}
obj.assignItem({
name,
depend_id:dependId,
delay
})
await obj.update()
let objPlan=await PlanService.getItemById(obj.getItem().plan_id)
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.createMileStone)
async createMileStone(@DHttpReqParamRequired("planId") planId: string,@DHttpReqParamRequired("name") name: string,@DHttpReqParam("parentId") parentId: string): Promise<typeof planApi.routes.createMileStone.res> {
let [objPlan,objParentItem,newSort]=await Promise.all([
(async ()=>{
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
return obj;
})(),
parentId?(async ()=>{
let objParent=await PlanTableService.getItemById(parentId)
if(!objParent) {
throw Err.Project.Plan.planItemNotFound
} else if(objParent.getItem().type!==ECommon_Model_Plan_Table.STAGE) {
throw Err.Project.Plan.planTypeNotMatched
}
return objParent.getItem()
})():null,
(async ()=>{
let sort=await PlanTableService.getNewSort(parentId??null,planId)
return sort
})()
])
let obj=new PlanTableService()
obj.assignItem({
plan_id:planId,
type:ECommon_Model_Plan_Table.MILESTONE,
sort:newSort,
name,
parent_id:parentId,
project_id:objPlan.getItem().project_id,
})
await obj.create()
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.editMileStone)
async editMileStone(@DHttpReqParamRequired("planItemId") planItemId: string,@DHttpReqParam("name") name: string): Promise<typeof planApi.routes.editMileStone.res> {
let obj=await PlanTableService.getItemById(planItemId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
}
obj.assignItem({
name,
})
await obj.update()
let objPlan=await PlanService.getItemById(obj.getItem().plan_id)
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.addIssue)
async addIssue(@DHttpReqParamRequired("planId") planId: string,@DHttpReqParam("parentId") parentId: string,@DHttpReqParamRequired("projectIssueId") projectIssueId: string,@DHttpReqParam("dependId") dependId: string,@DHttpReqParam("delay") delay: number): Promise<typeof planApi.routes.addIssue.res> {
let [objPlan,objParentItem,objProjectIssue,objDependItem,newSort]=await Promise.all([
(async ()=>{
let obj=await PlanService.getItemById(planId)
if(!obj) {
throw Err.Project.Plan.planNotFound
}
return obj;
})(),
parentId?(async ()=>{
let objParent=await PlanTableService.getItemById(parentId)
if(!objParent) {
throw Err.Project.Plan.planItemNotFound
} else if(objParent.getItem().type!==ECommon_Model_Plan_Table.STAGE) {
throw Err.Project.Plan.planTypeNotMatched
}
return objParent.getItem()
})():null,
ProjectIssueService.getItemById(projectIssueId),
dependId?(async ()=>{
let objDepend=await PlanTableService.getItemById(dependId)
if(!objDepend) {
throw Err.Project.Plan.planItemNotFound
} else if(objDepend.getItem().type===ECommon_Model_Plan_Table.MILESTONE) {
throw Err.Project.Plan.planTypeNotMatched
}
return objDepend.getItem()
})():null,
(async ()=>{
let sort=await PlanTableService.getNewSort(parentId??null,planId)
return sort
})()
])
if(objDependItem && objDependItem.parent_id!=parentId) {
throw Err.Project.Plan.dependItemNotMatchedParentItem
}
let obj=new PlanTableService()
obj.assignItem({
plan_id:planId,
type:ECommon_Model_Plan_Table.ISSUE,
sort:newSort,
parent_id:parentId,
project_id:objPlan.getItem().project_id,
delay,
depend_id:dependId,
ref_id:projectIssueId
})
await obj.create()
let childIssueList=await objProjectIssue.childIssueList()
if(childIssueList?.length>0) {
await Promise.all(childIssueList.map((item,index)=>{
let obj1=new PlanTableService()
obj1.assignItem({
plan_id:planId,
type:ECommon_Model_Plan_Table.ISSUE,
sort:index,
parent_id:obj.getId(),
project_id:objPlan.getItem().project_id,
ref_id:item.id
})
return obj1.create()
}))
}
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.editIssue)
async editIssue(@DHttpReqParamRequired("planItemId") planItemId: string,@DHttpReqParam("dependId") dependId: string,@DHttpReqParam("delay") delay: number,@DHttpReqParam("manDay") manDay: number): Promise<typeof planApi.routes.editIssue.res> {
let [obj,objDepend]=await Promise.all([
(async ()=>{
let obj=await PlanTableService.getItemById(planItemId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
}
if(obj.getItem().parent_id) {
let objParent=await PlanTableService.getItemById(obj.getItem().parent_id)
if(!objParent) {
throw Err.Project.Plan.planItemNotFound
}
}
return obj
})(),
dependId?(async ()=>{
let obj=await PlanTableService.getItemById(dependId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
} else if(obj.getItem().type===ECommon_Model_Plan_Table.MILESTONE) {
throw Err.Project.Plan.planTypeNotMatched
}
return obj;
}):null
])
obj.assignItem({
depend_id:dependId,
delay
})
await obj.update()
if(manDay) {
let objIssue=await ProjectIssueService.getItemById(obj.getItem().ref_id)
objIssue.assignItem({
man_day:manDay
})
await objIssue.update()
}
let objPlan=await PlanService.getItemById(obj.getItem().plan_id)
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.removeItem)
async removeItem(@DHttpReqParamRequired("planItemId") planItemId: string): Promise<typeof planApi.routes.removeItem.res> {
let obj=await PlanTableService.getItemById(planItemId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
}
let hasParentIssue=await obj.hasParentIssue()
if(hasParentIssue) {
throw Err.Project.Plan.operationForbidden
}
await obj.delete()
let objPlan=await PlanService.getItemById(obj.getItem().plan_id)
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.editProgress)
async editProgress(@DHttpReqParamRequired("planItemId") planItemId: string,@DHttpReqParamRequired("progress") progress: number): Promise<typeof planApi.routes.editProgress.res> {
let obj=await PlanTableService.getItemById(planItemId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
} else if(obj.getItem().type!==ECommon_Model_Plan_Table.ISSUE) {
throw Err.Project.Plan.planTypeNotMatched
}
let issueInfo=await obj.issueInfo()
if(issueInfo.workflow.status!==ECommon_Model_Workflow_Node_Status.INPROGRESS) {
throw Err.Project.Plan.operationForbidden
}
obj.assignItem({
progress
})
await obj.update()
let objPlan=await PlanService.getItemById(obj.getItem().plan_id)
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.moveItem)
async moveItem(@DHttpReqParamRequired("planItemId") planItemId: string,@DHttpReqParamRequired("targetId") targetId: string,@DHttpReqParamRequired("action") action: "in"|"top"|"bottom"): Promise<typeof planApi.routes.moveItem.res> {
let obj=await PlanTableService.getItemById(planItemId)
if(!obj) {
throw Err.Project.Plan.planItemNotFound
}
let objTarget:PlanTableService
if(targetId) {
objTarget=await PlanTableService.getItemById(targetId)
if(!objTarget) {
throw Err.Project.Plan.planItemNotFound
}
if(action==="in" && objTarget.getItem().type!==ECommon_Model_Plan_Table.STAGE) {
throw Err.Project.Plan.operationForbidden
}
}
if(action==="in") {
if(obj.getItem().parent_id!==objTarget.getItem().id) {
await obj.clearAllDepend()
}
let sort=await PlanTableService.getNewSort(targetId??null,obj.getItem().plan_id)
obj.assignItem({
depend_id:null,
parent_id:targetId??null,
sort
})
} else if(action==="top") {
if(obj.getItem().parent_id!==objTarget.getItem().parent_id) {
await obj.clearAllDepend()
}
await PlanTableService.moveUp(obj.getItem().parent_id,obj.getItem().sort,obj.getItem().plan_id)
await PlanTableService.moveDown(objTarget.getItem().parent_id,objTarget.getItem().sort,obj.getItem().plan_id)
obj.assignItem({
parent_id:objTarget.getItem().parent_id,
sort:objTarget.getItem().sort,
...(obj.getItem().parent_id!=objTarget.getItem().parent_id && {
depend_id:null
})
})
} else if(action==="bottom") {
if(obj.getItem().parent_id!==objTarget.getItem().parent_id) {
await obj.clearAllDepend()
}
await PlanTableService.moveUp(obj.getItem().parent_id,obj.getItem().sort,obj.getItem().plan_id)
await objTarget.loadItem()
await PlanTableService.moveDown(objTarget.getItem().parent_id,objTarget.getItem().sort+1,obj.getItem().plan_id)
obj.assignItem({
parent_id:objTarget.getItem().parent_id,
sort:objTarget.getItem().sort+1,
...(obj.getItem().parent_id!=objTarget.getItem().parent_id && {
depend_id:null
})
})
}
await obj.update()
let objPlan=await PlanService.getItemById(obj.getItem().plan_id)
let ret=await objPlan.info()
return ret;
}
@DHttpApi(planApi.routes.issuePlanList)
async issuePlanList(@DHttpReqParamRequired("projectIssueId") projectIssueId: string): Promise<typeof planApi.routes.issuePlanList.res> {
let objProjectIssue=await ProjectIssueService.getItemById(projectIssueId)
if(!objProjectIssue) {
throw Err.Project.ProjectIssue.projectIssueNotFound
}
let ret=await PlanService.issuePlanList(projectIssueId)
return ret;
}
@DHttpApi(planApi.routes.issuePlanEdit)
async issuePlanEdit(@DHttpReqParamRequired("projectIssueId") projectIssueId: string,@DHttpReqParamRequired("planList") planList: string[]): Promise<typeof planApi.routes.issuePlanEdit.res> {
await PlanService.issuePlanEdit(projectIssueId,planList);
let ret=await PlanService.issuePlanList(projectIssueId)
return ret;
}
}

View File

@ -709,12 +709,26 @@ class ProjectIssueMapper extends Mapper<typeof projectIssueModel> {
throw Err.Project.ProjectIssue.issueEqualForbidden
}
let mysql=getMysqlInstance()
let obj=await mysql.executeOne(generateQuerySql(projectIssueParentModel,["id"],{
child_id:projectChildIssueId
}))
if(obj) {
throw Err.Project.ProjectIssue.parentChildExists
}
await Promise.all([
(async ()=>{
let obj=await mysql.executeOne(generateQuerySql(projectIssueParentModel,["id"],{
child_id:projectChildIssueId,
parent_id:projectChildIssueId
},"or"))
if(obj) {
throw Err.Project.ProjectIssue.parentChildExists
}
})(),
(async ()=>{
let obj=await mysql.executeOne(generateQuerySql(projectIssueParentModel,["id"],{
child_id:projectParentIssueId,
parent_id:projectChildIssueId
}))
if(obj) {
throw Err.Project.ProjectIssue.parentChildExists
}
})()
])
await mysql.execute(generateCreateSql(projectIssueParentModel,{
id:await generateSnowId(),
parent_id:projectParentIssueId,
@ -798,6 +812,10 @@ class ProjectIssueMapper extends Mapper<typeof projectIssueModel> {
model:projectIssueParentModel,
value:projectIssueId
}
},"and",{
type:"asc",
model:projectIssueParentModel,
field:"child_id"
})
let ret=await mysql.execute(sql)
return ret;

View File

@ -0,0 +1,251 @@
import {Mapper} from "../../common/entity/mapper";
import {ICommon_Model_Plan, planModel} from "../../../common/model/plan";
import {ECommon_Model_Plan_Table, ICommon_Model_Plan_Table, planTableModel} from "../../../common/model/plan_table";
import {Err} from "../../../common/status/error";
import {
generateCommonListData,
generateDeleteSql,
generateGroupLeftJoinSql,
generateLeftJoin2Sql,
generateQuerySql,
generateUpdateSql
} from "../../common/util/sql";
import {getMysqlInstance} from "../../common/db/mysql";
import {keys} from "../../../common/transform";
import {ICommon_Model_Project_Issue, projectIssueModel} from "../../../common/model/project_issue";
import {ICommon_Model_Workflow_Node, workflowNodeModel} from "../../../common/model/workflow_node";
import {ICommon_Route_Res_Plan_Info_Item} from "../../../common/routes/response";
class PlanMapper extends Mapper<typeof planModel> {
constructor() {
super(planModel)
}
async list(projectId:string,page:number,size:number,keyword?:string) {
if(!projectId) {
throw Err.Project.projectNotFound
}
let sql=generateQuerySql(planModel,null,{
project_id:projectId,
...(keyword && {
name:{
exp:"%like%",
value:keyword
}
})
},"and",{
type:"asc",
field:"name"
},page*size,size)
let ret=generateCommonListData(sql,page,size)
return ret;
}
async info(planId:string) {
if(!planId) {
throw Err.Project.Plan.planNotFound
}
let mysql=getMysqlInstance()
let sql=generateLeftJoin2Sql({
model:planTableModel,
columns:keys<ICommon_Model_Plan_Table>().map(item=>item.name)
},{
model:projectIssueModel,
columns:keys<ICommon_Model_Project_Issue>().map(item=>item.name),
expression:{
id:{
model:planTableModel,
field:"ref_id"
}
},
aggregation:"issue"
},{
model:workflowNodeModel,
columns:keys<ICommon_Model_Workflow_Node>().map(item=>item.name),
expression:{
id:{
model:projectIssueModel,
field: "workflow_node_id"
}
},
aggregation:"workflow"
},{
plan_id:{
model:planTableModel,
value:planId
}
},"and",{
type:"asc",
model:planTableModel,
field:"sort"
})
let arr=await mysql.execute(sql)
let ret=this.handle(arr,null)
return ret;
}
handle(arr:ICommon_Route_Res_Plan_Info_Item[],parentId:string):ICommon_Route_Res_Plan_Info_Item[] {
let ret:ICommon_Route_Res_Plan_Info_Item[]=[]
for(let obj of arr) {
if(obj.parent_id===parentId) {
ret.push(obj)
if(obj.type===ECommon_Model_Plan_Table.ISSUE || obj.type===ECommon_Model_Plan_Table.STAGE) {
let temp=this.handle(arr,obj.id)
if(temp?.length>0) {
obj.children=temp
}
}
}
}
return ret;
}
async clearByProjects(projectIds:string[]) {
if(projectIds.length==0) {
return
}
let mysql=getMysqlInstance()
await Promise.all([
mysql.execute(generateDeleteSql(planModel,{
project_id:{
exp:"in",
value:projectIds
}
})),
mysql.execute(generateDeleteSql(planTableModel,{
project_id:{
exp:"in",
value:projectIds
}
}))
])
}
async issuePlanList(projectIssueId:string) {
if(!projectIssueId) {
throw Err.Project.ProjectIssue.projectIssueNotFound
}
let mysql=getMysqlInstance()
let sql=generateGroupLeftJoinSql({
model:planTableModel
},{
model:planModel,
columns:{
columns:keys<ICommon_Model_Plan>().map(item=>item.name),
calcColumns:[]
},
expression:{
id:{
model:planTableModel,
field:"plan_id"
}
}
},["plan_id"],{
ref_id:{
model:planTableModel,
value:projectIssueId
}
})
let arr=await mysql.execute(sql)
return arr;
}
}
export const planMapper=new PlanMapper
class PlanTableMapper extends Mapper<typeof planTableModel> {
constructor() {
super(planTableModel)
}
async clearByPlanId(planId:string) {
if(!planId) {
throw Err.Project.Plan.planNotFound
}
let mysql=getMysqlInstance()
await mysql.execute(generateDeleteSql(planTableModel,{
plan_id:planId
}))
}
async getNewSort(planItemId:string,planId:string) {
let mysql=getMysqlInstance()
let obj=await mysql.executeOne(generateQuerySql(planTableModel,["sort"],{
plan_id:planId,
parent_id:planItemId
},"and",{
type:"desc",
field:"sort"
}))
if(obj) {
return obj.sort+1
} else {
return 0
}
}
async moveUp(parentId:string,index:number,planId:string) {
let mysql=getMysqlInstance()
await mysql.execute(generateUpdateSql(planTableModel,{
sort:{
exp:"-",
value:1
}
},{
sort:{
exp:">",
value:index
},
parent_id:parentId,
plan_id:planId
}))
}
async moveDown(parentId:string,index:number,planId:string) {
let mysql=getMysqlInstance()
await mysql.execute(generateUpdateSql(planTableModel,{
sort:{
exp:"+",
value:1
}
},{
sort:{
exp:">=",
value:index
},
parent_id:parentId,
plan_id:planId
}))
}
async removeItems(ids:string[]) {
if(ids?.length==0) {
return
}
let mysql=getMysqlInstance()
await mysql.execute(generateDeleteSql(planTableModel,{
id:{
exp:"in",
value:ids
}
}))
}
async hasChild(planItemId:string) {
let mysql=getMysqlInstance()
let arr=await mysql.execute(generateQuerySql(planTableModel,["id"],{
parent_id:planItemId
}))
return arr.length>0
}
async clearDepend(planItemId:string) {
let mysql=getMysqlInstance()
await mysql.execute(generateUpdateSql(planTableModel,{
depend_id:null
},{
depend_id:planItemId
}))
}
}
export const planTableMapper=new PlanTableMapper

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,221 @@
import {Entity} from "../../common/entity/entity";
import {planModel} from "../../../common/model/plan";
import {planMapper, planTableMapper} from "../mapper/plan";
import {IServer_Common_Event_Types} from "../../common/event/types";
import {ECommon_Model_Plan_Table, planTableModel} from "../../../common/model/plan_table";
import {ICommon_Route_Res_Plan_Info_Item} from "../../../common/routes/response";
import {ProjectIssueService} from "./issue";
import {Err} from "../../../common/status/error";
import {WorkflowNodeService} from "./workflow";
export class PlanService extends Entity<typeof planModel,typeof planMapper> {
constructor() {
super(planMapper)
}
static async list(projectId:string,page:number,size:number,keyword?:string) {
let ret=await planMapper.list(projectId,page,size,keyword)
return ret;
}
override async delete(eventPublish?: keyof IServer_Common_Event_Types, ...param): Promise<void> {
await super.delete(eventPublish, ...param);
await planTableMapper.clearByPlanId(this.getId())
}
async info() {
let ret=await planMapper.info(this.getId())
return ret;
}
static async issuePlanList(projectIssueId:string) {
let ret=await planMapper.issuePlanList(projectIssueId)
return ret;
}
static async issuePlanEdit(projectIssueId:string,planList: string[]) {
let objProjectIssue=await ProjectIssueService.getItemById(projectIssueId)
if(!objProjectIssue) {
throw Err.Project.ProjectIssue.projectIssueNotFound
}
let originalList=await this.issuePlanList(projectIssueId)
let needAddList:string[]=[],needDeleteList:string[]=[]
for(let id of planList) {
let index=originalList.findIndex(item=>item.id===id)
if(index>-1) {
originalList.splice(index,1)
} else {
if(!needAddList.includes(id)) {
needAddList.push(id)
}
}
}
needDeleteList=[...originalList.map(item=>item.id)]
await Promise.all(needAddList.map(planId=>{
return (async ()=>{
let objPlan=await PlanService.getItemById(planId)
if(!objPlan) {
return
}
let sort=await PlanTableService.getNewSort(null,planId)
let obj=new PlanTableService()
obj.assignItem({
plan_id:planId,
type:ECommon_Model_Plan_Table.ISSUE,
sort:sort,
project_id:objPlan.getItem().project_id,
ref_id:projectIssueId
})
await obj.create()
let childIssueList=await objProjectIssue.childIssueList()
if(childIssueList?.length>0) {
await Promise.all(childIssueList.map((item,index)=>{
let obj1=new PlanTableService()
obj1.assignItem({
plan_id:planId,
type:ECommon_Model_Plan_Table.ISSUE,
sort:index,
parent_id:obj.getId(),
project_id:objPlan.getItem().project_id,
ref_id:item.id
})
return obj1.create()
}))
}
})()
}).concat(needDeleteList.map(planId=>{
return (async ()=>{
let arr=await PlanTableService.getItemsByExp({
plan_id:planId,
ref_id:projectIssueId
})
for(let obj of arr) {
await obj.delete()
}
})()
})))
}
}
export class PlanTableService extends Entity<typeof planTableModel,typeof planTableMapper> {
constructor() {
super(planTableMapper)
}
static async getNewSort(planItemId:string,planId:string) {
let ret=await planTableMapper.getNewSort(planItemId,planId)
return ret;
}
override async delete(eventPublish?: keyof IServer_Common_Event_Types, ...param): Promise<void> {
let objPlan=await PlanService.getItemById(this.item.plan_id)
let info=await objPlan.info()
await super.delete(eventPublish, ...param);
await planTableMapper.moveUp(this.item.parent_id,this.item.sort,this.item.plan_id)
let item=this.findItem(this.getId(),info)
if(item?.children?.length>0) {
let arr=this.getChildrenIds(item.children)
await planTableMapper.removeItems(arr)
}
await planTableMapper.clearDepend(this.getId())
}
findItem(id:string,data:ICommon_Route_Res_Plan_Info_Item[]):ICommon_Route_Res_Plan_Info_Item {
for(let obj of data) {
if(obj.id===id) {
return obj
}
if(obj.children?.length>0) {
let ret=this.findItem(id,obj.children)
if(ret) {
return ret;
}
}
}
}
getChildrenIds(data:ICommon_Route_Res_Plan_Info_Item[]) {
let ret:string[]=[]
for(let obj of data) {
ret.push(obj.id)
if(obj.children?.length>0) {
ret=ret.concat(this.getChildrenIds(obj.children))
}
}
return ret;
}
async hasParentIssue() {
if(this.getItem().parent_id) {
let objParent=await PlanTableService.getItemById(this.getItem().parent_id)
if(objParent) {
return objParent.getItem().type===ECommon_Model_Plan_Table.ISSUE
} else {
return false
}
} else {
return false;
}
}
async hasChild() {
let ret=await planTableMapper.hasChild(this.getId())
return ret;
}
async issueInfo() {
let objProjectIssue=await ProjectIssueService.getItemById(this.getItem().ref_id)
if(!objProjectIssue) {
throw Err.Project.ProjectIssue.projectIssueNotFound
}
let objWorkflow=await WorkflowNodeService.getItemById(objProjectIssue.getItem().workflow_node_id)
return {
issue:objProjectIssue.getItem(),
workflow:objWorkflow.getItem()
}
}
static async moveUp(parentId:string,index:number,planId:string) {
await planTableMapper.moveUp(parentId,index,planId)
}
static async moveDown(parentId:string,index:number,planId:string) {
await planTableMapper.moveDown(parentId, index, planId)
}
static async addChildIssue(projectIssueId:string,projectIssueChildId:string) {
let arr=await PlanTableService.getItemsByExp({
ref_id:projectIssueId
})
if(arr.length>0) {
await Promise.all(arr.map(item=>{
return (async ()=>{
let sort=await PlanTableService.getNewSort(item.getId(),item.getItem().plan_id)
let obj=new PlanTableService()
obj.assignItem({
plan_id:item.getItem().plan_id,
type:ECommon_Model_Plan_Table.ISSUE,
sort:sort,
parent_id:item.getId(),
project_id:item.getItem().project_id,
ref_id:projectIssueChildId
})
await obj.create()
})()
}))
}
}
static async removeIssue(projectIssueId:string) {
let arr = await PlanTableService.getItemsByExp({
ref_id: projectIssueId
})
await Promise.all(arr.map(item => {
return item.delete()
}))
}
async clearAllDepend() {
await planTableMapper.clearDepend(this.getId())
}
}

View File

@ -19,6 +19,7 @@ import {emitServiceEvent} from "../../common/event/event";
import {BoardService} from "./board";
import {REDIS_PROJECT} from "../../common/cache/keys/project";
import rpcUserApi from "../../user/rpc/user"
import {planMapper} from "../mapper/plan";
export class ProjectService extends Entity<typeof projectModel,typeof projectMapper> {
constructor(){
@ -44,7 +45,8 @@ export class ProjectService extends Entity<typeof projectModel,typeof projectMap
ProjectIssueService.clearByProjectIds([this.item.id]),
releaseMapper.clear(this.item.id),
issueTypeSolutionMapper.clearProjects([this.item.id]),
BoardService.clearByProjectId(this.getId())
BoardService.clearByProjectId(this.getId()),
planMapper.clearByProjects([this.item.id])
])
}
@ -177,7 +179,8 @@ export class ProjectService extends Entity<typeof projectModel,typeof projectMap
ProjectIssueService.clearByProjectIds(projectIds),
releaseMapper.clearByProjectIds(projectIds),
issueTypeSolutionMapper.clearProjects(projectIds),
BoardService.clearByProjectIds(projectIds)
BoardService.clearByProjectIds(projectIds),
planMapper.clearByProjects(projectIds)
])
}

View File

@ -1,11 +1,12 @@
import * as fs from "fs-extra";
import Application from "../../common/app/app";
import {Entity} from "../../common/entity/entity";
import {fileModel} from './../../../common/model/file';
import {ECommon_Model_File_Type, fileModel} from './../../../common/model/file';
import {fileMapper} from './../mapper/file';
import {IServer_Common_Http_Req_File} from "../../common/types/http";
import {emitServiceEvent} from "../../common/event/event";
import rpcUserApi from "../../user/rpc/user"
import {download} from "../../common/request/request";
import path = require("path");
export default class File extends Entity<typeof fileModel,typeof fileMapper> {
@ -22,15 +23,24 @@ export default class File extends Entity<typeof fileModel,typeof fileMapper> {
return null;
}
}
async upload(file:IServer_Common_Http_Req_File,meta?:string){
static async ensureDir(md5:string,fileName:string) {
let arr=(new Date).toLocaleDateString().split("/").reverse()
if(arr[2].length==1) {
arr[2]="0"+arr[2]
}
let dirPath = path.join(Application.uploadPath,arr[0]+arr[2]+arr[1])
let filePath=path.join(dirPath,file.md5)+file.fileName.substr(file.fileName.lastIndexOf("."));
let filePath=path.join(dirPath,md5)+fileName.substring(fileName.lastIndexOf("."));
let dbPath=filePath.substring(Application.uploadPath.length);
await fs.ensureDir(dirPath)
return {
filePath,
dbPath
}
}
async upload(file:IServer_Common_Http_Req_File,meta?:string){
let {dbPath,filePath}=await File.ensureDir(file.md5,file.fileName)
fs.writeFile(filePath,file.data)
this.item.path=dbPath
await super.create()
@ -40,6 +50,26 @@ export default class File extends Entity<typeof fileModel,typeof fileMapper> {
return this.item.id;
}
static async download(uri:string,userId:string):Promise<File> {
let url=new URL(uri)
let pathName=url.pathname
let fileName=pathName.substring(pathName.lastIndexOf("/")+1)
let tempPath=path.join("/tmp",fileName,String(Date.now()))
let {md5,size}=await download(uri,tempPath)
let {filePath,dbPath}=await this.ensureDir(md5,fileName)
await fs.move(tempPath,filePath)
let file = new File;
file.assignItem({
created_by_pure:userId,
size,
md5,
path:dbPath,
type:ECommon_Model_File_Type.LOCAL
})
await file.create()
return file
}
static async getPaths(ids:string[]){
let arrId=[...ids]
let ret=await fileMapper.getPaths(ids);

View File

@ -22,6 +22,7 @@ import finderApi from "../../../common/routes/finder";
import notificationApi from "../../../common/routes/notification";
import boardApi from "../../../common/routes/board";
import toolApi from "../../../common/routes/tool";
import planApi from "../../../common/routes/plan";
import {Err} from "../../../common/status/error";
import Application from "../../common/app/app";
import {EServer_Common_Http_Body_Type, IServer_Common_Http_Proxy} from "../../common/types/http";
@ -44,7 +45,7 @@ import userRpcApi from "../../user/rpc/user"
import {getRedisInstance} from "../../common/cache/redis";
import {initNotification} from "../../notification/app/app";
var apis:ICommon_HttpApi[]=[userApi,projectApi,teamApi,fileApi,issueTypeApi,workflowApi,fieldApi,issueApi,releaseApi,gatewayApi,organizationApi,wikiApi,calendarApi,meetingApi,finderApi,notificationApi,boardApi,toolApi];
var apis:ICommon_HttpApi[]=[userApi,projectApi,teamApi,fileApi,issueTypeApi,workflowApi,fieldApi,issueApi,releaseApi,gatewayApi,organizationApi,wikiApi,calendarApi,meetingApi,finderApi,notificationApi,boardApi,toolApi,planApi];
export default class GateWay extends Application {
override async config(app: Koa<Koa.DefaultState, Koa.DefaultContext>) {
let redis=getRedisInstance();

View File

@ -1,8 +1,9 @@
import gateApi from "../../../common/routes/gateway";
import Application from "../../common/app/app";
import { DComponent } from "../../common/decorate/component";
import { DHttpApi, DHttpController, DHttpReqParam, DHttpReqParamRequired } from "../../common/http/http";
import { GateWayService } from './../service/http';
import {DComponent} from "../../common/decorate/component";
import {DHttpApi, DHttpController, DHttpReqParam, DHttpReqParamRequired} from "../../common/http/http";
import {GateWayService} from './../service/http';
@DComponent
@DHttpController(gateApi)
class GatewayController {
@ -23,4 +24,11 @@ class GatewayController {
type:Application.mode
}
}
@DHttpApi(gateApi.routes.wechatAppId)
async wechatAppId():Promise<typeof gateApi.routes.wechatAppId.res> {
return {
appId:Application.privateConfig.wechat.appId
}
}
}

View File

@ -269,7 +269,7 @@ async function checkIfNeedPatch() {
}
console.log("patch end");
}
mysql.execute(generateUpdateSql(versionModel,{
await mysql.execute(generateUpdateSql(versionModel,{
version:curVersion
}))
}

View File

@ -1,4 +1,4 @@
-- MySQL dump 10.13 Distrib 8.0.31, for macos12 (x86_64)
-- MySQL dump 10.13 Distrib 8.0.34, for macos13 (x86_64)
--
-- Host: localhost Database: teamlinker_dev
-- ------------------------------------------------------
@ -653,6 +653,58 @@ CREATE TABLE `photo` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `plan`
--
DROP TABLE IF EXISTS `plan`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plan` (
`id` bigint(20) NOT NULL,
`name` varchar(65) COLLATE utf8mb4_general_ci NOT NULL,
`start_time` timestamp NOT NULL,
`organization_user_id` bigint(20) NOT NULL,
`project_id` bigint(20) NOT NULL,
`organization_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `organization_user_id` (`organization_user_id`),
KEY `project_id` (`project_id`),
KEY `organization_id` (`organization_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `plan_table`
--
DROP TABLE IF EXISTS `plan_table`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `plan_table` (
`id` bigint(20) NOT NULL,
`sort` tinyint(3) unsigned NOT NULL,
`type` tinyint(4) NOT NULL,
`name` varchar(65) COLLATE utf8mb4_general_ci DEFAULT NULL,
`ref_id` bigint(20) DEFAULT NULL,
`progress` tinyint(4) DEFAULT NULL,
`depend_id` bigint(20) DEFAULT NULL,
`delay` int(11) DEFAULT NULL,
`parent_id` bigint(20) DEFAULT NULL,
`plan_id` bigint(20) NOT NULL,
`project_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `sort` (`sort`),
KEY `ref_id` (`ref_id`),
KEY `depend_id` (`depend_id`),
KEY `parent_id` (`parent_id`),
KEY `plan_id` (`plan_id`),
KEY `project_id` (`project_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `project`
--
@ -697,6 +749,7 @@ CREATE TABLE `project_issue` (
`reporter_id` bigint(20) DEFAULT NULL,
`workflow_node_id` bigint(20) NOT NULL,
`unique_id` int(10) unsigned DEFAULT NULL,
`man_day` int(11) DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `project` (`project_id`),
@ -1092,8 +1145,12 @@ CREATE TABLE `user` (
`photo` bigint(20) DEFAULT NULL,
`role` tinyint(4) NOT NULL DEFAULT '0',
`count` int(11) DEFAULT '0',
`from_type` tinyint(4) DEFAULT '0',
`from_id` varchar(65) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`)
UNIQUE KEY `username_UNIQUE` (`username`),
UNIQUE KEY `from_id_UNIQUE` (`from_id`),
KEY `from_id` (`from_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
@ -1296,4 +1353,4 @@ CREATE TABLE `workflow_node_field_type_config` (
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2023-09-24 21:24:18
-- Dump completed on 2023-11-06 16:35:44

View File

@ -0,0 +1,45 @@
CREATE TABLE `teamlinker`.`plan` (
`id` bigint NOT NULL,
`name` varchar(65) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`start_time` timestamp NOT NULL,
`organization_user_id` bigint NOT NULL,
`project_id` bigint NOT NULL,
`organization_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `id_UNIQUE`(`id` ASC) USING BTREE,
INDEX `organization_user_id`(`organization_user_id` ASC) USING BTREE,
INDEX `project_id`(`project_id` ASC) USING BTREE,
INDEX `organization_id`(`organization_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `teamlinker`.`plan_table` (
`id` bigint NOT NULL,
`sort` tinyint UNSIGNED NOT NULL,
`type` tinyint NOT NULL,
`name` varchar(65) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`ref_id` bigint NULL DEFAULT NULL,
`progress` tinyint NULL DEFAULT NULL,
`depend_id` bigint NULL DEFAULT NULL,
`delay` int NULL DEFAULT NULL,
`parent_id` bigint NULL DEFAULT NULL,
`plan_id` bigint NOT NULL,
`project_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `id_UNIQUE`(`id` ASC) USING BTREE,
INDEX `sort`(`sort` ASC) USING BTREE,
INDEX `ref_id`(`ref_id` ASC) USING BTREE,
INDEX `depend_id`(`depend_id` ASC) USING BTREE,
INDEX `parent_id`(`parent_id` ASC) USING BTREE,
INDEX `plan_id`(`plan_id` ASC) USING BTREE,
INDEX `project_id`(`project_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
ALTER TABLE `teamlinker`.`project_issue` ADD COLUMN `man_day` int NULL DEFAULT 1 AFTER `unique_id`;
ALTER TABLE `teamlinker`.`user` ADD COLUMN `from_type` tinyint NULL DEFAULT 0 AFTER `count`;
ALTER TABLE `teamlinker`.`user` ADD COLUMN `from_id` varchar(65) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL AFTER `from_type`;
ALTER TABLE `teamlinker`.`user` ADD UNIQUE INDEX `from_id_UNIQUE`(`from_id` ASC) USING BTREE;
ALTER TABLE `teamlinker`.`user` ADD INDEX `from_id`(`from_id` ASC) USING BTREE;

View File

@ -0,0 +1,19 @@
import * as path from "path";
import * as Importer from "@pivvenit/mysql-import";
import {getConfigInstance} from "../common/config/config";
export default async function() {
let config=getConfigInstance()
let sqlPath=path.join(__dirname,"0.1.1.sql")
let importer = new Importer({
host:config.mysqlInfo.url,
user:config.mysqlInfo.username,
password:config.mysqlInfo.password,
database:config.mysqlInfo.database,
port:config.mysqlInfo.port,
charsetNumber:224
})
console.log("0.1.1 start")
await importer.import(sqlPath)
console.log("0.1.1 end")
}

View File

@ -1,8 +1,11 @@
import func_0_1_1 from "./0.1.1"
export const patchList:{
version:string,
func:any
}[]=[
{
version:"0.1.1",
func:func_0_1_1
}
]

View File

@ -14,12 +14,16 @@
},
"port":14000,
"jwt": "teamlinker",
"version":"0.1.0",
"version":"0.1.1",
"mq": "amqp://127.0.0.1",
"mail":{
"host": "smtp.exmail.qq.com",
"port": 465,
"user":"notify@team-linker.com",
"pass":"gjH9WwP9JEF42QBu"
},
"wechat":{
"appId":"wxe8213f696c7ac4ea",
"appSecret":"7a6f6dab82a82a764016cb2dc5bb4e12"
}
}

View File

@ -1,4 +1,4 @@
import {ECommon_User_Type} from "../../../common/model/user";
import {ECommon_User_From_Type, ECommon_User_Type} from "../../../common/model/user";
import userApi from "../../../common/routes/user";
import {Err} from "../../../common/status/error";
import {DComponent} from '../../common/decorate/component';
@ -21,6 +21,7 @@ import rpcFileApi from "../../file/rpc/file"
import {REDIS_USER} from "../../common/cache/keys/user";
import rpcFinderApi from "../../finder/rpc/finder";
import * as i18next from "i18next";
import File from "../../file/service/file";
@DComponent
@DHttpController(userApi)
@ -205,9 +206,9 @@ class UserController {
}
@DHttpApi(userApi.routes.confirmRegister)
async confirmRegister(@DHttpReqParamRequired("username") username:string,@DHttpReqParamRequired("code") code:string):Promise<typeof userApi.routes.confirmRegister.res> {
async confirmRegister(@DHttpReqParamRequired("username") username:string,@DHttpReqParamRequired("code") code:string,@DHttpReqParamRequired("openId") openId:string):Promise<typeof userApi.routes.confirmRegister.res> {
if(Application.mode===ECommon_Application_Mode.ONLINE) {
await UserService.conformRegister(username,code)
await UserService.conformRegister(username,code,openId)
return
} else {
throw Err.Common.interfaceForbidden
@ -290,4 +291,75 @@ class UserController {
return
}
@DHttpApi(userApi.routes.wechatCode)
async wechatCode(@DHttpReqParamRequired("code") code:string,@DHttpUser user:IUserSession):Promise<typeof userApi.routes.wechatCode.res> {
let ret=await UserService.handleWechatCode(code)
return ret;
}
@DHttpApi(userApi.routes.wechatLogin)
async wechatLogin(@DHttpReqParamRequired("openId") openId:string,@DHttpReqParamRequired("lang") lang:string,@DHttpContext ctx:HttpContext):Promise<typeof userApi.routes.wechatLogin.res> {
let user=await UserService.getItemByExp({
from_id:openId
})
if(user) {
if(!user.getItem().active) {
throw Err.User.accessDenied
}
let token=await user.startSession(lang)
if(user.getItem().count===0) {
rpcFinderApi.createFolder(user.getId(),i18next.getFixedT(lang)("backend.newFolder"),null)
}
user.assignItem({
count:user.getItem().count+1
})
let ret=await user.update()
delete ret.password
ctx.setHeader("token",token)
return ret
} else {
return;
}
}
@DHttpApi(userApi.routes.bindWechat)
async bindWechat(@DHttpReqParamRequired("openId") openId:string,@DHttpReqParamRequired("username") username:string,@DHttpReqParamRequired("password") password:string,@DHttpContext ctx:HttpContext):Promise<typeof userApi.routes.bindWechat.res> {
let objUser=await UserService.getItemByExp({
username,
password
})
if(!objUser) {
throw Err.User.userPasswordWrong
}
let img:string
if(openId) {
let objRedis=REDIS_USER.wechatOpenId(openId)
let value=await objRedis.get()
if(!value) {
throw Err.User.userCacheExpired
}
let user=await UserService.getItemByExp({
from_id:openId
})
if(user) {
throw Err.User.userExists
}
img=value.img
}
objUser.assignItem({
from_type:objUser.getItem().from_type | ECommon_User_From_Type.WECHAT,
from_id:openId
})
await objUser.update()
if(img && !objUser.getItem().photo) {
let file=await File.download(img,objUser.getId())
objUser.assignItem({
photo:file.getId()
})
await objUser.update()
}
return;
}
}

View File

@ -6,7 +6,7 @@ import {getConfigInstance} from '../../common/config/config';
import {Entity} from "../../common/entity/entity";
import {userMapper, userSettingMapper} from '../mapper/user';
import rpcFileApi from "../../file/rpc/file";
import {ECommon_User_Type, ICommon_Model_User, userModel} from './../../../common/model/user';
import {ECommon_User_From_Type, ECommon_User_Type, ICommon_Model_User, userModel} from './../../../common/model/user';
import rpcAuthApi from "../../auth/rpc/auth";
import {ICommon_Model_Team} from "../../../common/model/team";
import {ECommon_Model_Role_Reserved} from "../../../common/model/role";
@ -19,6 +19,10 @@ import rpcFinderApi from "../../finder/rpc/finder"
import rpcNotificationApi from "../../notification/rpc/notification"
import {PhotoService, StickyNoteService} from "./tool";
import {REDIS_ORGANIZATION} from "../../common/cache/keys/organization";
import {request} from "../../common/request/request"
import Application from "../../common/app/app";
import File from "../../file/service/file";
export class UserService extends Entity<typeof userModel,typeof userMapper> {
constructor(){
@ -83,7 +87,7 @@ export class UserService extends Entity<typeof userModel,typeof userMapper> {
mail.send(username,"Teamlinker Verification Code","code: "+code)
}
static async conformRegister(username:string,code:string) {
static async conformRegister(username:string,code:string,openId?:string) {
let obj=await userMapper.getUserByName(username)
if(obj) {
throw Err.User.userExists
@ -100,11 +104,37 @@ export class UserService extends Entity<typeof userModel,typeof userMapper> {
throw Err.User.codeNotMatch
}
let objUser=new UserService()
let img:string
if(openId) {
let objRedis=REDIS_USER.wechatOpenId(openId)
let value=await objRedis.get()
if(!value) {
throw Err.User.userCacheExpired
}
let user=await UserService.getItemByExp({
from_id:openId
})
if(user) {
throw Err.User.userExists
}
img=value.img
}
objUser.assignItem({
username,
password:content.password
password:content.password,
...(openId && {
from_type:ECommon_User_From_Type.WECHAT,
from_id:openId
})
})
await objUser.create()
let ret=await objUser.create()
if(img) {
let file=await File.download(img,ret.id)
objUser.assignItem({
photo:file.getId()
})
await objUser.update()
}
objRedis.del()
}
@ -368,6 +398,27 @@ export class UserService extends Entity<typeof userModel,typeof userMapper> {
let ret=await userMapper.getDeletedUser()
return ret;
}
static async handleWechatCode(code:string) {
let res=await request(`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${Application.privateConfig.wechat.appId}&secret=${Application.privateConfig.wechat.appSecret}&code=${code}&grant_type=authorization_code`)
if(res.statusCode==200) {
let obj=JSON.parse(res.body)
if(obj.openid && obj.access_token) {
let res=await request(`https://api.weixin.qq.com/sns/userinfo?access_token=${obj.access_token}&openid=${obj.openid}`)
if(res.statusCode==200) {
let obj1=JSON.parse(res.body)
let objKey=REDIS_USER.wechatOpenId(obj.openid)
await objKey.set({
img:obj1.headimgurl
})
return {
openId:obj.openid
}
}
}
}
}
}