Repository: datar5/yolov8-flask-vue-deploy Branch: main Commit: b16cb7b364a6 Files: 62 Total size: 172.8 MB Directory structure: gitextract_4lvw2n3m/ ├── README.md ├── detection-backend/ │ ├── api.py │ ├── app.py │ ├── config.py │ ├── demo.http │ ├── extension.py │ ├── modelPredict.py │ ├── models.py │ └── weights/ │ ├── yolov5nu_coco.pt │ ├── yolov5su_coco.pt │ ├── yolov8l_coco.pt │ ├── yolov8m_coco.pt │ ├── yolov8n_coco.pt │ └── yolov8s_coco.pt └── detection-fontend/ ├── LICENSE ├── README.md ├── README_EN.md ├── auto-imports.d.ts ├── components.d.ts ├── index.html ├── package.json ├── public/ │ ├── table.json │ └── template.xlsx ├── src/ │ ├── App.vue │ ├── api/ │ │ └── index.ts │ ├── assets/ │ │ └── css/ │ │ ├── color-dark.css │ │ ├── icon.css │ │ └── main.css │ ├── components/ │ │ ├── header.vue │ │ ├── sidebar.vue │ │ └── tags.vue │ ├── main.ts │ ├── router/ │ │ └── index.ts │ ├── store/ │ │ ├── permiss.ts │ │ ├── sidebar.ts │ │ └── tags.ts │ ├── utils/ │ │ └── request.ts │ ├── viewDetect/ │ │ ├── ResultVue.vue │ │ ├── detectVue.vue │ │ └── uploadFile.vue │ ├── views/ │ │ ├── 403.vue │ │ ├── 404.vue │ │ ├── charts.vue │ │ ├── dashboard.vue │ │ ├── donate.vue │ │ ├── editor.vue │ │ ├── export.vue │ │ ├── form.vue │ │ ├── home.vue │ │ ├── icon.vue │ │ ├── import.vue │ │ ├── login.vue │ │ ├── markdown.vue │ │ ├── permission.vue │ │ ├── table.vue │ │ ├── tabs.vue │ │ ├── upload.vue │ │ └── user.vue │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: README.md ================================================ # Yolov8-flask-vue(本科毕设) 这是一个基于ultralytics的一个部署到flask后端,然后vue作为前端所展示的一个通用的Yolo目标检测的展示页面,其实本质上类似于有着web页面外观的本地exe项目(因为数据库是个本地文件,放在sqlite上) 需要在pycharm等IDE安装好sqlite相关连接 基本上只要是ultralytics训练好的模型都可以运行,也就是说官方提供的COCO训练好的模型都可以在这个Yolo通用前端平台上使用 后端需要安装好环境才能运行,要注意先运行后端再是前端,因为前端的环境是很容易装好的 前端需要安装好Node.js到电脑上,然后只需要将,只需要在WebStorm上等随意的IDE点击 package.json 上的播放按钮就可以了 ![img.png](img/img.png) 后端是flask,要安装很多的库,先安装好相应的库,IDE会提示python那些库没有安装上去的 这是毕设模型变体的训练过程,其中comet的Yolov5su当中因为VPN不稳定中间断了一些就不显示了 ![img.png](img/mAP50.png) 个人毕设训练这些模型不方便公开,这里只是提供了一个通用的YOLO实现平台,可以将自己训练好的模型放入到上面去来实现目标检测展示功能 在这个毕设里面一共是200多张图片,6个yolo模型,拉下来需要一定时间 图片上传来自于本地或者是url地址,但url地址有着严格要求 图片上传后不能修改名称,除非删除再上传,同理,模型也是如此 ### 展示效果 ![img.png](img/show3.png) ![img.png](img/show.png) ![img.png](img/show2.png) ![img.png](img/show4.png) ### 个人环境 #### 开发技术及工具的选择: Windows 11 Python 3.9.13 Pytorch 1.13.1+cu117 Jetbrains-IDEA系列2022 #### 开发环境: 数据库:使用的是sqlite本地的 硬件:电脑内存32G左右,GPU RTX A5000 Windows 11以上操作系统; 软件: Anaconda22.9.0 + python3.9.13 + Pytorch1.13.1+cu117 + Pycharm(或者是DataSpell) + WebStorm + Node.js 18.14.1 (x64) ### 低配部署环境: 硬件:电脑内存16G左右,GPU RTX 1050Ti Windows 10以上操作系统; 软件: Pycharm(或者是DataSpell) + WebStorm + Node.js 18.14.1 (x64) + flask版本2.2左右 ### 后续说明 本人现在忙着准备深大的研究生复试(❌),差一名,初试分数太低了(不到320) 备考二战,初试结束,今年11408是目前最难的一年,个人觉得408以后难度还会刷新 毕业论文已经完成 现在Yolov8库的更新情况不知道怎么样了,这个项目有半年没碰过了。。 后面会对这个项目细节相关进行说明 ### 感谢 & 技术提供 ultralytics官网: https://github.com/ultralytics/ultralytics ![img.png](img/img9.png) ![img.png](img/img2.png) 实验数据记录: https://www.comet.com/site/lp/yolov5-with-comet/?utm_source=yolov5&utm_medium=partner&utm_campaign=partner_yolov5_2022&utm_content=github ![img.png](img/img6.png) 前端提供: https://github.com/lin-xin/vue-manage-system ================================================ FILE: detection-backend/api.py ================================================ import os import shutil from flask.views import MethodView, request from models import File, WModel, Object, Result from extension import db from config import save_path, \ result_path, img_url_show, result_url_show, cache_save_model_path, weights_path class FileApi(MethodView): """ 后端接口部分 RESTful API风格 """ def __init__(self): super(FileApi, self).__init__() def get(self, fid): """ 查询数据 :param fid: 文件id :return: json """ try: if not fid: files: [File] = File.query.all() results = [ { 'fid': file.fid, 'name': file.name, 'timestamp': file.timestamp, 'type': file.type, 'origin': file.origin, 'width': file.width, 'height': file.height } for file in files ] else: file = File.query.get(fid) results = { 'fid': file.fid, 'name': file.name, 'timestamp': file.timestamp, 'type': file.type, 'origin': file.origin, 'width': file.width, 'height': file.height } return { 'status': 'success', 'message': '数据查询成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '数据查询出现异常', 'info': str(e) } def delete(self, fid): """ 删除操作 :param fid: 文件id :return: json """ try: file: File = File.query.get(fid) db.session.delete(file) db.session.commit() return { 'status': 'success', 'message': '数据删除成功', } except Exception as e: return { 'status': 'error', 'message': '数据删除出现异常', 'info': str(e) } def post(self): # 添加数据 """ 添加操作 :return: json """ try: height = request.json.get("height") width = request.json.get("width") name = request.json.get("name") type = request.json.get("type") origin = request.json.get("origin") file = File() file.name = name file.height = height file.width = width file.type = type file.origin = origin db.session.add(file) db.session.commit() return { 'status': 'success', 'message': '数据添加成功' } except Exception as e: return { 'status': 'error', 'message': '数据添加出现异常', 'info': str(e) } class WModelApi(MethodView): """ 后端接口部分 RESTful API风格 """ def __init__(self): super(WModelApi, self).__init__() def get(self, wid): """ 查询数据 :param wid: 模型的wid :return: json """ try: if not wid: wmodels: [WModel] = WModel.query.all() results = [ { 'wid': wmodel.wid, 'model_name': wmodel.model_name, 'model_type': wmodel.model_type, 'model_dataset': wmodel.model_dataset, } for wmodel in wmodels ] else: wmodel: WModel = WModel.query.get(wid) results = { 'wid': wmodel.wid, 'model_name': wmodel.model_name, 'model_type': wmodel.model_type, 'model_dataset': wmodel.model_dataset, } return { 'status': 'success', 'message': '数据查询成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '数据查询出现异常', 'info': str(e) } def delete(self, wid): """ 删除操作 :param rid: 结果id :return: json """ try: if wid > 6: wmodel: WModel = WModel.query.get(wid) delete_path = weights_path + wmodel.model_name + '.pt' os.remove(delete_path) db.session.delete(wmodel) db.session.commit() return { 'status': 'success', 'message': '数据删除成功', } else: return { 'status': 'error', 'message': '不允许删除前六个模型', } except Exception as e: return { 'status': 'error', 'message': '数据删除出现异常', 'info': str(e) } def put(self, wid): """ 修改模型信息 :param wid: 模型id :return: json """ try: model: WModel = WModel.query.get(wid) model.model_name = request.json.get('model_name') model.model_type = request.json.get('model_type') model.model_dataset = request.json.get('model_dataset') db.session.commit() return { 'status': 'success', 'message': '数据修改成功' } except Exception as e: return { 'status': 'error', 'message': '数据修改出现异常', 'info': str(e) } def post(self): # 添加数据 """ 添加操作 :return: json """ try: model_name = request.json.get('model_name') model_type = request.json.get('model_type') model_dataset = request.json.get('model_dataset') wmodel: WModel = WModel() wmodel.model_name = model_name wmodel.model_type = model_type wmodel.model_dataset = model_dataset new_path = weights_path + model_name + '.pt' shutil.copy(src=cache_save_model_path, dst=new_path) db.session.add(wmodel) db.session.commit() return { 'status': 'success', 'message': '数据添加成功' } except Exception as e: return { 'status': 'error', 'message': '数据添加出现异常', 'info': str(e) } class ResultApi(MethodView): """ 后端接口部分 RESTful API风格 """ def __init__(self): super(ResultApi, self).__init__() def get(self, rid): """ 查询数据 :param rid: 结果rid :return: json """ try: if not rid: n_results: [Result] = Result.query.all() results = [ { 'rid': result.rid, 'wid': result.wid, # 首要默认为检测模型 'timestamp': result.timestamp, # 时间戳 'type': result.type, # 文件类型 'addition': result.addition, # 额外说明 'mission_type': result.mission_type, # 任务类型 'filename': result.filename, # 被检测的源头文件 'num': result.num # 一共有多少个检测物体 } for result in n_results ] else: result: Result = Result.query.get(rid) results = { 'rid': result.rid, 'wid': result.wid, # 同理 'timestamp': result.timestamp, 'type': result.type, 'addition': result.addition, 'mission_type': result.mission_type, 'filename': result.filename, 'num': result.num } return { 'status': 'success', 'message': '数据查询成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '数据查询出现异常', 'info': str(e) } def delete(self, rid): """ 删除操作,前面是与 @app.route('/delete/result', methods=['DELETE']) 紧接着操作的, 其对应的request.json格式是 { "addition": "单独的检测任务", "filename": "b7a8e795-ff80fddf", "mission_type": "detection", "num": 23, "rid": 1, "timestamp": 1677256499, "type": "jpg", "wid": 1 } 前端从@app.route('/delete/result', methods=['DELETE'])相应完了之后将json的rid传入此函数中 :param rid: 结果id :return: json """ try: result: Result = Result.query.get(rid) path = result_path + result.filename \ + '_' + str(result.timestamp) + '.' + result.type os.remove(path) db.session.delete(result) db.session.commit() return { 'status': 'success', 'message': '数据删除成功', } except Exception as e: return { 'status': 'error', 'message': '数据删除出现异常', 'info': str(e) } def put(self, rid): try: result: Result = Result.query.get(rid) result.addition = request.json.get('addition') result.mission_type = request.json.get('mission_type') db.session.commit() return { 'status': 'success', 'message': '数据修改成功', } except Exception as e: return { 'status': 'error', 'message': '数据修改出现异常', 'info': str(e) } def post(self): # 添加数据 """ 添加操作, json来自 @app.route('/detect', methods=['POST']) 那里 example: { "message": "检测成功", 传入的json为 { "addition": "单独的检测任务", "attr": "jpg", "filename": "b7a8e795-ff80fddf", "mission_type": "detection", "num": 21, "timestamp": 1677250829, "wid": 2 } } :return: json """ try: form = request.json # print(form) result = Result() result.wid = form.get('wid') result.num = form.get('num') result.type = form.get('type') result.timestamp = form.get('timestamp') result.addition = form.get('addition') result.mission_type = form.get('mission_type') result.filename = form.get('filename') db.session.add(result) db.session.commit() return { 'status': 'success', 'message': '数据添加成功' } except Exception as e: return { 'status': 'error', 'message': '数据添加出现异常', 'info': str(e) } class ObjApi(MethodView): """ 后端接口部分 RESTful API风格 """ def __init__(self): super(ObjApi, self).__init__() def get(self, rid): """ 查询数据 :param rid: 结果rid,外键查询 :return: json """ try: objects: [Object] = db.session.query(Object).filter(Object.rid == rid).all() # 根据外键rid来查询 results = [ { 'oid': object.oid, 'rid': object.rid, 'cls': object.cls, 'x1': object.x1, 'y1': object.y1, 'x2': object.x2, 'y2': object.y2, 'conf': object.conf } for object in objects ] return { 'status': 'success', 'message': '数据查询成功', 'length': len(results), 'results': results } except Exception as e: return { 'status': 'error', 'message': '数据查询出现异常', 'info': str(e) } def delete(self, rid): """ 删除操作,首先执行此函数, 然后再执行@app.route('/delete/result', methods=['DELETE'])删除目标文件操作 最后再将对应的result实例变量从数据库删除掉 :param rid: 结果id :return: json """ try: objects: [Object] = db.session.query(Object).filter(Object.rid == rid).all() # 根据外键rid来查询 for object in objects: db.session.delete(object) db.session.commit() return { 'status': 'success', 'message': '数据删除成功', } except Exception as e: return { 'status': 'error', 'message': '数据删除出现异常', 'info': str(e) } def post(self): # 添加数据 """ 添加操作, json来自 @app.route('/detect', methods=['POST']) 那里 example: { "message": "检测成功", "results": { "objects": [ ..... ], "log": { "addition": "单独的检测任务", "attr": "jpg", "filename": "b7a8e795-ff80fddf", "mission_type": "detection", "num": 21, "timestamp": 1677250829, "wid": 2 } }, "status": "success" } :return: json """ try: objects = request.json['results']["objects"] _about = request.json['results']['log'] result: Result = db.session.query(Result).filter(Result.timestamp == _about["timestamp"]) \ .filter(Result.filename == _about['filename']).first() for obj in objects: object = Object() object.rid = result.rid object.cls = obj['cls'] object.x1 = obj['x1'] object.y1 = obj['y1'] object.x2 = obj['x2'] object.y2 = obj['y2'] object.conf = obj['conf'] db.session.add(object) db.session.commit() return { 'status': 'success', 'message': '数据添加成功' } except Exception as e: return { 'status': 'error', 'message': '数据添加出现异常', 'info': str(e) } class PageApi(MethodView): # 分页接口 def __init__(self): super(PageApi, self).__init__() def get(self): try: # print(request.args) curPage = int(request.args.get('curPage')) pageSize = int(request.args.get('pageSize')) tableName = request.args.get('tableName') # print(curPage, pageSize, tableName) results = [] if tableName == 'file': resultsIteam = File.query.order_by(File.fid.asc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items results = [ { 'fid': file.fid, 'name': file.name, 'timestamp': file.timestamp, 'type': file.type, 'origin': file.origin, 'width': file.width, 'height': file.height, 'img_url': img_url_show + str(file.name) + '.' + str(file.type) } for file in resultsIteam ] elif tableName == 'model': resultsIteam = WModel.query.order_by(WModel.wid.asc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items results = [ { 'wid': wmodel.wid, 'model_name': wmodel.model_name, 'model_type': wmodel.model_type, 'model_dataset': wmodel.model_dataset, } for wmodel in resultsIteam ] elif tableName == 'result': resultsIteam = Result.query.order_by(Result.rid.asc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items results = [ { 'rid': result.rid, 'wid': result.wid, 'timestamp': result.timestamp, 'type': result.type, 'addition': result.addition, 'mission_type': result.mission_type, 'filename': result.filename, 'num': result.num, 'img_url': result_url_show + str(result.filename) + '_' + str(result.timestamp) + '.' + str(result.type) } for result in resultsIteam ] else: pass return { 'status': 'success', 'message': '分页查找成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '分页查找出现异常', 'info': str(e) } class ObjectPageApi(MethodView): def __init__(self): super(ObjectPageApi, self).__init__() def get(self, rid): """ :param rid: Object检测子结果的分页实现 :return: """ try: curPage = int(request.args.get('curPage')) pageSize = int(request.args.get('pageSize')) # print(curPage, pageSize) resultsItem = Object.query \ .filter(Object.rid == rid).order_by(Object.conf.desc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items # print(resultsItem) results = [ { 'oid': object.oid, 'rid': object.rid, 'cls': object.cls, 'x1': object.x1, 'y1': object.y1, 'x2': object.x2, 'y2': object.y2, 'conf': object.conf } for object in resultsItem ] return { 'status': 'success', 'message': '分页查找成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '分页查找出现异常', 'info': str(e) } class NumApi(MethodView): # 分页显示数目的接口 def __init__(self): super(NumApi, self).__init__() self.config = ['file', 'model', 'result'] def get(self): """ :param tableName: :return: 总数数字 """ try: results = 0 # print(results) # curPage = request.args.get('curPage') # pageSize = request.args.get('pageSize') # print(request.args) tableName = request.args.get('tableName') rid = int(request.args.get('rid')) if tableName in self.config: if tableName == 'file': results = File.query.count() elif tableName == 'model': results = WModel.query.count() else: results = Result.query.count() else: if rid > 0: results = Object.query.filter(Object.rid == rid).count() return { 'status': 'success', 'message': '数量查找成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '数量查找出现异常', 'info': str(e) } class DeblurSearchApi(MethodView): # 分页实现模糊查找的接口 def __init__(self): super(DeblurSearchApi, self).__init__() def get(self): try: results = [] tableName = request.args.get('tableName') keyword = request.args.get('keyword') curPage = int(request.args.get('curPage')) pageSize = int(request.args.get('pageSize')) # print(tableName, keyword, curPage, pageSize) if tableName == 'file': resultsIteam = File.query.filter(File.name.like('%' + keyword + '%')) \ .order_by(File.fid.desc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items # print(len(resultsIteam)) results = [ { 'fid': file.fid, 'name': file.name, 'timestamp': file.timestamp, 'type': file.type, 'origin': file.origin, 'width': file.width, 'height': file.height, 'img_url': img_url_show + str(file.name) + '.' + str(file.type) } for file in resultsIteam ] elif tableName == 'model': resultsIteam = WModel.query.filter(WModel.model_name.like('%' + keyword + '%')) \ .order_by(WModel.wid.asc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items results = [ { 'wid': wmodel.wid, 'model_name': wmodel.model_name, 'model_type': wmodel.model_type, 'model_dataset': wmodel.model_dataset, } for wmodel in resultsIteam ] elif tableName == 'result': resultsIteam = Result.query.filter(Result.filename.like('%' + keyword + '%')) \ .order_by(Result.rid.desc()).paginate( page=curPage, per_page=pageSize, error_out=True ).items results = [ { 'rid': result.rid, 'wid': result.wid, 'timestamp': result.timestamp, 'type': result.type, 'addition': result.addition, 'mission_type': result.mission_type, 'filename': result.filename, 'num': result.num, 'img_url': result_url_show + str(result.filename) + '_' + str(result.timestamp) + '.' + str(result.type) } for result in resultsIteam ] else: pass return { 'status': 'success', 'message': '模糊查找成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '模糊查找出现异常', 'info': str(e) } class DeblurSearchNumApi(MethodView): def __init__(self): super(DeblurSearchNumApi, self).__init__() self.config = ['file', 'model', 'result'] def get(self): try: tableName = request.args.get('tableName') # rid = int(request.args.get('rid')) keyword = request.args.get('keyword') results = 0 if tableName in self.config: if tableName == 'file': results = File.query.filter(File.name .like('%' + keyword + '%')).count() elif tableName == 'model': results = WModel.query.filter(WModel.model_name .like('%' + keyword + '%')).count() else: results = Result.query.filter(Result.filename .like('%' + keyword + '%')).count() else: pass return { 'status': 'success', 'message': '数量关键词模糊查找成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '数量关键词模糊查找出现异常', 'info': str(e) } ================================================ FILE: detection-backend/app.py ================================================ import datetime import random import time from flask import Flask, request, make_response from ultralytics import YOLO import cv2 from config import save_path, \ result_path, default_weights, default_model_json, weights_path, cache_save_model_path from api import FileApi, ResultApi, ObjApi, WModelApi from api import PageApi, ObjectPageApi, NumApi from api import DeblurSearchApi, DeblurSearchNumApi from models import File, WModel from extension import db, cors import os import io import sys from PIL import Image import requests # 项目系统路径配置 curPath = os.path.dirname(os.path.abspath(__file__)) root_path = os.path.abspath(os.path.dirname(curPath) + os.path.sep + os.path.sep) sys.path.append(root_path) # 创建flask项目 app = Flask(__name__) # flask项目常量初始化设置 app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///detection.sqlite' app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["model"] = YOLO(default_weights) app.config["model_json"] = default_model_json app.config["cls"] = app.config["model"].names # 爬虫伪造头 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/95.0.4638.54 Safari/537.36 Edg/95.0.1020.30 ' } db.init_app(app) # 注册实例化 cors.init_app(app) # 注册跨域请求伪造相关 """ 对数据库的各个表相关操作添加相应路由规则 """ file_view = FileApi.as_view('file_api') app.add_url_rule('/files/', defaults={'fid': None}, view_func=file_view, methods=['GET']) # 查询 app.add_url_rule('/files', view_func=file_view, methods=['POST']) # 添加 app.add_url_rule('/files/', view_func=file_view, methods=['GET', 'DELETE']) # 查询,删除 wmodel_view = WModelApi.as_view('wmodel_api') app.add_url_rule('/wmodel/', defaults={'wid': None}, view_func=wmodel_view, methods=['GET']) # 查询 app.add_url_rule('/wmodel', view_func=wmodel_view, methods=['POST']) # 添加 app.add_url_rule('/wmodel/', view_func=wmodel_view, methods=['GET', 'PUT', 'DELETE']) # 查询,修改,删除 result_view = ResultApi.as_view('result_api') app.add_url_rule('/result/', defaults={'rid': None}, view_func=result_view, methods=['GET']) # 查询 app.add_url_rule('/result', view_func=result_view, methods=['POST']) # 添加 app.add_url_rule('/result/', view_func=result_view, methods=['GET', 'DELETE', 'PUT']) # 查询,删除,修改 obj_view = ObjApi.as_view('obj_api') app.add_url_rule('/obj/', defaults={'rid': 1}, view_func=obj_view, methods=['GET']) # 查询 app.add_url_rule('/obj', view_func=obj_view, methods=['POST']) # 添加 app.add_url_rule('/obj/', view_func=obj_view, methods=['GET', 'DELETE']) # 查询,删除 """ 对数据库的各个表相关分页查找实现的路由规则 各个表相关总数输出实现路由规则 """ page_view = PageApi.as_view('page_api') app.add_url_rule('/page', view_func=page_view, methods=['GET']) object_page_view = ObjectPageApi.as_view('objectPage_api') app.add_url_rule('/objectPage/', view_func=object_page_view, methods=['GET']) num_view = NumApi.as_view('num_api') app.add_url_rule('/num', view_func=num_view, methods=['GET']) """ 对数据库的各个表相关关键词实现模糊查找 各个表模糊查找相关总数输出实现路由规则 """ deblur_view = DeblurSearchApi.as_view('deblurSearch_api') app.add_url_rule('/deblurS', view_func=deblur_view, methods=['GET']) deblur_view_num = DeblurSearchNumApi.as_view('deblurSearchNum_api') app.add_url_rule('/deblurSNum', view_func=deblur_view_num, methods=['GET']) """ 涉及到对数据库(相关消息传递)之外的功能 比如 显示图片,爬取图片,切换模型,图片检测,文件上传,文件删除等等 """ # @app.route('/') # def hello_world(): # put application's code here # return 'Hello World!' """ 显示图片,上传图片相关操作 """ @app.route('/img/') def show_img(name): img_url = save_path + name if request.method == 'GET': if name is None: pass else: image_data = open(img_url, "rb").read() response = make_response(image_data) response.headers['Content-Type'] = 'image/jpg' return response else: pass @app.route('/upload/pic/', methods=['POST']) def upload_load(): """ 从本地上上传图片到后端服务器上 :return: json """ try: filename = request.files.get('file').filename name = filename.split(".")[0] + "_" + str(int(time.time())) type = filename.split('.')[-1] origin = 'localhost://' + filename data = request.files.get('file') new_path = save_path + name + '.' + type data.save(new_path, buffer_size=10000000000) img = Image.open(new_path) # 为了获取图片的长宽信息 width, height = img.width, img.height return { 'status': 'success', 'message': '图片上传成功', 'code': 200, 'results': { 'name': name, 'type': type, 'origin': origin, 'width': width, 'height': height, } } except Exception as e: return { 'status': 'error', 'message': '图片上传出现异常', 'info': str(e) } @app.route('/upload/url', methods=['POST']) def get_url_pic(): """ 获取url上的图片 :param url: 图片的url :return: json """ try: url = request.json.get("url") res = requests.get(url, headers=headers) if res.status_code == 200: content = res.content file = url.split('/')[-1] name = file.split('.')[0] + '_' + str(int(time.time())) type = file.split('.')[-1] filePath = save_path + name + '.' + type # type = file.split('.')[-1] # name = file.split('.')[0] origin = url with open(filePath, 'wb') as f: f.write(content) f.close() img = Image.open(filePath) width, height = img.width, img.height return { 'status': 'success', 'message': '图片爬取成功', 'code': 200, 'results': { 'name': name, 'type': type, 'origin': origin, 'width': width, 'height': height, } } else: return { 'status': 'fail', 'message': '图片爬取失败', 'code': res.status_code } except Exception as e: return { 'status': 'error', 'message': '图片爬取出现异常', 'info': str(e) } """ 模型相关操作,切换模型,显示某个模型细节,显示当前模型 """ @app.route('/detail/model', methods=['GET']) def show_model_detail(): """ 查看某个模型的内部细节 :param wid: :return: """ try: args = request.args # print(model_json) path = weights_path + args.get('model_name') + '.pt' _cache_yolo: YOLO = YOLO(path) results = { 'names': _cache_yolo.names, 'params': _cache_yolo.model.yaml } return { 'status': 'success', 'message': '查看模型细节成功', 'results': results } except Exception as e: return { 'status': 'error', 'message': '查看模型细节失败', 'info': str(e) } @app.route('/current', methods=['GET']) def show_current_model(): """ 查看当前模型函数 :return: json """ try: return { 'status': 'success', 'message': '查看当前模型成功', 'results': app.config["model_json"] } except Exception as e: return { 'status': 'error', 'message': '查看当前模型失败', 'info': str(e) } @app.route('/switch', methods=["PUT"]) def switch_model(): """ 前端找wmodel类的参数 -> 获得参数 -> 切换模型 切换模型,首先参数要从前端request哪里获得 request.json格式是 { "model_dataset": "bdd", "model_name": "yolov8l_bdd", "model_type": "yolov8", "wid": 1 } :return: json """ try: model_json = request.json name = model_json['model_name'] if name is None: return { 'status': 'error', 'message': '切换模型失败', 'info': '模型名字为空' } path = weights_path + model_json['model_name'] + '.pt' app.config["model"]: YOLO = YOLO(path) # print(app.config["model"].model.args) # print(app.config['model']) app.config["model_json"] = model_json app.config["cls"] = app.config["model"].names return { 'status': 'success', 'message': '切换模型成功', } except Exception as e: return { 'status': 'error', 'message': '切换模型失败', 'info': str(e) } @app.route('/upload/model/', methods=["POST"]) def save_model_cache(): try: data = request.files.get('file') filename = request.files.get('file').filename type = filename.split('.')[-1] if type != 'pt': return { 'status': 'error', 'message': '传送的不是pt文件', } data.save(cache_save_model_path, buffer_size=10000000000) return { 'status': 'success', 'message': '模型暂缓成功', } except Exception as e: return { 'status': 'error', 'message': '模型暂缓出现异常', 'info': str(e) } """ 检测图片任务相关 """ @app.route('/resultImg/') def show_result_img(name): """ 返回结果相关 :param name: 结果文件名称 :return: """ img_url = result_path + name if request.method == 'GET': if name is None: pass else: image_data = open(img_url, "rb").read() response = make_response(image_data) response.headers['Content-Type'] = 'image/jpg' return response else: pass @app.route('/detect', methods=['POST']) def detection(): """ 实现检测的功能 返回的json参数 与POST http://127.0.0.1:5000/result 以及 POST http://127.0.0.1:5000/obj 配合使用 前端找File类的参数 ->前端找现存文件 -> 绘制图片 -> 保存文件 -> 获得参数 -> 接下来添加数据库对应记录 └─> 绘制失败则报错 与 POST http://127.0.0.1:5000/result 以及 POST http://127.0.0.1:5000/obj 结合使用 最后前端获得检测结果将结果保存到后端上(result,objection表) request.json格式是 { "fid": 2, "height": 720, "name": "b7a8e795-ff80fddf", "origin": "localhost://b7a8e795-ff80fddf.jpg", "timestamp": "Fri, 24 Feb 2023 19:04:44 GMT", "type": "jpg", "width": 1280 } :return: json """ try: detection_json = request.json filename = detection_json['name'] type = detection_json['type'] model = app.config["model"] cls = app.config["cls"] wid = app.config["model_json"]['wid'] # print(cls) path = save_path + filename + '.' + type # print(path) results = model(path) length = 0 info = [] for result in results: boxes = result.boxes.data.cpu().numpy().tolist() length = len(boxes) # pic_data = [] for j in range(length): pic_data = { 'x1': boxes[j][0], 'y1': boxes[j][1], 'x2': boxes[j][2], 'y2': boxes[j][3], 'conf': boxes[j][-2], 'cls': cls[int(boxes[j][-1])] } info.append(pic_data) res_plotted = results[0].plot(show_conf=True, line_width=2, font_size=3) timestamp = int(time.time()) img_path = result_path + filename + '_' + str(timestamp) + '.' + type # print(res_plotted) cv2.imwrite(img_path, res_plotted) return { 'status': 'success', 'message': '检测成功', 'results': { 'log': { 'wid': wid, 'addition': '单独的检测任务', 'timestamp': timestamp, 'mission_type': 'detection', 'filename': filename, 'type': type, 'num': length }, 'objects': info } } except Exception as e: return { 'status': 'error', 'message': '检测失败', 'info': str(e) } @app.route('/delete/result', methods=['DELETE']) def delete_result(): """ 这里的函数是实现删除结果图片文件,与 DELETE http://127.0.0.1:5000/result/ 以及 DELETE http://127.0.0.1:5000/obj/ 结合使用 前端返回参数 ->前端找现存文件 -> 删除图片 -> 删除完成 -> 接下来删除数据库对应记录 └─> 删除失败则报错 request.json格式是 { "addition": "单独的检测任务", "filename": "b7a8e795-ff80fddf", "mission_type": "detection", "num": 23, "rid": 1, "timestamp": 1677256499, "type": "jpg", "wid": 1 } :return: json """ try: result_json = request.json file = result_json['filename'] + '_' + str(result_json['timestamp']) + '.' + result_json['type'] path = result_path + file os.remove(path) return { 'status': 'success', 'message': '删除记录图片成功', } except Exception as e: return { 'status': 'error', 'message': '删除记录图片失败', 'info': str(e) } @app.route('/delete/file', methods=['DELETE']) def delete_file(): """ 这里的函数是实现删除结果图片文件,与 DELETE 结合使用 前端返回参数 ->前端找现存文件 -> 删除图片 -> 删除完成 -> 接下来删除数据库对应记录 └─> 删除失败则报错 request.json格式是,来自前端上面的 { type: response.results.type, name: response.results.name, } :return: json """ try: type = request.json.get("type") name = request.json.get("name") file_path = save_path + name + '.' + type # print(file_path) os.remove(file_path) # print(request.data) # print(request.json.get("type"), request.json.get('name')) # print(request.json) return { 'status': 'success', 'message': '删除图片成功', } except Exception as e: return { 'status': 'error', 'message': '删除图片失败', 'info': str(e) } pass # @app.route('/delete/model', methods=['DELETE']) # def delete_model(): # """ # 这里的函数是实现删除结果图片文件,与 DELETE http://127.0.0.1:5000/result/ 结合使用 # 前端返回参数 ->前端找现存文件 -> 删除图片 -> 删除完成 -> 接下来删除数据库对应记录 # └─> 删除失败则报错 # request.json格式是 # # # :return: json # """ # pass """ 指令相关 """ @app.cli.command('init') def init(): db.drop_all() # 注销所有的数据库 db.create_all() # 创建所有数据库 File.init_db() WModel.init_db() if __name__ == '__main__': app.run(debug=True) ================================================ FILE: detection-backend/config.py ================================================ save_path = '.\\pic\\save\\' cache_save_model_path = '.\\save\\cache.pt' result_path = '.\\pic\\result\\' weights_path = '.\\weights\\' cache_save_path = '.\\cache\\pic\\' cache_model_path = '.\\cache\\model\\' img_url_show = 'http://localhost:5000/img/' result_url_show = 'http://localhost:5000/resultImg/' default_weights = '.\\weights\\yolov8m_coco.pt' default_model_json = { "model_dataset": "bdd", "model_name": "yolov8m_bdd", "model_type": "yolov8", "wid": 2 } fixed_model_type = [ 'yolov5', 'yolov8', 'deepRFT' ] ================================================ FILE: detection-backend/demo.http ================================================ ### GET http://127.0.0.1:5000/files/2 ### GET http://127.0.0.1:5000/wmodel ### 下面是从切换模型,检测开始记录数据,查询数据,删除数据的步骤 ### ### PUT http://127.0.0.1:5000/switch Content-Type: application/json { "model_dataset": "bdd", "model_name": "yolov8l_bdd", "model_type": "yolov8", "wid": 1 } ### GET http://127.0.0.1:5000/files/113 ### POST http://127.0.0.1:5000/detect Content-Type: application/json { "fid": 113, "height": 720, "name": "b7bcc17a-72df68f9", "origin": "localhost://b7bcc17a-72df68f9.jpg", "timestamp": "Tue, 28 Feb 2023 09:32:58 GMT", "type": "jpg", "width": 1280 } ### POST http://127.0.0.1:5000/result Content-Type: application/json { "addition": "单独的检测任务", "filename": "b7bcc17a-72df68f9", "mission_type": "detection", "num": 26, "timestamp": 1677658144, "type": "jpg", "wid": 1 } ### POST http://127.0.0.1:5000/obj Content-Type: application/json { "message": "检测成功", "results": { "log": { "addition": "单独的检测任务", "filename": "b7bcc17a-72df68f9", "mission_type": "detection", "num": 26, "timestamp": 1677658144, "type": "jpg", "wid": 1 }, "objects": [ { "cls": "car", "conf": 0.9426229596138, "x1": 21.0, "x2": 335.0, "y1": 502.0, "y2": 651.0 }, { "cls": "bus", "conf": 0.9366695284843445, "x1": 819.0, "x2": 1019.0, "y1": 451.0, "y2": 611.0 }, { "cls": "car", "conf": 0.9037463665008545, "x1": 588.0, "x2": 695.0, "y1": 518.0, "y2": 608.0 }, { "cls": "car", "conf": 0.8302534222602844, "x1": 757.0, "x2": 812.0, "y1": 527.0, "y2": 567.0 }, { "cls": "car", "conf": 0.8223326802253723, "x1": 363.0, "x2": 419.0, "y1": 518.0, "y2": 555.0 }, { "cls": "car", "conf": 0.7934474945068359, "x1": 414.0, "x2": 448.0, "y1": 521.0, "y2": 550.0 }, { "cls": "car", "conf": 0.7931928038597107, "x1": 690.0, "x2": 723.0, "y1": 531.0, "y2": 560.0 }, { "cls": "car", "conf": 0.7816309332847595, "x1": 445.0, "x2": 468.0, "y1": 526.0, "y2": 548.0 }, { "cls": "traffic light", "conf": 0.7765074372291565, "x1": 548.0, "x2": 564.0, "y1": 409.0, "y2": 452.0 }, { "cls": "car", "conf": 0.7473191618919373, "x1": 545.0, "x2": 577.0, "y1": 527.0, "y2": 552.0 }, { "cls": "car", "conf": 0.7085943818092346, "x1": 18.0, "x2": 102.0, "y1": 496.0, "y2": 548.0 }, { "cls": "car", "conf": 0.7016028761863708, "x1": 723.0, "x2": 748.0, "y1": 531.0, "y2": 553.0 }, { "cls": "car", "conf": 0.64445960521698, "x1": 744.0, "x2": 765.0, "y1": 532.0, "y2": 557.0 }, { "cls": "car", "conf": 0.6331145763397217, "x1": 70.0, "x2": 132.0, "y1": 507.0, "y2": 536.0 }, { "cls": "traffic light", "conf": 0.6008689999580383, "x1": 1094.0, "x2": 1119.0, "y1": 489.0, "y2": 509.0 }, { "cls": "car", "conf": 0.5753867030143738, "x1": 523.0, "x2": 539.0, "y1": 528.0, "y2": 543.0 }, { "cls": "car", "conf": 0.5730618238449097, "x1": 2.0, "x2": 75.0, "y1": 499.0, "y2": 555.0 }, { "cls": "car", "conf": 0.5328835248947144, "x1": 587.0, "x2": 604.0, "y1": 526.0, "y2": 543.0 }, { "cls": "car", "conf": 0.5161319971084595, "x1": 462.0, "x2": 494.0, "y1": 524.0, "y2": 547.0 }, { "cls": "traffic light", "conf": 0.5084584951400757, "x1": 1078.0, "x2": 1095.0, "y1": 431.0, "y2": 471.0 }, { "cls": "car", "conf": 0.5038861632347107, "x1": 302.0, "x2": 337.0, "y1": 521.0, "y2": 564.0 }, { "cls": "car", "conf": 0.41518062353134155, "x1": 682.0, "x2": 699.0, "y1": 528.0, "y2": 547.0 }, { "cls": "car", "conf": 0.36542969942092896, "x1": 573.0, "x2": 589.0, "y1": 525.0, "y2": 539.0 }, { "cls": "car", "conf": 0.35099053382873535, "x1": 307.0, "x2": 361.0, "y1": 512.0, "y2": 543.0 }, { "cls": "car", "conf": 0.31251752376556396, "x1": 504.0, "x2": 527.0, "y1": 528.0, "y2": 545.0 }, { "cls": "traffic sign", "conf": 0.2699459195137024, "x1": 613.0, "x2": 634.0, "y1": 473.0, "y2": 492.0 } ] }, "status": "success" } ### GET http://127.0.0.1:5000/obj/1 ### DELETE http://127.0.0.1:5000/obj/1 ### GET http://127.0.0.1:5000/result/ ### GET http://127.0.0.1:5000/result/1 ### DELETE http://127.0.0.1:5000/delete/result Content-Type: application/json { "addition": "单独的检测任务", "filename": "b7a8e795-ff80fddf", "mission_type": "detection", "num": 23, "rid": 1, "timestamp": 1677258002, "type": "jpg", "wid": 1 } ### DELETE http://127.0.0.1:5000/result/1 ####分页函数测试 GET http://127.0.0.1:5000/page/ Content-Type: application/json { "curPage": 1, "pageSize": 2, "tableName": "file" } ### GET http://127.0.0.1:5000/objectPage/1 Content-Type: application/json { "curPage": 3, "pageSize": 5 } ### GET http://127.0.0.1:5000/num/ Content-Type: application/json { "rid": 1, "tableName": "model" } ####模糊查找测试 GET http://127.0.0.1:5000/deblurS/ Content-Type: application/json { "keyword": "b", "tableName": "result" } ###查看当前模型参数测试 GET http://127.0.0.1:5000/current ================================================ FILE: detection-backend/extension.py ================================================ from flask_sqlalchemy import SQLAlchemy from flask_cors import CORS db = SQLAlchemy() cors = CORS(resources={r'/*':{'origins':'*'}}) ================================================ FILE: detection-backend/modelPredict.py ================================================ from ultralytics import YOLO model_path = './weights/yolov8n_bdd.pt' video_path = 'video/save/2.mp4' model = YOLO(model_path) results = model(video_path, show=True, save=True) ================================================ FILE: detection-backend/models.py ================================================ import os from sqlalchemy import ForeignKey from config import cache_save_path from PIL import Image from extension import db import datetime fixed_model_type = [ 'yolov5', 'yolov8' ] class File(db.Model): __tablename__ = 'file_table' fid = db.Column(db.Integer, primary_key=True, autoincrement=True) name = db.Column(db.String(255), nullable=False) timestamp = db.Column(db.DateTime, default=datetime.datetime.now) type = db.Column(db.String(255), nullable=False) # 文件类型 origin = db.Column(db.String(255), nullable=False) # 来源 width = db.Column(db.Integer, nullable=False) height = db.Column(db.Integer, nullable=False) @staticmethod def init_db(): count: int = 1 files = os.listdir(cache_save_path) # rets = [ # (1, 'b6f77fda-02b6e75b', 'jpg', 'localhost://b6f77fda-02b6e75b.jpg', 1280, 720), # (2, 'b7a8e795-ff80fddf', 'jpg', 'localhost://b7a8e795-ff80fddf.jpg', 1280, 720), # ] rets = [] for filename in files: filename = cache_save_path + filename # print(filename) img = Image.open(filename) rets.append([ count, filename.split('\\')[-1].split('.')[0], filename.split('.')[-1], 'localhost://' + filename.split('\\')[-1], img.width, img.height ]) count += 1 # print(rets) for ret in rets: file = File() file.fid = ret[0] file.name = ret[1] file.type = ret[2] file.origin = ret[3] file.width = ret[4] file.height = ret[5] db.session.add(file) # db.session.commit() class WModel(db.Model): __tablename__ = 'model_table' wid = db.Column(db.Integer, primary_key=True, autoincrement=True) model_name = db.Column(db.String(255), nullable=False) # 名称 model_type = db.Column(db.String(255), nullable=False) # 类型 model_dataset = db.Column(db.String(255), nullable=False) # 适用数据集 @staticmethod def init_db(): rets = [ (1, 'yolov8l_coco', 'yolov8', 'coco'), (2, 'yolov8m_coco', 'yolov8', 'coco'), (3, 'yolov8s_coco', 'yolov8', 'coco'), (4, 'yolov8n_coco', 'yolov8', 'coco'), (5, 'yolov5su_coco', 'yolov5', 'coco'), (6, 'yolov5nu_coco', 'yolov5', 'coco') ] for ret in rets: wmodel = WModel() wmodel.wid = ret[0] wmodel.model_name = ret[1] wmodel.model_type = ret[2] wmodel.model_dataset = ret[3] db.session.add(wmodel) db.session.commit() class Result(db.Model): __tablename__ = 'result_table' rid = db.Column(db.Integer, primary_key=True, autoincrement=True) wid = db.Column(db.Integer, ForeignKey("model_table.wid", ondelete='SET NULL')) # 首要默认为检测模型 timestamp = db.Column(db.Integer, nullable=False) # 时间戳 type = db.Column(db.String(255), nullable=True) # 文件类型 addition = db.Column(db.String(255), nullable=True) # 额外说明 mission_type = db.Column(db.String(255), nullable=False) # 任务类型 filename = db.Column(db.String(255), nullable=False) # 被检测的源头文件 num = db.Column(db.Integer, nullable=False) # 一共有多少个检测物体 class Object(db.Model): __tablename__ = 'obj_table' oid = db.Column(db.Integer, primary_key=True, autoincrement=True) rid = db.Column(db.Integer, ForeignKey("result_table.rid", ondelete='SET NULL')) # 和检测完成好的文件相关联, 外键 cls = db.Column(db.String(255), nullable=False) # 类别名称 x1 = db.Column(db.Float, nullable=False) # 边框的坐标值 y1 = db.Column(db.Float, nullable=False) x2 = db.Column(db.Float, nullable=False) y2 = db.Column(db.Float, nullable=False) conf = db.Column(db.Float, nullable=False) # 置信度 ================================================ FILE: detection-backend/weights/yolov5su_coco.pt ================================================ [File too large to display: 17.7 MB] ================================================ FILE: detection-backend/weights/yolov8l_coco.pt ================================================ [File too large to display: 83.7 MB] ================================================ FILE: detection-backend/weights/yolov8m_coco.pt ================================================ [File too large to display: 49.7 MB] ================================================ FILE: detection-backend/weights/yolov8s_coco.pt ================================================ [File too large to display: 21.5 MB] ================================================ FILE: detection-fontend/LICENSE ================================================ MIT License Copyright (c) 2016-2023 vue-manage-system Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: detection-fontend/README.md ================================================ # vue-manage-system vue pinia license GitHub release donate 基于 Vue3 + pinia + Element Plus 的后台管理系统解决方案。[线上地址](https://lin-xin.gitee.io/example/work/) > Vue2 版本请看 [tag-V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0) [English document](https://github.com/lin-xin/manage-system/blob/master/README_EN.md) ## 赞助商 ### 好问 [](https://www.bestqa.net/home/index.html) 专业问卷服务,一对一客服,按需定制 ## 支持作者 请作者喝杯咖啡吧!(微信号:linxin_20) ![微信扫一扫](https://lin-xin.gitee.io/images/weixin.jpg) ## 前言 该方案作为一套多功能的后台框架模板,适用于绝大部分的后台管理系统开发。基于 Vue3 + pinia + typescript,引用 Element Plus 组件库,方便开发。实现逻辑简单,适合外包项目,快速交付。 ## 功能 - [x] Element Plus - [x] vite 3 - [x] pinia - [x] typescript - [x] 登录/注销 - [x] Dashboard - [x] 表格 - [x] Tab 选项卡 - [x] 表单 - [x] 图表 :bar_chart: - [x] 富文本/markdown编辑器 - [x] 图片拖拽/裁剪上传 - [x] 权限管理 - [x] 三级菜单 - [x] 自定义图标 ## 安装步骤 > 因为使用vite3,node版本需要 14.18+ ``` git clone https://github.com/lin-xin/vue-manage-system.git // 把模板下载到本地 cd vue-manage-system // 进入模板目录 npm install // 安装项目依赖,等待安装完成之后,安装失败可用 cnpm 或 yarn // 运行 npm run dev // 执行构建命令,生成的dist文件夹放在服务器下即可访问 npm run build ``` ## 组件使用说明与演示 ### vue-schart vue.js 封装 sChart.js 的图表组件。访问地址:[vue-schart](https://github.com/lin-xin/vue-schart#/)

Downloads

```html ``` ## 项目截图 ### 登录 ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png) ### 首页 ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png) ## License [MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE) ================================================ FILE: detection-fontend/README_EN.md ================================================ # vue-manage-system vue element-ui license GitHub release donate The web management system solution based on Vue3 and ElementPlus。[live demo](https://lin-xin.gitee.io/example/work/) Please check the version of vue2 in [tag V4.2.0](https://github.com/lin-xin/vue-manage-system/tree/V4.2.0) ## Donation ![WeChat](https://lin-xin.gitee.io/images/weixin.jpg) ## Preface The scheme as a set of multi-function background frame templates, suitable for most of the WEB management system development. Convenient development fast simple good components based on Vue3 and ElementPlus. Color separation of color style, support manual switch themes, and it is convenient to use a custom theme color. ## Function - [x] Element-UI - [x] Login/Logout - [x] Dashboard - [x] Table - [x] Tabs - [x] From - [x] Chart :bar_chart: - [x] Editor - [x] Markdown - [x] Upload pictures by clipping or dragging - [x] Permission - [x] Three level menu - [x] Custom icon ## Installation steps git clone https://github.com/lin-xin/vue-manage-system.git // Clone templates cd vue-manage-system // Enter template directory npm install // Installation dependency ## Local development npm run dev ## Constructing production // Constructing project npm run build ## Component description and presentation ### vue-schart Vue.js wrapper for sChart.js. Github : [vue-schart](https://github.com/lin-xin/vue-schart#/) ```html ``` ## Screenshot ### Default theme ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms1.png) ### Login ![Image text](https://github.com/lin-xin/manage-system/raw/master/screenshots/wms3.png) ## License [MIT](https://github.com/lin-xin/vue-manage-system/blob/master/LICENSE) ================================================ FILE: detection-fontend/auto-imports.d.ts ================================================ // Generated by 'unplugin-auto-import' export {} declare global { } ================================================ FILE: detection-fontend/components.d.ts ================================================ // generated by unplugin-vue-components // We suggest you to commit this file into source control // Read more: https://github.com/vuejs/core/pull/3399 import '@vue/runtime-core' export {} declare module '@vue/runtime-core' { export interface GlobalComponents { ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCascader: typeof import('element-plus/es')['ElCascader'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCol: typeof import('element-plus/es')['ElCol'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] 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'] ElLink: typeof import('element-plus/es')['ElLink'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElProgress: typeof import('element-plus/es')['ElProgress'] ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] ElTimePicker: typeof import('element-plus/es')['ElTimePicker'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElUpload: typeof import('element-plus/es')['ElUpload'] Header: typeof import('./src/components/header.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] Sidebar: typeof import('./src/components/sidebar.vue')['default'] Tags: typeof import('./src/components/tags.vue')['default'] } } ================================================ FILE: detection-fontend/index.html ================================================ vue-manage-system
================================================ FILE: detection-fontend/package.json ================================================ { "name": "vue-manage-system", "version": "5.3.0", "private": true, "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "serve": "vite preview" }, "dependencies": { "@element-plus/icons-vue": "^2.0.9", "axios": "^0.27.2", "element-plus": "^2.2.14", "md-editor-v3": "^2.2.1", "pinia": "^2.0.20", "vue": "^3.2.37", "vue-cropperjs": "^5.0.0", "vue-router": "^4.1.3", "vue-schart": "^2.0.0", "wangeditor": "^4.7.15", "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^3.0.0", "@vue/compiler-sfc": "^3.1.2", "typescript": "^4.6.4", "unplugin-auto-import": "^0.11.2", "unplugin-vue-components": "^0.22.4", "vite": "^3.0.0", "vite-plugin-vue-setup-extend": "^0.4.0", "vue-tsc": "^0.38.4" }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } ================================================ FILE: detection-fontend/public/table.json ================================================ { "list": [{ "id": 1, "name": "张三", "money": 123, "address": "广东省东莞市长安镇", "state": "成功", "date": "2019-11-1", "thumb": "https://lin-xin.gitee.io/images/post/wms.png" }, { "id": 2, "name": "李四", "money": 456, "address": "广东省广州市白云区", "state": "成功", "date": "2019-10-11", "thumb": "https://lin-xin.gitee.io/images/post/node3.png" }, { "id": 3, "name": "王五", "money": 789, "address": "湖南省长沙市", "state": "失败", "date": "2019-11-11", "thumb": "https://lin-xin.gitee.io/images/post/parcel.png" }, { "id": 4, "name": "赵六", "money": 1011, "address": "福建省厦门市鼓浪屿", "state": "成功", "date": "2019-10-20", "thumb": "https://lin-xin.gitee.io/images/post/notice.png" } ], "pageTotal": 4 } ================================================ FILE: detection-fontend/src/App.vue ================================================ ================================================ FILE: detection-fontend/src/api/index.ts ================================================ import request from '../utils/request'; export const fetchData = () => { return request({ url: './table.json', method: 'get' }); }; ================================================ FILE: detection-fontend/src/assets/css/color-dark.css ================================================ .header{ background-color: #242f42; } .login-wrap{ background: #324157; } .plugins-tips{ background: #eef1f6; } .plugins-tips a{ color: #20a0ff; } .tags-li.active { border: 1px solid #409EFF; background-color: #409EFF; } .message-title{ color: #20a0ff; } .collapse-btn:hover{ background: rgb(40,52,70); } ================================================ FILE: detection-fontend/src/assets/css/icon.css ================================================ [class*=" el-icon-lx"], [class^=el-icon-lx] { font-family: lx-iconfont !important; } ================================================ FILE: detection-fontend/src/assets/css/main.css ================================================ * { margin: 0; padding: 0; } html, body, #app, .wrapper { width: 100%; height: 100%; overflow: hidden; } body { font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif; } a { text-decoration: none } .content-box { position: absolute; left: 250px; right: 0; top: 70px; bottom: 0; padding-bottom: 30px; -webkit-transition: left .3s ease-in-out; transition: left .3s ease-in-out; background: #f0f0f0; } .content { width: auto; height: 100%; padding: 10px; overflow-y: scroll; box-sizing: border-box; } .content-collapse { left: 65px; } .container { padding: 30px; background: #fff; border: 1px solid #ddd; border-radius: 5px; } .crumbs { margin: 10px 0; } .el-table th { background-color: #f5f7fa !important; } .pagination { margin: 20px 0; text-align: right; } .plugins-tips { padding: 20px 10px; margin-bottom: 20px; } .el-button+.el-tooltip { margin-left: 10px; } .el-table tr:hover { background: #f6faff; } .mgb20 { margin-bottom: 20px; } .move-enter-active, .move-leave-active { transition: opacity .1s ease; } .move-enter-from, .move-leave-to { opacity: 0; } /*BaseForm*/ .form-box { width: 600px; } .form-box .line { text-align: center; } .el-time-panel__content::after, .el-time-panel__content::before { margin-top: -7px; } .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) { padding-bottom: 0; } [class*=" el-icon-"], [class^=el-icon-] { speak: none; font-style: normal; font-weight: 400; font-variant: normal; text-transform: none; line-height: 1; vertical-align: baseline; display: inline-block; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .el-sub-menu [class^=el-icon-] { vertical-align: middle; margin-right: 5px; width: 24px; text-align: center; font-size: 18px; } [hidden]{ display: none !important; } ================================================ FILE: detection-fontend/src/components/header.vue ================================================ ================================================ FILE: detection-fontend/src/components/sidebar.vue ================================================ ================================================ FILE: detection-fontend/src/components/tags.vue ================================================ ================================================ FILE: detection-fontend/src/main.ts ================================================ import { createApp } from 'vue'; import { createPinia } from 'pinia'; import * as ElementPlusIconsVue from '@element-plus/icons-vue'; import App from './App.vue'; import router from './router'; import { usePermissStore } from './store/permiss'; import 'element-plus/dist/index.css'; import './assets/css/icon.css'; import axios from "axios"; axios.defaults.baseURL = "http://localhost:5000" axios.defaults.headers["Content-Type"] = "application/json;charset=UTF-8" const app = createApp(App); app.use(createPinia()); app.use(router); // 注册elementplus图标 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component); } // 自定义权限指令 const permiss = usePermissStore(); app.directive('permiss', { mounted(el, binding) { if (!permiss.key.includes(String(binding.value))) { el['hidden'] = true; } }, }); app.mount('#app'); ================================================ FILE: detection-fontend/src/router/index.ts ================================================ import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { usePermissStore } from '../store/permiss'; import Home from '../views/home.vue'; const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/detection', }, { path: '/', name: 'Home', component: Home, children: [ // { // path: '/dashboard', // name: 'dashboard', // meta: { // title: '系统首页', // permiss: '1', // }, // component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard.vue'), // }, { path: '/table', name: 'basetable', meta: { title: '表格', permiss: '2', }, component: () => import(/* webpackChunkName: "table" */ '../views/table.vue'), }, { path: '/charts', name: 'basecharts', meta: { title: '图表', permiss: '11', }, component: () => import(/* webpackChunkName: "charts" */ '../views/charts.vue'), }, { path: '/form', name: 'baseform', meta: { title: '表单', permiss: '5', }, component: () => import(/* webpackChunkName: "form" */ '../views/form.vue'), }, { path: '/tabs', name: 'tabs', meta: { title: 'tab标签', permiss: '3', }, component: () => import(/* webpackChunkName: "tabs" */ '../views/tabs.vue'), }, { path: '/donate', name: 'donate', meta: { title: '鼓励作者', permiss: '14', }, component: () => import(/* webpackChunkName: "donate" */ '../views/donate.vue'), }, { path: '/permission', name: 'permission', meta: { title: '权限管理', permiss: '13', }, component: () => import(/* webpackChunkName: "permission" */ '../views/permission.vue'), }, { path: '/upload', name: 'upload', meta: { title: '上传插件', permiss: '6', }, component: () => import(/* webpackChunkName: "upload" */ '../views/upload.vue'), }, { path: '/icon', name: 'icon', meta: { title: '自定义图标', permiss: '10', }, component: () => import(/* webpackChunkName: "icon" */ '../views/icon.vue'), }, { path: '/user', name: 'user', meta: { title: '个人中心', }, component: () => import(/* webpackChunkName: "user" */ '../views/user.vue'), }, { path: '/editor', name: 'editor', meta: { title: '富文本编辑器', permiss: '8', }, component: () => import(/* webpackChunkName: "editor" */ '../views/editor.vue'), }, { path: '/markdown', name: 'markdown', meta: { title: 'markdown编辑器', permiss: '9', }, component: () => import(/* webpackChunkName: "markdown" */ '../views/markdown.vue'), }, { path: '/export', name: 'export', meta: { title: '导出Excel', permiss: '2', }, component: () => import(/* webpackChunkName: "export" */ '../views/export.vue'), }, { path: '/import', name: 'import', meta: { title: '导入Excel', permiss: '2', }, component: () => import(/* webpackChunkName: "import" */ '../views/import.vue'), }, { path: '/uploadFile', name: 'uploadFile', meta: { title: '上传文件', permiss: '2', }, component: () => import(/* webpackChunkName: "import" */ '../viewDetect/uploadFile.vue'), }, { path: '/detection', name: 'detection', meta: { title: '目标检测', permiss: '1', }, component: () => import(/* webpackChunkName: "import" */ '../viewDetect/detectVue.vue'), }, { path: '/resultManager', name: 'resultManager', meta: { title: '记录管理', permiss: '3', }, component: () => import(/* webpackChunkName: "import" */ '../viewDetect/ResultVue.vue'), }, ], }, { path: '/login', name: 'Login', meta: { title: '登录', }, component: () => import(/* webpackChunkName: "login" */ '../views/login.vue'), }, { path: '/403', name: '403', meta: { title: '没有权限', }, component: () => import(/* webpackChunkName: "403" */ '../views/403.vue'), }, ]; const router = createRouter({ history: createWebHashHistory(), routes, }); router.beforeEach((to, from, next) => { document.title = `${to.meta.title} | vue-manage-system`; const role = localStorage.getItem('ms_username'); const permiss = usePermissStore(); if (!role && to.path !== '/login') { next('/login'); } else if (to.meta.permiss && !permiss.key.includes(to.meta.permiss)) { // 如果没有权限,则进入403 next('/403'); } else { next(); } }); export default router; ================================================ FILE: detection-fontend/src/store/permiss.ts ================================================ import { defineStore } from 'pinia'; interface ObjectList { [key: string]: string[]; } export const usePermissStore = defineStore('permiss', { state: () => { const keys = localStorage.getItem('ms_keys'); return { key: keys ? JSON.parse(keys) : [], defaultList: { admin: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16'], user: ['1', '2', '3', '11', '13', '14', '15'] } }; }, actions: { handleSet(val: string[]) { this.key = val; } } }); ================================================ FILE: detection-fontend/src/store/sidebar.ts ================================================ import { defineStore } from 'pinia'; export const useSidebarStore = defineStore('sidebar', { state: () => { return { collapse: false }; }, getters: {}, actions: { handleCollapse() { this.collapse = !this.collapse; } } }); ================================================ FILE: detection-fontend/src/store/tags.ts ================================================ import { defineStore } from 'pinia'; interface ListItem { name: string; path: string; title: string; } export const useTagsStore = defineStore('tags', { state: () => { return { list: [] }; }, getters: { show: state => { return state.list.length > 0; }, nameList: state => { return state.list.map(item => item.name); } }, actions: { delTagsItem(index: number) { this.list.splice(index, 1); }, setTagsItem(data: ListItem) { this.list.push(data); }, clearTags() { this.list = []; }, closeTagsOther(data: ListItem[]) { this.list = data; }, closeCurrentTag(data: any) { for (let i = 0, len = this.list.length; i < len; i++) { const item = this.list[i]; if (item.path === data.$route.fullPath) { if (i < len - 1) { data.$router.push(this.list[i + 1].path); } else if (i > 0) { data.$router.push(this.list[i - 1].path); } else { data.$router.push('/'); } this.list.splice(i, 1); break; } } } } }); ================================================ FILE: detection-fontend/src/utils/request.ts ================================================ import axios, {AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig} from 'axios'; const service:AxiosInstance = axios.create({ timeout: 5000 }); service.interceptors.request.use( (config: AxiosRequestConfig) => { return config; }, (error: AxiosError) => { console.log(error); return Promise.reject(); } ); service.interceptors.response.use( (response: AxiosResponse) => { if (response.status === 200) { return response; } else { Promise.reject(); } }, (error: AxiosError) => { console.log(error); return Promise.reject(); } ); export default service; ================================================ FILE: detection-fontend/src/viewDetect/ResultVue.vue ================================================ ================================================ FILE: detection-fontend/src/viewDetect/detectVue.vue ================================================ ================================================ FILE: detection-fontend/src/viewDetect/uploadFile.vue ================================================ ================================================ FILE: detection-fontend/src/views/403.vue ================================================ ================================================ FILE: detection-fontend/src/views/404.vue ================================================ ================================================ FILE: detection-fontend/src/views/charts.vue ================================================ ================================================ FILE: detection-fontend/src/views/dashboard.vue ================================================ ================================================ FILE: detection-fontend/src/views/donate.vue ================================================ ================================================ FILE: detection-fontend/src/views/editor.vue ================================================ ================================================ FILE: detection-fontend/src/views/export.vue ================================================ ================================================ FILE: detection-fontend/src/views/form.vue ================================================ ================================================ FILE: detection-fontend/src/views/home.vue ================================================ ================================================ FILE: detection-fontend/src/views/icon.vue ================================================ ================================================ FILE: detection-fontend/src/views/import.vue ================================================ ================================================ FILE: detection-fontend/src/views/login.vue ================================================ ================================================ FILE: detection-fontend/src/views/markdown.vue ================================================ ================================================ FILE: detection-fontend/src/views/permission.vue ================================================ ================================================ FILE: detection-fontend/src/views/table.vue ================================================ ================================================ FILE: detection-fontend/src/views/tabs.vue ================================================ ================================================ FILE: detection-fontend/src/views/upload.vue ================================================ ================================================ FILE: detection-fontend/src/views/user.vue ================================================ ================================================ FILE: detection-fontend/src/vite-env.d.ts ================================================ /// declare module '*.vue' { import type { DefineComponent } from 'vue' const component: DefineComponent<{}, {}, any> export default component } declare module 'vue-schart'; declare module 'vue-cropperjs'; ================================================ FILE: detection-fontend/tsconfig.json ================================================ { "compilerOptions": { "target": "ESNext", "useDefineForClassFields": true, "module": "ESNext", "moduleResolution": "Node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "isolatedModules": true, "esModuleInterop": true, "lib": ["ESNext", "DOM"], "skipLibCheck": true }, "include": ["src/**/*.ts", "src/**/*.d.ts","src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] } ================================================ FILE: detection-fontend/tsconfig.node.json ================================================ { "compilerOptions": { "composite": true, "module": "ESNext", "moduleResolution": "Node", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] } ================================================ FILE: detection-fontend/vite.config.ts ================================================ import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import VueSetupExtend from 'vite-plugin-vue-setup-extend'; import AutoImport from 'unplugin-auto-import/vite'; import Components from 'unplugin-vue-components/vite'; import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; export default defineConfig({ base: './', plugins: [ vue(), VueSetupExtend(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ], optimizeDeps: { include: ['schart.js'] } });