This commit is contained in:
sx1989827 2022-12-19 22:08:30 +08:00
parent f70342db71
commit a0f77c17d9
504 changed files with 8957 additions and 33609 deletions

24
code/client/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
code/client/README.md Normal file
View File

@ -0,0 +1,16 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

View File

@ -2,9 +2,8 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- <link rel="icon" href="/favicon.ico" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>teamlinker</title>
<title>TeamLinker</title>
</head>
<body>
<div id="app"></div>

29
code/client/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "untitled5",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"pinia": "^2.0.28",
"uuid": "^9.0.0",
"vue": "^3.2.45",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@arco-design/web-vue": "^2.40.0",
"@rollup/plugin-typescript": "^10.0.1",
"@types/uuid": "^9.0.0",
"@vitejs/plugin-vue": "^4.0.0",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.4.1",
"typescript": "^4.6.4",
"vite": "^4.0.0",
"vite-plugin-typescript": "^1.0.4",
"vue-tsc": "^1.0.12"
}
}

11
code/client/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import enUS from '@arco-design/web-vue/es/locale/lang/en-us'</script>
<template>
<a-config-provider :locale="enUS">
<router-view></router-view>
</a-config-provider>
</template>
<style scoped>
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -0,0 +1,81 @@
import {renderComponent} from "../../../../teamOS/common/util/component";
import DialogView from "./dialogView.vue";
import {AppContext, Component, inject, ref} from "vue";
export function onDialogOk(func:()=>void){
const events:any=inject("dialogEvents");
events.onOk=func;
}
export function onDialogClose(func:()=>void) {
const events:any=inject("dialogEvents");
events.onClose=func;
}
export class Dialog {
static open(el:HTMLElement,appContext: AppContext,title:string,component:Component,props?:object) {
return new Promise((resolve,reject)=>{
let ele=document.createElement("div")
const events:{
onOk:()=>any,
onClose:()=>void
}={
onOk:null,
onClose:null
}
const loading=ref(false);
let destroyFunc=renderComponent(ele,DialogView,appContext,{
props,
component,
title,
events,
onOk,
onClose,
loading
});
el.appendChild(ele);
async function onOk(){
if(events.onOk) {
loading.value=true;
let ret=await events.onOk();
loading.value=false
if(ret!==false) {
destroyFunc();
ele.parentNode.removeChild(ele);
resolve(ret)
}
} else {
destroyFunc();
ele.parentNode.removeChild(ele);
resolve(null);
}
}
function onClose(){
events.onClose?.();
destroyFunc();
ele.parentNode.removeChild(ele);
resolve(null)
}
})
}
static confirm(el:HTMLElement,appContext: AppContext,content:string) {
return new Promise((resolve,reject)=>{
let ele=document.createElement("div")
let destroyFunc=renderComponent(ele,DialogView,appContext,{
onOk,
onClose,
title:content
});
el.appendChild(ele);
async function onOk(){
destroyFunc();
ele.parentNode.removeChild(ele);
resolve(true)
}
function onClose(){
destroyFunc();
ele.parentNode.removeChild(ele);
resolve(false)
}
})
}
}

View File

@ -0,0 +1,46 @@
<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)">
<div style="background-color: white;width: 60%;height:auto;max-height: 80%;border-radius: 5px;">
<div style="height: 35px;line-height: 35px;width: 100%;text-align: center;color: rgb(93,93,93);border-bottom: 1px solid gainsboro">
<b>{{component?title:"Alert"}}</b>
</div>
<div style="width: 95%;overflow: auto;height: calc(100% - 80px);padding:10px">
<component :is="component" v-bind="props.props" v-if="component"></component>
<div v-else style="min-height: 60px;padding: 10px;font-size: medium">
{{title}}
</div>
</div>
<div style="height: 45px;width: 100%;display: flex;justify-content: flex-end;border-top: 1px solid gainsboro">
<a-space size="medium">
<a-button type="primary" @click="onOk" size="small" html-type="submit" :loading="props.loading?props.loading.value:false">{{component?"Ok":"Yes"}}</a-button>
<a-button type="outline" style="margin-right: 10px" size="small" @click="onClose">{{component?"Close":"No"}}</a-button>
</a-space>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {provide} from "vue";
const props=defineProps<{
title:string,
component?:any,
props?:object,
events?:{
onOk:()=>any,
onClose:()=>void
},
onOk:()=>void,
onClose:()=>void,
loading?:any
}>()
if(props.events) {
provide("dialogEvents",props.events);
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,53 @@
<template>
<a-upload :custom-request="onUpload" :show-file-list="false" :accept="types">
<template #upload-button>
<div :class="`arco-upload-list-item`">
<div class="arco-upload-list-picture custom-upload-avatar" v-if="uri">
<img :src="uri" />
<div class="arco-upload-list-picture-mask">
<IconEdit />
</div>
</div>
<div class="arco-upload-picture-card" v-else>
<div class="arco-upload-picture-card-text">
<IconPlus />
<div style="margin-top: 10px; font-weight: 600">Upload</div>
</div>
</div>
</div>
</template>
</a-upload>
</template>
<script setup lang="ts">
import {ref, watchEffect} from "vue";
import {apiFile} from "../request/request";
const props=defineProps<{
defaultUri?:string,
types?:string
}>()
const emit=defineEmits<{
(e:'upload',id:string):void
}>()
let uri=ref("");
watchEffect(()=>{
uri.value=props.defaultUri;
})
const onUpload=async (option)=>{
const {onProgress, onError, onSuccess, fileItem, name} = option
uri.value=URL.createObjectURL(fileItem.file);
let ret=await apiFile.upload({
file:fileItem.file
})
if(ret?.code==0) {
onProgress(100)
emit("upload",ret.data.id);
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,162 @@
import {ECommon_HttpApi_Method, ICommon_Http_Route_List} from "../../../../../common/routes/types";
import field from "../../../../../common/routes/field"
import file from "../../../../../common/routes/file"
import issue from "../../../../../common/routes/issue"
import issueType from "../../../../../common/routes/issueType"
import organization from "../../../../../common/routes/organization"
import project from "../../../../../common/routes/project"
import release from "../../../../../common/routes/release"
import team from "../../../../../common/routes/team"
import workflow from "../../../../../common/routes/workflow"
import user from "../../../../../common/routes/user"
import {Ref} from "vue";
export type DCSType<T>={
[key in keyof T]:key extends "created_by"|"modified_by"|"assigner_id"|"reporter_id"?{
id:string,
username:string,
photo?:string,
nickname?:string
}:key extends "created_time"|"modified_time"?string:T[key] extends object?DCSType<T[key]>:T[key]
}
let g_funcError:()=>void
let g_authError:()=>void
let g_responseError:(response:Response)=>void
export function onRequestError(func:()=>void) {
if(func) {
g_funcError=func
}
}
export function onAuthError(func:()=>void) {
if(func) {
g_authError=func;
}
}
export function onResponseError(func:()=>void) {
if(func) {
g_responseError=func;
}
}
export function generatorApi<T extends ICommon_Http_Route_List>(api:{
baseUrl:string,
routes:T
}):{
[name in keyof T]:keyof T[name]["req"] extends ""?(loading?:Ref<boolean>)=>Promise<{
code:number,
msg?:string,
data:DCSType<T[name]["res"]>
}>:(param:T[name]["req"],loading?:Ref<boolean>)=>Promise<{
code:number,
msg?:string,
data:DCSType<T[name]["res"]>
}>
} {
let baseUrl=api.baseUrl
let map:any={}
for(let name in api.routes) {
let route=api.routes[name];
map[name]=async function(param?:any,loading?:Ref<boolean>):Promise<any> {
if(loading) {
loading.value=true;
}
let objBody:URLSearchParams|FormData
let uri="/api"+baseUrl+route.path
if(param) {
for(let key in param) {
let obj=param[key]
if(obj===null || obj==undefined) {
delete param[key]
}
}
if(route.method==ECommon_HttpApi_Method.POST || route.method==ECommon_HttpApi_Method.PUT) {
let isFormData=false
for(let key in param) {
if(param[key] instanceof File) {
isFormData=true;
break;
}
}
if(isFormData) {
objBody=new FormData();
for(let key in param) {
let obj=param[key]
if(obj instanceof File) {
objBody.append(key,obj,obj.name)
} else {
objBody.append(key,obj);
}
}
} else {
objBody = new URLSearchParams(param)
}
} else {
uri+="?"+new URLSearchParams(param)
}
}
try {
if(!route.ignoreValidate && !sessionStorage.getItem("userToken")) {
if(g_authError) {
g_authError();
}
if(loading) {
loading.value=false;
}
return ;
}
const response=await fetch(uri,{
method:route.method,
mode:"cors",
cache:"no-cache",
credentials:"include",
redirect:"follow",
headers:{
...(!route.ignoreValidate && {
"Authorization":"Bearer "+sessionStorage.getItem("userToken")
}),
},
...(objBody && {
body:objBody
})
})
if(response.headers.get("token")) {
sessionStorage.setItem("userToken",response.headers.get("token"))
}
if(!response.ok) {
if(g_responseError) {
g_responseError(response);
if(loading) {
loading.value=false
}
return
}
}
let ret=await response.json();
if(loading) {
loading.value=false
}
return ret;
} catch (e) {
if(g_funcError) {
g_funcError()
return null;
}
}
};
}
return map;
}
export const apiUser=generatorApi(user)
export const apiField=generatorApi(field)
export const apiFile=generatorApi(file)
export const apiTeam=generatorApi(team)
export const apiProject=generatorApi(project)
export const apiWorkflow=generatorApi(workflow)
export const apiOrganization=generatorApi(organization)
export const apiRelease=generatorApi(release)
export const apiIssue=generatorApi(issue)
export const apiIssueType=generatorApi(issueType)

View File

@ -0,0 +1,16 @@
import {_GettersTree, defineStore, DefineStoreOptions, StateTree, Store} from "pinia";
import {inject, provide} from "vue";
export function useStore<Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A = {}>(id: Id, options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>): Store<Id, S, G, A> {
let value=inject("store:"+id,null)
if(!value) {
let store=defineStore(id,options);
let instance=store()
provide("store:"+id,instance)
return instance;
} else {
return value
}
}

View File

@ -0,0 +1,8 @@
<template>
</template>
<script setup lang="ts"></script>
<style scoped>
</style>

View File

@ -0,0 +1,70 @@
<template>
<a-form :model="data.form" style="width: 80%;margin-top: 20px" @submitSuccess="onSubmit">
<a-form-item field="name" label="name" required>
<a-input v-model="data.form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="data.form.description" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="photo" label="logo">
<Upload types=".png,.jpg,.jpeg,.gif,.bmp,.svg" :default-uri="data.form.photo" @upload="onUpload"></Upload>
</a-form-item>
<a-form-item>
<a-button html-type="submit" :loading="loading">Submit</a-button>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {apiOrganization} from "../../../../common/request/request";
import {onBeforeMount, reactive, ref} from "vue";
import Upload from "../../../../common/component/upload.vue";
import {Message} from "@arco-design/web-vue";
import {useDesktopStore} from "../../../desktop/store/desktop";
const storeDesktop=useDesktopStore()
const data=reactive({
form:{
name:"",
description:"",
photo:""
}
})
const uploadUriId=ref("")
const onUpload=(id:string)=> {
uploadUriId.value=id
}
const loading=ref(false)
const onSubmit=async ()=>{
let body={
organizationId:sessionStorage.getItem("organizationId"),
name:data.form.name,
description: data.form.description,
...(uploadUriId.value && {
photo:uploadUriId.value
})
}
let ret=await apiOrganization.update(body,loading)
if(ret?.code==0) {
Message.success("update success")
storeDesktop.$update();
} else {
Message.error(ret?.msg??"unknown error")
}
}
onBeforeMount(async ()=>{
let ret=await apiOrganization.info({
organizationId:sessionStorage.getItem("organizationId")
})
if(ret?.code==0) {
data.form=ret.data
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,8 @@
<template>
</template>
<script setup lang="ts"></script>
<style scoped>
</style>

View File

@ -0,0 +1,99 @@
<template>
<div>
<a-form :model="form" style="width: 80%" ref="eleForm">
<a-form-item field="name" label="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="form.description" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="permissions" label="permissions">
<a-space :wrap="true">
<a-tag v-for="(item,index) in form.permissions" bordered color="arcoblue" checkable :checked="item.checked" @check="item.checked=!item.checked">{{item.name}}</a-tag>
</a-space>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import {onDialogOk} from "../../../../common/component/dialog/dialog";
import {getAllPermissions, Permission_Base, Permission_Types} from "../../../../../../../common/permission/permission";
import {reactive, ref} from "vue";
import {apiOrganization} from "../../../../common/request/request";
import {Message} from "@arco-design/web-vue";
const eleForm=ref(null)
const props=defineProps<{
type:"edit"|"add"
item?:{
id:string
name:string,
description:string,
permissions:Permission_Base[]
}
}>()
const form=reactive(props.type=="edit"?{
name:props.item.name,
description:props.item.description,
permissions:getAllPermissions(Permission_Types.Organization).filter(item=>{
if(item.name!=="ADMIN") {
return true;
}
}).map(item=>{
return {
...item,
checked:props.item.permissions.map(item=>item.name).includes(item.name)
}
})
}:{
name:"",
description:"",
permissions:getAllPermissions(Permission_Types.Organization).filter(item=>{
if(item.name!=="ADMIN") {
return true;
}
}).map(item=>{
return {
...item,
checked:false
}
})
});
onDialogOk(async ()=>{
let ret=await eleForm.value.validate()
if(ret) {
return false;
}
let value=form.permissions.filter(item=>{
if(item.checked) {
return true
}
}).reduce((previousValue, currentValue, currentIndex, array)=>{
return previousValue | currentValue.value
},0)
let res=await (props.type=="edit"?apiOrganization.editRole({
roleId:props.item.id,
name:form.name,
description:form.description,
value:value
}):apiOrganization.addRole({
organizationId:sessionStorage.getItem("organizationId"),
name:form.name,
description:form.description,
value:value
}))
if(res?.code==0) {
Message.success("update success")
return true
} else {
Message.error(res.msg);
return false
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,102 @@
<template>
<div>
<a-form :model="form" style="width: 80%" ref="eleForm">
<a-form-item field="name" label="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="form.description" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="permissions" label="permissions">
<a-space :wrap="true">
<a-tag v-for="(item,index) in form.permissions" bordered color="arcoblue" checkable :checked="item.checked" @check="item.checked=!item.checked">{{item.name}}</a-tag>
</a-space>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import {onDialogOk} from "../../../../common/component/dialog/dialog";
import {getAllPermissions, Permission_Base, Permission_Types} from "../../../../../../../common/permission/permission";
import {reactive, ref} from "vue";
import {apiProject} from "../../../../common/request/request";
import {Message} from "@arco-design/web-vue";
const eleForm=ref(null)
const props=defineProps<{
projectId?:string
type:"edit"|"add"
item?:{
id:string
name:string,
description:string,
permissions:Permission_Base[]
}
}>()
const form=reactive(props.type=="edit"?{
name:props.item.name,
description:props.item.description,
permissions:getAllPermissions(Permission_Types.Project).filter(item=>{
if(item.name!=="ADMIN") {
return true;
}
}).map(item=>{
return {
...item,
checked:props.item.permissions.map(item=>item.name).includes(item.name)
}
})
}:{
name:"",
description:"",
permissions:getAllPermissions(Permission_Types.Project).filter(item=>{
if(item.name!=="ADMIN") {
return true;
}
}).map(item=>{
return {
...item,
checked:false
}
})
});
onDialogOk(async ()=>{
let ret=await eleForm.value.validate()
if(ret) {
return false;
}
let value=form.permissions.filter(item=>{
if(item.checked) {
return true
}
}).reduce((previousValue, currentValue, currentIndex, array)=>{
return previousValue | currentValue.value
},0)
let res=await (props.type=="edit"?apiProject.editRole({
roleId:props.item.id,
name:form.name,
description:form.description,
value:value
}):apiProject.addRole({
...(props.projectId && {
projectId:props.projectId
}),
name:form.name,
description:form.description,
value:value
}))
if(res?.code==0) {
Message.success("update success")
return true
} else {
Message.error(res.msg);
return false
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,102 @@
<template>
<div>
<a-form :model="form" style="width: 80%" ref="eleForm">
<a-form-item field="name" label="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="form.description" allow-clear></a-textarea>
</a-form-item>
<a-form-item field="permissions" label="permissions">
<a-space :wrap="true">
<a-tag v-for="(item,index) in form.permissions" bordered color="arcoblue" checkable :checked="item.checked" @check="item.checked=!item.checked">{{item.name}}</a-tag>
</a-space>
</a-form-item>
</a-form>
</div>
</template>
<script setup lang="ts">
import {onDialogOk} from "../../../../common/component/dialog/dialog";
import {getAllPermissions, Permission_Base, Permission_Types} from "../../../../../../../common/permission/permission";
import {reactive, ref} from "vue";
import {apiTeam} from "../../../../common/request/request";
import {Message} from "@arco-design/web-vue";
const eleForm=ref(null)
const props=defineProps<{
teamId?:string
type:"edit"|"add"
item?:{
id:string
name:string,
description:string,
permissions:Permission_Base[]
}
}>()
const form=reactive(props.type=="edit"?{
name:props.item.name,
description:props.item.description,
permissions:getAllPermissions(Permission_Types.Team).filter(item=>{
if(item.name!=="ADMIN") {
return true;
}
}).map(item=>{
return {
...item,
checked:props.item.permissions.map(item=>item.name).includes(item.name)
}
})
}:{
name:"",
description:"",
permissions:getAllPermissions(Permission_Types.Team).filter(item=>{
if(item.name!=="ADMIN") {
return true;
}
}).map(item=>{
return {
...item,
checked:false
}
})
});
onDialogOk(async ()=>{
let ret=await eleForm.value.validate()
if(ret) {
return false;
}
let value=form.permissions.filter(item=>{
if(item.checked) {
return true
}
}).reduce((previousValue, currentValue, currentIndex, array)=>{
return previousValue | currentValue.value
},0)
let res=await (props.type=="edit"?apiTeam.editRole({
roleId:props.item.id,
name:form.name,
description:form.description,
value:value
}):apiTeam.addRole({
...(props.teamId && {
teamId:props.teamId
}),
name:form.name,
description:form.description,
value:value
}))
if(res?.code==0) {
Message.success("update success")
return true
} else {
Message.error(res.msg);
return false
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,140 @@
<template>
<div ref="root">
<a-button @click="onAdd" type="primary" style="margin-bottom: 10px">Add</a-button>
<a-table :columns="columns" :data="data" :pagination="false">
<template #description="{record}">
{{record.description}}
</template>
<template #permission="{record}">
<a-space wrap>
<a-tag v-for="item in record.permissions.map(item=>item.name)">{{item}}</a-tag>
</a-space>
</template>
<template #reserved="{record}">
<icon-check v-if="record.reserved" style="color: green"></icon-check>
<icon-close v-else style="color: red"></icon-close>
</template>
<template #operation="{record}">
<template v-if="!record.reserved">
<a-space wrap>
<a-button type="primary" size="small" @click="onEdit(record)">manage</a-button>
<a-button status="danger" size="small" @click="onDelete(record)">delete</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {getCurrentInstance, markRaw, onBeforeMount, ref} from "vue";
import {apiOrganization} from "../../../../common/request/request";
import {Dialog} from "../../../../common/component/dialog/dialog";
import EditOrganizationRole from "./editOrganizationRole.vue";
import {Permission_Base, Permission_Types} from "../../../../../../../common/permission/permission";
import {Message} from "@arco-design/web-vue";
type Item={
id:string,
name:string,
reserved:number,
description:string,
permissions:Permission_Base[]
}
const columns=[
{
title:"name",
dataIndex:"name"
},
{
title:"description",
slotName:"description"
},
{
title:"permission",
slotName:"permission"
},
{
title:"reserved",
slotName: "reserved"
},
{
title:"operation",
slotName: "operation"
}
]
const data=ref<Item[]>([])
const root=ref(null)
const appContext=getCurrentInstance().appContext
const onAdd=async ()=>{
let ret=await Dialog.open(root.value,appContext,"Add Role",markRaw(EditOrganizationRole),{
type:"add"
})
if(ret) {
init()
}
}
const onEdit=async (item:Item) =>{
let ret=await Dialog.open(root.value,appContext,"Edit Role",markRaw(EditOrganizationRole),{
type:"edit",
item:{
name:item.name,
id:item.id,
description:item.description,
permissions:item.permissions
}
})
if(ret) {
init()
}
}
const onDelete=async (item:Item)=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you want to delete this role?")
if(ret) {
let res=await apiOrganization.removeRole({
roleId:item.id
})
if(res?.code==0) {
Message.success("remove success")
init()
} else {
Message.error(res.msg);
}
}
}
const init=async ()=>{
let ret=await apiOrganization.listRole({
organizationId:sessionStorage.getItem("organizationId")
})
if(ret?.code==0) {
let value=ret.data;
let arr:Item[]=[]
arr.push({
name:value.admin.name,
id:value.admin.id,
reserved:value.admin.reserved,
description:value.admin.description,
permissions:[{
name:Permission_Types.Organization.ADMIN.name,
description:Permission_Types.Organization.ADMIN.description,
value:Permission_Types.Organization.ADMIN.value
}]
})
for(let obj of value.users) {
arr.push({
name:obj.name,
id:obj.id,
reserved:obj.reserved,
description:obj.description,
permissions:obj.permissions as Permission_Base[]
})
}
data.value=arr
}
}
onBeforeMount(init)
</script>
<style scoped>
</style>

View File

@ -0,0 +1,83 @@
<template>
<div ref="root">
<a-input-search style="width: 300px;margin-bottom: 10px" placeholder="please type project name" @search="search" search-button></a-input-search>
<a-table :columns="columns" :data="data" @pageChange="onPageChange" :pagination="pagination">
<template #operation="{record}">
<a-button type="primary" size="small" @click="onEdit(record)">manage role</a-button>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue";
import {ICommon_Model_Project} from "../../../../../../../common/model/project";
import {apiProject, DCSType} from "../../../../common/request/request";
import {getCurrentNavigator} from "../../../../../teamOS/common/component/navigator/navigator";
const columns=[
{
title:"name",
dataIndex:"name"
},
{
title:"description",
dataIndex:"description"
},
{
title:"keyword",
dataIndex:"keyword"
},
{
title:"operation",
slotName: "operation"
}
]
let data=ref<DCSType<ICommon_Model_Project[]>>([])
const root=ref(null)
const pagination=reactive({
total:0,
current:1,
pageSize:10
})
const keyword=ref("");
const search=async (key:string)=>{
keyword.value=key;
let ret=await apiProject.list({
...(key && {
keyword:key
}),
page:0,
size:10
})
if(ret?.code==0) {
data.value=ret.data.data
pagination.total=ret.data.count;
pagination.current=ret.data.page+1
}
}
let navigator=getCurrentNavigator();
const onEdit=async (item:ICommon_Model_Project)=>{
navigator.push("projectGlobalRole",{
projectId:item.id
},"Project Role")
}
const onPageChange=async (page:number)=>{
let ret=await apiProject.list({
...(keyword.value && {
keyword:keyword.value
}),
page:page-1,
size:10
})
if(ret?.code==0) {
data.value=ret.data.data
pagination.total=ret.data.count;
pagination.current=ret.data.page+1
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,155 @@
<template>
<div ref="root">
<a-button @click="onAdd" type="primary" style="margin-bottom: 10px">Add</a-button>
<a-table :columns="columns" :data="data" :pagination="false">
<template #description="{record}">
{{record.description}}
</template>
<template #permission="{record}">
<a-space wrap>
<a-tag v-for="item in record.permissions.map(item=>item.name)">{{item}}</a-tag>
</a-space>
</template>
<template #reserved="{record}">
<icon-check v-if="record.reserved" style="color: green"></icon-check>
<icon-close v-else style="color: red"></icon-close>
</template>
<template #operation="{record}">
<template v-if="!record.reserved">
<a-space wrap>
<a-button type="primary" size="small" @click="onEdit(record)">manage</a-button>
<a-button status="danger" size="small" @click="onDelete(record)">delete</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {getCurrentInstance, markRaw, onBeforeMount, ref} from "vue";
import {apiProject} from "../../../../common/request/request";
import {Dialog} from "../../../../common/component/dialog/dialog";
import {Permission_Base, Permission_Types} from "../../../../../../../common/permission/permission";
import {Message} from "@arco-design/web-vue";
import EditProjectRole from "./editProjectRole.vue";
const props=defineProps<{
projectId?:string
}>()
type Item={
id:string,
name:string,
reserved:number,
description:string,
permissions:Permission_Base[]
}
const columns=[
{
title:"name",
dataIndex:"name"
},
{
title:"description",
slotName:"description"
},
{
title:"permission",
slotName:"permission"
},
{
title:"reserved",
slotName: "reserved"
},
{
title:"operation",
slotName: "operation"
}
]
const data=ref<Item[]>([])
const root=ref(null)
const appContext=getCurrentInstance().appContext
const onAdd=async ()=>{
let ret=await Dialog.open(root.value,appContext,"Add Role",markRaw(EditProjectRole),{
type:"add",
...(props.projectId && {
projectId:props.projectId
})
})
if(ret) {
init()
}
}
const onEdit=async (item:Item) =>{
let ret=await Dialog.open(root.value,appContext,"Edit Role",markRaw(EditProjectRole),{
type:"edit",
item:{
name:item.name,
id:item.id,
description:item.description,
permissions:item.permissions
}
})
if(ret) {
init()
}
}
const onDelete=async (item:Item)=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you want to delete this role?")
if(ret) {
let res=await apiProject.removeRole({
roleId:item.id
})
if(res?.code==0) {
Message.success("remove success")
init()
} else {
Message.error(res.msg);
}
}
}
const init=async ()=>{
let ret=await apiProject.listRole({
...(props.projectId && {
projectId:props.projectId
})
})
if(ret?.code==0) {
let value=ret.data;
let arr:Item[]=[]
arr.push({
name:value.admin.name,
id:value.admin.id,
reserved:value.admin.reserved,
description:value.admin.description,
permissions:[{
name:Permission_Types.Project.ADMIN.name,
description:Permission_Types.Project.ADMIN.description,
value:Permission_Types.Project.ADMIN.value
}]
})
if(props.projectId) {
value.users=value.users.filter(item=>{
if(!item.global) {
return true
}
})
}
for(let obj of value.users) {
arr.push({
name:obj.name,
id:obj.id,
reserved:obj.reserved,
description:obj.description,
permissions:obj.permissions as Permission_Base[]
})
}
data.value=arr
}
}
onBeforeMount(init)
</script>
<style scoped>
</style>

View File

@ -0,0 +1,83 @@
<template>
<div ref="root">
<a-input-search style="width: 300px;margin-bottom: 10px" placeholder="please type team name" @search="search" search-button></a-input-search>
<a-table :columns="columns" :data="data" @pageChange="onPageChange" :pagination="pagination">
<template #operation="{record}">
<a-button type="primary" size="small" @click="onEdit(record)">manage role</a-button>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue";
import {apiTeam, DCSType} from "../../../../common/request/request";
import {getCurrentNavigator} from "../../../../../teamOS/common/component/navigator/navigator";
import {ICommon_Model_Team} from "../../../../../../../common/model/team";
const columns=[
{
title:"name",
dataIndex:"name"
},
{
title:"description",
dataIndex:"description"
},
{
title:"keyword",
dataIndex:"keyword"
},
{
title:"operation",
slotName: "operation"
}
]
let data=ref<DCSType<ICommon_Model_Team[]>>([])
const root=ref(null)
const pagination=reactive({
total:0,
current:1,
pageSize:10
})
const keyword=ref("");
const search=async (key:string)=>{
keyword.value=key;
let ret=await apiTeam.list({
...(key && {
keyword:key
}),
page:0,
size:10
})
if(ret?.code==0) {
data.value=ret.data.data
pagination.total=ret.data.count;
pagination.current=ret.data.page+1
}
}
let navigator=getCurrentNavigator();
const onEdit=async (item:ICommon_Model_Team)=>{
navigator.push("teamGlobalRole",{
teamId:item.id
},"Team Role")
}
const onPageChange=async (page:number)=>{
let ret=await apiTeam.list({
...(keyword.value && {
keyword:keyword.value
}),
page:page-1,
size:10
})
if(ret?.code==0) {
data.value=ret.data.data
pagination.total=ret.data.count;
pagination.current=ret.data.page+1
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,155 @@
<template>
<div ref="root">
<a-button @click="onAdd" type="primary" style="margin-bottom: 10px">Add</a-button>
<a-table :columns="columns" :data="data" :pagination="false">
<template #description="{record}">
{{record.description}}
</template>
<template #permission="{record}">
<a-space wrap>
<a-tag v-for="item in record.permissions.map(item=>item.name)">{{item}}</a-tag>
</a-space>
</template>
<template #reserved="{record}">
<icon-check v-if="record.reserved" style="color: green"></icon-check>
<icon-close v-else style="color: red"></icon-close>
</template>
<template #operation="{record}">
<template v-if="!record.reserved">
<a-space wrap>
<a-button type="primary" size="small" @click="onEdit(record)">manage</a-button>
<a-button status="danger" size="small" @click="onDelete(record)">delete</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {getCurrentInstance, markRaw, onBeforeMount, ref} from "vue";
import {apiTeam} from "../../../../common/request/request";
import {Dialog} from "../../../../common/component/dialog/dialog";
import {Permission_Base, Permission_Types} from "../../../../../../../common/permission/permission";
import {Message} from "@arco-design/web-vue";
import EditTeamRole from "./editTeamRole.vue";
const props=defineProps<{
teamId?:string
}>()
type Item={
id:string,
name:string,
reserved:number,
description:string,
permissions:Permission_Base[]
}
const columns=[
{
title:"name",
dataIndex:"name"
},
{
title:"description",
slotName:"description"
},
{
title:"permission",
slotName:"permission"
},
{
title:"reserved",
slotName: "reserved"
},
{
title:"operation",
slotName: "operation"
}
]
const data=ref<Item[]>([])
const root=ref(null)
const appContext=getCurrentInstance().appContext
const onAdd=async ()=>{
let ret=await Dialog.open(root.value,appContext,"Add Role",markRaw(EditTeamRole),{
type:"add",
...(props.teamId && {
teamId:props.teamId
})
})
if(ret) {
init()
}
}
const onEdit=async (item:Item) =>{
let ret=await Dialog.open(root.value,appContext,"Edit Role",markRaw(EditTeamRole),{
type:"edit",
item:{
name:item.name,
id:item.id,
description:item.description,
permissions:item.permissions
}
})
if(ret) {
init()
}
}
const onDelete=async (item:Item)=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you want to delete this role?")
if(ret) {
let res=await apiTeam.removeRole({
roleId:item.id
})
if(res?.code==0) {
Message.success("remove success")
init()
} else {
Message.error(res.msg);
}
}
}
const init=async ()=>{
let ret=await apiTeam.roles({
...(props.teamId && {
teamId:props.teamId
})
})
if(ret?.code==0) {
let value=ret.data;
let arr:Item[]=[]
arr.push({
name:value.admin.name,
id:value.admin.id,
reserved:value.admin.reserved,
description:value.admin.description,
permissions:[{
name:Permission_Types.Team.ADMIN.name,
description:Permission_Types.Team.ADMIN.description,
value:Permission_Types.Team.ADMIN.value
}]
})
if(props.teamId) {
value.users=value.users.filter(item=>{
if(!item.global) {
return true
}
})
}
for(let obj of value.users) {
arr.push({
name:obj.name,
id:obj.id,
reserved:obj.reserved,
description:obj.description,
permissions:obj.permissions as Permission_Base[]
})
}
data.value=arr
}
}
onBeforeMount(init)
</script>
<style scoped>
</style>

View File

@ -0,0 +1,106 @@
<template>
<a-layout style="height: 100%">
<a-layout-sider :resize-directions="['right']">
<a-menu style="width: 100%" @menu-item-click="onSubMenuClick">
<a-sub-menu key="organization">
<template #title>Organization</template>
<a-menu-item key="organizationEdit">Edit</a-menu-item>
</a-sub-menu>
<a-sub-menu key="userTeam">
<template #title>User & Team</template>
<a-menu-item key="userManage">User Manage</a-menu-item>
<a-menu-item key="teamManage">Team Manage</a-menu-item>
<a-menu-item key="tagManage">Tag Manage</a-menu-item>
</a-sub-menu>
<a-sub-menu key="role">
<template #title>Role</template>
<a-menu-item key="organizationRole">Organization</a-menu-item>
<a-sub-menu key="roleProject">
<template #title>Project</template>
<a-menu-item key="projectGlobalRole">Global</a-menu-item>
<a-menu-item key="projectSpecificRole">Specific</a-menu-item>
</a-sub-menu>
<a-sub-menu key="roleTeam">
<template #title>Team</template>
<a-menu-item key="teamGlobalRole">Global</a-menu-item>
<a-menu-item key="teamSpecificRole">Specific</a-menu-item>
</a-sub-menu>
</a-sub-menu>
<a-sub-menu key="project">
<template #title>Project</template>
<a-menu-item key="projectManage">Manage</a-menu-item>
</a-sub-menu>
<a-sub-menu key="issueSolution">
<template #title>Issue Solution</template>
<a-menu-item key="issueSolution">Manage</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content style="flex-direction: column;display: flex;padding: 10px">
<a-breadcrumb style="margin: 10px 0;flex: 0 1 auto">
<a-breadcrumb-item v-for="item in pathList">{{item}}</a-breadcrumb-item>
</a-breadcrumb>
<a-divider margin="0"></a-divider>
<div style="width: 100%;display: flex;flex: 1 1 auto;padding-top: 20px">
<NavigatorContainer :routes="objComponent" ref="eleNavigator"></NavigatorContainer>
</div>
</a-layout-content>
</a-layout>
</template>
<script setup lang="ts">
import {markRaw, onMounted, ref} from "vue";
import IssueIndex from "./issue/index.vue"
import OrganizationIndex from "./organization/index.vue"
import ProjectIndex from "./project/index.vue"
import UserIndex from "./user&team/user.vue"
import TeamIndex from "./user&team/team.vue"
import TagIndex from "./user&team/tag.vue"
import RoleOrganizationIndex from "./role/organizationRoleList.vue"
import RoleProjectList from "./role/projectList.vue"
import RoleGlobalProjectIndex from "./role/projectRoleList.vue"
import RoleTeamGlobalIndex from "./role/teamRoleList.vue"
import RoleTeamList from "./role/teamList.vue"
import NavigatorContainer from "../../../../teamOS/common/component/navigator/navigatorContainer.vue";
let objComponent={
issueSolution:markRaw(IssueIndex),
organizationEdit:markRaw(OrganizationIndex),
projectManage:markRaw(ProjectIndex),
userManage:markRaw(UserIndex),
teamManage:markRaw(TeamIndex),
tagManage:markRaw(TagIndex),
organizationRole:markRaw(RoleOrganizationIndex),
projectSpecificRole:markRaw(RoleProjectList),
projectGlobalRole:markRaw(RoleGlobalProjectIndex),
teamGlobalRole:markRaw(RoleTeamGlobalIndex),
teamSpecificRole:markRaw(RoleTeamList),
}
let type=ref("")
let objCrumb={
organizationEdit:"Organization Edit",
userManage:"User",
teamManage:"Team",
tagManage:"Tag",
project:"Project",
issueSolution:"Issue Solution",
organizationRole:"Organization Role",
projectGlobalRole:"Global Project Role",
projectSpecificRole:"Project List",
teamGlobalRole:"Team Role",
teamSpecificRole:"Team List"
}
const eleNavigator=ref<InstanceType<typeof NavigatorContainer>>(null)
let pathList=ref([]);
onMounted(()=>{
pathList.value=eleNavigator.value.navigator.getPath();
})
const onSubMenuClick=(key:string)=>{
type.value=key;
eleNavigator.value.navigator.replaceRoot(key,null,objCrumb[key]);
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,49 @@
<template>
<a-form :model="form">
<a-form-item field="name" label="name" required>
<a-input v-model="form.name"></a-input>
</a-form-item>
<a-form-item field="description" label="description">
<a-textarea v-model="form.description" allow-clear></a-textarea>
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import {ICommon_Model_Member_Tag} from "../../../../../../../common/model/member_tag";
import {reactive} from "vue";
import {onDialogOk} from "../../../../common/component/dialog/dialog";
import {apiOrganization} from "../../../../common/request/request";
import {Message} from "@arco-design/web-vue";
const props=defineProps<{
type:"edit"|"add"
item?:ICommon_Model_Member_Tag
}>()
let form=reactive<ICommon_Model_Member_Tag>(props.type=="edit"?JSON.parse(JSON.stringify(props.item)):{
name:"",
description:"",
organization_id:sessionStorage.getItem("organizationId")
})
onDialogOk(async ()=>{
let ret=await (props.type=="edit"?apiOrganization.editTag({
memberTagId:form.id,
name:form.name,
description:form.description
}):apiOrganization.addTag({
name:form.name,
description:form.description
}))
if(ret?.code==0) {
Message.success("operation success")
return true
} else {
Message.error(ret.msg)
return false;
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,87 @@
<template>
<div ref="root">
<a-button @click="onAdd" type="primary" style="margin-bottom: 10px">Add</a-button>
<a-table :columns="columns" :data="data" :pagination="false">
<template #operation="{record}">
<template v-if="!record.reserved">
<a-space wrap>
<a-button type="primary" size="small" @click="onEdit(record)">manage</a-button>
<a-button status="danger" size="small" @click="onDelete(record)">delete</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import {getCurrentInstance, markRaw, onBeforeMount, ref} from "vue";
import {ICommon_Model_Member_Tag} from "../../../../../../../common/model/member_tag";
import {apiOrganization} from "../../../../common/request/request";
import {Dialog} from "../../../../common/component/dialog/dialog";
import EditTag from "./editTag.vue";
import {Message} from "@arco-design/web-vue";
const columns=[
{
title:"name",
dataIndex:"name"
},
{
title:"description",
dataIndex:"description"
},
{
title:"operation",
slotName: "operation"
}
]
const data=ref<ICommon_Model_Member_Tag[]>([])
const root=ref(null);
const appContext=getCurrentInstance().appContext
const search=async ()=>{
let ret=await apiOrganization.listTag()
if(ret?.code==0) {
data.value=ret.data
}
}
const onAdd=async ()=>{
let ret=await Dialog.open(root.value,appContext,"Add Tag",markRaw(EditTag),{
type:"add"
})
if(ret) {
search();
}
}
const onEdit=async (item:ICommon_Model_Member_Tag)=>{
let ret=await Dialog.open(root.value,appContext,"Edit Tag",markRaw(EditTag),{
type:"edit",
item:item
})
if(ret) {
search();
}
}
const onDelete=async (item:ICommon_Model_Member_Tag)=>{
let ret=await Dialog.confirm(root.value,appContext,"Do you want to delete this tag?")
if(ret) {
let res=await apiOrganization.removeTag({
memberTagId:item.id
})
if(res?.code==0) {
Message.success("delete success")
search()
} else {
Message.error(res.msg);
}
}
}
onBeforeMount(()=>{
search();
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,9 @@
<template>
</template>
<script setup lang="ts"></script>
<style scoped>
</style>

View File

@ -0,0 +1,8 @@
<template>
</template>
<script setup lang="ts"></script>
<style scoped>
</style>

View File

@ -0,0 +1,78 @@
<template>
<TeamOS/>
</template>
<script setup lang="ts">
import TeamOS from "../../../teamOS/index.vue";
import img from "../../../assert/back.png"
import {getDesktopInstance} from "../../../teamOS/teamOS";
import {nextTick, onBeforeMount, watchEffect} from "vue";
import {useDesktopStore} from "./store/desktop";
import {useRouter} from "vue-router";
let desktop=getDesktopInstance().desktop;
desktop.setBackgroundImage(img)
const store=useDesktopStore()
const router=useRouter();
let iconManager=getDesktopInstance().iconManager
onBeforeMount(async ()=>{
let isAuth=await store.isAuth();
if(!isAuth) {
await router.replace("login");
return
}
await store.getOrganizationList()
if(sessionStorage.getItem("organizationId")) {
await store.enterOrganization(sessionStorage.getItem("organizationId"))
}
desktop.addEventListener("menuClick",async value => {
await store.enterOrganization(value);
})
})
watchEffect(()=>{
if(store.organizationList) {
let arr=[]
if(store.organizationList.create.length>0) {
arr.push({
group:"Create",
data:store.organizationList.create.map(item=>{
return {
name:item.name,
value:item.id
}
})
})
}
if(store.organizationList.join.length>0) {
arr.push({
group:"Join",
data:store.organizationList.join.map(item=>{
return {
name:item.name,
value:item.id
}
})
})
}
if(store.organizationInfo?.photo) {
desktop.setLogo(store.organizationInfo.photo)
}
if(sessionStorage.getItem("organizationId")) {
desktop.setMenu(arr,sessionStorage.getItem("organizationId"))
} else {
desktop.setMenu(arr)
}
}
iconManager.setList(store.appList)
nextTick(()=>{
iconManager.sort()
})
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconCalendar=new Icon("calendar","calendar")
iconCalendar.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("setting",ETeamOS_Window_Type.SIMPLE, "calendar",true,[
{
id:v4(),
meta:{
title:"calendar"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"calendar");
windowManager.open(win);
})

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconIM=new Icon("im","message")
iconIM.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("im",ETeamOS_Window_Type.SIMPLE, "im",true,[
{
id:v4(),
meta:{
title:"im"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"im");
windowManager.open(win);
})

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconMeeting=new Icon("meeting","video")
iconMeeting.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("meeting",ETeamOS_Window_Type.SIMPLE, "meeting",true,[
{
id:v4(),
meta:{
title:"meeting"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"meeting");
windowManager.open(win);
})

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconProject=new Icon("project","project")
iconProject.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("project",ETeamOS_Window_Type.TAB, "project",true,[
{
id:v4(),
meta:{
title:"project"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"project");
windowManager.open(win);
})

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconSetting=new Icon("setting","setting")
iconSetting.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("setting",ETeamOS_Window_Type.SIMPLE, "setting",true,[
{
id:v4(),
meta:{
title:"setting"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"setting");
windowManager.open(win);
})

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconTeam=new Icon("team","team")
iconTeam.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("team",ETeamOS_Window_Type.TAB, "team",true,[
{
id:v4(),
meta:{
title:"team"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"team");
windowManager.open(win);
})

View File

@ -0,0 +1,30 @@
import {Icon} from "../../../../teamOS/icon/icon";
import {v4} from "uuid"
import {ETeamOS_Window_Type, Window} from "../../../../teamOS/window/window";
import Setting from "../../app/setting/setting.vue";
import {Message} from "@arco-design/web-vue";
import {windowManager} from "../../../../teamOS/window/windowManager";
import {markRaw} from "vue";
export const iconWiki=new Icon("wiki","file-text")
iconWiki.addEventListener("dbClick",item => {
if(!sessionStorage.getItem("organizationId")) {
Message.error("you must choose organization")
return;
}
const win=new Window("wiki",ETeamOS_Window_Type.TAB, "wiki",true,[
{
id:v4(),
meta:{
title:"wiki"
},
components:{
setting:markRaw(Setting)
},
default:{
name:"setting"
}
}
],"wiki");
windowManager.open(win);
})

View File

@ -0,0 +1,70 @@
import {defineStore} from "pinia";
import {ICommon_Model_Organization} from "../../../../../../common/model/organization";
import {apiOrganization, apiUser, DCSType} from "../../../common/request/request";
import {ICommon_Route_Res_Organization_List} from "../../../../../../common/routes/response";
import {Icon} from "../../../../teamOS/icon/icon";
import {iconSetting} from "../icon/setting";
import {iconCalendar} from "../icon/calendar";
import {iconMeeting} from "../icon/meeting";
import {iconProject} from "../icon/project";
import {iconTeam} from "../icon/team";
import {iconWiki} from "../icon/wiki";
import {iconIM} from "../icon/im";
export const useDesktopStore=defineStore("desktop",{
state:()=>({
organizationInfo:null as DCSType<ICommon_Model_Organization>,
organizationList:null as DCSType<ICommon_Route_Res_Organization_List>,
appList:[
iconSetting,
iconCalendar,
iconMeeting,
iconProject,
iconTeam,
iconWiki,
iconIM
] as Icon[]
}),
actions:{
async getOrganizationList() {
let ret=await apiOrganization.list()
if(ret && ret.code==0) {
this.organizationList=ret.data;
}
},
async enterOrganization (organizationId:string){
let retEnter=await apiOrganization.enter({
organizationId
});
if(retEnter?.code==0) {
let retOrganization=await apiOrganization.info({
organizationId
})
if(retOrganization?.code==0) {
this.organizationInfo=retOrganization.data
sessionStorage.setItem("organizationId",retOrganization.data.id);
}
}
},
async isAuth():Promise<boolean> {
if(!sessionStorage.getItem("userToken")) {
return false
}
let ret=await apiUser.refresh();
if(!ret || ret.code!=0) {
return false
} else {
return true
}
},
async $update() {
await this.getOrganizationList();
let retOrganization=await apiOrganization.info({
organizationId:sessionStorage.getItem("organizationId")
})
if(retOrganization?.code==0) {
this.organizationInfo=retOrganization.data
}
}
}
})

View File

@ -0,0 +1,45 @@
<template>
<a-row align="center" justify="center" style="height: 100%">
<a-form :model="form" style="width: 600px" @submit="onSubmit">
<h3 style="text-align: center;margin-bottom: 50px">Teamlinker</h3>
<a-form-item field="username" label="username" required>
<a-input v-model="form.username" placeholder="please enter username"></a-input>
</a-form-item>
<a-form-item field="password" label="password" required>
<a-input v-model="form.password" type="password" placeholder="please enter password"></a-input>
</a-form-item>
<a-form-item>
<a-button html-type="submit">Submit</a-button>
</a-form-item>
</a-form>
</a-row>
</template>
<script setup lang="ts">
import {reactive} from "vue";
import {apiUser} from "../../common/request/request";
import {useRouter} from "vue-router";
import {Message} from "@arco-design/web-vue";
let form=reactive({
username:"",
password:""
})
let router=useRouter();
const onSubmit=async ()=>{
let ret=await apiUser.login({
username:form.username,
password:form.password
})
if(ret.code==0) {
sessionStorage.removeItem("organizationId")
await router.push("desktop")
} else {
Message.error(ret.msg);
}
}
</script>
<style scoped>
</style>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,86 @@
<template>
<div ref="iconRef" style="display: inline-flex;" :style="{ fontSize: `${size}px`, color: color as any }">
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { Ant, AntJson, AntName } from './Ant';
const val = {
[AntName]: AntJson,
}
const props = defineProps<{
name:keyof typeof val,
type:Ant,
size:string,
color:string
}>(
// {
// name: { default: 'Ant' as keyof typeof val },
// type: { default: 'up' as Ant },
// size: { default: '20' },
// color: { default: undefined as any },
// }
)
const iconRef = ref<HTMLDivElement>({} as any)
const str = {
svg: ['<svg style="width: 1em;height: 1em;vertical-align: middle;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">', '</svg>'],
getPath: (param: { d: string, fill?: string }) => {
return `<path d="${param.d}" fill="${param.fill ?? 'currentColor'}" />`
}
}
onMounted(() => {
//@ts-ignore
let obj: any = val[props.name][props.type]
if (!obj) {
const color = ['color:black;', 'color:red;']
console.info(`%c sicon组件传入name<%c${props.name}%c> => type<%c${props.type}%c> 不存在`, `${color[0]} font-size:12px`, color[1], color[0], color[1], color[0]);
return
}
let res = ''
if (typeof (obj) === 'string') {
res = `${str.svg[0]}<path d="${obj}" fill="currentColor" />${str.svg[1]}`
} else {
res = str.svg[0]
let objt: { d: string[]; fill: { [key: string]: number[] }; } = obj
const getFill = (fill: { [key: string]: number[] }, index: number) => {
const fillkeys = Object.keys(fill)
const colors = props.color
for (let i = 0; i < fillkeys.length; i++) {
const key = fillkeys[i];
const nums = fill[key]
if (nums.includes(index)) {
if (colors) {
if (Array.isArray(colors))
return colors[i] ?? colors[colors.length - 1]
return colors
} else {
return key
}
}
}
return 'currentColor'
}
objt.d.forEach((item, index) => {
res += str.getPath({
d: item,
fill: getFill(objt.fill, index)
})
})
res += str.svg[1]
}
iconRef.value.innerHTML = res
})
</script>

36
code/client/src/main.ts Normal file
View File

@ -0,0 +1,36 @@
import {createApp} from 'vue'
import './style.css'
import App from './App.vue'
import '@arco-design/web-vue/dist/arco.css';
import ArcoVue, {Message} from '@arco-design/web-vue';
import sicon from "./icon/sicon.vue"
import ArcoVueIcon from '@arco-design/web-vue/es/icon';
import {createPinia} from "pinia";
import Login from "./business/controller/login/login.vue";
import Desktop from "./business/controller/desktop/desktop.vue";
import {createRouter, createWebHashHistory} from "vue-router";
const routes=[
{
name:"login",
path:"/login",
component:Login
},
{
name:"desktop",
path:"/desktop",
component:Desktop
}
]
const router=createRouter({
history:createWebHashHistory(),
routes
});
let app=createApp(App)
Message._context=app._context
app.use(ArcoVue)
app.use(ArcoVueIcon)
app.use(createPinia())
app.use(router);
app.component("sicon",sicon)
app.mount('#app')

11
code/client/src/style.css Normal file
View File

@ -0,0 +1,11 @@
html,body {
width: 100%;
height: 100%;
margin: 0px;
}
#app {
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,21 @@
<template>
<a-row class="row" style="font-size:14px;width: 120px;">
<a-row class="row" v-for="(item,index) in data" style="height: 35px;line-height: 35px;overflow: hidden;text-overflow: ellipsis;width: 100%;padding-left: 10px;padding-right: 10px" @mouseenter="$event.currentTarget.style.backgroundColor='rgba(39,139,207,0.6)'" @mouseleave="$event.currentTarget.style.backgroundColor=''" @click="select(item)" justify="left" align="center" :style="{borderBottom:index!=data.length-1?'1px solid gray':''}">
<sicon name="Ant" :type="item.icon" color="" size=""></sicon>&nbsp;&nbsp;{{item.title}}
</a-row>
</a-row>
</template>
<script setup lang="ts">
import {ITeamOS_Menu} from "../type";
const props=defineProps<{
data:ITeamOS_Menu[],
}>()
const select=(item:ITeamOS_Menu)=>{
if(item.func) {
item.func(item.value);
}
}
</script>

View File

@ -0,0 +1,219 @@
import {Component, getCurrentInstance, h, inject, nextTick, reactive, ref, VNode} from "vue";
import {NavigatorManager} from "./navigatorManager";
import {Base} from "../../util/base";
export enum ETeamOS_Navigator_Action {
PUSH,
BACK,
GO,
REPLACE
}
export function onNavigatorShow(func:(action:ETeamOS_Navigator_Action)=>void) {
let navigator=getCurrentNavigator();
let instance=getCurrentInstance().vnode
navigator.setFunc(instance.type["__name"],func);
}
export function getCurrentNavigator() {
return inject("navigator",null) as Navigator
}
export function getCurrentNavigatorManager() {
return inject("navigatorManager",null) as NavigatorManager
}
export function getCurrentNavigatorMeta<T>() {
return inject("navigatorMeta",null) as {
title?:string,
data?:T
}
}
export class Navigator extends Base{
private name:string=""
private nodeShowFunc=new Map<string,(action:ETeamOS_Navigator_Action)=>void>()
private manager:NavigatorManager
private parent:Navigator;
private router=reactive<VNode[]>([])
private mapComponent:{
[name:string]:Component
}={}
private index=ref(-1)
private path=reactive([])
constructor(name:string,mapComponent:{
[name:string]:Component
}) {
super();
this.name=name;
for(let key in mapComponent) {
this.mapComponent[key]=mapComponent[key]
}
}
getName() {
return this.name;
}
setFunc(name:string,func) {
this.nodeShowFunc.set(name,func);
}
getIndex() {
return this.index;
}
getId() {
return this.id;
}
push(name:string,props?:object,title?:string) {
if(this.mapComponent[name]) {
let obj=this.mapComponent[name];
if(this.index.value==this.router.length-1) {
this.router.push(h(obj, {
...props,
key:Date.now()
}))
this.index.value++
this.path.push(title??" ")
} else {
let vNode=h(obj, {
...props,
key:Date.now()
})
this.path.splice(this.index.value+1,this.router.length,title??" ");
this.router.splice(this.index.value+1,this.router.length,vNode);
this.index.value++
}
this.manager.setCurrentNavigator(this);
(async ()=> {
await nextTick();
let objNew=this.router[this.index.value]
let func=this.nodeShowFunc.get(objNew.type["__name"]);
if(func) {
func(ETeamOS_Navigator_Action.PUSH)
}
})()
} else {
if(this.parent) {
this.parent.push(name,props)
}
}
}
register(name:string,component:Component) {
this.mapComponent[name]=component;
}
back() {
if(this.index.value>0) {
this.index.value--
this.manager.setCurrentNavigator(this);
let objNew=this.router[this.index.value]
let func=this.nodeShowFunc.get(objNew.type["__name"]);
if(func) {
func(ETeamOS_Navigator_Action.BACK)
}
} else {
if(this.parent) {
this.parent.back();
}
}
// if(this.index.value>0) {
// this.router.splice(this.index.value)
// this.index.value--
// this.manager.setCurrentNavigator(this);
// let objNew=this.router[this.index.value]
// let func=this.nodeShowFunc.get(objNew.type["__name"]);
// if(func) {
// func(ETeamOS_Navigator_Action.BACK)
// }
// } else {
// if(this.parent) {
// this.parent.back();
// }
// }
}
replace(name:string,props?:object,title?:string) {
if(this.mapComponent[name]) {
let obj=this.mapComponent[name];
this.router.splice(this.index.value,1,h(obj, {
...props,
key:Date.now()
}));
this.path.splice(this.index.value,1,title??" ")
this.manager.setCurrentNavigator(this);
let objNew=this.router[this.index.value]
let func=this.nodeShowFunc.get(objNew.type["__name"]);
if(func) {
func(ETeamOS_Navigator_Action.REPLACE)
}
} else {
if(this.parent) {
this.parent.replace(name,props)
}
}
}
replaceRoot(name:string,props:object,title?:string) {
if(this.mapComponent[name]) {
let obj=this.mapComponent[name];
this.router.splice(0,this.router.length,h(obj,{
...props,
key:Date.now()
}))
this.path.splice(0,this.path.length,title??" ");
this.index.value=0;
this.manager.setCurrentNavigator(this);
let objNew=this.router[this.index.value]
let func=this.nodeShowFunc.get(objNew.type["__name"]);
if(func) {
func(ETeamOS_Navigator_Action.REPLACE)
}
} else {
if(this.parent) {
this.parent.replaceRoot(name, props);
}
}
}
canBack(){
if(this.index.value>0) {
return true;
} else {
if(!this.parent) {
return false;
} else {
return this.parent.canBack();
}
}
}
canGo() {
if(this.index.value<this.router.length-1) {
return true
}
return false;
}
go() {
if(this.index.value<this.router.length-1) {
this.index.value++
this.manager.setCurrentNavigator(this);
let objNew=this.router[this.index.value]
let func=this.nodeShowFunc.get(objNew.type["__name"]);
if(func) {
func(ETeamOS_Navigator_Action.GO)
}
}
}
list() {
return this.router
}
getParent () {
return this.parent
}
setParent (parent:Navigator) {
this.parent=parent;
}
setManager(manager:NavigatorManager) {
this.manager=manager
manager.getNavigators()[this.name]=this;
}
getPath(){
return this.path
}
}

View File

@ -0,0 +1,86 @@
<template>
<div style="width: 100%;height: 100%" ref="root">
<template v-for="(item,index) in list">
<component :is="item" v-show="index==currentIndex"></component>
</template>
</div>
</template>
<script setup lang="ts">
import {inject, onMounted, onUnmounted, provide, ref, watchEffect} from "vue";
import {Navigator} from "./navigator";
import {NavigatorManager} from "./navigatorManager";
let props=defineProps<{
id?:string,
name?:string,
routes:{
[name:string]:any
}
default?:{
name:string,
props?:object,
title?:string
},
meta?:{
title?:string,
data?:any
},
path?:{
[name:string]:{
name:string,
props?:object
}
}
}>()
let root=ref(null);
let parent=inject("navigator",null) as Navigator;
const objNavigator=new Navigator(props.name,props.routes)
objNavigator.setParent(parent);
let manager:NavigatorManager;
if(!parent) {
manager=new NavigatorManager()
objNavigator.setManager(manager)
if(props.path) {
manager.locate(JSON.parse(JSON.stringify(props.path)))
}
provide("navigatorManager",manager)
} else {
manager=inject("navigatorManager",null) as NavigatorManager
objNavigator.setManager(manager);
}
const list=objNavigator.list();
const currentIndex=objNavigator.getIndex();
provide("navigator",objNavigator)
if(props.default) {
objNavigator.push(props.default.name,props.default.props,props.default.title);
}
if(props.meta) {
provide("navigatorMeta",props.meta)
}
watchEffect(()=>{
let objPath=manager.getObjPath();
let obj=objPath[props.name]
if(obj) {
objNavigator.replaceRoot(obj.name,obj.props);
delete objPath[props.name];
}
})
onUnmounted(()=>{
delete manager.getNavigators()[objNavigator.getName()]
})
defineExpose({
navigator:objNavigator,
navigatorManager:manager,
id:props.id
})
onMounted(()=>{
if(!parent) {
manager.setRootElement(root.value);
}
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,54 @@
import {Navigator} from "./navigator";
import {reactive, ref} from "vue";
import {Base} from "../../util/base";
export class NavigatorManager extends Base {
canGo=ref(false)
canBack=ref(false)
private currentNavigator:Navigator
private navigators:{
[name:string]:Navigator
}={}
private objPath=reactive<{
[name:string]:{
name:string,
props?:object
}
}>({})
private rootElement:HTMLElement
setRootElement(ele:HTMLElement) {
this.rootElement=ele;
}
getRootElement() {
return this.rootElement;
}
getObjPath() {
return this.objPath;
}
getCurrentNavigator() {
return this.currentNavigator
}
setCurrentNavigator(navigator:Navigator) {
this.currentNavigator=navigator;
this.canGo.value=navigator.canGo();
this.canBack.value=navigator.canBack()
}
go() {
if(this.currentNavigator) {
this.currentNavigator.go()
}
}
back() {
if(this.currentNavigator) {
this.currentNavigator.back()
}
}
getNavigators(){
return this.navigators;
}
locate(objPath:typeof this.objPath) {
for(let key in objPath) {
this.objPath[key]=objPath[key];
}
}
}

View File

@ -0,0 +1,66 @@
import {ITeamOS_Point} from "../type";
export const vDrag={
mounted(el:HTMLElement, binding, vnode, prevVnode){
el.draggable=true;
let point:ITeamOS_Point=binding.value;
let left:any,top:any,parentWidth:number,parentHeight:number,startLeft:number,startTop:number,parentElement:HTMLElement
const onDragStart=(ev: DragEvent)=>{
let target=ev.currentTarget as HTMLElement
if(binding.arg) {
target=document.getElementById(binding.arg)
}
left=ev.pageX - target.offsetLeft
top=ev.pageY - target.offsetTop
let ele:HTMLElement;
startLeft=(ev.pageX-left)/parentWidth*100
startTop=(ev.pageY-top)/parentHeight*100;
if(!parentElement) {
while(ele=target.parentElement) {
if(ele.tagName.toLowerCase()=="body" || ele.style.position=="absolute" || ele.style.position=="relative") {
parentHeight=ele.clientHeight;
parentWidth=ele.clientWidth;
parentElement=ele;
break
}
target=ele;
}
} else {
parentHeight=parentElement.clientHeight;
parentWidth=parentElement.clientWidth;
}
let img = new Image();
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
img.width=0;
img.height=0;
img.style.opacity="0"
ev.dataTransfer.setDragImage(img, 0, 0);
}
const onDrag=(ev: any)=>{
let calcLeft:number,calcTop:number
if(ev.pageX==0) {
calcLeft=startLeft
} else if(ev.pageX<left) {
calcLeft=(ev.pageX-left)/parentWidth*100
} else {
calcLeft=(ev.pageX-left)/parentWidth*100
}
if(ev.pageY==0) {
calcTop=startTop;
} else if(ev.pageY<top) {
calcTop=0
} else {
calcTop=(ev.pageY-top)/parentHeight*100;
}
point.left = `${calcLeft}%`;
point.top=`${calcTop}%`
}
const onDragOver=(ev: any)=>{
ev.stopPropagation()
ev.preventDefault()
}
el.addEventListener("dragstart",onDragStart)
el.addEventListener("drag",onDrag)
el.addEventListener("dragover",onDragOver)
},
}

View File

@ -0,0 +1,49 @@
import {renderComponent} from "../util/component";
import ContextMenu from "../component/contextMenu.vue";
import {VNode} from "vue";
export const vMenu={
mounted(el:HTMLElement, binding, vnode:VNode, prevVnode){
const onContextMenu=async (event:MouseEvent)=>{
let value=binding.value
let modifiers=binding.modifiers;
if(!value) {
return;
}
let appContext=(<any>vnode).ctx.appContext;
if(modifiers.self) {
if(event.target!==event.currentTarget) {
return;
}
}
event.stopPropagation();
event.preventDefault();
if(typeof(value)=="function") {
value=await value()
}
let ele=document.createElement("div")
ele.style.position="absolute"
ele.style.left=event.pageX+5+"px";
ele.style.top=event.pageY+5+"px"
ele.style.borderRadius="3px";
ele.style.boxShadow="0px 0px 2px 2px rgba(169, 169, 169, 0.2)"
ele.style.backgroundColor=`rgb(249,249,249)`
ele.style.zIndex="1000"
let destroyFunc=renderComponent(ele,ContextMenu,appContext,{
data:value
});
ele.style.color="rgb(93,93,93)"
ele.tabIndex=1000;
ele.onblur=()=>{
destroyFunc()
document.body.removeChild(ele);
}
ele.onclick=()=>{
destroyFunc()
}
document.body.appendChild(ele);
ele.focus();
}
el.addEventListener("contextmenu",onContextMenu)
},
}

View File

@ -0,0 +1,23 @@
import {Navigator} from "../component/navigator/navigator";
export const vNavigator={
mounted(el:HTMLElement, binding, vnode, prevVnode){
let objRouter=binding.value as {
name:string,
props?:object
};
let navigator=binding.arg as Navigator
let modifiers=binding.modifiers;
el.onclick=(ev:Event)=>{
ev.stopPropagation()
ev.preventDefault()
if(modifiers.replace) {
navigator.replace(objRouter.name,objRouter.props)
} else if(modifiers.root) {
navigator.replaceRoot(objRouter.name,objRouter.props)
} else {
navigator.push(objRouter.name,objRouter.props)
}
}
},
}

View File

@ -0,0 +1,39 @@
import {ITeamOS_Rect} from "../type";
import {getDesktopInstance} from "../../teamOS";
let g_parentElement:{
[id:string]:HTMLElement
}={
}
const resizeObserver=new ResizeObserver((entries, observer)=>{
for(let entry of entries) {
let id=entry.target.id
let win=getDesktopInstance().windowManager.getById(id);
if(win && g_parentElement[id]) {
let ele=g_parentElement[id]
win.rect.width= `${entry.contentRect.width/ele.clientWidth*100}%`
win.rect.height= `${entry.contentRect.height/ele.clientHeight*100}%`
}
}
})
export const vResize={
mounted(el:HTMLElement, binding, vnode, prevVnode){
el.style.overflow="hidden"
el.style.resize="both";
let rect:ITeamOS_Rect=binding.value;
resizeObserver.observe(el);
let ele:HTMLElement
while(ele=el.parentElement) {
if(ele.tagName.toLowerCase()=="body" || ele.style.position=="absolute" || ele.style.position=="relative") {
g_parentElement[el.id]=ele;
break
}
el=ele;
}
},
unmounted(el:HTMLElement) {
resizeObserver.unobserve(el);
delete g_parentElement[el.id];
}
}

View File

@ -0,0 +1,18 @@
export interface ITeamOS_Point {
left:`${number}%`,
top:`${number}%`
}
export interface ITeamOS_Rect {
left:`${number}%`,
top:`${number}%`,
width:`${number}%`,
height:`${number}%`,
}
export interface ITeamOS_Menu {
icon:string,
title:string,
value:any,
func:(value:any)=>void
}

View File

@ -0,0 +1,5 @@
import {v4} from "uuid";
export class Base {
id=v4()
}

View File

@ -0,0 +1,42 @@
export function getBaseColor(imgSrc:string) {
return new Promise((resolve, reject)=>{
let img = document.createElement("img");
img.crossOrigin = "Anonymous"
img.src=imgSrc
let canvas = document.createElement('canvas')
let context = canvas.getContext("2d");
img.onload=function() {
let width=canvas.width = img.width || img.offsetWidth || img.clientWidth;
let height=canvas.height = img.height || img.offsetHeight || img.clientHeight;
context.drawImage(img, 0, 0, canvas.width, canvas.height);
let data = context.getImageData(0, 0, img.width, img.height).data;
let r = 1,
g = 1,
b = 1;
for (let row = 0; row < img.height; row++) {
for (let col = 0; col < img.width; col++) {
if (row == 0) {
r += data[((img.width * row) + col)];
g += data[((img.width * row) + col) + 1];
b += data[((img.width * row) + col) + 2];
} else {
r += data[((img.width * row) + col) * 4];
g += data[((img.width * row) + col) * 4 + 1];
b += data[((img.width * row) + col) * 4 + 2];
}
}
}
r /= (img.width * img.height);
g /= (img.width * img.height);
b /= (img.width * img.height);
r = Math.round(r);
g = Math.round(g);
b = Math.round(b);
resolve([r, g, b])
}
})
}

View File

@ -0,0 +1,11 @@
import {AppContext, Component, h, render} from "vue";
export function renderComponent(el:HTMLElement,component:Component,context:AppContext,props?:object):()=>void {
let vNode=h(component,props)
vNode.appContext={...context}
render(vNode,el)
return ()=>{
render(null,el)
vNode=null
}
}

View File

@ -0,0 +1,73 @@
import {reactive, ref} from "vue";
import {getBaseColor} from "../common/util/color";
import {Base} from "../common/util/base";
export interface ITeamOS_Desktop_Event {
"menuClick":(value:string)=>void
}
export interface ITeamOS_Desktop_Menu {
group:string,
data:{
name:string,
value:string
}[]
}
class Desktop extends Base{
private backgroundImage=ref("")
private logo=ref("")
private menu=reactive<ITeamOS_Desktop_Menu[]>([])
private baseColor:{
r:number,
g:number,
b:number
}=reactive({
r:0,
g:0,
b:0
});
private menuValue=ref("")
onMenuClick:(value:string)=>void
setLogo(logo:string) {
this.logo.value=logo
}
setMenu(menu:ITeamOS_Desktop_Menu[],defaultValue?:string) {
if(this.menu.length>0) {
this.menu.splice(0)
}
this.menu.push(...menu);
if(defaultValue) {
this.menuValue.value=defaultValue
}
}
getMenuValue() {
return this.menuValue
}
getLogo() {
return this.logo
}
getMenu() {
return this.menu
}
getBackgroundImage() {
return this.backgroundImage
}
setBackgroundImage(url:string) {
this.backgroundImage.value=url
getBaseColor(url).then(([r,g,b])=>{
this.baseColor.r=r;
this.baseColor.g=g;
this.baseColor.b=b;
})
}
getBaseColor() {
return this.baseColor;
}
addEventListener<T extends keyof ITeamOS_Desktop_Event>(eventType:T,func:ITeamOS_Desktop_Event[T]) {
if(eventType=="menuClick") {
this.onMenuClick=func
}
}
}
export const desktop=new Desktop();

View File

@ -0,0 +1,46 @@
<template>
<a-row>
<a-col flex="80px" style="height: 100%">
<a-row align="center" justify="center" style="height: 100%">
<img :src="logo" style="max-height: 100%;max-width: 100%;height: auto;width: auto"/>
</a-row>
</a-col>
<a-col flex="auto" style="height: 100%;padding-left: 50px">
<a-row style="height: 100%" align="center" justify="start" :wrap="false">
<a-col v-for="item in list" :key="item.id" flex="auto" style="height: 80%;line-height: 32px;max-width: 200px;text-align: center;box-shadow: 0px 2px 2px rgba(93, 93, 93, 0.2);margin-left: 5px;cursor: pointer;color: rgb(249,249,249);border-radius: 3px;color: rgb(249,249,249)" :style="{backgroundColor:item.isFocus?`rgb(255,255,255,0.3)`:`rgb(255,255,255,0.1)`}" @click="item.status==ETeamOS_Window_Status.MIN?windowManager.show(item.id):windowManager.setFocus(item.id)">
{{item.title}}
</a-col>
</a-row>
</a-col>
<a-col flex="150px" style="height: 100%;" id="teamOS-Desktop-Menu">
<a-row align="center" justify="center" style="height: 100%;">
<a-select style="width: 100%;" placeholder="organization" :bordered="false" @change="onMenuClick" v-model:model-value="menuValue">
<a-optgroup v-for="(item,index) in menu" :label="item.group">
<a-option v-for="(item1,index1) in item.data" :label="item1.name" :value="item1.value"></a-option>
</a-optgroup>
</a-select>
</a-row>
</a-col>
</a-row>
</template>
<script setup lang="ts">
import {desktop} from "./desktop";
import {windowManager} from "../window/windowManager";
import {ETeamOS_Window_Status} from "../window/window.js";
const logo=desktop.getLogo();
const menu=desktop.getMenu();
const list=windowManager.getList();
const color=desktop.getBaseColor();
const menuValue=desktop.getMenuValue()
const onMenuClick=(value:string)=>{
if(desktop.onMenuClick) {
desktop.onMenuClick(value)
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,28 @@
<template>
<div id="teamOS" :style="{backgroundImage:'url('+backImag+')'}">
<DesktopBar style="height: 40px;background: rgba(255,255,255,0.3);">
</DesktopBar>
<IconContainer style="height: calc(100% - 40px)">
<WindowContainer></WindowContainer>
</IconContainer>
</div>
</template>
<script setup lang="ts">
import DesktopBar from "./desktopBar.vue";
import IconContainer from "../icon/iconContainer.vue";
import {desktop} from "./desktop";
import WindowContainer from "../window/windowContainer.vue";
let backImag=desktop.getBackgroundImage()
</script>
<style scoped>
#teamOS {
width: 100%;
height: 100%;
position: relative;
background-size: 100% 100%;
}
</style>

View File

@ -0,0 +1,36 @@
import {ITeamOS_Menu, ITeamOS_Point} from "../common/type";
import {Base} from "../common/util/base";
export interface ITeamOS_Icon_Event {
"move":(item:Icon)=>void
"dbClick":(item:Icon)=>void
"contextmenu":((item:Icon)=>ITeamOS_Menu[]) | ((item:Icon)=>Promise<ITeamOS_Menu[]>)
}
export class Icon extends Base{
static height=80;
static width=60;
name=""
icon=""
meta:any
point:ITeamOS_Point
onDBClick:(item:Icon)=>void
onMove:(item:Icon)=>void
onContextMenu:((item:Icon)=>ITeamOS_Menu[]) | ((item:Icon)=>Promise<ITeamOS_Menu[]>)
constructor(name:string,icon:string,point:ITeamOS_Point={left:"0%",top:"0%"},meta?:any) {
super()
this.name=name;
this.icon=icon;
this.point=point
this.meta=meta
}
addEventListener<T extends keyof ITeamOS_Icon_Event>(eventType:T,func:ITeamOS_Icon_Event[T]) {
if(eventType=="move") {
this.onMove=func.bind(null,this);
} else if(eventType=="dbClick") {
this.onDBClick=func.bind(null,this);
} else if(eventType=="contextmenu") {
this.onContextMenu=func.bind(null,this) as ITeamOS_Icon_Event["contextmenu"];
}
}
}

View File

@ -0,0 +1,30 @@
<template>
<div id="teamOS-iconWindow" style="position: relative;overflow: hidden" v-menu.self="menu">
<IconItem v-for="(item,index) in iconList" :index="index" :item="item" :key="item.id"></IconItem>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import {iconManager} from "./iconManager";
import IconItem from "./iconItem.vue";
import {vMenu} from "../common/directive/menu";
import {ITeamOS_Menu} from "../common/type";
vMenu;
let iconList=iconManager.getList();
let menu:ITeamOS_Menu[]=[
{
icon:"sort-ascending",
title:"sort",
value:null,
func:(value:any)=>{
iconManager.sort();
}
}
]
</script>
<style scoped>
</style>

View File

@ -0,0 +1,35 @@
<template>
<div :style="{width:Icon.width+'px',height:Icon.height+'px',left:point.left,top:point.top,position: 'absolute'}" @focus="onFocus" @blur="onBlur" :tabindex="index" v-drag="point" v-menu="item.onContextMenu" @dblclick="item.onDBClick" @dragend="item.onMove">
<a-row :style="{width: Icon.width+'px',height:Icon.width+'px',textAlign: 'center'}" style="justify-content: center;align-items: center">
<sicon name="Ant" :type="item.icon" size="40" color=""></sicon>
</a-row>
<div :style="{width: '100%',height:Icon.height-Icon.width+'px'}" style="overflow: hidden;text-overflow:ellipsis;textAlign:center;vertical-align:middle;white-space: nowrap;user-select: none;color: rgb(249,249,249)">
{{item.name}}
</div>
</div>
</template>
<script setup lang="ts">
import {Icon} from "./icon"
import {vDrag} from "../common/directive/drag";
import {vMenu} from "../common/directive/menu";
vMenu;
vDrag;
const props=defineProps<{
item:Icon,
index:number,
}>();
let point=props.item.point;
const onFocus=(ev)=> {
ev.target.style.backgroundColor="rgba(0,0,0,0.1)"
}
const onBlur=(ev)=>{
ev.target.style.backgroundColor="rgba(111,111,11,0)"
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,49 @@
import {Icon} from "./icon";
import {reactive} from "vue";
import {Base} from "../common/util/base";
class IconManager extends Base {
private iconList=reactive<Icon[]>([])
add(item:Icon) {
this.iconList.push(item)
}
remove(name:string) {
for(let i=0;i<this.iconList.length;i++) {
let obj=this.iconList[i]
if(obj.name==name) {
this.iconList.splice(i,1);
break;
}
}
}
setList(items:Icon[]) {
this.iconList.splice(0,this.iconList.length,...items)
}
getList() {
return this.iconList;
}
sort() {
let ele=document.getElementById("teamOS-iconWindow")
let height=ele.clientHeight
let width=ele.clientWidth
let count=Math.floor((height-40)/(Icon.height+20))
this.iconList.sort((a,b)=>{
if(a.name[0]>b.name[0]) {
return 1
} else {
return -1;
}
})
for(let i=0;i<this.iconList.length;i+=count) {
for(let j=0;j<count;j++) {
let obj=this.iconList[i*count+j]
if(obj) {
obj.point.left=`${(i+1)*20/width*100}%`;
obj.point.top=`${(20+((Icon.height+20)*j))/height*100}%`
}
}
}
}
}
export const iconManager=new IconManager();

View File

@ -0,0 +1,10 @@
<template>
<DesktopContainer></DesktopContainer>
</template>
<script setup lang="ts">
import DesktopContainer from "./desktop/desktopContainer.vue";</script>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
import {desktop} from "./desktop/desktop";
import {iconManager} from "./icon/iconManager";
import {windowManager} from "./window/windowManager";
export function getDesktopInstance() {
return {
desktop: desktop,
iconManager:iconManager,
windowManager:windowManager
}
}

View File

@ -0,0 +1,93 @@
import {ITeamOS_Rect} from "../common/type";
import {Component} from "vue";
import {Base} from "../common/util/base";
import {v4} from "uuid";
export enum ETeamOS_Window_Type {
SIMPLE,
TAB
}
export enum ETeamOS_Window_Status {
NORMAL,
MAX,
MIN
}
export interface ITeamOS_Window_Node {
id?:string
components:{
[name:string]:Component
}
default:{
name:string,
props?:object
},
meta?:{
title?:string,
data?:any
},
path?:{
[name:string]:{
name:string,
props?:object
}
}
}
export interface ITeamOS_Window_Event {
"open":(item:Window)=>void,
"move":(item:Window)=>void,
"newTab":(item:Window)=>Promise<ITeamOS_Window_Node>
"removeTab":(item:Window)=>void
}
export class Window extends Base{
name=""
type:ETeamOS_Window_Type
title=""
isFocus=false
rect:ITeamOS_Rect={
left:"10%",
top:"10%",
width:"80%",
height:"80%"
}
group:string
nodes:ITeamOS_Window_Node[]
status=ETeamOS_Window_Status.NORMAL
isControl=false
activeKey=""
onMove:(item:Window)=>void
onOpen:(item:Window)=>void
onNewTab:(item:Window)=>Promise<ITeamOS_Window_Node>
onRemoveTab:(item:Window)=>void
constructor(name:string,type:ETeamOS_Window_Type,group:string,isControl:boolean,nodes:ITeamOS_Window_Node[],title?:string) {
super()
this.name=name;
this.type=type;
this.group=group;
this.isControl=isControl
this.nodes=nodes;
for(let obj of this.nodes) {
if(!obj.id) {
obj.id=v4()
}
}
this.activeKey=this.nodes[0].id
if(title) {
this.title=title;
}
}
addEventListener<T extends keyof ITeamOS_Window_Event>(eventType:T,func:ITeamOS_Window_Event[T]) {
if(eventType=="move") {
this.onMove=func.bind(null,this);
} 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);
}
}
}

View File

@ -0,0 +1,18 @@
<template>
<WindowItem v-for="(item,index) in windowList" :item="item" :index="index"></WindowItem>
</template>
<script setup lang="ts">
import {windowManager} from "./windowManager";
import WindowItem from "./windowItem.vue";
let windowList=windowManager.getList();
</script>
<style scoped>
</style>

View File

@ -0,0 +1,177 @@
<template>
<div :data-id="item.id" :id="item.id" :style="{zIndex:item.isFocus?101:100,left:item.rect.left,top:item.rect.top,width:item.rect.width,height:item.rect.height}" style="box-shadow:0px 0px 2px 2px rgba(169, 169, 169, 0.2);position:absolute;border-radius: 5px" :tabindex="index" @focus="onFocus(item.id)" v-resize="item.rect" v-show="item.status!=ETeamOS_Window_Status.MIN">
<a-row style="height: 35px;" :style="{backgroundColor:`rgb(249,249,249)`}" v-drag:[item.id]="item.rect" @dragend="item.onMove">
<a-col flex="80px" style="height: 100%">
<a-row style="height: 100%" align="center" justify="left" :wrap="false" v-if="item.isControl">
<a-col flex="auto">
<a-button type="text" @click="back" v-show="action.canBack">
<template #icon>
<icon-arrow-left style="color: rgb(93,93,93)"></icon-arrow-left>
</template>
</a-button>
</a-col>
<a-col flex="auto">
<a-button type="text" @click="go" v-show="action.canGo">
<template #icon>
<icon-arrow-right style="color: rgb(93,93,93)"></icon-arrow-right>
</template>
</a-button>
</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: larger;cursor: move">
{{item.title}}
</a-col>
<a-col flex="80px" style="height: 100%">
<a-row style="height: 100%" align="center" justify="center" :wrap="false">
<a-col flex="auto">
<a-button type="text" @click="onMin(item.id)">
<template #icon>
<icon-minus style="color: rgb(93,93,93)"></icon-minus>
</template>
</a-button>
</a-col>
<a-col flex="auto">
<a-button type="text" @click="item.status==ETeamOS_Window_Status.NORMAL?onMax(item.id):onNormal(item.id)">
<template #icon>
<icon-expand v-if="item.status==ETeamOS_Window_Status.NORMAL" style="color: rgb(93,93,93)"></icon-expand>
<icon-shrink v-else-if="item.status==ETeamOS_Window_Status.MAX" style="color: rgb(93,93,93)"></icon-shrink>
</template>
</a-button>
</a-col>
<a-col flex="auto">
<a-button type="text" @click="onClose(item.id)">
<template #icon>
<icon-close style="color: rgb(93,93,93)"></icon-close>
</template>
</a-button>
</a-col>
</a-row>
</a-col>
</a-row>
<a-row style="height: calc(100% - 35px);background-color: white;position: relative">
<NavigatorContainer :routes="item.nodes[0].components" :id="item.nodes[0].id" :path="item.nodes[0].path" :default="item.nodes[0].default" ref="navigator" v-if="item.type==ETeamOS_Window_Type.SIMPLE" name="root"/>
<a-tabs type="card-gutter" :editable="true" show-add-button auto-switch :justify="true" v-else-if="item.type==ETeamOS_Window_Type.TAB" @change="change" style="width: 100%" @add="addTab" @delete="removeTab" @tabClick="change" :active-key="item.activeKey">
<a-tab-pane v-for="(node,index) in item.nodes" :key="node.id" :title="node.meta?.title" :tabindex="node.id" :closable="item.nodes.length>1">
<NavigatorContainer :id="node.id" :routes="node.components" :default="node.default" :meta="node.meta" ref="navigatorList" name="root" :path="node.path"/>
</a-tab-pane>
</a-tabs>
</a-row>
</div>
</template>
<script setup lang="ts">
import {windowManager} from "./windowManager";
import {onMounted, reactive, ref} from "vue";
import NavigatorContainer from "../common/component/navigator/navigatorContainer.vue";
import {ETeamOS_Window_Status, ETeamOS_Window_Type, Window} from "./window";
import {vDrag} from "../common/directive/drag";
import {vResize} from "../common/directive/resize";
import {v4} from "uuid"
vResize;
vDrag;
let props=defineProps<{
item:Window,
index:number
}>()
const onFocus=(id:string)=>{
windowManager.setFocus(id);
}
const onMax=(id:string)=>{
windowManager.max(id)
}
const onNormal=(id:string)=>{
windowManager.normal(id)
}
const onMin=(id:string)=>{
windowManager.hide(id)
}
const onClose=(id:string)=>{
windowManager.close(id)
}
const navigator=ref<InstanceType<typeof NavigatorContainer>>(null)
const navigatorList=ref<InstanceType<typeof NavigatorContainer>[]>([])
const back=()=>{
if(props.item.type==ETeamOS_Window_Type.SIMPLE) {
navigator.value.navigatorManager.back()
} else {
for(let o of navigatorList.value) {
if(o.id==props.item.activeKey) {
o.navigatorManager.back()
}
}
}
}
const go=()=>{
if(props.item.type==ETeamOS_Window_Type.SIMPLE) {
navigator.value.navigatorManager.go()
} else {
for(let o of navigatorList.value) {
if(o.id==props.item.activeKey) {
o.navigatorManager.go()
}
}
}
}
const change=(key)=>{
props.item.activeKey=key;
for(let o of navigatorList.value) {
if(o.id==props.item.activeKey) {
action.canBack=o.navigatorManager.canBack
action.canGo=o.navigatorManager.canGo
}
}
}
let action=reactive({
canBack:ref(false),
canGo:ref(false)
})
onMounted(()=>{
if(props.item.type==ETeamOS_Window_Type.SIMPLE) {
action.canBack=navigator.value.navigatorManager.canBack
action.canGo=navigator.value.navigatorManager.canGo
} else {
for(let o of navigatorList.value) {
if(o.id==props.item.activeKey) {
action.canBack=o.navigatorManager.canBack
action.canGo=o.navigatorManager.canGo
}
}
}
})
const addTab=async ()=>{
if(props.item.onNewTab) {
let ret=await props.item.onNewTab(props.item)
if(ret) {
if(!ret.id) {
ret.id=v4();
}
props.item.nodes.push(ret);
props.item.activeKey=ret.id;
}
}
}
const removeTab=async (id:string)=>{
for(let i=0;i<props.item.nodes.length;i++) {
let node=props.item.nodes[i]
if(node.id==id) {
props.item.nodes.splice(i,1);
if(id==props.item.activeKey) {
if(i>=props.item.nodes.length) {
props.item.activeKey=props.item.nodes[props.item.nodes.length-1].id;
}
}
break;
}
}
if(props.item.onRemoveTab) {
props.item.onRemoveTab(props.item);
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,136 @@
import {reactive} from "vue";
import {ETeamOS_Window_Status, ETeamOS_Window_Type, Window} from "./window";
import {Base} from "../common/util/base";
export class WindowManager extends Base{
private windowList=reactive<Window[]>([])
open(window:Window,isMulti:boolean=false) {
let arr=this.getWindowsByGroup(window.group)
if(!isMulti) {
if(arr.length==0) {
this.windowList.push(window)
this.setFocus(window.id)
if(window.onOpen) {
window.onOpen(window);
}
} else {
if(window.type==ETeamOS_Window_Type.SIMPLE) {
this.setFocus(arr[0].id)
} else {
let index=0,size=this.windowList[0].nodes.length;
for(let i=1;i<this.windowList.length;i++) {
if(this.windowList[i].nodes.length<size) {
size=this.windowList[i].nodes.length;
index=i;
}
}
this.windowList[index].nodes.push(...window.nodes)
this.windowList[index].activeKey=window.nodes[0].id;
this.setFocus(this.windowList[index].id)
if(window.onOpen) {
window.onOpen(window);
}
}
}
} else {
this.windowList.push(window)
this.setFocus(window.id)
if(window.onOpen) {
window.onOpen(window);
}
}
}
removeById(id:string) {
for(let i=0;i<this.windowList.length;i++) {
if(this.windowList[i].id==id) {
this.windowList.splice(i,1)
break
}
}
}
getById(id:string) {
for(let i=0;i<this.windowList.length;i++) {
if(this.windowList[i].id==id) {
return this.windowList[i];
}
}
return null;
}
getList() {
return this.windowList
}
setFocus(id:string) {
let obj:Window;
for(let i=0;i<this.windowList.length;i++) {
if(this.windowList[i].id==id) {
this.windowList[i].isFocus=true
obj=this.windowList[i];
} else {
this.windowList[i].isFocus=false
}
}
return obj;
}
getFocused() {
let obj:Window;
for(let i=0;i<this.windowList.length;i++) {
if(this.windowList[i].isFocus) {
obj=this.windowList[i];
break;
}
}
return obj;
}
max(id:string){
let obj=this.setFocus(id);
if(obj) {
obj.rect.left="0%"
obj.rect.top="0%"
obj.rect.width="100%"
obj.rect.height="100%"
obj.status=ETeamOS_Window_Status.MAX
}
}
normal(id:string){
let obj=this.setFocus(id);
if(obj) {
obj.rect.left="10%"
obj.rect.top="10%"
obj.rect.width="80%"
obj.rect.height="80%"
obj.status=ETeamOS_Window_Status.NORMAL
}
}
hide(id:string) {
let obj=this.getById(id);
if(obj) {
obj.status=ETeamOS_Window_Status.MIN
if(obj.isFocus) {
obj.isFocus=false
}
}
}
show(id:string) {
let obj=this.getById(id);
if(obj) {
obj.status=ETeamOS_Window_Status.NORMAL
obj.rect.width="80%"
obj.rect.height="80%"
}
this.setFocus(id);
}
close(id:string) {
this.removeById(id);
}
getWindowsByGroup(group:string):Window[] {
let arr:Window[]=[];
for(let i=0;i<this.windowList.length;i++) {
if(this.windowList[i].group===group) {
arr.push(this.windowList[i]);
}
}
return arr;
}
}
export const windowManager=new WindowManager()

41
code/client/src/test.vue Normal file
View File

@ -0,0 +1,41 @@
<template>
<div style="color: rebeccapurple">
efewfwefew<h1>{{name}}</h1>{{aa}}
<button @click="onClick" draggable="true">133</button>
<button @click="go" draggable="true">go</button>
<a-modal v-model:visible="visible" :render-to-body="false" style="position: absolute">
<template #title>
Title
</template>
<div>You can cusstomize modal body text by the current situation. This modal will be closed immediately once you press the OK button.</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import {reactive, ref} from "vue"
import {getCurrentNavigator, getCurrentNavigatorManager} from "./teamOS/common/component/navigator/navigator";
import {useStore} from "./business/common/store/store";
import {testStore} from "./testStore";
defineProps<{
name?:string
}>()
let visible=ref(false)
const onClick=()=>{
visible.value=true
}
let obj=getCurrentNavigator()
const go=()=>{
obj.push("test1")
}
let aa=reactive<any>({})
aa.name=123
aa.aaa="fdf"
let store=useStore(getCurrentNavigatorManager().id,testStore);
console.log(store.name);
</script>
<style scoped>
</style>

44
code/client/src/test1.vue Normal file
View File

@ -0,0 +1,44 @@
<template>
<div style="color: rebeccapurple">
111111
<button @click="onClick" draggable="true">back</button>
<button @click="push" draggable="true">push</button>
<button @click="go" draggable="true">go</button>
<a-modal v-model:visible="visible" :render-to-body="false" style="position: absolute">
<template #title>
Title
</template>
<div>You can cusstomize modal body text by the current situation. This modal will be closed immediately once you press the OK button.</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import {inject, onMounted, ref} from "vue"
import {getCurrentNavigatorMeta, Navigator} from "./teamOS/common/component/navigator/navigator";
let visible=ref(false)
let obj=inject("navigator",null) as Navigator;
const onClick=()=>{
obj.back();
}
const go=()=>{
obj.go()
}
const push=()=>{
obj.push("test3",{
time:Date.now()
})
}
let meta=getCurrentNavigatorMeta<string>()
if(meta) {
meta.title="eee"
}
onMounted(()=>{
console.log("mounted test1")
})
</script>
<style scoped>
</style>

33
code/client/src/test3.vue Normal file
View File

@ -0,0 +1,33 @@
<template>
<div>zzzzzzz{{time}}
<button @click="onClick" draggable="true">home</button>
<NavigatorContainer :routes="router" :default="{name:'test4'}" name="nav1"></NavigatorContainer>
</div>
</template>
<script setup lang="ts">
import {inject, markRaw, onMounted} from "vue";
import {Navigator} from "./teamOS/common/component/navigator/navigator";
import NavigatorContainer from "./teamOS/common/component/navigator/navigatorContainer.vue";
import Test4 from "./test4.vue";
import Test5 from "./test5.vue";
defineProps<{
time?:number
}>()
let obj=inject("navigator",null) as Navigator;
const onClick=()=>{
obj.replace("test")
}
let router={
test4:markRaw(Test4),
test5:markRaw(Test5)
}
onMounted(()=>{
console.log("mounted test3")
})
</script>
<style scoped>
</style>

19
code/client/src/test4.vue Normal file
View File

@ -0,0 +1,19 @@
<template>
<div style="background-color: blanchedalmond">7777777 <button v-navigator:[navigator]="{name:'test5',props:{name:'xxxxxxxxx'}}">click</button></div>
</template>
<script setup lang="ts">
import {inject} from "vue";
import {getCurrentNavigator, Navigator, onNavigatorShow} from "./teamOS/common/component/navigator/navigator";
import {vNavigator} from "./teamOS/common/directive/navigator";
vNavigator;
let navigator=getCurrentNavigator();
onNavigatorShow((action)=>{
console.log(action)
})
</script>
<style scoped>
</style>

23
code/client/src/test5.vue Normal file
View File

@ -0,0 +1,23 @@
<template>
<div style="background-color: cornflowerblue">55555555{{name}}</div>
</template>
<script setup lang="ts">
import {getCurrentNavigatorManager, getCurrentNavigatorMeta} from "./teamOS/common/component/navigator/navigator";
import {useStore} from "./business/common/store/store";
import {testStore} from "./testStore";
defineProps<{
name?:string
}>()
let meta=getCurrentNavigatorMeta<string>()
if(meta) {
meta.title="11"
}
let store=useStore(getCurrentNavigatorManager().id,testStore)
store.aaa();
</script>
<style scoped>
</style>

View File

@ -0,0 +1,12 @@
export const testStore={
state:()=>{
return {
name:"rty"
}
},
actions:{
aaa(){
console.log("aaa")
}
}
}

View File

@ -1,5 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
}

26
code/client/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noImplicitUseStrict":true,
"sourceMap": true,
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"noImplicitOverride": true,
"noImplicitThis": true,
"plugins": [
{ "transform": "../common/transform/transformer.js" }
]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@ -0,0 +1,32 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// typescript({
// // @ts-ignore
// typescript:ttypescript
// })
],
server:{
port: 3000,
hmr:true,
open: false, //自动打开
base: "./ ", //生产环境路径
proxy: { // 本地开发环境通过代理实现跨域,生产环境使用 nginx 转发
// 正则表达式写法
'^/api': {
target: 'http://localhost:14000/api', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/api/, '')
},
'^/file': {
target: 'http://localhost:14000/file', // 后端服务实际地址
changeOrigin: true, //开启代理
rewrite: (path) => path.replace(/^\/file/, '')
}
}
},
})

View File

@ -1,14 +0,0 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@ -1,6 +0,0 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = '/teamlinker'
VUE_APP_BASE_DOMAIN = 'http://175.27.166.37:13000'

View File

@ -1,7 +0,0 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = '/'
VUE_APP_BASE_DOMAIN = 'http://175.27.166.37:13000'

View File

@ -1,8 +0,0 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/teamlinker'

View File

@ -1,4 +0,0 @@
build/*.js
src/assets
public
dist

View File

@ -1,198 +0,0 @@
// module.exports = {
// root: true,
// parserOptions: {
// parser: 'babel-eslint',
// sourceType: 'module'
// },
// env: {
// browser: true,
// node: true,
// es6: true,
// },
// extends: ['plugin:vue/recommended', 'eslint:recommended'],
// // add your custom rules here
// //it is base on https://github.com/vuejs/eslint-config-vue
// rules: {
// "vue/max-attributes-per-line": [2, {
// "singleline": 10,
// "multiline": {
// "max": 1,
// "allowFirstLine": false
// }
// }],
// "vue/singleline-html-element-content-newline": "off",
// "vue/multiline-html-element-content-newline":"off",
// "vue/name-property-casing": ["error", "PascalCase"],
// "vue/no-v-html": "off",
// 'accessor-pairs': 2,
// 'arrow-spacing': [2, {
// 'before': true,
// 'after': true
// }],
// 'block-spacing': [2, 'always'],
// 'brace-style': [2, '1tbs', {
// 'allowSingleLine': true
// }],
// 'camelcase': [0, {
// 'properties': 'always'
// }],
// 'comma-dangle': [2, 'never'],
// 'comma-spacing': [2, {
// 'before': false,
// 'after': true
// }],
// 'comma-style': [2, 'last'],
// 'constructor-super': 2,
// 'curly': [2, 'multi-line'],
// 'dot-location': [2, 'property'],
// 'eol-last': 2,
// 'eqeqeq': ["error", "always", {"null": "ignore"}],
// 'generator-star-spacing': [2, {
// 'before': true,
// 'after': true
// }],
// 'handle-callback-err': [2, '^(err|error)$'],
// 'indent': [2, 2, {
// 'SwitchCase': 1
// }],
// 'jsx-quotes': [2, 'prefer-single'],
// 'key-spacing': [2, {
// 'beforeColon': false,
// 'afterColon': true
// }],
// 'keyword-spacing': [2, {
// 'before': true,
// 'after': true
// }],
// 'new-cap': [2, {
// 'newIsCap': true,
// 'capIsNew': false
// }],
// 'new-parens': 2,
// 'no-array-constructor': 2,
// 'no-caller': 2,
// 'no-console': 'off',
// 'no-class-assign': 2,
// 'no-cond-assign': 2,
// 'no-const-assign': 2,
// 'no-control-regex': 0,
// 'no-delete-var': 2,
// 'no-dupe-args': 2,
// 'no-dupe-class-members': 2,
// 'no-dupe-keys': 2,
// 'no-duplicate-case': 2,
// 'no-empty-character-class': 2,
// 'no-empty-pattern': 2,
// 'no-eval': 2,
// 'no-ex-assign': 2,
// 'no-extend-native': 2,
// 'no-extra-bind': 2,
// 'no-extra-boolean-cast': 2,
// 'no-extra-parens': [2, 'functions'],
// 'no-fallthrough': 2,
// 'no-floating-decimal': 2,
// 'no-func-assign': 2,
// 'no-implied-eval': 2,
// 'no-inner-declarations': [2, 'functions'],
// 'no-invalid-regexp': 2,
// 'no-irregular-whitespace': 2,
// 'no-iterator': 2,
// 'no-label-var': 2,
// 'no-labels': [2, {
// 'allowLoop': false,
// 'allowSwitch': false
// }],
// 'no-lone-blocks': 2,
// 'no-mixed-spaces-and-tabs': 2,
// 'no-multi-spaces': 2,
// 'no-multi-str': 2,
// 'no-multiple-empty-lines': [2, {
// 'max': 1
// }],
// 'no-native-reassign': 2,
// 'no-negated-in-lhs': 2,
// 'no-new-object': 2,
// 'no-new-require': 2,
// 'no-new-symbol': 2,
// 'no-new-wrappers': 2,
// 'no-obj-calls': 2,
// 'no-octal': 2,
// 'no-octal-escape': 2,
// 'no-path-concat': 2,
// 'no-proto': 2,
// 'no-redeclare': 2,
// 'no-regex-spaces': 2,
// 'no-return-assign': [2, 'except-parens'],
// 'no-self-assign': 2,
// 'no-self-compare': 2,
// 'no-sequences': 2,
// 'no-shadow-restricted-names': 2,
// 'no-spaced-func': 2,
// 'no-sparse-arrays': 2,
// 'no-this-before-super': 2,
// 'no-throw-literal': 2,
// 'no-trailing-spaces': 2,
// 'no-undef': 2,
// 'no-undef-init': 2,
// 'no-unexpected-multiline': 2,
// 'no-unmodified-loop-condition': 2,
// 'no-unneeded-ternary': [2, {
// 'defaultAssignment': false
// }],
// 'no-unreachable': 2,
// 'no-unsafe-finally': 2,
// 'no-unused-vars': [2, {
// 'vars': 'all',
// 'args': 'none'
// }],
// 'no-useless-call': 2,
// 'no-useless-computed-key': 2,
// 'no-useless-constructor': 2,
// 'no-useless-escape': 0,
// 'no-whitespace-before-property': 2,
// 'no-with': 2,
// 'one-var': [2, {
// 'initialized': 'never'
// }],
// 'operator-linebreak': [2, 'after', {
// 'overrides': {
// '?': 'before',
// ':': 'before'
// }
// }],
// 'padded-blocks': [2, 'never'],
// 'quotes': [2, 'single', {
// 'avoidEscape': true,
// 'allowTemplateLiterals': true
// }],
// 'semi': [2, 'never'],
// 'semi-spacing': [2, {
// 'before': false,
// 'after': true
// }],
// 'space-before-blocks': [2, 'always'],
// 'space-before-function-paren': [2, 'never'],
// 'space-in-parens': [2, 'never'],
// 'space-infix-ops': 2,
// 'space-unary-ops': [2, {
// 'words': true,
// 'nonwords': false
// }],
// 'spaced-comment': [2, 'always', {
// 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
// }],
// 'template-curly-spacing': [2, 'never'],
// 'use-isnan': 2,
// 'valid-typeof': 2,
// 'wrap-iife': [2, 'any'],
// 'yield-star-spacing': [2, 'both'],
// 'yoda': [2, 'never'],
// 'prefer-const': 2,
// 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
// 'object-curly-spacing': [2, 'always', {
// objectsInObjects: false
// }],
// 'array-bracket-spacing': [2, 'never']
// }
// }

View File

@ -1,16 +0,0 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
tests/**/coverage/
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@ -1,5 +0,0 @@
language: node_js
node_js: 10
script: npm run test
notifications:
email: false

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017-present PanJiaChen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,102 +0,0 @@
# vue-admin-template
> 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint这些搭建后台必要的东西。
[线上地址](http://panjiachen.github.io/vue-admin-template)
[国内访问](https://panjiachen.gitee.io/vue-admin-template)
目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`
## Extra
如果你想要根据用户角色来动态生成侧边栏和 router你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
## 相关项目
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目:
- [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2)
- [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac)
- [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35)
- [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56)
- [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836)
## Build Setup
```bash
# 克隆项目
git clone https://github.com/PanJiaChen/vue-admin-template.git
# 进入项目目录
cd vue-admin-template
# 安装依赖
npm install
# 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题
npm install --registry=https://registry.npm.taobao.org
# 启动服务
npm run dev
```
浏览器访问 [http://localhost:9528](http://localhost:9528)
## 发布
```bash
# 构建测试环境
npm run build:stage
# 构建生产环境
npm run build:prod
```
## 其它
```bash
# 预览发布环境效果
npm run preview
# 预览发布环境效果 + 静态资源分析
npm run preview -- --report
# 代码格式检查
npm run lint
# 代码格式检查并自动修复
npm run lint -- --fix
```
更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/)
## 购买贴纸
你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。
## Demo
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
## Browsers support
Modern browsers and Internet Explorer 10+.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| --------- | --------- | --------- | --------- |
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
## License
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
Copyright (c) 2017-present PanJiaChen

View File

@ -1,90 +0,0 @@
# vue-admin-template
English | [简体中文](./README-zh.md)
> A minimal vue admin template with Element UI & axios & iconfont & permission control & lint
**Live demo:** http://panjiachen.github.io/vue-admin-template
**The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`**
## Build Setup
```bash
# clone the project
git clone https://github.com/PanJiaChen/vue-admin-template.git
# enter the project directory
cd vue-admin-template
# install dependency
npm install
# develop
npm run dev
```
This will automatically open http://localhost:9528
## Build
```bash
# build for test environment
npm run build:stage
# build for production environment
npm run build:prod
```
## Advanced
```bash
# preview the release environment effect
npm run preview
# preview the release environment effect + static resource analysis
npm run preview -- --report
# code format check
npm run lint
# code format check and auto fix
npm run lint -- --fix
```
Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information
## Demo
![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif)
## Extra
If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control)
For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour))
## Related Project
- [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin)
- [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin)
- [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template)
- [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312)
## Browsers support
Modern browsers and Internet Explorer 10+.
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| --------- | --------- | --------- | --------- |
| IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions
## License
[MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license.
Copyright (c) 2017-present PanJiaChen

View File

@ -1,14 +0,0 @@
module.exports = {
presets: [
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
'@vue/cli-plugin-babel/preset'
],
'env': {
'development': {
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
// https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
'plugins': ['dynamic-import-node']
}
}
}

View File

@ -1,35 +0,0 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9526
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}

View File

@ -1,24 +0,0 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/tests/unit/coverage',
// 'collectCoverage': true,
'coverageReporters': [
'lcov',
'text-summary'
],
testURL: 'http://localhost/'
}

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@ -1,57 +0,0 @@
const Mock = require('mockjs')
const { param2Obj } = require('./utils')
const user = require('./user')
const table = require('./table')
const mocks = [
...user,
...table
]
// for front mock
// please use it cautiously, it will redefine XMLHttpRequest,
// which will cause many of your third-party libraries to be invalidated(like progress event).
function mockXHR() {
// mock patch
// https://github.com/nuysoft/Mock/issues/300
Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send
Mock.XHR.prototype.send = function() {
if (this.custom.xhr) {
this.custom.xhr.withCredentials = this.withCredentials || false
if (this.responseType) {
this.custom.xhr.responseType = this.responseType
}
}
this.proxy_send(...arguments)
}
function XHR2ExpressReqWrap(respond) {
return function(options) {
let result = null
if (respond instanceof Function) {
const { body, type, url } = options
// https://expressjs.com/en/4x/api.html#req
result = respond({
method: type,
body: JSON.parse(body),
query: param2Obj(url)
})
} else {
result = respond
}
return Mock.mock(result)
}
}
for (const i of mocks) {
Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))
}
}
module.exports = {
mocks,
mockXHR
}

View File

@ -1,92 +0,0 @@
/*
* @Descripttion:
* @version:
* @Author: gqwu
* @Date: 2021-10-09 11:08:42
* @LastEditors: gqwu
* @LastEditTime: 2021-10-09 11:21:29
*/
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const Mock = require('mockjs')
const mockDir = path.join(process.cwd(), 'mock')
function registerRoutes(app) {
let mockLastIndex
const { mocks } = require('./index.js')
const mocksForServer = mocks.map(route => {
return responseFake(route.url, route.type, route.response)
})
for (const mock of mocksForServer) {
// app[mock.type](mock.url, mock.response)
app[mock.type](mock.url, bodyParser.json(), bodyParser.urlencoded({ // 添加
extended: true
}), mock.response)
mockLastIndex = app._router.stack.length
}
const mockRoutesLength = Object.keys(mocksForServer).length
return {
mockRoutesLength: mockRoutesLength,
mockStartIndex: mockLastIndex - mockRoutesLength
}
}
function unregisterRoutes() {
Object.keys(require.cache).forEach(i => {
if (i.includes(mockDir)) {
delete require.cache[require.resolve(i)]
}
})
}
// for mock server
const responseFake = (url, type, respond) => {
return {
url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),
type: type || 'get',
response(req, res) {
console.log('request invoke:' + req.path)
res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
}
}
}
module.exports = app => {
// parse app.body
// https://expressjs.com/en/4x/api.html#req.body
// app.use(bodyParser.json())
// app.use(bodyParser.urlencoded({
// extended: true
// }))
const mockRoutes = registerRoutes(app)
var mockRoutesLength = mockRoutes.mockRoutesLength
var mockStartIndex = mockRoutes.mockStartIndex
// watch files, hot reload mock server
chokidar.watch(mockDir, {
ignored: /mock-server/,
ignoreInitial: true
}).on('all', (event, path) => {
if (event === 'change' || event === 'add') {
try {
// remove mock routes stack
app._router.stack.splice(mockStartIndex, mockRoutesLength)
// clear routes cache
unregisterRoutes()
const mockRoutes = registerRoutes(app)
mockRoutesLength = mockRoutes.mockRoutesLength
mockStartIndex = mockRoutes.mockStartIndex
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
} catch (error) {
console.log(chalk.redBright(error))
}
}
})
}

View File

@ -1,29 +0,0 @@
const Mock = require('mockjs')
const data = Mock.mock({
'items|30': [{
id: '@id',
title: '@sentence(10, 20)',
'status|1': ['published', 'draft', 'deleted'],
author: 'name',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}]
})
module.exports = [
{
url: '/vue-admin-template/table/list',
type: 'get',
response: config => {
const items = data.items
return {
code: 20000,
data: {
total: items.length,
items: items
}
}
}
}
]

View File

@ -1,92 +0,0 @@
/*
* @Descripttion:
* @version:
* @Author: gqwu
* @Date: 2021-10-09 11:08:42
* @LastEditors: gqwu
* @LastEditTime: 2021-10-09 11:15:50
*/
const tokens = {
admin: {
token: 'admin-token'
},
editor: {
token: 'editor-token'
}
}
const users = {
'admin-token': {
roles: ['admin'],
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
roles: ['editor'],
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
module.exports = [
// user login
{
url: '/api/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
},
// get user info
{
url: '/vue-admin-template/user/info\.*',
type: 'get',
response: config => {
const { token } = config.query
const info = users[token]
// mock error
if (!info) {
return {
code: 50008,
message: 'Login failed, unable to get user details.'
}
}
return {
code: 20000,
data: info
}
}
},
// user logout
{
url: '/vue-admin-template/user/logout',
type: 'post',
response: _ => {
return {
code: 20000,
data: 'success'
}
}
}
]

View File

@ -1,25 +0,0 @@
/**
* @param {string} url
* @returns {Object}
*/
function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}
module.exports = {
param2Obj
}

View File

@ -1,73 +0,0 @@
{
"name": "vue-admin-template",
"version": "4.4.0",
"description": "A vue admin template with Element UI & axios & iconfont & permission control & lint",
"author": "Pan <panfree23@gmail.com>",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml",
"lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit"
},
"dependencies": {
"axios": "0.18.1",
"core-js": "3.21.1",
"element-ui": "2.13.2",
"js-cookie": "2.2.0",
"jsplumb": "^2.15.6",
"moment": "^2.29.1",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"qs": "^6.10.3",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vuedraggable": "^2.24.3",
"vuex": "3.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.4",
"@vue/cli-plugin-eslint": "4.4.4",
"@vue/cli-plugin-unit-jest": "4.4.4",
"@vue/cli-service": "4.4.4",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "9.5.1",
"babel-eslint": "10.1.0",
"babel-jest": "23.6.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"mockjs": "1.0.1-beta3",
"runjs": "4.3.2",
"sass": "1.26.8",
"sass-loader": "8.0.2",
"script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "1.13.2",
"svg-sprite-loader": "4.1.3",
"svgo": "2.8.0",
"vue-template-compiler": "2.6.10",
"ts-loader": "^4.4.2",
"tslint": "^6.1.3",
"tslint-config-standard": "^9.0.0",
"tslint-loader": "^3.5.4",
"typescript": "^3.5.2",
"vue-property-decorator": "^9.1.2",
"vue-class-component": "^7.2.6"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"license": "MIT"
}

View File

@ -1,8 +0,0 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
'plugins': {
// to edit target browsers: use "browserslist" field in package.json
'autoprefixer': {}
}
}

Some files were not shown because too many files have changed in this diff Show More