This commit is contained in:
2026-02-04 15:08:56 +08:00
35 changed files with 1444 additions and 189 deletions

37
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@vue-flow/core": "^1.48.2",
"axios": "^1.3.6",
"crypto-js": "^4.2.0",
"dagre": "^0.8.5",
"element-plus": "^2.13.2",
"gsap": "^3.13.0",
"markdown-it": "^14.1.0",
@@ -2401,6 +2402,16 @@
"node": ">=12"
}
},
"node_modules/dagre": {
"version": "0.8.5",
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"license": "MIT",
"dependencies": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"node_modules/data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -3680,6 +3691,15 @@
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/gsap": {
"version": "3.13.0",
"resolved": "https://registry.npmmirror.com/gsap/-/gsap-3.13.0.tgz",
@@ -10373,6 +10393,15 @@
"d3-transition": "2 - 3"
}
},
"dagre": {
"version": "0.8.5",
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"requires": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"data-view-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
@@ -11359,6 +11388,14 @@
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
"graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmmirror.com/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"requires": {
"lodash": "^4.17.15"
}
},
"gsap": {
"version": "3.13.0",
"resolved": "https://registry.npmmirror.com/gsap/-/gsap-3.13.0.tgz",

View File

@@ -16,6 +16,7 @@
"@vue-flow/core": "^1.48.2",
"axios": "^1.3.6",
"crypto-js": "^4.2.0",
"dagre": "^0.8.5",
"element-plus": "^2.13.2",
"gsap": "^3.13.0",
"markdown-it": "^14.1.0",

View File

@@ -8,12 +8,10 @@
import { computed } from 'vue'
import { useGlobalStore } from '@/stores'
const globalStore = useGlobalStore()
const loading = computed(() => globalStore.state.loading)
const loading = computed(() => globalStore.state.loading || globalStore.state.view_loading)
</script>
<style lang="less">
#app {
font-size: 1.6rem;
}

View File

@@ -1,12 +1,18 @@
body,
html,
p,
div,
ul,
ol,
li {
li,
h1,
h2,
h3,
p {
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
@@ -22,28 +28,8 @@ body,
transform: rotate(360deg);
}
}
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.flex-1 {
flex: 1;
}
.flex-center {
align-items: center;
justify-content: center;
}
.flex-between {
justify-content: space-between;
}
.align-center {
align-items: center;
}
.relative {
position: relative;
}
.absolute {
position: absolute;
.background-pink {
background-color: #f8f7f5;
background-image: url('@/assets/images/home-bg.png');
background-size: 100% 100%;
}

View File

@@ -1,17 +1,40 @@
body,
html,
div,
ul,
li,
h1,
h2,
h3,
p {
margin: 0;
padding: 0;
}
* {
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
height: 100%;
overflow: hidden;
width: 100%;
height: 100%;
overflow: hidden;
}
@keyframes loading {
0% {
transform: rotate(0deg);
}
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
100% {
transform: rotate(360deg);
}
}
.background-pink {
background-color: rgba(248, 247, 245, 1);
background-image: url('@/assets/images/home-bg.png');
background-size: 100% 100%;
}

View File

@@ -0,0 +1,3 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.5424 6.70765C25.1525 7.31784 25.1525 8.30716 24.5424 8.91735L14.7097 18.75L24.5424 28.5826C25.1525 29.1928 25.1525 30.1822 24.5424 30.7924C23.9322 31.4025 22.9428 31.4025 22.3326 30.7924L11.3951 19.8549C10.785 19.2447 10.785 18.2553 11.3951 17.6451L22.3326 6.70765C22.9428 6.09745 23.9322 6.09745 24.5424 6.70765Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,103 @@
<template>
<div class="input-code">
<input
ref="inputRef"
type="tel"
maxlength="1"
v-for="(v, i) in props.length"
:key="i"
v-model="code[i]"
@input="handleInput(i)"
@keydown.delete="handleDelete(i)"
@paste="handlePaste"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, computed, watch, nextTick } from 'vue'
const emit = defineEmits(['submit', 'update:modelValue'])
const props = defineProps({
length: {
type: Number,
default: 6
}
})
const inputRef = ref('')
const code = ref([])
const codeStr = computed(() => code.value.join(''))
watch(codeStr, (newVal) => {
emit('update:modelValue', newVal)
})
const resetCode = (size) => {
code.value = []
for (let i = 0; i < size; i++) {
code.value.push('')
}
}
resetCode(props.length)
const handleInput = (index: number) => {
const value = code.value[index]
if (value) {
if (/[0-9]/.test(value)) {
code.value[index] = value
focusLast()
} else {
code.value[index] = ''
}
}
submit()
}
const handleDelete = (index: number) => {
if (code.value[index].length == 0) {
focusLast(-1)
}
}
const handlePaste = (e: ClipboardEvent) => {
const text = e.clipboardData?.getData('text')
if (text) {
const nums = text.match(/[0-9]/g) || []
if (nums.length === code.value.length) {
code.value = [...nums]
focusLast()
nextTick(submit)
}
}
}
// 聚焦最后一个没有输入的
const focusLast = (step = 0) => {
let index = code.value.findIndex((item) => !item) + step
index < 0 && (index = 0)
if (index >= 0 && index < props.length) {
inputRef.value[index]?.focus?.()
}
if (code.value.every((item) => item.length)) {
inputRef.value?.forEach((item) => item.blur?.())
}
}
const submit = () => {
if (codeStr.value.length === props.length) {
emit('submit', codeStr.value)
}
}
onMounted(() => {
focusLast()
})
</script>
<style lang="less" scoped>
.input-code {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
> input {
width: 7rem;
height: 7rem;
border-radius: 1rem;
border: 0.02rem solid #dfdfdf;
text-align: center;
margin: auto;
}
}
</style>

View File

@@ -7,53 +7,58 @@ import { createRouter, createWebHistory } from 'vue-router'
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
*/
const router = createRouter({
history: createWebHistory('/'),
// history: createWebHistory(import.meta.env.VITE_APP_URL),
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/',
name: 'index',
component: () => import('../views/login/index.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/login.vue')
},
{
path: '/home',
name: 'home',
component: () => import('../views/home/index.vue'),
children: [
{
path: 'test',
name: 'test',
component: () => import('../views/home/test.vue'),
meta: { topNavStyle: '2' }
},
{
path: '/home/versionTree',
name: 'versionTree',
component: () => import('../views/home/versionTree.vue'),
meta: { topNavStyle: '2' }
},
{
path: 'mainInput',
name: 'mainInput',
component: () => import('../views/home/mainInput.vue')
}
]
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue')
}
]
history: createWebHistory('/'),
// history: createWebHistory(import.meta.env.VITE_APP_URL),
routes: [
{
path: '/',
redirect: '/index'
},
{
path: '/index',
name: 'index',
component: () => import('../views/login/index.vue'),
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/login.vue'),
},
{
path: '/register',
name: 'register',
component: () => import('../views/login/register.vue'),
},
{
path: '/home',
name: 'home',
component: () => import('../views/home/index.vue'),
children: [
{
path: 'test',
name: 'test',
component: () => import('../views/home/test.vue'),
meta: { topNavStyle: '2' }
},
{
path: '/home/versionTree',
name: 'versionTree',
component: () => import('../views/home/versionTree.vue'),
meta: { topNavStyle: '2' }
},
{
path: 'mainInput',
name: 'mainInput',
component: () => import('../views/home/mainInput.vue')
}
]
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue'),
}
]
})
export default router

View File

@@ -1,8 +1,11 @@
import router from './index'
import { useGlobalStore } from '@/stores/global'
router.beforeEach((to, from, next) => {
useGlobalStore().setViewLoading(true)
next()
})
router.afterEach(() => {
useGlobalStore().setViewLoading(false)
})

View File

@@ -3,15 +3,18 @@ import { ref, computed } from 'vue'
export const useGlobalStore = defineStore('global', () => {
const state = ref({
loading: false,// 全局loading
view_loading: false,// 页面跳转loading
homeLeftNavCollapse: false,// 首页左侧导航是否折叠
})
const setLoading = (v: boolean) => { state.value.loading = v }
const setViewLoading = (v: boolean) => { state.value.view_loading = v }
const setHomeLeftNavCollapse = (v: boolean) => { state.value.homeLeftNavCollapse = v }
return {
state,
setLoading,
setViewLoading,
setHomeLeftNavCollapse,
}
})

View File

@@ -102,6 +102,18 @@ export function FormatDate(value: Date | number | string, format: string = 'yyyy
return str;
}
/**
* 倒计时
* @param time 倒计时时间,单位秒
* @returns 倒计时字符串,格式为 mm:ss
*/
export function CountDown(time: number) {
const mm = String(Math.floor(time / 60)).padStart(2, '0');
const ss = String(time % 60).padStart(2, '0');
return `${mm}:${ss}`;
}
/**
* 下载图片

184
src/views/css/style.css Normal file
View File

@@ -0,0 +1,184 @@
.register,
.login {
width: 100%;
height: 100%;
overflow: hidden;
padding: 2.5rem;
display: flex;
}
.register > .left,
.login > .left {
flex: 1;
height: 100%;
position: relative;
overflow: hidden;
}
.register > .left > .bg,
.login > .left > .bg {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 2rem;
}
.register > .left > .logo,
.login > .left > .logo {
position: absolute;
top: 2.4rem;
left: 4.2rem;
}
.register > .left > .logo > img,
.login > .left > .logo > img {
width: 6rem;
height: auto;
}
.register > .left > .logo > span,
.login > .left > .logo > span {
font-weight: 600;
font-size: 3.3rem;
}
.register > .right,
.login > .right {
width: 90rem;
min-width: 600px;
display: flex;
flex-direction: column;
}
.register > .right > .top,
.login > .right > .top {
display: flex;
padding: 0 3rem;
}
.register > .right > .top > .back,
.login > .right > .top > .back {
width: 5rem;
height: 5rem;
border-radius: 1.2rem;
border: 0.25rem solid #dfdfdf;
background-color: transparent;
cursor: pointer;
}
.register > .right > .box,
.login > .right > .box {
min-width: 50rem;
flex: 1;
overflow-y: auto;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.register > .right > .box > img,
.login > .right > .box > img {
width: 11rem;
height: auto;
margin-top: 2rem;
}
.register > .right > .box > .visible-code,
.login > .right > .box > .visible-code {
margin-top: 1.7rem;
margin-bottom: 7.2rem;
}
.register > .right > .box > .title,
.login > .right > .box > .title {
font-weight: 600;
font-size: 7rem;
display: flex;
align-items: center;
justify-content: center;
color: #252727;
margin-top: 1.7rem;
}
.register > .right > .box > .title > img,
.login > .right > .box > .title > img {
width: auto;
height: 9.8rem;
}
.register > .right > .box > .tip,
.login > .right > .box > .tip {
font-weight: 400;
font-family: General Sans Variable;
font-style: Regular;
font-size: 1.8rem;
color: #666;
margin-top: 0.4rem;
}
.register > .right > .box > .el-form,
.login > .right > .box > .el-form {
margin-top: 5rem;
width: 100%;
}
.register > .right > .box > .el-form::v-deep .el-form-item,
.login > .right > .box > .el-form::v-deep .el-form-item {
margin-bottom: 2rem;
}
.register > .right > .box > .el-form::v-deep .el-form-item__label,
.login > .right > .box > .el-form::v-deep .el-form-item__label {
color: #252727;
font-size: 1.8rem;
margin-bottom: 0.8rem;
}
.register > .right > .box > .el-form::v-deep .el-input,
.login > .right > .box > .el-form::v-deep .el-input {
--el-input-height: 5rem;
--el-input-border-radius: 0.8rem;
--el-input-text-color: #252727;
--el-border-color: #dfdfdf;
font-size: 1.4rem;
}
.register > .right > .box > .el-form::v-deep .forgetPassword,
.login > .right > .box > .el-form::v-deep .forgetPassword {
margin-top: -1.2rem;
margin-bottom: 2rem;
font-size: 1.6rem;
text-align: right;
color: #666666;
cursor: pointer;
text-decoration: underline;
}
.register > .right > .box > .el-form::v-deep .privacy,
.login > .right > .box > .el-form::v-deep .privacy {
--el-checkbox-height: auto;
margin-bottom: 4rem;
}
.register > .right > .box > .el-form::v-deep .privacy .el-checkbox__label,
.login > .right > .box > .el-form::v-deep .privacy .el-checkbox__label {
font-size: 1.6rem;
color: #666666;
font-weight: 400;
}
.register > .right > .box > .el-form::v-deep .privacy .el-checkbox__label > span,
.login > .right > .box > .el-form::v-deep .privacy .el-checkbox__label > span {
text-decoration: underline;
cursor: pointer;
}
.register > .right > .box > .el-form::v-deep .el-form-item__error,
.login > .right > .box > .el-form::v-deep .el-form-item__error {
padding-top: 1px;
font-size: 1.4rem;
}
.register > .right > .box > .el-form::v-deep .submit,
.login > .right > .box > .el-form::v-deep .submit {
width: 100%;
height: 6rem;
background: #252727;
font-size: 2rem;
border-radius: 0.8rem;
color: #fff;
font-weight: 600;
}
.register > .right > .box > .tip-2,
.login > .right > .box > .tip-2 {
font-weight: 400;
font-size: 1.6rem;
color: #666;
}
.register > .right > .box > .tip-2 > span,
.login > .right > .box > .tip-2 > span {
text-decoration: underline;
color: #FF7A50;
cursor: pointer;
}
.register > .right > .box > .other-login,
.login > .right > .box > .other-login {
margin-top: 5rem;
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="home">
<div class="home background-pink">
<left-nav />
<div class="right-main">
<top-nav />
@@ -23,9 +23,6 @@
height: 100%;
overflow: hidden;
display: flex;
background-color: rgba(248, 247, 245, 1);
background-image: url('@/assets/images/home-bg.png');
background-size: 100% 100%;
user-select: none;
> .right-main {
flex: 1;

View File

@@ -11,10 +11,10 @@
<span class="icon"><svg-icon name="add" size="16" /></span>
<span v-show="!isCollapse" class="text">New Project</span>
</button>
<div class="menu-item" @click="onHome">
<!-- <div class="menu-item" @click="onHome">
<span class="icon"><svg-icon name="home" size="24" /></span>
<span class="title" v-show="!isCollapse">Home</span>
</div>
</div> -->
<div class="menu-item" @click="onHistory" :class="{ active: showHistory }">
<span class="icon"><svg-icon name="history" size="24" /></span>
<span class="title" v-show="!isCollapse">History</span>
@@ -189,7 +189,6 @@
justify-content: space-between;
align-items: center;
padding: 0 0.8rem;
box-sizing: border-box;
}
> .title {
font-weight: 600;

View File

@@ -14,7 +14,7 @@ const props = defineProps({
})
//const emit = defineEmits([
//])
const treeState = ref(false)//
const treeState = ref(true)//
const openTree = ()=>{
treeState.value = !treeState.value
@@ -65,6 +65,13 @@ const {} = toRefs(data);
<style lang="less" scoped>
.versionTree{
--border-radius: 1rem;
--treeItem-width: 5.4rem;
--treeItem-height: 5.4rem;
--treeItem-raduis: 50%;
--treeItem-border: 2px solid #000;
--treeItem-background: #ffffff;
--treeItem-active-background: #e6e6e6;
:deep(.versionTreeBody){
--el-drawer-padding-primary: 0rem;
display: flex;

View File

@@ -2,6 +2,7 @@
import { ref, onMounted, onUnmounted, reactive, toRefs, watch } from "vue";
import view1Item from './view1Item.vue'
import view2 from './view2/index.vue'
import { versionsList } from './view2/tools/versionsData'
const props = defineProps({
treeState:{
@@ -22,6 +23,11 @@ watch(()=>props.treeState,(newVal,oldVal)=>{
},250)
})
const view2Ref = ref(null)
const pushView2Item = (item)=>{
view2Ref.value.push(item)
}
const view1List = ref([
{
name:'P1',
@@ -32,6 +38,8 @@ const view1List = ref([
}
])
onMounted(()=>{
// addView2Item()
view2Ref.value.init(versionsList)
})
onUnmounted(()=>{
})
@@ -40,11 +48,11 @@ const {} = toRefs(data);
</script>
<template>
<div class="tree" v-show="treeStateTime">
<div v-if="!treeState" class="box view1">
<div v-show="!treeState" class="box view1">
<view1Item v-for="item in view1List" :key="item.name" :item="item"></view1Item>
</div>
<div v-else class="box view2">
<view2 :list="view1List"></view2>
<div v-show="treeState" class="box view2">
<view2 ref="view2Ref"></view2>
</div>
</div>
</template>

View File

@@ -25,17 +25,18 @@ const {} = toRefs(data);
<style lang="less" scoped>
.btn{
font-size: 1.2rem;
width: 5rem;
height: 5rem;
width: var(--treeItem-width);
height: var(--treeItem-height);
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 2px solid #000;
border-radius: var(--treeItem-raduis);
border: var(--treeItem-border);
color: #000;
cursor: pointer;
margin-bottom: 4rem;
background-color: var(--treeItem-background);
position: relative;
&::after{
content: '';
@@ -53,7 +54,7 @@ const {} = toRefs(data);
background-origin: border-box;
}
&.active{
background-color: #e6e6e6;
background-color: var(--treeItem-active-background);
}
}
.btn:nth-child(1){

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import { ref, onMounted, onUnmounted, reactive, nextTick } from "vue";
import type { Node, Edge } from '@vue-flow/core'
import { VueFlow } from '@vue-flow/core'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import SpecialEdge from './speciaiEdge.vue'
import SpecialNode from './speciaiNode.vue'
import PrimaryNode from './primaryNode.vue'//主
import SecondaryNode from './secondaryNode.vue'//分支
import { useLayout } from './tools/tools'
const props = defineProps({
item: {
type: Object,
@@ -12,42 +14,110 @@ const props = defineProps({
})
//const emit = defineEmits([
//])
let data = reactive({
})
let selectId = ref(2)
const isLoad = ref(false)
// 节点类型input、output、default、custom
// input:开始点output结尾点default普通节点custom自定义节点
const position = { x: 0, y: 0 }
const nodes = ref<Node[]>([
{ id: '1', type: 'input', label: 'Node 1', position: { x: 250, y: 0 } },
{ id: '2', type: 'output', label: 'Node 2', position: { x: 100, y: 100 } },
{ id: '3', type: 'custom', label: 'Node 3', position: { x: 400, y: 100 } },
{ id: '4', type: 'custom', label: 'Node 3', position: { x: 400, y: 200 } },
// { id: '1', type: 'input', label: 'Node 1', class: 'custom-node start', position, sourcePosition: 'bottom' },
// { id: '2', type: 'PrimaryNode', class: 'custom-node', data: { id: '主 1' }, position },
// { id: '2-1', type: 'SecondaryNode', class: 'custom-node', data: { id: '分 1-1' }, position },
// { id: '3', type: 'PrimaryNode', class: 'custom-node', data: { id: '主 2' }, position },
])
// 边类型custom、default
// custom自定义边default普通边step直角边smoothstep平滑边
const edges = ref<Edge[]>([
{ id: 'e1-2', source: '1', target: '2', type: 'custom' },
{ id: 'e1-3', source: '1', target: '3', animated: true },
{ id: 'e1-4', source: '1', target: '4', },
// { id: 'e1-2', source: '1', target: '2', type: 'smoothstep' },
// { id: 'e1-3', source: '2', target: '2-1', type: 'smoothstep', sourceHandle:'right',},
// { id: 'e1-4', source: '2', target: '3', type: 'smoothstep',sourceHandle:'bottom', animated: true },
])
const { fitView } = useVueFlow()
const { layout } = useLayout()
async function layoutGraph(direction) {
setTimeout(() => {
nodes.value = layout(nodes.value, edges.value, direction)
console.log(nodes.value)
nextTick(() => {
fitView()
})
}, 0)
}
let elIndex = 1
const push = (item)=>{
if(nodes.value.length == 0){
nodes.value.push({ id: '0', type: 'input', label: 'Node 1', class: 'custom-node start', position, sourcePosition: 'bottom' })
}
let className = 'custom-node'
let id = item.id
let target = edges.value.length == 0?'0':item.id.slice(0, -2)
nodes.value.push({id,type:'SecondaryNode',class:className,position,data:item})
edges.value.push({ id, source: id, target, type: 'smoothstep' })
console.log()
}
function traverseArray(items, callback) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
callback(item, i)
if (item.child && Array.isArray(item.child) && item.child.length > 0) {
traverseArray(item.child, callback)
}
}
}
const init = (list)=>{
isLoad.value = false
traverseArray(list, (item, index) => {
console.log()
push(item)
})
isLoad.value = true
console.log(nodes.value,edges.value)
}
//是否可拖动节点
const nodesDraggable = ref(false)
const toggleNodesDraggable = () => {
nodesDraggable.value = !nodesDraggable.value
}
const handleVueFlowNodeClick = (node: Node) => {
console.log(node)
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
defineExpose({init,push})
// const {} = toRefs(data);
</script>
<template>
<div class="view2">
<VueFlow :nodes="nodes" :edges="edges">
<!-- bind your custom node type to a c omponent by using slots, slot names are always `node-<type>` -->
<template #node-custom="nodeProps">
<SpecialNode v-bind="nodeProps" />
</template>
<div class="vueFlowBox" v-if="isLoad">
<div @click="toggleNodesDraggable">拖拽节点</div>
<VueFlow :nodes="nodes" @nodes-initialized="layoutGraph('LR')" :edges="edges" @node-click="handleVueFlowNodeClick" :nodes-draggable="nodesDraggable">
<template #node-PrimaryNode="nodeProps">
<PrimaryNode v-bind="nodeProps" />
</template>
<template #node-SecondaryNode="nodeProps">
<SecondaryNode
v-bind="nodeProps"
:selectId="selectId"
/>
</template>
<!-- <template #edge-custom="edgeProps">
<SpecialEdge v-bind="edgeProps" />
</template> -->
</VueFlow>
<!-- <template #edge-custom="edgeProps">
<SpecialEdge v-bind="edgeProps" />
</template> -->
</VueFlow>
</div>
</div>
</template>
<style lang="less">
@@ -59,5 +129,48 @@ const {} = toRefs(data);
width: 100%;
height: 100%;
overflow: hidden;
>.vueFlowBox{
width: 100%;
height: 100%;
overflow: hidden;
}
:deep(.custom-node){
--vf-handle: #000;
--vf-node-color: #000;
--vf-box-shadow: #000;
font-size: 1.2rem;
width: var(--treeItem-width);
height: var(--treeItem-height);
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--treeItem-raduis);
border: var(--treeItem-border);
color: #000;
cursor: pointer;
background-color: var(--treeItem-background);
box-sizing: border-box;
.vue-flow__handle-right{
transform: translate(calc(50% + 2px), -50%);
}
.vue-flow__handle-left{
transform: translate(calc(-50% - 2px), -50%);
}
.vue-flow__handle-top{
transform: translate(-50%, calc(-50% - 2px));
}
.vue-flow__handle-bottom{
transform: translate(-50%, calc(50% + 2px));
}
&.active{
background-color: var(--treeItem-active-background);
}
&.start{
background-color: #7A7A7A;
color: #FFF;
border: 2px solid #7A7A7A;
}
}
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue'
const props = defineProps<{
data: {
type: Object,
default: () => ({
id: '',
})
}
}>()
</script>
<template>
<div class="node">
<Handle type="target" id="top" :position="Position.Top" />
<Handle type="source" id="bottom" :position="Position.Bottom" />
<Handle type="source" id="right" :position="Position.Right" />
<div>{{ props.data.id }}</div>
</div>
</template>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,25 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue'
const props = defineProps<{
data: {
type: Object,
default: () => ({
id: '',
})
}
}>()
</script>
<template>
<div class="node">
<Handle type="target" id="left" :position="Position.Left" />
<Handle type="source" id="right" :position="Position.Right" />
<div>{{ props.data.id }}</div>
</div>
</template>
<style lang="less" scoped>
</style>

View File

@@ -1,55 +0,0 @@
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core'
import { ref } from 'vue'
const counter = ref(0)
</script>
<template>
<div class="custom-node">
<Handle type="target" :position="Position.Top" />
<button class="increment nodrag" @click="counter++">Increment</button>
<div v-if="counter > 0" class="counter">
<div class="count" v-for="count of counter" :key="`count-${count}`">{{ count }}</div>
</div>
</div>
</template>
<style>
.custom-node {
min-width: 100px;
gap: 4px;
padding: 8px;
background: white;
border: 1px solid black;
border-radius: 4px;
}
.increment {
border-radius: 4px;
background: #42b983;
font-size: 10px;
color: #fff;
cursor: pointer;
border: none;
}
.increment:hover {
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.counter {
margin-top: 8px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 4px;
}
.count {
font-size: 6px;
color: #ff0072;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 8px;
}
</style>

View File

@@ -0,0 +1,57 @@
// import dagre from '@dagrejs/dagre'
import dagre from 'dagre'
import { Position, useVueFlow } from '@vue-flow/core'
import { ref } from 'vue'
/**
* Composable to run the layout algorithm on the graph.
* It uses the `dagre` library to calculate the layout of the nodes and edges.
*/
export function useLayout() {
const { findNode } = useVueFlow()
const graph = ref(new dagre.graphlib.Graph())
const previousDirection = ref('LR')
function layout(nodes, edges, direction) {
// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
const dagreGraph = new dagre.graphlib.Graph()
graph.value = dagreGraph
dagreGraph.setDefaultEdgeLabel(() => ({}))
const isHorizontal = direction === 'LR'
dagreGraph.setGraph({ rankdir: direction })
previousDirection.value = direction
for (const node of nodes) {
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
const graphNode = findNode(node.id)
dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
}
for (const edge of edges) {
dagreGraph.setEdge(edge.source, edge.target)
}
dagre.layout(dagreGraph)
// set nodes with updated positions
return nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id)
return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
}
})
}
return { graph, layout, previousDirection }
}

View File

@@ -0,0 +1,61 @@
export const versionsList = [
{
id: '1',
name:'V1',
child:[
{
id: '1-1',
name:'V1-1',
child:[
{
id: '1-1-1',
name:'V1-1-1',
}
]
},{
id: '1-2',
name:'V1-2',
child:[
{
id: '1-2-1',
name:'V1-2-1',
},{
id: '1-2-2',
name:'V1-2-2',
}
]
},{
id: '1-2',
name:'V1-2',
child:[
{
id: '1-2-1',
name:'V1-2-1',
child:[
{
id: '1-2-1-1',
name:'V1-2-1-1',
}
]
},{
id: '1-2-2',
name:'V1-2-2',
child:[
{
id: '1-2-2-1',
name:'V1-2-2-1',
},{
id: '1-2-2-2',
name:'V1-2-2-2',
}
]
},
]
},{
id: '1-3',
name:'V1-3',
}
]
}
]

View File

@@ -1,13 +1,63 @@
<template>
<div class="index"></div>
<div class="index background-pink">
<div class="header">
<p class="split"></p>
<button class="login" @click="onLogin">Log in</button>
<button class="register" @click="onRegister">Sign up</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useGlobalStore } from '@/stores'
const globalStore = useGlobalStore()
const loading = computed(() => globalStore.state.loading)
import { useRouter } from 'vue-router'
const router = useRouter()
const onLogin = () => {
router.push({ name: 'login' })
}
const onRegister = () => {
router.push({ name: 'register' })
}
</script>
<style lang="less" scoped>
.index {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
> .header {
position: absolute;
top: 3rem;
left: 0;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
> .split {
margin: 0 auto;
}
> button {
margin-right: 3rem;
width: 20rem;
height: 5.2rem;
border-radius: 5rem;
border: none;
outline: none;
font-size: 2.2rem;
font-weight: 600;
&:active {
opacity: 0.8;
}
}
> .login {
background-color: #ff7a51;
color: #fff;
}
> .register {
background-color: #fff;
color: #232323;
}
}
}
</style>

View File

@@ -1,13 +1,108 @@
<template>
<div class="login"></div>
<div class="login">
<div class="left">
<img class="bg" src="@/assets/images/login/left-bg.png" />
<div class="logo">
<img src="@/assets/images/logo.png" />
<span>FiDA</span>
</div>
</div>
<div class="right">
<div class="top">
<button class="back" @click="onBack">
<svg-icon name="arrow-left" size="37" />
</button>
</div>
<div class="box">
<img src="@/assets/images/login/elephant.png" />
<template v-if="!isVisible">
<div class="title">
<span>Log on to</span>
<img src="@/assets/images/logo-2.png" />
</div>
<div class="tip">A multi-agent canvas for rapid, trend driven design iteration.</div>
<el-form :model="formData" :rules="ruleForm" label-position="top" ref="formRef">
<el-form-item label="Email" prop="email">
<el-input v-model="formData.email" placeholder="Enter your email" name="email" />
</el-form-item>
<el-form-item label="Password" prop="password">
<el-input
v-model="formData.password"
placeholder="Enter your password"
type="password"
show-password
name="password"
/>
</el-form-item>
<div class="forgetPassword">
<span>forget password?</span>
</div>
<el-form-item prop="privacy" class="privacy">
<el-checkbox v-model="formData.privacy">
I agree to the <span @click.prevent="onClickPrivacy">Terms, Policy</span> and Fees.
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button class="submit" type="primary" @click="onSubmit">Log in</el-button>
</el-form-item>
</el-form>
<div class="tip-2">
Don't have an account? <span @click.prevent="onClickRegister">Sign up</span>
</div>
</template>
<visible-code v-else :email="formData.email" @submit="onVerifyCode" />
<other-login />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useGlobalStore } from '@/stores'
const globalStore = useGlobalStore()
const loading = computed(() => globalStore.state.loading)
import { computed, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { validateEmail, validatePass, validatePrivacy } from './tools'
import OtherLogin from './other-login.vue'
import VisibleCode from './visible-code.vue'
const router = useRouter()
const ruleForm = reactive({
email: [{ validator: validateEmail, trigger: 'blur' }],
password: [{ validator: validatePass, trigger: 'blur' }],
privacy: [{ validator: validatePrivacy, trigger: 'change' }]
})
const isVisible = ref(false)
const formData = reactive({
email: '',
password: '',
privacy: false
})
const formRef = ref(null)
const onBack = () => {
if (isVisible.value) {
isVisible.value = false
} else {
router.back()
}
}
const onSubmit = () => {
formRef.value?.validate?.((valid) => {
if (valid) {
console.log('submit!')
isVisible.value = true
} else {
console.log('error submit!')
}
})
}
const onVerifyCode = (code: string) => {
console.log(code)
router.push({ name: 'home' })
}
const onClickPrivacy = () => {}
const onClickRegister = () => {
router.push({ name: 'register' })
}
</script>
<style lang="less" scoped>
@import './style.less';
</style>

View File

@@ -0,0 +1,76 @@
<template>
<div class="other-login">
<div class="title">or continue with</div>
<div class="btns">
<el-button class="submit" @click="onGoogle">
<img src="@/assets/images/login/google.png" />
Sign in with Google
</el-button>
<el-button class="submit" @click="onWechat">
<img src="@/assets/images/login/wechat.png" />
Sign in with Wechat
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
const onGoogle = () => {}
const onWechat = () => {}
</script>
<style lang="less" scoped>
.other-login {
width: 100%;
> .title {
width: 100%;
color: #252727;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.6rem;
&::before,
&::after {
content: '';
flex: 1;
border-bottom: 0.05rem solid #333535;
}
&::before {
margin-right: 2.5rem;
}
&::after {
margin-left: 2.5rem;
}
}
> .btns {
margin-top: 3rem;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
> .el-button {
flex: 1;
height: 5rem;
--el-border-radius-base: 0.8rem;
--el-font-size-base: 1.6rem;
--el-border-color: #dfdfdf;
--el-border-text-color: #666;
margin-left: 2.4rem;
&:first-child {
margin-left: 0;
}
img {
width: auto;
height: 3rem;
margin-right: 2.5rem;
}
}
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="register">
<div class="left">
<img class="bg" src="@/assets/images/login/left-bg.png" />
<div class="logo">
<img src="@/assets/images/logo.png" />
<span>FiDA</span>
</div>
</div>
<div class="right">
<div class="top">
<button class="back" @click="onBack">
<svg-icon name="arrow-left" size="37" />
</button>
</div>
<div class="box">
<img src="@/assets/images/login/elephant.png" />
<template v-if="!isVisible">
<div class="title">
<span>Register for</span>
<img src="@/assets/images/logo-2.png" />
</div>
<div class="tip">A multi-agent canvas for rapid, trend driven design iteration.</div>
<el-form :model="formData" :rules="ruleForm" label-position="top" ref="formRef">
<el-form-item label="Name" prop="name">
<el-input name="name" v-model="formData.name" placeholder="Enter your name" />
</el-form-item>
<el-form-item label="Password" prop="password">
<el-input
name="password"
v-model="formData.password"
placeholder="Enter your password"
type="password"
show-password
/>
</el-form-item>
<el-form-item label="Email" prop="email">
<el-input name="email" v-model="formData.email" placeholder="Enter your email" />
</el-form-item>
<el-form-item prop="privacy" class="privacy">
<el-checkbox v-model="formData.privacy">
I agree to the <span @click.prevent="onClickPrivacy">Terms, Policy</span> and Fees.
</el-checkbox>
</el-form-item>
<el-form-item>
<el-button class="submit" type="primary" @click="onSubmit">Register</el-button>
</el-form-item>
</el-form>
<div class="tip-2">
Already have an account? <span @click.prevent="onClickLogin">Log in</span>
</div>
</template>
<visible-code v-else :email="formData.email" @submit="onVerifyCode" />
<other-login />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { validateName, validateEmail, validatePass, validatePrivacy } from './tools'
import OtherLogin from './other-login.vue'
import VisibleCode from './visible-code.vue'
const router = useRouter()
const ruleForm = reactive({
name: [{ validator: validateName, trigger: 'blur' }],
email: [{ validator: validateEmail, trigger: 'blur' }],
password: [{ validator: validatePass, trigger: 'blur' }],
privacy: [{ validator: validatePrivacy, trigger: 'change' }]
})
const isVisible = ref(false)
const formData = reactive({
name: '',
email: '',
password: '',
privacy: false
})
const formRef = ref(null)
const onBack = () => {
if (isVisible.value) {
isVisible.value = false
} else {
router.back()
}
}
const onSubmit = () => {
formRef.value?.validate?.((valid) => {
if (valid) {
console.log('submit!')
isVisible.value = true
} else {
console.log('error submit!')
}
})
}
const onVerifyCode = (code: string) => {
console.log(code)
router.push({ name: 'home' })
}
const onClickPrivacy = () => {}
const onClickLogin = () => {
router.push({ name: 'login' })
}
</script>
<style lang="less" scoped>
@import './style.less';
</style>

187
src/views/login/style.less Normal file
View File

@@ -0,0 +1,187 @@
.register,
.login {
width: 100%;
height: 100%;
overflow: hidden;
padding: 2.5rem;
display: flex;
>.left {
flex: 1;
height: 100%;
position: relative;
overflow: hidden;
>.bg {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 2rem;
}
>.logo {
position: absolute;
top: 2.4rem;
left: 4.2rem;
>img {
width: 6rem;
height: auto;
}
>span {
font-weight: 600;
font-size: 3.3rem;
}
}
}
>.right {
width: 90rem;
min-width: 600px;
display: flex;
flex-direction: column;
>.top {
display: flex;
padding: 0 3rem;
>.back {
width: 5rem;
height: 5rem;
border-radius: 1.2rem;
border: 0.25rem solid rgba(223, 223, 223, 1);
background-color: transparent;
cursor: pointer;
}
}
>.box {
min-width: 50rem;
flex: 1;
overflow-y: auto;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
>img {
width: 11rem;
height: auto;
margin-top: 2rem;
}
>.visible-code {
margin-top: 1.7rem;
margin-bottom: 7.2rem;
}
>.title {
font-weight: 600;
font-size: 7rem;
display: flex;
align-items: center;
justify-content: center;
color: #252727;
margin-top: 1.7rem;
>img {
width: auto;
height: 9.8rem;
}
}
>.tip {
font-weight: 400;
font-family: General Sans Variable;
font-style: Regular;
font-size: 1.8rem;
color: #666;
margin-top: 0.4rem;
}
>.el-form {
margin-top: 5rem;
width: 100%;
&::v-deep {
.el-form-item {
margin-bottom: 2rem;
}
.el-form-item__label {
color: #252727;
font-size: 1.8rem;
margin-bottom: 0.8rem;
}
.el-input {
--el-input-height: 5rem;
--el-input-border-radius: 0.8rem;
--el-input-text-color: #252727;
--el-border-color: #dfdfdf;
font-size: 1.4rem;
}
.forgetPassword {
margin-top: -1.2rem;
margin-bottom: 2rem;
font-size: 1.6rem;
text-align: right;
color: #666666;
cursor: pointer;
text-decoration: underline;
}
.privacy {
--el-checkbox-height: auto;
margin-bottom: 4rem;
.el-checkbox__label {
font-size: 1.6rem;
color: #666666;
font-weight: 400;
>span {
text-decoration: underline;
cursor: pointer;
}
}
}
.el-form-item__error {
padding-top: 1px;
font-size: 1.4rem;
}
.submit {
width: 100%;
height: 6rem;
background: #252727;
font-size: 2rem;
border-radius: 0.8rem;
color: #fff;
font-weight: 600;
}
}
}
>.tip-2 {
font-weight: 400;
font-size: 1.6rem;
color: #666;
>span {
text-decoration: underline;
color: #FF7A50;
cursor: pointer;
}
}
>.other-login {
margin-top: 5rem;
}
}
}
}

35
src/views/login/tools.js Normal file
View File

@@ -0,0 +1,35 @@
export const validateName = (rule, value, callback) => {
var str = ""
if (!value) {
str = 'Please input the name'
} else if (value.length < 2 || value.length > 20) {
str = 'Name length must be between 2 and 20 characters'
}
callback(str ? new Error(str) : undefined)
}
export const validateEmail = (rule, value, callback) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(\.[a-zA-Z]{2,})?$/
var str = ''
if (!value) {
str = 'Please input the email'
} else if (!emailRegex.test(value)) {
str = 'Please input the email again'
}
callback(str ? new Error(str) : undefined)
}
export const validatePass = (rule, value, callback) => {
var str = ''
if (!value) {
str = 'Please input the password'
} else if (value.length < 6 || value.length > 20) {
str = 'Password length must be between 6 and 20 characters'
}
callback(str ? new Error(str) : undefined)
}
export const validatePrivacy = (rule, value, callback) => {
if (!value) {
callback(new Error('Please agree to the Terms, Policy and Fees'))
} else {
callback()
}
}

View File

@@ -0,0 +1,105 @@
<template>
<div class="visible-code">
<div class="title">Verify your email address</div>
<div class="tip">
A verification code has been sent to <span>{{ email }}</span>
</div>
<input-code @submit="onVerify" v-model="code" />
<el-button class="verify" @click="onVerify">Verify</el-button>
<p class="time" v-if="time > -1">
<span @click="onResend">Resend Code </span> in {{ timeStr }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { CountDown } from '@/utils/tools'
import InputCode from '@/components/input-code.vue'
import { useRouter } from 'vue-router'
const router = useRouter()
const emit = defineEmits(['submit'])
const props = defineProps({
email: ''
})
const code = ref('')
const time = ref(60)
const timeStr = computed(() => CountDown(time.value))
const timeout = ref(null)
const setTime = (s = 120) => {
clearTime()
time.value = s
timeout.value = setInterval(() => {
time.value--
if (time.value <= 0) {
clearTime()
time.value = 0
}
}, 1000)
}
const clearTime = () => {
time.value = -1
clearTimeout(timeout.value)
}
onBeforeUnmount(() => {
clearTime()
})
onMounted(() => {
setTime()
})
const onResend = () => {
if (time.value > 0) return
setTime()
}
const onVerify = () => {
if (code.value.length !== 6) return
emit('submit', code.value)
}
</script>
<style lang="less" scoped>
.visible-code {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
> .title {
font-weight: 600;
font-size: 4rem;
color: #252727;
}
> .tip {
margin-top: 2rem;
font-size: 1.8rem;
color: #666;
> span {
color: #252727;
font-weight: 600;
}
}
> .input-code {
margin: 8.6rem 0;
}
> .verify {
width: 100%;
height: 6rem;
background: #252727;
font-size: 2rem;
border-radius: 0.8rem;
color: #fff;
font-weight: 600;
}
> .time {
user-select: none;
margin-top: 2rem;
font-size: 1.6rem;
color: #666;
> span {
color: #ff7a50;
text-decoration: underline;
cursor: pointer;
font-weight: 500;
}
}
}
</style>