new_project

This commit is contained in:
Zixiao Wang 2025-06-03 16:28:46 +08:00
commit 2936c4f394
39 changed files with 7226 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

36
README.en.md Normal file
View 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
View 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/)

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

102
public/sign-in.html Normal file
View 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
View 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
View 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>

View 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>

View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

14
tsconfig.node.json Normal file
View 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
View 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,
},
})