Commit a98e92f0 authored by Administrator's avatar Administrator

Initial commit

parents
Pipeline #402 failed with stages
VITE_APP_BASE_URL=http://se-test.wmdigit.com
VITE_APP_BASE_API =/api/
VITE_APP_TITLE=元芒智能终端系统
VITE_APP_PRJ=SAAS
VITE_APP_VERSION=master
VITE_APP_PATH=1
\ No newline at end of file
VITE_APP_BASE_URL='http://wm-tools-backend-test.frp.wmdigit.com:8888/'
\ No newline at end of file
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
\ No newline at end of file
{
"recommendations": [
"Vue.volar",
"Vue.vscode-typescript-vue-plugin",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
# aitools
This template should help get you started developing with Vue 3 in Vite.
包含一些AI工具:数据分析报告、样本处理
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
npm audit fix
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
}
<script setup lang="ts">
import utils from '@/utils/utils'
import { onUpdated, onMounted, reactive, ref } from 'vue'
import { detectDeviceType } from './compositions/common'
import { myFileUpload } from './compositions/fileUpload'
import { useCropBox } from './compositions/useCropBox'
import { onProcessing } from './compositions/process'
const title = ref('元芒数字')
const form = reactive({
source: '',
task_id: '',
file_path: '',
file_path_domain: '',
file_name: '',
file_size: 0,
crop_range: {},
box_range: {},
is_set: false,
sample_path: '',
})
const steps_active = ref(0)
const FileSizeLimitM = 500
const file_loading = ref(false)
const my_file_upload = myFileUpload(FileSizeLimitM, form, file_loading)
const upload = my_file_upload.upload
const actionUrl = my_file_upload.actionUrl
// 裁剪
const videoPlayer = ref<HTMLVideoElement | null>(null)
const canvas = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const cropBox = useCropBox(form, canvas, ctx)
const result_loading = ref(false)
const processing = onProcessing(form, steps_active, result_loading)
onMounted(async () => {
// 检测设备类型
form.source = detectDeviceType();
// 设置页面标题
document.title = title.value;
// 生成task_id
form.task_id = utils.genDateTimeStr();
console.log('页面加载,task_id =', form.task_id);
})
onUpdated(() => {
if (steps_active.value === 0 && form.is_set && form.sample_path !== '') {
console.log('切换到步骤 0 时执行脚本');
form.task_id = utils.genDateTimeStr();
console.log('返回首页,task_id =', form.task_id);
// 显示裁剪框
setTimeout(() => {
console.log('waiting for video size...')
cropBox.generate_canvas();
}, 1000)
}
})
</script>
<template>
<main class="home-container">
<!-- 标题 -->
<div class="title"><el-text>生成训练样本</el-text></div>
<div class="subtitle"><el-text>上传视频生成训练样本</el-text></div>
<!-- 步骤条 -->
<div>
<el-steps :active="steps_active" align-center finish-status="success">
<el-step title="上传视频并设置范围"/>
<!-- <el-step title="选取背景图"/> -->
<el-step title="生成样本"/>
</el-steps>
</div>
<div class="content-container">
<!-- 文件页 -->
<div class="upload-section" v-if="steps_active === 0">
<div><el-text class="section-title">上传文件、设置范围</el-text></div>
<div><el-text class="section-desc">上传视频后,可以播放、暂停,然后点击设置,先画裁剪框,再画扫描框</el-text></div>
<div class="upload-div" v-loading="file_loading" v-if="!form.file_path">
<el-upload
ref="upload"
:show-file-list="false"
:limit="1"
drag
accept=".mp4"
:action="actionUrl"
:on-success="my_file_upload.handleUploadSuccess"
:on-exceed="my_file_upload.handleUploadExceed"
:on-error="my_file_upload.handleUploadError"
:on-remove="my_file_upload.handleRemoveFile"
:before-upload="my_file_upload.handleBeforeUpload"
:on-progress="my_file_upload.handleUploadProgress"
list-type="picture"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖拽文件到这里 或 <em>浏览文件</em></div>
<div class="el-upload__tip" slot="tip">文件格式mp4, 大小限制{{FileSizeLimitM}}M</div>
</el-upload>
</div>
<div class="uploaded-div" v-if="form.file_path" >
<div class="uploaded-file-info">
<video id="video-player" :src="form.file_path_domain" controls></video>
<canvas
id="crop-canvas"
style="background-color: rgba(255, 0, 0, 0.1); position: absolute; left: 0; z-index: -1;"
@mousedown="cropBox.startDrawing"
@mousemove="cropBox.draw"
@mouseup="cropBox.endDrawing"
>
</canvas>
</div>
<el-button color="#181818" @click="cropBox.generate_canvas">设置</el-button>
<el-button color="#181818" @click="cropBox.reset">重置</el-button>
<p>裁剪框坐标:{{ form.crop_range }}</p>
<p>扫描框坐标:{{ form.box_range }}</p>
<!-- <p>{{ form.task_id }}</p> -->
</div>
<div class="button">
<el-button class="next" color="#181818" size="large" @click="processing.onProcess" :disabled="!form.is_set">Next</el-button>
</div>
</div>
<!-- 结果页 -->
<div class="report-section" v-if="steps_active === 1">
<div><el-text class="section-title">结果</el-text></div>
<div><el-text class="section-desc">下载样本</el-text></div>
<!-- 预览 -->
<div v-loading="result_loading">
<el-text class="progress-info-sub">
{{ form.sample_path}}
</el-text>
</div>
<!-- 按钮 -->
<div class="button">
<el-button class="back" size="large" @click="processing.back_to_first">Back</el-button>
<el-button class="next" color="#181818" size="large"
@click="processing.downloadFile(form.sample_path)"
:disabled="!(form.sample_path && form.sample_path.length > 0)"
>Download</el-button>
</div>
</div>
</div>
</main>
</template>
<style lang="scss" scoped src="./index.css"></style>
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElStep: typeof import('element-plus/es')['ElStep']
ElSteps: typeof import('element-plus/es')['ElSteps']
ElText: typeof import('element-plus/es')['ElText']
ElUpload: typeof import('element-plus/es')['ElUpload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
export interface ComponentCustomProperties {
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
}
}
/// <reference types="vite/client" />
/// <reference path="./typings/index.d.ts" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>元芒数字</title>
</head>
<body>
<div id="app"></div>
<!-- 引入微信 JS-SDK -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
{
"name": "aitools",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"axios": "^1.6.1",
"bootstrap": "^5.3.3",
"cropperjs": "^1.6.2",
"crypto-js": "^4.2.0",
"element-plus": "^2.10.7",
"file-saver": "^2.0.5",
"marked": "^15.0.8",
"pinia": "^2.1.7",
"sass": "^1.69.5",
"vue": "^3.3.4",
"vue-drag-resize": "^2.0.3",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.2",
"@types/crypto-js": "^4.2.2",
"@types/file-saver": "^2.0.7",
"@types/node": "^18.18.9",
"@vitejs/plugin-vue": "^4.4.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"npm-run-all2": "^6.1.1",
"prettier": "^3.0.3",
"typescript": "~5.2.0",
"unplugin-auto-import": "^0.16.7",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"vue-tsc": "^1.8.22"
}
}
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
import { ElConfigProvider } from 'element-plus'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const locale = zhCn
</script>
<template>
<el-config-provider :locale="locale">
<RouterView />
</el-config-provider>
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
</style>
import { getToken } from '@/utils/token'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
const VERSION = '20210531'
const APP_VERSION = '' + VERSION.replace(/\"/g, '') // 当前应用版本
const API_VERSION = '0.0.5' // 当前API版本
const OS_VERSION = 'window10' // 当前操作操作系统版本
var loading: any
var hide_loading = false
/**
* 创建axios实例
*/
const request: any = axios.create({
baseURL: import.meta.env.MODE === 'production' ? '/' : '/api/', // api的base_url
timeout: 1200000 // 请求超时时间
})
/** 下一次请求不显示Loading */
request.hideLoadingOnce = function () {
hide_loading = true
}
/**
* 是否隐藏Loading
*
* @returns
*/
function isHideLoading() {
var b = hide_loading
hide_loading = false
return b
}
/**
* 设置拦截器: 请求拦截器
*/
request.interceptors.request.use(
(config: any) => {
config.headers['tenant'] = '0'
// permission 处理
if (config.url === 'user/refresh') {
const refreshToken = getToken('refresh_token')
if (refreshToken) {
config.headers['Authorization'] = refreshToken
}
} else {
const accessToken = getToken('access_token')
if (accessToken) {
config.headers['Authorization'] = accessToken
}
}
// 关掉全屏loading by zcb 20250423
// if (!isHideLoading()) {
// loading = ElLoading.service({ fullscreen: true })
// }
config.headers['app-version'] = APP_VERSION
config.headers['api-version'] = API_VERSION
config.headers['os-version'] = OS_VERSION
let url = config.url!
// get参数编码
if (config.method === 'get' && config.params) {
url += '?'
const keys = Object.keys(config.params)
for (const key of keys) {
if (config.params[key] != null) {
url += `${key}=${encodeURIComponent(config.params[key])}&`
}
}
url = url.substring(0, url.length - 1)
config.params = {}
}
config.url = url
return config
},
(error: any) => {
loading && loading.close()
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
}
)
/**
* 设置拦截器: 响应拦截器
*/
request.interceptors.response.use(
(response: any) => {
loading && loading.close()
const res = response.data
return res
},
(error: any) => {
loading && loading.close()
console.log(error)
if (error.response && error.response.data) {
try {
var data = error.response.data
if (typeof data === 'string') {
if (error.response.status === 503) {
data = {
code: -1,
message: '当前服务繁忙,请稍后再试!'
}
} else {
data = JSON.parse(error.response.data)
}
}
return Promise.reject(data)
} catch (e) {
return Promise.reject({
code: -1,
message: '网络异常: ' + error.response.data
})
}
} else {
return Promise.reject({
code: -1,
message: error
})
}
}
)
export default request
/**
* 相关接口
*/
import request from '@/api/request'
export default {
// 通用接口
commonApi(api_name: string, api: string, post_data: any): Promise<any> {
if (!api_name || !api || !post_data) {
return Promise.reject('所有参数不能为空')
}
// console.log(post_data);
return request
.post(`/aitools/${api}`, post_data)
.then((res: any) => {
// console.log(res);
if (res && res.code === 0) {
if (res.data.result) {
return res.data.result
} else {
return Promise.reject(`${api_name}接口未返回结果`)
}
} else {
const errorMessage = res ? res.message : `${api_name}接口返回错误`
return Promise.reject(errorMessage)
}
})
.catch((err: any) => {
console.log(`err = ${JSON.stringify(err)}`)
try {
return Promise.reject(err)
} catch (e) {
return Promise.reject(`与后端${api_name}接口通讯失败`)
}
})
},
}
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
/* background: var(--color-background); */
background-color: #f9fafb;
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
@import './base.css';
#app {
/* max-width: 1280px; */
margin: 0 auto;
/* padding: 2rem; */
display: flex;
justify-content: center;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.mount('#app')
import { createRouter, createWebHashHistory } from 'vue-router'
import HomeView from '../views/home/index.vue'
import SampleHandleView from '../views/sample_handle/index.vue'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: SampleHandleView
},
{
path: '/report',
name: 'report',
component: HomeView
},
{
path: '/sample_handle',
name: 'sample_handle',
component: SampleHandleView
}
]
})
export default router
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
import { getToken } from '@/utils/token'
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({
isLoggedIn: getToken('access_token') ? true : false
}),
actions: {
login() {
this.isLoggedIn = true
},
logout() {
this.isLoggedIn = false
}
}
})
import FileSaver from 'file-saver';
export default class fileSave {
/**
* 导出Excel文件
* @param {*} res 文件流
* @param {*} name 文件名
*/
static getExcel(res, name) {
const blob = new Blob([res], {
type: 'application/vnd.ms-excel'
});
FileSaver.saveAs(blob, name + '.xlsx');
}
/**
* 导出CSV文件
* @param {*} res 文件流
* @param {*} name 文件名
*/
static getCsv(res, name) {
const blob = new Blob([res], {
type: 'application/vnd.ms-excel'
});
FileSaver.saveAs(blob, name + '.csv');
}
/**
* 导出图片1
* @param {*} url 图片地址
* @param {*} name 文件名
*/
static getImgURLs(url, name) {
const last = url.substring(url.lastIndexOf('.'), url.length);
FileSaver.saveAs(url, `${name}${last}`);
}
/**
* 导出图片2
* @param {*} res 文件流
* @param {*} name 文件名
*/
static downLoadImg(res, filename, type = 'image/jpeg') {
const blob = new Blob([res], {
type: type
});
FileSaver.saveAs(blob, `${filename}`);
}
/**
* 导出图片3
* @param {*} res 文件流
* @param {*} name 文件名
*/
static downLoadImgByUrl(url, filename, type = 'image/jpeg') {
FileSaver.saveAs(url, `${filename}`);
}
/**
* 下载文本文件
*/
static getTxtFile(url, name) {
FileSaver.saveAs(url, `${name}.txt`);
}
/**
* 下载文件
*/
static getVideoFile(url, name) {
FileSaver.saveAs(url, `${name}`);
}
/**
* 下载文件
*/
static getFile(url, name) {
FileSaver.saveAs(url, `${name}`);
}
/**
* 下载文件
*/
static getFile2(res, name) {
const blob = new Blob([res]);
FileSaver.saveAs(blob, `${name}`);
}
}
import FileSaver from 'file-saver'
export default class fileSave {
/**
* 导出Excel文件
* @param {*} res 文件流
* @param {*} name 文件名
*/
static getExcel(res: any, name: string) {
const blob = new Blob([res], {
type: 'application/vnd.ms-excel'
})
FileSaver.saveAs(blob, name + '.xlsx')
}
/**
* 导出CSV文件
* @param {*} res 文件流
* @param {*} name 文件名
*/
static getCsv(res: any, name: string) {
const blob = new Blob([res], {
type: 'application/vnd.ms-excel'
})
FileSaver.saveAs(blob, name + '.csv')
}
/**
* 导出图片1
* @param {*} url 图片地址
* @param {*} name 文件名
*/
static getImgURLs(url: string, name: string) {
const last = url.substring(url.lastIndexOf('.'), url.length)
FileSaver.saveAs(url, `${name}${last}`)
}
/**
* 导出图片2
* @param {*} res 文件流
* @param {*} name 文件名
*/
static downLoadImg(res: any, filename: string, type = 'image/jpeg') {
const blob = new Blob([res], {
type: type
})
FileSaver.saveAs(blob, `${filename}`)
}
/**
* 导出图片3
* @param {*} res 文件流
* @param {*} name 文件名
*/
static downLoadImgByUrl(url: any, filename: string, type = 'image/jpeg') {
FileSaver.saveAs(url, `${filename}`)
}
/**
* 下载文本文件
*/
static getTxtFile(url: string, name: string) {
FileSaver.saveAs(url, `${name}.txt`)
}
/**
* 下载文件
*/
static getVideoFile(url: Blob, name: string) {
FileSaver.saveAs(url, `${name}`)
}
/**
* 下载文件
*/
static getFile(url: string, name: string) {
FileSaver.saveAs(url, `${name}`)
}
}
/**
* 存储tokens
* @param {string} accessToken
* @param {string} refreshToken
*/
export function saveTokens(accessToken: string, refreshToken: string) {
localStorage.setItem('access_token', `Bearer ${accessToken}`)
localStorage.setItem('refresh_token', `Bearer ${refreshToken}`)
}
/**
* 存储access_token
* @param {string} accessToken
*/
export function saveAccessToken(accessToken: string) {
localStorage.setItem('access_token', `Bearer ${accessToken}`)
}
/**
* 获得某个token
* @param {string} tokenKey
*/
export function getToken(tokenKey: string) {
return localStorage.getItem(tokenKey)
}
/**
* 移除token
*/
export function removeToken() {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
}
import _cryptoJs from 'crypto-js'
export default class utils {
// 格式化为json字符串
static formatJson(jsonString: string) {
try {
return JSON.stringify(JSON.parse(jsonString), null, 2) // 对JSON字符串进行格式化处理
} catch (error) {
return 'Invalid JSON'
}
}
// 格式化为json
static formatJsonObj(jsonString: string) {
try {
return JSON.parse(jsonString) // 对JSON字符串进行格式化处理
} catch (error) {
return JSON.parse('{"error": "Invalid JSON"}')
}
}
// 从文本中提取 JSON
static extractJSON(text: string) {
// 正则表达式匹配 JSON 格式的字符串
const jsonRegex = /\[\s*\{\s*"序号"\s*:\s*\d+.*?\}\s*\]/s
const matches = text.match(jsonRegex)
return matches ? matches[0] : null
}
// 生成年月日时分秒毫秒字符串
static genDateTimeStr() {
const now = new Date()
const year = now.getFullYear()
const month = (now.getMonth() + 1).toString().padStart(2, '0')
const day = now.getDate().toString().padStart(2, '0')
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const seconds = now.getSeconds().toString().padStart(2, '0')
const milliseconds = now.getMilliseconds().toString().padStart(3, '0')
const formattedDateTime = `${year}${month}${day}${hours}${minutes}${seconds}${milliseconds}`
// console.log(formattedDateTime); // 输出类似:20221231120530123
return formattedDateTime
}
// 对一句话再次拆分文本
static splitDetailText(sentences: string[]) {
// console.log(sentences)
let result_sentences: string[] = []
let currentSentence = ''
for (let i = 0; i < sentences.length; i++) {
const str = sentences[i]
currentSentence += str + ','
if (i < sentences.length - 1 && (currentSentence + sentences[i + 1]).length <= 20) {
continue
}
if (i === sentences.length - 2 && sentences[i + 1].length <= 5) {
continue
}
if (currentSentence.length < 10) {
continue
}
result_sentences.push(
currentSentence.endsWith(',') ? currentSentence.slice(0, -1) : currentSentence
)
currentSentence = ''
}
// console.log('result_sentences=', result_sentences);
return result_sentences
}
static splitMoreText(sentences: string[]) {
// console.log(sentences)
let result_sentences: string[] = []
for (let i = 0; i < sentences.length; i++) {
const str = sentences[i]
let tempSentences = str.split(/[!|?|。|!|?|,|,]/)
let newList = utils.splitDetailText(tempSentences)
result_sentences = result_sentences.concat(newList)
}
// console.log('result_sentences=', result_sentences);
return result_sentences
}
// 拆分文本
static splitText(str: string, type: string = 'default') {
str = str.replaceAll('“', '').replaceAll('”', '')
// 使用正则表达式拆分文本
let sentences = str.split(/[!|?|。|!|?]/)
// 过滤掉长度为 0 的句子
sentences = sentences.filter((s) => s.length > 0)
// 过滤掉只包含标点符号的句子
sentences = sentences.filter((s) => !utils.containsOnlyPunctuation(s))
if (type == 'default') {
return sentences
} else {
let detailSplit = utils.splitMoreText(sentences)
return detailSplit
}
}
// 拆分英文文本
static splitTextEn(str: string) {
str = str.replaceAll('"', '').replaceAll('"', '')
// 使用正则表达式拆分文本
let sentences = str.split(/[!|?|.]/)
// 过滤掉长度为 0 的句子
sentences = sentences.filter((s) => s.length > 0)
// console.log(sentences)
return sentences
}
// 过滤掉中文字符
static filterChineseAndPunctuation(inputString: string) {
return inputString
.replace(/[\u4E00-\u9FA5\u3000-\u303F\uff00-\uffef]/g, '') // 过滤中文字符
.replace(/[^\w\s]|_/g, '') // 过滤标点符号
.replace(/\s+/g, ' ') // 连续多个空格替换为一个空格
}
// 检查该字符串是否只包含中文标点符号和英文标点符号
static containsOnlyPunctuation(str: string) {
// 使用正则表达式匹配是否只包含标点符号(包括中文标点)
return /^[!-\/:-@\[-`{-~\p{P}\p{S}\s]*$/u.test(str)
}
// 加密
static aesEncrypt(word: string) {
var key = _cryptoJs.enc.Utf8.parse('e6ef616dc57343248f6b3e98a07e1dde')
var srcs = _cryptoJs.enc.Utf8.parse(word)
var encrypted = _cryptoJs.AES.encrypt(srcs, key, {
mode: _cryptoJs.mode.ECB,
padding: _cryptoJs.pad.Pkcs7
})
return encrypted.toString()
}
static checkIsMobile = () => {
const userAgent = navigator.userAgent || navigator.vendor
if (/Mobile|Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
return true
} else {
return false
}
}
static getFileType = (url: string) => {
const videoExtensions = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'];
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
const extension = (url.split('.').pop() || '').toLowerCase();
if (videoExtensions.includes(extension)) {
return 'video';
} else if (imageExtensions.includes(extension)) {
return 'image';
} else {
return 'unknown';
}
}
static isPhone(phone: string): boolean {
const phoneRegex = /^1[3-9]\d{9}$/; // 简单的手机号正则匹配
return phoneRegex.test(phone);
}
}
import aitoolsService from '@/api/service/aitoolsService'
// 检测设备类型
export const detectDeviceType = () => {
const userAgent = navigator.userAgent.toLowerCase()
let source = ''
if (userAgent.match(/mobile/i) || userAgent.match(/android/i) || userAgent.match(/iphone/i) || userAgent.match(/ipad/i)) {
source = 'mobile'
// if (userAgent.match(/iphone/i) || userAgent.match(/ipad/i)) {
// source = 'ios'
// } else if (userAgent.match(/android/i)) {
// source = 'android'
// }
if (userAgent.match(/micromessenger/i)) {
source = 'wechat'
}
} else {
source = 'pc'
}
console.log('设备类型:', source)
return source
}
// 记录用户操作
export const trackUserAction = async (source: string, user_id: string, content: string, action: string) => {
let param: any = {
source: source,
user_id: user_id,
content: content,
action: action,
}
aitoolsService.commonApi('提交用户轨迹数据', 'track', param)
.then((response) => {
if (response == 'ok') {
console.log(action, '上报成功')
} else {
console.log(action, '上报失败')
}
})
.catch((error) => {
console.log(action, '上报失败:', error)
})
}
import aitoolsService from '@/api/service/aitoolsService';
import { trackUserAction } from './common';
import { ElMessage, ElMessageBox } from 'element-plus';
import utils from '@/utils/utils';
import { reactive, ref } from 'vue';
// 客户留资
export const myCustomerInfo = (form: any, title: any) => {
const customer = reactive({
name: '',
mobile: '',
company: '',
note: '',
})
const customerInfoVisible = ref(false)
const onCustomerInfoSubmit = async () => {
if (!customer.name || !customer.mobile) {
ElMessage({
message: '请填写姓名和手机号',
type: 'warning'
})
return
}
if (!utils.isPhone(customer.mobile)) {
ElMessage({
message: '手机号格式不正确',
type: 'warning'
})
return
}
try {
console.log(customer)
let param: any = {
name: customer.name,
mobile: customer.mobile,
company: customer.company,
source: form.source,
note: customer.note,
}
await aitoolsService.commonApi('记录客户信息', 'take_customer_info', param);
// 统计
trackUserAction(form.source, form.user_id, title.value, '客户留资')
customerInfoVisible.value = false
// 防止重复提交
localStorage.setItem("isSubmitCustomerInfo", 'yes');
// 新开一个浏览器窗口,打开一个链接
openNewWindow();
} catch (error) {
ElMessage({
message: String(error),
type: 'error'
})
}
}
const openNewWindow = () => {
if (form.source === 'pc') {
window.open(form.report_url, '_blank');
} else {
if (form.source === 'wechat') {
ElMessageBox.confirm(
'请先<span style="color: red;">「点击确认」</span>查看完整报告,如需保存报告到手机,再至右上角选择<span style="color: red;">「默认浏览器」</span>打开',
'微信提示',
{
confirmButtonText: '确认',
type: 'info',
center: true,
showClose: false,
showCancelButton: false,
dangerouslyUseHTMLString: true,
closeOnClickModal: false,
closeOnPressEscape: false,
}
).then(() => {
window.location.href = form.report_url;
})
} else {
window.location.href = form.report_url;
}
}
}
const onClickDownloadPDF = () => {
const isSubmitCustomerInfo = localStorage.getItem("isSubmitCustomerInfo");
console.log('isSubmitCustomerInfo =', isSubmitCustomerInfo);
if (isSubmitCustomerInfo === 'yes') {
customerInfoVisible.value = false
// 直接打开链接
openNewWindow();
} else {
customerInfoVisible.value = true
}
}
return {
customer,
customerInfoVisible,
onCustomerInfoSubmit,
onClickDownloadPDF,
}
}
import { ref } from 'vue'
import {
ElMessage,
genFileId,
type UploadInstance,
type UploadProps,
type UploadRawFile,
type UploadFile,
} from 'element-plus'
export const myExcelUpload = (FileSizeLimitM: number, form: any) => {
// ############ 处理上传文件 begin #############
const upload = ref<UploadInstance>()
const actionUrl = ref(
import.meta.env.MODE === 'production'
? '/file'
: import.meta.env.VITE_APP_BASE_API + '/file'
)
const handleBeforeUpload = async (file: any) => {
const isLimit = file.size / 1024 / 1024 <= FileSizeLimitM
if (!isLimit) {
ElMessage.error('上传文件大小不能超过 '+FileSizeLimitM+'MB!')
return false
}
}
const handleUploadSuccess = (val: Wm.UploadResult, file: UploadFile) => {
// console.log(val)
if (val.code == 0) {
form.excel_path = val.data[0].path
console.log('form.excel_path =', form.excel_path)
// console.log('file =', file.name, file.size, file.url)
if (file.name && file.size !== undefined) {
form.excel_name = file.name
form.excel_size = file.size
}
ElMessage({
message: '上传成功',
type: 'success'
})
} else {
ElMessage({
message: '上传失败:' + val.message,
type: 'error'
})
}
}
const handleUploadExceed: UploadProps['onExceed'] = (files) => {
// 清除已上传的文件
upload.value!.clearFiles()
// 获取超出限制的第一个文件
const file = files[0] as UploadRawFile
// 给文件分配一个新的唯一标识
file.uid = genFileId()
// 手动触发文件上传
upload.value!.handleStart(file)
// 提交上传
upload.value!.submit()
}
const handleUploadError = (error: Error) => {
ElMessage({
message: String(error.message),
type: 'error'
})
}
const handleUploadProgress = (event: ProgressEvent, file: UploadFile) => {
file.percentage = Math.round(event.loaded / event.total * 100)
console.log('上传进度:', file.percentage)
file.url = '/excel-icon.jpeg'
}
const handleRemoveFile = () => {
// 清除已上传的文件
// upload.value!.clearFiles()
form.excel_path = ""
form.excel_name = ""
form.excel_size = 0
console.log('文件列表移除Excel, form.excel_path =', form.excel_path)
}
// ############ 处理上传文件 end #############
return {
upload,
actionUrl,
handleBeforeUpload,
handleUploadSuccess,
handleUploadExceed,
handleUploadError,
handleUploadProgress,
handleRemoveFile,
}
}
import aitoolsService from '@/api/service/aitoolsService'
import utils from '@/utils/utils'
import {
ElMessage,
ElLoading,
} from 'element-plus'
import { trackUserAction } from './common'
import { ref } from 'vue'
const delay = (ms: any) => new Promise((res) => setTimeout(res, ms))
export const opDataAnalysis = (form: any, title: any, steps_active: any, progress_percentage: any) => {
// 开始分析
const step_read_file_result = ref('');
const step_data_clean_result = ref('');
const step_analysis_result = ref('');
const step_data_clean_result_store = ref('');
const step_data_clean_result_dates = ref('');
const onAnalysis = async () => {
if (!form.excel_path || form.excel_path.length == 0) {
ElMessage({
message: '请先上传Excel',
type: 'warning'
})
return
}
// 进入报告页
from_analysis_to_report();
// 清除变量
form.process_info = '';
form.report = '';
form.report_url = '';
form.process_status = '';
// 提交数据分析请求
try {
// 统计提交次数
trackUserAction(form.source, form.user_id, title.value, '提交数据分析')
let param: any = {
task_id: form.task_id,
excel_path: form.excel_path,
suggestion: form.suggestion,
}
// 发起第一个请求
aitoolsService.commonApi('提交数据分析', 'op_data_analysis', param)
.then((response) => {
// console.log(form)
console.log(`数据分析接口返回:${response}`)
})
.catch((error) => {
// ElMessage({
// message: error,
// type: 'error'
// });
// 重置task_id
form.task_id = utils.genDateTimeStr();
console.log('重置 task_id =', form.task_id);
})
// 立即开始轮询
const query_task_id = form.task_id;
form.process_info = '【开始运行】\n';
let error_cnt = 0;
let all_cnt = 0;
while (true) {
try {
console.log('获取数据分析处理进度');
// 每隔1秒获取一次进度
await delay(1000);
// 发送请求获取分析进度
const query_result = await aitoolsService.commonApi('查询数据分析进度', 'get_analysis_process_info', {task_id: query_task_id});
form.process_status = query_result.status;
console.log('数据分析', form.process_status);
let process_info_obj = query_result.process_info;
if ('read_file_result' in process_info_obj && 'read_file_count' in process_info_obj && step_read_file_result.value == '') {
form.process_info += process_info_obj.read_file_result + '\n(还剩下 2 个步骤)\n\n<span style="font-color: #999; font-size: 16px;">正在运行数据清洗程序...</span>\n\n';
step_read_file_result.value = process_info_obj.read_file_count;
}
if ('data_clean_result' in process_info_obj && step_data_clean_result.value == '') {
form.process_info += process_info_obj.data_clean_result + '\n(还剩下 1 个步骤)\n\n<span style="font-color: #999; font-size: 16px;">DeepSeek正在分析数据并书写建议...</span>\n\n';
step_data_clean_result.value = 'ok';
if ('store_to_process' in process_info_obj) {
step_data_clean_result_store.value = process_info_obj.store_to_process;
}
if ('dates' in process_info_obj) {
step_data_clean_result_dates.value = process_info_obj.dates;
}
}
if ('analysis_result' in process_info_obj && step_analysis_result.value == '') {
form.process_info += process_info_obj.analysis_result + '\n最后一步\n\n<span style="font-color: #999; font-size: 16px;">正在为您生成专属数据报告...</span>\n\n';
step_analysis_result.value = 'ok';
}
// console.log(form.process_info);
if (form.process_status == 'done') {
// await delay(2000);
form.report = query_result.report;
form.report_url = query_result.report_url;
form.process_info += '请收好您的专属数据报告链接~\n'
// form.process_info += '<a href="' + form.report_url + '" target="_blank">点击查看</a>\n\n';
// form.process_info += '【运行结束】';
console.log(form)
break;
}
if (form.process_status == 'error') {
form.process_info += process_info_obj.error_result + '请检查Excel重新提交分析。\n';
form.process_info += '【运行结束】';
break;
}
} catch (error: any) {
console.log(error, error_cnt);
error_cnt += 1;
if (error_cnt > 10) {
break;
}
continue;
}
all_cnt += 1;
if (all_cnt > 600) {
break;
}
}
} catch (error: any) {
ElMessage({
message: error,
type: 'error'
});
} finally {
// 重置task_id
form.task_id = utils.genDateTimeStr();
console.log('重置 task_id =', form.task_id);
console.log('isSubmitCustomerInfo =', localStorage.getItem("isSubmitCustomerInfo"));
}
}
const from_file_to_analysis = () => {
steps_active.value = 1
progress_percentage.value = 50
}
const back_to_file = () => {
steps_active.value = 0
progress_percentage.value = 0
}
const from_analysis_to_report = () => {
steps_active.value = 2
progress_percentage.value = 100
}
const back_to_analysis = () => {
steps_active.value = 1
progress_percentage.value = 50
}
return {
from_file_to_analysis,
back_to_file,
onAnalysis,
back_to_analysis,
step_read_file_result,
step_data_clean_result,
step_analysis_result,
step_data_clean_result_store,
step_data_clean_result_dates,
}
}
\ No newline at end of file
import aitoolsService from '@/api/service/aitoolsService';
declare var wx: any;
// 初始化微信JS-SDK
export const myWechatSDK = () => {
const initWechatSDK = () => {
// 确保 wx 对象存在
if (typeof wx !== 'undefined') {
// 获取当前URL,不包含#及其后面的部分
var currentUrl = window.location.href.split('#')[0];
// 调用后端接口获取微信配置
aitoolsService.commonApi('提交用户轨迹数据', 'fetch_wx_config', {current_url: currentUrl}).then(config => {
// console.log('config:', config);
if (!config || !config.appId || !config.timestamp || !config.nonceStr || !config.signature) {
console.error('获取微信配置失败:', config);
return;
}
wx.config({
debug: false,
appId: config.appId, // 从后端获取的 appId
timestamp: config.timestamp, // 从后端获取的时间戳
nonceStr: config.nonceStr, // 从后端获取的随机字符串
signature: config.signature, // 从后端获取的签名
jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData','onMenuShareTimeline','onMenuShareAppMessage'] // 需要使用的微信 API
});
wx.ready(() => {
// 分享到朋友圈
const shareData = {
title: '元芒数字-运营数据分析',
link: currentUrl, // 当前页面链接
imgUrl: 'https://mmecoa.qpic.cn/sz_mmecoa_jpg/icJ3dTLKWY5Wo8W283kdW4QhHbFFLRCqb6TwsnkCWC6LXs8DuVEicyaib980gGKkVjU4SVO8UWS8DhMiaXur0YAD8w/640?wx_fmt=jpeg', // 分享图标
desc: '全面的运营数据分析,一键生成报告,助力决策!📈',
success: () => {
console.log('分享设置成功');
}
};
// 设置分享给朋友
wx.updateAppMessageShareData(shareData);
// 设置分享至朋友圈
wx.updateTimelineShareData(shareData);
// 分享到朋友圈
wx.onMenuShareTimeline(shareData);
// 分享到朋友
wx.onMenuShareAppMessage(shareData);
});
wx.error((error: any) => {
console.error('微信配置错误:', error);
});
});
} else {
console.error('wx 对象未定义');
}
}
return {
initWechatSDK,
wx,
}
}
.home-container {
width: 100%;
}
.title {
:is(span) {
font-size: 25px;
font-weight: bold;
color: #181818;
}
text-align: center;
margin: 20px 0 0 0;
}
.subtitle {
:is(span) {
font-size: 13px;
color: #181818;
}
text-align: center;
margin: 0 0 20px 0;
}
.progress {
margin: 10px 50px;
text-align: center;
}
.content-container {
margin: 20px;
display: flex;
flex-direction: row; /* 默认横向排列 */
justify-content: center; /* 水平居中 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
border-radius: 8px; /* 可选:添加圆角 */
background-color: #fff; /* 可选:添加背景色 */
@media (max-width: 768px) { /* 当屏幕宽度小于768px时变为竖向排列 */
flex-direction: column;
}
}
/* 几个页面的公共样式 */
.upload-section,
.progress-section,
.report-section {
padding: 20px;
/* 标题 */
.section-title {
font-size: 18px;
font-weight: bold;
color: #181818;
}
/* 描述 */
.section-desc {
font-size: 12px;
}
}
/* 上传页 */
.upload-section {
/* 上传区域 */
.upload-div {
margin: 20px 0;
/* border: 2px dashed #dddfe5; */
border-radius: 4px;
}
/* 已上传文件 */
.uploaded-div {
margin: 20px 0;
padding: 20px 10px;
border-radius: 4px;
border: 1px dashed #dddfe5; /* 添加边框样式 */
.uploaded-file-info {
background-color: #f8f8f8;
display: flex;
align-items: center;
.file-info {
display: flex;
align-items: center;
}
.remove-button {
margin-left: 10px;
}
}
}
/* 按钮 */
.button {
display: flex;
flex-direction: column;
align-items: flex-start;
.next {
align-self: flex-end;
}
}
}
/* 分析页 报告页*/
.progress-section,
.report-section {
/* 按钮 */
.button {
margin: 20px 0;
display: flex;
flex-direction: row; /* 修改为行排列 */
justify-content: space-between; /* 使按钮两端对齐 */
align-items: center; /* 垂直居中对齐 */
}
}
/* 报告页 */
.report-section {
.report-progress,
.markdown-report {
margin: 20px 0;
padding: 10px;
border-radius: 4px;
border: 1px solid #dddfe5; /* 添加边框样式 */
}
.progress-info-main {
font-size: 16px;
font-weight: bold;
}
.progress-info-sub {
margin-left: 20px;
}
.markdown-report {
:is(span) {
font-size: 10px;
/* 保留空白符序列,但是正常地进行换行 */
white-space: pre-wrap;
}
}
}
/* 自定义旋转动画 */
.rotate {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
\ No newline at end of file
This diff is collapsed.
import aitoolsService from '@/api/service/aitoolsService'
// 检测设备类型
export const detectDeviceType = () => {
const userAgent = navigator.userAgent.toLowerCase()
let source = ''
if (userAgent.match(/mobile/i) || userAgent.match(/android/i) || userAgent.match(/iphone/i) || userAgent.match(/ipad/i)) {
source = 'mobile'
// if (userAgent.match(/iphone/i) || userAgent.match(/ipad/i)) {
// source = 'ios'
// } else if (userAgent.match(/android/i)) {
// source = 'android'
// }
if (userAgent.match(/micromessenger/i)) {
source = 'wechat'
}
} else {
source = 'pc'
}
console.log('设备类型:', source)
return source
}
// 记录用户操作
export const trackUserAction = async (source: string, user_id: string, content: string, action: string) => {
let param: any = {
source: source,
user_id: user_id,
content: content,
action: action,
}
aitoolsService.commonApi('提交用户轨迹数据', 'track', param)
.then((response) => {
if (response == 'ok') {
console.log(action, '上报成功')
} else {
console.log(action, '上报失败')
}
})
.catch((error) => {
console.log(action, '上报失败:', error)
})
}
import aitoolsService from '@/api/service/aitoolsService';
import { trackUserAction } from './common';
import { ElMessage, ElMessageBox } from 'element-plus';
import utils from '@/utils/utils';
import { reactive, ref } from 'vue';
// 客户留资
export const myCustomerInfo = (form: any, title: any) => {
const customer = reactive({
name: '',
mobile: '',
company: '',
note: '',
})
const customerInfoVisible = ref(false)
const onCustomerInfoSubmit = async () => {
if (!customer.name || !customer.mobile) {
ElMessage({
message: '请填写姓名和手机号',
type: 'warning'
})
return
}
if (!utils.isPhone(customer.mobile)) {
ElMessage({
message: '手机号格式不正确',
type: 'warning'
})
return
}
try {
console.log(customer)
let param: any = {
name: customer.name,
mobile: customer.mobile,
company: customer.company,
source: form.source,
note: customer.note,
}
await aitoolsService.commonApi('记录客户信息', 'take_customer_info', param);
// 统计
trackUserAction(form.source, form.user_id, title.value, '客户留资')
customerInfoVisible.value = false
// 防止重复提交
localStorage.setItem("isSubmitCustomerInfo", 'yes');
// 新开一个浏览器窗口,打开一个链接
openNewWindow();
} catch (error) {
ElMessage({
message: String(error),
type: 'error'
})
}
}
const openNewWindow = () => {
if (form.source === 'pc') {
window.open(form.report_url, '_blank');
} else {
if (form.source === 'wechat') {
ElMessageBox.confirm(
'请先<span style="color: red;">「点击确认」</span>查看完整报告,如需保存报告到手机,再至右上角选择<span style="color: red;">「默认浏览器」</span>打开',
'微信提示',
{
confirmButtonText: '确认',
type: 'info',
center: true,
showClose: false,
showCancelButton: false,
dangerouslyUseHTMLString: true,
closeOnClickModal: false,
closeOnPressEscape: false,
}
).then(() => {
window.location.href = form.report_url;
})
} else {
window.location.href = form.report_url;
}
}
}
const onClickDownloadPDF = () => {
const isSubmitCustomerInfo = localStorage.getItem("isSubmitCustomerInfo");
console.log('isSubmitCustomerInfo =', isSubmitCustomerInfo);
if (isSubmitCustomerInfo === 'yes') {
customerInfoVisible.value = false
// 直接打开链接
openNewWindow();
} else {
customerInfoVisible.value = true
}
}
return {
customer,
customerInfoVisible,
onCustomerInfoSubmit,
onClickDownloadPDF,
}
}
import { ref } from 'vue'
import {
ElMessage,
genFileId,
type UploadInstance,
type UploadProps,
type UploadRawFile,
type UploadFile,
} from 'element-plus'
export const myFileUpload = (FileSizeLimitM: number, form: any, loading: any) => {
// ############ 处理上传文件 begin #############
const upload = ref<UploadInstance>()
const actionUrl = ref(
import.meta.env.MODE === 'production'
? '/file'
: import.meta.env.VITE_APP_BASE_API + '/file'
)
const handleBeforeUpload = async (file: any) => {
const isLimit = file.size / 1024 / 1024 <= FileSizeLimitM
if (!isLimit) {
ElMessage.error('上传文件大小不能超过 '+FileSizeLimitM+'MB!')
return false
}
}
const handleUploadSuccess = (val: Wm.UploadResult, file: UploadFile) => {
// console.log(val)
if (val.code == 0) {
form.file_path = val.data[0].path
console.log('form.file_path =', form.file_path)
form.file_path_domain = val.data[0].url
console.log('form.file_path_domain =', form.file_path_domain)
// console.log('file =', file.name, file.size, file.url)
if (file.name && file.size !== undefined) {
form.file_name = file.name
form.file_size = file.size
}
ElMessage({
message: '上传成功',
type: 'success'
})
} else {
ElMessage({
message: '上传失败:' + val.message,
type: 'error'
})
}
loading.value = false
}
const handleUploadExceed: UploadProps['onExceed'] = (files) => {
// 清除已上传的文件
upload.value!.clearFiles()
// 获取超出限制的第一个文件
const file = files[0] as UploadRawFile
// 给文件分配一个新的唯一标识
file.uid = genFileId()
// 手动触发文件上传
upload.value!.handleStart(file)
// 提交上传
upload.value!.submit()
}
const handleUploadError = (error: Error) => {
ElMessage({
message: String(error.message),
type: 'error'
})
loading.value = false
}
const handleUploadProgress = (event: ProgressEvent, file: UploadFile) => {
file.percentage = Math.round(event.loaded / event.total * 100)
console.log('上传进度:', file.percentage)
file.url = '/video-icon.png'
// 根据进度控制 loading 状态
if (file.percentage <= 100) {
loading.value = true
}
// if (file.percentage == 100) {
// loading.value = false
// }
}
const handleRemoveFile = () => {
// 清除已上传的文件
// upload.value!.clearFiles()
form.file_path = ""
form.file_path_domain = ""
form.file_name = ""
form.file_size = 0
console.log('文件列表移除File, form.file_path =', form.file_path, ', form.file_path_domain =', form.file_path_domain)
}
// ############ 处理上传文件 end #############
return {
loading,
upload,
actionUrl,
handleBeforeUpload,
handleUploadSuccess,
handleUploadExceed,
handleUploadError,
handleUploadProgress,
handleRemoveFile,
}
}
import aitoolsService from '@/api/service/aitoolsService'
import utils from '@/utils/utils'
import {
ElMessage,
ElLoading,
ElMessageBox
} from 'element-plus'
import type { Action } from 'element-plus'
export const onProcessing = (form: any, steps_active: any, process_loading: any, result_loading: any) => {
const fileToClassify = async () => {
if (!form.file_path || form.file_path.length == 0) {
ElMessage({
message: '请先上传文件',
type: 'warning'
})
return
}
// 进入第二页
from_first_to_second();
// 清除结果
form.sample_result = {
root_path: '',
data: [{
dir_name: '',
images: [],
}]
};
// 提交处理请求
try {
let param: any = {
task_id: form.task_id,
file_path: form.file_path,
crop_range: JSON.stringify(form.crop_range),
box_range: JSON.stringify(form.box_range),
output_type: "dir",
classes_select: "",
target_fps: form.target_fps, // 目标帧率
}
process_loading.value = true;
// 发起请求
aitoolsService.commonApi('提交处理', 'gen_sample_from_video', param)
.then((response) => {
// console.log(form)
console.log(`接口返回:${response}`);
form.sample_result = response;
// 设置默认值
form.classes_select = {}
form.classes_select2 = {}
response.data.forEach((item: { dir_name: string; images: string[] }) => {
form.classes_select[item.dir_name] = '0';
form.classes_select2[item.dir_name] = '';
});
process_loading.value = false;
})
.catch((error) => {
ElMessage({
message: error,
type: 'error'
});
// // 重置task_id
// form.task_id = utils.genDateTimeStr();
// console.log('重置 task_id =', form.task_id);
process_loading.value = false;
})
} catch (error: any) {
ElMessage({
message: error,
type: 'error'
});
process_loading.value = false;
}
}
function isAllDataClassified(): boolean {
let is_ok = false;
if (!form.sample_result?.data || !form.classes_select) {
ElMessage({
message: '数据未正确初始化',
type: 'error'
});
}
form.sample_result.data.every((item: { dir_name: string; images: string[] }) => {
if (!form.classes_select.hasOwnProperty(item.dir_name)) {
ElMessage({
message: `请为${item.dir_name}分类`,
type: 'error'
});
} else {
is_ok = true
}
});
return is_ok;
}
const classifyToDownload = async () => {
if (!isAllDataClassified()) {
return
}
// 进入第3页
from_second_to_third();
// 清除结果
form.sample_path = '';
// 提交处理请求
try {
let param: any = {
task_id: form.task_id,
file_path: form.file_path,
crop_range: JSON.stringify(form.crop_range),
box_range: JSON.stringify(form.box_range),
output_type: "zip",
classes_select: JSON.stringify(form.classes_select),
target_fps: form.target_fps, // 目标帧率
}
result_loading.value = true;
// 发起请求
aitoolsService.commonApi('提交处理', 'gen_sample_from_video', param)
.then((response) => {
// console.log(form)
console.log(`接口返回:${response}`);
form.sample_path = response.sample_path;
result_loading.value = false;
})
.catch((error) => {
ElMessage({
message: error,
type: 'error'
});
// // 重置task_id
// form.task_id = utils.genDateTimeStr();
// console.log('重置 task_id =', form.task_id);
result_loading.value = false;
})
} catch (error: any) {
ElMessage({
message: error,
type: 'error'
});
result_loading.value = false;
}
}
const from_first_to_second = () => {
steps_active.value = 1
}
const back_to_first = () => {
steps_active.value = 0;
form.task_id = utils.genDateTimeStr();
console.log('返回首页,task_id =', form.task_id);
}
const from_second_to_third = () => {
steps_active.value = 2;
}
const back_to_second = () => {
steps_active.value = 1;
form.classes_select2 = JSON.parse(JSON.stringify(form.classes_select));
}
function downloadFile(url: string, filename?: string) {
const a = document.createElement('a')
a.href = url
if (filename) {
a.download = filename // 指定下载文件名
}
a.target = '_blank'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
function deleteOneSample(sample_dir: string) {
ElMessageBox.alert(`确认删除 ${sample_dir} 这行样本吗?`, '删除样本', {
// autofocus: false,
confirmButtonText: '删除',
callback: (action: Action) => {
if (action === 'confirm') {
// 删除 form.sample_result.data
const index = form.sample_result.data.findIndex((item: any) => item.dir_name === sample_dir);
form.sample_result.data.splice(index, 1);
// 删除 form.classes_select[sample_dir]
delete form.classes_select[sample_dir];
// 删除 form.classes_select2[sample_dir]
delete form.classes_select2[sample_dir];
ElMessage({
type: 'success',
message: '删除成功'
})
}
},
})
}
return {
from_first_to_second,
back_to_first,
from_second_to_third,
back_to_second,
fileToClassify,
classifyToDownload,
downloadFile,
process_loading,
result_loading,
deleteOneSample,
}
}
\ No newline at end of file
import { ref } from 'vue'
import {
ElMessage,
} from 'element-plus'
export function useCropBox(form: any, canvas: any, ctx: any) {
const isDrawing = ref(false)
const startX = ref(0)
const startY = ref(0)
const endX = ref(0)
const endY = ref(0)
const boxes = ref<Array<{ start: number[], end: number[] }>>([])
function generate_canvas() {
const el = document.getElementById('crop-canvas') as HTMLCanvasElement
if (el) {
canvas.value = el
ctx.value = canvas.value.getContext('2d')
// 设置 canvas 尺寸与视频一致
const video = document.getElementById('video-player') as HTMLVideoElement
if (video) {
if (video.videoWidth > 0 && video.videoHeight > 0) {
console.log('video size =', video.videoWidth, video.videoHeight)
canvas.value.width = video.videoWidth
canvas.value.height = video.videoHeight
canvas.value.style.zIndex = '2'
drawBoxes()
}
}
}
}
function startDrawing(e: MouseEvent) {
if (boxes.value.length >= 2) {
while (boxes.value.length > 2) {
boxes.value.pop()
}
return
}
const rect = canvas.value!.getBoundingClientRect()
isDrawing.value = true
startX.value = e.clientX - rect.left
startY.value = e.clientY - rect.top
endX.value = startX.value
endY.value = startY.value
console.log('开始绘制,起点:', startX.value, startY.value)
}
function draw(e: MouseEvent) {
if (!isDrawing.value) return
const rect = canvas.value!.getBoundingClientRect()
endX.value = e.clientX - rect.left
endY.value = e.clientY - rect.top
clearCanvas()
drawBoxes()
drawRect(startX.value, startY.value, endX.value - startX.value, endY.value - startY.value)
}
function endDrawing() {
if (!isDrawing.value) return
isDrawing.value = false
boxes.value.push({
start: [startX.value, startY.value],
end: [endX.value, endY.value]
})
clearCanvas()
drawBoxes()
console.log('结束绘制,终点:', endX.value, endY.value)
console.log('框', boxes.value.length, '个: ', boxes.value)
if (boxes.value.length === 1) {
form.crop_range = boxes.value[0]
}
if (boxes.value.length === 2) {
if (boxes.value[1].start[0] < boxes.value[0].start[0]
|| boxes.value[1].start[1] < boxes.value[0].start[1]
|| boxes.value[1].end[0] > boxes.value[0].end[0]
|| boxes.value[1].end[1] > boxes.value[0].end[1]) {
ElMessage({
message: '扫描框必须在裁剪框内',
type: 'warning'
})
reset()
} else {
form.box_range = boxes.value[1]
form.is_set = true
}
}
}
function drawRect(x: number, y: number, width: number, height: number) {
const c = ctx.value!
c.strokeStyle = 'green'
c.lineWidth = 2
c.strokeRect(x, y, width, height)
}
function drawBoxes() {
boxes.value.forEach(box => {
drawRect(box.start[0], box.start[1], box.end[0] - box.start[0], box.end[1] - box.start[1])
})
}
function clearCanvas() {
const c = canvas.value!
ctx.value!.clearRect(0, 0, c.width, c.height)
}
function reset() {
if (boxes.value.length === 0) {
return
}
while (boxes.value.length > 0) {
boxes.value.pop()
}
clearCanvas()
form.crop_range = {}
form.box_range = {}
form.is_set = false
console.log('重置绘制数据')
}
return {
startDrawing,
draw,
endDrawing,
clearCanvas,
reset,
generate_canvas
}
}
\ No newline at end of file
import { ref } from 'vue'
import {
ElMessage,
} from 'element-plus'
export function useCropBox(form: any, canvas: any, ctx: any) {
const isDrawing = ref(false)
const startX = ref(0)
const startY = ref(0)
const endX = ref(0)
const endY = ref(0)
const boxes = ref<Array<{ start: number[], end: number[] }>>([])
const draggingBoxIndex = ref<number | null>(null) // 正在拖动的框索引
const resizingBoxIndex = ref<number | null>(null) // 正在缩放的框索引
const resizeHandle = ref<'nw' | 'ne' | 'sw' | 'se' | null>(null) // 缩放角方向
const dragStart = ref({ x: 0, y: 0 }) // 拖动起始点
const boxStart = ref({ x: 0, y: 0, width: 0, height: 0 }) // 框原始位置
function isInsideBox(x: number, y: number, box: any): boolean {
return (
x >= box.start[0] &&
x <= box.end[0] &&
y >= box.start[1] &&
y <= box.end[1]
)
}
function getHandleAt(x: number, y: number, box: any): 'nw' | 'ne' | 'sw' | 'se' | null {
const size = 10
const [x1, y1] = box.start
const [x2, y2] = box.end
if (Math.abs(x - x1) < size && Math.abs(y - y1) < size) return 'nw'
if (Math.abs(x - x2) < size && Math.abs(y - y1) < size) return 'ne'
if (Math.abs(x - x1) < size && Math.abs(y - y2) < size) return 'sw'
if (Math.abs(x - x2) < size && Math.abs(y - y2) < size) return 'se'
return null
}
function onMouseDown(e: MouseEvent) {
const rect = canvas.value!.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
// 检查是否点击到了某个框的角
for (let i = 0; i < boxes.value.length; i++) {
const box = boxes.value[i]
const handle = getHandleAt(x, y, box)
if (handle) {
resizingBoxIndex.value = i
resizeHandle.value = handle
return
}
if (isInsideBox(x, y, box)) {
draggingBoxIndex.value = i
dragStart.value = { x, y }
const [bx1, by1] = box.start
const [bx2, by2] = box.end
boxStart.value = {
x: bx1,
y: by1,
width: bx2 - bx1,
height: by2 - by1
}
return
}
}
}
function onMouseMove(e: MouseEvent) {
const rect = canvas.value!.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top
if (resizingBoxIndex.value !== null && resizeHandle.value) {
const i = resizingBoxIndex.value
const box = boxes.value[i]
const [bx1, by1] = box.start
const [bx2, by2] = box.end
switch (resizeHandle.value) {
case 'nw':
boxes.value[i] = { start: [x, y], end: [bx2, by2] }
break
case 'ne':
boxes.value[i] = { start: [bx1, y], end: [x, by2] }
break
case 'sw':
boxes.value[i] = { start: [x, by1], end: [bx2, y] }
break
case 'se':
boxes.value[i] = { start: [bx1, by1], end: [x, y] }
break
}
clearCanvas()
drawBoxes()
if (i === 0) {
form.crop_range = boxes.value[0]
} else {
form.box_range = boxes.value[1]
}
} else if (draggingBoxIndex.value !== null) {
const i = draggingBoxIndex.value
const dx = x - dragStart.value.x
const dy = y - dragStart.value.y
const box = boxes.value[i]
boxes.value[i] = {
start: [box.start[0] + dx, box.start[1] + dy],
end: [box.end[0] + dx, box.end[1] + dy]
}
clearCanvas()
drawBoxes()
if (i === 0) {
form.crop_range = boxes.value[0]
} else {
form.box_range = boxes.value[1]
}
}
}
function onMouseUp() {
draggingBoxIndex.value = null
resizingBoxIndex.value = null
resizeHandle.value = null
}
function generate_canvas() {
const el = document.getElementById('crop-canvas') as HTMLCanvasElement
if (el) {
canvas.value = el
ctx.value = canvas.value.getContext('2d')
// 设置 canvas 尺寸与视频一致
const video = document.getElementById('video-player') as HTMLVideoElement
if (video) {
if (video.videoWidth > 0 && video.videoHeight > 0) {
console.log('video size =', video.videoWidth, video.videoHeight)
canvas.value.width = video.videoWidth
canvas.value.height = video.videoHeight
canvas.value.style.zIndex = '2'
drawBoxes()
}
}
// 绑定事件监听器
canvas.value.addEventListener('mousedown', onMouseDown)
canvas.value.addEventListener('mousemove', onMouseMove)
canvas.value.addEventListener('mouseup', onMouseUp)
canvas.value.addEventListener('mouseleave', onMouseUp)
}
}
function startDrawing(e: MouseEvent) {
if (boxes.value.length >= 2) {
while (boxes.value.length > 2) {
boxes.value.pop()
}
return
}
const rect = canvas.value!.getBoundingClientRect()
isDrawing.value = true
startX.value = e.clientX - rect.left
startY.value = e.clientY - rect.top
endX.value = startX.value
endY.value = startY.value
console.log('开始绘制,起点:', startX.value, startY.value)
}
function draw(e: MouseEvent) {
if (!isDrawing.value) return
const rect = canvas.value!.getBoundingClientRect()
endX.value = e.clientX - rect.left
endY.value = e.clientY - rect.top
clearCanvas()
drawBoxes()
drawRect(startX.value, startY.value, endX.value - startX.value, endY.value - startY.value)
}
function endDrawing() {
if (!isDrawing.value) return
isDrawing.value = false
boxes.value.push({
start: [startX.value, startY.value],
end: [endX.value, endY.value]
})
clearCanvas()
drawBoxes()
console.log('结束绘制,终点:', endX.value, endY.value)
console.log('框', boxes.value.length, '个: ', boxes.value)
if (boxes.value.length === 1) {
form.crop_range = boxes.value[0]
}
if (boxes.value.length === 2) {
if (boxes.value[1].start[0] < boxes.value[0].start[0]
|| boxes.value[1].start[1] < boxes.value[0].start[1]
|| boxes.value[1].end[0] > boxes.value[0].end[0]
|| boxes.value[1].end[1] > boxes.value[0].end[1]) {
ElMessage({
message: '扫描框必须在裁剪框内',
type: 'warning'
})
reset()
} else {
form.box_range = boxes.value[1]
form.is_set = true
}
}
}
function drawRect(x: number, y: number, width: number, height: number) {
const c = ctx.value!
c.strokeStyle = 'green'
c.lineWidth = 2
c.strokeRect(x, y, width, height)
}
function drawBoxes() {
boxes.value.forEach(box => {
drawRect(box.start[0], box.start[1], box.end[0] - box.start[0], box.end[1] - box.start[1])
})
}
function clearCanvas() {
const c = canvas.value!
ctx.value!.clearRect(0, 0, c.width, c.height)
}
function reset() {
if (boxes.value.length === 0) {
return
}
while (boxes.value.length > 0) {
boxes.value.pop()
}
clearCanvas()
form.crop_range = {}
form.box_range = {}
form.is_set = false
console.log('重置绘制数据')
// 移除事件监听器
const el = document.getElementById('crop-canvas') as HTMLCanvasElement
if (el) {
el.removeEventListener('mousedown', onMouseDown)
el.removeEventListener('mousemove', onMouseMove)
el.removeEventListener('mouseup', onMouseUp)
el.removeEventListener('mouseleave', onMouseUp)
}
}
return {
startDrawing,
draw,
endDrawing,
clearCanvas,
reset,
generate_canvas
}
}
\ No newline at end of file
.home-container {
width: 100%;
}
.title {
:is(span) {
font-size: 25px;
font-weight: bold;
color: #181818;
}
text-align: center;
margin: 20px 0 0 0;
}
.subtitle {
:is(span) {
font-size: 13px;
color: #181818;
}
text-align: center;
margin: 0 0 20px 0;
}
.progress {
margin: 10px 50px;
text-align: center;
}
.content-container {
margin: 20px;
display: flex;
flex-direction: row; /* 默认横向排列 */
justify-content: center; /* 水平居中 */
/* box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 添加阴影效果 */
border-radius: 8px; /* 可选:添加圆角 */
/* background-color: #fff; /* 可选:添加背景色 */
@media (max-width: 768px) { /* 当屏幕宽度小于768px时变为竖向排列 */
flex-direction: column;
}
}
/* 几个页面的公共样式 */
.upload-section,
.progress-section,
.report-section {
padding: 20px;
/* 标题 */
.section-title {
font-size: 18px;
font-weight: bold;
color: #181818;
}
/* 描述 */
.section-desc {
font-size: 12px;
}
}
/* 上传页 */
.upload-section {
/* 上传区域 */
.upload-div {
margin: 20px 0;
/* border: 2px dashed #dddfe5; */
border-radius: 4px;
}
/* 已上传文件 */
.uploaded-div {
margin: 20px 0;
padding: 20px 10px;
border-radius: 4px;
border: 1px dashed #dddfe5; /* 添加边框样式 */
.uploaded-file-info {
position: relative;
width: auto;
}
}
/* 按钮 */
.button {
display: flex;
flex-direction: column;
align-items: flex-start;
.next {
align-self: flex-end;
}
}
}
/* 处理页 */
.progress-section {
.row {
display: flex;
margin: 10px 10px 0 0;
.images {
width: 150px;
display: inline-block;
margin-right: 10px; /* 可选:为每个图片之间添加间距 */
}
}
/* 按钮 */
.button {
background-color: #f56c6c;;
margin: 20px 0;
padding: 10px;
display: flex;
flex-direction: row; /* 修改为行排列 */
justify-content: space-between; /* 使按钮两端对齐 */
align-items: center; /* 垂直居中对齐 */
position: fixed;
bottom: 20px;
gap: 30px;
}
}
/* 结果页*/
.report-section {
/* 按钮 */
.button {
margin: 20px 0;
display: flex;
flex-direction: row; /* 修改为行排列 */
justify-content: space-between; /* 使按钮两端对齐 */
align-items: center; /* 垂直居中对齐 */
}
}
<script setup lang="ts">
import utils from '@/utils/utils'
import { onUpdated, onMounted, reactive, ref } from 'vue'
import { detectDeviceType } from './compositions/common'
import { myFileUpload } from './compositions/fileUpload'
import { useCropBox } from './compositions/useCropBox'
import { onProcessing } from './compositions/process'
import {
Check,
Delete,
Edit,
Message,
Search,
Star,
} from '@element-plus/icons-vue'
const title = ref('元芒数字')
const form = reactive({
source: '',
task_id: '',
file_path: '',
file_path_domain: '',
file_name: '',
file_size: 0,
crop_range: {},
box_range: {},
is_set: false,
classes_select: <Record<string, string>>({}),
classes_select2: <Record<string, string>>({}),
sample_result: {
root_path: '',
data: [{
dir_name: '',
images: [],
}]
}, // 样本处理结果
sample_path: '',
// 目标帧率
target_fps: '10',
})
const steps_active = ref(0)
const FileSizeLimitM = 500
const file_loading = ref(false)
const my_file_upload = myFileUpload(FileSizeLimitM, form, file_loading)
const upload = my_file_upload.upload
const actionUrl = my_file_upload.actionUrl
// 裁剪
const videoPlayer = ref<HTMLVideoElement | null>(null)
const canvas = ref<HTMLCanvasElement | null>(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const cropBox = useCropBox(form, canvas, ctx)
// 处理
const classes = [{value:'0',label:'0'},{value:'1',label:'1'},{value:'2',label:'2'},{value:'3',label:'3'}]
const process_loading = ref(false)
const result_loading = ref(false)
const processing = onProcessing(form, steps_active, process_loading, result_loading)
// 目标帧率选项
const fps_options = [
{value: '10', label: '10'},
{value: '25', label: '25'},
]
onMounted(async () => {
// 检测设备类型
form.source = detectDeviceType();
// 设置页面标题
document.title = title.value;
// 生成task_id
form.task_id = utils.genDateTimeStr();
console.log('页面加载,task_id =', form.task_id);
})
const onVideoLoaded = () => {
if (steps_active.value === 0 && form.is_set && form.sample_result.root_path !== '') {
console.log('切换到步骤 0 视频加载完成');
cropBox.generate_canvas();
}
};
</script>
<template>
<main class="home-container">
<!-- 标题 -->
<div class="title"><el-text>生成训练样本</el-text></div>
<div class="subtitle"><el-text>上传视频生成训练样本</el-text></div>
<!-- 步骤条 -->
<div>
<el-steps :active="steps_active" align-center finish-status="success">
<el-step title="上传视频并设置范围"/>
<el-step title="样本分类"/>
<el-step title="下载样本"/>
</el-steps>
</div>
<div class="content-container">
<!-- 文件页 -->
<div class="upload-section" v-if="steps_active === 0">
<div><el-text class="section-title">上传文件、设置范围</el-text></div>
<div><el-text class="section-desc">上传视频后,可以播放、暂停,然后点击设置,先画裁剪框,再画扫描框</el-text></div>
<div class="upload-div" v-loading="file_loading" v-if="!form.file_path">
<el-upload
ref="upload"
:show-file-list="false"
:limit="1"
drag
accept=".mp4"
:action="actionUrl"
:on-success="my_file_upload.handleUploadSuccess"
:on-exceed="my_file_upload.handleUploadExceed"
:on-error="my_file_upload.handleUploadError"
:on-remove="my_file_upload.handleRemoveFile"
:before-upload="my_file_upload.handleBeforeUpload"
:on-progress="my_file_upload.handleUploadProgress"
list-type="picture"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖拽文件到这里 或 <em>浏览文件</em></div>
<div class="el-upload__tip" slot="tip">文件格式mp4, 大小限制{{FileSizeLimitM}}M</div>
</el-upload>
</div>
<div class="uploaded-div" v-if="form.file_path" >
<div class="uploaded-file-info">
<video id="video-player" :src="form.file_path_domain" controls @loadeddata="onVideoLoaded"></video>
<canvas
id="crop-canvas"
style="background-color: rgba(255, 0, 0, 0.1); position: absolute; left: 0; z-index: -1;"
@mousedown="cropBox.startDrawing"
@mousemove="cropBox.draw"
@mouseup="cropBox.endDrawing"
>
</canvas>
</div>
<el-button color="#181818" @click="cropBox.generate_canvas">设置</el-button>
<el-button color="#181818" @click="cropBox.reset">重置</el-button>
<el-text style="color: #181818; margin-left: 10px; font-weight: bold;">目标帧率:</el-text>
<el-select v-model="form.target_fps" placeholder="请选择" style="width: 80px">
<el-option
v-for="item in fps_options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<p>裁剪框坐标:{{ form.crop_range }}</p>
<p>扫描框坐标:{{ form.box_range }}</p>
<!-- <p>{{ form.task_id }}</p> -->
</div>
<div class="button">
<el-button class="next" color="#181818" size="large"
@click="processing.fileToClassify"
:disabled="!form.is_set">Next</el-button>
</div>
</div>
<!-- 处理页 -->
<div class="progress-section" v-if="steps_active === 1">
<div><el-text class="section-title">样本分类</el-text></div>
<div><el-text class="section-desc">标注所有样本的所属分类(当前fps={{ form.target_fps }},点击图片可查看原图或放大)</el-text></div>
<!-- 内容 -->
<div v-loading="process_loading">
<div class="row" v-for="dir in form.sample_result.data">
<div class="images" v-for="image in dir.images">
<el-image :src="form.sample_result.root_path + dir.dir_name + '/' + form.classes_select2[dir.dir_name] + '/' + image" lazy
:zoom-rate="1.2"
:max-scale="2"
:min-scale="1"
:preview-src-list="dir.images.map(img => form.sample_result.root_path + dir.dir_name + '/' + form.classes_select2[dir.dir_name] + '/' + img)"
show-progress
:initial-index="dir.images.indexOf(image)"
fit="cover"
:hide-on-click-modal="true"
:infinite="false"
>
<template #progress="{ activeIndex, total }">
<span>{{ activeIndex + '/' + (total-1) }}</span>
</template>
</el-image>
<p>{{ image }}</p>
</div>
<div class="classifications">
<p style="margin-bottom: 5px;">{{ dir.dir_name }}</p>
<el-select v-model="form.classes_select[dir.dir_name]"
placeholder="分类" size="large" style="width: 100px;"
:class="[`class-select-${form.classes_select[dir.dir_name] || 'default'}`]"
>
<el-option
v-for="item in classes"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="danger" :icon="Delete" circle @click="processing.deleteOneSample(dir.dir_name)" style="margin-left: 10px;"/>
</div>
</div>
</div>
<!-- <p>{{ form.classes_select }}</p> -->
<!-- 按钮 -->
<div class="button">
<el-button class="back" size="large" @click="processing.back_to_first">Back</el-button>
<el-button class="next" color="#181818" size="large"
@click="processing.classifyToDownload"
>Next</el-button>
</div>
</div>
<!-- 结果页 -->
<div class="report-section" v-if="steps_active === 2">
<div><el-text class="section-title">结果</el-text></div>
<div><el-text class="section-desc">下载样本</el-text></div>
<!-- 内容 -->
<div v-loading="result_loading">
<el-text class="progress-info-sub">
{{ form.sample_path}}
</el-text>
</div>
<!-- 按钮 -->
<div class="button">
<el-button class="back" size="large" @click="processing.back_to_second">Back</el-button>
<el-button class="next" color="#181818" size="large"
@click="processing.downloadFile(form.sample_path)"
:disabled="!(form.sample_path && form.sample_path.length > 0)"
>Download</el-button>
</div>
</div>
</div>
</main>
</template>
<!-- 样式 只在当前页面生效,优先级比组件样式低 -->
<style lang="scss" scoped src="./index.css"></style>
<!-- 全局样式,如果要改组件样式,得在这里改,但为了避免所有组件都改,这里可以设置class层级 -->
<style lang="scss">
.progress-section {
.classifications {
.class-select-1 {
.el-select__wrapper{
background-color: aquamarine;
}
}
.class-select-2 {
.el-select__wrapper{
background-color: rgb(121, 160, 245);
}
}
.class-select-3 {
.el-select__wrapper{
background-color: rgb(164, 139, 246);
}
}
}
}
</style>
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"typings",
"env.d.ts",
"src/**/*",
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.vue",
"config/index.ts",
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
"typeRoots": [
"typings"
],
"lib": ["es2021", "dom"]
}
}
\ No newline at end of file
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"compilerOptions": {
"lib": ["es2021", "dom"],
}
}
\ No newline at end of file
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
],
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": [
"node"
],
}
}
\ No newline at end of file
/// <reference path="./types/index.d.ts" />
declare module 'element-plus/dist/locale/zh-cn.mjs'
declare module 'vue-drag-resize';
\ No newline at end of file
/// <reference path="./wm/index.d.ts" />
/// <reference path="./lib.wm.api.d.ts" />
/// <reference path="./lib.wm.event.d.ts" />
declare namespace Wm {
interface UploadResult {
code: int
data: [
{
id: int
key: string
path: string
url: string
}
]
message: string
}
interface LoginParams {
username: string
password: string
captcha?: string
}
}
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
base: './',
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
proxy: {
'/api/': {
// target: 'http://127.0.0.1:5001/', // 本机后端服务
// target: 'http://wm-tools-backend.frp.wmdigit.com:8888/', // new3090后端服务
target: 'http://wm-tools-backend-test.frp.wmdigit.com:8888/', // 测试后端服务
changeOrigin: true,
rewrite: (path: any) => path.replace(/^\/api/, '')
}
},
host: '0.0.0.0',
port: 9529,
https: false,
}
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment