上传全量
This commit is contained in:
parent
aeda78b8dc
commit
f007460b1e
@ -6,7 +6,7 @@ from . import *
|
||||
from .views import blus
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__, static_folder='D:\\2025\\fraud-detection-ml\\uploads')
|
||||
app = Flask(__name__)
|
||||
# 注册蓝图
|
||||
app.register_blueprint(blueprint=blus)
|
||||
# MySQL所在主机名,默认127.0.0.1
|
||||
|
@ -52,8 +52,9 @@ class FinancialTransaction(db.Model):
|
||||
browser_info = db.Column(db.String(255), nullable=True)
|
||||
mobile = db.Column(db.Integer, nullable=True)
|
||||
is_fraud = db.Column(db.Boolean, nullable=False)
|
||||
status = 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):
|
||||
browser_info, is_fraud,status):
|
||||
self.user_name = user_name
|
||||
self.transaction_amount = transaction_amount
|
||||
self.transaction_time = transaction_time
|
||||
@ -64,10 +65,12 @@ class FinancialTransaction(db.Model):
|
||||
self.is_fraud = is_fraud
|
||||
self.transaction_status = transaction_status
|
||||
self.mobile = mobile
|
||||
self.status = status
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'transaction_id': self.transaction_id,
|
||||
'status': self.status,
|
||||
'user_name': self.user_name,
|
||||
'transaction_amount': self.transaction_amount,
|
||||
'transaction_time': self.transaction_time.strftime('%Y-%d-%m %H:%M:%S'),
|
||||
|
88
App/views.py
88
App/views.py
@ -7,14 +7,16 @@ from flask import Blueprint
|
||||
import hashlib
|
||||
from math import ceil
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
from .utils.api_utils import APIUtils
|
||||
from .models import *
|
||||
blus = Blueprint("user", __name__)
|
||||
db_config = {
|
||||
'host': 'localhost',
|
||||
'host': '192.168.15.2',
|
||||
'user': 'root',
|
||||
'password': '123456',
|
||||
'database': 'job',
|
||||
'password': 'minxianrui',
|
||||
'database': 'fraud_detection_ml',
|
||||
'charset': 'utf8mb4'
|
||||
}
|
||||
# 注册
|
||||
@ -112,14 +114,11 @@ def get_users():
|
||||
|
||||
# 获取 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)
|
||||
|
||||
@ -150,42 +149,10 @@ def get_users():
|
||||
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'],
|
||||
@ -196,7 +163,6 @@ def add_transaction():
|
||||
browser_info=data.get('browser_info', ''),
|
||||
is_fraud=data['is_fraud']
|
||||
)
|
||||
|
||||
db.session.add(new_transaction)
|
||||
db.session.commit()
|
||||
|
||||
@ -209,16 +175,27 @@ def get_transactions():
|
||||
page = request.args.get('page', 1, type=int) # 默认第一页
|
||||
page_size = request.args.get('page_size', 10, type=int) # 默认每页10条
|
||||
|
||||
query = FinancialTransaction.query
|
||||
transactionStatus = request.args.get('transactionStatus')
|
||||
status = request.args.get('status')
|
||||
|
||||
|
||||
if transactionStatus and status:
|
||||
query = query.filter(and_(
|
||||
FinancialTransaction.is_fraud.like(f'%{transactionStatus}%'),
|
||||
FinancialTransaction.status.like(f'%{status}%')
|
||||
))
|
||||
elif transactionStatus:
|
||||
query = query.filter(FinancialTransaction.is_fraud.like(f'%{transactionStatus}%'))
|
||||
elif status:
|
||||
query = query.filter(FinancialTransaction.status.like(f'%{status}%'))
|
||||
# 计算分页偏移量
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 查询交易记录,使用 limit 和 offset 实现分页
|
||||
transactions = FinancialTransaction.query.offset(offset).limit(page_size).all()
|
||||
|
||||
transactions = query.offset(offset).limit(page_size).all()
|
||||
# 获取总记录数,用于计算总页数
|
||||
total_count = FinancialTransaction.query.count()
|
||||
total_count = query.count()
|
||||
total_pages = ceil(total_count / page_size)
|
||||
|
||||
# 构建响应数据,包括分页信息
|
||||
response = {
|
||||
'data': [transaction.to_dict() for transaction in transactions],
|
||||
@ -229,15 +206,11 @@ def get_transactions():
|
||||
"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):
|
||||
@ -252,12 +225,9 @@ def get_transaction(transaction_id):
|
||||
@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)
|
||||
@ -266,12 +236,23 @@ def update_transaction(transaction_id):
|
||||
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/utransactions/<int:transaction_id>', methods=['PUT'])
|
||||
def update_transaction1(transaction_id):
|
||||
transaction = FinancialTransaction.query.get(transaction_id)
|
||||
|
||||
print(transaction_id)
|
||||
transaction.status = 1
|
||||
db.session.commit()
|
||||
return jsonify(transaction.to_dict())
|
||||
|
||||
|
||||
|
||||
# 删:删除交易记录
|
||||
@blus.route('/api/transactions/<int:transaction_id>', methods=['DELETE'])
|
||||
def delete_transaction(transaction_id):
|
||||
@ -289,7 +270,6 @@ def delete_transaction(transaction_id):
|
||||
@blus.route('/api/mysql', methods=['POST'])
|
||||
def mysql():
|
||||
data = request.get_json()
|
||||
|
||||
# 检查 SQL 参数是否存在
|
||||
if not data['sql']:
|
||||
return APIUtils.error_response(message="没有sql参数")
|
||||
|
158
data/model.py
Normal file
158
data/model.py
Normal file
@ -0,0 +1,158 @@
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from sklearn.linear_model import LogisticRegression # 逻辑回归
|
||||
from sklearn.tree import DecisionTreeClassifier # 决策树
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
|
||||
import joblib
|
||||
|
||||
# pandas的显示设置来增加可以显示的列数
|
||||
pd.set_option('display.max_columns', None)
|
||||
# 读取数据
|
||||
data = pd.read_csv('creditcard.csv')
|
||||
# 查看默认的前5行数据
|
||||
data.head(5)
|
||||
|
||||
# 查看数据的信息
|
||||
print(data.shape)
|
||||
data.info()
|
||||
# 查看数据的描述
|
||||
data.describe()
|
||||
# 检查是否有空置
|
||||
data.isnull().sum()
|
||||
# 查看没类的个数
|
||||
data['Class'].value_counts()
|
||||
# 划分特征和标签
|
||||
X = data.drop('Class', axis=1)
|
||||
y = data['Class']
|
||||
# 划分训练集和测试集
|
||||
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
||||
# 特征标准化
|
||||
scaler = StandardScaler()
|
||||
X_train = scaler.fit_transform(X_train)
|
||||
X_test = scaler.transform(X_test)
|
||||
print("原始数据训练-----")
|
||||
# 使用逻辑回归算法进行训练和评估
|
||||
print("\nLogistic Regression:")
|
||||
# 初始化逻辑回归模型
|
||||
model = LogisticRegression(max_iter=1000, random_state=42)
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
# 打印性能指标
|
||||
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
|
||||
print(f'Precision: {precision_score(y_test, y_pred)}')
|
||||
print(f'Recall: {recall_score(y_test, y_pred)}')
|
||||
print(f'F1 Score: {f1_score(y_test, y_pred)}')
|
||||
|
||||
print("\nDecisionTreeClassifier:")
|
||||
# 初始化决策树模型
|
||||
model = DecisionTreeClassifier()
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
# 打印性能指标
|
||||
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
|
||||
print(f'Precision: {precision_score(y_test, y_pred)}')
|
||||
print(f'Recall: {recall_score(y_test, y_pred)}')
|
||||
print(f'F1 Score: {f1_score(y_test, y_pred)}')
|
||||
|
||||
from imblearn.under_sampling import RandomUnderSampler
|
||||
|
||||
print("下采样以平衡数据")
|
||||
rus = RandomUnderSampler(random_state=42)
|
||||
X_res, y_res = rus.fit_resample(X, y)
|
||||
|
||||
# 分割数据为训练集和测试集
|
||||
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res, test_size=0.2, random_state=42)
|
||||
|
||||
# 特征缩放
|
||||
scaler = StandardScaler()
|
||||
X_train = scaler.fit_transform(X_train)
|
||||
X_test = scaler.transform(X_test)
|
||||
|
||||
# 使用逻辑回归算法进行训练和评估
|
||||
print("\nLogistic Regression:")
|
||||
# 初始化逻辑回归模型
|
||||
model = LogisticRegression(max_iter=1000, random_state=42)
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
# 打印性能指标
|
||||
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
|
||||
print(f'Precision: {precision_score(y_test, y_pred)}')
|
||||
print(f'Recall: {recall_score(y_test, y_pred)}')
|
||||
print(f'F1 Score: {f1_score(y_test, y_pred)}')
|
||||
|
||||
print("\nDecisionTreeClassifier:")
|
||||
# 初始化决策树模型
|
||||
model = DecisionTreeClassifier()
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
# 打印性能指标
|
||||
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
|
||||
print(f'Precision: {precision_score(y_test, y_pred)}')
|
||||
print(f'Recall: {recall_score(y_test, y_pred)}')
|
||||
print(f'F1 Score: {f1_score(y_test, y_pred)}')
|
||||
|
||||
from imblearn.over_sampling import SMOTE
|
||||
|
||||
print("上采样以平衡数据")
|
||||
smote = SMOTE(random_state=42)
|
||||
X_res, y_res = smote.fit_resample(X, y)
|
||||
# 分割数据为训练集和测试集
|
||||
X_train, X_test, y_train, y_test = train_test_split(X_res, y_res,
|
||||
test_size=0.2, random_state=42)
|
||||
# 特征缩放
|
||||
scaler = StandardScaler()
|
||||
X_train = scaler.fit_transform(X_train)
|
||||
X_test = scaler.transform(X_test)
|
||||
|
||||
# 模型调优
|
||||
print("\nDecisionTreeClassifier:")
|
||||
# 初始化决策树模型
|
||||
model = DecisionTreeClassifier()
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
# 打印性能指标
|
||||
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
|
||||
print(f'Precision: {precision_score(y_test, y_pred)}')
|
||||
print(f'Recall: {recall_score(y_test, y_pred)}')
|
||||
print(f'F1 Score: {f1_score(y_test, y_pred)}')
|
||||
|
||||
# 使用逻辑回归算法进行训练和评估
|
||||
print("\nLogistic Regression:")
|
||||
# 初始化逻辑回归模型
|
||||
model = LogisticRegression(max_iter=1000, random_state=42)
|
||||
model.fit(X_train, y_train)
|
||||
y_pred = model.predict(X_test)
|
||||
# 打印性能指标
|
||||
print(f'Accuracy: {accuracy_score(y_test, y_pred)}')
|
||||
print(f'Precision: {precision_score(y_test, y_pred)}')
|
||||
print(f'Recall: {recall_score(y_test, y_pred)}')
|
||||
print(f'F1 Score: {f1_score(y_test, y_pred)}')
|
||||
from sklearn.model_selection import learning_curve
|
||||
# 获取学习曲线数据
|
||||
train_sizes, train_scores, test_scores = learning_curve(model, X, y, cv=5, n_jobs=-1,
|
||||
train_sizes=np.linspace(0.1, 1.0, 5))
|
||||
# 计算训练和测试分数的平均值与标准差
|
||||
train_scores_mean = np.mean(train_scores, axis=1)
|
||||
train_scores_std = np.std(train_scores, axis=1)
|
||||
test_scores_mean = np.mean(test_scores, axis=1)
|
||||
test_scores_std = np.std(test_scores, axis=1)
|
||||
|
||||
# 绘制学习曲线
|
||||
plt.figure()
|
||||
plt.title("Learning Curve")
|
||||
plt.xlabel("Training Examples")
|
||||
plt.ylabel("Score")
|
||||
plt.grid()
|
||||
plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="Training Score")
|
||||
plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="Cross-validation Score")
|
||||
|
||||
plt.legend(loc="best")
|
||||
plt.show()
|
||||
|
||||
# 加载逻辑回归模型
|
||||
logistic_model = joblib.load('logistic_regression_model.pkl')
|
||||
# 加载决策树模型
|
||||
decision_tree_model = joblib.load('decision_tree_model.pkl')
|
@ -78,10 +78,8 @@ const state = reactive({
|
||||
* 退出登录
|
||||
*/
|
||||
const logout = () => {
|
||||
logoutAdmin().then(()=>{
|
||||
toast.success("退出成功~")
|
||||
router.push('/login');
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,9 @@
|
||||
<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="姓名" />
|
||||
@ -10,15 +12,15 @@
|
||||
<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 prop="ip_address" label="IP地址" width="180" />
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button @click="editTransaction(scope.row)" size="small">编辑</el-button>
|
||||
<!-- <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"
|
||||
@ -28,8 +30,9 @@
|
||||
layout="total, prev, pager, jumper"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog v-model="state.dialogVisible" title="新增交易记录" width="50%">
|
||||
<el-dialog v-model="state.dialogVisible" :title="formData.transaction_id ? '编辑交易记录' : '新增交易记录'" 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" />
|
||||
@ -38,7 +41,12 @@
|
||||
<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-date-picker
|
||||
v-model="formData.transaction_time"
|
||||
type="datetime"
|
||||
placeholder="选择交易时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</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="否" />
|
||||
@ -54,6 +62,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// 交易记录数据
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
@ -69,31 +79,34 @@ const formData = reactive({
|
||||
user_id: '',
|
||||
transaction_amount: '',
|
||||
transaction_time: '',
|
||||
is_fraud: false
|
||||
is_fraud: false,
|
||||
})
|
||||
|
||||
// 获取交易记录
|
||||
const init = () => {
|
||||
adminRequest.get('/api/transactions', {
|
||||
const init = async () => {
|
||||
try {
|
||||
const res = await 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
|
||||
})
|
||||
} catch (error) {
|
||||
ElMessage.error('获取交易记录失败')
|
||||
console.error('Error fetching transactions:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增交易记录
|
||||
// 打开新增对话框
|
||||
const openAddDialog = () => {
|
||||
ElMessage.success("同步成功!")
|
||||
// formData.transaction_id = null
|
||||
// formData.user_id = ''
|
||||
// formData.transaction_amount = ''
|
||||
// formData.transaction_time = ''
|
||||
// formData.is_fraud = false
|
||||
// state.dialogVisible = true
|
||||
formData.transaction_id = null
|
||||
formData.user_id = ''
|
||||
formData.transaction_amount = ''
|
||||
formData.transaction_time = ''
|
||||
formData.is_fraud = false
|
||||
state.dialogVisible = true
|
||||
}
|
||||
|
||||
// 编辑交易记录
|
||||
@ -108,23 +121,35 @@ const editTransaction = (row: any) => {
|
||||
|
||||
// 保存交易记录
|
||||
const saveTransaction = async () => {
|
||||
try {
|
||||
if (formData.transaction_id) {
|
||||
// 更新交易记录
|
||||
await adminRequest.put(`/api/transactions/${formData.transaction_id}`, formData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
// 新增交易记录
|
||||
await adminRequest.post('api/transactions', formData)
|
||||
await adminRequest.post('/api/transactions', formData)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
init()
|
||||
state.dialogVisible = false
|
||||
init() // 重新加载数据
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
console.error('Error saving transaction:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除交易记录
|
||||
const deleteTransaction = async (transaction_id: number) => {
|
||||
try {
|
||||
await adminRequest.delete(`api/transactions/${transaction_id}`)
|
||||
init()
|
||||
await ElMessageBox.confirm('确定删除该交易记录吗?', '提示', {
|
||||
type: 'warning',
|
||||
})
|
||||
await adminRequest.delete(`/api/transactions/${transaction_id}`)
|
||||
ElMessage.success('删除成功')
|
||||
init() // 重新加载数据
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
console.error('Error deleting transaction:', error)
|
||||
}
|
||||
}
|
||||
@ -140,6 +165,7 @@ onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
99
ui/src/pages/admin/view.vue
Normal file
99
ui/src/pages/admin/view.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-row v-loading="loading">
|
||||
<!-- 折线图 -->
|
||||
<el-col :span="12">
|
||||
<v-chart class="chart" :option="demandByRegionOption" autoresize />
|
||||
</el-col>
|
||||
<!-- 饼图 -->
|
||||
<el-col :span="12">
|
||||
<v-chart class="chart" :option="pieChartOption" autoresize />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } 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 demandByRegionOption = ref({
|
||||
title: { text: '交易地区占比(折线图)' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['交易量'] },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: [] // 初始为空,等待接口数据
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '交易量',
|
||||
type: 'line',
|
||||
data: [] // 初始为空,等待接口数据
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 定义饼图配置
|
||||
const pieChartOption = ref({
|
||||
title: { text: '交易地区占比(饼图)' },
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: '10%', left: 'center' },
|
||||
series: [
|
||||
{
|
||||
name: '交易量',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: [], // 初始为空,等待接口数据
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(true)
|
||||
|
||||
// 在 onMounted 中获取数据并更新图表
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 调用接口获取数据
|
||||
const response = await adminRequest.post('/api/mysql', {
|
||||
sql: "SELECT transaction_location name, count(*) value FROM fraud_detection_ml.tb_financial_transactions GROUP BY transaction_location"
|
||||
})
|
||||
|
||||
// 更新折线图配置
|
||||
demandByRegionOption.value.xAxis.data = response.map(item => item.name)
|
||||
demandByRegionOption.value.series[0].data = response.map(item => item.value)
|
||||
|
||||
// 更新饼图配置
|
||||
pieChartOption.value.series[0].data = response.map(item => ({
|
||||
name: item.name,
|
||||
value: item.value
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false // 关闭加载状态
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart {
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
@ -1,122 +0,0 @@
|
||||
<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>
|
@ -1,173 +0,0 @@
|
||||
<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>
|
@ -1,15 +1,22 @@
|
||||
<template>
|
||||
<!-- 销售额与毛利对比 -->
|
||||
<!-- 销售情况按地区分布 -->
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<v-chart class="chart" :option="salesMarginOption" ref="salesMarginChart" autoresize />
|
||||
<v-chart class="chart" :option="salesByRegionOption" autoresize />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 线上与线下收入对比 -->
|
||||
<!-- 地区需求对比 -->
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<v-chart class="chart" :option="revenueSourceOption" ref="revenueSourceChart" autoresize />
|
||||
<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>
|
||||
@ -17,100 +24,92 @@
|
||||
<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 { 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, LineChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent, ToolboxComponent])
|
||||
use([CanvasRenderer, BarChart, PieChart, LineChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
|
||||
|
||||
// 模拟数据
|
||||
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 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 salesMarginOption = ref({
|
||||
title: { text: '销售额与毛利对比' },
|
||||
// 销售情况按地区分布(柱状图)
|
||||
const salesByRegionOption = ref({
|
||||
title: { text: '各地区销售情况' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['销售额', '毛利'] },
|
||||
legend: { data: ['销售量'] },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: salesData.value.map(item => item.date)
|
||||
data: regionSalesData.value.map(item => item.region)
|
||||
},
|
||||
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: '销售额',
|
||||
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' } }
|
||||
data: regionSalesData.value.map(item => item.sales)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 线上与线下收入对比(柱状图)
|
||||
const revenueSourceOption = ref({
|
||||
title: { text: '线上与线下收入对比' },
|
||||
// 地区需求对比(折线图)
|
||||
const demandByRegionOption = ref({
|
||||
title: { text: '各地区需求对比' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: ['线上收入', '线下收入'] },
|
||||
legend: { data: ['需求量'] },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: salesData.value.map(item => item.date)
|
||||
data: regionSalesData.value.map(item => item.region)
|
||||
},
|
||||
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: '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: '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)' } // 显色不明显
|
||||
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)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
188
ui/src/pages/admin/waring.vue
Normal file
188
ui/src/pages/admin/waring.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="fraud-detection-container">
|
||||
<!-- 标题 -->
|
||||
<h2>欺诈检测预警系统</h2>
|
||||
<!-- 筛选条件 -->
|
||||
<el-row :gutter="20" class="filter-row">
|
||||
|
||||
<el-col :span="6">
|
||||
<el-select v-model="state.status" placeholder="交易状态">
|
||||
<el-option label="未处理" :value=0 />
|
||||
<el-option label="已处理" :value=1 />
|
||||
</el-select>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="6">
|
||||
<el-button type="primary" @click="fetchData">筛选</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
|
||||
<el-row :gutter="20" class="chart-row">
|
||||
<!-- <el-col :span="24">-->
|
||||
<!-- <v-chart class="chart" :option="pieChartOption" autoresize />-->
|
||||
<!-- </el-col>-->
|
||||
<el-col :span="24">
|
||||
<v-chart class="chart" :option="barChartOption" autoresize />
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="state.getList" v-loading="loading">
|
||||
<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="status" label="处理状态" >
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status" type="success">已处理</el-tag>
|
||||
<el-tag v-else type="warning">未处理</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.status ==0" size="small" @click="markAsProcessed(scope.row)">标记为已处理</el-button>
|
||||
<!-- <el-button size="small" type="danger" @click="deleteTransaction(scope.row)">删除</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"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, reactive} from 'vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { PieChart, BarChart } from 'echarts/charts'
|
||||
import { TitleComponent, TooltipComponent, LegendComponent, GridComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { use } from 'echarts/core'
|
||||
import {ElMessage} from "element-plus";
|
||||
// 引入并使用图表组件
|
||||
use([CanvasRenderer, PieChart, BarChart, TitleComponent, TooltipComponent, LegendComponent, GridComponent])
|
||||
|
||||
const state = reactive({
|
||||
dialogVisible: false,
|
||||
getList: [],
|
||||
totalCount: 0, // 总记录数
|
||||
page: 1, // 当前页码
|
||||
pageSize: 10, // 每页显示的记录数
|
||||
transactionStatus:1,
|
||||
status:1,
|
||||
})
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
const res = await adminRequest.get('/api/transactions', {
|
||||
params: {
|
||||
page: state.page,
|
||||
page_size: state.pageSize,
|
||||
transactionStatus: state.transactionStatus,
|
||||
status: state.status,
|
||||
},
|
||||
})
|
||||
state.getList = res.data.data
|
||||
state.totalCount = res.data.page.total_count
|
||||
} catch (error) {
|
||||
ElMessage.error('获取交易记录失败')
|
||||
console.error('Error fetching transactions:', error)
|
||||
}
|
||||
}
|
||||
const loading = ref(false)
|
||||
|
||||
|
||||
// 柱状图配置
|
||||
const barChartOption = ref({
|
||||
title: { text: '已欺诈交易趋势' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: []
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '欺诈交易',
|
||||
type: 'bar',
|
||||
data: []
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
init()
|
||||
}
|
||||
|
||||
// 标记为已处理
|
||||
const markAsProcessed = async (row) => {
|
||||
try {
|
||||
await adminRequest.put(`/api/utransactions/${row.transaction_id}`)
|
||||
init()
|
||||
ElMessage.success("修改成功")
|
||||
} catch (error) {
|
||||
console.error('标记失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页码改变时的处理函数
|
||||
const handlePageChange = (page: number) => {
|
||||
state.page = page
|
||||
init()
|
||||
}
|
||||
// 初始化数据
|
||||
onMounted(async () => {
|
||||
init()
|
||||
fetchData()
|
||||
try {
|
||||
// 调用接口获取数据
|
||||
const response = await adminRequest.post('/api/mysql', {
|
||||
sql: "SELECT transaction_location name, count(*) value FROM fraud_detection_ml.tb_financial_transactions GROUP BY transaction_location"
|
||||
})
|
||||
// 更新折线图配置
|
||||
barChartOption.value.xAxis.data = response.map(item => item.name)
|
||||
barChartOption.value.series[0].data = response.map(item => item.value)
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false // 关闭加载状态
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fraud-detection-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
@ -1,6 +1,5 @@
|
||||
<template>
|
||||
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
@ -12,5 +11,4 @@ onMounted(()=>{
|
||||
router.push("/admin")
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
@ -17,20 +17,21 @@ export const getAdminList = () => {
|
||||
"icon": "House",
|
||||
},
|
||||
{
|
||||
"path": "/admin/view1",
|
||||
"name": "交易记录管理",
|
||||
"path": "/admin/data",
|
||||
"name": "数据管理",
|
||||
"icon": "DataAnalysis",
|
||||
},
|
||||
{
|
||||
"path": "/admin/view2",
|
||||
"path": "/admin/waring",
|
||||
"name": "欺诈检测预警",
|
||||
"icon": "DataAnalysis",
|
||||
},
|
||||
{
|
||||
"path": "/admin/view3",
|
||||
"name": "数据管理",
|
||||
"path": "/admin/view",
|
||||
"name": "交易数据分析",
|
||||
"icon": "DataAnalysis",
|
||||
},
|
||||
|
||||
]
|
||||
return routes;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user