new_project
This commit is contained in:
commit
2936c4f394
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
36
README.en.md
Normal file
36
README.en.md
Normal file
@ -0,0 +1,36 @@
|
||||
# aivue
|
||||
|
||||
#### Description
|
||||
{**When you're done, you can delete the content in this README and update the file with details for others getting started with your repository**}
|
||||
|
||||
#### Software Architecture
|
||||
Software architecture description
|
||||
|
||||
#### Installation
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Instructions
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Contribution
|
||||
|
||||
1. Fork the repository
|
||||
2. Create Feat_xxx branch
|
||||
3. Commit your code
|
||||
4. Create Pull Request
|
||||
|
||||
|
||||
#### Gitee Feature
|
||||
|
||||
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
||||
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
||||
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
||||
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
||||
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# aivue
|
||||
|
||||
#### 介绍
|
||||
{**以下是 Gitee 平台说明,您可以替换此简介**
|
||||
Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台
|
||||
无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)}
|
||||
|
||||
#### 软件架构
|
||||
软件架构说明
|
||||
|
||||
|
||||
#### 安装教程
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### 使用说明
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 新建 Feat_xxx 分支
|
||||
3. 提交代码
|
||||
4. 新建 Pull Request
|
||||
|
||||
|
||||
#### 特技
|
||||
|
||||
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
4094
package-lock.json
generated
Normal file
4094
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "hello_vue3",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/core": "^1.0.0",
|
||||
"@tabler/icons-vue": "^3.28.1",
|
||||
"@types/bootstrap": "^5.2.10",
|
||||
"apexcharts": "^3.54.1",
|
||||
"axios": "^1.7.9",
|
||||
"bootstrap": "^5.3.3",
|
||||
"echarts": "^5.6.0",
|
||||
"pinia": "^2.3.1",
|
||||
"sass": "^1.83.4",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuex": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-vue-devtools": "^7.6.8",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
BIN
public/000m.jpg
Normal file
BIN
public/000m.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
public/logo.ico
Normal file
BIN
public/logo.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
102
public/sign-in.html
Normal file
102
public/sign-in.html
Normal file
@ -0,0 +1,102 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
* Tabler - Premium and Open Source dashboard template with responsive and high quality UI.
|
||||
* @version 1.0.0-beta20
|
||||
* @link https://tabler.io
|
||||
* Copyright 2018-2023 The Tabler Authors
|
||||
* Copyright 2018-2023 codecalm.net Paweł Kuna
|
||||
* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>Sign in - Tabler - Premium and Open Source dashboard template with responsive and high quality UI.</title>
|
||||
<!-- CSS files -->
|
||||
<link href="./dist/css/tabler.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/tabler-flags.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/tabler-payments.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/tabler-vendors.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/demo.min.css?1692870487" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('https://rsms.me/inter/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class=" d-flex flex-column">
|
||||
<script src="./dist/js/demo-theme.min.js?1692870487"></script>
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<a href="." class="navbar-brand navbar-brand-autodark">
|
||||
<img src="./static/logo.svg" width="110" height="32" alt="Tabler" class="navbar-brand-image">
|
||||
</a>
|
||||
</div>
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="h2 text-center mb-4">Login to your account</h2>
|
||||
<form action="./" method="get" autocomplete="off" novalidate>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" placeholder="your@email.com" autocomplete="off">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">
|
||||
Password
|
||||
<span class="form-label-description">
|
||||
<a href="./forgot-password.html">I forgot password</a>
|
||||
</span>
|
||||
</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="password" class="form-control" placeholder="Your password" autocomplete="off">
|
||||
<span class="input-group-text">
|
||||
<a href="#" class="link-secondary" title="Show password" data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="form-check-input"/>
|
||||
<span class="form-check-label">Remember me on this device</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-primary w-100">Sign in</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="hr-text">or</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col"><a href="#" class="btn w-100">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/brand-github -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-github" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" /></svg>
|
||||
Login with Github
|
||||
</a></div>
|
||||
<div class="col"><a href="#" class="btn w-100">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/brand-twitter -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-twitter" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c0 -.249 1.51 -2.772 1.818 -4.013z" /></svg>
|
||||
Login with Twitter
|
||||
</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-secondary mt-3">
|
||||
Don't have account yet? <a href="./sign-up.html" tabindex="-1">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="./dist/js/tabler.min.js?1692870487" defer></script>
|
||||
<script src="./dist/js/demo.min.js?1692870487" defer></script>
|
||||
</body>
|
||||
</html>
|
84
public/sign-up.html
Normal file
84
public/sign-up.html
Normal file
@ -0,0 +1,84 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
* Tabler - Premium and Open Source dashboard template with responsive and high quality UI.
|
||||
* @version 1.0.0-beta20
|
||||
* @link https://tabler.io
|
||||
* Copyright 2018-2023 The Tabler Authors
|
||||
* Copyright 2018-2023 codecalm.net Paweł Kuna
|
||||
* Licensed under MIT (https://github.com/tabler/tabler/blob/master/LICENSE)
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>Sign up - Tabler - Premium and Open Source dashboard template with responsive and high quality UI.</title>
|
||||
<!-- CSS files -->
|
||||
<link href="./dist/css/tabler.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/tabler-flags.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/tabler-payments.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/tabler-vendors.min.css?1692870487" rel="stylesheet"/>
|
||||
<link href="./dist/css/demo.min.css?1692870487" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('https://rsms.me/inter/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class=" d-flex flex-column">
|
||||
<script src="./dist/js/demo-theme.min.js?1692870487"></script>
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<a href="." class="navbar-brand navbar-brand-autodark">
|
||||
<img src="./static/logo.svg" width="110" height="32" alt="Tabler" class="navbar-brand-image">
|
||||
</a>
|
||||
</div>
|
||||
<form class="card card-md" action="./" method="get" autocomplete="off" novalidate>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">Create new account</h2>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" placeholder="Enter name">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" placeholder="Enter email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="password" class="form-control" placeholder="Password" autocomplete="off">
|
||||
<span class="input-group-text">
|
||||
<a href="#" class="link-secondary" title="Show password" data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="form-check-input"/>
|
||||
<span class="form-check-label">Agree the <a href="./terms-of-service.html" tabindex="-1">terms and policy</a>.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button type="submit" class="btn btn-primary w-100">Create new account</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center text-secondary mt-3">
|
||||
Already have account? <a href="./sign-in.html" tabindex="-1">Sign in</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="./dist/js/tabler.min.js?1692870487" defer></script>
|
||||
<script src="./dist/js/demo.min.js?1692870487" defer></script>
|
||||
</body>
|
||||
</html>
|
15
src/App.vue
Normal file
15
src/App.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
|
||||
<!-- 页面内容区域 -->
|
||||
<div class="page-body">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Layout from '@/components/Layout.vue'
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
132
src/components/CreateProject.vue
Normal file
132
src/components/CreateProject.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<!-- 创建数据集隐藏页面 -->
|
||||
<div class="modal modal-blur fade" id="modal-report" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">创建一个新项目</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
||||
@click="handleClose"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">项目名称</label>
|
||||
<input type="text" class="form-control" v-model="projectForm.name" placeholder="您的项目名称">
|
||||
</div>
|
||||
<label class="form-label">任务类型</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col-lg-6" v-for="taskType in taskTypes" :key="taskType.value">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="report-type" :value="taskType.value"
|
||||
v-model="projectForm.taskType" class="form-selectgroup-input" checked>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">{{ taskType.label }}</span>
|
||||
<!-- <span class="d-block text-secondary">Provide only basic data needed for the report</span> -->
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">项目描述</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" class="form-control" v-model="projectForm.description"
|
||||
name="example-text-input" placeholder="描述您的项目">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary" data-bs-dismiss="modal" @click="handleCancel">
|
||||
取消
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary ms-auto" id="upload-btn" @click="handleCreateProject">
|
||||
创建项目
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="CreateProject">
|
||||
import { ref, reactive } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
|
||||
let username = ''
|
||||
let storedUser = sessionStorage.getItem('user')
|
||||
if (storedUser) {
|
||||
username = JSON.parse(storedUser).username
|
||||
} else {
|
||||
username = ''
|
||||
console.log("用户信息有误,请重新登录")
|
||||
}
|
||||
|
||||
let projectForm = reactive({
|
||||
name: '',
|
||||
user: username,
|
||||
taskType: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
let currentUploadRequest = ref<any>(null)
|
||||
|
||||
let taskTypes = [
|
||||
{ value: 'Classify', label: '图像分类' },
|
||||
{ value: 'Detect', label: '目标检测' },
|
||||
{ value: 'Split', label: '图像分割' }
|
||||
]
|
||||
|
||||
|
||||
function clearForm() {
|
||||
projectForm.name = ''
|
||||
projectForm.user = username
|
||||
projectForm.description = ''
|
||||
projectForm.taskType = 'Classify'
|
||||
}
|
||||
|
||||
//创建项目
|
||||
const handleCreateProject = async () => {
|
||||
if(projectForm.name === ''){
|
||||
alert('请输入项目名称')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('name', projectForm.name)
|
||||
formData.append('taskType', projectForm.taskType)
|
||||
formData.append('description', projectForm.description)
|
||||
formData.append('user', projectForm.user)
|
||||
|
||||
try {
|
||||
currentUploadRequest = await axios.post(`${API_URL}/datasets/create_project/`, formData)
|
||||
|
||||
clearForm()
|
||||
alert('创建成功')
|
||||
window.location.reload()
|
||||
|
||||
} catch (error) {
|
||||
alert('创建失败')
|
||||
console.error('创建失败:', error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
clearForm()
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
clearForm()
|
||||
}
|
||||
|
||||
</script>
|
151
src/components/DatasetCard.vue
Normal file
151
src/components/DatasetCard.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<RouterLink :to="{
|
||||
path: '/dateset-detail',
|
||||
query: {
|
||||
datasetName: dataset.name,
|
||||
user: 'lzz',
|
||||
taskType: dataset.task_type
|
||||
}
|
||||
}" class="card card-link hover-effect" style="cursor: pointer;">
|
||||
<div class="card-body" style="font-size: 1.1em; display: flex; align-items: center;"> <!-- 使用 flexbox 布局 -->
|
||||
<div style="flex-grow: 1;"> <!-- 让文本部分占据剩余空间 -->
|
||||
<div class="subheader">数据集名称</div> <!-- 项目名称 -->
|
||||
<div class="h3 mb-0 me-2">{{ dataset.name }}</div>
|
||||
<div class="text-muted">类型: {{ dataset.task_type }}</div> <!-- 新增信息框 -->
|
||||
<div class="text-muted">数量: {{ dataset.size }}</div> <!-- 新增信息框 -->
|
||||
<!-- <div class="text-muted">大小: {{ dataset.size | floatformat: 0 }} bytes</div> 新增信息框 -->
|
||||
<div class="mt-3"> <!-- 链接信息 -->
|
||||
<a href="#" class="text-primary">编辑</a> |
|
||||
<a href="#" class="text-warning">归档</a> |
|
||||
<a href="#" class="text-danger" :class="{ 'disabled-link': !isDeletable }"
|
||||
@click="(event) => { event.preventDefault(); handleDeleteDataset('lzz', dataset.name) }">删除</a>
|
||||
</div>
|
||||
</div>
|
||||
<div style="width: 100px; height: 100px; margin-left: 20px;"> <!-- 固定大小的图片框 -->
|
||||
<img :src="imageUrl" alt="展示图片" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
<!-- 图片 -->
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="DatasetCard">
|
||||
import axios from 'axios'
|
||||
import { API_URL, FLASK_API_URL } from '@/config/config'
|
||||
import { defineProps, ref, onMounted } from 'vue';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
dataset: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
let imageUrl = ref('')
|
||||
let taskType = ref('')
|
||||
let isDeletable = ref(false)
|
||||
|
||||
if (props.dataset.task_type == "Detect") {
|
||||
taskType.value = 'Detection'
|
||||
} else if (props.dataset.task_type == "Segment") {
|
||||
taskType.value = 'Segmentation'
|
||||
} else if (props.dataset.task_type == "Classify") {
|
||||
taskType.value = 'Classification'
|
||||
} else {
|
||||
taskType.value = 'Detection'
|
||||
}
|
||||
|
||||
async function handleDeleteDataset(user: String, datasetName: String) {
|
||||
// 新增:确认删除
|
||||
const confirmDelete = confirm('您确定要删除这个数据集吗?');
|
||||
if (!confirmDelete) {
|
||||
return; // 如果用户选择取消,则退出函数
|
||||
}
|
||||
|
||||
let response1 = await axios.get(`${API_URL}/datasets/delete_dataset/`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName
|
||||
}
|
||||
});
|
||||
|
||||
// 记得改,全都由Django负责请求,不要跨平台请求
|
||||
let response2 = await axios.post(`${FLASK_API_URL}/delete_temp_dir`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName
|
||||
}
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json' // 确保内容类型为JSON
|
||||
}
|
||||
});
|
||||
|
||||
if (response1.data.success && response2.data.success) {
|
||||
alert('删除成功');
|
||||
console.log('删除成功');
|
||||
} else {
|
||||
alert('删除失败');
|
||||
console.log('删除失败');
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
async function fetchImageUrl(user: string, datasetName: string, nextImage: string, taskType: string, pageSize: Number) {
|
||||
|
||||
let response = await axios.get(`${API_URL}/datasets/get_minio_links/`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName,
|
||||
nextImage: nextImage,
|
||||
taskType: taskType,
|
||||
pageSize: pageSize,
|
||||
}
|
||||
})
|
||||
|
||||
imageUrl.value = response.data.links[0]
|
||||
console.log(response.data)
|
||||
}
|
||||
|
||||
async function checkDatasetStatus(user: string, datasetName: string) {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/datasets/get_dataset_is_upload/`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName
|
||||
}
|
||||
});
|
||||
isDeletable.value = response.data.is_upload; // 假设返回的状态为布尔值
|
||||
} catch (error) {
|
||||
console.error('获取数据集状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchImageUrl(props.dataset.user, props.dataset.name, '', taskType.value, 60); // 组件挂载时调用获取图片的函数
|
||||
checkDatasetStatus('lzz', props.dataset.name)
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.disabled-link {
|
||||
pointer-events: none;
|
||||
/* 禁用链接点击 */
|
||||
color: gray;
|
||||
/* 设置为灰色以表示不可用 */
|
||||
}
|
||||
|
||||
.hover-effect {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
22
src/components/DatasetImageCard.vue
Normal file
22
src/components/DatasetImageCard.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="col-md-2 mb-3 mx-2">
|
||||
<a data-fslightbox="gallery"
|
||||
:href="props.imageUrl" target="_blank">
|
||||
<!-- Photo -->
|
||||
<div class="img-responsive img-responsive-1x1 rounded-3 border"
|
||||
:style="{ backgroundImage: `url(${props.imageUrl})` }">
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="DatasetImageCard">
|
||||
import {ref} from 'vue'
|
||||
let props = defineProps({
|
||||
imageUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
260
src/components/DatasetUploadModel.vue
Normal file
260
src/components/DatasetUploadModel.vue
Normal file
@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<!-- 创建数据集隐藏页面 -->
|
||||
<div class="modal modal-blur fade" id="modal-report" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">上传您的数据集</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
||||
@click="handleClose"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">数据集名称</label>
|
||||
<input type="text" class="form-control" v-model="datasetForm.name" placeholder="您的数据集名称">
|
||||
</div>
|
||||
<label class="form-label">任务类型</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col-lg-6" v-for="taskType in taskTypes" :key="taskType.value">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="report-type" :value="taskType.value"
|
||||
v-model="datasetForm.taskType" class="form-selectgroup-input" checked>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">{{ taskType.label }}</span>
|
||||
<!-- <span class="d-block text-secondary">Provide only basic data needed for the report</span> -->
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">描述</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="text" class="form-control" v-model="datasetForm.description"
|
||||
name="example-text-input" placeholder="描述您的数据集">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" style="white-space: nowrap;">上传文件
|
||||
(注意:数据集的整理格式必须符合要求,否则无法解析!)</label>
|
||||
<div class="border rounded p-3 text-center"
|
||||
style="height: 200px; background-color: #f8f9fa;">
|
||||
|
||||
<input type="file" class="form-control" style="display: none;" ref="fileInput"
|
||||
@change="handleFileSelect" accept=".zip">
|
||||
|
||||
<label @click="triggerFileInput"
|
||||
class="d-flex justify-content-center align-items-center"
|
||||
style="height: 100%; cursor: pointer;">
|
||||
<span class="text-muted">点击这里上传文件或拖放文件到此处</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="file-info" class="mt-2 text-muted"></div> <!-- 显示文件信息 -->
|
||||
<div v-if="selectedFile" class="" mt-2>
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span>{{ selectedFile.name }}</span>
|
||||
<span class="ms-2 text-muted">>({{ formatFileSize(selectedFile.size) }})</span>
|
||||
<button class="btn btn-sm btn-ghost-danger ms-2"
|
||||
@click="clearSelectedFile">删除</button>
|
||||
</div>
|
||||
<div v-if="uploadProgress > 0" class="progress mt-2">
|
||||
<div class="progress-bar" :style="{ width: uploadProgress + '%' }">{{
|
||||
uploadProgress }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#">查看示例数据集</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary" data-bs-dismiss="modal" @click="handleCancel">
|
||||
取消
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary ms-auto" id="upload-btn" @click="handleUpload">
|
||||
上传数据集
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="DatasetUploadModel">
|
||||
import { ref, reactive } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
import type { CancelTokenSource } from 'axios'
|
||||
|
||||
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
let username = ''
|
||||
if (storedUser) {
|
||||
username = JSON.parse(storedUser).username
|
||||
} else {
|
||||
username = ''
|
||||
}
|
||||
|
||||
let datasetForm = reactive({
|
||||
name: '',
|
||||
user: username,
|
||||
taskType: 'Detect',
|
||||
size: '0Bytes',
|
||||
description: '',
|
||||
file: ''
|
||||
})
|
||||
|
||||
let selectedFile = ref<File | null>(null)
|
||||
let uploadProgress = ref(0)
|
||||
let fileInput = ref<HTMLInputElement | null>(null)
|
||||
// let currentUploadRequest = ref<any>(null)
|
||||
let cancelTokenSource: CancelTokenSource | null = null
|
||||
|
||||
let taskTypes = [
|
||||
// { value: 'Classify', label: '图像分类' },
|
||||
{ value: 'Detect', label: '目标检测' },
|
||||
// { value: 'Split', label: '图像分割' }
|
||||
]
|
||||
|
||||
function triggerFileInput() {
|
||||
if (fileInput.value) {
|
||||
fileInput.value.click()
|
||||
} else {
|
||||
console.error('文件输入框未正确引用')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function clearForm() {
|
||||
datasetForm.name = ''
|
||||
datasetForm.size = '0Bytes'
|
||||
datasetForm.user = ''
|
||||
datasetForm.description = ''
|
||||
datasetForm.taskType = 'Classify'
|
||||
selectedFile.value = null
|
||||
}
|
||||
|
||||
// 清除选中的文件
|
||||
function clearSelectedFile() {
|
||||
selectedFile.value = null
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
if (cancelTokenSource) {
|
||||
cancelTokenSource.cancel('上传被取消'); // 中止上传请求
|
||||
cancelTokenSource = null; // 重置取消令牌
|
||||
}
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
function isValidFileType(file: any) {
|
||||
const validTypes = ['.zip', '.rar', '.7z']
|
||||
return validTypes.some(type => file.name.toLowerCase().endsWith(type))
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
function handleFileSelect(event: any) {
|
||||
const file = event.target.files[0] // 只取第一个文件
|
||||
if (file && isValidFileType(file)) {
|
||||
selectedFile.value = file
|
||||
} else {
|
||||
alert('请选择有效的压缩文件().zip)')
|
||||
event.target.value = '' // 清空输入
|
||||
}
|
||||
}
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile.value) {
|
||||
alert('请先选择一个文件')
|
||||
return
|
||||
}
|
||||
if (datasetForm.taskType === '') {
|
||||
alert('请选择任务类型')
|
||||
return
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
formData.append('name', datasetForm.name)
|
||||
formData.append('taskType', datasetForm.taskType)
|
||||
formData.append('description', datasetForm.description)
|
||||
formData.append('user', datasetForm.user)
|
||||
// 确保 selectedFile.value 不为 null
|
||||
if (selectedFile.value) {
|
||||
formData.append('file', selectedFile.value) // 上传单个文件
|
||||
datasetForm.size = formatFileSize(selectedFile.value.size)
|
||||
formData.append('size', datasetForm.size)
|
||||
}
|
||||
|
||||
cancelTokenSource = axios.CancelToken.source(); // 创建新的取消令牌
|
||||
|
||||
try {
|
||||
let response = await axios.post(`${API_URL}/datasets/upload/`, formData, {
|
||||
cancelToken: cancelTokenSource.token,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
uploadProgress.value = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
)
|
||||
} else {
|
||||
console.warn('无法获取上传总大小');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
alert(response.data.message)
|
||||
} else {
|
||||
alert(response.data.message)
|
||||
console.error('上传失败:', response.data.message)
|
||||
}
|
||||
|
||||
clearForm()
|
||||
window.location.reload()
|
||||
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
uploadProgress.value = 0
|
||||
alert('取消上传成功')
|
||||
console.log('上传被取消');
|
||||
} else {
|
||||
uploadProgress.value = 0
|
||||
alert('上传失败')
|
||||
console.error('上传失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function formatFileSize(bytes: number) {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
let k = 1024
|
||||
let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
let i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
clearForm()
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
clearForm()
|
||||
}
|
||||
|
||||
</script>
|
504
src/components/Layout.vue
Normal file
504
src/components/Layout.vue
Normal file
@ -0,0 +1,504 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<header class="navbar navbar-expand-md d-print-none">
|
||||
<div class="container-xl">
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<a href="/">
|
||||
<img src="/public/logo.ico" width="110" height="32" alt="Tabler" class="navbar-brand-image">
|
||||
</a>
|
||||
</h1>
|
||||
<div class="navbar-nav flex-row order-md-last">
|
||||
<!-- 最近更新区域 - 仅在md(中等)及以上屏幕显示 -->
|
||||
<div class="d-none d-md-flex">
|
||||
<div class="nav-item dropdown d-none d-md-flex me-3">
|
||||
<!-- 最近更新下拉菜单 -->
|
||||
<div class="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
|
||||
<div class="card">
|
||||
<!-- 卡片标题 -->
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Last updates</h3>
|
||||
</div>
|
||||
<!-- 更新列表 -->
|
||||
<div class="list-group list-group-flush list-group-hoverable">
|
||||
<!-- 更新项目1 - 带红色动画状态点 -->
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<!-- 状态指示点 -->
|
||||
<div class="col-auto">
|
||||
<span class="status-dot status-dot-animated bg-red d-block"></span>
|
||||
</div>
|
||||
<!-- 更新内容 -->
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">Example 1</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">
|
||||
Change deprecated html tags to text decoration classes (#29604)
|
||||
</div>
|
||||
</div>
|
||||
<!-- 星标按钮 -->
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions">
|
||||
<!-- 星形图标 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted"
|
||||
width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="status-dot d-block"></span></div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">Example 2</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">
|
||||
justify-content:between ⇒ justify-content:space-between (#29734)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions show">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/star -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-yellow"
|
||||
width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="status-dot d-block"></span></div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">Example 3</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">
|
||||
Update change-version.js (#29736)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/star -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted"
|
||||
width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span
|
||||
class="status-dot status-dot-animated bg-green d-block"></span>
|
||||
</div>
|
||||
<div class="col text-truncate">
|
||||
<a href="#" class="text-body d-block">Example 4</a>
|
||||
<div class="d-block text-secondary text-truncate mt-n1">
|
||||
Regenerate package-lock.json (#29730)
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="list-group-item-actions">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/star -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-muted"
|
||||
width="24" height="24" viewBox="0 0 24 24" stroke-width="2"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 用户头像和下拉菜单 -->
|
||||
<div class="nav-item dropdown">
|
||||
<!-- 用户信息触发器 -->
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown"
|
||||
aria-label="Open user menu">
|
||||
<!-- 用户头像 -->
|
||||
<span class="avatar avatar-sm"
|
||||
style="background-image: url(/public/000m.jpg)"></span>
|
||||
<!-- 用户信息 - 仅在xl(特大)屏幕显示 -->
|
||||
<div class="d-none d-xl-block ps-2">
|
||||
<div>{{ userName }}</div>
|
||||
</div>
|
||||
</a>
|
||||
<!-- 用户菜单下拉列表 -->
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<a href="#" class="dropdown-item">个人中心</a>
|
||||
<!-- 分隔线 -->
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="./settings.html" class="dropdown-item">设置</a>
|
||||
<a class="dropdown-item" @click="logout">退出登录</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<header class="navbar-expand-md">
|
||||
<div class="collapse navbar-collapse" id="navbar-menu">
|
||||
<div class="navbar">
|
||||
<div class="container-xl">
|
||||
<ul class="navbar-nav">
|
||||
<!-- 项目导航 -->
|
||||
<li
|
||||
:class="{ 'nav-item active': $route.path === '/project', 'nav-item': $route.path !== '/project' }">
|
||||
<RouterLink class="nav-link" to="./project">
|
||||
<span
|
||||
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from http://tabler-icons.io/i/home -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 12l-2 0l9 -9l9 9l-2 0" />
|
||||
<path d="M5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-7" />
|
||||
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v6" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">
|
||||
项目
|
||||
</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<!-- 数据集导航 -->
|
||||
<li
|
||||
:class="{ 'nav-item active': $route.path === '/dataset', 'nav-item': $route.path !== '/dataset' }">
|
||||
<RouterLink class="nav-link" to="./dataset"
|
||||
:class="{ active: $route.path === '/dataset' }">
|
||||
<span
|
||||
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from http://tabler-icons.io/i/package -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3l8 4.5l0 9l-8 4.5l-8 -4.5l0 -9l8 -4.5" />
|
||||
<path d="M12 12l8 -4.5" />
|
||||
<path d="M12 12l0 9" />
|
||||
<path d="M12 12l-8 -4.5" />
|
||||
<path d="M16 5.25l-8 4.5" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">
|
||||
数据集
|
||||
</span>
|
||||
</RouterLink>
|
||||
<div class="dropdown-menu">
|
||||
<div class="dropdown-menu-columns">
|
||||
<div class="dropdown-menu-column">
|
||||
<a class="dropdown-item" href="./alerts.html">
|
||||
Alerts
|
||||
</a>
|
||||
<a class="dropdown-item" href="./accordion.html">
|
||||
Accordion
|
||||
</a>
|
||||
<div class="dropend">
|
||||
<a class="dropdown-item dropdown-toggle" href="#sidebar-authentication"
|
||||
data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button"
|
||||
aria-expanded="false">
|
||||
Authentication
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a href="./sign-in.html" class="dropdown-item">
|
||||
Sign in
|
||||
</a>
|
||||
<a href="./sign-in-link.html" class="dropdown-item">
|
||||
Sign in link
|
||||
</a>
|
||||
<a href="./sign-in-illustration.html" class="dropdown-item">
|
||||
Sign in with illustration
|
||||
</a>
|
||||
<a href="./sign-in-cover.html" class="dropdown-item">
|
||||
Sign in with cover
|
||||
</a>
|
||||
<a href="./sign-up.html" class="dropdown-item">
|
||||
Sign up
|
||||
</a>
|
||||
<a href="./forgot-password.html" class="dropdown-item">
|
||||
Forgot password
|
||||
</a>
|
||||
<a href="./terms-of-service.html" class="dropdown-item">
|
||||
Terms of service
|
||||
</a>
|
||||
<a href="./auth-lock.html" class="dropdown-item">
|
||||
Lock screen
|
||||
</a>
|
||||
<a href="./2-step-verification.html" class="dropdown-item">
|
||||
2 step verification
|
||||
</a>
|
||||
<a href="./2-step-verification-code.html" class="dropdown-item">
|
||||
2 step verification code
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="dropdown-item" href="./blank.html">
|
||||
Blank page
|
||||
</a>
|
||||
<a class="dropdown-item" href="./badges.html">
|
||||
Badges
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./buttons.html">
|
||||
Buttons
|
||||
</a>
|
||||
<div class="dropend">
|
||||
<a class="dropdown-item dropdown-toggle" href="#sidebar-cards"
|
||||
data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button"
|
||||
aria-expanded="false">
|
||||
Cards
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a href="./cards.html" class="dropdown-item">
|
||||
Sample cards
|
||||
</a>
|
||||
<a href="./card-actions.html" class="dropdown-item">
|
||||
Card actions
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a href="./cards-masonry.html" class="dropdown-item">
|
||||
Cards Masonry
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="dropdown-item" href="./carousel.html">
|
||||
Carousel
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./charts.html">
|
||||
Charts
|
||||
</a>
|
||||
<a class="dropdown-item" href="./colors.html">
|
||||
Colors
|
||||
</a>
|
||||
<a class="dropdown-item" href="./colorpicker.html">
|
||||
Color picker
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./datagrid.html">
|
||||
Data grid
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./datatables.html">
|
||||
Datatables
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./dropdowns.html">
|
||||
Dropdowns
|
||||
</a>
|
||||
<a class="dropdown-item" href="./dropzone.html">
|
||||
Dropzone
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<div class="dropend">
|
||||
<a class="dropdown-item dropdown-toggle" href="#sidebar-error"
|
||||
data-bs-toggle="dropdown" data-bs-auto-close="outside" role="button"
|
||||
aria-expanded="false">
|
||||
Error pages
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a href="./error-404.html" class="dropdown-item">
|
||||
404 page
|
||||
</a>
|
||||
<a href="./error-500.html" class="dropdown-item">
|
||||
500 page
|
||||
</a>
|
||||
<a href="./error-maintenance.html" class="dropdown-item">
|
||||
Maintenance page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a class="dropdown-item" href="./flags.html">
|
||||
Flags
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./inline-player.html">
|
||||
Inline player
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="dropdown-menu-column">
|
||||
<a class="dropdown-item" href="./lightbox.html">
|
||||
Lightbox
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./lists.html">
|
||||
Lists
|
||||
</a>
|
||||
<a class="dropdown-item" href="./modals.html">
|
||||
Modal
|
||||
</a>
|
||||
<a class="dropdown-item" href="./maps.html">
|
||||
Map
|
||||
</a>
|
||||
<a class="dropdown-item" href="./map-fullsize.html">
|
||||
Map fullsize
|
||||
</a>
|
||||
<a class="dropdown-item" href="./maps-vector.html">
|
||||
Map vector
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./markdown.html">
|
||||
Markdown
|
||||
</a>
|
||||
<a class="dropdown-item" href="./navigation.html">
|
||||
Navigation
|
||||
</a>
|
||||
<a class="dropdown-item" href="./offcanvas.html">
|
||||
Offcanvas
|
||||
</a>
|
||||
<a class="dropdown-item" href="./pagination.html">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/pie-chart -->
|
||||
Pagination
|
||||
</a>
|
||||
<a class="dropdown-item" href="./placeholder.html">
|
||||
Placeholder
|
||||
</a>
|
||||
<a class="dropdown-item" href="./steps.html">
|
||||
Steps
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./stars-rating.html">
|
||||
Stars rating
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
<a class="dropdown-item" href="./tabs.html">
|
||||
Tabs
|
||||
</a>
|
||||
<a class="dropdown-item" href="./tags.html">
|
||||
Tags
|
||||
</a>
|
||||
<a class="dropdown-item" href="./tables.html">
|
||||
Tables
|
||||
</a>
|
||||
<a class="dropdown-item" href="./typography.html">
|
||||
Typography
|
||||
</a>
|
||||
<a class="dropdown-item" href="./tinymce.html">
|
||||
TinyMCE
|
||||
<span
|
||||
class="badge badge-sm bg-green-lt text-uppercase ms-auto">New</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<!-- 模型导航 -->
|
||||
<!-- <li class="nav-item">
|
||||
<a class="nav-link" href="./">
|
||||
<span
|
||||
class="nav-link-icon d-md-none d-lg-inline-block">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 11l3 3l8 -8" />
|
||||
<path
|
||||
d="M20 12v6a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h9" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="nav-link-title">
|
||||
模型选择
|
||||
</span>
|
||||
</a>
|
||||
</li> -->
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="Layout">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useAuthStore } from '@/stores/mytoken'; // 引入 Pinia store
|
||||
import { API_URL } from '@/config/config';
|
||||
import axios from 'axios';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const userName = ref('');
|
||||
const router = useRouter();
|
||||
|
||||
// 在组件挂载时检查用户信息
|
||||
onMounted(async () => {
|
||||
// 检查 sessionStorage 中是否有用户信息
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
userName.value = JSON.parse(storedUser).username; // 使用存储的用户信息
|
||||
// console.log(userName.value)
|
||||
} else {
|
||||
// 如果没有存储的用户信息,请求最新的用户信息
|
||||
try {
|
||||
console.log(localStorage.getItem('accessToken'))
|
||||
const response = await axios.get(`${API_URL}/accounts/get_user_info`, {
|
||||
headers: {
|
||||
Authorization: localStorage.getItem('accessToken')
|
||||
}
|
||||
});
|
||||
userName.value = response.data.username; // 更新用户信息
|
||||
let user = {
|
||||
username: response.data.username,
|
||||
email: response.data.email
|
||||
};
|
||||
// 将用户信息存储到 sessionStorage
|
||||
authStore.saveUser(user);
|
||||
} catch (error: unknown) { // 指定 error 的类型为 unknown
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
// 如果 token 过期,跳转到登录页面
|
||||
if (error.response.status === 401) {
|
||||
router.push('/sign-in'); // 确保您已导入 router
|
||||
} else {
|
||||
console.error('获取用户信息失败', error.response.data);
|
||||
}
|
||||
} else {
|
||||
console.error('获取用户信息失败', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const logout = () => {
|
||||
// 清除 Pinia store 中的用户信息
|
||||
authStore.logout()
|
||||
|
||||
// 路由到登录页面
|
||||
router.push('/sign-in');
|
||||
};
|
||||
</script>
|
224
src/components/ModelCard.vue
Normal file
224
src/components/ModelCard.vue
Normal file
@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">训练记录</h3>
|
||||
</div>
|
||||
<div class="card-body border-bottom py-3">
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table card-table table-vcenter text-nowrap datatable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1 text-center"><input class="form-check-input m-0 align-middle" type="checkbox"
|
||||
aria-label="Select all invoices"></th>
|
||||
<th class="w-1 text-center">训练编号
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm icon-thick" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M6 15l6 -6l6 6" />
|
||||
</svg>
|
||||
</th>
|
||||
<th class="text-center">创建时间</th>
|
||||
<th class="text-center">预训练模型</th>
|
||||
<th class="text-center">数据集名称</th>
|
||||
<th class="text-center">任务类型</th>
|
||||
<th class="text-center">mAP</th>
|
||||
<th class="text-center">precision</th>
|
||||
<th class="text-center">当前状态</th>
|
||||
<th class="text-center">action</th>
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="task in project_training_tasks">
|
||||
<td class="text-center"><input class="form-check-input m-0 align-middle" type="checkbox"
|
||||
aria-label="Select invoice"></td>
|
||||
<td class="text-center"><span class="text-secondary">{{ task.id }}</span></td>
|
||||
<td class="text-center"><a href="invoice.html" class="text-reset" tabindex="-1"></a>{{
|
||||
formatCreateTime(task.create_time) }}</td>
|
||||
<td class="text-center">
|
||||
<span class="flag flag-xs flag-country-us me-2"></span>
|
||||
{{ task.pre_model_name }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ task.dataset_name }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ task.task_type === 'Detect' ? '目标检测' : task.task_type === 'Classify' ? '图像分类' : task.task_type
|
||||
}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{{ task.epochData }}
|
||||
</td>
|
||||
<td class="text-center">{{ task.precision }}</td>
|
||||
<td class="text-center">
|
||||
<span class="badge me-1" :class="{
|
||||
'bg-warning': task.status === '初始化',
|
||||
'bg-info': task.status === '训练中',
|
||||
'bg-success': task.status === '训练完成',
|
||||
'bg-danger': task.status === '训练异常'
|
||||
}">
|
||||
</span>
|
||||
{{ task.status }}
|
||||
</td>
|
||||
|
||||
<td class="text-end">
|
||||
<span>
|
||||
<button class="btn dropdown-toggle align-text-top" data-bs-boundary="viewport"
|
||||
data-bs-toggle="dropdown">操作</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<router-link class="dropdown-item"
|
||||
:to="{ path: '/train-detail', query: { task_id: task.id } }">
|
||||
查看详细
|
||||
</router-link>
|
||||
<a class="dropdown-item" href="#">
|
||||
编辑
|
||||
</a>
|
||||
<a class="dropdown-item" href="#"
|
||||
@click="deleteTask(task.id, task.user, task.project_name, task.dataset_name)">
|
||||
删除
|
||||
</a>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ModelCard">
|
||||
import axios from 'axios'
|
||||
import { get } from 'http';
|
||||
import { API_URL } from '@/config/config'
|
||||
import { ref, onMounted } from 'vue';
|
||||
// 定义任务的类型
|
||||
interface Task {
|
||||
id: number;
|
||||
user: string;
|
||||
project_name: string;
|
||||
pre_model_name: string;
|
||||
dataset_name: string;
|
||||
task_type: string;
|
||||
status: string;
|
||||
epoch: number;
|
||||
batch_size: number;
|
||||
image_size: number;
|
||||
create_time: string;
|
||||
model_size: string;
|
||||
epochData: number;
|
||||
precision: number;
|
||||
}
|
||||
|
||||
let props = defineProps({
|
||||
project_training_tasks: {
|
||||
type: Array as () => Task[], // 指定类型为 Task 数组
|
||||
default: () => [{
|
||||
id: 0,
|
||||
user: '',
|
||||
project_name: '',
|
||||
pre_model_name: '',
|
||||
dataset_name: '',
|
||||
task_type: '',
|
||||
status: '',
|
||||
epoch: 0,
|
||||
batch_size: 0,
|
||||
image_size: 0,
|
||||
create_time: '',
|
||||
model_size: '',
|
||||
epochData: 0.0,
|
||||
precision: 0.0,
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
let mAP = ref<number>(0.0)
|
||||
|
||||
async function get_curr_epoch_data(training_id: number) {
|
||||
try {
|
||||
let response = await axios.get(`${API_URL}/training/get_curr_epoch_data/`, {
|
||||
params: {
|
||||
training_id: training_id
|
||||
}
|
||||
});
|
||||
// 返回一个对象,包含 mAP95 和 precision
|
||||
return {
|
||||
mAP95: response.data.data.mAP95,
|
||||
precision: response.data.data.precision
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return { mAP95: 0, precision: 0 }; // 返回默认值
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function deleteTask(id: number, user: string, projectName: string, datasetName: string) {
|
||||
let response1 = await axios.get(`${API_URL}/training/delete_project_training_task/`, {
|
||||
params: {
|
||||
id: id,
|
||||
user: user,
|
||||
projectName: projectName
|
||||
}
|
||||
})
|
||||
|
||||
if (response1.data.success) {
|
||||
alert('删除成功')
|
||||
window.location.reload()
|
||||
} else {
|
||||
alert('删除失败')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 在 onMounted 中获取每个任务的 epoch 数据
|
||||
onMounted(async () => {
|
||||
for (let task of props.project_training_tasks) {
|
||||
const { mAP95, precision } = await get_curr_epoch_data(task.id);
|
||||
task.epochData = mAP95; // 或者根据需要存储 precision
|
||||
task.precision = precision;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 功能函数
|
||||
function formatCreateTime(dateString: string) {
|
||||
if (!dateString) { // 检查 dateString 是否有效
|
||||
return '无效的日期'; // 返回一个默认值或错误提示
|
||||
}
|
||||
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) { // 检查日期是否有效
|
||||
return '无效的日期'; // 返回一个默认值或错误提示
|
||||
}
|
||||
return date.toISOString().slice(0, 19).replace('T', ' '); // 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card-link {
|
||||
border: 1px solid transparent;
|
||||
/* 默认边框 */
|
||||
transition: border 0.3s;
|
||||
/* 添加过渡效果 */
|
||||
}
|
||||
|
||||
.card-link:hover {
|
||||
border: 2px solid rgb(242, 238, 238);
|
||||
/* 悬停时的边框效果 */
|
||||
}
|
||||
</style>
|
83
src/components/ProjectCard.vue
Normal file
83
src/components/ProjectCard.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<!-- <RouterLink :to="`/project-detail?projectName=${project.name}&user=lzz`" class="card card-link hover-effect" style="cursor: pointer;"> -->
|
||||
<RouterLink :to="{
|
||||
path:'/project-detail',
|
||||
query:{
|
||||
projectName:project.name,
|
||||
user:username
|
||||
}
|
||||
}"
|
||||
class="card card-link hover-effect" style="cursor: pointer;">
|
||||
<div class="card-body">
|
||||
<div class="subheader">项目</div> <!-- 项目名称 -->
|
||||
<div class="h3 mb-0 me-2">{{ project.name }}</div>
|
||||
<div class="text-muted">任务类型: {{ project.task_type }}</div> <!-- 任务类型 -->
|
||||
<div class="text-muted">创建时间: {{ formatCreateTime(project.create_time) }}</div> <!-- 创建时间 -->
|
||||
<div class="mt-3"> <!-- 链接信息 -->
|
||||
<a href="#" class="text-primary">编辑</a> |
|
||||
<a href="#" class="text-warning">归档</a> |
|
||||
<a href="#" class="text-danger" @click="(event) => { event.preventDefault(); handleDeleteProject(username, project.name) }">删除</a>
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ProjectBaseCard">
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
// 获取当前用户信息
|
||||
let storedUser = sessionStorage.getItem("user")
|
||||
let username = ''
|
||||
if (storedUser) {
|
||||
username = JSON.parse(storedUser).username
|
||||
} else {
|
||||
username = ''
|
||||
console.error("用户信息错误, 请重新登录")
|
||||
}
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
async function handleDeleteProject(user: String, projectName: String) {
|
||||
let reponse = await axios.get(`${API_URL}/datasets/delete_project/`, {
|
||||
params: {
|
||||
user: user,
|
||||
projectName: projectName
|
||||
}
|
||||
})
|
||||
|
||||
if (reponse.data.success) {
|
||||
alert('删除成功')
|
||||
console.log('删除成功')
|
||||
|
||||
} else {
|
||||
alert('删除失败')
|
||||
console.log('删除失败')
|
||||
}
|
||||
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function formatCreateTime(dateString: string) {
|
||||
const date = new Date(dateString);
|
||||
return date.toISOString().slice(0, 19).replace('T', ' '); // 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hover-effect {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
75
src/components/SmallDatasetCard.vue
Normal file
75
src/components/SmallDatasetCard.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="card" style="max-width: 300px;"> <!-- 设置最大宽度 -->
|
||||
<div class="card-body" style="font-size: 1.1em; display: flex; align-items: center; padding: 10px;">
|
||||
<!-- 调整内边距 -->
|
||||
<div style="flex-grow: 1;"> <!-- 让文本部分占据剩余空间 -->
|
||||
<div class="subheader">数据集名称</div> <!-- 项目名称 -->
|
||||
<div class="h3 mb-0 me-2">{{ dataset.name }}</div>
|
||||
</div>
|
||||
<div style="width: 100px; height: 100px; margin-left: 20px;"> <!-- 固定大小的图片框 -->
|
||||
<img :src="imageUrl" alt="展示图片" style="width: 100%; height: 100%; object-fit: cover;">
|
||||
<!-- 图片 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="DatasetCard">
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
import { defineProps, ref, onMounted } from 'vue';
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
dataset: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
let imageUrl = ref('')
|
||||
let taskType = ref('')
|
||||
|
||||
if (props.dataset.task_type == "Detect") {
|
||||
taskType.value = 'Detection'
|
||||
} else if (props.dataset.task_type == "Segment") {
|
||||
taskType.value = 'Segmentation'
|
||||
} else if (props.dataset.task_type == "Classify") {
|
||||
taskType.value = 'Classification'
|
||||
} else {
|
||||
taskType.value = 'Detection'
|
||||
}
|
||||
|
||||
async function fetchImageUrl(user: string, datasetName: string, nextImage: string, taskType: string, pageSize: Number) {
|
||||
|
||||
let response = await axios.get(`${API_URL}/datasets/get_minio_links/`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName,
|
||||
nextImage: nextImage,
|
||||
taskType: taskType,
|
||||
pageSize: pageSize,
|
||||
}
|
||||
})
|
||||
|
||||
imageUrl.value = response.data.links[0]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchImageUrl(props.dataset.user, props.dataset.name, '', taskType.value, 60); // 组件挂载时调用获取图片的函数
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hover-effect {
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
</style>
|
286
src/components/TrainModelCard.vue
Normal file
286
src/components/TrainModelCard.vue
Normal file
@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<!-- 创建数据集隐藏页面 -->
|
||||
<div class="modal modal-blur fade" id="train-model-card" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">创建训练任务</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
||||
@click="handleClose"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">选择训练数据集</label>
|
||||
<div class="mb-3" style="display: flex; flex-wrap: wrap; justify-content: space-between; max-height: 260px; overflow-y: auto;">
|
||||
<div v-for="(item, index) in datasets" :key="index" style="flex: 0 1 calc(50% - 10px); margin: 5px;">
|
||||
<div @click="selectDataset(item.name)" :style="{ cursor: 'pointer', border: selectedDataset === item.name ? '2px solid blue' : 'none' }">
|
||||
<SmallDatasetCard :dataset="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">请选择预训练模型</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col-lg-3" v-for="pre_trained_model in pre_trained_models"
|
||||
:key="pre_trained_model.value">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="report-type" :value="pre_trained_model.value"
|
||||
v-model="trainModelForm.preModelName" class="form-selectgroup-input" checked>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">{{ pre_trained_model.label
|
||||
}}</span>
|
||||
<!-- <span class="d-block text-secondary">Provide only basic data needed for the report</span> -->
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="mb-3">
|
||||
<!-- <label class="form-label">性能参考</label> -->
|
||||
<div class="row">
|
||||
<div class="col" style="margin-right: 50px;">
|
||||
<label class="form-label">Accuracy-{{ 75 }}%</label>
|
||||
<div class="progress mb-2" style="max-width: 200px;">
|
||||
<div class="progress-bar" style="width: 75%" role="progressbar" aria-valuenow="75"
|
||||
aria-valuemin="0" aria-valuemax="100" aria-label="75% Complete">
|
||||
<span class="visually-hidden">75% Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Speed-{{ 56 }}ms</label>
|
||||
<div class="progress mb-2" style="max-width: 200px;">
|
||||
<div class="progress-bar" style="width: 60%" role="progressbar" aria-valuenow="60"
|
||||
aria-valuemin="0" aria-valuemax="100" aria-label="60% Complete">
|
||||
<span class="visually-hidden">60% Complete</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label">自定义训练参数</label>
|
||||
<div class="mb-3">
|
||||
<div class="divide-y">
|
||||
<div>
|
||||
<label class="row">
|
||||
<span class="col" style="font-size: 1.2em; font-weight: bold;">Epochs</span>
|
||||
<span class="col-auto">
|
||||
<input type="text" class="form-control" placeholder="Epochs" v-model="trainModelForm.epochs">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<label class="row">
|
||||
<span class="col" style="font-size: 1.2em; font-weight: bold;">Batch Size</span>
|
||||
<span class="col-auto">
|
||||
<input type="text" class="form-control" placeholder="Batch Size" v-model="trainModelForm.batchSize">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="row">
|
||||
<span class="col" style="font-size: 1.2em; font-weight: bold;">Image Size</span>
|
||||
<span class="col-auto">
|
||||
<input type="text" class="form-control" placeholder="Image Size" v-model="trainModelForm.imageSize">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link link-secondary" data-bs-dismiss="modal" @click="handleCancel">
|
||||
取消
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary ms-auto" id="upload-btn" @click="handleUpload">
|
||||
提交训练任务
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="TrainModelCard">
|
||||
import { ref, reactive, computed, onMounted, defineProps } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
import type { CancelTokenSource } from 'axios'
|
||||
import SmallDatasetCard from '@/components/SmallDatasetCard.vue'
|
||||
|
||||
interface Dataset {
|
||||
name: string;
|
||||
// 其他属性可以在这里定义
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
let username = ''
|
||||
if (storedUser) {
|
||||
username = JSON.parse(storedUser).username
|
||||
} else {
|
||||
username = ''
|
||||
}
|
||||
|
||||
// 接收当前项目的所有信息
|
||||
const props = defineProps({
|
||||
project: Object,
|
||||
loadData: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
let datasetForm = reactive({
|
||||
name: '',
|
||||
user: username,
|
||||
taskType: 'Detect',
|
||||
size: '0Bytes',
|
||||
description: '',
|
||||
file: ''
|
||||
})
|
||||
|
||||
let trainModelForm = reactive({
|
||||
projectName: props.project ? props.project.name : '',
|
||||
datasetName: '',
|
||||
user: username,
|
||||
taskType: props.project ? props.project.task_type : '',
|
||||
preModelName: '',
|
||||
epochs: 50,
|
||||
batchSize: 16,
|
||||
imageSize: 640
|
||||
|
||||
})
|
||||
|
||||
let cancelTokenSource: CancelTokenSource | null = null
|
||||
let datasets = reactive<Dataset[]>([])
|
||||
let selectedDataset = ref<string | null>(null);
|
||||
|
||||
let pre_trained_models = [
|
||||
{ value: 'yolov5n', label: 'yolov5n' },
|
||||
{ value: 'yolov5s', label: 'yolov5s' },
|
||||
{ value: 'yolov5m', label: 'yolov5m' },
|
||||
{ value: 'yolov5l', label: 'yolov5l' },
|
||||
{ value: 'yolov5x', label: 'yolov5x' }
|
||||
]
|
||||
|
||||
|
||||
function clearForm() {
|
||||
trainModelForm.datasetName = ''
|
||||
trainModelForm.preModelName = ''
|
||||
trainModelForm.epochs = 50
|
||||
trainModelForm.batchSize = 16
|
||||
trainModelForm.imageSize = 640
|
||||
clearSelectDataset()
|
||||
}
|
||||
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async () => {
|
||||
if (selectedDataset.value === null) {
|
||||
alert('请选择一个数据集')
|
||||
return
|
||||
}
|
||||
|
||||
if (trainModelForm.preModelName === '') {
|
||||
alert('请选择一个预训练模型')
|
||||
return
|
||||
}
|
||||
|
||||
let trainFormData = new FormData()
|
||||
trainFormData.append('datasetName', trainModelForm.datasetName)
|
||||
trainFormData.append('projectName', trainModelForm.projectName)
|
||||
trainFormData.append('taskType', datasetForm.taskType)
|
||||
trainFormData.append('preModelName', trainModelForm.preModelName)
|
||||
trainFormData.append('epochs', trainModelForm.epochs.toString())
|
||||
trainFormData.append('batchSize', trainModelForm.batchSize.toString())
|
||||
trainFormData.append('imageSize', trainModelForm.imageSize.toString())
|
||||
trainFormData.append('user', trainModelForm.user)
|
||||
|
||||
cancelTokenSource = axios.CancelToken.source(); // 创建新的取消令牌
|
||||
|
||||
try {
|
||||
let is_upload = await axios.get(`${API_URL}/datasets/get_dataset_is_upload/`, {
|
||||
params: {
|
||||
user: trainModelForm.user,
|
||||
datasetName: trainModelForm.datasetName,
|
||||
}
|
||||
});
|
||||
|
||||
// 根据获取的状态判断是否发送训练请求
|
||||
if (is_upload.data.is_upload) {
|
||||
let response = await axios.post(`${API_URL}/training/create_training_task/`, trainFormData)
|
||||
|
||||
if (response.data.is_upload) {
|
||||
alert(response.data.message)
|
||||
} else {
|
||||
alert(response.data.message)
|
||||
console.error('上传失败:', response.data.message)
|
||||
}
|
||||
|
||||
clearForm()
|
||||
window.location.reload()
|
||||
} else {
|
||||
alert('请等待数据集完全上传至服务器')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('请求数据集上传状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getDatasets(user: String) {
|
||||
let response = await axios.get(`${API_URL}/datasets/get_user_datasets/`, {
|
||||
params: {
|
||||
user: user
|
||||
}
|
||||
})
|
||||
datasets = Object.assign(datasets, response.data.datasets)
|
||||
}
|
||||
|
||||
// 创建一个方法来加载数据
|
||||
async function fetchData() {
|
||||
if (props.project && props.project.name) {
|
||||
await getDatasets(props.project.user); // 假设 project 对象中有 user 属性
|
||||
}
|
||||
}
|
||||
|
||||
// 通过 expose 暴露 fetchData 方法
|
||||
defineExpose({ fetchData });
|
||||
|
||||
function handleClose() {
|
||||
clearForm()
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
clearForm()
|
||||
}
|
||||
|
||||
function selectDataset(name: string) {
|
||||
selectedDataset.value = name; // 更新选中的数据集
|
||||
trainModelForm.datasetName = name
|
||||
}
|
||||
|
||||
function clearSelectDataset() {
|
||||
selectedDataset.value = ''; // 更新选中的数据集
|
||||
trainModelForm.datasetName = ''
|
||||
}
|
||||
</script>
|
144
src/components/charts/Metrics.vue
Normal file
144
src/components/charts/Metrics.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-cards">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="d-flex">
|
||||
<div ref="chart" style="width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Metrics">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '@/config/config';
|
||||
import type internal from 'stream';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
export interface EpochData {
|
||||
id: number;
|
||||
epoch_number: number;
|
||||
mAP50: number;
|
||||
mAP95: number;
|
||||
precision: number;
|
||||
recall: number;
|
||||
create_time: Date;
|
||||
}
|
||||
|
||||
let route = useRoute()
|
||||
|
||||
const chart = ref<HTMLDivElement | null>(null);
|
||||
let myChart: ECharts | null = null;
|
||||
|
||||
let epoch_data = ref<EpochData[]>([])
|
||||
const epoch = ref<number[]>([]);
|
||||
let task_id = Number(route.query.task_id) as number;
|
||||
|
||||
async function get_training_epoch_data(training_id: number) {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/training/get_training_epoch_data`, {
|
||||
params: {
|
||||
training_id: training_id,
|
||||
}
|
||||
});
|
||||
|
||||
// console.log(response.data.epochs)
|
||||
epoch_data.value = response.data.epochs
|
||||
|
||||
// 新增代码:创建 epoch 数组
|
||||
epoch.value = Array.from({ length: epoch_data.value.length }, (_, i) => i + 1)
|
||||
} catch (error: unknown) { // 指定 error 的类型为 unknown
|
||||
console.log("EEEEEEEEEE")
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
get_training_epoch_data(task_id).then(() => { // 确保数据获取完成后再设置图表
|
||||
if (chart.value) {
|
||||
myChart = echarts.init(chart.value);
|
||||
|
||||
console.log(epoch_data.value.map(item => item.mAP50))
|
||||
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Metrics'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {
|
||||
data: ['mAP50(B)', 'mAP50-95(B)', 'precision(B)', 'recall(B)']
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: epoch.value // 使用 epoch 数组
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: 'mAP50(B)',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false, // 不显示数据点
|
||||
data: epoch_data.value.map(item => item.mAP50) // 从 epoch_data 中提取 mAP50
|
||||
},
|
||||
{
|
||||
name: 'mAP50-95(B)',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false, // 不显示数据点
|
||||
data: epoch_data.value.map(item => item.mAP95) // 从 epoch_data 中提取 mAP95
|
||||
},
|
||||
{
|
||||
name: 'precision(B)',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false, // 不显示数据点
|
||||
data: epoch_data.value.map(item => item.precision) // 从 epoch_data 中提取 precision
|
||||
},
|
||||
{
|
||||
name: 'recall(B)',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: false, // 不显示数据点
|
||||
data: epoch_data.value.map(item => item.recall) // 从 epoch_data 中提取 recall
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
myChart.setOption(option);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (myChart) {
|
||||
myChart.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 这里可以添加自定义样式 */
|
||||
</style>
|
2
src/config/config.ts
Normal file
2
src/config/config.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const API_URL = 'http://192.168.31.138:8100';
|
||||
export const FLASK_API_URL = 'http://192.168.31.138:5000';
|
13
src/main.ts
Normal file
13
src/main.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import '@tabler/core/dist/css/tabler.min.css'
|
||||
import '@tabler/core/dist/js/tabler.min.js'
|
||||
import router from './router'
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
70
src/router/index.ts
Normal file
70
src/router/index.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Dataset from '@/views/Dataset.vue'
|
||||
import DatasetDetail from '@/views/DatasetDetail.vue'
|
||||
import ProjectDetail from '@/views/ProjectDetail.vue'
|
||||
import Project from '@/views/Project.vue'
|
||||
import SignIn from '@/views/SignIn.vue'
|
||||
import SignUp from '@/views/SignUp.vue'
|
||||
import { useAuthStore } from '@/stores/mytoken'
|
||||
import TrainDetail from '@/views/TrainDetail.vue'
|
||||
|
||||
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Project,
|
||||
meta: { requiresAuth: true}
|
||||
},
|
||||
{
|
||||
path: '/sign-in',
|
||||
name: "sign-in",
|
||||
component: SignIn
|
||||
},
|
||||
{
|
||||
path: '/sign-up',
|
||||
name: "sign-up",
|
||||
component: SignUp
|
||||
},
|
||||
{
|
||||
path: '/project',
|
||||
component: Project,
|
||||
meta: { requiresAuth: true}
|
||||
},
|
||||
{
|
||||
path: '/dataset',
|
||||
component: Dataset,
|
||||
meta: { requiresAuth: true}
|
||||
},
|
||||
{
|
||||
path:'/project-detail',
|
||||
component: ProjectDetail,
|
||||
meta: { requiresAuth: true}
|
||||
},
|
||||
{
|
||||
path:'/dateset-detail',
|
||||
component: DatasetDetail,
|
||||
meta: { requiresAuth: true}
|
||||
},
|
||||
{
|
||||
path:'/train-detail',
|
||||
component: TrainDetail,
|
||||
meta: { requiresAuth: true}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next)=>{
|
||||
if (to.matched.some(r=>r.meta.requiresAuth)){
|
||||
const store = useAuthStore()
|
||||
if (!store.accessToken) {
|
||||
next({name: 'sign-in'})
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
28
src/stores/mytoken.ts
Normal file
28
src/stores/mytoken.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const accessToken = ref(localStorage.getItem('accessToken') || ''); // 从 localStorage 获取 token
|
||||
const user = ref(null);
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value); // 判断用户是否已认证
|
||||
|
||||
function saveTokens(access: string) {
|
||||
accessToken.value = access;
|
||||
localStorage.setItem('accessToken', access); // 保存到 localStorage
|
||||
}
|
||||
|
||||
function saveUser(userInfo: any) {
|
||||
user.value = userInfo; // 更新响应式用户信息
|
||||
sessionStorage.setItem('user', JSON.stringify(userInfo)); // 存储原始用户信息到 sessionStorage
|
||||
}
|
||||
|
||||
function logout() {
|
||||
accessToken.value = '';
|
||||
user.value = null;
|
||||
localStorage.removeItem('accessToken'); // 从 localStorage 删除 token
|
||||
sessionStorage.removeItem('user');
|
||||
}
|
||||
|
||||
return { accessToken, user, isAuthenticated, saveTokens, saveUser, logout };
|
||||
});
|
15
src/types/index.ts
Normal file
15
src/types/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export interface PersonInter {
|
||||
id: string,
|
||||
name: string,
|
||||
age: number
|
||||
}
|
||||
|
||||
export interface EpochData {
|
||||
id: number;
|
||||
epoch_number: number;
|
||||
mAP50: number;
|
||||
mAP95: number;
|
||||
precision: number;
|
||||
recall: number;
|
||||
create_time: Date;
|
||||
}
|
84
src/views/Dataset.vue
Normal file
84
src/views/Dataset.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<Layout></Layout>
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<!-- Page title -->
|
||||
<h2 class="page-title">
|
||||
数据集列表
|
||||
</h2>
|
||||
</div>
|
||||
<!-- Page title actions -->
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
|
||||
data-bs-target="#modal-report">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 5l0 14" />
|
||||
<path d="M5 12l14 0" />
|
||||
</svg>
|
||||
创建一个新数据集
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面主体 -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
<div class="col-sm-6 col-lg-6" v-for="(item, index) in datasets" :key="index"> <!-- 修改为 col-lg-6 以占据一半宽度 -->
|
||||
<DatasetCard :dataset="item"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DatasetUploadModel />
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Dataset">
|
||||
import { reactive , onMounted, ref} from 'vue';
|
||||
import axios from 'axios';
|
||||
import { API_URL } from '@/config/config'
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import DatasetCard from '@/components/DatasetCard.vue'
|
||||
import DatasetUploadModel from '@/components/DatasetUploadModel.vue'
|
||||
|
||||
const datasets = ref([]); // 定义响应式数据集数组
|
||||
|
||||
async function getDatasets(user: string) {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/datasets/get_user_datasets/`, {
|
||||
params: {
|
||||
user: user
|
||||
}
|
||||
});
|
||||
datasets.value = response.data.datasets; // 更新数据集
|
||||
} catch (error) {
|
||||
console.error('获取数据集失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 从 sessionStorage 获取用户信息
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const user = JSON.parse(storedUser); // 解析存储的用户信息
|
||||
await getDatasets(user.username); // 使用当前登录用户的用户名
|
||||
} else {
|
||||
console.error('用户未登录或信息缺失');
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
151
src/views/DatasetDetail.vue
Normal file
151
src/views/DatasetDetail.vue
Normal file
@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<Layout></Layout>
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
{{ dataset.name }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<!-- 大卡片 -->
|
||||
<div class="card" style="height: 72vh;">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<!-- 左半部分:数据集基本信息 -->
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">数据集基本信息</h5>
|
||||
<!-- 按钮区域 -->
|
||||
<div class="btn-group w-100 mb-3" role="group">
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical"
|
||||
id="btn-radio-vertical-1" autocomplete="off">
|
||||
<label for="btn-radio-vertical-1" type="button" class="btn">训练集</label>
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical"
|
||||
id="btn-radio-vertical-2" autocomplete="off">
|
||||
<label for="btn-radio-vertical-2" type="button" class="btn">验证集</label>
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical"
|
||||
id="btn-radio-vertical-3" autocomplete="off">
|
||||
<label for="btn-radio-vertical-3" type="button" class="btn">测试集</label>
|
||||
</div>
|
||||
<!-- 搜索框 -->
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" placeholder="搜索类别...">
|
||||
</div>
|
||||
<!-- 类别列表 -->
|
||||
<ul class="list-group" style="height: 500px; overflow-y: auto;">
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center"
|
||||
v-for="(category, index) in dataset.categories" :key="index">
|
||||
{{ category }}
|
||||
<span class="badge bg-primary rounded-pill">数量</span>
|
||||
</li>
|
||||
<!-- 更多类别 -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8" id="imageContainer" style="height: 72vh; overflow-y: auto"
|
||||
@scroll="handleScroll">
|
||||
<div class="card-body" style="height: auto;">
|
||||
<h5 class="card-title">数据集图片展示</h5>
|
||||
<div class="card-body d-flex flex-wrap" style="height: auto;">
|
||||
<DatasetImageCard v-for="image_url in imageList" :key="image_url"
|
||||
:image-url="image_url" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="DatasetDetail">
|
||||
import axios from 'axios'
|
||||
import { reactive, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router';
|
||||
import { API_URL } from '@/config/config'
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import DatasetImageCard from '@/components/DatasetImageCard.vue'
|
||||
let route = useRoute()
|
||||
|
||||
let dataset = reactive({ name: '', create_time: '', task_type: '', user: '', size: '', categories: [] })
|
||||
|
||||
let imageList = reactive<string[]>([])
|
||||
let nextImage = ref('')
|
||||
let taskType = ref('')
|
||||
let isLoading = ref(false);
|
||||
let hasMoreData = ref(true);
|
||||
let username = ''
|
||||
let storedUser = sessionStorage.getItem('user')
|
||||
if (storedUser) {
|
||||
username = JSON.parse(storedUser).username
|
||||
} else {
|
||||
console.error("用户信息错误,请重新登录")
|
||||
}
|
||||
|
||||
async function getDataset(user: String, datasetName: String) {
|
||||
let response = await axios.get(`${API_URL}/datasets/get_dataset/`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName
|
||||
}
|
||||
})
|
||||
dataset = Object.assign(dataset, response.data.dataset)
|
||||
}
|
||||
|
||||
async function getDatasetImages(user: string, datasetName: string, nextImage: string, taskType: string, pageSize: Number) {
|
||||
let image_links = await axios.get(`${API_URL}/datasets/get_minio_links/`, {
|
||||
params: {
|
||||
user: user,
|
||||
datasetName: datasetName,
|
||||
nextImage: nextImage,
|
||||
taskType: taskType,
|
||||
pageSize: pageSize,
|
||||
}
|
||||
})
|
||||
|
||||
if (image_links.data.links.length === 0) {
|
||||
hasMoreData.value = false;
|
||||
} else {
|
||||
imageList.push(...image_links.data.links);
|
||||
}
|
||||
}
|
||||
|
||||
if (route.query.taskType == "Detect") {
|
||||
taskType.value = 'Detection'
|
||||
} else if (route.query.taskType == "Segment") {
|
||||
taskType.value = 'Segmentation'
|
||||
} else if (route.query.taskType == "Classify") {
|
||||
taskType.value = 'Classification'
|
||||
} else {
|
||||
taskType.value = 'Detection'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getDataset(username, String(route.query.datasetName))
|
||||
await getDatasetImages(username, String(route.query.datasetName), '', taskType.value, 60)
|
||||
})
|
||||
|
||||
function handleScroll(event: Event) {
|
||||
const element = event.target as HTMLElement;
|
||||
if (element && !isLoading.value && hasMoreData.value && element.scrollTop + element.clientHeight >= element.scrollHeight - 30) {
|
||||
isLoading.value = true;
|
||||
nextImage.value = imageList[imageList.length - 1]
|
||||
getDatasetImages(username, String(route.query.datasetName), nextImage.value, taskType.value, 60).finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
76
src/views/Project.vue
Normal file
76
src/views/Project.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<Layout></Layout>
|
||||
<div>
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<!-- Page title -->
|
||||
<h2 class="page-title">
|
||||
项目列表
|
||||
</h2>
|
||||
</div>
|
||||
<!-- Page title actions -->
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
|
||||
data-bs-target="#modal-report">
|
||||
创建一个新项目 <!-- 移除了加号图标 -->
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-cards row-deck">
|
||||
<div class="col-sm-6 col-lg-3" v-for="(item, index) in projects" :key="index">
|
||||
<ProjectBaseCard :project="item" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CreateProject />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="Project">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
import Layout from '@/components/Layout.vue'
|
||||
import ProjectBaseCard from '@/components/ProjectCard.vue'
|
||||
import CreateProject from '@/components/CreateProject.vue'
|
||||
|
||||
// 模拟项目数据
|
||||
let projects = reactive([])
|
||||
|
||||
async function getProject(username: string) {
|
||||
try {
|
||||
const response = await axios.get(`${API_URL}/datasets/get_user_projects/`, {
|
||||
params: {
|
||||
user: username // 使用传入的用户名
|
||||
}
|
||||
});
|
||||
console.log('项目数据:', response.data);
|
||||
projects = Object.assign(projects, response.data.projects)
|
||||
} catch (error) {
|
||||
console.error('获取项目失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 从 sessionStorage 获取用户信息
|
||||
const storedUser = sessionStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
const user = JSON.parse(storedUser); // 解析存储的用户信息
|
||||
await getProject(user.username); // 使用当前登录用户的用户名
|
||||
} else {
|
||||
console.error('用户未登录或信息缺失');
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
119
src/views/ProjectDetail.vue
Normal file
119
src/views/ProjectDetail.vue
Normal file
@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<Layout></Layout>
|
||||
<div>
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<!-- Page title -->
|
||||
<h2 class="page-title">
|
||||
{{ route.query.projectName }} - {{ formatCreateTime(project.create_time) }}
|
||||
</h2>
|
||||
</div>
|
||||
<!-- Page title actions -->
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
|
||||
data-bs-target="#train-model-card" @click="loadTrainModelCard">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/plus -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 5l0 14" />
|
||||
<path d="M5 12l14 0" />
|
||||
</svg>
|
||||
训练模型
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 仅在 project_training_tasks 有值时渲染 ModelCard -->
|
||||
<ModelCard v-if="project_training_tasks.length" :project_training_tasks="project_training_tasks"/>
|
||||
|
||||
<!-- 仅在 project 有值时渲染 TrainModelCard -->
|
||||
<TrainModelCard v-if="project.name" :project="project" ref="trainModelCard"/>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ProjectDetail">
|
||||
import axios from 'axios'
|
||||
import { API_URL } from '@/config/config'
|
||||
import { reactive, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router';
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import ModelCard from '@/components/ModelCard.vue'
|
||||
import TrainModelCard from '@/components/TrainModelCard.vue'
|
||||
|
||||
let route = useRoute()
|
||||
|
||||
// 获取当前用户信息
|
||||
let storedUser = sessionStorage.getItem("user")
|
||||
let username = ''
|
||||
if (storedUser) {
|
||||
username = JSON.parse(storedUser).username
|
||||
} else {
|
||||
username = ''
|
||||
console.error("用户信息错误, 请重新登录")
|
||||
}
|
||||
|
||||
// 模拟项目数据
|
||||
let project = reactive({name:'',create_time:'',task_type:'',user:''})
|
||||
let project_training_tasks = reactive([])
|
||||
const trainModelCard = ref<InstanceType<typeof TrainModelCard> | null>(null); // 指定类型
|
||||
|
||||
// 获取项目基本信息
|
||||
async function getProejct(user: string, projectName: string) {
|
||||
let response = await axios.get(`${API_URL}/datasets/get_project/`, {
|
||||
params: {
|
||||
user: route.query.user,
|
||||
projectName: route.query.projectName
|
||||
}
|
||||
})
|
||||
project = Object.assign(project, response.data.project)
|
||||
}
|
||||
|
||||
|
||||
function formatCreateTime(dateString: string) {
|
||||
if (!dateString) { // 检查 dateString 是否有效
|
||||
return '无效的日期'; // 返回一个默认值或错误提示
|
||||
}
|
||||
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) { // 检查日期是否有效
|
||||
return '无效的日期'; // 返回一个默认值或错误提示
|
||||
}
|
||||
return date.toISOString().slice(0, 19).replace('T', ' '); // 格式化为 YYYY-MM-DD HH:mm:ss
|
||||
}
|
||||
|
||||
// 获取当前项目的所有模型训练信息
|
||||
async function getProjectModels(user: string, projectName: string) {
|
||||
let response = await axios.get(`${API_URL}/training/get_project_training_tasks/`, {
|
||||
params: {
|
||||
user: user,
|
||||
projectName: projectName
|
||||
}
|
||||
})
|
||||
project_training_tasks = Object.assign(project_training_tasks, response.data.tasks)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getProejct(username, String(route.query.projectName))
|
||||
console.log(project)
|
||||
await getProjectModels(username, String(route.query.projectName))
|
||||
})
|
||||
|
||||
console.log(project)
|
||||
// 点击事件,调用 TrainModelCard 的 fetchData 方法
|
||||
async function loadTrainModelCard() {
|
||||
if (trainModelCard.value) {
|
||||
await trainModelCard.value.fetchData(); // 调用 fetchData 方法
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
136
src/views/SignIn.vue
Normal file
136
src/views/SignIn.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<!-- <a href="." class="navbar-brand navbar-brand-autodark">
|
||||
<img src="/public/logo.ico" width="110" height="32" alt="Tabler" class="navbar-brand-image">
|
||||
</a> -->
|
||||
<h1 class="navbar-brand-name" style="font-size: 24px; margin-left: 10px;">算法工厂</h1>
|
||||
<!-- 使用 h1 标签并增加样式 -->
|
||||
</div>
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="h2 text-center mb-4">登录</h2>
|
||||
<div autocomplete="off" novalidate>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">邮箱地址</label>
|
||||
<input type="email" v-model="formData.email" class="form-control" placeholder="输入邮箱"
|
||||
required>
|
||||
<div v-if="emailError" class="text-danger">{{ emailError }}</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">
|
||||
密码
|
||||
<span class="form-label-description">
|
||||
<a href="./forgot-password.html">忘记密码</a>
|
||||
</span>
|
||||
</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="password" v-model="formData.password" class="form-control"
|
||||
placeholder="输入密码" required>
|
||||
<span class="input-group-text">
|
||||
<a href="#" class="link-secondary" title="Show password"
|
||||
data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="passwordError" class="text-danger">{{ passwordError }}</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="form-check-input" />
|
||||
<span class="form-check-label">下次自动登录</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-footer">
|
||||
<button @click="handleSubmit" class="btn btn-primary w-100">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="text-center text-secondary mt-3">
|
||||
还没有账号? <a href="./sign-up" tabindex="-1">注册</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="SignIn">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { API_URL } from '@/config/config'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/mytoken'
|
||||
import axios from 'axios' // 引入 axios
|
||||
import router from '@/router'
|
||||
|
||||
const authStore = useAuthStore(); // 使用 Pinia store
|
||||
let formData = reactive({
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
const emailError = ref('')
|
||||
const passwordError = ref('')
|
||||
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
emailError.value = ''
|
||||
passwordError.value = ''
|
||||
|
||||
if (!formData.email) {
|
||||
emailError.value = '邮箱地址是必填的'
|
||||
} else if (!validateEmail(formData.email)) {
|
||||
emailError.value = '邮箱地址格式不正确'
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
passwordError.value = '密码是必填的'
|
||||
}
|
||||
|
||||
if (formData.email && formData.password && !emailError.value) {
|
||||
try {
|
||||
// 提交请求的逻辑
|
||||
const response = await axios.post(`${API_URL}/accounts/login/`, formData)
|
||||
if (response.data.success) {
|
||||
console.log(response.data.token)
|
||||
// pinia存储 JWT
|
||||
authStore.saveTokens(response.data.token); // 使用 Pinia store 保存 token
|
||||
// 提取用户信息
|
||||
let user = {
|
||||
username: response.data.username,
|
||||
email: response.data.email
|
||||
};
|
||||
// 存储用户信息到 Pinia store
|
||||
authStore.saveUser(user);
|
||||
// 登录成功后的逻辑,例如跳转到主页
|
||||
router.push('/project'); // 假设主页的路由是 '/home'
|
||||
} else {
|
||||
// 处理登录失败的情况
|
||||
console.error('登录失败', response.data.message);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
alert('登录异常')
|
||||
// window.location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 邮箱格式正则表达式
|
||||
return emailPattern.test(email);
|
||||
}
|
||||
</script>
|
127
src/views/SignUp.vue
Normal file
127
src/views/SignUp.vue
Normal file
@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<!-- <a href="." class="navbar-brand navbar-brand-autodark">
|
||||
<img src="./static/logo.svg" width="110" height="32" alt="Tabler" class="navbar-brand-image">
|
||||
</a> -->
|
||||
<h1 class="navbar-brand-name" style="font-size: 24px; margin-left: 10px;">算法工厂</h1>
|
||||
</div>
|
||||
<div class="card card-md" autocomplete="off" novalidate>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-center mb-4">创建一个新账户</h2>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">账户名称</label>
|
||||
<input type="text" v-model="formData.username" class="form-control" placeholder="输入名称" required>
|
||||
<div v-if="usernameError" class="text-danger">{{ usernameError }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">邮箱地址</label>
|
||||
<input type="email" v-model="formData.email" class="form-control" placeholder="输入邮箱" required>
|
||||
<div v-if="emailError" class="text-danger">{{ emailError }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">密码</label>
|
||||
<div class="input-group input-group-flat">
|
||||
<input type="password" v-model="formData.password" class="form-control" placeholder="输入密码" required>
|
||||
<span class="input-group-text">
|
||||
<a href="#" class="link-secondary" title="Show password"
|
||||
data-bs-toggle="tooltip"><!-- Download SVG icon from http://tabler-icons.io/i/eye -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24"
|
||||
viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path
|
||||
d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="passwordError" class="text-danger">{{ passwordError }}</div>
|
||||
</div>
|
||||
<!-- <div class="mb-3">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="form-check-input" />
|
||||
<span class="form-check-label">Agree the <a href="./terms-of-service.html"
|
||||
tabindex="-1">terms and policy</a>.</span>
|
||||
</label>
|
||||
</div> -->
|
||||
<div class="form-footer">
|
||||
<button class="btn btn-primary w-100" @click="handleSubmit">注册</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center text-secondary mt-3">
|
||||
已经有一个账户? <a href="./sign-in" tabindex="-1">登录</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="SignUp">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { API_URL } from '@/config/config'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios' // 引入 axios
|
||||
import router from '@/router'
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const usernameError = ref('')
|
||||
const emailError = ref('')
|
||||
const passwordError = ref('')
|
||||
|
||||
const validateEmail = (email: string) => {
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 邮箱格式正则表达式
|
||||
return emailPattern.test(email);
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
usernameError.value = ''
|
||||
emailError.value = ''
|
||||
passwordError.value = ''
|
||||
|
||||
if (!formData.username) {
|
||||
usernameError.value = '账户名称是必填的'
|
||||
} else if (formData.username.length > 10) {
|
||||
usernameError.value = '账户名称不能超过10位'
|
||||
} else if (/\s/.test(formData.username)) {
|
||||
usernameError.value = '账户名称不能包含空格'
|
||||
}
|
||||
|
||||
if (!formData.email) {
|
||||
emailError.value = '邮箱地址是必填的'
|
||||
} else if (!validateEmail(formData.email)) {
|
||||
emailError.value = '邮箱地址格式不正确'
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
passwordError.value = '密码是必填的'
|
||||
} else if (formData.password.length > 20) {
|
||||
passwordError.value = '密码不能超过20位'
|
||||
} else if (formData.password.length < 6){
|
||||
passwordError.value = '密码至少6位'
|
||||
} else if (/\s/.test(formData.password)) {
|
||||
passwordError.value = '密码不能包含空格'
|
||||
}
|
||||
|
||||
if (formData.username && formData.email && formData.password && !emailError.value && !usernameError.value && !passwordError.value) {
|
||||
try {
|
||||
const response = await axios.post(`${API_URL}/accounts/register/`, formData)
|
||||
if (response.data.success) {
|
||||
router.push('/sign-in')
|
||||
} else{
|
||||
alert(response.data.message)
|
||||
}
|
||||
} catch (error) {
|
||||
alert('注册异常')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
35
src/views/TrainDetail.vue
Normal file
35
src/views/TrainDetail.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<Layout></Layout>
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="btn-group w-100 mb-3" role="group">
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical" id="btn-radio-vertical-1"
|
||||
autocomplete="off">
|
||||
<label for="btn-radio-vertical-1" type="button" class="btn">训练记录</label>
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical" id="btn-radio-vertical-2"
|
||||
autocomplete="off">
|
||||
<label for="btn-radio-vertical-2" type="button" class="btn">训练参数</label>
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical" id="btn-radio-vertical-3"
|
||||
autocomplete="off">
|
||||
<label for="btn-radio-vertical-3" type="button" class="btn">在线推理</label>
|
||||
<input type="radio" class="btn-check" name="btn-radio-vertical" id="btn-radio-vertical-4"
|
||||
autocomplete="off">
|
||||
<label for="btn-radio-vertical-4" type="button" class="btn">快速部署</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Metrics :task_id=route.query.task_id></Metrics>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="TrainDetail">
|
||||
import Layout from '@/components/Layout.vue'
|
||||
import Metrics from '@/components/charts/Metrics.vue'
|
||||
import { useRoute } from 'vue-router' // 导入 useRoute
|
||||
|
||||
let route = useRoute() // 创建 router 实例
|
||||
let task_id = route.query.task_id
|
||||
|
||||
</script>
|
19
tsconfig.app.json
Normal file
19
tsconfig.app.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
14
tsconfig.node.json
Normal file
14
tsconfig.node.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
}
|
||||
}
|
22
vite.config.ts
Normal file
22
vite.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 58098,
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue
Block a user