1. 新增接入高德地图API
2. 新增按钮级别权限 3. vue-element-plus-admin版本更新
This commit is contained in:
parent
cc3ca58dd6
commit
06d118cad7
@ -70,7 +70,7 @@ github地址:https://github.com/vvandk/kinit 👩👦👦
|
|||||||
|
|
||||||
- [x] 📚字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
- [x] 📚字典管理:对系统中经常使用的一些较为固定的数据进行维护。
|
||||||
|
|
||||||
- [ ] 📁附件管理:对平台上所有文件、图片等进行统一管理,对接阿里云OSS。
|
- [x] 📁文件上传:对接阿里云OSS与本地存储。
|
||||||
|
|
||||||
- [x] 🔒登录认证:目前支持用户使用手机号+密码方式登录。
|
- [x] 🔒登录认证:目前支持用户使用手机号+密码方式登录。
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ github地址:https://github.com/vvandk/kinit 👩👦👦
|
|||||||
|
|
||||||
网站标题,LOGO,描述,ICO,备案号,底部内容,百度统计代码,等等
|
网站标题,LOGO,描述,ICO,备案号,底部内容,百度统计代码,等等
|
||||||
|
|
||||||
- [ ] 数据分析:根据用户的登录用户地址分析出哪个地区的人最多
|
- [x] 用户分布:接入高德地图显示各地区用户分布情况
|
||||||
|
|
||||||
- [x] 🗓️登录日志:用户登录日志记录和查询。
|
- [x] 🗓️登录日志:用户登录日志记录和查询。
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ github地址:https://github.com/vvandk/kinit 👩👦👦
|
|||||||
|
|
||||||
- [x] 导入导出:灵活支持数据导入导出功能
|
- [x] 导入导出:灵活支持数据导入导出功能
|
||||||
|
|
||||||
- [x] 手机验证码登录功能’
|
- [x] 手机验证码登录功能
|
||||||
|
|
||||||
## TODO
|
## 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-json-viewer](https://gitee.com/isfive/vue3-json-viewer):简单易用的json内容展示组件,适配vue3和vite。
|
||||||
- [vue3-slide-verify](https://github.com/monoplasty/vue3-slide-verify):滑块验证码插件 vue3 + typescript
|
- [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) 允许拖放和与视图模型数组同步。
|
- [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 为主要绘图手段,本着“更轻、更快、更易用”的服务原则,广泛采用了各种前沿技术,交互体验、视觉体验大幅提升,同时提供了众多新增能力和特性。
|
||||||
|
|
||||||
#### 后端
|
#### 后端
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-element-plus-admin",
|
"name": "vue-element-plus-admin",
|
||||||
"version": "1.8.4",
|
"version": "1.8.5",
|
||||||
"description": "一套基于vue3、element-plus、typesScript、vite3的后台集成方案。",
|
"description": "一套基于vue3、element-plus、typesScript、vite3的后台集成方案。",
|
||||||
"author": "Archer <502431556@qq.com>",
|
"author": "Archer <502431556@qq.com>",
|
||||||
"private": false,
|
"private": false,
|
||||||
@ -24,9 +24,10 @@
|
|||||||
"analysis": "windicss-analysis"
|
"analysis": "windicss-analysis"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@amap/amap-jsapi-loader": "^1.0.1",
|
||||||
"@iconify/iconify": "^3.0.0",
|
"@iconify/iconify": "^3.0.0",
|
||||||
"@vueuse/core": "^9.4.0",
|
"@vueuse/core": "^9.5.0",
|
||||||
"@wangeditor/editor": "^5.1.22",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||||
"@zxcvbn-ts/core": "^2.1.0",
|
"@zxcvbn-ts/core": "^2.1.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
@ -45,7 +46,7 @@
|
|||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"qs": "^6.11.0",
|
"qs": "^6.11.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"vue": "3.2.41",
|
"vue": "3.2.45",
|
||||||
"vue-i18n": "9.2.2",
|
"vue-i18n": "9.2.2",
|
||||||
"vue-router": "^4.1.6",
|
"vue-router": "^4.1.6",
|
||||||
"vue-types": "^4.2.1",
|
"vue-types": "^4.2.1",
|
||||||
@ -56,7 +57,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^17.2.0",
|
"@commitlint/cli": "^17.2.0",
|
||||||
"@commitlint/config-conventional": "^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",
|
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
|
||||||
"@purge-icons/generated": "^0.9.0",
|
"@purge-icons/generated": "^0.9.0",
|
||||||
"@types/intro.js": "^5.1.0",
|
"@types/intro.js": "^5.1.0",
|
||||||
@ -65,35 +66,35 @@
|
|||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/qrcode": "^1.5.0",
|
"@types/qrcode": "^1.5.0",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"@typescript-eslint/parser": "^5.42.0",
|
"@typescript-eslint/parser": "^5.43.0",
|
||||||
"@vitejs/plugin-vue": "^3.2.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",
|
"autoprefixer": "^10.4.13",
|
||||||
"eslint": "^8.27.0",
|
"eslint": "^8.27.0",
|
||||||
"eslint-config-prettier": "^8.5.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-prettier": "^4.2.1",
|
||||||
"eslint-plugin-vue": "^9.7.0",
|
"eslint-plugin-vue": "^9.7.0",
|
||||||
"husky": "^8.0.1",
|
"husky": "^8.0.2",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"lint-staged": "^13.0.3",
|
"lint-staged": "^13.0.3",
|
||||||
"plop": "^3.1.1",
|
"plop": "^3.1.1",
|
||||||
"postcss": "^8.4.18",
|
"postcss": "^8.4.19",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^3.2.5",
|
"rollup": "^3.3.0",
|
||||||
"stylelint": "^14.14.1",
|
"stylelint": "^14.15.0",
|
||||||
"stylelint-config-html": "^1.1.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-recommended": "^9.0.0",
|
||||||
"stylelint-config-standard": "^29.0.0",
|
"stylelint-config-standard": "^29.0.0",
|
||||||
"stylelint-order": "^5.0.0",
|
"stylelint-order": "^5.0.0",
|
||||||
"typescript": "4.8.4",
|
"typescript": "4.9.3",
|
||||||
"unplugin-vue-macros": "^0.16.0",
|
"unplugin-vue-macros": "^0.16.3",
|
||||||
"vite": "3.2.2",
|
"vite": "3.2.4",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.0",
|
||||||
"vite-plugin-mock": "^2.9.6",
|
"vite-plugin-mock": "^2.9.6",
|
||||||
|
5
kinit-admin/src/api/dashboard/map/index.ts
Normal file
5
kinit-admin/src/api/dashboard/map/index.ts
Normal 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/' })
|
||||||
|
}
|
@ -245,11 +245,7 @@ export default defineComponent({
|
|||||||
vModel={formModel.value[item.field]}
|
vModel={formModel.value[item.field]}
|
||||||
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
{...(autoSetPlaceholder && setTextPlaceholder(item))}
|
||||||
{...setComponentProps(item)}
|
{...setComponentProps(item)}
|
||||||
style={
|
style={item.componentProps?.style}
|
||||||
item?.component === 'Input'
|
|
||||||
? { width: '100%', ...item.componentProps?.style }
|
|
||||||
: { ...item.componentProps?.style }
|
|
||||||
}
|
|
||||||
{...(notRenderOptions.includes(item?.component as string) &&
|
{...(notRenderOptions.includes(item?.component as string) &&
|
||||||
item?.componentProps?.options
|
item?.componentProps?.options
|
||||||
? { options: item?.componentProps?.options || [] }
|
? { options: item?.componentProps?.options || [] }
|
||||||
@ -276,8 +272,8 @@ export default defineComponent({
|
|||||||
return renderRadioOptions(item)
|
return renderRadioOptions(item)
|
||||||
case 'Checkbox':
|
case 'Checkbox':
|
||||||
case 'CheckboxButton':
|
case 'CheckboxButton':
|
||||||
const { renderChcekboxOptions } = useRenderCheckbox()
|
const { renderCheckboxOptions } = useRenderCheckbox()
|
||||||
return renderChcekboxOptions(item)
|
return renderCheckboxOptions(item)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import { ElCheckbox, ElCheckboxButton } from 'element-plus'
|
|||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
|
|
||||||
export const useRenderCheckbox = () => {
|
export const useRenderCheckbox = () => {
|
||||||
const renderChcekboxOptions = (item: FormSchema) => {
|
const renderCheckboxOptions = (item: FormSchema) => {
|
||||||
// 如果有别名,就取别名
|
// 如果有别名,就取别名
|
||||||
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
const labelAlias = item?.componentProps?.optionsAlias?.labelField
|
||||||
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
const valueAlias = item?.componentProps?.optionsAlias?.valueField
|
||||||
@ -11,17 +11,16 @@ export const useRenderCheckbox = () => {
|
|||||||
typeof defineComponent
|
typeof defineComponent
|
||||||
>
|
>
|
||||||
return item?.componentProps?.options?.map((option) => {
|
return item?.componentProps?.options?.map((option) => {
|
||||||
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
|
const { value, ...other } = option
|
||||||
// const { value, ...other } = option
|
return (
|
||||||
// return (
|
<Com {...other} label={option[valueAlias || 'value']}>
|
||||||
// <Com label={option[labelAlias || 'value']} {...other}>
|
{option[labelAlias || 'label']}
|
||||||
// {option[valueAlias || 'label']}
|
</Com>
|
||||||
// </Com>
|
)
|
||||||
// )
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
renderChcekboxOptions
|
renderCheckboxOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,13 +11,12 @@ export const useRenderRadio = () => {
|
|||||||
typeof defineComponent
|
typeof defineComponent
|
||||||
>
|
>
|
||||||
return item?.componentProps?.options?.map((option) => {
|
return item?.componentProps?.options?.map((option) => {
|
||||||
return <Com label={option[labelAlias || 'value']}>{option[valueAlias || 'label']}</Com>
|
const { value, ...other } = option
|
||||||
// const { value, ...other } = option
|
return (
|
||||||
// return (
|
<Com {...other} label={option[valueAlias || 'value']}>
|
||||||
// <Com label={option[labelAlias || 'value']} {...other}>
|
{option[labelAlias || 'label']}
|
||||||
// {option[valueAlias || 'label']}
|
</Com>
|
||||||
// </Com>
|
)
|
||||||
// )
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,9 +36,9 @@ export const useRenderSelect = (slots: Slots) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ElOption
|
<ElOption
|
||||||
|
{...other}
|
||||||
label={labelAlias ? option[labelAlias] : label}
|
label={labelAlias ? option[labelAlias] : label}
|
||||||
value={valueAlias ? option[valueAlias] : value}
|
value={valueAlias ? option[valueAlias] : value}
|
||||||
{...other}
|
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
default: () =>
|
default: () =>
|
||||||
|
@ -52,7 +52,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: ['update:limit', 'update:page', 'register'],
|
emits: ['update:limit', 'update:page', 'register'],
|
||||||
setup(props, { attrs, slots, emit, expose }) {
|
setup(props, { attrs, slots, emit, expose }) {
|
||||||
console.log('attrs', attrs)
|
|
||||||
const elTableRef = ref<ComponentRef<typeof ElTable>>()
|
const elTableRef = ref<ComponentRef<typeof ElTable>>()
|
||||||
|
|
||||||
// 注册
|
// 注册
|
||||||
|
@ -32,6 +32,14 @@ const toHome = () => {
|
|||||||
push('/system/home')
|
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
|
const user = authStore.getUser
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -52,6 +60,12 @@ const user = authStore.getUser
|
|||||||
<ElDropdownItem>
|
<ElDropdownItem>
|
||||||
<ElButton @click="toHome" link>个人主页</ElButton>
|
<ElButton @click="toHome" link>个人主页</ElButton>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem>
|
||||||
|
<ElButton @click="toGitee" link>Gitee</ElButton>
|
||||||
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem>
|
||||||
|
<ElButton @click="toGithub" link>Github</ElButton>
|
||||||
|
</ElDropdownItem>
|
||||||
<ElDropdownItem divided>
|
<ElDropdownItem divided>
|
||||||
<ElButton @click="loginOut" link>退出系统</ElButton>
|
<ElButton @click="loginOut" link>退出系统</ElButton>
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
|
@ -53,9 +53,8 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
if (res) {
|
if (res) {
|
||||||
wsCache.set(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
|
wsCache.set(appStore.getToken, `${res.data.token_type} ${res.data.access_token}`)
|
||||||
// 存储用户信息
|
// 存储用户信息
|
||||||
wsCache.set(appStore.getUserInfo, res.data.user)
|
const auth = useAuthStore()
|
||||||
this.user = res.data.user
|
await auth.getUserInfo()
|
||||||
this.isUser = true
|
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
|
183
kinit-admin/src/views/Dashboard/Map.vue
Normal file
183
kinit-admin/src/views/Dashboard/Map.vue
Normal 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, // 申请好的Web端开发者Key,首次调用 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, // 3D视图下,CircleMarker半径不要超过64px
|
||||||
|
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>
|
@ -129,8 +129,8 @@ const signIn = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
const { getFormData } = methods
|
const { getFormData } = methods
|
||||||
const formData = await getFormData<UserLoginType>()
|
const formData = await getFormData<UserLoginType>()
|
||||||
|
const authStore = useAuthStoreWithOut()
|
||||||
try {
|
try {
|
||||||
const authStore = useAuthStoreWithOut()
|
|
||||||
const res = await authStore.login(formData)
|
const res = await authStore.login(formData)
|
||||||
if (res) {
|
if (res) {
|
||||||
if (!res.data.is_reset_password) {
|
if (!res.data.is_reset_password) {
|
||||||
@ -140,8 +140,10 @@ const signIn = async () => {
|
|||||||
// 是否使用动态路由
|
// 是否使用动态路由
|
||||||
getMenu()
|
getMenu()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
} finally {
|
} catch (e: any) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,8 +97,8 @@ const telephoneCodeLogin = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
const { getFormData } = methods
|
const { getFormData } = methods
|
||||||
const formData = await getFormData<UserLoginType>()
|
const formData = await getFormData<UserLoginType>()
|
||||||
|
const authStore = useAuthStoreWithOut()
|
||||||
try {
|
try {
|
||||||
const authStore = useAuthStoreWithOut()
|
|
||||||
const res = await authStore.login(formData)
|
const res = await authStore.login(formData)
|
||||||
if (res) {
|
if (res) {
|
||||||
if (!res.data.is_reset_password) {
|
if (!res.data.is_reset_password) {
|
||||||
@ -108,8 +108,10 @@ const telephoneCodeLogin = async () => {
|
|||||||
// 是否使用动态路由
|
// 是否使用动态路由
|
||||||
getMenu()
|
getMenu()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
} finally {
|
} catch (e: any) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -127,17 +129,21 @@ const getSMSCode = async () => {
|
|||||||
SMSCodeNumber.value = 60
|
SMSCodeNumber.value = 60
|
||||||
const { getFormData } = methods
|
const { getFormData } = methods
|
||||||
const formData = await getFormData<UserLoginType>()
|
const formData = await getFormData<UserLoginType>()
|
||||||
const res = await postSMSCodeApi({ telephone: formData.telephone })
|
try {
|
||||||
if (res?.data) {
|
const res = await postSMSCodeApi({ telephone: formData.telephone })
|
||||||
let timer = setInterval(() => {
|
if (res?.data) {
|
||||||
SMSCodeNumber.value--
|
let timer = setInterval(() => {
|
||||||
if (SMSCodeNumber.value < 1) {
|
SMSCodeNumber.value--
|
||||||
SMSCodeStatus.value = true
|
if (SMSCodeNumber.value < 1) {
|
||||||
clearInterval(timer)
|
SMSCodeStatus.value = true
|
||||||
}
|
clearInterval(timer)
|
||||||
}, 1000)
|
}
|
||||||
} else {
|
}, 1000)
|
||||||
ElMessage.error('发送失败,请联系管理员')
|
} else {
|
||||||
|
ElMessage.error('发送失败,请联系管理员')
|
||||||
|
SMSCodeStatus.value = true
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
SMSCodeStatus.value = true
|
SMSCodeStatus.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,8 +107,10 @@ const save = async () => {
|
|||||||
if (res) {
|
if (res) {
|
||||||
// 是否使用动态路由
|
// 是否使用动态路由
|
||||||
getMenu()
|
getMenu()
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
} finally {
|
} catch (e: any) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { useValidator } from '@/hooks/web/useValidator'
|
|||||||
import { getMenuTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
import { getMenuTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
||||||
import { ElButton, ElInput } from 'element-plus'
|
import { ElButton, ElInput } from 'element-plus'
|
||||||
import { schema } from './menu.data'
|
import { schema } from './menu.data'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
|
||||||
const { required } = useValidator()
|
const { required } = useValidator()
|
||||||
|
|
||||||
@ -13,7 +14,8 @@ const props = defineProps({
|
|||||||
currentRow: {
|
currentRow: {
|
||||||
type: Object as PropType<Nullable<any>>,
|
type: Object as PropType<Nullable<any>>,
|
||||||
default: () => null
|
default: () => null
|
||||||
}
|
},
|
||||||
|
parentId: propTypes.number.def(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
@ -53,6 +55,10 @@ const getMenuTreeOptions = async () => {
|
|||||||
value: res.data
|
value: res.data
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
if (props.parentId) {
|
||||||
|
const { setValue } = methods
|
||||||
|
setValue('parent_id', props.parentId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export const columns = reactive<TableColumn[]>([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: 'action',
|
field: 'action',
|
||||||
width: '150px',
|
width: '200px',
|
||||||
label: '操作',
|
label: '操作',
|
||||||
show: true
|
show: true
|
||||||
}
|
}
|
||||||
@ -77,7 +77,9 @@ export const schema = reactive<FormSchema[]>([
|
|||||||
width: '100%'
|
width: '100%'
|
||||||
},
|
},
|
||||||
checkStrictly: true,
|
checkStrictly: true,
|
||||||
placeholder: '请选择上级菜单'
|
placeholder: '请选择上级菜单',
|
||||||
|
nodeKey: 'value',
|
||||||
|
defaultExpandAll: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -130,6 +132,11 @@ export const schema = reactive<FormSchema[]>([
|
|||||||
component: 'InputNumber',
|
component: 'InputNumber',
|
||||||
colProps: {
|
colProps: {
|
||||||
span: 12
|
span: 12
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
style: {
|
||||||
|
width: '100%'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -216,6 +223,6 @@ export const schema = reactive<FormSchema[]>([
|
|||||||
colProps: {
|
colProps: {
|
||||||
span: 12
|
span: 12
|
||||||
},
|
},
|
||||||
ifshow: (values) => values.menu_type !== '0'
|
ifshow: (values) => values.menu_type === '2'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
@ -45,9 +45,11 @@ const dialogVisible = ref(false)
|
|||||||
const dialogTitle = ref('')
|
const dialogTitle = ref('')
|
||||||
const delLoading = ref(false)
|
const delLoading = ref(false)
|
||||||
const actionType = ref('')
|
const actionType = ref('')
|
||||||
|
const parentId = ref()
|
||||||
|
|
||||||
// 添加事件
|
// 添加事件
|
||||||
const AddAction = () => {
|
const AddAction = () => {
|
||||||
|
parentId.value = null
|
||||||
dialogTitle.value = t('exampleDemo.add')
|
dialogTitle.value = t('exampleDemo.add')
|
||||||
tableObject.currentRow = null
|
tableObject.currentRow = null
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@ -56,6 +58,7 @@ const AddAction = () => {
|
|||||||
|
|
||||||
// 编辑事件
|
// 编辑事件
|
||||||
const updateAction = (row: any) => {
|
const updateAction = (row: any) => {
|
||||||
|
parentId.value = null
|
||||||
dialogTitle.value = '编辑'
|
dialogTitle.value = '编辑'
|
||||||
tableObject.currentRow = row
|
tableObject.currentRow = row
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
@ -64,6 +67,7 @@ const updateAction = (row: any) => {
|
|||||||
|
|
||||||
// 删除事件
|
// 删除事件
|
||||||
const delData = async (row: any) => {
|
const delData = async (row: any) => {
|
||||||
|
parentId.value = null
|
||||||
tableObject.currentRow = row
|
tableObject.currentRow = row
|
||||||
const { delListApi } = methods
|
const { delListApi } = methods
|
||||||
delLoading.value = true
|
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 loading = ref(false)
|
||||||
|
|
||||||
const writeRef = ref<ComponentRef<typeof Write>>()
|
const writeRef = ref<ComponentRef<typeof Write>>()
|
||||||
@ -124,7 +137,9 @@ watch(
|
|||||||
<div class="mb-8px flex justify-between">
|
<div class="mb-8px flex justify-between">
|
||||||
<ElRow :gutter="10">
|
<ElRow :gutter="10">
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5">
|
||||||
<ElButton type="primary" @click="AddAction">新增菜单</ElButton>
|
<ElButton type="primary" v-hasPermi="['auth.menu.create']" @click="AddAction"
|
||||||
|
>新增菜单</ElButton
|
||||||
|
>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
<RightToolbar @get-list="getList" v-model:table-size="tableSize" v-model:columns="columns" />
|
<RightToolbar @get-list="getList" v-model:table-size="tableSize" v-model:columns="columns" />
|
||||||
@ -149,10 +164,31 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #action="{ row }">
|
<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') }}
|
{{ t('exampleDemo.edit') }}
|
||||||
</ElButton>
|
</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') }}
|
{{ t('exampleDemo.del') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
@ -173,7 +209,7 @@ watch(
|
|||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
<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>
|
<template #footer>
|
||||||
<ElButton type="primary" :loading="loading" @click="save">
|
<ElButton type="primary" :loading="loading" @click="save">
|
||||||
|
@ -5,7 +5,7 @@ import { PropType, reactive, watch, ref } from 'vue'
|
|||||||
import { useValidator } from '@/hooks/web/useValidator'
|
import { useValidator } from '@/hooks/web/useValidator'
|
||||||
import { getMenuRoleTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
import { getMenuRoleTreeOptionsApi } from '@/api/vadmin/auth/menu'
|
||||||
import { schema } from './role.data'
|
import { schema } from './role.data'
|
||||||
import { ElTree } from 'element-plus'
|
import { ElTree, ElCheckbox } from 'element-plus'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
|
|
||||||
const { required } = useValidator()
|
const { required } = useValidator()
|
||||||
@ -50,7 +50,7 @@ const defaultProps = {
|
|||||||
children: 'children',
|
children: 'children',
|
||||||
label: 'label'
|
label: 'label'
|
||||||
}
|
}
|
||||||
let data = ref([])
|
let data = ref([] as Recordable[])
|
||||||
|
|
||||||
const getMenuRoleTreeOptions = async () => {
|
const getMenuRoleTreeOptions = async () => {
|
||||||
const res = await getMenuRoleTreeOptionsApi()
|
const res = await getMenuRoleTreeOptionsApi()
|
||||||
@ -72,23 +72,72 @@ defineExpose({
|
|||||||
getFormData: methods.getFormData,
|
getFormData: methods.getFormData,
|
||||||
getTreeCheckedKeys: getTreeCheckedKeys
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Form :rules="rules" @register="register">
|
<Form :rules="rules" @register="register">
|
||||||
<template #menu_ids>
|
<template #menu_ids>
|
||||||
<ElTree
|
<div>
|
||||||
ref="treeRef"
|
<div>
|
||||||
:data="data"
|
<ElCheckbox
|
||||||
show-checkbox
|
v-model="defaultExpandAll"
|
||||||
node-key="value"
|
@change="handleCheckedTreeExpand"
|
||||||
:props="defaultProps"
|
label="展开/折叠"
|
||||||
:default-checked-keys="defaultCheckedKeys"
|
size="large"
|
||||||
>
|
/>
|
||||||
<template #default="{ node }">
|
<ElCheckbox
|
||||||
<span>{{ t(node.label) }}</span>
|
v-model="selectAll"
|
||||||
</template>
|
@change="handleCheckedTreeNodeAll"
|
||||||
</ElTree>
|
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>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,6 +53,7 @@ const updateAction = async (row: any) => {
|
|||||||
dialogTitle.value = '编辑'
|
dialogTitle.value = '编辑'
|
||||||
tableObject.currentRow = res.data
|
tableObject.currentRow = res.data
|
||||||
defaultCheckedKeys.value = res.data.menus.map((item: any) => item.id)
|
defaultCheckedKeys.value = res.data.menus.map((item: any) => item.id)
|
||||||
|
console.log(defaultCheckedKeys.value)
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
actionType.value = 'edit'
|
actionType.value = 'edit'
|
||||||
}
|
}
|
||||||
@ -123,7 +124,7 @@ watch(
|
|||||||
|
|
||||||
<div class="mb-8px flex justify-between">
|
<div class="mb-8px flex justify-between">
|
||||||
<ElRow :gutter="10">
|
<ElRow :gutter="10">
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5" v-hasPermi="['auth.role.create']">
|
||||||
<ElButton type="primary" @click="AddAction">新增角色</ElButton>
|
<ElButton type="primary" @click="AddAction">新增角色</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
@ -144,10 +145,24 @@ watch(
|
|||||||
@register="register"
|
@register="register"
|
||||||
>
|
>
|
||||||
<template #action="{ row }">
|
<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') }}
|
{{ t('exampleDemo.edit') }}
|
||||||
</ElButton>
|
</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') }}
|
{{ t('exampleDemo.del') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
@ -161,7 +176,7 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px">
|
<Dialog v-model="dialogVisible" :title="dialogTitle" width="700px" maxHeight="600px">
|
||||||
<Write
|
<Write
|
||||||
ref="writeRef"
|
ref="writeRef"
|
||||||
:current-row="tableObject.currentRow"
|
:current-row="tableObject.currentRow"
|
||||||
|
@ -179,19 +179,19 @@ const sendPasswordToSMS = async () => {
|
|||||||
|
|
||||||
<div class="mb-8px flex justify-between">
|
<div class="mb-8px flex justify-between">
|
||||||
<ElRow :gutter="10">
|
<ElRow :gutter="10">
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5" v-hasPermi="['auth.user.create']">
|
||||||
<ElButton type="primary" @click="AddAction">新增用户</ElButton>
|
<ElButton type="primary" @click="AddAction">新增用户</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5" v-hasPermi="['auth.user.import']">
|
||||||
<ElButton @click="importList">批量导入用户</ElButton>
|
<ElButton @click="importList">批量导入用户</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5" v-hasPermi="['auth.user.export']">
|
||||||
<ElButton @click="exportQueryList">导出筛选用户</ElButton>
|
<ElButton @click="exportQueryList">导出筛选用户</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5" v-hasPermi="['auth.user.reset']">
|
||||||
<ElButton @click="sendPasswordToSMS">重置密码通知短信</ElButton>
|
<ElButton @click="sendPasswordToSMS">重置密码通知短信</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
<ElCol :span="1.5">
|
<ElCol :span="1.5" v-hasPermi="['auth.user.delete']">
|
||||||
<ElButton type="danger" @click="delDatas(null, true)">批量删除</ElButton>
|
<ElButton type="danger" @click="delDatas(null, true)">批量删除</ElButton>
|
||||||
</ElCol>
|
</ElCol>
|
||||||
</ElRow>
|
</ElRow>
|
||||||
@ -212,11 +212,18 @@ const sendPasswordToSMS = async () => {
|
|||||||
@register="register"
|
@register="register"
|
||||||
>
|
>
|
||||||
<template #action="{ row }">
|
<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') }}
|
{{ t('exampleDemo.edit') }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
type="danger"
|
type="danger"
|
||||||
|
v-hasPermi="['auth.user.delete']"
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
@click="delDatas(row, false)"
|
@click="delDatas(row, false)"
|
||||||
|
@ -273,7 +273,7 @@ class MenuDal(DalBase):
|
|||||||
3:获取菜单树列表,角色添加菜单权限时使用
|
3:获取菜单树列表,角色添加菜单权限时使用
|
||||||
"""
|
"""
|
||||||
if mode == 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:
|
else:
|
||||||
sql = select(self.model)
|
sql = select(self.model)
|
||||||
queryset = await self.db.execute(sql)
|
queryset = await self.db.execute(sql)
|
||||||
|
@ -10,20 +10,16 @@ from apps.vadmin.auth import crud, models
|
|||||||
from .validation import AuthValidation, Auth
|
from .validation import AuthValidation, Auth
|
||||||
|
|
||||||
|
|
||||||
async def get_user_permissions(user: models.VadminUser, db: AsyncSession):
|
async def get_user_permissions(user):
|
||||||
"""
|
"""
|
||||||
获取跟进系统用户所有权限列表
|
获取跟进系统用户所有权限列表
|
||||||
"""
|
"""
|
||||||
roles = []
|
if any([role.is_admin for role in user.roles]):
|
||||||
for i in user.roles:
|
return ['*.*.*']
|
||||||
if i.is_admin:
|
|
||||||
return ["*:*:*"]
|
|
||||||
roles.append(i.id)
|
|
||||||
permissions = set()
|
permissions = set()
|
||||||
for data_id in roles:
|
for role_obj in user.roles:
|
||||||
role_obj = await crud.RoleDal(db).get_data(data_id, options=[models.VadminUser])
|
|
||||||
for menu in role_obj.menus:
|
for menu in role_obj.menus:
|
||||||
if menu.perms and menu.status:
|
if menu.perms and not menu.disabled:
|
||||||
permissions.add(menu.perms)
|
permissions.add(menu.perms)
|
||||||
return list(permissions)
|
return list(permissions)
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
# @IDE : PyCharm
|
# @IDE : PyCharm
|
||||||
# @desc : 安全认证视图
|
# @desc : 安全认证视图
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
JWT 表示 「JSON Web Tokens」。https://jwt.io/
|
JWT 表示 「JSON Web Tokens」。https://jwt.io/
|
||||||
|
|
||||||
@ -16,13 +15,13 @@ JWT 表示 「JSON Web Tokens」。https://jwt.io/
|
|||||||
|
|
||||||
我们需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌:pip install python-jose[cryptography]
|
我们需要安装 python-jose 以在 Python 中生成和校验 JWT 令牌:pip install python-jose[cryptography]
|
||||||
|
|
||||||
PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。推荐的算法是 「Bcrypt」:pip install passlib[bcrypt]
|
PassLib 是一个用于处理哈希密码的很棒的 Python 包。它支持许多安全哈希算法以及配合算法使用的实用程序。
|
||||||
|
推荐的算法是 「Bcrypt」:pip install passlib[bcrypt]
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from core.database import db_getter
|
from core.database import db_getter
|
||||||
from utils.response import SuccessResponse, ErrorResponse
|
from utils.response import SuccessResponse, ErrorResponse
|
||||||
from application import settings
|
from application import settings
|
||||||
@ -45,33 +44,22 @@ async def login_for_access_token(request: Request, data: LoginForm, manage: Logi
|
|||||||
else:
|
else:
|
||||||
return ErrorResponse(msg="请使用正确的登录方式")
|
return ErrorResponse(msg="请使用正确的登录方式")
|
||||||
if not result.status:
|
if not result.status:
|
||||||
res = {"message": result.msg}
|
resp = {"message": result.msg}
|
||||||
telephone = data.telephone
|
telephone = data.telephone
|
||||||
await VadminLoginRecord.\
|
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)
|
return ErrorResponse(msg=result.msg)
|
||||||
|
|
||||||
user = result.user
|
user = result.user
|
||||||
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
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)
|
access_token = LoginManage.create_access_token(data={"sub": user.telephone}, expires_delta=access_token_expires)
|
||||||
res = {
|
resp = {
|
||||||
"access_token": access_token,
|
"access_token": access_token,
|
||||||
"token_type": "bearer",
|
"token_type": "bearer",
|
||||||
"is_reset_password": user.is_reset_password,
|
"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]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await VadminLoginRecord.\
|
await VadminLoginRecord.create_login_record(db, user.telephone, result.status, request, resp)
|
||||||
create_login_record(telephone=user.telephone, status=result.status, request=request, response=res, db=db)
|
return SuccessResponse(resp)
|
||||||
return SuccessResponse(res)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/getMenuList/", summary="获取当前用户菜单树")
|
@app.get("/getMenuList/", summary="获取当前用户菜单树")
|
||||||
|
@ -44,7 +44,7 @@ class LoginValidation:
|
|||||||
|
|
||||||
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
|
async def __call__(self, data: LoginForm, db: AsyncSession, request: Request) -> LoginResult:
|
||||||
self.result = 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)
|
user = await crud.UserDal(db).get_data(telephone=data.telephone, return_none=True, options=options)
|
||||||
if not user:
|
if not user:
|
||||||
self.result.msg = "该手机号不存在!"
|
self.result.msg = "该手机号不存在!"
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
from fastapi import APIRouter, Depends, Body, UploadFile, Request
|
from fastapi import APIRouter, Depends, Body, UploadFile, Request
|
||||||
from utils.response import SuccessResponse, ErrorResponse
|
from utils.response import SuccessResponse, ErrorResponse
|
||||||
from . import schemas, crud, models
|
from . import schemas, crud, models
|
||||||
from core.dependencies import Paging, IdList
|
from core.dependencies import IdList
|
||||||
from apps.vadmin.auth.utils.current import login_auth, Auth
|
from apps.vadmin.auth.utils.current import login_auth, Auth, get_user_permissions, full_admin
|
||||||
from .params import UserParams, RoleParams
|
from .params import UserParams, RoleParams
|
||||||
|
|
||||||
app = APIRouter()
|
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="获取当前用户基本信息")
|
@app.get("/user/current/info/", summary="获取当前用户基本信息")
|
||||||
async def get_user_current_info(auth: Auth = Depends(login_auth)):
|
async def get_user_current_info(auth: Auth = Depends(full_admin)):
|
||||||
return SuccessResponse(schemas.UserSimpleOut.from_orm(auth.user).dict())
|
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="获取当前用户基本信息")
|
@app.get("/user/current/info/", summary="获取当前用户基本信息")
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
# @File : crud.py
|
# @File : crud.py
|
||||||
# @IDE : PyCharm
|
# @IDE : PyCharm
|
||||||
# @desc : 数据库 增删改查操作
|
# @desc : 数据库 增删改查操作
|
||||||
|
import random
|
||||||
|
from typing import List
|
||||||
|
|
||||||
# sqlalchemy 查询操作:https://segmentfault.com/a/1190000016767008
|
# sqlalchemy 查询操作:https://segmentfault.com/a/1190000016767008
|
||||||
# sqlalchemy 关联查询:https://www.jianshu.com/p/dfad7c08c57a
|
# sqlalchemy 关联查询:https://www.jianshu.com/p/dfad7c08c57a
|
||||||
@ -15,9 +17,56 @@ from core.crud import DalBase
|
|||||||
|
|
||||||
|
|
||||||
class LoginRecordDal(DalBase):
|
class LoginRecordDal(DalBase):
|
||||||
|
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
super(LoginRecordDal, self).__init__(db, models.VadminLoginRecord, schemas.LoginRecordSimpleOut)
|
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):
|
class SMSSendRecordDal(DalBase):
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
|
@ -35,24 +35,22 @@ class VadminLoginRecord(BaseModel):
|
|||||||
request = Column(TEXT, comment="请求信息")
|
request = Column(TEXT, comment="请求信息")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_login_record(cls, telephone: str, status: bool, request: Request, response: dict,
|
async def create_login_record(cls, db: AsyncSession, telephone: str, status: bool, req: Request, resp: dict):
|
||||||
db: AsyncSession):
|
|
||||||
"""
|
"""
|
||||||
创建登录记录
|
创建登录记录
|
||||||
@return:
|
@return:
|
||||||
"""
|
"""
|
||||||
header = {}
|
header = {}
|
||||||
for k, v in request.headers.items():
|
for k, v in req.headers.items():
|
||||||
header[k] = v
|
header[k] = v
|
||||||
body = json.loads((await request.body()).decode())
|
body = json.loads((await req.body()).decode())
|
||||||
user_agent = parse(request.headers.get("user-agent"))
|
user_agent = parse(req.headers.get("user-agent"))
|
||||||
system = f"{user_agent.os.family} {user_agent.os.version_string}"
|
system = f"{user_agent.os.family} {user_agent.os.version_string}"
|
||||||
browser = f"{user_agent.browser.family} {user_agent.browser.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()
|
location = await ip.parse()
|
||||||
resp = json.dumps(response)
|
params = json.dumps({"body": body, "headers": header})
|
||||||
resq = json.dumps({"body": body, "headers": header})
|
|
||||||
obj = VadminLoginRecord(**location.dict(), telephone=telephone, status=status, browser=browser,
|
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)
|
db.add(obj)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
@ -5,9 +5,7 @@
|
|||||||
# @IDE : PyCharm
|
# @IDE : PyCharm
|
||||||
# @desc : 主要接口文件
|
# @desc : 主要接口文件
|
||||||
|
|
||||||
from typing import Optional
|
from fastapi import APIRouter, Depends
|
||||||
from fastapi import APIRouter, Depends, Query
|
|
||||||
from core.dependencies import Paging, IdList
|
|
||||||
from utils.response import SuccessResponse
|
from utils.response import SuccessResponse
|
||||||
from . import crud, schemas
|
from . import crud, schemas
|
||||||
from apps.vadmin.auth.utils.current import login_auth, Auth
|
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())
|
datas = await crud.SMSSendRecordDal(auth.db).get_datas(**params.dict())
|
||||||
count = await crud.SMSSendRecordDal(auth.db).get_count(**params.to_count())
|
count = await crud.SMSSendRecordDal(auth.db).get_count(**params.to_count())
|
||||||
return SuccessResponse(datas, count=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())
|
||||||
|
@ -22,14 +22,12 @@ pip install alibabacloud_dysmsapi20170525
|
|||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import uuid
|
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
from core.exception import CustomException
|
from core.exception import CustomException
|
||||||
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
|
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
|
||||||
from alibabacloud_tea_openapi import models as open_api_models
|
from alibabacloud_tea_openapi import models as open_api_models
|
||||||
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
|
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
|
||||||
from alibabacloud_tea_util import models as util_models
|
from alibabacloud_tea_util import models as util_models
|
||||||
from alibabacloud_tea_util.client import Client as UtilClient
|
|
||||||
from core.logger import logger
|
from core.logger import logger
|
||||||
import datetime
|
import datetime
|
||||||
from aioredis.client import Redis
|
from aioredis.client import Redis
|
||||||
@ -44,8 +42,8 @@ class AliyunSMS:
|
|||||||
|
|
||||||
@unique
|
@unique
|
||||||
class Scene(Enum):
|
class Scene(Enum):
|
||||||
login = "template_code_1"
|
login = "sms_template_code_1"
|
||||||
reset_password = "template_code_2"
|
reset_password = "sms_template_code_2"
|
||||||
|
|
||||||
def __init__(self, rd: Redis, telephone: str):
|
def __init__(self, rd: Redis, telephone: str):
|
||||||
self.check_telephone_format(telephone)
|
self.check_telephone_format(telephone)
|
||||||
@ -67,14 +65,14 @@ class AliyunSMS:
|
|||||||
raise CustomException("获取短信配置信息失败,请联系管理员!", code=status.HTTP_ERROR)
|
raise CustomException("获取短信配置信息失败,请联系管理员!", code=status.HTTP_ERROR)
|
||||||
else:
|
else:
|
||||||
aliyun_sms = json.loads(aliyun_sms)
|
aliyun_sms = json.loads(aliyun_sms)
|
||||||
self.access_key = aliyun_sms.get("access_key")
|
self.access_key = aliyun_sms.get("sms_access_key")
|
||||||
self.access_key_secret = aliyun_sms.get("access_key_secret")
|
self.access_key_secret = aliyun_sms.get("sms_access_key_secret")
|
||||||
self.send_interval = int(aliyun_sms.get("send_interval"))
|
self.send_interval = int(aliyun_sms.get("sms_send_interval"))
|
||||||
self.valid_time = int(aliyun_sms.get("valid_time"))
|
self.valid_time = int(aliyun_sms.get("sms_valid_time"))
|
||||||
if self.scene == self.Scene.login:
|
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:
|
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)
|
self.template_code = aliyun_sms.get(self.scene.value)
|
||||||
|
|
||||||
async def main_async(self, scene: Scene, **kwargs) -> bool:
|
async def main_async(self, scene: Scene, **kwargs) -> bool:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user