Hippo登录展示功能
1. 后台实现登录
1.1 Xadmin
1.11安装
https://github.com/sshwsfc/xadmin.git #django2
https://github.com/zgljl2012/xadmin-x #django3
pip install xadmin-x
在配置文件中注册如下应用
INSTALLED_APPS = [
'xadmin',
'crispy_forms',
'reversion',
]
# 修改使用中文界面
LANGUAGE_CODE = 'zh-Hans'
# 修改时区
TIME_ZONE = 'Asia/Shanghai'
USE_TZ = False
xadmin有建立自己的数据库模型类,需要进行数据库迁移
python manage.py makemigrations
python manage.py migrate
在总路由中添加xadmin的路由信息
import xadmin
xadmin.autodiscover()
# version模块自动注册需要版本控制的 Model
from xadmin.plugins import xversion
xversion.register_models()
urlpatterns = [
path(r'xadmin/', xadmin.site.urls),
]
如果之前没有创建超级用户,需要创建,如果有了,则可以直接使用之前的。
python manage.py createsuperuser
1.11 给admin配置基本站点
在当前子应用中创建adminx.py,添加如下代码
import xadmin
from xadmin import views
class BaseSetting(object):
"""xadmin的基本配置"""
enable_themes = True # 开启主题切换功能
use_bootswatch = True
xadmin.site.register(views.BaseAdminView, BaseSetting)
class GlobalSettings(object):
"""xadmin的全局配置"""
site_title = "hippo" # 设置站点标题
site_footer = "@追梦nan-2020" # 设置站点的页脚
menu_style = "accordion" # 设置菜单折叠
xadmin.site.register(views.CommAdminView, GlobalSettings)
1.12 注册模型到xadmin中
在xadmin中配置信息
# 轮播图
from .models import BannerInfo
class BannerInfoModelAdmin(object):
list_display=["name","orders","is_show"]
xadmin.site.register(BannerInfo, BannerInfoModelAdmin)
list_display #控制列表展示的字段
search_fields # 控制可以通过搜索框搜索的字段名称,xadmin使用的是模糊查询
list_filter #可以进行过滤操作的列,对于分类、性别、状态
ordering #默认排序的字段
show_detail_fields #在列表页提供快速显示详情信息
list_editable #在列表页可以快速直接编辑的字段
refresh_times #指定列表页的定时刷新
list_export #控制列表页导出数据的可选格式
show_bookmarks #控制是否显示书签功能
data_charts #控制显示图表的样式
data_charts = { #控制显示图表的样式
"order_amount": { #随便写的名称order_amount
'title': '图书发布日期表', #控制图标名称
"x-field": "bpub_date", #控制x轴字段
"y-field": ('btitle',), #控制y轴字段,可以是多个值
"order": ('id',), # 控制默认排序
},
model_icon #控制菜单的图标
readonly_fields #在编辑页面的只读字段
exclude # 在编辑页面隐藏的字段
1.13 修改xadmin中子应用名
apps.py
class HomeConfig(AppConfig):
name = 'home'
verbose_name = '我的首页'
__init__.py
default_app_config = "home.apps.HomeConfig"
1.2 Django_Auth
Django默认已经提供了认证系统。认证系统包含:
- 用户管理
- 权限
- 用户组
- 密码哈希系统
- 用户登录或内容显示的表单和视图
- 一个可插拔的后台系统
Django默认用户的认证机制依赖Session机制,我们在项目中将引入JWT认证机制,将用户的身份凭据存放在Token中,然后对接Django的认证系统,帮助我们来实现:
- 用户的数据模型
- 用户密码的加密与验证
- 用户的权限系统
Django用户模型类
Django认证系统中提供了用户模型类User保存用户的数据,默认的User包含以下常见的基本字段:
字段名 | 字段描述 |
---|---|
username |
必选。150个字符以内。 用户名可能包含字母数字,_ ,@ ,+ . 和- 个字符。 |
first_name |
可选(blank=True )。 少于等于30个字符。 |
last_name |
可选(blank=True )。 少于等于30个字符。 |
email |
可选(blank=True )。 邮箱地址。 |
password |
必选。 密码的哈希加密串。 (Django 不保存原始密码)。 原始密码可以无限长而且可以包含任意字符。 |
groups |
与Group 之间的多对多关系。 |
user_permissions |
与Permission 之间的多对多关系。 |
is_staff |
布尔值。 设置用户是否可以访问Admin 站点。 |
is_active |
布尔值。 指示用户的账号是否激活。 它不是用来控制用户是否能够登录,而是描述一种帐号的使用状态。 |
is_superuser |
是否是超级用户。超级用户具有所有权限。 |
last_login |
用户最后一次登录的时间。 |
date_joined |
账户创建的时间。 当账号创建时,默认设置为当前的date/time。 |
常用方法:
-
set_password
(raw_password)设置用户的密码为给定的原始字符串,并负责密码的。 不会保存
User
对象。当None
为raw_password
时,密码将设置为一个不可用的密码。 -
check_password
(raw_password)如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。
管理器方法:
管理器方法即可以通过User.objects.
进行调用的方法。
-
create_user
(username, email=None, password=None, ***extra_fields*)创建、保存并返回一个
User
对象。 -
create_superuser
(username, email, password, ***extra_fields*)与
create_user()
相同,但是设置is_staff
和is_superuser
为True
。
1.3 创建User应用
python ../../manage.py startapp users
在settings.py
文件中注册子应用。
# 新增一个系统导包路径
import sys
sys.path.insert(0,os.path.join(BASE_DIR,"apps"))
INSTALLED_APPS = [
...
'users',
]
新增导包路径pycahrm会变黄 将pycahrm设置为源根
1.4 JWT
安装
pip install djangorestframework-jwt
配置
REST_FRAMEWORK = {
# 异常处理
'EXCEPTION_HANDLER': 'pippo_api.utils.exceptions.custom_exception_handler',
#jwt
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
import datetime
#jwt有效时间
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
在用户注册或登录成功后,在序列化器中返回用户信息以后同时返回token即可。
1.5 后端登陆认证
Django REST framework JWT提供了登录获取token的视图,可以直接使用
在子应用路由urls.py中
from rest_framework_jwt.views import obtain_jwt_token
from django.urls import path
urlpatterns = [
path('login/', obtain_jwt_token),
]
在主路由中,引入当前子应用的路由文件
urlpatterns = [
...
path('users/', include("users.urls")),
# include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点
]
接下来,我们可以通过postman来测试下功能
1.6 实时刷新token
user
中引入refresh_jwt_token
from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token
from django.urls import path
urlpatterns = [
path('login/', obtain_jwt_token),
path('verify/', refresh_jwt_token), #校监并刷新token
]
修改settings.py配置文件
# JWT
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_ALLOW_REFRESH': True, #这个参数要改True,才能刷新token
}
2. 前端显示页面
1. 添加拦截器
实现每次请求到后端都携带token
新建axios.js文件
import axios from 'axios'
// 配置默认的host,假如你的API host是:http://api.htmlx.club
axios.defaults.baseURL = 'http://127.0.0.1:8000'
// 添加请求拦截器
axios.interceptors.request.use(config=> {
// 在发送请求之前做些什么
if(localStorage.token){
config.headers.Authorization =localStorage.token
}
return config
}, error => {
// 对请求错误做些什么
return Promise.reject(error)
});
在main.js中引入
import './axios'
2. 导航守卫
实现每个页面是否携带token,如果存在,那刷新token反之跳转到登录页面
在路由index.js中追加
router.beforeEach((to, from, next) => {
let token = localStorage.token;
if (to.path === '/') {
next()
} else {
if (token) {
axios.post('http://127.0.0.1:8000/users/verify/', {
token: token,
}).then((res) => {
//更新本地的token
localStorage.token = res.data.token;
}).catch((error) => {
//刷新token失败或者token失效
next('/')
})
next()
} else {
next('/')
}
}
})
3. 登录页组件
Login.vue
样式
<template>
<div id="box">
<img src="../assets/login/login.png" alt="">
<div class="login_box">
<div class="login-title">
<p>儒风若梦!</p>
</div>
<div class="login-container">
<div class="title">
<span>登录</span>
</div>
<div class="inp">
<a-form id="components-form-demo-normal-login" :form="form" @submit="handleSubmit">
<a-form-item>
<a-input
v-decorator="['username',{ rules: [{ required: true, message: '请输入用户名!' }] },]"
placeholder="用户名">
<a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
</a-input>
</a-form-item>
<a-form-item>
<a-input v-decorator="['password', { rules: [{ required: true, message: '请输入密码!' }] },]"
type="password"
placeholder="密码">
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
</a-input>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" class="login-form-button">登录</a-button>
</a-form-item>
</a-form>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
beforeCreate() {
this.form = this.$form.createForm(this, {name: 'normal_login'});
},
methods: {
handleSubmit(e) {
e.preventDefault();
this.form.validateFields((err, values) => {
if (!err) {
// console.log('Received values of form: ', values);
//发送ajax请求
this.axios.post('/users/login/', {
username: values.username,
password: values.password
//获取数据并保存token
}).then(res => {
localStorage.token = res.data.token;
//拿到数据跳转到指定页面
this.$router.push('/hippo')
}).catch(error=>{
this.$message.error('用户名或者密码不对')
})
// 首页加载时验证token有效性
}
});
},
},
}
</script>
<style scoped>
body {
margin: 0;
}
#box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
#box img {
width: 100%;
height: 100%;
}
.login_box {
position: absolute;
width: 600px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -338px;
}
.login-title {
width: 100%;
text-align: center;
}
.login-title p {
font-family: PingFangSC-Regular;
font-size: 20px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.login-container {
width: 550px;
height: auto;
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.title {
font-size: 22px;
color: #9b9b9b;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 20px 60px 10px 60px;
margin-bottom: 20px;
}
.title span {
color: #4a4a4a;
}
.inp {
width: 450px;
margin: auto;
}
#components-form-demo-normal-login .login-form-button {
width: 100%;
}
</style>
绑定路由
import Login from '@/views/Login'
{
path: '/',
component: Login
},
4. 导航栏
使用了路由嵌套。
官方文档 https://router.vuejs.org/zh/guide/essentials/nested-routes.html
hippo.vue
<template>
<a-layout id="components-layout-demo-custom-trigger" style="min-height: 100vh">
<a-layout-sider v-model="collapsed" :trigger="null" collapsible>
<!-- <div class="logo" />-->
<div class="logo" style="color:#fff;font-size: 18px;text-align: center;">
儒风
</div>
<a-menu theme="dark" :default-selected-keys="['1']" mode="inline">
<template v-for="v in menu_list">
<a-menu-item :key='v.id' v-if="v.children.length==0">
<!-- 路由指向 点击标签进行跳转到指定的url-->
<router-link :to="v.menu_url">
<a-icon :type="v.icon"/>
<span>{{ v.title }}</span>
</router-link>
</a-menu-item>
<a-sub-menu :key="v.id" v-else>
<span slot="title"><a-icon :type="v.icon"/><span>{{ v.title }}</span></span>
<a-menu-item :key="c.id" v-for="c in v.children">
{{ c.title }}
</a-menu-item>
</a-sub-menu>
</template>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header style="background: #fff; padding: 0">
<a-icon
class="trigger"
:type="collapsed ? 'menu-unfold' : 'menu-fold'"
@click="() => (collapsed = !collapsed)"
/>
</a-layout-header>
<a-layout-content :style="{ margin: '24px 16px', padding: '5px', minHeight: '280px' }">
<router-view></router-view>
</a-layout-content>
<a-layout-footer style="text-align: center">
Ant Design ©2018 Created by Ant UED
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script>
export default {
name:"Hippo",
data() {
return {
collapsed: false,
menu_list: [
{id: 1, icon: 'desktop', title: '控制中心', tube: '', 'menu_url': '/hippo/', children: []},
{
id: 2, icon: 'container', title: '主机管理', 'menu_url': '/hippo/host/', children: []
},
{
id: 3, icon: 'bold', title: '批量执行', tube: '', 'menu_url': '/hippo/workbench', children: [
{id: 10, title: '执行任务', 'menu_url': '/hippo/host/'},
{id: 11, title: '模版管理', 'menu_url': '/hippo/host/'},
]
},
{
id: 4, icon: 'flag', title: '代码发布', tube: '', 'menu_url': '/hippo/workbench', children: [
{id: 12, title: '应用管理', 'menu_url': '/hippo/release'},
{id: 13, title: '发布申请', 'menu_url': '/hippo/release'},
]
},
{id: 5, icon: 'schedule', title: '定时计划', tube: '', 'menu_url': '/hippo/workbench', children: []},
{
id: 6, icon: 'deployment-unit', title: '配置中心', tube: '', 'menu_url': '/hippo/workbench', children: [
{id: 14, title: '环境管理', 'menu_url': '/hippo/environment'},
{id: 15, title: '服务配置', 'menu_url': '/hippo/workbench'},
{id: 16, title: '应用配置', 'menu_url': '/hippo/workbench'},
]
},
{id: 7, icon: 'radar-chart', title: '监控中心', tube: '', 'menu_url': '/hippo/workbench', children: []},
{
id: 8, icon: 'alert', title: '报警中心', tube: '', 'menu_url': '/hippo/workbench', children: [
{id: 17, title: '报警历史', 'menu_url': '/hippo/workbench'},
{id: 18, title: '报警联系人', 'menu_url': '/hippo/workbench'},
{id: 19, title: '报警联系组', 'menu_url': '/hippo/workbench'},
]
},
{
id: 9, icon: 'setting', title: '系统管理', tube: '', 'menu_url': '/hippo/workbench', children: [
{id: 20, title: '账户管理', tube: '', 'menu_url': '/hippo/workbench'},
{id: 21, title: '角色管理', tube: '', 'menu_url': '/hippo/workbench'},
{id: 22, title: '系统设置', tube: '', 'menu_url': '/hippo/workbench'},
]
},
],
};
},
};
</script>
<style>
#components-layout-demo-custom-trigger .trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
#components-layout-demo-custom-trigger .trigger:hover {
color: #1890ff;
}
#components-layout-demo-custom-trigger .logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
</style>
5. 仪表盘
home.vue
<template>
<div class="out">
<div class="top">
<!-- 栅格-->
<a-row :gutter="[16,16]">
<a-col :span="6">
<!-- 卡片-->
<a-card>
<!-- js小手-->
<a href="javascript:void(0);">
<!-- 统计-->
<a-statistic title="应用" valueStyle="color:green;" style="margin-right: 50px" :value="4" suffix="个">
</a-statistic>
</a>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a href="javascript:void(0);">
<a-statistic title="主机" valueStyle="color:green;" style="margin-right: 50px" :value="1" suffix="个">
</a-statistic>
</a>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a href="javascript:void(0);">
<a-statistic title="任务" valueStyle="color:green;" style="margin-right: 50px" :value="2" suffix="个">
</a-statistic>
</a>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<a href="javascript:void(0);">
<a-statistic title="监控" valueStyle="color:green;" style="margin-right: 50px" :value="2" suffix="个">
</a-statistic>
</a>
</a-card>
</a-col>
</a-row>
</div>
<div class="center" style="margin-top: 30px;">
<!-- 别忘了给这个标签设置高度,不然看不到效果 -->
<a-card title="报警趋势">
<!-- 级联标签-->
<a-cascader placeholder="过滤监控项,默认所有" slot="extra" :options="options" change-on-select @change="onChange"
style="width:260px;"/>
<div>
<div id="main" style="height: 400px;width:92%;" ref="chart">
<!-- 报警趋势-->
</div>
</div>
</a-card>
</div>
<div class="down" style="margin-top: 30px">
<a-row>
<a-col :span="14">
<a-card title="发布申请Top10">
<a-button type="link" slot="extra">
今日
</a-button>
<a-button type="link" slot="extra">
本周
</a-button>
<a-button type="link" slot="extra">
本月
</a-button>
<a-range-picker @change="onDateChange" slot="extra" style="width: 200px;"/>
<div id="pub" style="height: 300px;width:94%;" ref="pub">
<!-- 柱状图-->
</div>
</a-card>
</a-col>
<a-col :span="9" :offset="1">
<a-card title="最近30天登录">
<a-list :data-source="login_list" style="overflow: auto;height: 308px">
<a-list-item slot="renderItem" slot-scope="item, index">
{{ item }}
</a-list-item>
</a-list>
</a-card>
</a-col>
</a-row>
</div>
</div>
</template>
<script>
import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN';
export default {
name: 'Home',
data() {
return {
locale: zhCN,
login_list: [
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
'11-18 13:12:02 管理员 通过 1.1.1.1 登录',
],
options: [
{
value: 'test',
label: '站点检测',
children: [
{
value: '1',
label: 'elatic',
},
{
value: '2',
label: 'www.baidu.com',
},
{
value: '3',
label: 'ssh端口',
},
{
value: '4',
label: 'spug官网',
},
],
},
{
value: 'jiangsu',
label: '端口检测',
children: [
{
value: 'nanjing',
label: 'ssh端口',
},
],
},
{
value: 'jiangsu2',
label: '进程检测',
children: [
{
value: 'nanjing',
label: 'aaa',
},
],
},
{
value: 'jiangsu3',
label: '自定义脚本',
children: [
{
value: 'nanjing',
label: 'top',
},
],
},
],
};
},
methods: {
onChange(value) {
console.log(value);
},
onDateChange(date, dateString) {
console.log(date, dateString);
},
},
created() {
this.$nextTick(() => {
//console.log(this.$refs);
// data: ['2019-10-10', '2019-10-11', '2019-10-12', '2019-10-13', '2019-10-14', '2019-10-15', '2019-10-16']
// 报警图表
var myChart = this.$echarts.init(this.$refs.chart);
var alert_option = {
//
tooltip: {
trigger: 'axis'
},
color: 'blue',
grid: {
left: '3%',
right: '8%', // 控制距离左右上下的边距
bottom: '3%',
containLabel: true // 完整显示,自动缩放
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['2019-10-10', '2019-10-11', '2019-10-12', '2019-10-13', '2019-10-14', '2019-10-15', '2019-10-16',]
},
yAxis: {
type: 'value'
},
series: [
{
name: '报警次数',
type: 'line',
stack: '总量',
data: [0, 1, 0, 2, 1, 0, 1],
smooth: true,
},
]
};
// myChart.setOption(alert_option);
// window.onresize = myChart.resize
//
// 发布申请柱状图
var myChart2 = this.$echarts.init(this.$refs.pub);
var pub_option = {
color: ['#3398DB'],
tooltip: {
trigger: 'axis',
axisPointer: { // 坐标轴指示器,坐标轴触发有效
type: 'shadow' // 默认为直线,可选为:'line' | 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: ['订单服务', 'test11', '系统管理', 'test'],
axisTick: {
alignWithLabel: true
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '直接访问',
type: 'bar',
barWidth: '60%',
data: [10, 52, 200, 334]
}
]
};
myChart.setOption(alert_option);
myChart2.setOption(pub_option);
window.addEventListener('resize', function () {
myChart.resize();
myChart2.resize();
})
}
)
// 注意:直接通过this.$refs.chart获取该标签不生效,因为vue的标签加载是异步的,可能标签加载还没完成就过去获取这个标签是拿不到的,所以我们需要延迟回调方法来等vue的dom更新完成之后,再获取标签
// console.log('>>>>', this.$refs, typeof this.$refs.chart)
// var echarts = require('echarts');
}
};
</script>
配置路由
{
path: '/hippo',
component: Hippo,
children:[
{
path: '',
component: Home,
}
]
},
访问http://localhost:8080/hippo
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?