Merge branch 'main' of http://18.167.251.121:10003/aidlab/FiDA_Front
This commit is contained in:
37
package-lock.json
generated
37
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
3
src/assets/icons/arrow-left.svg
Normal file
3
src/assets/icons/arrow-left.svg
Normal 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 |
BIN
src/assets/images/login/elephant.png
Normal file
BIN
src/assets/images/login/elephant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/assets/images/login/google.png
Normal file
BIN
src/assets/images/login/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
src/assets/images/login/left-bg.png
Normal file
BIN
src/assets/images/login/left-bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/assets/images/login/wechat.png
Normal file
BIN
src/assets/images/login/wechat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
BIN
src/assets/images/logo-2.png
Normal file
BIN
src/assets/images/logo-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.2 KiB |
103
src/components/input-code.vue
Normal file
103
src/components/input-code.vue
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
184
src/views/css/style.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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>
|
||||
26
src/views/home/versionTree/tree/view2/primaryNode.vue
Normal file
26
src/views/home/versionTree/tree/view2/primaryNode.vue
Normal 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>
|
||||
25
src/views/home/versionTree/tree/view2/secondaryNode.vue
Normal file
25
src/views/home/versionTree/tree/view2/secondaryNode.vue
Normal 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>
|
||||
@@ -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>
|
||||
57
src/views/home/versionTree/tree/view2/tools/tools.js
Normal file
57
src/views/home/versionTree/tree/view2/tools/tools.js
Normal 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 }
|
||||
}
|
||||
61
src/views/home/versionTree/tree/view2/tools/versionsData.js
Normal file
61
src/views/home/versionTree/tree/view2/tools/versionsData.js
Normal 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',
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
76
src/views/login/other-login.vue
Normal file
76
src/views/login/other-login.vue
Normal 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>
|
||||
110
src/views/login/register.vue
Normal file
110
src/views/login/register.vue
Normal 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
187
src/views/login/style.less
Normal 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
35
src/views/login/tools.js
Normal 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()
|
||||
}
|
||||
}
|
||||
105
src/views/login/visible-code.vue
Normal file
105
src/views/login/visible-code.vue
Normal 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>
|
||||
Reference in New Issue
Block a user