全部上传
This commit is contained in:
parent
4c7edd356c
commit
72f343314c
8
.cursorignore
Normal file
8
.cursorignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||||
|
front/node_modules
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.gitignore
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitignore
|
175
.gitignore
vendored
175
.gitignore
vendored
@ -1,162 +1,29 @@
|
|||||||
# ---> Python
|
# Compiled class file
|
||||||
# Byte-compiled / optimized / DLL files
|
*.class
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
# Log file
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
*.log
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
# BlueJ files
|
||||||
instance/
|
*.ctxt
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
# Mobile Tools for Java (J2ME)
|
||||||
.scrapy
|
.mtj.tmp/
|
||||||
|
target
|
||||||
|
|
||||||
# Sphinx documentation
|
# Package Files #
|
||||||
docs/_build/
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|
||||||
# PyBuilder
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
.pybuilder/
|
hs_err_pid*
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
.idea
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|
||||||
|
/upload/
|
26
App/__init__.py
Normal file
26
App/__init__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# __init__.py 初始化文件创建Flask应用
|
||||||
|
import pymysql
|
||||||
|
from .exts import init_exts
|
||||||
|
from flask import Flask
|
||||||
|
from . import *
|
||||||
|
from .views import blus
|
||||||
|
def create_app():
|
||||||
|
app = Flask(__name__, static_folder='')
|
||||||
|
# 注册蓝图
|
||||||
|
app.register_blueprint(blueprint=blus)
|
||||||
|
# MySQL所在主机名,默认127.0.0.1
|
||||||
|
HOSTNAME = "192.168.229.122"
|
||||||
|
# MySQL监听的端口号,默认3306
|
||||||
|
PORT = 3306
|
||||||
|
# 连接MySQL的用户名,自己设置
|
||||||
|
USERNAME = "root"
|
||||||
|
# 连接MySQL的密码,自己设置
|
||||||
|
PASSWORD = "123456"
|
||||||
|
# MySQL上创建的数据库名称
|
||||||
|
DATABASE = "bigdata_ibecs"
|
||||||
|
# 通过修改以下代码来操作不同的SQL比写原生SQL简单很多 --》通过ORM可以实现从底层更改使用的SQL
|
||||||
|
app.config[
|
||||||
|
'SQLALCHEMY_DATABASE_URI'] = f"mysql+pymysql://{USERNAME}:{PASSWORD}@{HOSTNAME}:{PORT}/{DATABASE}?charset=utf8mb4"
|
||||||
|
# 初始化插件
|
||||||
|
init_exts(app=app)
|
||||||
|
return app
|
16
App/exts.py
Normal file
16
App/exts.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 存放插件
|
||||||
|
|
||||||
|
# 扩展第三方插件
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from flask_migrate import Migrate
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
# 2 初始化
|
||||||
|
db = SQLAlchemy()
|
||||||
|
migrate = Migrate() #数据迁移
|
||||||
|
# 3 和app绑定
|
||||||
|
|
||||||
|
def init_exts(app):
|
||||||
|
CORS(app) # 允许所有域名的跨域请求
|
||||||
|
db.init_app(app=app)
|
||||||
|
migrate.init_app(app=app,db=db)
|
46
App/models.py
Normal file
46
App/models.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# 模型
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
|
|
||||||
|
from .exts import db
|
||||||
|
|
||||||
|
|
||||||
|
# 模型
|
||||||
|
class User(db.Model):
|
||||||
|
# 表名
|
||||||
|
__tablename__ = "tb_user"
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
role = db.Column(db.Integer, autoincrement=True)
|
||||||
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
|
password = db.Column(db.String(128), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class JobPosition(db.Model):
|
||||||
|
__tablename__ = 'tb_companies'
|
||||||
|
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True, comment='主键ID')
|
||||||
|
company_name = db.Column(db.String(50))
|
||||||
|
industry = db.Column(db.String(200))
|
||||||
|
registered_capital = db.Column(db.String(200))
|
||||||
|
establishment_date = db.Column(db.String(200))
|
||||||
|
registration_addres = db.Column(db.String(200))
|
||||||
|
def to_dict(self):
|
||||||
|
"""将模型转换为字典"""
|
||||||
|
return {
|
||||||
|
'id': self.id,
|
||||||
|
'companyName': self.company_name,
|
||||||
|
'industry': self.industry,
|
||||||
|
'registeredCapital': self.registered_capital,
|
||||||
|
'establishmentDate': self.establishment_date,
|
||||||
|
'registrationAddres': self.registration_addres,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Config(db.Model):
|
||||||
|
# 表名
|
||||||
|
__tablename__ = "tb_sysconfig"
|
||||||
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True) # 主键
|
||||||
|
name = db.Column(db.String(50), nullable=False)
|
||||||
|
key = db.Column(db.String(50), unique=True, nullable=False) # 配置键
|
||||||
|
value = db.Column(db.String(255), nullable=False) # 配置值
|
||||||
|
description = db.Column(db.String(255)) # 描述
|
54
App/utils/api_utils.py
Normal file
54
App/utils/api_utils.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from flask import jsonify
|
||||||
|
# 统一返回接口
|
||||||
|
class APIUtils:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def success_response(data=None, message="Success", status_code=200):
|
||||||
|
"""成功响应封装"""
|
||||||
|
response = {
|
||||||
|
'code': '200',
|
||||||
|
'message': message,
|
||||||
|
'data': data
|
||||||
|
}
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def error_response(message="Error", status_code=200):
|
||||||
|
"""错误响应封装"""
|
||||||
|
response = {
|
||||||
|
'code': '500',
|
||||||
|
'msg': message
|
||||||
|
}
|
||||||
|
return jsonify(response), status_code
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_json(request_json, required_fields):
|
||||||
|
"""验证 JSON 请求体中是否包含所需字段"""
|
||||||
|
missing_fields = [field for field in required_fields if field not in request_json]
|
||||||
|
if missing_fields:
|
||||||
|
return False, f'缺失字段: {", ".join(missing_fields)}'
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def paginate(query, page, per_page):
|
||||||
|
"""分页处理"""
|
||||||
|
total = query.count()
|
||||||
|
items = query.offset((page - 1) * per_page).limit(per_page).all()
|
||||||
|
return {
|
||||||
|
'total': total,
|
||||||
|
'items': items
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CustomException(Exception):
|
||||||
|
"""自定义异常类,所有自定义异常应继承自此类"""
|
||||||
|
|
||||||
|
def __init__(self, message, status_code=400):
|
||||||
|
super().__init__(message)
|
||||||
|
self.message = message
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""将异常信息转换为字典格式"""
|
||||||
|
return {'error': self.message, 'status_code': self.status_code}
|
41
App/utils/config.py
Normal file
41
App/utils/config.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
from ..models import *
|
||||||
|
class ConfigManager:
|
||||||
|
@staticmethod
|
||||||
|
def get_value(key):
|
||||||
|
"""通过键获取配置值"""
|
||||||
|
config = Config.query.filter_by(key=key).first()
|
||||||
|
return config.value if config else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_config(name, key, value, description=None):
|
||||||
|
"""添加新的配置项"""
|
||||||
|
if Config.query.filter_by(key=key).first():
|
||||||
|
raise ValueError("配置键已存在!")
|
||||||
|
new_config = Config(name=name, key=key, value=value, description=description)
|
||||||
|
db.session.add(new_config)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_config(key, value):
|
||||||
|
"""更新配置项的值"""
|
||||||
|
config = Config.query.filter_by(key=key).first()
|
||||||
|
if not config:
|
||||||
|
raise ValueError("配置项不存在!")
|
||||||
|
|
||||||
|
config.value = value
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_config(key):
|
||||||
|
"""删除配置项"""
|
||||||
|
config = Config.query.filter_by(key=key).first()
|
||||||
|
if not config:
|
||||||
|
raise ValueError("配置项不存在!")
|
||||||
|
|
||||||
|
db.session.delete(config)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_file_path(key):
|
||||||
|
|
||||||
|
return ""
|
419
App/views.py
Normal file
419
App/views.py
Normal file
@ -0,0 +1,419 @@
|
|||||||
|
# views.py 路由 + 视图函数
|
||||||
|
import os
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
|
import jieba
|
||||||
|
import numpy as np
|
||||||
|
import pymysql
|
||||||
|
from flask import request, jsonify
|
||||||
|
from flask import Blueprint
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from matplotlib import pyplot as plt
|
||||||
|
from sqlalchemy import desc
|
||||||
|
|
||||||
|
from .utils.api_utils import APIUtils
|
||||||
|
from .models import *
|
||||||
|
from .utils.prediction import get_finance_job_data, preprocess_data, arima_forecast, prepare_ml_data, \
|
||||||
|
train_random_forest
|
||||||
|
|
||||||
|
blus = Blueprint("user", __name__)
|
||||||
|
db_config = {
|
||||||
|
'host': '192.168.229.122',
|
||||||
|
'user': 'root',
|
||||||
|
'password': '123456',
|
||||||
|
'database': 'bigdata_ibecs',
|
||||||
|
'charset': 'utf8mb4'
|
||||||
|
}
|
||||||
|
# 注册
|
||||||
|
@blus.route('/api/register', methods=['POST'])
|
||||||
|
def user_register():
|
||||||
|
required_fields = ['username', 'password']
|
||||||
|
is_valid, message = APIUtils.validate_json(request.json, required_fields)
|
||||||
|
if not is_valid:
|
||||||
|
return APIUtils.error_response(message, status_code=400)
|
||||||
|
username = request.json['username']
|
||||||
|
password = request.json['password']
|
||||||
|
|
||||||
|
# 检查用户名是否已存在
|
||||||
|
existing_user = User.query.filter_by(username=username).first()
|
||||||
|
if existing_user:
|
||||||
|
return APIUtils.error_response("用户名已经存在!", status_code=400)
|
||||||
|
# 哈希处理密码
|
||||||
|
hashed_password = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
# 创建新用户
|
||||||
|
new_user = User(username=username, password=hashed_password,role=1)
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
return APIUtils.success_response(message="登录成功!")
|
||||||
|
|
||||||
|
@blus.route('/items/<int:item_id>', methods=['DELETE'])
|
||||||
|
def delete_item(item_id):
|
||||||
|
item = JobPosition.query.get_or_404(item_id)
|
||||||
|
db.session.delete(item)
|
||||||
|
db.session.commit()
|
||||||
|
return APIUtils.success_response(message="删除成功!")
|
||||||
|
@blus.route('/items', methods=['GET'])
|
||||||
|
def get_items():
|
||||||
|
page = request.args.get('current', 1, type=int)
|
||||||
|
size = request.args.get('size', 10, type=int)
|
||||||
|
companyName = request.args.get('companyName', '', type=str)
|
||||||
|
city = request.args.get('city', '', type=str)
|
||||||
|
query = JobPosition.query
|
||||||
|
if companyName:
|
||||||
|
query = query.filter(JobPosition.company_name.like(f'%{companyName}%'))
|
||||||
|
# 如果传入 city,则进行模糊查询
|
||||||
|
if city:
|
||||||
|
query = query.filter(JobPosition.city.like(f'%{city}%'))
|
||||||
|
# 获取分页数据
|
||||||
|
pagination = query.paginate(page=page, per_page=size, error_out=False)
|
||||||
|
# 构建响应数据
|
||||||
|
response = {
|
||||||
|
'list': [item.to_dict() for item in pagination.items],
|
||||||
|
'page': {
|
||||||
|
'total': pagination.total, # 总记录数
|
||||||
|
'current': page, # 当前页码
|
||||||
|
'size': size, # 每页请求的记录数
|
||||||
|
'pages': pagination.pages # 总页数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APIUtils.success_response(data=response, message="获取数据成功")
|
||||||
|
|
||||||
|
@blus.route('/api/login', methods=['POST'])
|
||||||
|
def user_login():
|
||||||
|
required_fields = ['username', 'password']
|
||||||
|
is_valid, message = APIUtils.validate_json(request.json, required_fields)
|
||||||
|
if not is_valid:
|
||||||
|
return APIUtils.error_response(message, status_code=400)
|
||||||
|
username = request.json['username']
|
||||||
|
password = request.json['password']
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user is None:
|
||||||
|
return APIUtils.error_response("用户名错误或不存在!", status_code=500)
|
||||||
|
hashed_password = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
if hashed_password != user.password:
|
||||||
|
return APIUtils.error_response("密码错误或不存在!", status_code=500)
|
||||||
|
return APIUtils.success_response(data={'token': user.id,'userId': user.id, 'username': user.username,'role':user.role}, message="登录成功!")
|
||||||
|
|
||||||
|
@blus.route('/sys/user/info', methods=['GET'])
|
||||||
|
def user_info():
|
||||||
|
token = request.headers.get('token')
|
||||||
|
|
||||||
|
user = User.query.filter_by(id=token).first()
|
||||||
|
|
||||||
|
return APIUtils.success_response(data={'token': user.id,'userId': user.id, 'username': user.username,'role':user.role}, message="登录成功!")
|
||||||
|
|
||||||
|
@blus.route('/change_password', methods=['POST'])
|
||||||
|
def change_password():
|
||||||
|
required_fields = ['username', 'old_password', 'new_password']
|
||||||
|
is_valid, message = APIUtils.validate_json(request.json, required_fields)
|
||||||
|
if not is_valid:
|
||||||
|
return APIUtils.error_response(message, status_code=400)
|
||||||
|
username = request.json['username']
|
||||||
|
old_password = request.json['old_password']
|
||||||
|
new_password = request.json['new_password']
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user is None:
|
||||||
|
return APIUtils.error_response("用户不存在!", status_code=404)
|
||||||
|
hashed_old_password = hashlib.sha256(old_password.encode()).hexdigest()
|
||||||
|
if hashed_old_password != user.password:
|
||||||
|
return APIUtils.error_response("原始密码不正确!", status_code=401)
|
||||||
|
# 哈希处理新密码
|
||||||
|
hashed_new_password = hashlib.sha256(new_password.encode()).hexdigest()
|
||||||
|
user.password = hashed_new_password
|
||||||
|
db.session.commit()
|
||||||
|
return APIUtils.success_response(message="密码修改成功!")
|
||||||
|
|
||||||
|
@blus.route('/api/users/<int:user_id>', methods=['DELETE'])
|
||||||
|
def delete_user(user_id):
|
||||||
|
# 根据用户 ID 查询用户
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
if user is None:
|
||||||
|
return APIUtils.error_response("用户不存在!", status_code=404)
|
||||||
|
# 检查是否为 admin 用户
|
||||||
|
if user.username.lower() == 'admin':
|
||||||
|
return APIUtils.error_response("无法删除管理员账户!", status_code=403)
|
||||||
|
# 删除用户
|
||||||
|
db.session.delete(user)
|
||||||
|
db.session.commit()
|
||||||
|
return APIUtils.success_response(message="用户删除成功!")
|
||||||
|
# 用户管理
|
||||||
|
@blus.route('/api/users/page', methods=['GET'])
|
||||||
|
def get_users():
|
||||||
|
# 获取分页参数,默认为第 1 页,每页 10 条记录
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
per_page = request.args.get('limit', 10, type=int)
|
||||||
|
# 获取 username 参数,如果没有则为 None
|
||||||
|
username = request.args.get('username', type=str)
|
||||||
|
# 构建查询,先查询所有用户
|
||||||
|
query = User.query
|
||||||
|
# 如果提供了 username,则根据 username 进行筛选
|
||||||
|
if username:
|
||||||
|
query = query.filter(User.username.like(f'%{username}%'))
|
||||||
|
|
||||||
|
# 执行分页查询
|
||||||
|
users_pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||||
|
|
||||||
|
# 获取用户数据
|
||||||
|
users = users_pagination.items
|
||||||
|
|
||||||
|
# 将用户数据转为 JSON 格式
|
||||||
|
users_list = []
|
||||||
|
for user in users:
|
||||||
|
users_list.append({
|
||||||
|
'id': user.id,
|
||||||
|
'username': user.username,
|
||||||
|
'password': user.password,
|
||||||
|
'role': user.role
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
# 构建响应数据,包括分页信息
|
||||||
|
response = {
|
||||||
|
'list': users_list,
|
||||||
|
'page': {
|
||||||
|
'total': users_pagination.total, # 总记录数
|
||||||
|
'page': users_pagination.page, # 当前页码
|
||||||
|
'limit': users_pagination.per_page # 每页记录数
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return APIUtils.success_response(data=response, message="获取用户列表成功")
|
||||||
|
|
||||||
|
# 新增用户
|
||||||
|
@blus.route('/api/users', methods=['POST'])
|
||||||
|
def add_user():
|
||||||
|
data = request.get_json()
|
||||||
|
# 验证必填字段
|
||||||
|
if not all([data.get('username'),data.get('password')]):
|
||||||
|
return APIUtils.error_response(message="用户名、手机号和密码不能为空", code=400)
|
||||||
|
# 检查用户名是否已存在
|
||||||
|
if User.query.filter_by(username=data['username']).first():
|
||||||
|
return APIUtils.error_response(message="用户名已存在")
|
||||||
|
# 创建新用户
|
||||||
|
new_user = User(
|
||||||
|
username=data['username'],
|
||||||
|
|
||||||
|
password=data['password'], # 注意:实际项目中密码应该加密存储
|
||||||
|
role=data.get('role', 1) # 默认普通用户
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_user)
|
||||||
|
db.session.commit()
|
||||||
|
return APIUtils.success_response(message="用户添加成功")
|
||||||
|
|
||||||
|
# 修改用户
|
||||||
|
@blus.route('/api/users/<int:user_id>', methods=['PUT'])
|
||||||
|
def update_user(user_id):
|
||||||
|
data = request.get_json()
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
return APIUtils.error_response(message="用户不存在", code=404)
|
||||||
|
# 更新字段(密码单独处理)
|
||||||
|
if 'username' in data:
|
||||||
|
# 检查新用户名是否已被其他用户使用
|
||||||
|
if User.query.filter(User.username == data['username'], User.id != user_id).first():
|
||||||
|
return APIUtils.error_response(message="用户名已存在", code=400)
|
||||||
|
user.username = data['username']
|
||||||
|
|
||||||
|
if 'role' in data:
|
||||||
|
user.role = data['role']
|
||||||
|
db.session.commit()
|
||||||
|
return APIUtils.success_response(message="用户信息更新成功")
|
||||||
|
|
||||||
|
def plot_to_base64(plt_figure):
|
||||||
|
"""将matplotlib图表转换为base64编码的图片"""
|
||||||
|
buf = io.BytesIO()
|
||||||
|
plt_figure.savefig(buf, format='png', dpi=100)
|
||||||
|
buf.seek(0)
|
||||||
|
return base64.b64encode(buf.read()).decode('utf-8')
|
||||||
|
|
||||||
|
def generate_prediction_report(job_count_forecast, salary_forecast, forecast_dates):
|
||||||
|
"""生成预测报告(返回字典格式)"""
|
||||||
|
report = {
|
||||||
|
"预测时间范围": f"{forecast_dates[0].strftime('%Y-%m-%d')} 至 {forecast_dates[-1].strftime('%Y-%m-%d')}",
|
||||||
|
"总预测招聘岗位数": int(np.sum(job_count_forecast)),
|
||||||
|
"日均预测招聘数": round(np.mean(job_count_forecast), 1),
|
||||||
|
"预测平均薪资变化": f"{round((salary_forecast[-1] - salary_forecast[0]) / salary_forecast[0] * 100, 2)}%",
|
||||||
|
"预测最高薪资": round(np.max(salary_forecast), 2),
|
||||||
|
"预测最低薪资": round(np.min(salary_forecast), 2),
|
||||||
|
"预测趋势": "上升" if salary_forecast[-1] > salary_forecast[0] else "下降"
|
||||||
|
}
|
||||||
|
return report
|
||||||
|
@blus.route('/api/prediction', methods=['GET'])
|
||||||
|
def get_prediction():
|
||||||
|
try:
|
||||||
|
# 1. 获取数据
|
||||||
|
job_data = get_finance_job_data()
|
||||||
|
# 2. 数据预处理
|
||||||
|
daily_job_data, raw_data = preprocess_data(job_data)
|
||||||
|
# 3. 时间序列预测
|
||||||
|
job_count_forecast, forecast_dates = arima_forecast(daily_job_data, 'job_count', 30)
|
||||||
|
salary_forecast, _ = arima_forecast(daily_job_data, 'salary_avg', 30)
|
||||||
|
|
||||||
|
# 4. 机器学习预测
|
||||||
|
ml_data = prepare_ml_data(daily_job_data)
|
||||||
|
job_count_model = train_random_forest(ml_data, 'job_count')
|
||||||
|
salary_model = train_random_forest(ml_data, 'salary_avg')
|
||||||
|
|
||||||
|
# 5. 生成图表并转换为base64
|
||||||
|
# 招聘数量趋势图
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
plt.plot(daily_job_data.index, daily_job_data['job_count'], label='历史数据')
|
||||||
|
plt.plot(forecast_dates, job_count_forecast, label='预测数据', color='red')
|
||||||
|
plt.title('金融行业招聘数量趋势预测')
|
||||||
|
plt.xlabel('日期')
|
||||||
|
plt.ylabel('数量')
|
||||||
|
plt.legend()
|
||||||
|
plt.grid()
|
||||||
|
job_count_plot = plot_to_base64(plt)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
# 平均薪资趋势图
|
||||||
|
plt.figure(figsize=(12, 6))
|
||||||
|
plt.plot(daily_job_data.index, daily_job_data['salary_avg'], label='历史数据')
|
||||||
|
plt.plot(forecast_dates, salary_forecast, label='预测数据', color='red')
|
||||||
|
plt.title('金融行业平均薪资趋势预测')
|
||||||
|
plt.xlabel('日期')
|
||||||
|
plt.ylabel('薪资')
|
||||||
|
plt.legend()
|
||||||
|
plt.grid()
|
||||||
|
salary_plot = plot_to_base64(plt)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
# 6. 生成报告
|
||||||
|
prediction_report = generate_prediction_report(job_count_forecast, salary_forecast, forecast_dates)
|
||||||
|
|
||||||
|
# 准备响应数据
|
||||||
|
response_data = {
|
||||||
|
"report": prediction_report,
|
||||||
|
"plots": {
|
||||||
|
"job_count": job_count_plot,
|
||||||
|
"salary": salary_plot
|
||||||
|
},
|
||||||
|
"forecast_data": {
|
||||||
|
"dates": [date.strftime('%Y-%m-%d') for date in forecast_dates],
|
||||||
|
"job_count": job_count_forecast.tolist(),
|
||||||
|
"salary": salary_forecast.tolist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"message": "预测数据获取成功",
|
||||||
|
"data": response_data
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"预测数据获取失败: {str(e)}",
|
||||||
|
"data": None
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# SQL查询
|
||||||
|
@blus.route('/api/mysql', methods=['POST'])
|
||||||
|
def mysql():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# 检查 SQL 参数是否存在
|
||||||
|
if not data['sql']:
|
||||||
|
return APIUtils.error_response(message="没有sql参数")
|
||||||
|
try:
|
||||||
|
# 连接数据库
|
||||||
|
connection = pymysql.connect(**db_config)
|
||||||
|
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||||
|
# 自定义 SQL 查询
|
||||||
|
cursor.execute(data['sql'])
|
||||||
|
# 获取查询结果
|
||||||
|
results = cursor.fetchall()
|
||||||
|
return results
|
||||||
|
except pymysql.MySQLError as e:
|
||||||
|
return APIUtils.error_response(message=f"数据库连接失败:{str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
return APIUtils.error_response(message=f"查询执行失败:{str(e)}")
|
||||||
|
|
||||||
|
@blus.route('/api/word', methods=['GET'])
|
||||||
|
def word():
|
||||||
|
try:
|
||||||
|
# 构造停用词文件的动态路径
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
stopwords_file = os.path.join(base_dir, 'utils', 'stopwords.txt')
|
||||||
|
|
||||||
|
# 读取停用词文件,存入集合,提高查找效率
|
||||||
|
stopwords = set()
|
||||||
|
with open(stopwords_file, encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
word = line.strip()
|
||||||
|
if word:
|
||||||
|
stopwords.add(word)
|
||||||
|
|
||||||
|
connection = pymysql.connect(**db_config)
|
||||||
|
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||||
|
# 获取 job_description 字段
|
||||||
|
query = "SELECT job_description FROM job_positions LIMIT 1000"
|
||||||
|
cursor.execute(query)
|
||||||
|
# 词频统计字典
|
||||||
|
word_counts = {}
|
||||||
|
# 处理每个 job_description
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
job_desc = row['job_description']
|
||||||
|
# 分词并统计
|
||||||
|
for word in jieba.cut(job_desc):
|
||||||
|
word = word.strip()
|
||||||
|
# 过滤空字符串和停用词
|
||||||
|
if word and word not in stopwords:
|
||||||
|
word_counts[word] = word_counts.get(word, 0) + 1
|
||||||
|
# 转换为要求的格式
|
||||||
|
result = [
|
||||||
|
{"name": word, "value": count}
|
||||||
|
for word, count in word_counts.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return APIUtils.success_response(data=result)
|
||||||
|
except pymysql.MySQLError as err:
|
||||||
|
return APIUtils.error_response(message=str(err))
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
@blus.route('/api/caiji', methods=['GET'])
|
||||||
|
def caiji():
|
||||||
|
try:
|
||||||
|
# 构造停用词文件的动态路径
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
stopwords_file = os.path.join(base_dir, 'utils', 'stopwords.txt')
|
||||||
|
|
||||||
|
# 读取停用词文件,存入集合,提高查找效率
|
||||||
|
stopwords = set()
|
||||||
|
with open(stopwords_file, encoding='utf-8') as f:
|
||||||
|
for line in f:
|
||||||
|
word = line.strip()
|
||||||
|
if word:
|
||||||
|
stopwords.add(word)
|
||||||
|
|
||||||
|
connection = pymysql.connect(**db_config)
|
||||||
|
with connection.cursor(pymysql.cursors.DictCursor) as cursor:
|
||||||
|
# 获取 job_description 字段
|
||||||
|
query = "SELECT job_description FROM job_positions LIMIT 1000"
|
||||||
|
cursor.execute(query)
|
||||||
|
# 词频统计字典
|
||||||
|
word_counts = {}
|
||||||
|
# 处理每个 job_description
|
||||||
|
for row in cursor.fetchall():
|
||||||
|
job_desc = row['job_description']
|
||||||
|
# 分词并统计
|
||||||
|
for word in jieba.cut(job_desc):
|
||||||
|
word = word.strip()
|
||||||
|
# 过滤空字符串和停用词
|
||||||
|
if word and word not in stopwords:
|
||||||
|
word_counts[word] = word_counts.get(word, 0) + 1
|
||||||
|
# 转换为要求的格式
|
||||||
|
result = [
|
||||||
|
{"name": word, "value": count}
|
||||||
|
for word, count in word_counts.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
return APIUtils.success_response(data=result)
|
||||||
|
except pymysql.MySQLError as err:
|
||||||
|
return APIUtils.error_response(message=str(err))
|
||||||
|
finally:
|
||||||
|
connection.close()
|
39
README.md
39
README.md
@ -1,3 +1,38 @@
|
|||||||
# bigdata-ibecs
|
### 选题十 智能银行企业信用系统
|
||||||
|
|
||||||
智能银行企业信用系统
|
#### 一、项目背景
|
||||||
|
为了提高银行对企业信用的管理和评估能力,实时监控和分析企业信用数据(如企业信息、贷款记录、还款记录等)是至关重要的。本作业将基于大数据平台模拟一个智能银行企业信用管理系统,学生需要完成数据采集、存储、处理、分析及可视化展示等任务。
|
||||||
|
|
||||||
|
#### 二、项目要求
|
||||||
|
|
||||||
|
1. **数据采集与存储**
|
||||||
|
- 提供多个CSV格式的初始数据集(或自选数据集),包括:
|
||||||
|
- 企业信息数据(companies.csv):企业ID、企业名称、行业、注册资本、成立日期、注册地址
|
||||||
|
- 贷款记录数据(loans.csv):贷款ID、企业ID、贷款金额、贷款日期、到期日期、贷款状态
|
||||||
|
- 还款记录数据(repayments.csv):还款ID、贷款ID、还款金额、还款日期、还款状态
|
||||||
|
- 银行数据(banks.csv):银行ID、银行名称、银行地址、银行等级
|
||||||
|
- 将这些数据集导入到MySQL数据库中,并创建相应的表结构。
|
||||||
|
- 使用Sqoop将历史银行企业信用数据从MySQL导入到HDFS中。
|
||||||
|
- 在HBase中创建表结构,并将处理后的数据存储到HBase中。
|
||||||
|
|
||||||
|
2. **数据处理与分析(使用Spark)**
|
||||||
|
- **基础分析**:
|
||||||
|
- 使用Spark进行数据清洗,去除噪声和无效数据。
|
||||||
|
- 计算各行业的平均贷款金额、还款率等基础指标。
|
||||||
|
- **高级分析**:
|
||||||
|
- **信用评估分析**:基于贷款和还款数据,分析不同企业的信用等级,提出信用评估模型。
|
||||||
|
- **贷款风险分析**:分析不同行业和地区的贷款风险,找出高风险行业和地区,并提出风险控制建议。
|
||||||
|
- **还款行为分析**:使用关联规则算法(如Apriori)分析还款行为与企业特征的关联,评估还款行为模式。
|
||||||
|
|
||||||
|
3. **数据整合与查询**
|
||||||
|
- 在HDFS上使用Hive创建表结构,对数据进行分区存储和管理,提高查询效率。
|
||||||
|
- 创建分区表,根据时间、行业等进行分区存储。
|
||||||
|
- 创建视图,简化复杂查询,提高查询效率。
|
||||||
|
- 使用HiveQL进行复杂查询,如计算每个行业的日均贷款金额、查询高还款率企业、对比不同时间段的贷款变化等。
|
||||||
|
- 在HBase中存储实时还款数据,并进行快速查询。
|
||||||
|
- 创建表结构,包括列族和列,如:时间、贷款ID、还款金额、还款状态。
|
||||||
|
- 使用HBase API对实时数据进行快速查询和分析。
|
||||||
|
|
||||||
|
4. **可视化分析**
|
||||||
|
- 搭建一个可视化平台,展示企业信用数据分析结果,包括贷款金额变化图、还款率图、信用等级分布图等。
|
||||||
|
- 实现数据的动态更新功能,根据时间段或行业选择展示不同的企业信用数据。
|
5
app.py
Normal file
5
app.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from App import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
73
bigdata/hive.sql
Normal file
73
bigdata/hive.sql
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
-- 企业信用等级分布
|
||||||
|
CREATE table dim_bank_grade as
|
||||||
|
SELECT bank_grade as name ,count(*) as value from tb_banks GROUP BY bank_grade;
|
||||||
|
|
||||||
|
-- 还款状态分析
|
||||||
|
CREATE table dim_loan_status as
|
||||||
|
SELECT loan_status as name ,count(*) as value from tb_loans GROUP BY loan_status;
|
||||||
|
|
||||||
|
-- 贷款金额变化趋势
|
||||||
|
CREATE table dim_loan_amount as
|
||||||
|
SELECT
|
||||||
|
loan_date AS date,
|
||||||
|
round( SUM(loan_amount) / 10000,2 )AS amount
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
loan_date;
|
||||||
|
|
||||||
|
-- 企业还款状态分析
|
||||||
|
CREATE table dim_repayment_status as
|
||||||
|
SELECT
|
||||||
|
repayment_date,
|
||||||
|
ROUND(SUM(CASE WHEN repayment_status = '按时还款' THEN repayment_amount ELSE 0 END), 2) AS on_time_amount,
|
||||||
|
ROUND(SUM(CASE WHEN repayment_status = '逾期还款' THEN repayment_amount ELSE 0 END), 2) AS overdue_amount,
|
||||||
|
ROUND(SUM(repayment_amount), 2) AS total_amount
|
||||||
|
FROM
|
||||||
|
tb_repayments
|
||||||
|
WHERE
|
||||||
|
repayment_status IN ('按时还款', '逾期还款')
|
||||||
|
GROUP BY
|
||||||
|
repayment_date
|
||||||
|
ORDER BY
|
||||||
|
repayment_date;
|
||||||
|
|
||||||
|
|
||||||
|
-- 每年借款数量
|
||||||
|
CREATE TABLE dim_jie_count as
|
||||||
|
SELECT
|
||||||
|
YEAR(STR_TO_DATE(loan_date, '%Y/%m/%d')) AS name,
|
||||||
|
COUNT(*) AS value
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
YEAR(STR_TO_DATE(loan_date, '%Y/%m/%d'))
|
||||||
|
ORDER BY name;
|
||||||
|
-- 每年逾期占比
|
||||||
|
CREATE table dim_total_count as
|
||||||
|
SELECT
|
||||||
|
t.year,
|
||||||
|
t.loan_status,
|
||||||
|
t.status_count,
|
||||||
|
ROUND(t.status_count * 100.0 / total.total_count, 2) AS percentage
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
YEAR(STR_TO_DATE(loan_date, '%Y/%m/%d')) AS year,
|
||||||
|
loan_status,
|
||||||
|
COUNT(*) AS status_count
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
YEAR(STR_TO_DATE(loan_date, '%Y/%m/%d')), loan_status
|
||||||
|
) t
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
YEAR(STR_TO_DATE(loan_date, '%Y/%m/%d')) AS year,
|
||||||
|
COUNT(*) AS total_count
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
YEAR(STR_TO_DATE(loan_date, '%Y/%m/%d'))
|
||||||
|
) total ON t.year = total.year
|
||||||
|
ORDER BY
|
||||||
|
t.year, t.status_count DESC;
|
86
bigdata/hive1.sql
Normal file
86
bigdata/hive1.sql
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
CREATE TABLE dim_bank_grade AS
|
||||||
|
SELECT
|
||||||
|
bank_grade AS name,
|
||||||
|
COUNT(*) AS value
|
||||||
|
FROM
|
||||||
|
tb_banks
|
||||||
|
GROUP BY
|
||||||
|
bank_grade;
|
||||||
|
|
||||||
|
CREATE TABLE dim_loan_status AS
|
||||||
|
SELECT
|
||||||
|
loan_status AS name,
|
||||||
|
COUNT(*) AS value
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
loan_status;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE dim_loan_amount AS
|
||||||
|
SELECT
|
||||||
|
loan_date AS date,
|
||||||
|
ROUND(SUM(loan_amount) / 10000, 2) AS amount
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
loan_date;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE dim_repayment_status AS
|
||||||
|
SELECT
|
||||||
|
repayment_date,
|
||||||
|
ROUND(SUM(CASE WHEN repayment_status = '按时还款' THEN repayment_amount ELSE 0 END), 2) AS on_time_amount,
|
||||||
|
ROUND(SUM(CASE WHEN repayment_status = '逾期还款' THEN repayment_amount ELSE 0 END), 2) AS overdue_amount,
|
||||||
|
ROUND(SUM(repayment_amount), 2) AS total_amount
|
||||||
|
FROM
|
||||||
|
tb_repayments
|
||||||
|
WHERE
|
||||||
|
repayment_status IN ('按时还款', '逾期还款')
|
||||||
|
GROUP BY
|
||||||
|
repayment_date
|
||||||
|
ORDER BY
|
||||||
|
repayment_date;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE dim_jie_count AS
|
||||||
|
SELECT
|
||||||
|
YEAR(FROM_UNIXTIME(UNIX_TIMESTAMP(loan_date, 'yyyy/MM/dd'))) AS name,
|
||||||
|
COUNT(*) AS value
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
YEAR(FROM_UNIXTIME(UNIX_TIMESTAMP(loan_date, 'yyyy/MM/dd')))
|
||||||
|
ORDER BY
|
||||||
|
name;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE dim_total_count AS
|
||||||
|
SELECT
|
||||||
|
t.year,
|
||||||
|
t.loan_status,
|
||||||
|
t.status_count,
|
||||||
|
ROUND(t.status_count * 100.0 / total.total_count, 2) AS percentage
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
YEAR(FROM_UNIXTIME(UNIX_TIMESTAMP(loan_date, 'yyyy/MM/dd'))) AS year,
|
||||||
|
loan_status,
|
||||||
|
COUNT(*) AS status_count
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
YEAR(FROM_UNIXTIME(UNIX_TIMESTAMP(loan_date, 'yyyy/MM/dd'))),
|
||||||
|
loan_status
|
||||||
|
) t
|
||||||
|
JOIN (
|
||||||
|
SELECT
|
||||||
|
YEAR(FROM_UNIXTIME(UNIX_TIMESTAMP(loan_date, 'yyyy/MM/dd'))) AS year,
|
||||||
|
COUNT(*) AS total_count
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
YEAR(FROM_UNIXTIME(UNIX_TIMESTAMP(loan_date, 'yyyy/MM/dd')))
|
||||||
|
) total ON t.year = total.year
|
||||||
|
ORDER BY
|
||||||
|
t.year, t.status_count DESC;
|
21
bigdata/mysqlToHive.sh
Normal file
21
bigdata/mysqlToHive.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MySQL 数据库连接参数
|
||||||
|
MYSQL_HOST="localhost"
|
||||||
|
MYSQL_USER="root"
|
||||||
|
MYSQL_PASSWORD="123456"
|
||||||
|
DATABASE="bigdata_ibecs"
|
||||||
|
|
||||||
|
# 删除 Hive 数据库(如果存在)
|
||||||
|
hive -e "DROP DATABASE IF EXISTS $DATABASE CASCADE;"
|
||||||
|
|
||||||
|
# 创建 Hive 数据库(如果不存在)
|
||||||
|
hive -e "CREATE DATABASE IF NOT EXISTS $DATABASE;"
|
||||||
|
|
||||||
|
echo "正在导入表 $DATABASE 到 Hive..."
|
||||||
|
|
||||||
|
# 导入数据到 Hive
|
||||||
|
/opt/sqoop/bin/sqoop import-all-tables --connect "jdbc:mysql://$MYSQL_HOST:3306/$DATABASE" --username $MYSQL_USER --password $MYSQL_PASSWORD --hive-import --hive-database $DATABASE --create-hive-table --hive-overwrite --fields-terminated-by ',' --null-string '\\N' --null-non-string '\\N' --escaped-by '\\' -m 1
|
||||||
|
|
||||||
|
echo "导入完毕!"
|
||||||
|
|
132
bigdata/spark.py
Normal file
132
bigdata/spark.py
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from pyspark.sql import SparkSession
|
||||||
|
from pyspark.sql.functions import col, year,count, to_date, from_unixtime, unix_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_spark():
|
||||||
|
"""初始化SparkSession"""
|
||||||
|
try:
|
||||||
|
spark = SparkSession.builder \
|
||||||
|
.appName("FinancialAnalysis") \
|
||||||
|
.config("spark.jars", "/path/to/mysql-connector-java-8.0.23.jar") \
|
||||||
|
.enableHiveSupport() \
|
||||||
|
.config("hive.metastore.uris", "thrift://192.168.229.122:9083") \
|
||||||
|
.getOrCreate()
|
||||||
|
|
||||||
|
return spark
|
||||||
|
except Exception as e:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_bank_grade(spark, mysql_url, mysql_properties):
|
||||||
|
"""分析企业信用等级分布"""
|
||||||
|
dim_bank_grade = spark.sql("""
|
||||||
|
SELECT
|
||||||
|
bank_grade AS name,
|
||||||
|
COUNT(*) AS value
|
||||||
|
FROM
|
||||||
|
tb_banks
|
||||||
|
GROUP BY
|
||||||
|
bank_grade
|
||||||
|
""")
|
||||||
|
dim_bank_grade.write \
|
||||||
|
.jdbc(mysql_url, "dim_bank_grade", mode="overwrite", properties=mysql_properties)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_loan_status(spark, mysql_url, mysql_properties):
|
||||||
|
"""分析还款状态"""
|
||||||
|
|
||||||
|
dim_loan_status = spark.sql("""
|
||||||
|
SELECT
|
||||||
|
loan_status AS name,
|
||||||
|
COUNT(*) AS value
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
loan_status
|
||||||
|
""")
|
||||||
|
|
||||||
|
dim_loan_status.write \
|
||||||
|
.jdbc(mysql_url, "dim_loan_status", mode="overwrite", properties=mysql_properties)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_loan_amount(spark, mysql_url, mysql_properties):
|
||||||
|
"""分析贷款金额变化趋势"""
|
||||||
|
dim_loan_amount = spark.sql("""
|
||||||
|
SELECT
|
||||||
|
loan_date AS date,
|
||||||
|
ROUND(SUM(loan_amount) / 10000, 2) AS amount
|
||||||
|
FROM
|
||||||
|
tb_loans
|
||||||
|
GROUP BY
|
||||||
|
loan_date
|
||||||
|
""")
|
||||||
|
|
||||||
|
dim_loan_amount.write \
|
||||||
|
.jdbc(mysql_url, "dim_loan_amount", mode="overwrite", properties=mysql_properties)
|
||||||
|
|
||||||
|
def analyze_repayment_status(spark, mysql_url, mysql_properties):
|
||||||
|
"""分析企业还款状态"""
|
||||||
|
dim_repayment_status = spark.sql("""
|
||||||
|
SELECT
|
||||||
|
repayment_date,
|
||||||
|
ROUND(SUM(CASE WHEN repayment_status = '按时还款' THEN repayment_amount ELSE 0 END), 2) AS on_time_amount,
|
||||||
|
ROUND(SUM(CASE WHEN repayment_status = '逾期还款' THEN repayment_amount ELSE 0 END), 2) AS overdue_amount,
|
||||||
|
ROUND(SUM(repayment_amount), 2) AS total_amount
|
||||||
|
FROM
|
||||||
|
tb_repayments
|
||||||
|
WHERE
|
||||||
|
repayment_status IN ('按时还款', '逾期还款')
|
||||||
|
GROUP BY
|
||||||
|
repayment_date
|
||||||
|
ORDER BY
|
||||||
|
repayment_date
|
||||||
|
""")
|
||||||
|
|
||||||
|
dim_repayment_status.write \
|
||||||
|
.jdbc(mysql_url, "dim_repayment_status", mode="overwrite", properties=mysql_properties)
|
||||||
|
|
||||||
|
def analyze_jie_count(spark, mysql_url, mysql_properties):
|
||||||
|
"""分析每年借款数量"""
|
||||||
|
tb_loans_df = spark.table("tb_loans")
|
||||||
|
dim_jie_count = tb_loans_df.withColumn(
|
||||||
|
"loan_date_parsed",
|
||||||
|
to_date(from_unixtime(unix_timestamp(col("loan_date"), "yyyy/MM/dd")))
|
||||||
|
).groupBy(
|
||||||
|
year("loan_date_parsed").alias("name")
|
||||||
|
).agg(
|
||||||
|
count("*").alias("value")
|
||||||
|
).orderBy("name")
|
||||||
|
|
||||||
|
dim_jie_count.write \
|
||||||
|
.jdbc(mysql_url, "dim_jie_count", mode="overwrite", properties=mysql_properties)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
# 初始化Spark
|
||||||
|
spark = initialize_spark()
|
||||||
|
|
||||||
|
# MySQL连接配置
|
||||||
|
mysql_properties = {
|
||||||
|
"user": "root",
|
||||||
|
"password": "123456",
|
||||||
|
"driver": "com.mysql.jdbc.Driver"
|
||||||
|
}
|
||||||
|
mysql_url = "jdbc:mysql://192.168.229.122:3306/bigdata_ibecs?useSSL=false"
|
||||||
|
# 执行各项分析任务
|
||||||
|
analyze_bank_grade(spark, mysql_url, mysql_properties)
|
||||||
|
analyze_loan_status(spark, mysql_url, mysql_properties)
|
||||||
|
analyze_loan_amount(spark, mysql_url, mysql_properties)
|
||||||
|
analyze_repayment_status(spark, mysql_url, mysql_properties)
|
||||||
|
analyze_jie_count(spark, mysql_url, mysql_properties)
|
||||||
|
finally:
|
||||||
|
# 关闭SparkSession
|
||||||
|
if 'spark' in locals():
|
||||||
|
spark.stop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
213967
db/bigdata_ibecs.sql
Normal file
213967
db/bigdata_ibecs.sql
Normal file
File diff suppressed because it is too large
Load Diff
44
front/.devcontainer/devcontainer.json
Normal file
44
front/.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||||
|
{
|
||||||
|
"name": "Node.js & TypeScript",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-buster",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers-contrib/features/pnpm:2": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"antfu.goto-alias",
|
||||||
|
"mikestead.dotenv",
|
||||||
|
"redhat.vscode-yaml",
|
||||||
|
"Vue.volar",
|
||||||
|
"steoates.autoimport",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"usernamehw.errorlens",
|
||||||
|
"shd101wyy.markdown-preview-enhanced",
|
||||||
|
"voorjaar.windicss-intellisense",
|
||||||
|
"yoavbls.pretty-ts-errors",
|
||||||
|
"bodil.prettier-toml",
|
||||||
|
"bungcip.better-toml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "yarn install",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
23
front/.editorConfig
Normal file
23
front/.editorConfig
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
# 匹配全部文件
|
||||||
|
[*]
|
||||||
|
# 设置字符集
|
||||||
|
charset = utf-8
|
||||||
|
# 缩进风格,可选 space、tab
|
||||||
|
indent_style = tab
|
||||||
|
# 缩进的空格数,当 indent_style = tab 将使用 tab_width
|
||||||
|
# 否则使用 indent_size
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
# 结尾换行符,可选 lf、cr、crlf
|
||||||
|
end_of_line = crlf
|
||||||
|
# 在文件结尾插入新行
|
||||||
|
insert_final_newline = true
|
||||||
|
# 删除一行中的前后空格
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
# 匹配 md 结尾的文件
|
||||||
|
[*.md]
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
30
front/.env
Normal file
30
front/.env
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# 通用环境变量
|
||||||
|
# 前端接口
|
||||||
|
VITE_API_FRONT_BASE_URL = http://localhost:18081
|
||||||
|
#后端接口
|
||||||
|
VITE_ADMIN_API_BASE_URL = http://localhost:5000
|
||||||
|
|
||||||
|
VITE_LOGIN_BG = "/login/e36341619bf8f04dcbdc6b01105a85a.png"
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
VITE_APP_TITLE = 系统
|
||||||
|
|
||||||
|
# markdown 渲染支持
|
||||||
|
VITE_APP_MARKDOWN = true
|
||||||
|
|
||||||
|
# 开发时的开发面板
|
||||||
|
VITE_APP_DEV_TOOLS = false
|
||||||
|
|
||||||
|
# 生产时 mock 支持
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION = false
|
||||||
|
|
||||||
|
# 生产时压缩算法,可选 gzip, brotliCompress, deflate, deflateRaw
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM = gzip
|
||||||
|
|
||||||
|
# api 自动按需引入
|
||||||
|
# 注意设置关闭时,其他的 api 自动按需引入也将自动关闭
|
||||||
|
VITE_APP_API_AUTO_IMPORT = true
|
||||||
|
|
||||||
|
|
||||||
|
# 项目级 api 自动按需导入
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT = true
|
4
front/.eslintignore
Normal file
4
front/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# 忽略 eslint 检查
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
12
front/.gitignore
vendored
Normal file
12
front/.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
dist
|
||||||
|
.nitro
|
||||||
|
.output
|
||||||
|
env.d.ts
|
||||||
|
node_modules
|
||||||
|
.eslintcache
|
||||||
|
components.d.ts
|
||||||
|
type-router.d.ts
|
||||||
|
auto-imports.d.ts
|
||||||
|
.eslintrc-auto-import.json
|
||||||
|
vite.config.ts.timestamp*
|
||||||
|
.idea
|
1
front/.npmrc
Normal file
1
front/.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
registry=https://registry.npmmirror.com/
|
1
front/.nvmrc
Normal file
1
front/.nvmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
20.12.2
|
3
front/.prettierignore
Normal file
3
front/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
5
front/.prettierrc.json
Normal file
5
front/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
1
front/demo
Normal file
1
front/demo
Normal file
@ -0,0 +1 @@
|
|||||||
|
import chinaMap from "@/utils/china.json";
|
13
front/index.html
Normal file
13
front/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
front/locales/English/common.yml
Normal file
10
front/locales/English/common.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# English
|
||||||
|
|
||||||
|
home: home
|
||||||
|
about: about
|
||||||
|
echarts: echarts
|
||||||
|
edit: Edit
|
||||||
|
to test HMR: to test HMR
|
||||||
|
check out: Check out
|
||||||
|
The total number of views is: The total number of views is
|
||||||
|
the official Tov + Vue + Vite template: the official Tov + Vue + Vite template
|
3
front/locales/English/test.yml
Normal file
3
front/locales/English/test.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# English
|
||||||
|
|
||||||
|
test.module: lanugae module test
|
10
front/locales/简体中文/common.yml
Normal file
10
front/locales/简体中文/common.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 中文
|
||||||
|
|
||||||
|
home: 主页
|
||||||
|
about: 关于
|
||||||
|
echarts: 图表
|
||||||
|
edit: 编辑
|
||||||
|
to test HMR: 测试热更新
|
||||||
|
check out: 查看
|
||||||
|
The total number of views is: 总浏览数
|
||||||
|
the official Tov + Vue + Vite template: 公共的 Tov + Vue + Vite 模板
|
3
front/locales/简体中文/test.yml
Normal file
3
front/locales/简体中文/test.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 简体中文
|
||||||
|
|
||||||
|
test.module: 多语言多模块测试
|
4
front/netlify.toml
Normal file
4
front/netlify.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[[redirects]]
|
||||||
|
to = "/index.html"
|
||||||
|
from = "/*"
|
||||||
|
status = 200
|
113
front/package.json
Normal file
113
front/package.json
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"name": "后台",
|
||||||
|
"version": "1.19.0",
|
||||||
|
"description": "后台",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite -- open",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.12.2"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@8.15.8",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ityped": "^1.0.3",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"@typescript-eslint/parser": "7.8.0",
|
||||||
|
"@unocss/eslint-config": "0.59.4",
|
||||||
|
"@unocss/reset": "^0.59.4",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
|
"@vueuse/components": "^10.9.0",
|
||||||
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"@vueuse/integrations": "^10.9.0",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"browserslist": "^4.23.0",
|
||||||
|
"c8": "^9.1.0",
|
||||||
|
"changelogen": "^0.5.5",
|
||||||
|
"consola": "^3.2.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"defu": "^6.1.4",
|
||||||
|
"echarts": "^5.5.0",
|
||||||
|
"eslint": "8.57.0",
|
||||||
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
"eslint-plugin-prettier": "5.1.3",
|
||||||
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"husky": "^9.0.11",
|
||||||
|
"ityped": "^1.0.3",
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"lightningcss": "^1.24.1",
|
||||||
|
"lint-staged": "^15.2.2",
|
||||||
|
"local-pkg": "^0.5.0",
|
||||||
|
"mockjs": "^1.1.0",
|
||||||
|
"nprogress": "^0.2.0",
|
||||||
|
"perfect-debounce": "^1.0.0",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
|
"plop": "^4.0.1",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"prism-theme-vars": "^0.2.5",
|
||||||
|
"simple-git": "^3.24.0",
|
||||||
|
"taze": "^0.13.7",
|
||||||
|
"terser": "^5.31.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"unocss": "^0.59.4",
|
||||||
|
"unplugin-auto-import": "^0.17.5",
|
||||||
|
"unplugin-vue-components": "^0.26.0",
|
||||||
|
"unplugin-vue-markdown": "^0.26.2",
|
||||||
|
"unplugin-vue-router": "^0.8.6",
|
||||||
|
"vite": "^5.2.10",
|
||||||
|
"vite-auto-import-resolvers": "^3.2.1",
|
||||||
|
"vite-layers": "^0.5.2",
|
||||||
|
"vite-plugin-compression": "^0.5.1",
|
||||||
|
"vite-plugin-env-types": "^0.1.4",
|
||||||
|
"vite-plugin-legacy-swc": "^1.1.0",
|
||||||
|
"vite-plugin-use-modules": "^1.4.8",
|
||||||
|
"vite-plugin-vue-devtools": "^7.1.3",
|
||||||
|
"vite-plugin-vue-meta-layouts": "^0.4.3",
|
||||||
|
"vitest": "^1.5.3",
|
||||||
|
"vue": "^3.4.26",
|
||||||
|
"vue-echarts": "^6.7.1",
|
||||||
|
"vue-request": "2.0.4",
|
||||||
|
"vue-router": "^4.3.2",
|
||||||
|
"vue-toastification": "2.0.0-rc.5"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{js,jsx,ts,tsx,vue}": "eslint --cache --fix"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"sourcemap-codec": "npm:@jridgewell/sourcemap-codec@latest",
|
||||||
|
"array-includes": "npm:@nolyfill/array-includes@latest",
|
||||||
|
"array.prototype.findlastindex": "npm:@nolyfill/array.prototype.findlastindex@latest",
|
||||||
|
"array.prototype.flat": "npm:@nolyfill/array.prototype.flat@latest",
|
||||||
|
"array.prototype.flatmap": "npm:@nolyfill/array.prototype.flatmap@latest",
|
||||||
|
"arraybuffer.prorotype.slice": "npm:@nolyfill/arraybuffer.prorotype.slice@latest",
|
||||||
|
"function.prototype.name": "npm:@nolyfill/function.prototype.name@latest",
|
||||||
|
"has": "npm:@nolyfill/has@latest",
|
||||||
|
"is-regex": "npm:@nolyfill/is-regex@latest",
|
||||||
|
"object-keys": "npm:@nolyfill/object-keys@latest",
|
||||||
|
"object.assign": "npm:@nolyfill/object.assign@latest",
|
||||||
|
"object.entries": "npm:@nolyfill/object.entries@latest",
|
||||||
|
"object.fromentries": "npm:@nolyfill/object.fromentries@latest",
|
||||||
|
"object.values": "npm:@nolyfill/object.values@latest",
|
||||||
|
"vue-demi": "npm:vue-demi@latest"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"url": "https://github.com/dishait/tov-template"
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">= 0.25%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"Android >= 4.0",
|
||||||
|
"iOS >= 8"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"echarts-wordcloud": "^2.1.0",
|
||||||
|
"element-plus": "^2.9.2"
|
||||||
|
}
|
||||||
|
}
|
7335
front/pnpm-lock.yaml
generated
Normal file
7335
front/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
front/presets/autoprefixer.ts
Normal file
33
front/presets/autoprefixer.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { Preset } from 'unocss'
|
||||||
|
import browserslist from 'browserslist'
|
||||||
|
import { defaultBuildTargets } from './shared/detect'
|
||||||
|
import { browserslistToTargets, transformStyleAttribute } from 'lightningcss'
|
||||||
|
|
||||||
|
export default function autoprefixerPreset(
|
||||||
|
targets: string[] = defaultBuildTargets,
|
||||||
|
): Preset {
|
||||||
|
return {
|
||||||
|
name: 'unocss-preset-autoprefixer',
|
||||||
|
postprocess: (util) => {
|
||||||
|
const entries = util.entries
|
||||||
|
const { code } = transformStyleAttribute({
|
||||||
|
code: Buffer.from(
|
||||||
|
entries
|
||||||
|
.filter((item) => !item[0].startsWith('--un'))
|
||||||
|
.map((x) => x.join(':'))
|
||||||
|
.join(';'),
|
||||||
|
),
|
||||||
|
targets: browserslistToTargets(browserslist(targets)),
|
||||||
|
minify: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
util.entries = [
|
||||||
|
...entries.filter((item) => item[0].startsWith('--un')),
|
||||||
|
...(code
|
||||||
|
.toString()
|
||||||
|
.split(';')
|
||||||
|
.map((i) => i.split(':')) as [string, string | number][]),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
234
front/presets/index.ts
Normal file
234
front/presets/index.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import UnoCss from 'unocss/vite'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import {
|
||||||
|
AntDesignVueResolver,
|
||||||
|
ArcoResolver,
|
||||||
|
DevUiResolver,
|
||||||
|
ElementPlusResolver,
|
||||||
|
HeadlessUiResolver,
|
||||||
|
IduxResolver,
|
||||||
|
InklineResolver,
|
||||||
|
LayuiVueResolver,
|
||||||
|
NaiveUiResolver,
|
||||||
|
PrimeVueResolver,
|
||||||
|
QuasarResolver,
|
||||||
|
TDesignResolver,
|
||||||
|
VantResolver,
|
||||||
|
VarletUIResolver,
|
||||||
|
ViewUiResolver,
|
||||||
|
VueUseComponentsResolver,
|
||||||
|
Vuetify3Resolver,
|
||||||
|
} from 'unplugin-vue-components/resolvers'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import Markdown from 'unplugin-vue-markdown/vite'
|
||||||
|
import { VueRouterAutoImports } from 'unplugin-vue-router'
|
||||||
|
import Router from 'unplugin-vue-router/vite'
|
||||||
|
|
||||||
|
import { AutoGenerateImports, vue3Presets } from 'vite-auto-import-resolvers'
|
||||||
|
import Compression from 'vite-plugin-compression'
|
||||||
|
import EnvTypes from 'vite-plugin-env-types'
|
||||||
|
import Modules from 'vite-plugin-use-modules'
|
||||||
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import Layouts from 'vite-plugin-vue-meta-layouts'
|
||||||
|
import Legacy from 'vite-plugin-legacy-swc'
|
||||||
|
import Vue from '@vitejs/plugin-vue'
|
||||||
|
import Jsx from '@vitejs/plugin-vue-jsx'
|
||||||
|
|
||||||
|
// 内置插件
|
||||||
|
import {
|
||||||
|
Alias,
|
||||||
|
Lightningcss,
|
||||||
|
Restart,
|
||||||
|
Warmup,
|
||||||
|
Layers,
|
||||||
|
Optimize,
|
||||||
|
} from './plugins'
|
||||||
|
import { defaultBuildTargets, detectResolvers, useEnv } from './shared/detect'
|
||||||
|
import { r } from './shared/path'
|
||||||
|
import type { PluginOption } from 'vite'
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const env = useEnv()
|
||||||
|
const safelist =
|
||||||
|
'prose px-2 sm:px-0 md:prose-lg lg:prose-lg dark:prose-invert text-left w-screen prose-slate prose-img:rounded-xl prose-headings:underline prose-a:text-blue-600'
|
||||||
|
|
||||||
|
const plugins: PluginOption[] = [
|
||||||
|
/**
|
||||||
|
* vite 配置层
|
||||||
|
* 通过 mode 区分 vite 配置文件 (experimental)
|
||||||
|
*/
|
||||||
|
Layers(),
|
||||||
|
/**
|
||||||
|
* 兼容不支持 esmModule 的浏览器
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-legacy
|
||||||
|
*/
|
||||||
|
Legacy({
|
||||||
|
targets: defaultBuildTargets,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 智能启动 lightningcss
|
||||||
|
*/
|
||||||
|
Lightningcss(),
|
||||||
|
/**
|
||||||
|
* 启动优化
|
||||||
|
*/
|
||||||
|
Optimize(),
|
||||||
|
/**
|
||||||
|
* 环境变量类型提示
|
||||||
|
* https://github.com/dishait/vite-plugin-env-types
|
||||||
|
*/
|
||||||
|
EnvTypes({
|
||||||
|
dts: r('presets/types/env.d.ts'),
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 内置的预热,可以加快冷启动
|
||||||
|
*/
|
||||||
|
Warmup(),
|
||||||
|
/**
|
||||||
|
* 文件路由
|
||||||
|
* https://github.com/posva/unplugin-vue-router
|
||||||
|
*/
|
||||||
|
Router({
|
||||||
|
routesFolder: r('src/pages'),
|
||||||
|
dts: r('presets/types/type-router.d.ts'),
|
||||||
|
extensions: ['.md', '.vue', '.tsx', '.jsx'],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 自动安装 vue 插件
|
||||||
|
* https://github.com/dishait/vite-plugin-use-modules
|
||||||
|
*/
|
||||||
|
Modules({
|
||||||
|
auto: true,
|
||||||
|
// 内部使用虚拟模块,运行在前端,所以不需要 r 重写路径
|
||||||
|
target: 'src/plugins',
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* vue 官方插件,用来解析 sfc 单文件组件
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-vue
|
||||||
|
*/
|
||||||
|
Vue({
|
||||||
|
include: [/\.vue$/, /\.md$/],
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 布局系统
|
||||||
|
* https://github.com/dishait/vite-plugin-vue-meta-layouts
|
||||||
|
*/
|
||||||
|
Layouts({
|
||||||
|
skipTopLevelRouteLayout: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件自动按需引入
|
||||||
|
* https://github.com/antfu/unplugin-vue-components
|
||||||
|
*/
|
||||||
|
Components({
|
||||||
|
directoryAsNamespace: true,
|
||||||
|
include: [/\.vue$/, /\.vue\?vue/, /\.[tj]sx$/, /\.md$/],
|
||||||
|
extensions: ['md', 'vue', 'tsx', 'jsx'],
|
||||||
|
dts: r('presets/types/components.d.ts'),
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
from: 'vue-router',
|
||||||
|
names: ['RouterLink', 'RouterView'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resolvers: detectResolvers({
|
||||||
|
onlyExist: [
|
||||||
|
[VantResolver(), 'vant'],
|
||||||
|
[QuasarResolver(), 'quasar'],
|
||||||
|
[DevUiResolver(), 'vue-devui'],
|
||||||
|
[NaiveUiResolver(), 'naive-ui'],
|
||||||
|
[Vuetify3Resolver(), 'vuetify'],
|
||||||
|
[PrimeVueResolver(), 'primevue'],
|
||||||
|
[ViewUiResolver(), 'view-design'],
|
||||||
|
[LayuiVueResolver(), 'layui-vue'],
|
||||||
|
[VarletUIResolver(), '@varlet/ui'],
|
||||||
|
[IduxResolver(), '@idux/components'],
|
||||||
|
[InklineResolver(), '@inkline/inkline'],
|
||||||
|
[ElementPlusResolver(), 'element-plus'],
|
||||||
|
[HeadlessUiResolver(), '@headlessui/vue'],
|
||||||
|
[ArcoResolver(), '@arco-design/web-vue'],
|
||||||
|
[AntDesignVueResolver({ importStyle: false }), 'ant-design-vue'],
|
||||||
|
[VueUseComponentsResolver(), '@vueuse/components'],
|
||||||
|
[TDesignResolver({ library: 'vue-next' }), 'tdesign-vue-next'],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* jsx 和 tsx 支持
|
||||||
|
* https://www.npmjs.com/package/@vitejs/plugin-vue-jsx
|
||||||
|
*/
|
||||||
|
Jsx(),
|
||||||
|
/**
|
||||||
|
* 生产环境资源压缩
|
||||||
|
* https://github.com/vbenjs/vite-plugin-compression
|
||||||
|
*/
|
||||||
|
Compression({
|
||||||
|
// @ts-ignore
|
||||||
|
algorithm: env.VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* 别名插件 (内置)
|
||||||
|
* 支持 `~` 和 `@` 别名到 `src`
|
||||||
|
*/
|
||||||
|
Alias(),
|
||||||
|
/**
|
||||||
|
* 强制重启 (内置)
|
||||||
|
* 如果 package.json 或 pnpm-lock.yaml 更新的话,强制重启
|
||||||
|
*/
|
||||||
|
Restart(),
|
||||||
|
/**
|
||||||
|
* css 原子引擎
|
||||||
|
* https://github.com/unocss/unocss
|
||||||
|
*/
|
||||||
|
UnoCss({
|
||||||
|
safelist: env.VITE_APP_MARKDOWN ? safelist.split(' ') : undefined,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
/**
|
||||||
|
* 开发面板
|
||||||
|
* https://github.com/webfansplz/vite-plugin-vue-devtools
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_DEV_TOOLS) {
|
||||||
|
plugins.push(VueDevTools())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* api 自动按需引入
|
||||||
|
* https://github.com/antfu/unplugin-auto-import
|
||||||
|
*/
|
||||||
|
if (env.VITE_APP_API_AUTO_IMPORT) {
|
||||||
|
const dirs = env.VITE_APP_DIR_API_AUTO_IMPORT
|
||||||
|
? ['src/stores/**', 'src/composables/**', 'src/api/**']
|
||||||
|
: []
|
||||||
|
plugins.push(
|
||||||
|
AutoImport({
|
||||||
|
dirs,
|
||||||
|
vueTemplate: true,
|
||||||
|
dts: r('presets/types/auto-imports.d.ts'),
|
||||||
|
imports: [
|
||||||
|
...AutoGenerateImports({
|
||||||
|
include: [...vue3Presets],
|
||||||
|
exclude: ['vue-router'],
|
||||||
|
}),
|
||||||
|
VueRouterAutoImports,
|
||||||
|
],
|
||||||
|
resolvers: detectResolvers({
|
||||||
|
onlyExist: [
|
||||||
|
[ElementPlusResolver(), 'element-plus'],
|
||||||
|
[TDesignResolver({ library: 'vue-next' }), 'tdesign-vue-next'],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
eslintrc: {
|
||||||
|
enabled: true,
|
||||||
|
globalsPropValue: true,
|
||||||
|
filepath: r('presets/eslint/.eslintrc-auto-import.json'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
}
|
27
front/presets/plugins/alias.ts
Normal file
27
front/presets/plugins/alias.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 别名插件
|
||||||
|
* @description 支持 `~` 和 `@` 别名到 `src`
|
||||||
|
*/
|
||||||
|
export function Alias(): Plugin {
|
||||||
|
const src = r('./src')
|
||||||
|
return {
|
||||||
|
name: 'vite-alias',
|
||||||
|
enforce: 'pre',
|
||||||
|
config(config) {
|
||||||
|
config.resolve ??= {}
|
||||||
|
config.resolve.alias = [
|
||||||
|
{
|
||||||
|
find: /^~/,
|
||||||
|
replacement: src,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
find: /^@\//,
|
||||||
|
replacement: src + '/',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
6
front/presets/plugins/index.ts
Normal file
6
front/presets/plugins/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { Alias } from './alias'
|
||||||
|
export { Layers } from './layers'
|
||||||
|
export { Warmup } from './warmup'
|
||||||
|
export { Restart } from './restart'
|
||||||
|
export { Optimize } from './optimize'
|
||||||
|
export { Lightningcss } from './lightningcss'
|
41
front/presets/plugins/layers.ts
Normal file
41
front/presets/plugins/layers.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import { basename } from 'path'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
import { Restart } from './restart'
|
||||||
|
import { createConsola } from 'consola'
|
||||||
|
import type { Plugin, UserConfig } from 'vite'
|
||||||
|
import { Layers as loadLayer, detectMode } from 'vite-layers'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('layers')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vite 配置层
|
||||||
|
* @description 通过 mode 区分 vite 配置文件 (experimental)
|
||||||
|
*/
|
||||||
|
export function Layers(): Plugin {
|
||||||
|
const mode = detectMode()
|
||||||
|
const modeFiles = [mode.slice(0, 3), mode].map((mode) =>
|
||||||
|
r(`vite.config.${mode}.ts`),
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
...Restart(modeFiles.map((modeFile) => basename(modeFile))),
|
||||||
|
name: 'vite-plugin-layers',
|
||||||
|
enforce: 'post',
|
||||||
|
async config(config) {
|
||||||
|
const modeFile = modeFiles.find((modeFile) => existsSync(modeFile))
|
||||||
|
if (modeFile) {
|
||||||
|
logger
|
||||||
|
.withTag(mode)
|
||||||
|
.success(
|
||||||
|
`vite.config.ts → ${basename(modeFile)} ${gray(`(experimental)`)}`,
|
||||||
|
)
|
||||||
|
return loadLayer({
|
||||||
|
logger: false,
|
||||||
|
extends: [config, modeFile],
|
||||||
|
}) as UserConfig
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
62
front/presets/plugins/lightningcss.ts
Normal file
62
front/presets/plugins/lightningcss.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { existsSync } from 'fs'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { createConsola } from 'consola'
|
||||||
|
import { isPackageExists } from 'local-pkg'
|
||||||
|
import { browserslistToTargets } from 'lightningcss'
|
||||||
|
import { defaultBuildTargets } from '../shared/detect'
|
||||||
|
|
||||||
|
const name = 'vite-plugin-fire-lightningcss'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('css')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能开启 lightningcss (如果不使用预处理器,或者 postcss)
|
||||||
|
*/
|
||||||
|
export function Lightningcss(): Plugin {
|
||||||
|
const packages = ['less', 'sass', 'stylus']
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
config(config) {
|
||||||
|
config.css ??= {}
|
||||||
|
config.build ??= {}
|
||||||
|
const hasPreprocessor = packages.some((p) => isPackageExists(p))
|
||||||
|
|
||||||
|
const { postcss, modules, transformer } = config.css
|
||||||
|
const conflictConfiguration = [postcss, modules, transformer].some(
|
||||||
|
(c) => !isUndefined(c),
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasPostcssConfigFile = [
|
||||||
|
'postcss.config.js',
|
||||||
|
'postcss.config.cts',
|
||||||
|
'postcss.config.ts',
|
||||||
|
].some((c) => existsSync(c))
|
||||||
|
|
||||||
|
// 如果有预处理器,冲突配置或者 postcss 配置文件则禁用
|
||||||
|
const disabled =
|
||||||
|
hasPreprocessor || conflictConfiguration || hasPostcssConfigFile
|
||||||
|
if (!disabled) {
|
||||||
|
const transformer = 'lightningcss'
|
||||||
|
config.css.transformer = transformer
|
||||||
|
let tip = `${transformer} ${gray(transformer)}`
|
||||||
|
|
||||||
|
if (isUndefined(config.build.cssMinify)) {
|
||||||
|
config.build.cssMinify = 'lightningcss'
|
||||||
|
tip = `${transformer} ${gray('(transformer + cssMinify)')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUndefined(config.css.lightningcss?.targets)) {
|
||||||
|
config.css.lightningcss ??= {}
|
||||||
|
config.css.lightningcss.targets =
|
||||||
|
browserslistToTargets(defaultBuildTargets)
|
||||||
|
}
|
||||||
|
logger.success(tip)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUndefined(v: unknown): v is undefined {
|
||||||
|
return typeof v === 'undefined'
|
||||||
|
}
|
||||||
|
}
|
19
front/presets/plugins/optimize.ts
Normal file
19
front/presets/plugins/optimize.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { createConsola } from 'consola'
|
||||||
|
import { gray } from 'kolorist'
|
||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
const logger = createConsola().withTag('optimize')
|
||||||
|
export function Optimize(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-optimize',
|
||||||
|
config(config) {
|
||||||
|
config.css ??= {}
|
||||||
|
config.optimizeDeps ??= {}
|
||||||
|
config.css.preprocessorMaxWorkers = true
|
||||||
|
config.optimizeDeps.holdUntilCrawlEnd = false
|
||||||
|
logger.success(
|
||||||
|
`optimize ${gray('(preprocessorMaxWorkers + closeHoldUntilCrawlEnd)')}`,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
30
front/presets/plugins/restart.ts
Normal file
30
front/presets/plugins/restart.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
import { utimes } from 'fs/promises'
|
||||||
|
import { r } from '../shared/path'
|
||||||
|
import { debounce } from 'perfect-debounce'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
import { slash } from 'vite-layers'
|
||||||
|
|
||||||
|
const defaultPaths = ['package.json', 'pnpm-lock.yaml']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制重启
|
||||||
|
* @description 如果监听更新的话,强制重启项目
|
||||||
|
* @param paths 监听重启路径,默认为 ['package.json', 'pnpm-lock.yaml']
|
||||||
|
*/
|
||||||
|
export function Restart(paths = defaultPaths): Plugin {
|
||||||
|
paths = paths.map((path) => slash(resolve(path)))
|
||||||
|
const restart = debounce(async function touch() {
|
||||||
|
const time = new Date()
|
||||||
|
await utimes(r('vite.config.ts'), time, time)
|
||||||
|
}, 1000)
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-force-restart',
|
||||||
|
apply: 'serve',
|
||||||
|
async watchChange(id) {
|
||||||
|
if (paths.includes(id)) {
|
||||||
|
await restart()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
21
front/presets/plugins/warmup.ts
Normal file
21
front/presets/plugins/warmup.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { Plugin } from 'vite'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预热
|
||||||
|
* @description 内置的预热,可以加快冷启动
|
||||||
|
*/
|
||||||
|
export function Warmup(): Plugin {
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-warmup',
|
||||||
|
apply: 'serve',
|
||||||
|
config(config) {
|
||||||
|
const src = './src/**/*'
|
||||||
|
config.server ??= {}
|
||||||
|
config.server.warmup ??= {}
|
||||||
|
config.server.warmup.clientFiles ??= []
|
||||||
|
if (!config.server.warmup.clientFiles.includes(src)) {
|
||||||
|
config.server.warmup.clientFiles.push(src)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
76
front/presets/shared/detect.ts
Normal file
76
front/presets/shared/detect.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* 侦察模块
|
||||||
|
* @description 自动检测环境并智能生成
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { r } from './path'
|
||||||
|
import { loadEnv } from 'vite'
|
||||||
|
import browserslist from 'browserslist'
|
||||||
|
import { detectMode } from 'vite-layers'
|
||||||
|
import { isPackageExists } from 'local-pkg'
|
||||||
|
import type { ComponentResolver } from 'unplugin-vue-components'
|
||||||
|
|
||||||
|
const { loadConfig: browserslistLoadConfig } = browserslist
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认打包目标 (浏览器兼容程度)
|
||||||
|
*/
|
||||||
|
export const defaultBuildTargets = browserslistLoadConfig({
|
||||||
|
path: r('./'),
|
||||||
|
}) || ['last 2 versions and not dead, > 0.3%, Firefox ESR']
|
||||||
|
|
||||||
|
type Arrayable<T> = T | Array<T>
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
onlyExist?: [Arrayable<ComponentResolver>, string][]
|
||||||
|
include?: ComponentResolver[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发现 resolvers
|
||||||
|
*/
|
||||||
|
export function detectResolvers(options: Options = {}) {
|
||||||
|
const { onlyExist = [], include = [] } = options
|
||||||
|
|
||||||
|
const existedResolvers = []
|
||||||
|
for (let i = 0; i < onlyExist.length; i++) {
|
||||||
|
const [resolver, packageName] = onlyExist[i]
|
||||||
|
if (
|
||||||
|
isPackageExists(packageName, {
|
||||||
|
paths: [r('./')],
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
existedResolvers.push(resolver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existedResolvers.push(...include)
|
||||||
|
|
||||||
|
return existedResolvers
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取环境变量
|
||||||
|
export function useEnv() {
|
||||||
|
function stringToBoolean(v: string) {
|
||||||
|
return Boolean(v === 'true' || false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_APP_DEV_TOOLS,
|
||||||
|
VITE_APP_MARKDOWN,
|
||||||
|
VITE_APP_API_AUTO_IMPORT,
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION,
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT,
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
} = loadEnv(detectMode(), '.')
|
||||||
|
|
||||||
|
return {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_APP_COMPRESSINON_ALGORITHM,
|
||||||
|
VITE_APP_DEV_TOOLS: stringToBoolean(VITE_APP_DEV_TOOLS),
|
||||||
|
VITE_APP_MARKDOWN: stringToBoolean(VITE_APP_MARKDOWN),
|
||||||
|
VITE_APP_API_AUTO_IMPORT: stringToBoolean(VITE_APP_API_AUTO_IMPORT),
|
||||||
|
VITE_APP_MOCK_IN_PRODUCTION: stringToBoolean(VITE_APP_MOCK_IN_PRODUCTION),
|
||||||
|
VITE_APP_DIR_API_AUTO_IMPORT: stringToBoolean(VITE_APP_DIR_API_AUTO_IMPORT),
|
||||||
|
}
|
||||||
|
}
|
89
front/presets/shared/mock.ts
Normal file
89
front/presets/shared/mock.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
/**
|
||||||
|
* issue: https://github.com/vbenjs/vite-plugin-mock/issues/47
|
||||||
|
* fix: https://github.com/vbenjs/vite-plugin-mock/issues/47#issuecomment-982724613
|
||||||
|
*/
|
||||||
|
import Mock from 'mockjs'
|
||||||
|
|
||||||
|
export function createFetchSever(mockList: any[]) {
|
||||||
|
if (!window['originFetch']) {
|
||||||
|
window['originFetch'] = window.fetch
|
||||||
|
window.fetch = function (fetchUrl: string, init: any) {
|
||||||
|
const currentMock = mockList.find((mi) => fetchUrl.includes(mi.url))
|
||||||
|
if (currentMock) {
|
||||||
|
const result = createFetchReturn(currentMock, init)
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return window['originFetch'](fetchUrl, init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __param2Obj__(url: string) {
|
||||||
|
const search = url.split('?')[1]
|
||||||
|
if (!search) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
return JSON.parse(
|
||||||
|
'{"' +
|
||||||
|
decodeURIComponent(search)
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/&/g, '","')
|
||||||
|
.replace(/=/g, '":"')
|
||||||
|
.replace(/\+/g, ' ') +
|
||||||
|
'"}',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function __Fetch2ExpressReqWrapper__(handle: () => any) {
|
||||||
|
return function (options: any) {
|
||||||
|
let result = null
|
||||||
|
if (typeof handle === 'function') {
|
||||||
|
const { body, method, url, headers } = options
|
||||||
|
|
||||||
|
let b = body
|
||||||
|
b = JSON.parse(body)
|
||||||
|
result = handle({
|
||||||
|
method,
|
||||||
|
body: b,
|
||||||
|
query: __param2Obj__(url),
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
result = handle
|
||||||
|
}
|
||||||
|
|
||||||
|
return Mock.mock(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep = (delay = 0) => {
|
||||||
|
if (delay) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, delay)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createFetchReturn(mock: any, init) {
|
||||||
|
const { timeout, response } = mock
|
||||||
|
const mockFn = __Fetch2ExpressReqWrapper__(response)
|
||||||
|
const data = mockFn(init)
|
||||||
|
await sleep(timeout)
|
||||||
|
const result = {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
clone() {
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
text() {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
},
|
||||||
|
json() {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
15
front/presets/shared/path.ts
Normal file
15
front/presets/shared/path.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { dirname, resolve } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
|
const _dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|
||||||
|
const root = resolve(_dirname, '../../')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径重写
|
||||||
|
* @param path 相对路径
|
||||||
|
* @returns 基于根目录的相对路径
|
||||||
|
*/
|
||||||
|
export function r(path: string) {
|
||||||
|
return resolve(root, path).replaceAll('\\', '/')
|
||||||
|
}
|
17
front/presets/types/vite.d.ts
vendored
Normal file
17
front/presets/types/vite.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-plugin-pages/client" />
|
||||||
|
/// <reference types="unplugin-vue-router/client" />
|
||||||
|
/// <reference types="vite-plugin-use-modules/client" />
|
||||||
|
/// <reference types="vite-plugin-vue-meta-layouts/client" />
|
||||||
|
|
||||||
|
declare module "*.vue" {
|
||||||
|
import type { DefineComponent } from "vue";
|
||||||
|
const component: DefineComponent<{}, {}, any>;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "*.md" {
|
||||||
|
import { ComponentOptions } from "vue";
|
||||||
|
const Component: ComponentOptions;
|
||||||
|
export default Component;
|
||||||
|
}
|
BIN
front/public/login/e36341619bf8f04dcbdc6b01105a85a.png
Normal file
BIN
front/public/login/e36341619bf8f04dcbdc6b01105a85a.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 342 KiB |
BIN
front/public/logo.png
Normal file
BIN
front/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 371 KiB |
1
front/public/notFound/33.svg
Normal file
1
front/public/notFound/33.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.4 KiB |
3
front/renovate.json
Normal file
3
front/renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["github>unjs/renovate-config"]
|
||||||
|
}
|
76
front/scripts/create.cjs
Normal file
76
front/scripts/create.cjs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const { existsSync } = require('fs')
|
||||||
|
|
||||||
|
const { showDir, showExt, moduleTypes } = require('./shared/base.cjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动创建
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function create(plop) {
|
||||||
|
let exist = null
|
||||||
|
let modulePath = null
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动创建',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'component',
|
||||||
|
message: '您希望生成哪种类型的模块?',
|
||||||
|
choices: moduleTypes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'isMarkdown',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否 markdown 类型?',
|
||||||
|
default: false,
|
||||||
|
// 如果是 page 类型需要询问是否为 markdown 类型
|
||||||
|
when({ type }) {
|
||||||
|
return type === 'page'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'input',
|
||||||
|
message({ type }) {
|
||||||
|
return `请输入 ${type} 的命名`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldReset',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message({ type }) {
|
||||||
|
return `目标 ${type} 已存在,是否重置?`
|
||||||
|
},
|
||||||
|
// 确认模块是否已存在,是则询问是否重置
|
||||||
|
when({ type, name, isMarkdown }) {
|
||||||
|
const dir = showDir(type)
|
||||||
|
const ext = showExt(type, isMarkdown)
|
||||||
|
modulePath = `src/${dir}/${name}.${ext}`
|
||||||
|
exist = existsSync(modulePath)
|
||||||
|
if (exist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type, shouldReset } = answer
|
||||||
|
if (exist && !shouldReset) {
|
||||||
|
throw new Error(`${type} 创建失败`)
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: `../${modulePath}`,
|
||||||
|
templateFile: `./template/${type}.hbs`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = create
|
36
front/scripts/deps-fresh.cjs
Normal file
36
front/scripts/deps-fresh.cjs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
const { execSync } = require('child_process')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动更新依赖
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function depsFresh(plop) {
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动更新依赖',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'patch',
|
||||||
|
message: '你希望发布一个什么版本?',
|
||||||
|
choices: ['patch', 'minor', 'major'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldWrite',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message: '是否直接更新?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type, shouldWrite } = answer
|
||||||
|
|
||||||
|
execSync(`npx taze ${type} ${shouldWrite ? '-w' : ''}`, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = depsFresh
|
65
front/scripts/release.cjs
Normal file
65
front/scripts/release.cjs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
const { createConsola } = require('consola')
|
||||||
|
const { execSync } = require('child_process')
|
||||||
|
const { repository } = require('../package.json')
|
||||||
|
const { gray } = require('kolorist')
|
||||||
|
const { simpleGit } = require('simple-git')
|
||||||
|
const logger = createConsola().withTag('release')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动发版
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
async function release(plop) {
|
||||||
|
const git = simpleGit()
|
||||||
|
|
||||||
|
const remotes = await git.getRemotes(true)
|
||||||
|
|
||||||
|
const urls = remotes.map((r) => {
|
||||||
|
return r.refs.push
|
||||||
|
.replace('git@github.com:', 'https://github.com/')
|
||||||
|
.replace('.git', '')
|
||||||
|
})
|
||||||
|
let allowRelease = false
|
||||||
|
if (!urls.includes(repository.url)) {
|
||||||
|
allowRelease = await logger.prompt(`是否发布到 ${gray(repository.url)}`, {
|
||||||
|
type: 'confirm',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
allowRelease = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowRelease) {
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动发版',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
default: 'patch',
|
||||||
|
message: '你希望发布一个什么版本?',
|
||||||
|
choices: [
|
||||||
|
'patch',
|
||||||
|
'minor',
|
||||||
|
'major',
|
||||||
|
'prepatch',
|
||||||
|
'premajor',
|
||||||
|
'preminor',
|
||||||
|
'prerelease',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
const { type } = answer
|
||||||
|
execSync(
|
||||||
|
`npx changelogen --${type} --release && git push --follow-tags`,
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = release
|
75
front/scripts/remove.cjs
Normal file
75
front/scripts/remove.cjs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
const { unlinkSync } = require('fs')
|
||||||
|
const { readdir } = require('fs/promises')
|
||||||
|
const { basename } = require('path')
|
||||||
|
const { showDir, moduleTypes } = require('./shared/base.cjs')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动删除
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function remove(plop) {
|
||||||
|
plop.setActionType('remove', (answers) => {
|
||||||
|
const { name, type, shouldRemove } = answers
|
||||||
|
const dir = showDir(type)
|
||||||
|
const target = `./src/${dir}/${name}`
|
||||||
|
if (shouldRemove) {
|
||||||
|
return unlinkSync(target)
|
||||||
|
}
|
||||||
|
throw new Error(`删除 ${target} 失败`)
|
||||||
|
})
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '自动删除',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
type: 'list',
|
||||||
|
message: '请选择您要删除的类型',
|
||||||
|
async choices() {
|
||||||
|
const entrys = await readdir('./src', {
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
const dirs = entrys.filter((e) => e.isDirectory())
|
||||||
|
const types = moduleTypes.filter((type) => {
|
||||||
|
const dir = showDir(type)
|
||||||
|
return dirs.includes(`./src/${dir}`)
|
||||||
|
})
|
||||||
|
return types
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'list',
|
||||||
|
message({ type }) {
|
||||||
|
return `请选择您要删除的 ${type} 模块`
|
||||||
|
},
|
||||||
|
async choices({ type }) {
|
||||||
|
const dir = showDir(type)
|
||||||
|
const entrys = await readdir(`src/${dir}`, {
|
||||||
|
recursive: false,
|
||||||
|
withFileTypes: true,
|
||||||
|
})
|
||||||
|
let modules = entrys.filter((e) => e.isFile())
|
||||||
|
modules = modules.map((module) => {
|
||||||
|
return basename(module)
|
||||||
|
})
|
||||||
|
return modules
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shouldRemove',
|
||||||
|
type: 'confirm',
|
||||||
|
default: false,
|
||||||
|
message: '再次确认是否删除',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
type: 'remove',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = remove
|
113
front/scripts/safe-init.cjs
Normal file
113
front/scripts/safe-init.cjs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
const { resolve } = require('path')
|
||||||
|
const { gray, green } = require('kolorist')
|
||||||
|
const { createConsola } = require('consola')
|
||||||
|
const { existsSync, lstatSync } = require('fs')
|
||||||
|
const { removeSync, emptyDirSync } = require('fs-extra')
|
||||||
|
|
||||||
|
function slash(path) {
|
||||||
|
return path.replace(/\\/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function r(dir) {
|
||||||
|
return slash(resolve(__dirname, '../', dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
const entrys = [
|
||||||
|
'src/components',
|
||||||
|
'src/api',
|
||||||
|
'mock',
|
||||||
|
'layouts/default.vue',
|
||||||
|
'src/pages/index.vue',
|
||||||
|
'src/pages/about.md',
|
||||||
|
'src/pages/echarts.vue',
|
||||||
|
'src/stores',
|
||||||
|
'locales/简体中文',
|
||||||
|
'locales/English',
|
||||||
|
]
|
||||||
|
|
||||||
|
const resolvedEntrys = entrys.map((entry) => r(entry))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全初始化
|
||||||
|
* @param {import('plop').NodePlopAPI} plop
|
||||||
|
*/
|
||||||
|
function safeInit(plop) {
|
||||||
|
const logger = createConsola().withTag('safe:init')
|
||||||
|
|
||||||
|
logger.warn('实验性功能')
|
||||||
|
|
||||||
|
plop.setGenerator('controller', {
|
||||||
|
description: '安全初始化',
|
||||||
|
prompts: [
|
||||||
|
{
|
||||||
|
name: 'yes',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否安全的初始化?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cleanStyles',
|
||||||
|
type: 'confirm',
|
||||||
|
message: '是否清理 styles?',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions(answer) {
|
||||||
|
if (!answer.yes) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answer.cleanStyles) {
|
||||||
|
resolvedEntrys.push(r('src/styles'))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
|
||||||
|
// 这里不用异步是因为 plop action 只支持同步
|
||||||
|
resolvedEntrys.forEach((e) => {
|
||||||
|
if (!existsSync(e)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const entry = lstatSync(e)
|
||||||
|
if (entry.isFile()) {
|
||||||
|
removeSync(e)
|
||||||
|
logClean(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
emptyDirSync(e)
|
||||||
|
logClean(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: '../src/pages/index.vue',
|
||||||
|
templateFile: './template/page.hbs',
|
||||||
|
data: {
|
||||||
|
name: 'index',
|
||||||
|
isMarkdown: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'add',
|
||||||
|
force: true,
|
||||||
|
path: '../src/layouts/default.vue',
|
||||||
|
templateFile: './template/layout.hbs',
|
||||||
|
data: {
|
||||||
|
name: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function logClean(path) {
|
||||||
|
console.log(`${green('√ clean')} ${gray(path)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = safeInit
|
41
front/scripts/shared/base.cjs
Normal file
41
front/scripts/shared/base.cjs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 获取扩展名
|
||||||
|
* @param {string} type 模块类型
|
||||||
|
* @param {boolean} isMarkdown 是否是 markdown,默认为 false
|
||||||
|
* @returns {string} 扩展名
|
||||||
|
*/
|
||||||
|
const showExt = (type, isMarkdown = false) => {
|
||||||
|
const isTs = type === 'api' || type === 'store' || type === 'module'
|
||||||
|
const ext = isMarkdown ? 'md' : isTs ? 'ts' : 'vue'
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块类型
|
||||||
|
*/
|
||||||
|
const moduleTypes = [
|
||||||
|
'api',
|
||||||
|
'page',
|
||||||
|
'store',
|
||||||
|
'layout',
|
||||||
|
'module',
|
||||||
|
'component',
|
||||||
|
'composable',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取目录
|
||||||
|
* @param {string} type 类型
|
||||||
|
*/
|
||||||
|
const showDir = (type) => {
|
||||||
|
if (type === 'api') {
|
||||||
|
return 'api'
|
||||||
|
}
|
||||||
|
return `${type}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
showExt,
|
||||||
|
showDir,
|
||||||
|
moduleTypes,
|
||||||
|
}
|
2
front/scripts/template/api.hbs
Normal file
2
front/scripts/template/api.hbs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
11
front/scripts/template/component.hbs
Normal file
11
front/scripts/template/component.hbs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{name}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
5
front/scripts/template/composable.hbs
Normal file
5
front/scripts/template/composable.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
export default {{name}} = () => {
|
||||||
|
|
||||||
|
}
|
4
front/scripts/template/layout.hbs
Normal file
4
front/scripts/template/layout.hbs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
{{pascalCase name}} layout
|
||||||
|
<router-view />
|
||||||
|
</template>
|
6
front/scripts/template/module.hbs
Normal file
6
front/scripts/template/module.hbs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { App } from "vue"
|
||||||
|
|
||||||
|
|
||||||
|
export default (app: App) => {
|
||||||
|
|
||||||
|
}
|
17
front/scripts/template/page.hbs
Normal file
17
front/scripts/template/page.hbs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{#if isMarkdown}}
|
||||||
|
## {{pascalCase name}} Page
|
||||||
|
|
||||||
|
> The page is markdown file
|
||||||
|
{{else}}
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{pascalCase name}} page
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
{{/if}}
|
9
front/scripts/template/store.hbs
Normal file
9
front/scripts/template/store.hbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('{{name}}', {
|
||||||
|
state() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {}
|
||||||
|
})
|
9
front/src/App.vue
Normal file
9
front/src/App.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
p {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
41
front/src/api/user/adminUserApi.ts
Normal file
41
front/src/api/user/adminUserApi.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { adminRequest } from '~/composables/adminRequest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function captchaAdmin(uid: Number) {
|
||||||
|
return adminRequest.get("/captcha",{
|
||||||
|
params:{uuid:uid}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function registerAdmin(data: any) {
|
||||||
|
return adminRequest.post("/api/register", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出
|
||||||
|
*/
|
||||||
|
export function logoutAdmin() {
|
||||||
|
return adminRequest.post("/logout")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
export function userInfoAdmin() {
|
||||||
|
return adminRequest.get("/sys/user/info")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
export function updatePasswordAdmin(data:any) {
|
||||||
|
return adminRequest.put("/sys/user/password",data)
|
||||||
|
}
|
185
front/src/components/Heads.vue
Normal file
185
front/src/components/Heads.vue
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div class="head">
|
||||||
|
<div class="head_l">
|
||||||
|
</div>
|
||||||
|
<span class="head-title">智能银行企业信用系统</span>
|
||||||
|
<el-dropdown>
|
||||||
|
<div class="head_r">
|
||||||
|
<!-- <img :src="userStore().adminUserInfo.avatar" alt="头像" class="profile" />-->
|
||||||
|
<div class="head_user">
|
||||||
|
<!-- <div class="head_user_name">{{ userStore().userInfo.username }}</div>-->
|
||||||
|
<div class="head_user_desc"> {{userStore().userInfo.role === 0 ? '管理员' : '普通用户'}} </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item @click="drawer = true">个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
<el-drawer
|
||||||
|
v-model="drawer"
|
||||||
|
title="个人中心"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="state.form"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item
|
||||||
|
prop="old_password"
|
||||||
|
label="原始密码"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '原始密码不能为空',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.form.old_password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
prop="new_password"
|
||||||
|
label="新密码"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '新密码不能为空',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.form.new_password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
|
||||||
|
<el-button type="primary" @click="submitForm(formRef)">确定修改</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-drawer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import type { FormInstance } from 'element-plus'
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
import { adminRequest } from '~/composables/adminRequest'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const drawer = ref(false)
|
||||||
|
|
||||||
|
// 明确userInfo类型,避免类型错误
|
||||||
|
interface UserInfo {
|
||||||
|
username?: string;
|
||||||
|
role?: number;
|
||||||
|
}
|
||||||
|
const state = reactive(<any>{
|
||||||
|
form: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
function userStore() {
|
||||||
|
// 假设全局有userStore,返回类型加上userInfo类型声明
|
||||||
|
return {
|
||||||
|
isLogin: false,
|
||||||
|
userInfo: {} as UserInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
ElMessage.success('退出成功~')
|
||||||
|
userStore().isLogin = false
|
||||||
|
userStore().userInfo = {}
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param formEl
|
||||||
|
*/
|
||||||
|
const submitForm = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
if (state.form.old_password !== state.form.new_password) {
|
||||||
|
ElMessage.error('两次密码输入不一致')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.form.username = userStore().userInfo.username
|
||||||
|
adminRequest.post('/change_password', state.form).then(() => {
|
||||||
|
userStore().isLogin = false
|
||||||
|
window.location.href = '/login'
|
||||||
|
ElMessage.success('修改成功')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.head {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background: linear-gradient(90deg, #2563eb 60%, #1e3a8a 100%);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.head_l {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head_l img {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head_r {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 30px;
|
||||||
|
|
||||||
|
.profile {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f6f6f6;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head_user {
|
||||||
|
.head_user_name {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #333333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head_user_desc {
|
||||||
|
color: #a7a7a7;
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.head-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 22px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
45
front/src/composables/adminRequest.ts
Normal file
45
front/src/composables/adminRequest.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const adminRequest = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_ADMIN_API_BASE_URL
|
||||||
|
})
|
||||||
|
// 添加请求拦截器
|
||||||
|
adminRequest.interceptors.request.use(
|
||||||
|
function(config) {
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
toast.warning(error.message ?? '未知请求错误')
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 添加响应拦截器
|
||||||
|
adminRequest.interceptors.response.use(
|
||||||
|
function(response) {
|
||||||
|
const code = response.data.code
|
||||||
|
switch (Number(code)) {
|
||||||
|
case 500:
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
case 401:
|
||||||
|
ElMessage.error(response.data.msg)
|
||||||
|
window.open(`/login`, '_self')
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
default:
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
let { msg, message } = error.response?.data ?? {}
|
||||||
|
if (!msg && message) {
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
ElMessage.warning(msg)
|
||||||
|
// 超出 2xx 范围的状态码都会触发该函数。
|
||||||
|
// 对响应错误做点什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
26
front/src/composables/env.ts
Normal file
26
front/src/composables/env.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* 是否在开发环境
|
||||||
|
* @description 在 `if (IN_DEV)` 块中的代码在 `vite build` 时会自动编译掉,不会带到生产环境
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* if (IN_DEV) {
|
||||||
|
* console.log("开发环境")
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const IN_DEV = import.meta.env.DEV
|
||||||
|
/**
|
||||||
|
* 是否在生产环境
|
||||||
|
* @example
|
||||||
|
* if (IN_PROD) {
|
||||||
|
* console.log("生产环境")
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const IN_PROD = import.meta.env.PROD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不带尾部 / 的 BASE_URL
|
||||||
|
*/
|
||||||
|
export const BASE_URL_WITHOUT_TAIL = import.meta.env.BASE_URL.endsWith('/')
|
||||||
|
? import.meta.env.BASE_URL.slice(0, -1)
|
||||||
|
: import.meta.env.BASE_URL
|
7
front/src/composables/path.ts
Normal file
7
front/src/composables/path.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* base 安全的路径解析
|
||||||
|
* @param path 路径
|
||||||
|
*/
|
||||||
|
export function safeResolve(path: string) {
|
||||||
|
return BASE_URL_WITHOUT_TAIL + path
|
||||||
|
}
|
3
front/src/composables/toast.ts
Normal file
3
front/src/composables/toast.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import 'vue-toastification/dist/index.css'
|
||||||
|
import { createToastInterface } from 'vue-toastification'
|
||||||
|
export default createToastInterface()
|
12
front/src/composables/useLanguage.ts
Normal file
12
front/src/composables/useLanguage.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export default () => {
|
||||||
|
const { t, locale } = useI18n()
|
||||||
|
const toggleLocale = () => {
|
||||||
|
locale.value = locale.value === 'zh-CN' ? 'en' : 'zh-CN'
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = computed(() =>
|
||||||
|
locale.value === 'zh-CN' ? '中文' : 'English',
|
||||||
|
)
|
||||||
|
|
||||||
|
return { t, language, toggleLocale }
|
||||||
|
}
|
14
front/src/composables/useTyped.ts
Normal file
14
front/src/composables/useTyped.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { init } from 'ityped'
|
||||||
|
|
||||||
|
export default (strings: string[]) => {
|
||||||
|
const typedRef = ref<Element>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init(typedRef.value!, {
|
||||||
|
strings,
|
||||||
|
showCursor: false,
|
||||||
|
disableBackTyping: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return typedRef
|
||||||
|
}
|
25
front/src/composables/useVisits.ts
Normal file
25
front/src/composables/useVisits.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useRequest } from 'vue-request'
|
||||||
|
|
||||||
|
export function useVisits() {
|
||||||
|
// 开发环境下
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const visits = useStorage('visits-kv', 0)
|
||||||
|
if (typeof visits.value === 'number') {
|
||||||
|
visits.value++
|
||||||
|
}
|
||||||
|
return visits
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: visits } = useRequest(async function () {
|
||||||
|
try {
|
||||||
|
const n = await http.get('https://visits-kv.deno.dev/tov-template', {
|
||||||
|
baseURL: '',
|
||||||
|
})
|
||||||
|
return Number(n) ?? 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return visits ?? 0
|
||||||
|
}
|
112
front/src/layouts/default.vue
Normal file
112
front/src/layouts/default.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<heads></heads>
|
||||||
|
</el-header>
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px" style="background-color: #0c2461">
|
||||||
|
<el-menu
|
||||||
|
style="background-color: #0c2461"
|
||||||
|
:default-active="navStore().adminPath"
|
||||||
|
router
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
|
style="color:#efecec;"
|
||||||
|
v-for="r in state.getList"
|
||||||
|
:key="r.name"
|
||||||
|
:index="r.path"
|
||||||
|
>
|
||||||
|
<component class="icons" :is="r.icon"/>
|
||||||
|
<template #title>{{ r.name }}</template>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
<el-main class="main">
|
||||||
|
<router-view></router-view>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {getAdminList} from '~/utils/utils'
|
||||||
|
import navStore from '~/stores/navStore'
|
||||||
|
import {reactive} from 'vue'
|
||||||
|
|
||||||
|
// 明确userInfo类型,避免类型错误
|
||||||
|
interface UserInfo {
|
||||||
|
role?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
|
navStore().adminPath = key
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = reactive(<any>{
|
||||||
|
getList: []
|
||||||
|
})
|
||||||
|
|
||||||
|
function userStore() {
|
||||||
|
// 假设全局有userStore,返回类型加上userInfo类型声明
|
||||||
|
return {
|
||||||
|
isLogin: false,
|
||||||
|
userInfo: {} as UserInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
state.getList = getAdminList()
|
||||||
|
|
||||||
|
const user = userStore()
|
||||||
|
if (user.userInfo.role == 0) {
|
||||||
|
|
||||||
|
state.getList.push({
|
||||||
|
"path": "/user",
|
||||||
|
"name": "用户管理",
|
||||||
|
"icon": "UserFilled",
|
||||||
|
},)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
background-color: #f6f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增el-header样式 */
|
||||||
|
:deep(.el-header) {
|
||||||
|
background: linear-gradient(90deg, #2563eb 60%, #1e3a8a 100%) !important;
|
||||||
|
color: #fff;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧菜单栏渐变背景和字体色 */
|
||||||
|
:deep(.el-aside) {
|
||||||
|
background: linear-gradient(180deg, #4e628d 60%, #1e3a8a 100%) !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
:deep(.el-menu) {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
:deep(.el-menu-item) {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background: rgba(37,99,235,0.18) !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
</style>
|
3
front/src/layouts/notFound.vue
Normal file
3
front/src/layouts/notFound.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
17
front/src/main.ts
Normal file
17
front/src/main.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// https://unocss.dev/ 原子 css 库
|
||||||
|
import '@unocss/reset/tailwind-compat.css' // unocss reset
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
import 'virtual:unocss-devtools'
|
||||||
|
// 你自定义的 css
|
||||||
|
import './styles/main.css'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
const app = createApp(App)
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
|
app.mount('#app')
|
58
front/src/pages/[...notFound].vue
Normal file
58
front/src/pages/[...notFound].vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const typedRef = useTyped([' is not found!'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="h-screen flex flex-wrap items-center justify-around text-center">
|
||||||
|
<div class="desc font-blod">
|
||||||
|
<div class="code text-7xl">404</div>
|
||||||
|
<div ref="typedRef" class="content mb-5 text-3xl">The Page</div>
|
||||||
|
<RouterLink :to="safeResolve('/')">
|
||||||
|
<button
|
||||||
|
class="rounded bg-light-800 px-5 py-2 text-lg transition"
|
||||||
|
hover="shadow-md"
|
||||||
|
dark="text-black"
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</button>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
:src="safeResolve('/notFound/32.svg')"
|
||||||
|
class="cover"
|
||||||
|
alt="page not found"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.code {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
height: auto;
|
||||||
|
width: 700px;
|
||||||
|
margin: 0 5px;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
flex: 1;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"title": "404",
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
101
front/src/pages/city.vue
Normal file
101
front/src/pages/city.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="stackedChart" class="chart-container"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
const stackedChart = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
adminRequest.post("/api/mysql", { sql: "SELECT * FROM dim_total_count ORDER BY year, loan_status" }).then(res => {
|
||||||
|
// 处理数据
|
||||||
|
const years = [...new Set(res.map(item => item.year))].sort()
|
||||||
|
const statusTypes = [...new Set(res.map(item => item.loan_status))]
|
||||||
|
|
||||||
|
const seriesData = statusTypes.map(status => {
|
||||||
|
return {
|
||||||
|
name: status,
|
||||||
|
type: 'bar',
|
||||||
|
stack: 'total',
|
||||||
|
emphasis: { focus: 'series' },
|
||||||
|
data: years.map(year => {
|
||||||
|
const item = res.find(d => d.year === year && d.loan_status === status)
|
||||||
|
return item ? item.status_count : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chartInstance = echarts.init(stackedChart.value)
|
||||||
|
chartInstance.setOption({
|
||||||
|
title: {
|
||||||
|
text: '年度贷款状态分布',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, fontWeight: 'bold', color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: function(params) {
|
||||||
|
let result = `<div style="margin-bottom:5px;font-weight:bold">${params[0].axisValue}年</div>`
|
||||||
|
params.forEach(param => {
|
||||||
|
const total = res.find(d => d.year === param.axisValue)?.total_count || 1
|
||||||
|
const percent = ((param.value / total) * 100).toFixed(1)
|
||||||
|
result += `
|
||||||
|
<div style="display:flex;justify-content:space-between;margin:3px 0">
|
||||||
|
<span>${param.seriesName}:</span>
|
||||||
|
<span style="margin-left:20px;font-weight:bold">${param.value} (${percent}%)</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: statusTypes,
|
||||||
|
bottom: 10
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
top: '15%',
|
||||||
|
bottom: '15%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: years.map(y => `${y}年`),
|
||||||
|
axisLabel: { rotate: 30 }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '贷款数量',
|
||||||
|
axisLine: { show: true },
|
||||||
|
axisLabel: { formatter: '{value}' }
|
||||||
|
},
|
||||||
|
series: seriesData,
|
||||||
|
color: ['#5470C6', '#91CC75', '#FAC858', '#EE6666', '#73C0DE']
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => chartInstance.resize())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', () => chartInstance.resize())
|
||||||
|
chartInstance?.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
164
front/src/pages/data.vue
Normal file
164
front/src/pages/data.vue
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form :inline="true" :model="state.page">
|
||||||
|
<el-form-item label="企业名称">
|
||||||
|
<el-input
|
||||||
|
style="width: 150px"
|
||||||
|
placeholder="请输入企业名称"
|
||||||
|
v-model="state.page.companyName"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery"><i class="el-icon-search"></i> 查询</el-button>
|
||||||
|
<el-button @click="handleReset"><i class="el-icon-refresh"></i> 重置</el-button>
|
||||||
|
<!-- <el-button type="success" @click="handleAdd"><i class="el-icon-plus"></i> 新增</el-button>-->
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-row gutter="20">
|
||||||
|
<el-table :data="state.getData" stripe style="width: 100%">
|
||||||
|
<!-- 基础信息 -->
|
||||||
|
<el-table-column
|
||||||
|
prop="id"
|
||||||
|
label="企业ID"
|
||||||
|
width="80"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
sortable
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="companyName"
|
||||||
|
label="企业名称"
|
||||||
|
width="260"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="industry"
|
||||||
|
label="行业"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="registeredCapital"
|
||||||
|
label="注册资本"
|
||||||
|
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="establishmentDate"
|
||||||
|
label="成立日期"
|
||||||
|
width="120"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table-column
|
||||||
|
prop="registrationAddres"
|
||||||
|
label="注册地址"
|
||||||
|
width="300"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column
|
||||||
|
fixed="right"
|
||||||
|
label="操作"
|
||||||
|
align="center"
|
||||||
|
header-align="center"
|
||||||
|
min-width="220">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="danger" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页组件 -->
|
||||||
|
<el-pagination background layout="prev, pager, next" :total="state.page.total" @current-change="handlePageChange"/>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {reactive, onMounted} from 'vue'
|
||||||
|
import {ElMessage, ElMessageBox} from 'element-plus' // 引入需要的组件
|
||||||
|
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const state = reactive({
|
||||||
|
page: {
|
||||||
|
companyName:""
|
||||||
|
},
|
||||||
|
getData: []
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
function init() {
|
||||||
|
adminRequest.get(`/items`, {params: state.page}).then((res) => {
|
||||||
|
state.getData = res.data.list
|
||||||
|
state.page = res.data.page
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载后加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
init(state.page.current)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
state.page.job_title = ''
|
||||||
|
state.page.city = ''
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
function handleQuery() {
|
||||||
|
state.page.page = 1
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除确认逻辑
|
||||||
|
function handleDelete(id) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确认删除该企业?', // 提示信息
|
||||||
|
'删除企业', // 弹框标题
|
||||||
|
{
|
||||||
|
type: 'warning', // 设置为警告类型
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
// 用户点击确定后执行删除操作
|
||||||
|
adminRequest.delete(`/items/${id}`).then(() => {
|
||||||
|
init() // 删除后重新加载数据
|
||||||
|
ElMessage.success('删除成功') // 提示删除成功
|
||||||
|
})
|
||||||
|
}).catch(() => {
|
||||||
|
// 用户点击取消后的逻辑
|
||||||
|
ElMessage.info('取消删除')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页改变时加载数据
|
||||||
|
function handlePageChange(page) {
|
||||||
|
state.page.current = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
/* 表头样式 */
|
||||||
|
.el-table ::v-deep .el-table__header th {
|
||||||
|
background-color: #f5f7fa; /* 表头背景色 */
|
||||||
|
font-weight: bold; /* 表头字体加粗 */
|
||||||
|
color: #303133; /* 表头字体颜色 */
|
||||||
|
}
|
||||||
|
</style>
|
273
front/src/pages/edg.vue
Normal file
273
front/src/pages/edg.vue
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<el-row :gutter="24" class="analysis-container">
|
||||||
|
<el-col :span="24" class="chart-col">
|
||||||
|
<div ref="repaymentChart" class="chart-card chart-line"></div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { onMounted, ref, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
const repaymentChart = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
chartInstance?.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
|
||||||
|
adminRequest.post("/api/mysql", { sql: "SELECT * FROM dim_repayment_status ORDER BY repayment_date" }).then(res => {
|
||||||
|
if (!res || res.length === 0) return
|
||||||
|
// 处理日期格式
|
||||||
|
const dateList = res.map(item => {
|
||||||
|
const date = new Date(item.repayment_date)
|
||||||
|
return `${date.getMonth()+1}/${date.getDate()}`
|
||||||
|
})
|
||||||
|
|
||||||
|
chartInstance = echarts.init(repaymentChart.value)
|
||||||
|
chartInstance.setOption({
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
title: {
|
||||||
|
text: '企业还款状态分析',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#3a5169'
|
||||||
|
},
|
||||||
|
subtext: '按时还款与逾期还款金额对比',
|
||||||
|
subtextStyle: {
|
||||||
|
color: '#7f8c9a',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
},
|
||||||
|
formatter: params => {
|
||||||
|
let html = `<div style="font-weight:bold;margin-bottom:5px">${params[0].axisValue}</div>`
|
||||||
|
params.forEach(item => {
|
||||||
|
html += `<div>${item.seriesName}: <span style="color:${item.color};font-weight:bold">${item.value}元</span></div>`
|
||||||
|
})
|
||||||
|
return html
|
||||||
|
},
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
||||||
|
borderColor: '#e4e7ed',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: {
|
||||||
|
color: '#3a5169'
|
||||||
|
},
|
||||||
|
extraCssText: 'box-shadow: 0 4px 20px rgba(0,0,0,0.1); border-radius: 8px; padding: 10px;'
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['按时还款', '逾期还款', '总还款'],
|
||||||
|
right: 20,
|
||||||
|
top: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: '#7f8c9a'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
top: '20%',
|
||||||
|
bottom: '15%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: dateList,
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#e4e7ed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#7f8c9a',
|
||||||
|
fontSize: 12
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(元)',
|
||||||
|
nameTextStyle: {
|
||||||
|
color: '#7f8c9a',
|
||||||
|
padding: [0, 0, 0, 40]
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#7f8c9a',
|
||||||
|
formatter: value => `${value}`
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#f0f2f5',
|
||||||
|
type: 'dashed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '按时还款',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: '30%',
|
||||||
|
data: res.map(item => item.on_time_amount),
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#83bff6' },
|
||||||
|
{ offset: 1, color: '#188df0' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#2378f7' },
|
||||||
|
{ offset: 1, color: '#09307a' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '逾期还款',
|
||||||
|
type: 'bar',
|
||||||
|
barWidth: '30%',
|
||||||
|
data: res.map(item => item.overdue_amount),
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff9f7f' },
|
||||||
|
{ offset: 1, color: '#ff6e76' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff6e76' },
|
||||||
|
{ offset: 1, color: '#c12e34' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '总还款',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
data: res.map(item => item.total_amount),
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: {
|
||||||
|
width: 3,
|
||||||
|
color: '#37a2ff'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#37a2ff',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
formatter: '{c}元'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dataZoom: [{
|
||||||
|
type: 'slider',
|
||||||
|
show: true,
|
||||||
|
xAxisIndex: [0],
|
||||||
|
height: 20,
|
||||||
|
bottom: 10,
|
||||||
|
start: 0,
|
||||||
|
end: 100,
|
||||||
|
handleIcon: 'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z',
|
||||||
|
handleSize: '80%',
|
||||||
|
handleStyle: {
|
||||||
|
color: '#fff',
|
||||||
|
shadowBlur: 3,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.6)',
|
||||||
|
shadowOffsetX: 2,
|
||||||
|
shadowOffsetY: 2
|
||||||
|
},
|
||||||
|
backgroundColor: '#f0f2f5',
|
||||||
|
fillerColor: 'rgba(55, 162, 255, 0.2)',
|
||||||
|
borderColor: '#e4e7ed'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('resize', resizeChart)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', resizeChart)
|
||||||
|
chartInstance?.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.analysis-container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 500px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-line::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #ff7b7b 0%, #ffb8b8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.chart-col {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
min-height: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-card {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
276
front/src/pages/index.vue
Normal file
276
front/src/pages/index.vue
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<!-- 顶部可视化区块 -->
|
||||||
|
<el-row :gutter="20" class="mb-20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<div ref="loanAmountChart" class="chart-card"></div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<!-- 数据表格区块 -->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="custom-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">🏅 企业信用等级分布</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="creditLevelChart" style="width: 100%;height: 350px"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-card class="custom-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">💹 行业还款率分布</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div ref="repaymentRateChart" style="width: 100%;height: 350px"></div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {ref, onMounted} from 'vue'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
const loanAmountChart = ref(null)
|
||||||
|
const creditLevelChart = ref(null)
|
||||||
|
const repaymentRateChart = ref(null)
|
||||||
|
|
||||||
|
function initCharts() {
|
||||||
|
// 贷款金额变化趋势
|
||||||
|
adminRequest.post("/api/mysql", {sql: "SELECT * FROM dim_loan_amount"}).then(res => {
|
||||||
|
const chart = echarts.init(loanAmountChart.value)
|
||||||
|
chart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '贷款金额变化趋势',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1e3a8a'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: 'rgba(30,58,138,0.8)',
|
||||||
|
borderColor: '#2563eb',
|
||||||
|
textStyle: { color: '#fff' },
|
||||||
|
axisPointer: {
|
||||||
|
type: 'shadow'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: res.map(e => e.date),
|
||||||
|
axisLabel: {
|
||||||
|
|
||||||
|
rotate: 35,
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#1e3a8a',
|
||||||
|
|
||||||
|
},
|
||||||
|
axisLine: { lineStyle: { color: '#2563eb' } }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
axisLabel: { color: '#1e3a8a' },
|
||||||
|
splitLine: { lineStyle: { type: 'dashed', color: '#c7d2fe' } }
|
||||||
|
},
|
||||||
|
dataZoom: [{
|
||||||
|
type: 'slider',
|
||||||
|
show: true,
|
||||||
|
xAxisIndex: [0],
|
||||||
|
start: 0,
|
||||||
|
end: 30,
|
||||||
|
height: 20,
|
||||||
|
bottom: 0
|
||||||
|
}],
|
||||||
|
series: [{
|
||||||
|
name: '贷款金额',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
data: res.map(e => e.amount),
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: { color: '#2563eb', width: 3 },
|
||||||
|
itemStyle: { color: '#2563eb', borderColor: '#fff', borderWidth: 2 },
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{offset: 0, color: 'rgba(37,99,235,0.3)'},
|
||||||
|
{offset: 1, color: 'rgba(37,99,235,0.05)'}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
})
|
||||||
|
// 2.企业信用等级分布
|
||||||
|
adminRequest.post("/api/mysql", {sql: "SELECT * FROM dim_bank_grade"}).then(res => {
|
||||||
|
// 排序,最大项高亮
|
||||||
|
const sorted = [...res].sort((a, b) => b.value - a.value)
|
||||||
|
const mainColor = '#2563eb'
|
||||||
|
const colors = [mainColor, '#1e40af', '#60a5fa', '#a5b4fc', '#818cf8', '#fbbf24', '#f87171']
|
||||||
|
const chart = echarts.init(creditLevelChart.value)
|
||||||
|
chart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '企业信用等级分布',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, fontWeight: 'bold', color: '#1e3a8a' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: 'rgba(30,58,138,0.9)',
|
||||||
|
textStyle: { color: '#fff' },
|
||||||
|
formatter: p => `${p.name}:${p.value}家(${p.percent}%)`
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'left',
|
||||||
|
top: 'middle',
|
||||||
|
textStyle: { fontSize: 12, color: '#1e3a8a' }
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '信用等级',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
center: ['55%', '50%'],
|
||||||
|
roseType: 'radius',
|
||||||
|
data: sorted.map((item, i) => ({
|
||||||
|
...item,
|
||||||
|
itemStyle: {
|
||||||
|
color: i === 0 ? mainColor : colors[i % colors.length],
|
||||||
|
borderRadius: 8,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2,
|
||||||
|
shadowBlur: i === 0 ? 20 : 10,
|
||||||
|
shadowColor: i === 0 ? 'rgba(37,99,235,0.3)' : 'rgba(30,58,138,0.1)'
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: p => `${p.name} ${p.value}家(${p.percent}%)`,
|
||||||
|
color: '#1e3a8a',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
scale: true,
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 30,
|
||||||
|
shadowColor: 'rgba(37,99,235,0.4)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// 3.还款状态分析
|
||||||
|
adminRequest.post("/api/mysql", {sql: "SELECT * FROM dim_loan_status"}).then(res => {
|
||||||
|
const sorted = [...res].sort((a, b) => b.value - a.value)
|
||||||
|
const mainColor = '#10b981'
|
||||||
|
const colors = [mainColor, '#3b82f6', '#f59e42', '#f87171', '#818cf8', '#a5b4fc']
|
||||||
|
const chart = echarts.init(repaymentRateChart.value)
|
||||||
|
chart.setOption({
|
||||||
|
title: {
|
||||||
|
text: '还款状态分析',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, fontWeight: 'bold', color: '#1e3a8a' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: 'rgba(30,58,138,0.9)',
|
||||||
|
textStyle: { color: '#fff' },
|
||||||
|
formatter: p => `${p.name}:${p.value}笔(${p.percent}%)`
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 10,
|
||||||
|
top: 'center',
|
||||||
|
textStyle: { fontSize: 12, color: '#1e3a8a' }
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '还款状态',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
center: ['60%', '50%'],
|
||||||
|
roseType: 'radius',
|
||||||
|
data: sorted.map((item, i) => ({
|
||||||
|
...item,
|
||||||
|
itemStyle: {
|
||||||
|
color: i === 0 ? mainColor : colors[i % colors.length],
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2,
|
||||||
|
shadowBlur: i === 0 ? 20 : 10,
|
||||||
|
shadowColor: i === 0 ? 'rgba(16,185,129,0.3)' : 'rgba(30,58,138,0.1)'
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: p => `${p.name} ${p.value}笔(${p.percent}%)`,
|
||||||
|
color: '#1e3a8a',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: 14
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
scale: true,
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 30,
|
||||||
|
shadowColor: 'rgba(16,185,129,0.4)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initCharts()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 300px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
border: none;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e3a8a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-20 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
189
front/src/pages/login.vue
Normal file
189
front/src/pages/login.vue
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bank-login-container">
|
||||||
|
<div class="bank-login-card">
|
||||||
|
<div class="bank-login-header">
|
||||||
|
<h2 class="bank-title">智能银行企业信用系统</h2>
|
||||||
|
<!-- <p class="bank-subtitle">基于大数据与AI的企业信用智能评估平台</p>-->
|
||||||
|
</div>
|
||||||
|
<div class="bank-form">
|
||||||
|
<div class="bank-form-item">
|
||||||
|
<label class="bank-label">企业账号</label>
|
||||||
|
<input
|
||||||
|
class="bank-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入企业名称或注册邮箱"
|
||||||
|
v-model="login.username"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="bank-form-item">
|
||||||
|
<label class="bank-label">密码</label>
|
||||||
|
<input
|
||||||
|
class="bank-input"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
v-model="login.password"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button class="bank-login-btn" @click="onLogin">登 录</button>
|
||||||
|
<div class="bank-footer">
|
||||||
|
<!-- <span @click="router.push('/register')">企业注册</span>-->
|
||||||
|
<!-- <span @click="router.push('/forget')">忘记密码</span>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { adminRequest } from '~/composables/adminRequest'
|
||||||
|
const router = useRouter()
|
||||||
|
const login = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
const onLogin = () => {
|
||||||
|
adminRequest.post("/api/login", login).then(res => {
|
||||||
|
const user = userStore()
|
||||||
|
user.userInfo = res.data
|
||||||
|
user.isLogin = true
|
||||||
|
ElMessage.success("登录成功")
|
||||||
|
router.push('/')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bank-login-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #2563eb, #1e3a8a);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-card {
|
||||||
|
width: 420px;
|
||||||
|
background: #FFF;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 30px rgba(30,58,138,0.15);
|
||||||
|
padding: 40px 32px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-subtitle {
|
||||||
|
color: #555;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-form-item {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-label {
|
||||||
|
display: block;
|
||||||
|
color: #222;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid #c7d2fe;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-input:focus {
|
||||||
|
border-color: #2563eb;
|
||||||
|
box-shadow: 0 0 0 2px rgba(37,99,235,0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 46px;
|
||||||
|
background: linear-gradient(90deg, #2563eb 60%, #1e40af 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 24px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-btn:hover {
|
||||||
|
background: #1e40af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #2563eb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-footer span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-banner {
|
||||||
|
position: absolute;
|
||||||
|
right: 10%;
|
||||||
|
color: white;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-banner h3 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-login-banner p {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bank-banner-features {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.bank-banner-features p {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
314
front/src/pages/register.vue
Normal file
314
front/src/pages/register.vue
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<template>
|
||||||
|
<div class="music-register-container">
|
||||||
|
<div class="music-register-card">
|
||||||
|
<div class="music-register-header">
|
||||||
|
<h2 class="music-title">网易云音乐用户行为分析系统</h2>
|
||||||
|
<p class="music-subtitle">基于大数据技术的音乐行为研究</p>
|
||||||
|
</div>
|
||||||
|
<div class="music-form">
|
||||||
|
<div class="music-form-item">
|
||||||
|
<label class="music-label">用户名</label>
|
||||||
|
<input
|
||||||
|
class="music-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
v-model="register.username"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="music-form-item">
|
||||||
|
<label class="music-label">密码</label>
|
||||||
|
<input
|
||||||
|
class="music-input"
|
||||||
|
type="password"
|
||||||
|
placeholder="至少8位字符,包含大小写字母和数字"
|
||||||
|
v-model="register.password"
|
||||||
|
@keyup.enter="onRegister"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="music-form-item">
|
||||||
|
<label class="music-label">确认密码</label>
|
||||||
|
<input
|
||||||
|
class="music-input"
|
||||||
|
type="password"
|
||||||
|
placeholder="请再次输入密码"
|
||||||
|
v-model="register.confirmPassword"
|
||||||
|
@keyup.enter="onRegister"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="music-register-btn" @click="onRegister">
|
||||||
|
<span v-if="!loading">注 册</span>
|
||||||
|
<el-icon v-else class="is-loading"><Loading /></el-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="music-agreement">
|
||||||
|
<el-checkbox v-model="register.agreed">
|
||||||
|
我已阅读并同意<a href="#" @click.prevent>《用户协议》</a>和<a href="#" @click.prevent>《隐私政策》</a>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="music-footer">
|
||||||
|
<span @click="router.push('/login')">已有账号?立即登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="music-register-banner">
|
||||||
|
<h3>音乐用户行为分析平台</h3>
|
||||||
|
<p>千万级用户听歌数据分析 · 行为模式挖掘 · 个性化推荐研究</p>
|
||||||
|
<ul class="music-features">
|
||||||
|
<li><i class="el-icon-headset"></i> 分析超过1亿条用户听歌记录</li>
|
||||||
|
<li><i class="el-icon-data-line"></i> 挖掘用户音乐偏好与行为模式</li>
|
||||||
|
<li><i class="el-icon-magic-stick"></i> 研究个性化推荐算法效果</li>
|
||||||
|
<li><i class="el-icon-trend-charts"></i> 实时监测音乐市场趋势</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
import { adminRequest } from '~/composables/adminRequest'
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const register = reactive({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
agreed: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
if (!register.username) {
|
||||||
|
ElMessage.error("请输入用户名")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!register.password) {
|
||||||
|
ElMessage.error("请输入密码")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (register.password.length < 5) {
|
||||||
|
ElMessage.error("密码长度不能少于8位")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (register.password !== register.confirmPassword) {
|
||||||
|
ElMessage.error("两次输入的密码不一致")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!register.agreed) {
|
||||||
|
ElMessage.error("请先同意用户协议和隐私政策")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRegister = async () => {
|
||||||
|
if (!validateForm()) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await adminRequest.post("/api/register", register)
|
||||||
|
ElMessage.success("注册成功")
|
||||||
|
router.push("/login")
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(error.response?.data?.message || "注册失败")
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.music-register-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #d83c3c, #b62b2b);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-card {
|
||||||
|
width: 450px;
|
||||||
|
background: #FFF;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
padding: 40px;
|
||||||
|
z-index: 1;
|
||||||
|
animation: fadeIn 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-title {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #d83c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-subtitle {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-label {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-input:focus {
|
||||||
|
border-color: #d83c3c;
|
||||||
|
box-shadow: 0 0 0 2px rgba(216,60,60,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
background: #d83c3c;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-btn:hover {
|
||||||
|
background: #b62b2b;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(216,60,60,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-agreement {
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-agreement a {
|
||||||
|
color: #d83c3c;
|
||||||
|
margin: 0 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-agreement a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #d83c3c;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-footer span:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-banner {
|
||||||
|
position: absolute;
|
||||||
|
right: 10%;
|
||||||
|
color: white;
|
||||||
|
max-width: 400px;
|
||||||
|
animation: slideIn 0.8s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from { opacity: 0; transform: translateX(50px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-banner h3 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-register-banner p {
|
||||||
|
font-size: 16px;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-features {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-features li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 25px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.music-features li i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #ffcc00;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.music-register-banner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.music-register-card {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
137
front/src/pages/sal.vue
Normal file
137
front/src/pages/sal.vue
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<template>
|
||||||
|
<el-row :gutter="24" class="analysis-container">
|
||||||
|
<el-col :span="24" class="chart-col">
|
||||||
|
<div ref="salaryChart" class="chart-card chart-line"></div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import { onMounted, ref, onBeforeUnmount } from 'vue'
|
||||||
|
|
||||||
|
const salaryChart = ref(null)
|
||||||
|
let chartInstance = null
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
chartInstance?.resize()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
adminRequest.post("/api/mysql", { sql: "select * from dim_jie_count" }).then(res => {
|
||||||
|
|
||||||
|
chartInstance = echarts.init(salaryChart.value)
|
||||||
|
chartInstance.setOption({
|
||||||
|
title: {
|
||||||
|
text: '每年借款数量',
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, fontWeight: 'bold', color: '#1e3a8a' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: 'rgba(30,58,138,0.9)',
|
||||||
|
textStyle: { color: '#fff' },
|
||||||
|
formatter: '{b}<br/>还款:{c}' },
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 10,
|
||||||
|
top: 'center',
|
||||||
|
textStyle: { fontSize: 12, color: '#1e3a8a' }
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '行业还款率',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
center: ['60%', '50%'],
|
||||||
|
roseType: 'radius',
|
||||||
|
data: res,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2,
|
||||||
|
shadowBlur: 15,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(30,58,138,0.1)'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: '{b|{b}}\n{c|{c}%}',
|
||||||
|
rich: {
|
||||||
|
b: {fontSize: 14, color: '#1e3a8a', fontWeight: 'bold'},
|
||||||
|
c: {fontSize: 12, color: '#2563eb'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
labelLine: { length: 15, length2: 20, lineStyle: { color: '#2563eb', width: 1 } },
|
||||||
|
emphasis: {
|
||||||
|
scale: true,
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 30,
|
||||||
|
shadowColor: 'rgba(30,58,138,0.2)'
|
||||||
|
},
|
||||||
|
label: { fontSize: 16, fontWeight: 'bold' }
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
window.addEventListener('resize', resizeChart)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', resizeChart)
|
||||||
|
chartInstance?.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.analysis-container {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 500px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 20px;
|
||||||
|
transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-line::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #ff7b7b 0%, #ffb8b8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.chart-col {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
min-height: 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.chart-card {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
224
front/src/pages/user.vue
Normal file
224
front/src/pages/user.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<el-form :inline="true" :model="state.queryParams">
|
||||||
|
<el-form-item label="用户账号">
|
||||||
|
<el-input v-model="state.queryParams.username" placeholder="请输入账号" clearable @clear="handleReset" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery"><i class="el-icon-search"></i> 查询</el-button>
|
||||||
|
<el-button @click="handleReset"><i class="el-icon-refresh"></i> 重置</el-button>
|
||||||
|
<el-button type="success" @click="handleAdd"><i class="el-icon-plus"></i> 新增</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 用户表格 -->
|
||||||
|
<el-row style="margin-top: 20px">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-table :data="state.getList" style="width: 100%" border>
|
||||||
|
<el-table-column prop="id" label="用户编号" align="center" />
|
||||||
|
|
||||||
|
<el-table-column prop="username" label="用户账号" align="center" />
|
||||||
|
<el-table-column prop="role" label="角色" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.role === 0 ? 'danger' : 'success'">
|
||||||
|
{{ scope.row.role === 0 ? '管理员' : '普通用户' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button limit="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||||
|
<el-button limit="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<el-row style="margin-top: 20px">
|
||||||
|
<el-col :span="24" style="text-align: right">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="prev, pager, next, limits, total"
|
||||||
|
:total="state.page.total"
|
||||||
|
:page-limits="[10, 20, 50]"
|
||||||
|
:page-limit="state.page.limit"
|
||||||
|
@page-change="handlePageChange"
|
||||||
|
@limit-change="handlelimitChange"
|
||||||
|
/>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog :title="state.dialog.title" v-model="state.dialog.visible" width="500px">
|
||||||
|
<el-form :model="state.form" :rules="rules" ref="formRef" label-width="80px">
|
||||||
|
<el-form-item label="用户账号" prop="username">
|
||||||
|
<el-input v-model="state.form.username" placeholder="请输入账号" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password" v-if="state.dialog.isAdd">
|
||||||
|
<el-input v-model="state.form.password" placeholder="请输入密码" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色" prop="role">
|
||||||
|
<el-select v-model="state.form.role" placeholder="请选择角色">
|
||||||
|
<el-option label="管理员" :value="0" />
|
||||||
|
<el-option label="普通用户" :value="1" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="state.dialog.visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
interface User {
|
||||||
|
id?: number
|
||||||
|
username: string
|
||||||
|
phone: string
|
||||||
|
role: number
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const state = reactive({
|
||||||
|
getList: [] as User[],
|
||||||
|
page: {
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0
|
||||||
|
},
|
||||||
|
queryParams: {
|
||||||
|
username: ''
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
id: undefined,
|
||||||
|
username: '',
|
||||||
|
phone: '',
|
||||||
|
role: 1,
|
||||||
|
password: ''
|
||||||
|
} as User,
|
||||||
|
dialog: {
|
||||||
|
visible: false,
|
||||||
|
title: '',
|
||||||
|
isAdd: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = reactive<FormRules>({
|
||||||
|
username: [{ required: true, message: '请输入用户账号', trigger: 'blur' }],
|
||||||
|
|
||||||
|
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
function init() {
|
||||||
|
adminRequest.get("/api/users/page", {
|
||||||
|
params: {
|
||||||
|
page: state.page.page,
|
||||||
|
limit: state.page.limit,
|
||||||
|
username: state.queryParams.username
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
state.getList = res.data.list
|
||||||
|
state.page = res.data.page
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
function handleQuery() {
|
||||||
|
state.page.page = 1
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询
|
||||||
|
function handleReset() {
|
||||||
|
state.queryParams.username = ''
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
state.page.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每页条数变化
|
||||||
|
function handlelimitChange(limit: number) {
|
||||||
|
state.page.limit = limit
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
function handleAdd() {
|
||||||
|
state.dialog = {
|
||||||
|
visible: true,
|
||||||
|
title: '新增用户',
|
||||||
|
isAdd: true
|
||||||
|
}
|
||||||
|
state.form = {
|
||||||
|
username: '',
|
||||||
|
phone: '',
|
||||||
|
role: 1,
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
function handleEdit(row: User) {
|
||||||
|
state.dialog = {
|
||||||
|
visible: true,
|
||||||
|
title: '编辑用户',
|
||||||
|
isAdd: false
|
||||||
|
}
|
||||||
|
state.form = JSON.parse(JSON.stringify(row))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
function handleDelete(row: User) {
|
||||||
|
ElMessageBox.confirm(`确认删除用户【${row.username}】吗?`, '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
adminRequest.delete(`/api/users/${row.id}`).then(() => {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
function submitForm() {
|
||||||
|
formRef.value?.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
const url = state.dialog.isAdd ? '/api/users' : `/api/users/${state.form.id}`
|
||||||
|
const method = state.dialog.isAdd ? 'post' : 'put'
|
||||||
|
adminRequest[method](url, state.form).then(() => {
|
||||||
|
ElMessage.success(state.dialog.isAdd ? '新增成功' : '修改成功')
|
||||||
|
state.dialog.visible = false
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-form--inline .el-form-item {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
9
front/src/plugins/nprogress.ts
Normal file
9
front/src/plugins/nprogress.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { router } from './router'
|
||||||
|
import { useNProgress } from '@vueuse/integrations/useNProgress'
|
||||||
|
|
||||||
|
// https://vueuse.org/integrations/useNProgress/
|
||||||
|
const { start, done } = useNProgress()
|
||||||
|
|
||||||
|
router.beforeEach(() => start())
|
||||||
|
|
||||||
|
router.afterEach(() => done(true))
|
7
front/src/plugins/pinia.ts
Normal file
7
front/src/plugins/pinia.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import persistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
pinia.use(persistedstate)
|
||||||
|
|
||||||
|
export default pinia
|
38
front/src/plugins/router.ts
Normal file
38
front/src/plugins/router.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { setupLayouts } from 'virtual:meta-layouts'
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { routes as fileRoutes } from 'vue-router/auto-routes'
|
||||||
|
import { safeResolve } from '~/composables/path'
|
||||||
|
declare module 'vue-router' {}
|
||||||
|
// 重定向 BASE_URL
|
||||||
|
fileRoutes.flat(Infinity).forEach((route) => {
|
||||||
|
route.path = safeResolve(route.path)
|
||||||
|
})
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: setupLayouts(fileRoutes),
|
||||||
|
})
|
||||||
|
// 路由拦截
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
// 需要排除的路径
|
||||||
|
const noAuthPaths = ['/login', '/register'];
|
||||||
|
|
||||||
|
// 如果当前访问路径在排除列表中,则直接放行
|
||||||
|
if (noAuthPaths.includes(to.path)) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进行管理员认证
|
||||||
|
console.log("管理员认证~");
|
||||||
|
console.log(userStore().isLogin);
|
||||||
|
|
||||||
|
// 判断有没有登录
|
||||||
|
if (!userStore().isLogin) {
|
||||||
|
ElMessage.warning("认证失败~");
|
||||||
|
return next('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default router
|
17
front/src/plugins/title.ts
Normal file
17
front/src/plugins/title.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { router } from './router'
|
||||||
|
|
||||||
|
useTitle(
|
||||||
|
() => {
|
||||||
|
const { path, meta } = router.currentRoute.value
|
||||||
|
if (meta.title) {
|
||||||
|
return `· ${meta.title}`
|
||||||
|
}
|
||||||
|
if (path === '/') {
|
||||||
|
return '· home'
|
||||||
|
}
|
||||||
|
return path.replaceAll('/', ' · ')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titleTemplate: `${import.meta.env.VITE_APP_TITLE} %s`,
|
||||||
|
},
|
||||||
|
)
|
16
front/src/stores/navStore.ts
Normal file
16
front/src/stores/navStore.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('navStore', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
adminPath: "/",
|
||||||
|
frontPath: "/front/",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
inc() {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
})
|
16
front/src/stores/userStore.ts
Normal file
16
front/src/stores/userStore.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('userStore', {
|
||||||
|
state() {
|
||||||
|
return {
|
||||||
|
isLogin: false,
|
||||||
|
userInfo:{},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
inc() {
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: true,
|
||||||
|
})
|
53
front/src/styles/main.css
Normal file
53
front/src/styles/main.css
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
background: #100c2a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*进度条配色*/
|
||||||
|
#nprogress .bar {
|
||||||
|
@apply bg-blue-700 bg-opacity-75;
|
||||||
|
background: repeating-linear-gradient(90deg, #00dc82 0, #34cdfe 50%, #0047e1);
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 全局滚动条
|
||||||
|
*/
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 11px;
|
||||||
|
background-color: rgb(246, 247, 248);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgb(233, 236, 239);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: rgb(246, 247, 248);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar,
|
||||||
|
html.dark ::-webkit-scrollbar-track {
|
||||||
|
background-color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #343a40;
|
||||||
|
}
|
11147
front/src/utils/china.json
Normal file
11147
front/src/utils/china.json
Normal file
File diff suppressed because it is too large
Load Diff
30
front/src/utils/utils.ts
Normal file
30
front/src/utils/utils.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export const getAdminList = () => {
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
"path": "/",
|
||||||
|
"name": "首页",
|
||||||
|
"icon": "House",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/data",
|
||||||
|
"name": "企业数据",
|
||||||
|
"icon": "Coin",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/edg",
|
||||||
|
"name": "企业行为",
|
||||||
|
"icon": "SetUp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/sal",
|
||||||
|
"name": "借款行为",
|
||||||
|
"icon": "SetUp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/city",
|
||||||
|
"name": "还款行为",
|
||||||
|
"icon": "Postcard",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return routes;
|
||||||
|
}
|
32
front/tsconfig.json
Normal file
32
front/tsconfig.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "esnext",
|
||||||
|
"sourceMap": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsxImportSource": "vue",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"],
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"srcipts",
|
||||||
|
"presets",
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.d.ts",
|
||||||
|
"src/**/*.tsx",
|
||||||
|
"src/**/*.vue",
|
||||||
|
"./vite.config.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
24
front/uno.config.ts
Normal file
24
front/uno.config.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
defineConfig,
|
||||||
|
presetAttributify,
|
||||||
|
presetIcons,
|
||||||
|
presetTypography,
|
||||||
|
presetUno,
|
||||||
|
transformerVariantGroup,
|
||||||
|
transformerDirectives,
|
||||||
|
} from 'unocss'
|
||||||
|
|
||||||
|
import presetAutoprefixer from './presets/autoprefixer'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
transformers: [transformerDirectives(), transformerVariantGroup()],
|
||||||
|
presets: [
|
||||||
|
presetAttributify(),
|
||||||
|
presetIcons({
|
||||||
|
autoInstall: true,
|
||||||
|
}),
|
||||||
|
presetUno(),
|
||||||
|
presetTypography(),
|
||||||
|
presetAutoprefixer(),
|
||||||
|
],
|
||||||
|
})
|
6
front/vite.config.ts
Normal file
6
front/vite.config.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Tov from './presets'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [Tov()],
|
||||||
|
})
|
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user