1. 新增接入高德地图API

2. 新增按钮级别权限
3. vue-element-plus-admin版本更新
This commit is contained in:
ktianc 2022-11-18 20:59:10 +08:00
parent cc3ca58dd6
commit 06d118cad7
29 changed files with 517 additions and 154 deletions

View File

@ -70,7 +70,7 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
- [x] 📚字典管理:对系统中经常使用的一些较为固定的数据进行维护。
- [ ] 📁附件管理对平台上所有文件、图片等进行统一管理对接阿里云OSS
- [x] 📁文件上传对接阿里云OSS与本地存储
- [x] 🔒登录认证:目前支持用户使用手机号+密码方式登录。
@ -82,7 +82,7 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
网站标题LOGO描述ICO备案号底部内容百度统计代码等等
- [ ] 数据分析:根据用户的登录用户地址分析出哪个地区的人最多
- [x] 用户分布:接入高德地图显示各地区用户分布情况
- [x] 🗓️登录日志:用户登录日志记录和查询。
@ -94,7 +94,7 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
- [x] 导入导出:灵活支持数据导入导出功能
- [x] 手机验证码登录功能
- [x] 手机验证码登录功能
## TODO
@ -123,6 +123,7 @@ github地址https://github.com/vvandk/kinit 👩‍👦‍👦
- [vue3-json-viewer](https://gitee.com/isfive/vue3-json-viewer)简单易用的json内容展示组件,适配vue3和vite。
- [vue3-slide-verify](https://github.com/monoplasty/vue3-slide-verify):滑块验证码插件 vue3 + typescript
- [SortableJS/vue.draggable.next](https://github.com/SortableJS/vue.draggable.next)Vue 组件 Vue.js 3.0 允许拖放和与视图模型数组同步。
- [高德地图API (amap.com)](https://lbs.amap.com/api/jsapi-v2/guide/webcli/map-vue1):地图 JSAPI 2.0 是高德开放平台免费提供的第四代 Web 地图渲染引擎, 以 WebGL 为主要绘图手段,本着“更轻、更快、更易用”的服务原则,广泛采用了各种前沿技术,交互体验、视觉体验大幅提升,同时提供了众多新增能力和特性。
#### 后端

View File

@ -1,6 +1,6 @@
{
"name": "vue-element-plus-admin",
"version": "1.8.4",
"version": "1.8.5",
"description": "一套基于vue3、element-plus、typesScript、vite3的后台集成方案。",
"author": "Archer <502431556@qq.com>",
"private": false,
@ -24,9 +24,10 @@
"analysis": "windicss-analysis"
},
"dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1",
"@iconify/iconify": "^3.0.0",
"@vueuse/core": "^9.4.0",
"@wangeditor/editor": "^5.1.22",
"@vueuse/core": "^9.5.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^2.1.0",
"animate.css": "^4.1.1",
@ -45,7 +46,7 @@
"qrcode": "^1.5.1",
"qs": "^6.11.0",
"url": "^0.11.0",
"vue": "3.2.41",
"vue": "3.2.45",
"vue-i18n": "9.2.2",
"vue-router": "^4.1.6",
"vue-types": "^4.2.1",
@ -56,7 +57,7 @@
"devDependencies": {
"@commitlint/cli": "^17.2.0",
"@commitlint/config-conventional": "^17.2.0",
"@iconify/json": "^2.1.134",
"@iconify/json": "^2.1.139",
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.0",
@ -65,35 +66,35 @@
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.0",
"@vitejs/plugin-vue-jsx": "^2.1.1",
"autoprefixer": "^10.4.13",
"eslint": "^8.27.0",
"eslint-config-prettier": "^8.5.0",
"eslint-define-config": "^1.11.0",
"eslint-define-config": "^1.12.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.7.0",
"husky": "^8.0.1",
"husky": "^8.0.2",
"less": "^4.1.3",
"lint-staged": "^13.0.3",
"plop": "^3.1.1",
"postcss": "^8.4.18",
"postcss": "^8.4.19",
"postcss-html": "^1.5.0",
"postcss-less": "^6.0.0",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"rollup": "^3.2.5",
"stylelint": "^14.14.1",
"rollup": "^3.3.0",
"stylelint": "^14.15.0",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-recommended": "^9.0.0",
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^5.0.0",
"typescript": "4.8.4",
"unplugin-vue-macros": "^0.16.0",
"vite": "3.2.2",
"typescript": "4.9.3",
"unplugin-vue-macros": "^0.16.3",
"vite": "3.2.4",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.0",
"vite-plugin-mock": "^2.9.6",

View File

@ -0,0 +1,5 @@
import request from '@/config/axios'
export const getUserLoginDistributeApi = (): Promise<IResponse> => {
return request.get({ url: '/vadmin/record/analysis/user/login/distribute/' })
}

View File

@ -245,11 +245,7 @@ export default defineComponent({
vModel={formModel.value[item.field]}
{...(autoSetPlaceholder && setTextPlaceholder(item))}
{...setComponentProps(item)}
style={
item?.component === 'Input'
? { width: '100%', ...item.componentProps?.style }
: { ...item.componentProps?.style }
}
style={item.componentProps?.style}
{...(notRenderOptions.includes(item?.component as string) &&
item?.componentProps?.options
? { options: item?.componentProps?.options || [] }
@ -276,8 +272,8 @@ export default defineComponent({
return renderRadioOptions(item)
case 'Checkbox':
case 'CheckboxButton':
const { renderChcekboxOptions } = useRenderCheckbox()
return renderChcekboxOptions(item)
const { renderCheckboxOptions } = useRenderCheckbox()
return renderCheckboxOptions(item)
default:
break
}

View File

@ -3,7 +3,7 @@ import { ElCheckbox, ElCheckboxButton } from 'element-plus'
import { defineComponent } from 'vue'
export const useRenderCheckbox = () => {
const renderChcekboxOptions = (item: FormSchema) => {
const renderCheckboxOptions = (item: FormSchema) => {
// 如果有别名,就取别名
const labelAlias = item?.componentProps?.optionsAlias?.labelField
const valueAlias = item?.componentProps?.optionsAlias?.valueField
@ -11,17 +11,16 @@ export const useRenderCheckbox = () => {
typeof defineComponent
>
return item?.componentProps?.options?.map((option) => {
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
// const { value, ...other } = option
// return (
// <Com label={option[labelAlias || 'value']} {...other}>
// {option[valueAlias || 'label']}
// </Com>
// )
const { value, ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
{option[labelAlias || 'label']}
</Com>
)
})
}
return {
renderChcekboxOptions
renderCheckboxOptions
}
}

View File

@ -11,13 +11,12 @@ export const useRenderRadio = () => {
typeof defineComponent
>
return item?.componentProps?.options?.map((option) => {
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
// const { value, ...other } = option
// return (
// <Com label={option[labelAlias || 'value']} {...other}>
// {option[valueAlias || 'label']}
// </Com>
// )
const { value, ...other } = option
return (
<Com {...other} label={option[valueAlias || 'value']}>
{option[labelAlias || 'label']}
</Com>
)
})
}

View File

@ -36,9 +36,9 @@ export const useRenderSelect = (slots: Slots) => {
return (
<ElOption
{...other}
label={labelAlias ? option[labelAlias] : label}
value={valueAlias ? option[valueAlias] : value}
{...other}
>
{{
default: () =>

View File

@ -52,7 +52,6 @@ export default defineComponent({
},
emits: ['update:limit', 'update:page', 'register'],
setup(props, { attrs, slots, emit, expose }) {
console.log('attrs', attrs)
const elTableRef = ref<ComponentRef<typeof ElTable>>()
//

View File

@ -32,6 +32,14 @@ const toHome = () => {
push('/system/home')
}
const toGitee = () => {
window.open('https://gitee.com/ktianc/kinit')
}
const toGithub = () => {
window.open('https://github.com/vvandk/kinit')
}
const user = authStore.getUser
</script>
@ -52,6 +60,12 @@ const user = authStore.getUser
<ElDropdownItem>
<ElButton @click="toHome" link>个人主页</ElButton>
</ElDropdownItem>
<ElDropdownItem>
<ElButton @click="toGitee" link>Gitee</ElButton>
</ElDropdownItem>
<ElDropdownItem>
<ElButton @click="toGithub" link>Github</ElButton>
</ElDropdownItem>
<ElDropdownItem divided>
<ElButton @click="loginOut" link>退出系统</ElButton>
</ElDropdownItem>

View File

@ -53,9 +53,8 @@ export const useAuthStore = defineStore('auth', {
if (res) {
wsCache.set(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
// 存储用户信息
wsCache.set(appStore.getUserInfo, res.data.user)
this.user = res.data.user
this.isUser = true
const auth = useAuthStore()
await auth.getUserInfo()
}
return res
},

View File

@ -0,0 +1,183 @@
<script setup lang="ts">
import AMapLoader from '@amap/amap-jsapi-loader'
import { shallowRef, ref } from 'vue'
import { getSystemSettingsApi } from '@/api/vadmin/system/settings'
import { getUserLoginDistributeApi } from '@/api/dashboard/map/index'
let map = shallowRef()
let AMap = shallowRef()
// AMap.Map https://lbs.amap.com/api/javascript-api/reference/map
// InfoWindow https://lbs.amap.com/api/javascript-api/reference/infowindow#InfoWindow
// https://blog.csdn.net/qq_39417037/article/details/124040318
const initMap = async () => {
const res = await getSystemSettingsApi({ tab_id: 8 })
if (res) {
AMapLoader.load({
key: res.data.map_key, // WebKey load
version: '2.0', // JSAPI 1.4.15
plugins: [''] // 使'AMap.Scale'
})
.then(async (A) => {
AMap.value = A
map.value = new A.Map('map-container', {
// id
pitch: res.data.map_pitch, // 0 - 83
terrain: true, //
viewMode: res.data.map_view_mode, // 3D
zoom: res.data.map_zoom, //
resizeEnable: true,
mapStyle: res.data.map_style, //
center: JSON.parse(res.data.map_center) //
})
await setValues()
})
.catch((e) => {
console.log(e)
})
}
}
const setValues = async () => {
const infoWindow = new AMap.value.InfoWindow({
offset: new AMap.value.Pixel(2, 15),
closeWhenClickMap: true,
isCustom: true,
anchor: 'top-left'
})
const res = await getUserLoginDistributeApi()
if (res) {
const markers = res.data.map((item) => {
const center = item.center
let circleMarker = ref()
if (item.total > 40) {
circleMarker.value = new AMap.value.Marker({
position: center,
offset: new AMap.value.Pixel(0, 15) //
})
// div
var markerDiv = document.createElement('div')
// className,
markerDiv.className = 'alarmDevice'
// div
circleMarker.value.setContent(markerDiv)
} else {
circleMarker.value = new AMap.value.CircleMarker({
center: center,
radius: item.total > 30 ? 20 : item.total / 2, // 3DCircleMarker64px
strokeColor: '#f05b72',
strokeWeight: 2,
strokeOpacity: 0.5,
fillColor: '#f05b72',
fillOpacity: 0.5,
zIndex: 10,
bubble: true,
cursor: 'pointer',
clickable: true
})
}
//
circleMarker.value.on('mouseover', function (e) {
infoWindow.setContent(
`<div class="description">
<div class="name-box">
<span class="point"></span>
<span class="name">${item.name}</span>
</div>
<span>${item.total}</span>
</div>`
)
infoWindow.open(map.value, center)
})
//
circleMarker.value.on('mouseout', function (e) {
infoWindow.close(map.value, center)
})
return circleMarker.value
})
map.value.add(markers)
}
}
initMap()
</script>
<template>
<div id="map-container"></div>
</template>
<style scoped lang="less">
#map-container {
padding: 0px;
margin: 0px;
width: 100%;
height: 800px;
}
#map-container :deep(.description) {
background-color: #fff;
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
box-sizing: border-box;
border: 2px solid #f05b72;
border-radius: 5px;
font-size: 14px;
}
#map-container :deep(.point) {
display: inline-block;
width: 9px;
height: 9px;
border-radius: 50%;
background-color: #f05b72;
margin-bottom: 1px;
margin-right: 2px;
}
#map-container :deep(.name-box) {
display: inline;
margin-right: 8px;
}
#map-container :deep(.alarmDevice) {
text-align: center;
margin: 0 auto;
width: 30px;
height: 30px;
background-color: #f13737;
box-shadow: 0px 0px 15px #f61212;
border-radius: 50%;
-webkit-animation-name: 'alarmDeviceBreath'; /*动画属性名也就是我们前面keyframes定义的动画名*/
-webkit-animation-duration: 1s; /*动画持续时间*/
-webkit-animation-timing-function: ease; /*动画频率和transition-timing-function是一样的*/
-webkit-animation-delay: 0s; /*动画延迟时间*/
-webkit-animation-iteration-count: infinite; /*定义循环资料infinite为无限次*/
-webkit-animation-direction: alternate; /*定义动画方式*/
}
</style>
<style>
@keyframes alarmDeviceBreath {
0% {
margin-left: 0;
margin-top: 0;
width: 30px;
height: 30px;
box-shadow: 0px 0px 15px #f61212;
opacity: 1.2;
}
100% {
margin-left: 5px;
margin-top: 5px;
width: 20px;
height: 20px;
box-shadow: 0px 0px 10px #f61212;
opacity: 0.6;
}
}
</style>

View File

@ -129,8 +129,8 @@ const signIn = async () => {
loading.value = true
const { getFormData } = methods
const formData = await getFormData<UserLoginType>()
try {
const authStore = useAuthStoreWithOut()
try {
const res = await authStore.login(formData)
if (res) {
if (!res.data.is_reset_password) {
@ -140,8 +140,10 @@ const signIn = async () => {
// 使
getMenu()
}
} else {
loading.value = false
}
} finally {
} catch (e: any) {
loading.value = false
}
}

View File

@ -97,8 +97,8 @@ const telephoneCodeLogin = async () => {
loading.value = true
const { getFormData } = methods
const formData = await getFormData<UserLoginType>()
try {
const authStore = useAuthStoreWithOut()
try {
const res = await authStore.login(formData)
if (res) {
if (!res.data.is_reset_password) {
@ -108,8 +108,10 @@ const telephoneCodeLogin = async () => {
// 使
getMenu()
}
} else {
loading.value = false
}
} finally {
} catch (e: any) {
loading.value = false
}
}
@ -127,6 +129,7 @@ const getSMSCode = async () => {
SMSCodeNumber.value = 60
const { getFormData } = methods
const formData = await getFormData<UserLoginType>()
try {
const res = await postSMSCodeApi({ telephone: formData.telephone })
if (res?.data) {
let timer = setInterval(() => {
@ -140,6 +143,9 @@ const getSMSCode = async () => {
ElMessage.error('发送失败,请联系管理员')
SMSCodeStatus.value = true
}
} catch (e: any) {
SMSCodeStatus.value = true
}
}
})
}

View File

@ -107,8 +107,10 @@ const save = async () => {
if (res) {
// 使
getMenu()
} else {
loading.value = false
}
} finally {
} catch (e: any) {
loading.value = false
}
}

View File

@ -6,6 +6,7 @@ import { useValidator } from '@/hooks/web/useValidator'
import { getMenuTreeOptionsApi } from '@/api/vadmin/auth/menu'
import { ElButton, ElInput } from 'element-plus'
import { schema } from './menu.data'
import { propTypes } from '@/utils/propTypes'
const { required } = useValidator()
@ -13,7 +14,8 @@ const props = defineProps({
currentRow: {
type: Object as PropType<Nullable<any>>,
default: () => null
}
},
parentId: propTypes.number.def(undefined)
})
const rules = reactive({
@ -53,6 +55,10 @@ const getMenuTreeOptions = async () => {
value: res.data
}
])
if (props.parentId) {
const { setValue } = methods
setValue('parent_id', props.parentId)
}
}
}

View File

@ -58,7 +58,7 @@ export const columns = reactive<TableColumn[]>([
},
{
field: 'action',
width: '150px',
width: '200px',
label: '操作',
show: true
}
@ -77,7 +77,9 @@ export const schema = reactive<FormSchema[]>([
width: '100%'
},
checkStrictly: true,
placeholder: '请选择上级菜单'
placeholder: '请选择上级菜单',
nodeKey: 'value',
defaultExpandAll: true
}
},
{
@ -130,6 +132,11 @@ export const schema = reactive<FormSchema[]>([
component: 'InputNumber',
colProps: {
span: 12
},
componentProps: {
style: {
width: '100%'
}
}
},
{
@ -216,6 +223,6 @@ export const schema = reactive<FormSchema[]>([
colProps: {
span: 12
},
ifshow: (values) => values.menu_type !== '0'
ifshow: (values) => values.menu_type === '2'
}
])

View File

@ -45,9 +45,11 @@ const dialogVisible = ref(false)
const dialogTitle = ref('')
const delLoading = ref(false)
const actionType = ref('')
const parentId = ref()
//
const AddAction = () => {
parentId.value = null
dialogTitle.value = t('exampleDemo.add')
tableObject.currentRow = null
dialogVisible.value = true
@ -56,6 +58,7 @@ const AddAction = () => {
//
const updateAction = (row: any) => {
parentId.value = null
dialogTitle.value = '编辑'
tableObject.currentRow = row
dialogVisible.value = true
@ -64,6 +67,7 @@ const updateAction = (row: any) => {
//
const delData = async (row: any) => {
parentId.value = null
tableObject.currentRow = row
const { delListApi } = methods
delLoading.value = true
@ -72,6 +76,15 @@ const delData = async (row: any) => {
})
}
//
const addSonMenu = async (row: any) => {
parentId.value = row.id
dialogTitle.value = t('exampleDemo.add')
tableObject.currentRow = null
dialogVisible.value = true
actionType.value = 'add'
}
const loading = ref(false)
const writeRef = ref<ComponentRef<typeof Write>>()
@ -124,7 +137,9 @@ watch(
<div class="mb-8px flex justify-between">
<ElRow :gutter="10">
<ElCol :span="1.5">
<ElButton type="primary" @click="AddAction">新增菜单</ElButton>
<ElButton type="primary" v-hasPermi="['auth.menu.create']" @click="AddAction"
>新增菜单</ElButton
>
</ElCol>
</ElRow>
<RightToolbar @get-list="getList" v-model:table-size="tableSize" v-model:columns="columns" />
@ -149,10 +164,31 @@ watch(
</div>
</template>
<template #action="{ row }">
<ElButton type="primary" link size="small" @click="updateAction(row)">
<ElButton
type="primary"
v-hasPermi="['auth.menu.update']"
link
size="small"
@click="updateAction(row)"
>
{{ t('exampleDemo.edit') }}
</ElButton>
<ElButton type="danger" link size="small" @click="delData(row)">
<ElButton
type="primary"
v-hasPermi="['auth.menu.create']"
link
size="small"
@click="addSonMenu(row)"
>
添加子菜单
</ElButton>
<ElButton
type="danger"
v-hasPermi="['auth.menu.delete']"
link
size="small"
@click="delData(row)"
>
{{ t('exampleDemo.del') }}
</ElButton>
</template>
@ -173,7 +209,7 @@ watch(
</Table>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<Write ref="writeRef" :current-row="tableObject.currentRow" />
<Write ref="writeRef" :current-row="tableObject.currentRow" :parent-id="parentId" />
<template #footer>
<ElButton type="primary" :loading="loading" @click="save">

View File

@ -5,7 +5,7 @@ import { PropType, reactive, watch, ref } from 'vue'
import { useValidator } from '@/hooks/web/useValidator'
import { getMenuRoleTreeOptionsApi } from '@/api/vadmin/auth/menu'
import { schema } from './role.data'
import { ElTree } from 'element-plus'
import { ElTree, ElCheckbox } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
const { required } = useValidator()
@ -50,7 +50,7 @@ const defaultProps = {
children: 'children',
label: 'label'
}
let data = ref([])
let data = ref([] as Recordable[])
const getMenuRoleTreeOptions = async () => {
const res = await getMenuRoleTreeOptionsApi()
@ -72,23 +72,72 @@ defineExpose({
getFormData: methods.getFormData,
getTreeCheckedKeys: getTreeCheckedKeys
})
let selectAll = ref(false)
let defaultExpandAll = ref(true)
let checkStrictly = ref(false)
// key
const getTreeNodeKeys = (nodes: Recordable[]): number[] => {
let keys = [] as number[]
for (let i = 0; i < nodes.length; i++) {
keys.push(nodes[i].value)
if (nodes[i].children && nodes[i].children.length > 0) {
keys = keys.concat(getTreeNodeKeys(nodes[i].children))
}
}
return keys
}
// /
const handleCheckedTreeExpand = () => {
for (let i = 0; i < data.value.length; i++) {
treeRef.value!.store.nodesMap[data.value[i].value].expanded = defaultExpandAll.value
}
}
///
function handleCheckedTreeNodeAll() {
treeRef.value!.setCheckedKeys(selectAll.value ? getTreeNodeKeys(data.value) : [])
}
</script>
<template>
<Form :rules="rules" @register="register">
<template #menu_ids>
<div>
<div>
<ElCheckbox
v-model="defaultExpandAll"
@change="handleCheckedTreeExpand"
label="展开/折叠"
size="large"
/>
<ElCheckbox
v-model="selectAll"
@change="handleCheckedTreeNodeAll"
label="全选/全不选"
size="large"
/>
<ElCheckbox v-model="checkStrictly" label="父子联动" size="large" />
</div>
<div class="max-h-390px border p-10px overflow-auto">
<ElTree
ref="treeRef"
:data="data"
show-checkbox
node-key="value"
:props="defaultProps"
:default-expand-all="defaultExpandAll"
:check-strictly="!checkStrictly"
:default-checked-keys="defaultCheckedKeys"
>
<template #default="{ node }">
<span>{{ t(node.label) }}</span>
</template>
</ElTree>
</div>
</div>
</template>
</Form>
</template>

View File

@ -53,6 +53,7 @@ const updateAction = async (row: any) => {
dialogTitle.value = '编辑'
tableObject.currentRow = res.data
defaultCheckedKeys.value = res.data.menus.map((item: any) => item.id)
console.log(defaultCheckedKeys.value)
dialogVisible.value = true
actionType.value = 'edit'
}
@ -123,7 +124,7 @@ watch(
<div class="mb-8px flex justify-between">
<ElRow :gutter="10">
<ElCol :span="1.5">
<ElCol :span="1.5" v-hasPermi="['auth.role.create']">
<ElButton type="primary" @click="AddAction">新增角色</ElButton>
</ElCol>
</ElRow>
@ -144,10 +145,24 @@ watch(
@register="register"
>
<template #action="{ row }">
<ElButton type="primary" link size="small" @click="updateAction(row)" v-if="row.id !== 1">
<ElButton
type="primary"
v-hasPermi="['auth.role.update']"
link
size="small"
@click="updateAction(row)"
v-if="row.id !== 1"
>
{{ t('exampleDemo.edit') }}
</ElButton>
<ElButton type="danger" link size="small" @click="delData(row)" v-if="row.id !== 1">
<ElButton
type="danger"
v-hasPermi="['auth.role.delete']"
link
size="small"
@click="delData(row)"
v-if="row.id !== 1"
>
{{ t('exampleDemo.del') }}
</ElButton>
</template>
@ -161,7 +176,7 @@ watch(
</template>
</Table>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px" maxHeight="600px">
<Write
ref="writeRef"
:current-row="tableObject.currentRow"

View File

@ -179,19 +179,19 @@ const sendPasswordToSMS = async () => {
<div class="mb-8px flex justify-between">
<ElRow :gutter="10">
<ElCol :span="1.5">
<ElCol :span="1.5" v-hasPermi="['auth.user.create']">
<ElButton type="primary" @click="AddAction">新增用户</ElButton>
</ElCol>
<ElCol :span="1.5">
<ElCol :span="1.5" v-hasPermi="['auth.user.import']">
<ElButton @click="importList">批量导入用户</ElButton>
</ElCol>
<ElCol :span="1.5">
<ElCol :span="1.5" v-hasPermi="['auth.user.export']">
<ElButton @click="exportQueryList">导出筛选用户</ElButton>
</ElCol>
<ElCol :span="1.5">
<ElCol :span="1.5" v-hasPermi="['auth.user.reset']">
<ElButton @click="sendPasswordToSMS">重置密码通知短信</ElButton>
</ElCol>
<ElCol :span="1.5">
<ElCol :span="1.5" v-hasPermi="['auth.user.delete']">
<ElButton type="danger" @click="delDatas(null, true)">批量删除</ElButton>
</ElCol>
</ElRow>
@ -212,11 +212,18 @@ const sendPasswordToSMS = async () => {
@register="register"
>
<template #action="{ row }">
<ElButton type="primary" link size="small" @click="updateAction(row)">
<ElButton
type="primary"
v-hasPermi="['auth.user.update']"
link
size="small"
@click="updateAction(row)"
>
{{ t('exampleDemo.edit') }}
</ElButton>
<ElButton
type="danger"
v-hasPermi="['auth.user.delete']"
link
size="small"
@click="delDatas(row, false)"

View File

@ -273,7 +273,7 @@ class MenuDal(DalBase):
3获取菜单树列表角色添加菜单权限时使用
"""
if mode == 3:
sql = select(self.model).where(self.model.disabled == 0, self.model.menu_type != "2")
sql = select(self.model).where(self.model.disabled == 0)
else:
sql = select(self.model)
queryset = await self.db.execute(sql)

View File

@ -10,20 +10,16 @@ from apps.vadmin.auth import crud, models
from .validation import AuthValidation, Auth
async def get_user_permissions(user: models.VadminUser, db: AsyncSession):
async def get_user_permissions(user):
"""
获取跟进系统用户所有权限列表
"""
roles = []
for i in user.roles:
if i.is_admin:
return ["*:*:*"]
roles.append(i.id)
if any([role.is_admin for role in user.roles]):
return ['*.*.*']
permissions = set()
for data_id in roles:
role_obj = await crud.RoleDal(db).get_data(data_id, options=[models.VadminUser])
for role_obj in user.roles:
for menu in role_obj.menus:
if menu.perms and menu.status:
if menu.perms and not menu.disabled:
permissions.add(menu.perms)
return list(permissions)

View File

@ -5,7 +5,6 @@
# @IDE : PyCharm
# @desc : 安全认证视图
"""
JWT 表示 JSON Web Tokenshttps://jwt.io/
@ -16,13 +15,13 @@ JWT 表示 「JSON Web Tokens」。https://jwt.io/
我们需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌pip install python-jose[cryptography]
PassLib 是一个用于处理哈希密码的很棒的 Python 它支持许多安全哈希算法以及配合算法使用的实用程序推荐的算法是 Bcryptpip install passlib[bcrypt]
PassLib 是一个用于处理哈希密码的很棒的 Python 它支持许多安全哈希算法以及配合算法使用的实用程序
推荐的算法是 Bcryptpip install passlib[bcrypt]
"""
import json
from datetime import timedelta
from fastapi import APIRouter, Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
from core.database import db_getter
from utils.response import SuccessResponse, ErrorResponse
from application import settings
@ -45,33 +44,22 @@ async def login_for_access_token(request: Request, data: LoginForm, manage: Logi
else:
return ErrorResponse(msg="请使用正确的登录方式")
if not result.status:
res = {"message": result.msg}
resp = {"message": result.msg}
telephone = data.telephone
await VadminLoginRecord.\
create_login_record(telephone=telephone, status=result.status, request=request, response=res, db=db)
create_login_record(db, telephone, result.status, request, resp)
return ErrorResponse(msg=result.msg)
user = result.user
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires)
res = {
resp = {
"access_token": access_token,
"token_type": "bearer",
"is_reset_password": user.is_reset_password,
"user": {
"id": user.id,
"telephone": user.telephone,
"name": user.name,
"nickname": user.nickname,
"avatar": user.avatar,
"gender": user.gender,
"create_datetime": user.create_datetime,
"roles": [{"name": i.name, "value": i.role_key} for i in user.roles]
"is_reset_password": user.is_reset_password
}
}
await VadminLoginRecord.\
create_login_record(telephone=user.telephone, status=result.status, request=request, response=res, db=db)
return SuccessResponse(res)
await VadminLoginRecord.create_login_record(db, user.telephone, result.status, request, resp)
return SuccessResponse(resp)
@app.get("/getMenuList/", summary="获取当前用户菜单树")

View File

@ -44,7 +44,7 @@ class LoginValidation:
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
self.result = LoginResult()
options = [models.VadminUser.roles]
options = [models.VadminUser.roles, "roles.menus"]
user = await crud.UserDal(db).get_data(telephone=data.telephone, return_none=True, options=options)
if not user:
self.result.msg = "该手机号不存在!"

View File

@ -9,8 +9,8 @@
from fastapi import APIRouter, Depends, Body, UploadFile, Request
from utils.response import SuccessResponse, ErrorResponse
from . import schemas, crud, models
from core.dependencies import Paging, IdList
from apps.vadmin.auth.utils.current import login_auth, Auth
from core.dependencies import IdList
from apps.vadmin.auth.utils.current import login_auth, Auth, get_user_permissions, full_admin
from .params import UserParams, RoleParams
app = APIRouter()
@ -65,8 +65,10 @@ async def post_user_current_update_info(data: schemas.UserUpdate, auth: Auth = D
@app.get("/user/current/info/", summary="获取当前用户基本信息")
async def get_user_current_info(auth: Auth = Depends(login_auth)):
return SuccessResponse(schemas.UserSimpleOut.from_orm(auth.user).dict())
async def get_user_current_info(auth: Auth = Depends(full_admin)):
result = schemas.UserSimpleOut.from_orm(auth.user).dict()
result["permissions"] = await get_user_permissions(auth.user)
return SuccessResponse(result)
@app.get("/user/current/info/", summary="获取当前用户基本信息")

View File

@ -5,6 +5,8 @@
# @File : crud.py
# @IDE : PyCharm
# @desc : 数据库 增删改查操作
import random
from typing import List
# sqlalchemy 查询操作https://segmentfault.com/a/1190000016767008
# sqlalchemy 关联查询https://www.jianshu.com/p/dfad7c08c57a
@ -15,9 +17,56 @@ from core.crud import DalBase
class LoginRecordDal(DalBase):
def __init__(self, db: AsyncSession):
super(LoginRecordDal, self).__init__(db, models.VadminLoginRecord, schemas.LoginRecordSimpleOut)
async def get_user_distribute(self) -> List[dict]:
"""
获取用户登录分布情况
高德经纬度查询https://lbs.amap.com/tools/picker
{
name: '北京',
center: [116.407394, 39.904211],
total: 20
}
@return: List[dict]
"""
result = [{
"name": '北京',
"center": [116.407394, 39.904211],
},
{
"name": '重庆',
"center": [106.551643, 29.562849],
},
{
"name": '郑州',
"center": [113.778584, 34.759197],
},
{
"name": '南京',
"center": [118.796624, 32.059344],
},
{
"name": '武汉',
"center": [114.304569, 30.593354],
},
{
"name": '乌鲁木齐',
"center": [87.616824, 43.825377],
},
{
"name": '新乡',
"center": [113.92679, 35.303589],
}]
for data in result:
assert isinstance(data, dict)
data["total"] = random.randint(2, 80)
return result
class SMSSendRecordDal(DalBase):
def __init__(self, db: AsyncSession):

View File

@ -35,24 +35,22 @@ class VadminLoginRecord(BaseModel):
request = Column(TEXT, comment="请求信息")
@classmethod
async def create_login_record(cls, telephone: str, status: bool, request: Request, response: dict,
db: AsyncSession):
async def create_login_record(cls, db: AsyncSession, telephone: str, status: bool, req: Request, resp: dict):
"""
创建登录记录
@return:
"""
header = {}
for k, v in request.headers.items():
for k, v in req.headers.items():
header[k] = v
body = json.loads((await request.body()).decode())
user_agent = parse(request.headers.get("user-agent"))
body = json.loads((await req.body()).decode())
user_agent = parse(req.headers.get("user-agent"))
system = f"{user_agent.os.family} {user_agent.os.version_string}"
browser = f"{user_agent.browser.family} {user_agent.browser.version_string}"
ip = IPManage(request.client.host)
ip = IPManage(req.client.host)
location = await ip.parse()
resp = json.dumps(response)
resq = json.dumps({"body": body, "headers": header})
params = json.dumps({"body": body, "headers": header})
obj = VadminLoginRecord(**location.dict(), telephone=telephone, status=status, browser=browser,
system=system, response=resp, request=resq)
system=system, response=json.dumps(resp), request=params)
db.add(obj)
await db.flush()

View File

@ -5,9 +5,7 @@
# @IDE : PyCharm
# @desc : 主要接口文件
from typing import Optional
from fastapi import APIRouter, Depends, Query
from core.dependencies import Paging, IdList
from fastapi import APIRouter, Depends
from utils.response import SuccessResponse
from . import crud, schemas
from apps.vadmin.auth.utils.current import login_auth, Auth
@ -40,3 +38,11 @@ async def get_sms_send_list(params: SMSParams = Depends(), auth: Auth = Depends(
datas = await crud.SMSSendRecordDal(auth.db).get_datas(**params.dict())
count = await crud.SMSSendRecordDal(auth.db).get_count(**params.to_count())
return SuccessResponse(datas, count=count)
###########################################################
# 日志分析
###########################################################
@app.get("/analysis/user/login/distribute/", summary="获取用户登录分布情况列表")
async def get_user_login_distribute(auth: Auth = Depends(login_auth)):
return SuccessResponse(await crud.LoginRecordDal(auth.db).get_user_distribute())

View File

@ -22,14 +22,12 @@ pip install alibabacloud_dysmsapi20170525
import json
import random
import re
import uuid
from enum import Enum, unique
from core.exception import CustomException
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
from core.logger import logger
import datetime
from aioredis.client import Redis
@ -44,8 +42,8 @@ class AliyunSMS:
@unique
class Scene(Enum):
login = "template_code_1"
reset_password = "template_code_2"
login = "sms_template_code_1"
reset_password = "sms_template_code_2"
def __init__(self, rd: Redis, telephone: str):
self.check_telephone_format(telephone)
@ -67,14 +65,14 @@ class AliyunSMS:
raise CustomException("获取短信配置信息失败,请联系管理员!", code=status.HTTP_ERROR)
else:
aliyun_sms = json.loads(aliyun_sms)
self.access_key = aliyun_sms.get("access_key")
self.access_key_secret = aliyun_sms.get("access_key_secret")
self.send_interval = int(aliyun_sms.get("send_interval"))
self.valid_time = int(aliyun_sms.get("valid_time"))
self.access_key = aliyun_sms.get("sms_access_key")
self.access_key_secret = aliyun_sms.get("sms_access_key_secret")
self.send_interval = int(aliyun_sms.get("sms_send_interval"))
self.valid_time = int(aliyun_sms.get("sms_valid_time"))
if self.scene == self.Scene.login:
self.sign_name = aliyun_sms.get("sign_name_1")
self.sign_name = aliyun_sms.get("sms_sign_name_1")
else:
self.sign_name = aliyun_sms.get("sign_name_2")
self.sign_name = aliyun_sms.get("sms_sign_name_2")
self.template_code = aliyun_sms.get(self.scene.value)
async def main_async(self, scene: Scene, **kwargs) -> bool: