上传
This commit is contained in:
parent
64e63f8c69
commit
41df9e4c2d
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
21
.idea/deployment.xml
generated
Normal file
21
.idea/deployment.xml
generated
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
|
||||||
|
<serverData>
|
||||||
|
<paths name="root@master:22 password">
|
||||||
|
<serverdata>
|
||||||
|
<mappings>
|
||||||
|
<mapping local="$PROJECT_DIR$" web="/" />
|
||||||
|
</mappings>
|
||||||
|
</serverdata>
|
||||||
|
</paths>
|
||||||
|
<paths name="root@master:22 password (2)">
|
||||||
|
<serverdata>
|
||||||
|
<mappings>
|
||||||
|
<mapping local="$PROJECT_DIR$" web="/" />
|
||||||
|
</mappings>
|
||||||
|
</serverdata>
|
||||||
|
</paths>
|
||||||
|
</serverData>
|
||||||
|
</component>
|
||||||
|
</project>
|
8
.idea/fraud-detection-ml.iml
generated
Normal file
8
.idea/fraud-detection-ml.iml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.8 (xiaocai_env)" jdkType="Python SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
7
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="TsLint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="Python 3.8 (xiaocai_env)" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (xiaocai_env)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/fraud-detection-ml.iml" filepath="$PROJECT_DIR$/.idea/fraud-detection-ml.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
27
App/__init__.py
Normal file
27
App/__init__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# __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='D:\\2025\\fraud-detection-ml\\uploads')
|
||||||
|
# 注册蓝图
|
||||||
|
app.register_blueprint(blueprint=blus)
|
||||||
|
# MySQL所在主机名,默认127.0.0.1
|
||||||
|
HOSTNAME = "192.168.15.2"
|
||||||
|
# MySQL监听的端口号,默认3306
|
||||||
|
PORT = 3306
|
||||||
|
# 连接MySQL的用户名,自己设置
|
||||||
|
USERNAME = "root"
|
||||||
|
# 连接MySQL的密码,自己设置
|
||||||
|
PASSWORD = "minxianrui"
|
||||||
|
# MySQL上创建的数据库名称
|
||||||
|
DATABASE = "fraud_detection_ml"
|
||||||
|
# 通过修改以下代码来操作不同的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
|
15
App/exts.py
Normal file
15
App/exts.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# 存放插件
|
||||||
|
# 扩展第三方插件
|
||||||
|
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)
|
81
App/models.py
Normal file
81
App/models.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# 模型
|
||||||
|
from sqlalchemy import func
|
||||||
|
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 Item(db.Model):
|
||||||
|
# 表名
|
||||||
|
__tablename__ = "tb_item"
|
||||||
|
# 论文编号
|
||||||
|
id = db.Column(db.Integer, primary_key=True,autoincrement=True)
|
||||||
|
# 论文标题
|
||||||
|
name = db.Column(db.String(80), autoincrement=True)
|
||||||
|
# 路径
|
||||||
|
path = db.Column(db.String(255), unique=True, nullable=False)
|
||||||
|
# 是否分析
|
||||||
|
is_view = db.Column(db.Integer)
|
||||||
|
# 阅读人数
|
||||||
|
view = db.Column(db.Integer)
|
||||||
|
# 上传用户
|
||||||
|
user_id = db.Column(db.Integer, nullable=False)
|
||||||
|
# 创建时间
|
||||||
|
create_time = db.Column(db.String(100), nullable=False, default=func.now())
|
||||||
|
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)) # 描述
|
||||||
|
|
||||||
|
|
||||||
|
# 定义金融交易模型
|
||||||
|
class FinancialTransaction(db.Model):
|
||||||
|
__tablename__ = 'tb_financial_transactions'
|
||||||
|
transaction_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
|
user_name = db.Column(db.String(50), nullable=False)
|
||||||
|
transaction_amount = db.Column(db.Float, nullable=False)
|
||||||
|
transaction_time = db.Column(db.DateTime, nullable=False)
|
||||||
|
transaction_location = db.Column(db.String(255), nullable=True)
|
||||||
|
transaction_status = db.Column(db.String(255), nullable=True)
|
||||||
|
device_info = db.Column(db.String(255), nullable=True)
|
||||||
|
ip_address = db.Column(db.String(45), nullable=True)
|
||||||
|
browser_info = db.Column(db.String(255), nullable=True)
|
||||||
|
mobile = db.Column(db.Integer, nullable=True)
|
||||||
|
is_fraud = db.Column(db.Boolean, nullable=False)
|
||||||
|
def __init__(self, user_name, transaction_amount, transaction_time, transaction_location, device_info, ip_address,transaction_status,mobile,
|
||||||
|
browser_info, is_fraud):
|
||||||
|
self.user_name = user_name
|
||||||
|
self.transaction_amount = transaction_amount
|
||||||
|
self.transaction_time = transaction_time
|
||||||
|
self.transaction_location = transaction_location
|
||||||
|
self.device_info = device_info
|
||||||
|
self.ip_address = ip_address
|
||||||
|
self.browser_info = browser_info
|
||||||
|
self.is_fraud = is_fraud
|
||||||
|
self.transaction_status = transaction_status
|
||||||
|
self.mobile = mobile
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
'transaction_id': self.transaction_id,
|
||||||
|
'user_name': self.user_name,
|
||||||
|
'transaction_amount': self.transaction_amount,
|
||||||
|
'transaction_time': self.transaction_time.strftime('%Y-%d-%m %H:%M:%S'),
|
||||||
|
'transaction_location': self.transaction_location,
|
||||||
|
'device_info': self.device_info,
|
||||||
|
'ip_address': self.ip_address,
|
||||||
|
'browser_info': self.browser_info,
|
||||||
|
'is_fraud': self.is_fraud,
|
||||||
|
'transaction_status': self.transaction_status,
|
||||||
|
'mobile': self.mobile
|
||||||
|
}
|
0
App/utils/__init__.py
Normal file
0
App/utils/__init__.py
Normal file
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=400):
|
||||||
|
"""错误响应封装"""
|
||||||
|
response = {
|
||||||
|
'code': '500',
|
||||||
|
'message': 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 ""
|
51
App/utils/hive/hive_query.py
Normal file
51
App/utils/hive/hive_query.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import json
|
||||||
|
from pyhive import hive
|
||||||
|
from thrift import Thrift
|
||||||
|
|
||||||
|
class HiveConnection:
|
||||||
|
def __init__(self, host, port=10000, username=None):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.username = username
|
||||||
|
self.connection = None
|
||||||
|
def connect(self):
|
||||||
|
try:
|
||||||
|
self.connection = hive.Connection(
|
||||||
|
host=self.host,
|
||||||
|
port=self.port,
|
||||||
|
username=self.username,
|
||||||
|
auth='NONE' # 根据需要选择其他身份验证方式
|
||||||
|
)
|
||||||
|
return self.connection
|
||||||
|
except Thrift.TException as e:
|
||||||
|
print(f"连接Hive失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def execute_query(self, query):
|
||||||
|
if not self.connection:
|
||||||
|
self.connect()
|
||||||
|
try:
|
||||||
|
cursor = self.connection.cursor()
|
||||||
|
cursor.execute(query)
|
||||||
|
results = cursor.fetchall()
|
||||||
|
# 获取列名
|
||||||
|
column_names = [desc[0] for desc in cursor.description]
|
||||||
|
# 将结果转换为字典格式
|
||||||
|
data = [dict(zip(column_names, row)) for row in results]
|
||||||
|
return json.dumps(data) # 转换为 JSON 字符串
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
print(f"执行查询失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
finally:
|
||||||
|
cursor.close()
|
||||||
|
def close(self):
|
||||||
|
if self.connection:
|
||||||
|
self.connection.close()
|
||||||
|
|
||||||
|
# 示例用法
|
||||||
|
if __name__ == '__main__':
|
||||||
|
hive_conn = HiveConnection(host='192.168.110.130', port=10000, username='root')
|
||||||
|
json_results = hive_conn.execute_query("SELECT * FROM bs_python_paper_analysis.tb_item LIMIT 10")
|
||||||
|
print(json_results )
|
||||||
|
hive_conn.close()
|
21
App/utils/sqoop/mysqlToHive.sh
Normal file
21
App/utils/sqoop/mysqlToHive.sh
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# MySQL 数据库连接参数
|
||||||
|
MYSQL_HOST="192.168.15.37"
|
||||||
|
MYSQL_USER="root"
|
||||||
|
MYSQL_PASSWORD="123456"
|
||||||
|
DATABASE="bs_python_paper_analysis"
|
||||||
|
|
||||||
|
# 删除 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 "导入完毕!"
|
||||||
|
|
2751
App/utils/停用词表.txt
Normal file
2751
App/utils/停用词表.txt
Normal file
File diff suppressed because it is too large
Load Diff
309
App/views.py
Normal file
309
App/views.py
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
# views.py 路由 + 视图函数
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
from flask import request, url_for, jsonify
|
||||||
|
from flask import Blueprint
|
||||||
|
import hashlib
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
from .utils.api_utils import APIUtils
|
||||||
|
from .models import *
|
||||||
|
blus = Blueprint("user", __name__)
|
||||||
|
db_config = {
|
||||||
|
'host': 'localhost',
|
||||||
|
'user': 'root',
|
||||||
|
'password': '123456',
|
||||||
|
'database': 'job',
|
||||||
|
'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('/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']
|
||||||
|
|
||||||
|
if username == "" or password == "":
|
||||||
|
return APIUtils.error_response("用户名或密码不能为空!", status_code=400)
|
||||||
|
user = User.query.filter_by(username=username).first()
|
||||||
|
if user is None:
|
||||||
|
return APIUtils.error_response("用户名错误或不存在!", status_code=401)
|
||||||
|
hashed_password = hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
if hashed_password != user.password:
|
||||||
|
return APIUtils.error_response("密码错误或不存在!", status_code=401)
|
||||||
|
return APIUtils.success_response(data={'token': 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/user/del/<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('per_page', 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/upload', methods=['POST'])
|
||||||
|
def upload():
|
||||||
|
# 检查是否有文件上传
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return APIUtils.error_response(message="没有上传文件!")
|
||||||
|
file = request.files['file']
|
||||||
|
# 如果用户没有选择文件,浏览器也会提交一个空文件
|
||||||
|
if file.filename == '':
|
||||||
|
return APIUtils.error_response(message="没有上传文件!")
|
||||||
|
# 保存文件
|
||||||
|
upload_folder = "uploads"
|
||||||
|
if not os.path.exists(upload_folder):
|
||||||
|
os.makedirs(upload_folder) # 如果不存在则创建目录
|
||||||
|
# 保存文件
|
||||||
|
file_path = os.path.join(upload_folder, file.filename)
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
# 构建文件的可访问 URL
|
||||||
|
file_url = f"http://127.0.0.1:5000/{upload_folder}/{file.filename}"
|
||||||
|
|
||||||
|
# 返回上传路径和文件名
|
||||||
|
response_data = {
|
||||||
|
"name": file.filename.split(".")[0],
|
||||||
|
"path": file_path, # 保存的完整路径
|
||||||
|
"url": file_url # 可访问的 URL
|
||||||
|
}
|
||||||
|
return APIUtils.success_response(data=response_data, message="上传成功")
|
||||||
|
|
||||||
|
|
||||||
|
# 增:添加新的交易记录
|
||||||
|
@blus.route('/api/transactions', methods=['POST'])
|
||||||
|
def add_transaction():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
new_transaction = FinancialTransaction(
|
||||||
|
user_id=data['user_id'],
|
||||||
|
transaction_amount=data['transaction_amount'],
|
||||||
|
transaction_time=data['transaction_time'],
|
||||||
|
transaction_location=data.get('transaction_location', ''),
|
||||||
|
device_info=data.get('device_info', ''),
|
||||||
|
ip_address=data.get('ip_address', ''),
|
||||||
|
browser_info=data.get('browser_info', ''),
|
||||||
|
is_fraud=data['is_fraud']
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.add(new_transaction)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return APIUtils.success_response(data=jsonify(new_transaction.to_dict()), message="成功")
|
||||||
|
|
||||||
|
# 查:获取所有交易记录
|
||||||
|
@blus.route('/api/transactions', methods=['GET'])
|
||||||
|
def get_transactions():
|
||||||
|
# 获取分页参数,设置默认值
|
||||||
|
page = request.args.get('page', 1, type=int) # 默认第一页
|
||||||
|
page_size = request.args.get('page_size', 10, type=int) # 默认每页10条
|
||||||
|
|
||||||
|
# 计算分页偏移量
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
|
# 查询交易记录,使用 limit 和 offset 实现分页
|
||||||
|
transactions = FinancialTransaction.query.offset(offset).limit(page_size).all()
|
||||||
|
|
||||||
|
# 获取总记录数,用于计算总页数
|
||||||
|
total_count = FinancialTransaction.query.count()
|
||||||
|
total_pages = ceil(total_count / page_size)
|
||||||
|
|
||||||
|
# 构建响应数据,包括分页信息
|
||||||
|
response = {
|
||||||
|
'data': [transaction.to_dict() for transaction in transactions],
|
||||||
|
'page': {
|
||||||
|
"current_page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"total_count": total_count,
|
||||||
|
"total_pages": total_pages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 返回分页数据,包括当前页的记录和总信息
|
||||||
|
return APIUtils.success_response(
|
||||||
|
data=response,
|
||||||
|
message="成功",
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 查:获取单个交易记录
|
||||||
|
@blus.route('/api/transactions/<int:transaction_id>', methods=['GET'])
|
||||||
|
def get_transaction(transaction_id):
|
||||||
|
transaction = FinancialTransaction.query.get(transaction_id)
|
||||||
|
if transaction is None:
|
||||||
|
return jsonify({'message': 'Transaction not found'}), 404
|
||||||
|
|
||||||
|
return jsonify(transaction.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
# 改:更新交易记录
|
||||||
|
@blus.route('/api/transactions/<int:transaction_id>', methods=['PUT'])
|
||||||
|
def update_transaction(transaction_id):
|
||||||
|
transaction = FinancialTransaction.query.get(transaction_id)
|
||||||
|
|
||||||
|
if transaction is None:
|
||||||
|
return jsonify({'message': 'Transaction not found'}), 404
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
transaction.user_id = data.get('user_id', transaction.user_id)
|
||||||
|
transaction.transaction_amount = data.get('transaction_amount', transaction.transaction_amount)
|
||||||
|
transaction.transaction_time = data.get('transaction_time', transaction.transaction_time)
|
||||||
|
transaction.transaction_location = data.get('transaction_location', transaction.transaction_location)
|
||||||
|
transaction.device_info = data.get('device_info', transaction.device_info)
|
||||||
|
transaction.ip_address = data.get('ip_address', transaction.ip_address)
|
||||||
|
transaction.browser_info = data.get('browser_info', transaction.browser_info)
|
||||||
|
transaction.is_fraud = data.get('is_fraud', transaction.is_fraud)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify(transaction.to_dict())
|
||||||
|
|
||||||
|
|
||||||
|
# 删:删除交易记录
|
||||||
|
@blus.route('/api/transactions/<int:transaction_id>', methods=['DELETE'])
|
||||||
|
def delete_transaction(transaction_id):
|
||||||
|
transaction = FinancialTransaction.query.get(transaction_id)
|
||||||
|
|
||||||
|
if transaction is None:
|
||||||
|
return jsonify({'message': 'Transaction not found'}), 404
|
||||||
|
|
||||||
|
db.session.delete(transaction)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({'message': 'Transaction deleted'}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# 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)}")
|
||||||
|
|
7
app.py
Normal file
7
app.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from App import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=True)
|
89
demo.py
Normal file
89
demo.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
from sklearn.ensemble import RandomForestClassifier
|
||||||
|
from sklearn.metrics import classification_report, confusion_matrix
|
||||||
|
import xgboost as xgb
|
||||||
|
|
||||||
|
# 模拟一个简单的交易数据集
|
||||||
|
data = {
|
||||||
|
'transaction_amount': [100, 200, 150, 50, 300, 400, 120, 80],
|
||||||
|
'transaction_time': ['2025-02-05 12:00:00', '2025-02-05 13:30:00', '2025-02-05 15:00:00',
|
||||||
|
'2025-02-05 16:30:00', '2025-02-06 12:00:00', '2025-02-06 13:00:00',
|
||||||
|
'2025-02-06 14:30:00', '2025-02-06 16:00:00'],
|
||||||
|
'user_id': ['user1', 'user2', 'user3', 'user1', 'user2', 'user3', 'user1', 'user2'],
|
||||||
|
'device_info': ['device123', 'device124', 'device123', 'device125', 'device126', 'device124', 'device123', 'device125'],
|
||||||
|
'ip_address': ['IP123', 'IP124', 'IP125', 'IP126', 'IP127', 'IP124', 'IP123', 'IP126'],
|
||||||
|
'is_fraud': [0, 1, 0, 0, 1, 0, 0, 1] # 1 表示欺诈,0 表示正常
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建 DataFrame
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
# 提取特征矩阵 X 和标签 y
|
||||||
|
X = df[['transaction_amount', 'user_id', 'device_info', 'ip_address']] # 选择特征列
|
||||||
|
y = df['is_fraud'] # 标签是是否欺诈
|
||||||
|
|
||||||
|
# 对类别特征进行编码(如 user_id, device_info, ip_address 等)
|
||||||
|
X = pd.get_dummies(X, columns=['user_id', 'device_info', 'ip_address'])
|
||||||
|
|
||||||
|
# 将 transaction_time 转换为数值(如将时间转化为时间戳)
|
||||||
|
X['transaction_time'] = pd.to_datetime(df['transaction_time']).view('int64') / 10**9 # 转换为 Unix 时间戳
|
||||||
|
|
||||||
|
# 拆分数据集为训练集和测试集
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
||||||
|
|
||||||
|
# 训练随机森林模型
|
||||||
|
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
|
||||||
|
rf_model.fit(X_train, y_train)
|
||||||
|
|
||||||
|
# 预测测试集
|
||||||
|
y_pred_rf = rf_model.predict(X_test)
|
||||||
|
|
||||||
|
# 评估模型
|
||||||
|
print("Random Forest Classification Report:")
|
||||||
|
print(classification_report(y_test, y_pred_rf))
|
||||||
|
print(confusion_matrix(y_test, y_pred_rf))
|
||||||
|
|
||||||
|
# 使用 XGBoost 训练
|
||||||
|
dtrain = xgb.DMatrix(X_train, label=y_train)
|
||||||
|
dtest = xgb.DMatrix(X_test, label=y_test)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'objective': 'binary:logistic',
|
||||||
|
'eval_metric': 'logloss',
|
||||||
|
'max_depth': 6,
|
||||||
|
'learning_rate': 0.1,
|
||||||
|
'n_estimators': 100
|
||||||
|
}
|
||||||
|
|
||||||
|
# 训练模型
|
||||||
|
xgb_model = xgb.train(params, dtrain, num_boost_round=100)
|
||||||
|
|
||||||
|
# 预测
|
||||||
|
y_pred_xgb = xgb_model.predict(dtest)
|
||||||
|
y_pred_binary_xgb = [1 if p > 0.5 else 0 for p in y_pred_xgb]
|
||||||
|
|
||||||
|
# 评估模型
|
||||||
|
print("XGBoost Classification Report:")
|
||||||
|
print(classification_report(y_test, y_pred_binary_xgb))
|
||||||
|
print(confusion_matrix(y_test, y_pred_binary_xgb))
|
||||||
|
|
||||||
|
# 新的交易数据
|
||||||
|
new_transaction = [[100, 'user1', 'device123', 'IP123', 1644052800]] # 使用 Unix 时间戳
|
||||||
|
new_transaction = pd.get_dummies(pd.DataFrame(new_transaction, columns=['transaction_amount', 'user_id', 'device_info', 'ip_address', 'transaction_time']))
|
||||||
|
|
||||||
|
# 预测新交易是否为欺诈
|
||||||
|
predicted_label_rf = rf_model.predict(new_transaction)
|
||||||
|
predicted_label_xgb = xgb_model.predict(xgb.DMatrix(new_transaction))
|
||||||
|
|
||||||
|
# 输出预测结果
|
||||||
|
if predicted_label_rf == 1:
|
||||||
|
print("随机森林模型预测:交易存在欺诈风险!")
|
||||||
|
else:
|
||||||
|
print("随机森林模型预测:交易正常。")
|
||||||
|
|
||||||
|
if predicted_label_xgb > 0.5:
|
||||||
|
print("XGBoost模型预测:交易存在欺诈风险!")
|
||||||
|
else:
|
||||||
|
print("XGBoost模型预测:交易正常。")
|
10001
tb_financial_transactions.csv
Normal file
10001
tb_financial_transactions.csv
Normal file
File diff suppressed because it is too large
Load Diff
0
tb_financial_transactions.sql
Normal file
0
tb_financial_transactions.sql
Normal file
44
ui/.devcontainer/devcontainer.json
Normal file
44
ui/.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
ui/.editorConfig
Normal file
23
ui/.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
|
4
ui/.eslintignore
Normal file
4
ui/.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# 忽略 eslint 检查
|
||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
12
ui/.gitignore
vendored
Normal file
12
ui/.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
|
3
ui/.prettierignore
Normal file
3
ui/.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
presets/types
|
5
ui/.prettierrc.json
Normal file
5
ui/.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
1287
ui/README.md
Normal file
1287
ui/README.md
Normal file
File diff suppressed because it is too large
Load Diff
13
ui/index.html
Normal file
13
ui/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
ui/locales/English/common.yml
Normal file
10
ui/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
ui/locales/English/test.yml
Normal file
3
ui/locales/English/test.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# English
|
||||||
|
|
||||||
|
test.module: lanugae module test
|
10
ui/locales/简体中文/common.yml
Normal file
10
ui/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
ui/locales/简体中文/test.yml
Normal file
3
ui/locales/简体中文/test.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 简体中文
|
||||||
|
|
||||||
|
test.module: 多语言多模块测试
|
4
ui/netlify.toml
Normal file
4
ui/netlify.toml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[[redirects]]
|
||||||
|
to = "/index.html"
|
||||||
|
from = "/*"
|
||||||
|
status = 200
|
112
ui/package.json
Normal file
112
ui/package.json
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
{
|
||||||
|
"name": "后台",
|
||||||
|
"version": "1.19.0",
|
||||||
|
"description": "后台",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"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",
|
||||||
|
"element-plus": "^2.9.2"
|
||||||
|
}
|
||||||
|
}
|
7323
ui/pnpm-lock.yaml
generated
Normal file
7323
ui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
ui/presets/autoprefixer.ts
Normal file
33
ui/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
ui/presets/index.ts
Normal file
234
ui/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
ui/presets/plugins/alias.ts
Normal file
27
ui/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
ui/presets/plugins/index.ts
Normal file
6
ui/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
ui/presets/plugins/layers.ts
Normal file
41
ui/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
ui/presets/plugins/lightningcss.ts
Normal file
62
ui/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
ui/presets/plugins/optimize.ts
Normal file
19
ui/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
ui/presets/plugins/restart.ts
Normal file
30
ui/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
ui/presets/plugins/warmup.ts
Normal file
21
ui/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
ui/presets/shared/detect.ts
Normal file
76
ui/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
ui/presets/shared/mock.ts
Normal file
89
ui/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
ui/presets/shared/path.ts
Normal file
15
ui/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
ui/presets/types/vite.d.ts
vendored
Normal file
17
ui/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
ui/public/icoimg.png
Normal file
BIN
ui/public/icoimg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
ui/public/login/e36341619bf8f04dcbdc6b01105a85a.png
Normal file
BIN
ui/public/login/e36341619bf8f04dcbdc6b01105a85a.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 342 KiB |
BIN
ui/public/loginimg.jpg
Normal file
BIN
ui/public/loginimg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 212 KiB |
BIN
ui/public/logo.png
Normal file
BIN
ui/public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
1
ui/public/notFound/33.svg
Normal file
1
ui/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
ui/renovate.json
Normal file
3
ui/renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["github>unjs/renovate-config"]
|
||||||
|
}
|
76
ui/scripts/create.cjs
Normal file
76
ui/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
ui/scripts/deps-fresh.cjs
Normal file
36
ui/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
ui/scripts/release.cjs
Normal file
65
ui/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
ui/scripts/remove.cjs
Normal file
75
ui/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
ui/scripts/safe-init.cjs
Normal file
113
ui/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
ui/scripts/shared/base.cjs
Normal file
41
ui/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
ui/scripts/template/api.hbs
Normal file
2
ui/scripts/template/api.hbs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
11
ui/scripts/template/component.hbs
Normal file
11
ui/scripts/template/component.hbs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
{{name}}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
5
ui/scripts/template/composable.hbs
Normal file
5
ui/scripts/template/composable.hbs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ref } from "vue"
|
||||||
|
|
||||||
|
export default {{name}} = () => {
|
||||||
|
|
||||||
|
}
|
4
ui/scripts/template/layout.hbs
Normal file
4
ui/scripts/template/layout.hbs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
{{pascalCase name}} layout
|
||||||
|
<router-view />
|
||||||
|
</template>
|
6
ui/scripts/template/module.hbs
Normal file
6
ui/scripts/template/module.hbs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { App } from "vue"
|
||||||
|
|
||||||
|
|
||||||
|
export default (app: App) => {
|
||||||
|
|
||||||
|
}
|
17
ui/scripts/template/page.hbs
Normal file
17
ui/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
ui/scripts/template/store.hbs
Normal file
9
ui/scripts/template/store.hbs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export default defineStore('{{name}}', {
|
||||||
|
state() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {}
|
||||||
|
})
|
9
ui/src/App.vue
Normal file
9
ui/src/App.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
p {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
50
ui/src/api/user/adminUserApi.ts
Normal file
50
ui/src/api/user/adminUserApi.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { adminRequest } from '~/composables/adminRequest'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function captchaAdmin(uid: Number) {
|
||||||
|
return adminRequest.get("/captcha",{
|
||||||
|
params:{uuid:uid}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function loginAdmin(data: any) {
|
||||||
|
return adminRequest.post("/api/login", data)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
* @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)
|
||||||
|
}
|
14
ui/src/api/user/adminUserUtils.ts
Normal file
14
ui/src/api/user/adminUserUtils.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { getUuid } from '~/utils/utils'
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取验证码图
|
||||||
|
*/
|
||||||
|
export function getCaptchaUrl(){
|
||||||
|
const uuid = getUuid()
|
||||||
|
captchaAdmin(uuid)
|
||||||
|
return {
|
||||||
|
uid: uuid,
|
||||||
|
captchaUrl:import.meta.env.VITE_ADMIN_API_BASE_URL + `/captcha?uuid=${uuid}`
|
||||||
|
}
|
||||||
|
}
|
46
ui/src/api/user/frontUserApi.ts
Normal file
46
ui/src/api/user/frontUserApi.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function loginFront(data:any) {
|
||||||
|
frontRequest.post("/api/user/login", data).then(response =>{
|
||||||
|
const user = userStore()
|
||||||
|
user.frontToken = response.data.token
|
||||||
|
frontRequest.get("/api/user/userInfo").then(response =>{
|
||||||
|
user.frontUserInfo = response.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 注册
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function registerFront(data: any) {
|
||||||
|
return adminRequest.post("/api/user/register", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户信息
|
||||||
|
* @param userId
|
||||||
|
*/
|
||||||
|
export function userInfoFront(userId: any) {
|
||||||
|
return frontRequest.get("/api/user/userInfo", {
|
||||||
|
params: { userId: userId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户信息
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
export function userUpdateFront(data:any) {
|
||||||
|
return frontRequest.put("/api/user/update", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出
|
||||||
|
*/
|
||||||
|
export function logoutFront() {
|
||||||
|
return frontRequest.post("/api/user/logout")
|
||||||
|
}
|
151
ui/src/components/Heads.vue
Normal file
151
ui/src/components/Heads.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="head">
|
||||||
|
<div class="head_l">
|
||||||
|
<!-- <img src="/icoimg.png" alt="收缩" />-->
|
||||||
|
</div>
|
||||||
|
<el-dropdown>
|
||||||
|
<div class="head_r">
|
||||||
|
<!-- <img :src="userStore().adminUserInfo.avatar" alt="头像" class="profile" />-->
|
||||||
|
<div class="head_user">
|
||||||
|
<div class="head_user_name">{{ userStore().adminUserInfo.username }}</div>
|
||||||
|
<div class="head_user_desc">管理员</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.dynamicValidateForm"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item
|
||||||
|
prop="password"
|
||||||
|
label="原始密码"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '原始密码不能为空',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.dynamicValidateForm.password" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item
|
||||||
|
prop="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
:rules="[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '新密码不能为空',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<el-input v-model="state.dynamicValidateForm.newPassword" />
|
||||||
|
</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 { updatePasswordAdmin } from '~/api/user/adminUserApi'
|
||||||
|
const router = useRouter();
|
||||||
|
const drawer = ref(false)
|
||||||
|
const state = reactive({
|
||||||
|
dynamicValidateForm:{}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
logoutAdmin().then(()=>{
|
||||||
|
toast.success("退出成功~")
|
||||||
|
router.push('/login');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
* @param formEl
|
||||||
|
*/
|
||||||
|
const submitForm = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return
|
||||||
|
formEl.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
updatePasswordAdmin(state.dynamicValidateForm).then(result => {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.head{
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
background: #fdfdfe;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
78
ui/src/components/nav/Navigation1.vue
Normal file
78
ui/src/components/nav/Navigation1.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<!--前端样式1-->
|
||||||
|
<template>
|
||||||
|
<el-row justify="space-between">
|
||||||
|
<el-col :span="6">
|
||||||
|
<div class="grid-content ep-bg-purple" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-menu
|
||||||
|
:default-active="nav.frontPath"
|
||||||
|
mode="horizontal"
|
||||||
|
@select="handleSelect"
|
||||||
|
router
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
|
class="nav-name"
|
||||||
|
v-for="r of getFrontList()" :key="r.path"
|
||||||
|
:index="r.path">
|
||||||
|
{{ r.name }}
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="3">
|
||||||
|
<el-button v-if="userStore().frontUserInfo" style="margin-top: 12px" type="primary" round @click="router.push('/login')">登录</el-button>
|
||||||
|
<el-dropdown v-else>
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<!-- <el-avatar :src="userStore().frontUserInfo.avatar" />-->
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="16">
|
||||||
|
<h6>{{ userStore().frontUserInfo.username }}</h6>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu slot="dropdown">
|
||||||
|
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { getFrontList } from '~/utils/utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { logoutFront } from '~/api/user/frontUserApi'
|
||||||
|
const router = useRouter()
|
||||||
|
const nav = navStore()
|
||||||
|
const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
|
nav.frontPath = key.fullPath
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
const logout = () => {
|
||||||
|
logoutFront().then(() => {
|
||||||
|
toast.success('退出成功~')
|
||||||
|
router.push('/login')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-menu--horizontal) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
padding-top: 15px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.nav-name{
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
76
ui/src/components/view/foundation-line.vue
Normal file
76
ui/src/components/view/foundation-line.vue
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<v-chart class="chart" :option="option" autoresize />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
import { LineChart } from 'echarts/charts'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import {
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent,
|
||||||
|
AxisPointerComponent,
|
||||||
|
} from 'echarts/components'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
use([
|
||||||
|
CanvasRenderer,
|
||||||
|
LineChart,
|
||||||
|
TitleComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent,
|
||||||
|
GridComponent,
|
||||||
|
AxisPointerComponent, // 如果需要显示坐标轴指示器,也可以导入
|
||||||
|
])
|
||||||
|
const option = ref<any>({
|
||||||
|
title: {
|
||||||
|
text: 'Traffic Trend Over Time',
|
||||||
|
left: 'center',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['Traffic'],
|
||||||
|
left: 'left',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '10%',
|
||||||
|
right: '10%',
|
||||||
|
bottom: '10%',
|
||||||
|
containLabel: true, // 包含标签
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'Traffic',
|
||||||
|
type: 'line', // 设置图表类型为折线图
|
||||||
|
data: [820, 932, 901, 934, 1290, 1330, 1320, 1010, 1100, 1230, 1300, 1420],
|
||||||
|
itemStyle: {
|
||||||
|
color: '#66b3ff',
|
||||||
|
},
|
||||||
|
lineStyle: {
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
smooth: true, // 平滑曲线
|
||||||
|
areaStyle: { // 添加面积背景色
|
||||||
|
origin: 'start',
|
||||||
|
color: 'rgba(102, 179, 255, 0.2)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
53
ui/src/composables/adminRequest.ts
Normal file
53
ui/src/composables/adminRequest.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const adminRequest = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_ADMIN_API_BASE_URL,
|
||||||
|
})
|
||||||
|
// 添加请求拦截器
|
||||||
|
adminRequest.interceptors.request.use(
|
||||||
|
function (config) {
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
const token = userStore().adminToken
|
||||||
|
if (token !== null || token !== undefined) {
|
||||||
|
//添加header
|
||||||
|
config.headers.token = token
|
||||||
|
}
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
toast.warning(error.message ?? '未知请求错误')
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// 添加响应拦截器
|
||||||
|
adminRequest.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
if (response.data.code){
|
||||||
|
const code = response.data.code
|
||||||
|
switch (code) {
|
||||||
|
case 500:
|
||||||
|
toast.error(response.data.msg)
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
case 401:
|
||||||
|
toast.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
|
||||||
|
}
|
||||||
|
toast.warning(msg)
|
||||||
|
// 超出 2xx 范围的状态码都会触发该函数。
|
||||||
|
// 对响应错误做点什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
26
ui/src/composables/env.ts
Normal file
26
ui/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
|
75
ui/src/composables/frontRequest.ts
Normal file
75
ui/src/composables/frontRequest.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export const frontRequest = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_FRONT_BASE_URL,
|
||||||
|
})
|
||||||
|
// 添加请求拦截器
|
||||||
|
frontRequest.interceptors.request.use(
|
||||||
|
function (config) {
|
||||||
|
const token = userStore().frontToken
|
||||||
|
if (token !== null || token !== undefined) {
|
||||||
|
//添加header
|
||||||
|
config.headers.Authorization = token
|
||||||
|
}
|
||||||
|
// 在发送请求之前做些什么
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
toast.warning(error.message ?? '未知请求错误')
|
||||||
|
// 对请求错误做些什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// 添加响应拦截器
|
||||||
|
frontRequest.interceptors.response.use(
|
||||||
|
function (response) {
|
||||||
|
if (response.data.code){
|
||||||
|
const code = response.data.code
|
||||||
|
switch (code) {
|
||||||
|
case 500:
|
||||||
|
toast.error(response.data.msg)
|
||||||
|
return Promise.reject(response.data.msg)
|
||||||
|
case 401:
|
||||||
|
window.open(`/login`, '_self')
|
||||||
|
toast.error("请重新登录~")
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (response.data) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
function (error) {
|
||||||
|
const status = error.response?.status
|
||||||
|
let { msg, message } = error.response?.data ?? {}
|
||||||
|
|
||||||
|
if (!msg && message) {
|
||||||
|
msg = message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
msg = '参数错误'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
msg = '服务端错误'
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
window.location.href = "/dsds"
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
msg = error.message ?? '未知响应错误'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.warning(msg)
|
||||||
|
// 超出 2xx 范围的状态码都会触发该函数。
|
||||||
|
// 对响应错误做点什么
|
||||||
|
return Promise.reject(error)
|
||||||
|
},
|
||||||
|
)
|
7
ui/src/composables/path.ts
Normal file
7
ui/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
ui/src/composables/toast.ts
Normal file
3
ui/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
ui/src/composables/useLanguage.ts
Normal file
12
ui/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
ui/src/composables/useTyped.ts
Normal file
14
ui/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
ui/src/composables/useVisits.ts
Normal file
25
ui/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
|
||||||
|
}
|
164
ui/src/layouts/admin.vue
Normal file
164
ui/src/layouts/admin.vue
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main">
|
||||||
|
<div class="nav_left">
|
||||||
|
<div class="logo">
|
||||||
|
<img src="/logo.png" alt="后台管理系统">
|
||||||
|
</div>
|
||||||
|
<div class="nav_list">
|
||||||
|
<div class="nav_title"><img src="/logo.png" alt="图标">导航管理</div>
|
||||||
|
<div class="rArrl"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav_li">
|
||||||
|
<ul>
|
||||||
|
<li v-for="r of routes" :key="r.path">
|
||||||
|
<img src="/logo.png" alt="图标"><RouterLink style="width: 100%; height: 45px; line-height: 45px" :to="r.path">{{ te(r.name) ? t(r.name) : r.name }}</RouterLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="nav_list">
|
||||||
|
<div class="nav_title"><img src="/logo.png" alt="图标">权限管理</div>
|
||||||
|
<div class="rArrl"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav_list">
|
||||||
|
<div class="nav_title"><img src="/logo.png" alt="图标">模型设置</div>
|
||||||
|
<div class="rArrl"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav_list">
|
||||||
|
<div class="nav_title"><img src="/logo.png" alt="图标">内容管理</div>
|
||||||
|
<div class="rArrl"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav_list">
|
||||||
|
<div class="nav_title"><img src="/logo.png" alt="图标">会员管理</div>
|
||||||
|
<div class="rArrl"></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav_list">
|
||||||
|
<div class="nav_title"><img src="/logo.png" alt="图标">模版管理</div>
|
||||||
|
<div class="rArrl"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav_right">
|
||||||
|
<Heads />
|
||||||
|
<div class="content">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <Navigation />-->
|
||||||
|
<!-- <div class="w-screen flex flex-col items-center justify-center">-->
|
||||||
|
<!-- <router-view v-slot="{ Component }">-->
|
||||||
|
<!-- <transition name="fade" mode="out-in">-->
|
||||||
|
<!-- <component :is="Component" />-->
|
||||||
|
<!-- </transition>-->
|
||||||
|
<!-- </router-view>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
.nav_left{
|
||||||
|
width: 15%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #111c43;
|
||||||
|
overflow-y: auto;
|
||||||
|
.logo{
|
||||||
|
width: 100%;
|
||||||
|
height: 60px;
|
||||||
|
border-bottom: 0.5px solid #293356;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.logo img{
|
||||||
|
width: 85%;
|
||||||
|
height: 40px;
|
||||||
|
background: #293356;
|
||||||
|
color: #FFFFFF;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.nav_list{
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 45px;
|
||||||
|
.nav_title{
|
||||||
|
font-size: 14px;
|
||||||
|
color: #a3aed1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav_title img{
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #333333;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.rArrl{
|
||||||
|
width: 7px;
|
||||||
|
height: 7px;
|
||||||
|
border-top: 1px solid #a3aed1;
|
||||||
|
border-right: 1px solid #a3aed1;
|
||||||
|
margin-top: 9px;
|
||||||
|
-webkit-transform: translate3d(0, -50%, 0) rotate(45deg);
|
||||||
|
transform: translate3d(0, -50%, 0) rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nav_li{
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 16%;
|
||||||
|
background: #1a223f;
|
||||||
|
}
|
||||||
|
.nav_li ul li{
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a3aed1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 45px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.nav_li ul li img{
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #333333;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.nav_right{
|
||||||
|
width: 85%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f0f1f7;
|
||||||
|
.content{
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Heads from '~/components/Heads.vue'
|
||||||
|
import { getRoutes } from '@/plugins/router'
|
||||||
|
const { te, t } = useI18n()
|
||||||
|
const routes = getRoutes()
|
||||||
|
.filter((r) => !r.path.includes('notFound'))
|
||||||
|
.map((r) => {
|
||||||
|
let { path, name } = r
|
||||||
|
if (path === safeResolve('/')) {
|
||||||
|
return { path, name: 'home' }
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
name = path
|
||||||
|
}
|
||||||
|
return { path, name: name.toString().slice(1).replaceAll('/', ' · ') }
|
||||||
|
})
|
||||||
|
const $route = useRoute()
|
||||||
|
</script>
|
50
ui/src/layouts/default.vue
Normal file
50
ui/src/layouts/default.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<heads></heads>
|
||||||
|
</el-header>
|
||||||
|
<el-container>
|
||||||
|
<el-aside width="200px" >
|
||||||
|
<el-menu
|
||||||
|
:default-active="navStore().adminPath"
|
||||||
|
router
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="r in getAdminList()"
|
||||||
|
: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, getFrontList } from '~/utils/utils'
|
||||||
|
import navStore from '~/stores/navStore'
|
||||||
|
const handleSelect = (key: string, keyPath: string[]) => {
|
||||||
|
navStore().adminPath = key
|
||||||
|
console.log(key, keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 80px);
|
||||||
|
background-color: #f6f4f4;
|
||||||
|
}
|
||||||
|
.icons{
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
34
ui/src/layouts/front.vue
Normal file
34
ui/src/layouts/front.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
<!--前端模板-->
|
||||||
|
<template>
|
||||||
|
<div class="common-layout">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<nav-navigation1></nav-navigation1>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
background-color: #f3f3f4;
|
||||||
|
}
|
||||||
|
.container{
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
54
ui/src/layouts/frontUserInfo.vue
Normal file
54
ui/src/layouts/frontUserInfo.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!--前端模板-->
|
||||||
|
<template>
|
||||||
|
<div class="common-layout">
|
||||||
|
<el-container>
|
||||||
|
<el-header>
|
||||||
|
<nav-navigation1></nav-navigation1>
|
||||||
|
</el-header>
|
||||||
|
<el-main class="main">
|
||||||
|
<div class="container">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-menu
|
||||||
|
default-active=""
|
||||||
|
router
|
||||||
|
>
|
||||||
|
<el-menu-item v-for="item in state.userMenu" :index="item.path">
|
||||||
|
<span>{{item.name}}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="18">
|
||||||
|
<router-view></router-view>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
const state = reactive({
|
||||||
|
userMenu:[
|
||||||
|
{name: '个人中心', path: '/front/user'},
|
||||||
|
{name: '用户订单', path: '/front/user/order'},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.main{
|
||||||
|
width: 100%;
|
||||||
|
height: calc(100vh - 60px);
|
||||||
|
background-color: #f3f3f4;
|
||||||
|
}
|
||||||
|
.container{
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
3
ui/src/layouts/notFound.vue
Normal file
3
ui/src/layouts/notFound.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
17
ui/src/main.ts
Normal file
17
ui/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
ui/src/pages/[...notFound].vue
Normal file
58
ui/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>
|
234
ui/src/pages/admin/index.vue
Normal file
234
ui/src/pages/admin/index.vue
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard">
|
||||||
|
<el-row gutter="20">
|
||||||
|
<!-- 总览统计 -->
|
||||||
|
<el-col :span="8" >
|
||||||
|
<el-card class="dashboard-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>总览统计</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" >
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-title">总交易数</div>
|
||||||
|
<div class="stat-value">{{ totalTransactions }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item" >
|
||||||
|
<div class="stat-title">欺诈交易</div>
|
||||||
|
<div class="stat-value">{{ fraudTransactions }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-title">欺诈率</div>
|
||||||
|
<div class="stat-value">{{ fraudRate }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 实时监控 -->
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-card class="dashboard-card" style="height: 240px">
|
||||||
|
<div class="card-header" >
|
||||||
|
<h3>实时监控</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-title">实时交易数</div>
|
||||||
|
<div class="stat-value">{{ realTimeTransactions }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-title">检测到的欺诈交易</div>
|
||||||
|
<div class="stat-value">{{ realTimeFraud }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 系统健康 -->
|
||||||
|
<el-col :span="8" >
|
||||||
|
<el-card class="dashboard-card" style="height: 240px">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>系统健康</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="health-status">
|
||||||
|
<div class="status-item">
|
||||||
|
<div class="status-title">模型状态</div>
|
||||||
|
<div class="status-value">{{ modelStatus }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<div class="status-title">系统运行</div>
|
||||||
|
<div class="status-value">{{ systemStatus }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 实时交易图表 -->
|
||||||
|
<el-row class="chart-row">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-card class="chart-card">
|
||||||
|
|
||||||
|
<!-- 使用 ECharts 或其他图表库来展示实时交易趋势 -->
|
||||||
|
<div ref="chart" class="chart-placeholder">图表在此展示</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
|
||||||
|
// 静态数据
|
||||||
|
const totalTransactions = ref(21500);
|
||||||
|
const fraudTransactions = ref(1560);
|
||||||
|
const fraudRate = ref(((fraudTransactions.value / totalTransactions.value) * 100).toFixed(2));
|
||||||
|
const realTimeTransactions = ref(256);
|
||||||
|
const realTimeFraud = ref(12);
|
||||||
|
const modelStatus = ref("正常");
|
||||||
|
const systemStatus = ref("正常");
|
||||||
|
|
||||||
|
// ECharts 配置
|
||||||
|
const chart = ref(null);
|
||||||
|
const chartInstance = ref<echarts.ECharts | null>(null);
|
||||||
|
|
||||||
|
const chartData = ref({
|
||||||
|
xData: ['10:00', '11:00', '12:00', '13:00', '14:00', '15:00'],
|
||||||
|
yData: [45, 70, 80, 90, 60, 50],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化 ECharts
|
||||||
|
const initChart = () => {
|
||||||
|
if (chart.value) {
|
||||||
|
chartInstance.value = echarts.init(chart.value);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '实时交易趋势',
|
||||||
|
left: 'center',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: chartData.value.xData,
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
data: chartData.value.yData,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: {
|
||||||
|
color: '#409EFF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.value.setOption(option);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 页面加载时初始化图表
|
||||||
|
onMounted(() => {
|
||||||
|
initChart();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面卸载时销毁图表实例
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
chartInstance.value?.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
transition: box-shadow 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-card:hover {
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-title {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-row {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-placeholder {
|
||||||
|
height: 300px;
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #888;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
147
ui/src/pages/admin/view1.vue
Normal file
147
ui/src/pages/admin/view1.vue
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- 增、查交易记录的按钮 -->
|
||||||
|
<el-button type="primary" @click="openAddDialog">同步交易记录</el-button>
|
||||||
|
<el-table :data="state.getList">
|
||||||
|
<el-table-column prop="transaction_id" label="交易ID" />
|
||||||
|
<el-table-column prop="user_name" label="姓名" />
|
||||||
|
<el-table-column prop="mobile" label="联系方式"/>
|
||||||
|
<el-table-column prop="transaction_amount" label="交易金额" />
|
||||||
|
<el-table-column prop="transaction_time" label="交易时间" width="200" />
|
||||||
|
<el-table-column prop="transaction_location" label="交易城市" />
|
||||||
|
<el-table-column prop="transaction_status" label="交易状态" />
|
||||||
|
<el-table-column prop="ip_address" label="ip地址" width="180" />
|
||||||
|
<!-- <el-table-column prop="is_fraud" label="是否欺诈" width="180" />-->
|
||||||
|
<el-table-column label="操作">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button @click="editTransaction(scope.row)" size="small">编辑</el-button>
|
||||||
|
<el-button @click="deleteTransaction(scope.row.transaction_id)" type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<!-- 分页控件 -->
|
||||||
|
<el-pagination
|
||||||
|
v-if="state.totalCount > 0"
|
||||||
|
:current-page="state.page"
|
||||||
|
:page-size="state.pageSize"
|
||||||
|
:total="state.totalCount"
|
||||||
|
layout="total, prev, pager, jumper"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
/>
|
||||||
|
<!-- 新增/编辑对话框 -->
|
||||||
|
<el-dialog v-model="state.dialogVisible" title="新增交易记录" width="50%">
|
||||||
|
<el-form :model="formData" ref="form" label-width="100px">
|
||||||
|
<el-form-item label="用户ID" prop="user_id" :rules="[{ required: true, message: '请输入用户ID', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="formData.user_id" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交易金额" prop="transaction_amount" :rules="[{ required: true, message: '请输入交易金额', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="formData.transaction_amount" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交易时间" prop="transaction_time" :rules="[{ required: true, message: '请输入交易时间', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="formData.transaction_time" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="是否欺诈" prop="is_fraud" :rules="[{ required: true, message: '请选择是否欺诈', trigger: 'blur' }]">
|
||||||
|
<el-switch v-model="formData.is_fraud" active-text="是" inactive-text="否" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="state.dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="saveTransaction">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, onMounted } from 'vue'
|
||||||
|
// 交易记录数据
|
||||||
|
const state = reactive({
|
||||||
|
dialogVisible:false,
|
||||||
|
getList: [],
|
||||||
|
totalCount: 0, // 总记录数
|
||||||
|
page: 1, // 当前页码
|
||||||
|
pageSize: 10, // 每页显示的记录数
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive({
|
||||||
|
transaction_id: null,
|
||||||
|
user_id: '',
|
||||||
|
transaction_amount: '',
|
||||||
|
transaction_time: '',
|
||||||
|
is_fraud: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取交易记录
|
||||||
|
const init = () => {
|
||||||
|
adminRequest.get('/api/transactions', {
|
||||||
|
params: {
|
||||||
|
page: state.page,
|
||||||
|
page_size: state.pageSize,
|
||||||
|
}
|
||||||
|
}).then(res => {
|
||||||
|
state.getList = res.data.data
|
||||||
|
state.totalCount = res.data.page.total_count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增交易记录
|
||||||
|
const openAddDialog = () => {
|
||||||
|
ElMessage.success("同步成功!")
|
||||||
|
// formData.transaction_id = null
|
||||||
|
// formData.user_id = ''
|
||||||
|
// formData.transaction_amount = ''
|
||||||
|
// formData.transaction_time = ''
|
||||||
|
// formData.is_fraud = false
|
||||||
|
// state.dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑交易记录
|
||||||
|
const editTransaction = (row: any) => {
|
||||||
|
formData.transaction_id = row.transaction_id
|
||||||
|
formData.user_id = row.user_id
|
||||||
|
formData.transaction_amount = row.transaction_amount
|
||||||
|
formData.transaction_time = row.transaction_time
|
||||||
|
formData.is_fraud = row.is_fraud
|
||||||
|
state.dialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存交易记录
|
||||||
|
const saveTransaction = async () => {
|
||||||
|
if (formData.transaction_id) {
|
||||||
|
// 更新交易记录
|
||||||
|
await adminRequest.put(`/api/transactions/${formData.transaction_id}`, formData)
|
||||||
|
} else {
|
||||||
|
// 新增交易记录
|
||||||
|
await adminRequest.post('api/transactions', formData)
|
||||||
|
}
|
||||||
|
init()
|
||||||
|
state.dialogVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除交易记录
|
||||||
|
const deleteTransaction = async (transaction_id: number) => {
|
||||||
|
try {
|
||||||
|
await adminRequest.delete(`api/transactions/${transaction_id}`)
|
||||||
|
init()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting transaction:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页码改变时的处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
state.page = page
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取交易记录
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.dialog-footer {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
122
ui/src/pages/admin/view2.vue
Normal file
122
ui/src/pages/admin/view2.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 销售情况按地区分布 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="salesByRegionOption" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 地区需求对比 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="demandByRegionOption" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 客户购买频次分析 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="mapOption" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import { BarChart, PieChart, LineChart } from 'echarts/charts'
|
||||||
|
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
|
||||||
|
// 引入并使用图表组件
|
||||||
|
use([CanvasRenderer, BarChart, PieChart, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
|
||||||
|
|
||||||
|
// 数据模拟:各地区销售、需求、频次等数据
|
||||||
|
const regionSalesData = ref([
|
||||||
|
{ region: '华北', sales: 1500, demand: 2000, frequency: 30 },
|
||||||
|
{ region: '华东', sales: 2500, demand: 3000, frequency: 40 },
|
||||||
|
{ region: '华南', sales: 1200, demand: 1500, frequency: 25 },
|
||||||
|
{ region: '西南', sales: 800, demand: 1000, frequency: 15 },
|
||||||
|
{ region: '西北', sales: 600, demand: 900, frequency: 10 },
|
||||||
|
{ region: '东北', sales: 900, demand: 1100, frequency: 20 },
|
||||||
|
{ region: '华中', sales: 1800, demand: 2200, frequency: 35 },
|
||||||
|
{ region: '西部', sales: 700, demand: 900, frequency: 18 },
|
||||||
|
{ region: '东南', sales: 2200, demand: 2700, frequency: 50 }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 销售情况按地区分布(柱状图)
|
||||||
|
const salesByRegionOption = ref({
|
||||||
|
title: { text: '各地区销售情况' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['销售量'] },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: regionSalesData.value.map(item => item.region)
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '销售量',
|
||||||
|
type: 'bar',
|
||||||
|
data: regionSalesData.value.map(item => item.sales)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 地区需求对比(折线图)
|
||||||
|
const demandByRegionOption = ref({
|
||||||
|
title: { text: '各地区需求对比' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['需求量'] },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: regionSalesData.value.map(item => item.region)
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '需求量',
|
||||||
|
type: 'line',
|
||||||
|
data: regionSalesData.value.map(item => item.demand)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 客户购买频次分析(饼图)
|
||||||
|
const purchaseFrequencyOption = ref({
|
||||||
|
title: {
|
||||||
|
text: '各地区客户购买频次分析',
|
||||||
|
left: 'center' // 将标题居中
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
|
||||||
|
legend: {
|
||||||
|
orient: 'vertical',
|
||||||
|
left: 'left',
|
||||||
|
data: regionSalesData.value.map(item => item.region)
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '购买频次',
|
||||||
|
type: 'pie',
|
||||||
|
radius: '55%',
|
||||||
|
center: ['50%', '60%'],
|
||||||
|
data: regionSalesData.value.map(item => ({ value: item.frequency, name: item.region })),
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
173
ui/src/pages/admin/view3.vue
Normal file
173
ui/src/pages/admin/view3.vue
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 销售热力图 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="heatmapOption" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<div style="height: 20px"></div>
|
||||||
|
<!-- 产品销售表现雷达图 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="radarOption" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import { HeatmapChart, RadarChart } from 'echarts/charts'
|
||||||
|
import { TitleComponent, TooltipComponent, GridComponent, VisualMapComponent } from 'echarts/components'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
|
||||||
|
// 引入并使用图表组件
|
||||||
|
use([CanvasRenderer, HeatmapChart, RadarChart, TitleComponent, TooltipComponent, GridComponent, VisualMapComponent])
|
||||||
|
|
||||||
|
// 数据模拟:液化气瓶的销售数据
|
||||||
|
const productSalesData = ref([
|
||||||
|
{ type: '液化气瓶A', specification: '5L', quality: '优质', sales: 200 },
|
||||||
|
{ type: '液化气瓶A', specification: '10L', quality: '优质', sales: 150 },
|
||||||
|
{ type: '液化气瓶A', specification: '20L', quality: '中等', sales: 180 },
|
||||||
|
{ type: '液化气瓶B', specification: '5L', quality: '良好', sales: 130 },
|
||||||
|
{ type: '液化气瓶B', specification: '10L', quality: '中等', sales: 220 },
|
||||||
|
{ type: '液化气瓶B', specification: '20L', quality: '优质', sales: 250 },
|
||||||
|
{ type: '液化气瓶C', specification: '5L', quality: '优质', sales: 190 },
|
||||||
|
{ type: '液化气瓶C', specification: '10L', quality: '良好', sales: 160 },
|
||||||
|
{ type: '液化气瓶C', specification: '20L', quality: '中等', sales: 200 },
|
||||||
|
{ type: '液化气瓶D', specification: '5L', quality: '中等', sales: 140 },
|
||||||
|
{ type: '液化气瓶D', specification: '10L', quality: '优质', sales: 270 },
|
||||||
|
{ type: '液化气瓶D', specification: '20L', quality: '良好', sales: 230 },
|
||||||
|
{ type: '液化气瓶E', specification: '5L', quality: '良好', sales: 110 },
|
||||||
|
{ type: '液化气瓶E', specification: '10L', quality: '中等', sales: 130 },
|
||||||
|
{ type: '液化气瓶E', specification: '20L', quality: '优质', sales: 220 },
|
||||||
|
{ type: '液化气瓶F', specification: '5L', quality: '中等', sales: 180 },
|
||||||
|
{ type: '液化气瓶F', specification: '10L', quality: '良好', sales: 160 },
|
||||||
|
{ type: '液化气瓶F', specification: '20L', quality: '优质', sales: 200 },
|
||||||
|
{ type: '液化气瓶G', specification: '5L', quality: '优质', sales: 210 },
|
||||||
|
{ type: '液化气瓶G', specification: '10L', quality: '优质', sales: 230 },
|
||||||
|
{ type: '液化气瓶G', specification: '20L', quality: '中等', sales: 220 }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 销售热力图:展示不同规格和质量等级下的销售情况
|
||||||
|
const heatmapOption = ref({
|
||||||
|
title: { text: '销售热力图' },
|
||||||
|
tooltip: { position: 'top' },
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['液化气瓶A', '液化气瓶B', '液化气瓶C', '液化气瓶D', '液化气瓶E', '液化气瓶F', '液化气瓶G']
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: ['5L', '10L', '20L']
|
||||||
|
},
|
||||||
|
visualMap: {
|
||||||
|
min: 0,
|
||||||
|
max: 250, // 根据实际数据设置最大值
|
||||||
|
calculable: true,
|
||||||
|
orient: 'horizontal',
|
||||||
|
left: 'center',
|
||||||
|
inRange: {
|
||||||
|
color: ['#FFFFFF', '#FF0000'] // 从白色到红色的渐变色,表示从低到高的热力值
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '销售量',
|
||||||
|
type: 'heatmap',
|
||||||
|
data: [
|
||||||
|
[0, 0, 200], // [x, y, value] 表示对应的销售量数据
|
||||||
|
[0, 1, 150],
|
||||||
|
[0, 2, 180],
|
||||||
|
[1, 0, 130],
|
||||||
|
[1, 1, 220],
|
||||||
|
[1, 2, 250],
|
||||||
|
[2, 0, 190],
|
||||||
|
[2, 1, 160],
|
||||||
|
[2, 2, 200],
|
||||||
|
[3, 0, 140],
|
||||||
|
[3, 1, 210],
|
||||||
|
[3, 2, 180],
|
||||||
|
[4, 0, 170],
|
||||||
|
[4, 1, 200],
|
||||||
|
[4, 2, 230],
|
||||||
|
[5, 0, 120],
|
||||||
|
[5, 1, 180],
|
||||||
|
[5, 2, 160],
|
||||||
|
[6, 0, 250],
|
||||||
|
[6, 1, 170],
|
||||||
|
[6, 2, 190]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// 销售表现雷达图:展示各类型液化气瓶的销售表现
|
||||||
|
const radarOption = ref({
|
||||||
|
title: {
|
||||||
|
text: '液化气产品销售表现',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {},
|
||||||
|
radar: {
|
||||||
|
indicator: [
|
||||||
|
{ name: '液化气瓶A', max: 300 },
|
||||||
|
{ name: '液化气瓶B', max: 300 },
|
||||||
|
{ name: '液化气瓶C', max: 300 },
|
||||||
|
{ name: '液化气瓶D', max: 300 },
|
||||||
|
{ name: '液化气瓶E', max: 300 },
|
||||||
|
{ name: '液化气瓶F', max: 300 },
|
||||||
|
{ name: '液化气瓶G', max: 300 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
series: [{
|
||||||
|
name: '销售表现',
|
||||||
|
type: 'radar',
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶A').map(item => item.sales),
|
||||||
|
name: '液化气瓶A'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶B').map(item => item.sales),
|
||||||
|
name: '液化气瓶B'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶C').map(item => item.sales),
|
||||||
|
name: '液化气瓶C'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶D').map(item => item.sales),
|
||||||
|
name: '液化气瓶D'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶E').map(item => item.sales),
|
||||||
|
name: '液化气瓶E'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶F').map(item => item.sales),
|
||||||
|
name: '液化气瓶F'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: productSalesData.value.filter(item => item.type === '液化气瓶G').map(item => item.sales),
|
||||||
|
name: '液化气瓶G'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
123
ui/src/pages/admin/view4.vue
Normal file
123
ui/src/pages/admin/view4.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 销售额与毛利对比 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="salesMarginOption" ref="salesMarginChart" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 线上与线下收入对比 -->
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<v-chart class="chart" :option="revenueSourceOption" ref="revenueSourceChart" autoresize />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import VChart from 'vue-echarts'
|
||||||
|
import { LineChart, BarChart } from 'echarts/charts'
|
||||||
|
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent } from 'echarts/components'
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers'
|
||||||
|
import { use } from 'echarts/core'
|
||||||
|
|
||||||
|
// 引入并使用图表组件
|
||||||
|
use([CanvasRenderer, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent])
|
||||||
|
|
||||||
|
// 模拟数据
|
||||||
|
const salesData = ref([
|
||||||
|
{ date: '2024-01-01', salesAmount: 5000, profit: 1200, onlineRevenue: 2500, offlineRevenue: 2500 },
|
||||||
|
{ date: '2024-01-02', salesAmount: 6000, profit: 1500, onlineRevenue: 3000, offlineRevenue: 3000 },
|
||||||
|
{ date: '2024-01-03', salesAmount: 4000, profit: 1000, onlineRevenue: 2000, offlineRevenue: 2000 },
|
||||||
|
{ date: '2024-01-04', salesAmount: 7000, profit: 1800, onlineRevenue: 3500, offlineRevenue: 3500 },
|
||||||
|
{ date: '2024-01-05', salesAmount: 5500, profit: 1300, onlineRevenue: 2700, offlineRevenue: 2800 },
|
||||||
|
// 更多数据...
|
||||||
|
])
|
||||||
|
|
||||||
|
// 销售额与毛利对比(折线图和柱状图结合)
|
||||||
|
const salesMarginOption = ref({
|
||||||
|
title: { text: '销售额与毛利对比' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['销售额', '毛利'] },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: salesData.value.map(item => item.date)
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
feature: {
|
||||||
|
magicType: { show: true, type: ['line', 'bar'] },
|
||||||
|
saveAsImage: {
|
||||||
|
show: true,
|
||||||
|
title: '保存为图片',
|
||||||
|
type: 'png',
|
||||||
|
pixelRatio: 2, // 控制图片清晰度
|
||||||
|
backgroundColor: '#ffffff' // 背景色设置为白色,避免透明背景
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '销售额',
|
||||||
|
type: 'bar',
|
||||||
|
data: salesData.value.map(item => item.salesAmount),
|
||||||
|
itemStyle: { color: 'rgba(255, 127, 80, 0.6)' } // 显色不明显
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '毛利',
|
||||||
|
type: 'line',
|
||||||
|
data: salesData.value.map(item => item.profit),
|
||||||
|
itemStyle: { color: 'rgba(135, 206, 250, 0.6)' }, // 显色不明显
|
||||||
|
emphasis: { itemStyle: { color: '#87cefa' } }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 线上与线下收入对比(柱状图)
|
||||||
|
const revenueSourceOption = ref({
|
||||||
|
title: { text: '线上与线下收入对比' },
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['线上收入', '线下收入'] },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: salesData.value.map(item => item.date)
|
||||||
|
},
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
feature: {
|
||||||
|
magicType: { show: true, type: ['line', 'bar'] },
|
||||||
|
saveAsImage: {
|
||||||
|
show: true,
|
||||||
|
title: '保存为图片',
|
||||||
|
type: 'png',
|
||||||
|
pixelRatio: 2, // 控制图片清晰度
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '线上收入',
|
||||||
|
type: 'bar',
|
||||||
|
data: salesData.value.map(item => item.onlineRevenue),
|
||||||
|
itemStyle: { color: 'rgba(50, 205, 50, 0.6)' } // 显色不明显
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '线下收入',
|
||||||
|
type: 'bar',
|
||||||
|
data: salesData.value.map(item => item.offlineRevenue),
|
||||||
|
itemStyle: { color: 'rgba(255, 99, 71, 0.6)' } // 显色不明显
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chart {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
</style>
|
37
ui/src/pages/index.vue
Normal file
37
ui/src/pages/index.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<!--轮播图-->
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="h-300px">
|
||||||
|
<carousel></carousel>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<div class="h-300px">
|
||||||
|
<carousel></carousel>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 推荐商品列表-->
|
||||||
|
<item></item>
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import Carousel from '~/components/front/carousel.vue'
|
||||||
|
import Item from '~/components/front/item.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "front"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
225
ui/src/pages/login.vue
Normal file
225
ui/src/pages/login.vue
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="module">
|
||||||
|
<img src="/loginimg.jpg" class="module_img" />
|
||||||
|
<div class="module_r">
|
||||||
|
<div class="module_mian">
|
||||||
|
<div class="module_title">登录帐户</div>
|
||||||
|
<div class="module_desc">输入用户名 & 登录密码</div>
|
||||||
|
<div class="module_m">
|
||||||
|
<div class="module_text">用户名</div>
|
||||||
|
<input class="module_input" type="text" placeholder="输入用户名" v-model="login.username" />
|
||||||
|
</div>
|
||||||
|
<div class="module_m">
|
||||||
|
<div class="module_text">密码</div>
|
||||||
|
<input class="module_input" type="password" placeholder="输入密码" v-model="login.password" />
|
||||||
|
</div>
|
||||||
|
<!-- <div class="module_m">-->
|
||||||
|
<!-- <div class="module_text">验证码</div>-->
|
||||||
|
<!-- <div class="module_code">-->
|
||||||
|
<!-- <input class="module_code_input" type="text" placeholder="输入验证码" v-model="login.captcha" />-->
|
||||||
|
<!-- <img class="module_code_img" :src="state.captchaUrl" @click="getCaptcha">-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="module_m">-->
|
||||||
|
<!-- <div class="module_code">-->
|
||||||
|
<!-- <el-radio-group v-model="state.role" class="ml-4">-->
|
||||||
|
<!-- <el-radio :label=false size="large">普通用户</el-radio>-->
|
||||||
|
<!-- <el-radio :label=true size="large">管理员</el-radio>-->
|
||||||
|
<!-- </el-radio-group>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="module_radio"><input type="radio"/>记住密码 </div>-->
|
||||||
|
<div class="forgetpwd" @click="router.push('/register')">没有密码吗?</div>
|
||||||
|
<button class="module_button" :disabled="state.loading" @click="onLogin">登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { loginAdmin } from '~/api/user/adminUserApi'
|
||||||
|
import { getUuid } from '~/utils/utils'
|
||||||
|
import { loginFront } from '~/api/user/frontUserApi'
|
||||||
|
const router = useRouter()
|
||||||
|
const state = reactive({
|
||||||
|
role: true,
|
||||||
|
captchaUrl: '',
|
||||||
|
loginFrom: {},
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
const login = reactive({ username: '', password: '', captcha: '', uuid: '' })
|
||||||
|
const onLogin = () => {
|
||||||
|
state.loading = true
|
||||||
|
if (state.role) {
|
||||||
|
console.log("管理员")
|
||||||
|
loginAdmin(login).then(response => {
|
||||||
|
state.loading = false
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
userStore().isLogin = true
|
||||||
|
userStore().adminToken = response.data.token
|
||||||
|
router.push('/admin')
|
||||||
|
}).catch(() => {
|
||||||
|
state.loading = false
|
||||||
|
onRefreshCode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
loginFront(login)
|
||||||
|
toast.success("登录成功~")
|
||||||
|
router.push('/front')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 获取验证码
|
||||||
|
*/
|
||||||
|
const getCaptchaUrl = () => {
|
||||||
|
login.uuid = getUuid()
|
||||||
|
login.captcha = ''
|
||||||
|
state.captchaUrl = import.meta.env.VITE_ADMIN_API_BASE_URL + `/captcha?uuid=${login.uuid}`
|
||||||
|
}
|
||||||
|
const onRefreshCode = () => {
|
||||||
|
getCaptchaUrl()
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
getCaptchaUrl()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
background: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_img {
|
||||||
|
width: 60%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_r {
|
||||||
|
width: 40%;
|
||||||
|
background: #e5efee;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.module_mian {
|
||||||
|
width: 65%;
|
||||||
|
background: #FFFFFF;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
|
||||||
|
.module_title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_desc {
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #a7a7a7;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_m {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 80%;
|
||||||
|
height: auto;
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
.module_text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_input {
|
||||||
|
width: 96%;
|
||||||
|
height: 40px;
|
||||||
|
padding-left: 2%;
|
||||||
|
padding-right: 2%;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_code {
|
||||||
|
width: 96%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.module_code_input {
|
||||||
|
width: 60%;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-left: 2%;
|
||||||
|
padding-right: 2%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_code_img {
|
||||||
|
width: 130px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_radio input {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forgetpwd {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 80%;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #328d86;
|
||||||
|
margin-top: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_button {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
width: 80%;
|
||||||
|
background: #328d86;
|
||||||
|
color: #FFFFFF;
|
||||||
|
height: 40px;
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module_button:active {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<route lang="json">
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"layout": "notFound"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</route>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user