This commit is contained in:
ljw
2024-09-13 16:34:15 +08:00
commit 364064e5ce
62 changed files with 8448 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
ENV = 'development'
VITE_DEV_PORT = 8888
VITE_SERVER_API = /api/admin
VITE_SERVER_PATH = http://127.0.0.1:21114
+5
View File
@@ -0,0 +1,5 @@
ENV = 'production'
VITE_DEV_PORT = 5000
VITE_SERVER_API =/api/admin
VITE_SERVER_PATH = http://127.0.0.1:5000
+20
View File
@@ -0,0 +1,20 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016-2021 vue-manage-system
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
# Gwen-Admin
# 基于 Vue3 + Element Plus 的后台管理系统
<a href="https://github.com/vuejs/vue-next">
<img src="https://img.shields.io/badge/vue-^3.2.16-brightgreen.svg" alt="vue3">
</a>
<a href="https://github.com/element-plus/element-plus">
<img src="https://img.shields.io/badge/element--plus-^1.2.0--beta.1-brightgreen.svg" alt="element-plus">
</a>
<a href="https://github.com/lejianwen/Gwen-admin/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/mashape/apistatus.svg" alt="license">
</a>
# 安装步骤
~~~shell script
git clone https://github.com/lejianwen/Gwen-admin.git
cd Gwen-admin
npm install
// 本地开发
npm run dev
// 打包
npm run build
// 本地预览
npm run server
~~~
## 功能
- [x] Element Plus
- [x] 登录/注销
- [x] 路由权限
- [x] Dashboard
- [x] 表格
- [x] 表单
- [x] 图片本地/oss上传
- [x] 404
- [x] 多级菜单
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gwen-Admin</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules"
]
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "hello-vue3",
"version": "0.0.0",
"scripts": {
"dev": "vite --host",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"axios": "1.6.0",
"element-plus": "^2.8.2",
"js-cookie": "^3.0.1",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"pinia": "2.0.3",
"vue": "3.2.37",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@element-plus/icons": "0.0.11",
"@vitejs/plugin-vue": "^1.9.3",
"dotenv": "^10.0.0",
"qs": "^6.10.2",
"sass-loader": "^12.3.0",
"sass": "^1.43.4",
"vite": "^2.9.18"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+19
View File
@@ -0,0 +1,19 @@
<template>
<router-view/>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
props: {},
setup (props) {
},
created () {
},
})
</script>
<style>
</style>
+46
View File
@@ -0,0 +1,46 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/address_book/list',
params,
})
}
export function detail (id) {
return request({
url: `/address_book/detail/${id}`,
})
}
export function create (data) {
return request({
url: '/address_book/create',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/address_book/update',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/address_book/delete',
method: 'post',
data,
})
}
export function changePwd (data) {
return request({
url: '/address_book/changePwd',
method: 'post',
data,
})
}
+7
View File
@@ -0,0 +1,7 @@
import request from '@/utils/request'
export function ossToken () {
return request({
url: '/file/oss_token',
})
}
+46
View File
@@ -0,0 +1,46 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/group/list',
params,
})
}
export function detail (id) {
return request({
url: `/group/detail/${id}`,
})
}
export function create (data) {
return request({
url: '/group/create',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/group/update',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/group/delete',
method: 'post',
data,
})
}
export function changePwd (data) {
return request({
url: '/group/changePwd',
method: 'post',
data,
})
}
+46
View File
@@ -0,0 +1,46 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/peer/list',
params,
})
}
export function detail (id) {
return request({
url: `/peer/detail/${id}`,
})
}
export function create (data) {
return request({
url: '/peer/create',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/peer/update',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/peer/delete',
method: 'post',
data,
})
}
export function changePwd (data) {
return request({
url: '/peer/changePwd',
method: 'post',
data,
})
}
+8
View File
@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function config () {
return request({
url: '/server-config',
method: 'get',
})
}
+46
View File
@@ -0,0 +1,46 @@
import request from '@/utils/request'
export function list (params) {
return request({
url: '/tag/list',
params,
})
}
export function detail (id) {
return request({
url: `/tag/detail/${id}`,
})
}
export function create (data) {
return request({
url: '/tag/create',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/tag/update',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/tag/delete',
method: 'post',
data,
})
}
export function changePwd (data) {
return request({
url: '/tag/changePwd',
method: 'post',
data,
})
}
+69
View File
@@ -0,0 +1,69 @@
import request from '@/utils/request'
export function login (data) {
return request({
url: '/login',
method: 'post',
data,
})
}
export function current () {
return request({
url: '/user/current',
method: 'get',
})
}
export function list (params) {
return request({
url: '/user/list',
params,
})
}
export function detail (id) {
return request({
url: `/user/detail/${id}`,
})
}
export function create (data) {
return request({
url: '/user/create',
method: 'post',
data,
})
}
export function update (data) {
return request({
url: '/user/update',
method: 'post',
data,
})
}
export function remove (data) {
return request({
url: '/user/delete',
method: 'post',
data,
})
}
export function changePwd (data) {
return request({
url: '/user/changePwd',
method: 'post',
data,
})
}
export function changeCurPwd (data) {
return request({
url: '/user/changeCurPwd',
method: 'post',
data,
})
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

+99
View File
@@ -0,0 +1,99 @@
<template>
<el-form-item ref="formAddress" :label="label" :prop="prop">
<el-select v-model="currentProvince" clearable placeholder="省" @change="changeProvince">
<el-option v-for="(_, name) in pca" :key="name" :label="name" :value="name"/>
</el-select>
<el-select v-model="currentCity" clearable placeholder="市" @change="changeCity">
<el-option v-for="(_, name) in cities" :key="name" :label="name" :value="name"/>
</el-select>
<el-select v-model="currentCounty" clearable placeholder="区" @change="changeCounty">
<el-option v-for="item in counties" :key="item" :label="item" :value="item"/>
</el-select>
</el-form-item>
</template>
<script>
import { defineComponent, ref, computed } from 'vue'
import pca from '@/utils/pca.json'
export default defineComponent({
name: 'FormAddress',
props: {
prop: {
type: String,
default: '',
},
label: {
type: String,
default: '省/市/区',
},
province: {
type: String,
default: '',
},
city: {
type: String,
default: '',
},
county: {
type: String,
default: '',
},
},
setup (props, context) {
const cities = computed(() => pca[props.province] || [])
const counties = computed(() => pca[props.province] && pca[props.province][props.city] ? pca[props.province][props.city] : [])
let currentProvince = computed({
get: () => props.province,
set: (val) => {
context.emit('update:province', val)
},
})
let currentCity = computed({
get: () => props.city,
set: (val) => {
context.emit('update:city', val)
},
})
let currentCounty = computed({
get: () => props.county,
set: (val) => {
context.emit('update:county', val)
},
})
const changeProvince = (val) => {
currentCity = ''
currentCounty = ''
context.emit('changeProvince', val)
}
const changeCity = (val) => {
currentCounty = ''
context.emit('changeCity', val)
}
const changeCounty = (val) => {
context.emit('changeCounty', val)
}
return {
pca,
cities,
counties,
currentProvince,
currentCity,
currentCounty,
changeProvince,
changeCity,
changeCounty,
}
},
})
</script>
<style scoped>
</style>
+198
View File
@@ -0,0 +1,198 @@
<template>
<div class="upload-order-file">
<el-upload
size="mini"
ref="upload"
:on-success="fileUploadSuccess"
:before-upload="beforeFileUpload"
:on-preview="onPreview"
:on-remove="fileRemove"
:on-error="onError"
name="file"
:file-list="fileList"
:action="fileUploadHost"
:data="fileUploadData"
:headers="headers"
list-type="picture-card"
:limit="0"
accept="image/*"
>
<template #default>
<div class="default-slot">
<slot name="default">
<el-icon class="default-icon">
<plus/>
</el-icon>
</slot>
</div>
</template>
</el-upload>
<el-dialog v-model="showPreview" top="5vh">
<el-image :src="showImage" class="preview-image" fit="contain"></el-image>
</el-dialog>
</div>
</template>
<script>
import { defineComponent, ref, computed, reactive, unref, readonly, toRefs } from 'vue'
import { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check } from '@element-plus/icons'
import { useOss } from '@/components/form/upload/oss'
import { ElMessage } from 'element-plus'
import { useLocal } from '@/components/form/upload/local'
export default defineComponent({
name: 'imageUpload',
props: {
limit: {
type: Number,
default: 0,
},
beforeUpload: {
type: Function,
default: function () {
return true
},
},
host: {
type: String,
default: import.meta.env.VITE_BASE_API + '/file/upload',
},
modelValue: {
type: String,
default: '',
},
type: {
type: String,
default: 'local', //local oss
},
width: {
type: String,
default: '148px',
},
},
components: { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check },
setup (props, context) {
const showPreview = ref(false)
const showImage = ref('')
let fileList = computed(() => props.modelValue ? [{ url: props.modelValue, status: 'success' }] : [])
let fileUpload = reactive({
fileUploadHost: '',
fileUploadData: {},
beforeFileUpload: null,
headers: {},
})
if (props.type === 'oss') {
fileUpload = useOss(props.beforeUpload, props.multiple)
} else {
fileUpload = useLocal(props.beforeUpload, props.host)
}
function removeImage (file) {
let fList = unref(fileList)
const index = fList.findIndex(f => f.url === file.url)
fList.splice(index, 1)
updateValue(fList)
}
function updateValue (_fileList) {
let fList = unref(_fileList)
context.emit(
'update:modelValue',
fList.length ? fList[0].url : '',
)
}
function fileRemove (file, _fileList) {
updateValue(_fileList)
}
function onError () {
}
function fileUploadSuccess (response, file, _fileList) {
file.url = response?.data?.url || file.url
if (_fileList.length > 1) {
_fileList.splice(0, 1)
}
if (_fileList.every(f => f.status === 'success')) {
updateValue(_fileList)
}
}
function onPreview (file) {
showImage.value = file.url
showPreview.value = true
}
return {
fileList,
...toRefs(fileUpload),
fileRemove,
onError,
fileUploadSuccess,
onPreview,
removeImage,
showPreview,
showImage,
}
},
})
</script>
<style scoped lang="scss">
.upload-order-file {
::v-deep(.el-upload-list__item-thumbnail) {
object-fit: contain;
}
::v-deep(.el-upload--picture-card) {
width: v-bind(width);
height: v-bind(width);
}
::v-deep(.el-upload-list__item) {
width: v-bind(width);
height: v-bind(width);
}
::v-deep(.el-progress) {
width: v-bind(width) !important;
height: v-bind(width) !important;
}
::v-deep(.el-progress-circle) {
width: v-bind(width) !important;
height: v-bind(width) !important;
}
.default-slot {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.default-icon {
margin-top: 0;
}
}
}
.preview-image {
width: 100%;
::v-deep(img) {
max-height: 700px;
}
}
</style>
+272
View File
@@ -0,0 +1,272 @@
<template>
<div class="upload-order-file">
<el-upload
ref="upload"
:on-success="fileUploadSuccess"
:before-upload="beforeFileUpload"
:on-remove="fileRemove"
:on-exceed="onExceed"
:on-error="onError"
name="file"
:multiple="multiple"
:file-list="fileList"
:action="fileUploadHost"
:data="fileUploadData"
:headers="headers"
list-type="picture-card"
:limit="limit"
accept="image/*"
:drag="drag"
>
<template #default>
<div class="default-slot">
<slot name="default">
<div>
<el-icon class="default-icon">
<plus/>
</el-icon>
<div class="drag-tips">点击上传<span v-if="drag">或直接拖入文件</span></div>
</div>
</slot>
</div>
</template>
<template #file="{file}">
<img
v-if="file.status === 'success'"
class="el-upload-list__item-thumbnail"
:src="file.url"
alt=""
>
<label class="el-upload-list__item-status-label">
<el-icon color="white">
<check/>
</el-icon>
</label>
<el-progress
v-if="file.status === 'uploading'"
type="circle"
:stroke-width="6"
:percentage="parseInt(file.percentage)"
/>
<span v-else-if="file.status === 'success'" class="el-upload-list__item-actions">
<el-icon class="el-upload-list__item-icon" @click="leftImage(file)"><arrow-left/></el-icon>
<el-icon class="el-upload-list__item-icon" @click="removeImage(file)"><Delete/></el-icon>
<el-icon class="el-upload-list__item-icon" @click="rightImage(file)"><arrow-right/></el-icon>
</span>
</template>
</el-upload>
</div>
</template>
<script>
import { defineComponent, ref, computed, reactive, unref, readonly, toRefs } from 'vue'
import { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check } from '@element-plus/icons'
import { useOss } from '@/components/form/upload/oss'
import { ElMessage } from 'element-plus'
import { useLocal } from '@/components/form/upload/local'
export default defineComponent({
name: 'imagesUpload',
props: {
drag: {
type: Boolean,
default: false,
},
limit: {
type: Number,
default: 0,
},
beforeUpload: {
type: Function,
default: function () {
return true
},
},
host: {
type: String,
default: import.meta.env.VITE_BASE_API + '/file/upload',
},
modelValue: {
type: Array,
default: function () {
return []
},
},
type: {
type: String,
default: 'local', //local oss
},
multiple: {
type: Boolean,
default: false,
},
width: {
type: String,
default: '148px',
},
},
components: { Plus, ZoomIn, Delete, ArrowLeft, ArrowRight, Check },
setup (props, context) {
let fileList = computed(() => props.modelValue.map(url => { return { url, status: 'success' } }))
let fileUpload = reactive({
fileUploadHost: '',
fileUploadData: {},
beforeFileUpload: null,
headers: {},
})
if (props.type === 'oss') {
fileUpload = useOss(props.beforeUpload, props.multiple)
} else {
fileUpload = useLocal(props.beforeUpload, props.host)
}
function leftImage (file) {
let fList = unref(fileList)
const index = fList.findIndex(f => f.url === file.url)
if (index === 0 || index === -1) {
return
}
fList[index] = fList.splice(index - 1, 1, fList[index])[0]
updateValue(fList)
}
function rightImage (file) {
let fList = unref(fileList)
const index = fList.findIndex(f => f.url === file.url)
if (index === fList.length - 1 || index === -1) {
return
}
fList[index] = fList.splice(index + 1, 1, fList[index])[0]
updateValue(fList)
}
function removeImage (file) {
let fList = unref(fileList)
const index = fList.findIndex(f => f.url === file.url)
fList.splice(index, 1)
updateValue(fList)
}
function updateValue (_fileList) {
let fList = unref(_fileList)
context.emit(
'update:modelValue',
fList.filter(f => f.status === 'success').map(file => file.url),
)
}
function fileRemove (file, _fileList) {
updateValue(_fileList)
}
function onError () {
}
function fileUploadSuccess (response, file, _fileList) {
file.url = response?.data?.url || file.url
if (_fileList.every(f => f.status === 'success')) {
updateValue(_fileList)
}
}
function onExceed () {
ElMessage.error('超出数量限制')
}
return {
fileList,
...toRefs(fileUpload),
onExceed,
fileRemove,
onError,
fileUploadSuccess,
leftImage,
rightImage,
removeImage,
}
},
})
</script>
<style scoped lang="scss">
.upload-order-file {
::v-deep(.el-upload-dragger) {
border: none;
width: 100%;
height: 100%;
}
::v-deep(.el-upload--picture-card) {
width: v-bind(width);
height: v-bind(width);
}
::v-deep(.el-upload-list__item) {
width: v-bind(width);
height: v-bind(width);
}
::v-deep(.el-progress) {
width: v-bind(width) !important;
height: v-bind(width) !important;
}
::v-deep(.el-progress-circle) {
width: v-bind(width) !important;
height: v-bind(width) !important;
}
.drag-tips {
font-size: 12px;
color: #999;
}
::v-deep(.el-upload-list__item) {
transition: none !important;
}
::v-deep(.el-upload-list) {
transition: none !important;
}
.el-upload-list__item-thumbnail {
object-fit: contain;
}
.el-upload-list__item-actions {
display: flex;
justify-content: space-around;
align-items: center;
&:after {
display: none;
}
.el-upload-list__item-icon {
cursor: pointer;
font-size: 20px;
color: #fff;
}
}
.default-slot {
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.default-icon {
margin-top: 0;
}
}
}
</style>
+21
View File
@@ -0,0 +1,21 @@
import { getToken } from '@/utils/auth'
export function useLocal (beforeUp, host) {
const fileUploadData = {}
const fileUploadHost = host
const headers = { 'api-token': getToken() }
const beforeFileUpload = async (file) => {
if (beforeUp) {
const br = await beforeUp(file)
if (!br) { return Promise.reject() }
}
return Promise.resolve()
}
return {
fileUploadData,
fileUploadHost,
beforeFileUpload,
headers,
}
}
+52
View File
@@ -0,0 +1,52 @@
import { ossToken } from '@/api/file'
import { random_filename } from '@/utils/file'
import { reactive, ref } from 'vue'
export function useOss (beforeUp, multiple) {
let fileUploadData = reactive({
policy: '',
OSSAccessKeyId: '',
success_action_status: '200', // 让服务端返回200,不然,默认会返回204
callback: '',
signature: '',
'x:dir': '',
})
const fileExpire = ref(0)
const fileUploadHost = ref('')
const beforeFileUpload = async (file) => {
if (beforeUp) {
const br = await beforeUp(file)
if (!br) { return Promise.reject() }
}
const now = Date.parse(new Date()) / 1000
if (fileExpire.value < now) {
const res = await ossToken()
const obj = JSON.parse(res.data)
fileExpire.value = parseInt(obj['expire'])
fileUploadData.policy = obj['policy']
fileUploadData.OSSAccessKeyId = obj['accessid']
fileUploadData.callback = obj['callback']
fileUploadData.signature = obj['signature']
fileUploadData['x:dir'] = obj['dir']
fileUploadHost.value = obj['host']
}
//多选文件时需要这个,不然每个文件上传的都是一样的data
if (multiple) {
await new Promise(resolve => {
setTimeout(() => { resolve() }, 50)
})
}
fileUploadData['x:origin_filename'] = file.name
fileUploadData.key = fileUploadData['x:dir'] + random_filename(file.name)
return Promise.resolve()
}
return {
fileUploadHost,
fileUploadData,
beforeFileUpload,
headers: {},
}
}
+19
View File
@@ -0,0 +1,19 @@
import { ref, reactive, watch } from 'vue'
import { list as fetchUsers } from '@/api/user'
export function loadAllUsers () {
const allUsers = ref([])
const getAllUsers = async () => {
const res = await fetchUsers({ page_size: 9999 }).catch(_ => false)
if (res) {
allUsers.value = res.data.list
}
}
return {
allUsers,
getAllUsers,
}
}
+20
View File
@@ -0,0 +1,20 @@
<template>
<el-scrollbar class="scroll-sidebar" height="100vh">
<menus></menus>
</el-scrollbar>
</template>
<script>
import Menus from '@/layout/components/menu/index.vue'
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
name: 'GAside',
components: { Menus },
})
</script>
<style scoped>
.scroll-sidebar{
position: fixed;
}
</style>
+72
View File
@@ -0,0 +1,72 @@
<template>
<el-icon class="ex-icon" @click="expandOrFoldSlider">
<el-icon-expand v-if="setting.sideIsCollapse"></el-icon-expand>
<el-icon-fold v-else></el-icon-fold>
</el-icon>
<div class="header-logo">
<img :src="setting.logo" alt="" class="logo">
<div class="title">{{setting.title}}</div>
</div>
<Setting></Setting>
</template>
<script>
import { defineComponent, computed } from 'vue'
import HeaderMenu from '@/layout/components/menu/index.vue'
import Setting from '@/layout/components/setting/index.vue'
import { useAppStore } from '@/store/app'
import GTags from '@/layout/components/tags/index.vue'
export default defineComponent({
name: 'LayerHeader',
created () {
},
components: { HeaderMenu, Setting, GTags },
watch: {},
setup (props) {
const appStore = useAppStore()
const setting = computed(() => appStore.setting)
const expandOrFoldSlider = () => {
appStore.sideCollapse()
}
return {
setting,
expandOrFoldSlider,
}
},
})
</script>
<style scoped lang="scss">
.ex-icon {
height: 100%;
display: flex;
align-items: center;
margin-right: 10px;
font-size: 16px;
cursor: pointer;
}
.header-logo {
display: flex;
height: 100%;
align-items: center;
.title {
display: block;
margin-left: 10px;
}
.logo {
display: block;
width: 30px;
height: 30px;
}
}
</style>
<style lang="scss">
</style>
+56
View File
@@ -0,0 +1,56 @@
<template>
<el-menu
class="menus"
:collapse="isCollapse"
:default-active="activeIndex"
background-color="#2d3a4b"
text-color="#fff"
active-text-color="#409eff"
router
>
<menu-item v-for="(route,index) in routes" :key="route.name" :route="route"></menu-item>
</el-menu>
</template>
<script>
import { defineComponent, ref, onMounted, watch, computed } from 'vue'
import { useRouteStore } from '@/store/router'
import MenuItem from '@/layout/components/menu/item.vue'
import { useRoute } from 'vue-router'
import { useAppStore } from '@/store/app'
export default defineComponent({
name: 'Menu',
created () {
},
components: { MenuItem },
setup () {
const routes = ref([])
const route = useRoute()
const app = useAppStore()
const isCollapse = computed(() => app.setting.sideIsCollapse)
const activeIndex = computed(() => route.name)
routes.value = useRouteStore().routes
return {
routes,
activeIndex,
isCollapse,
}
},
})
</script>
<style lang="scss" scoped>
.menus {
min-height: 100vh;
border-right: none;
&:not(.el-menu--collapse) {
width: 210px;
}
}
</style>
<style>
</style>
+52
View File
@@ -0,0 +1,52 @@
<template>
<el-sub-menu v-if="route.children&&route.children.filter(c=>!c.meta?.hide).length>1&&route.children.some(r => !r.meta?.hide)"
:key="route.name"
:index="route.name"
>
<template #title>
<el-icon v-if="route.meta?.icon">
<component :is="`el-icon-${route.meta.icon}`"></component>
</el-icon>
<span>{{route.meta?.title||route.name}}</span>
</template>
<menu-item v-for="(_route,_index) in route.children"
:route="_route"
:key="_route.name">
</menu-item>
</el-sub-menu>
<el-menu-item v-else-if="!parseRoute(route).meta?.hide" :route="parseRoute(route)" :index="parseRoute(route).name">
<el-icon v-if="parseRoute(route).meta?.icon">
<component :is="`el-icon-${parseRoute(route).meta.icon}`"></component>
</el-icon>
<span>{{parseRoute(route).meta?.title||parseRoute(route).name}}</span>
</el-menu-item>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
name: 'MenuItem',
props: {
route: {},
},
mounted () {
},
setup (props) {
//判断仅有一个子项的route
const parseRoute = (route) => {
if (route.children && route.children.filter(c => !c.meta?.hide).length === 1) {
return route.children.filter(c => !c.meta?.hide)[0]
} else {
return route
}
}
return {
parseRoute,
}
},
})
</script>
<style lang="scss" scoped>
</style>
+140
View File
@@ -0,0 +1,140 @@
<template>
<div class="setting">
<el-dropdown class="menu-item">
<div class="title">
<!-- <el-image class="avatar" :src="user.avatar"></el-image>-->
<span class="nickname">{{ user.username }}</span>
<el-icon>
<el-icon-arrow-down/>
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="showChangePwd">修改密码</el-dropdown-item>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dialog v-model="changePwdVisible" width="50%">
<el-form ref="cpwd" :model="changePwdForm" :rules="chagePwdRules" label-width="120px" style="margin-top: 20px">
<el-form-item label="旧密码" prop="old_password">
<el-input v-model="changePwdForm.old_password" show-password></el-input>
</el-form-item>
<el-form-item label="新密码" prop="new_password">
<el-input v-model="changePwdForm.new_password" show-password></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPwd">
<el-input v-model="changePwdForm.confirmPwd" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button @click="changePwdVisible=false">取消</el-button>
<el-button type="primary" @click="changePassword">确定</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { useUserStore } from '@/store/user'
import { changeCurPwd } from '@/api/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { reactive, ref } from 'vue'
const userStore = useUserStore()
const user = userStore
const logout = () => {
userStore.logout()
window.location.reload()
}
const changePwdVisible = ref(false)
const showChangePwd = () => {
changePwdVisible.value = true
changePwdForm.old_password = ''
changePwdForm.new_password = ''
changePwdForm.confirmPwd = ''
}
const changePwdForm = reactive({
old_password: '',
new_password: '',
confirmPwd: '',
})
const chagePwdRules = reactive({
old_password: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value === changePwdForm.old_password) {
callback(new Error('新密码不能与旧密码相同'))
} else {
callback()
}
},
trigger: 'blur',
}],
confirmPwd: [
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== changePwdForm.new_password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
},
trigger: 'blur',
},
],
})
const cpwd = ref(null)
const changePassword = async () => {
//验证
const valid = await cpwd.value.validate().catch(_ => false)
if (!valid) {
return
}
console.log('changePassword')
const confirm = await ElMessageBox.confirm('确定修改密码么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(_ => false)
if (!confirm) {
return
}
const res = await changeCurPwd(changePwdForm).catch(_ => false)
if (!res) {
return
}
ElMessageBox.alert('修改成功', '修改密码', {
autofocus: true,
confirmButtonText: 'OK',
callback: (action) => {
logout()
},
})
}
</script>
<style lang="scss" scoped>
.setting {
margin-left: auto;
display: flex;
align-items: center;
justify-content: space-around;
.title {
color: #fff;
display: flex;
align-items: center;
justify-content: space-around;
.nickname {
padding: 0 10px;
}
}
}
</style>
+80
View File
@@ -0,0 +1,80 @@
<template>
<el-tag v-for="(t, i) in tags"
:key="t.name"
class="tag"
:closable="t.closeable"
@close="close(t)"
@click="toTag(t)"
:type="t.active?'primary':'info'"
:effect="t.active?'dark':'plain'">
{{t.title}}
</el-tag>
</template>
<script>
import { defineComponent, ref, onMounted, watch } from 'vue'
import { useTagsStore } from '@/store/tags'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
name: 'Index',
setup () {
const tags = ref([])
const tagsStore = useTagsStore()
const route = useRoute()
const router = useRouter()
tags.value = tagsStore.tags
const addTag = (route) => {
if (!route.meta?.hide && route.name) {
tagsStore.addTag(route)
}
}
const close = (tag) => {
tagsStore.removeTag(tag)
if (tag.active) {
toLastTag()
}
}
const toLastTag = () => {
if (tags.value.length) {
router.push({ name: tags.value[tags.value.length - 1].name })
}
}
const init = () => {
if (!tagsStore.tags.length) {
tagsStore.initTags()
}
addTag(route)
}
const toTag = (tag) => {
if (tag.name !== route.name) {
router.push({ name: tag.name })
}
}
onMounted(init)
watch(route, (val) => {
addTag(val)
})
return {
tags,
addTag,
close,
toLastTag,
toTag,
}
},
})
</script>
<style lang="scss" scoped>
.tag {
border-radius: 0;
cursor: pointer;
&.active {
}
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<template>
<el-container>
<el-aside :width="leftWidth" class="app-left">
<g-aside></g-aside>
</el-aside>
<el-container class="app-container ">
<el-header class="app-header">
<g-header></g-header>
</el-header>
<div class="header-tags">
<tags></tags>
</div>
<el-main class="app-main">
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="[...cachedTags]">
<component :is="Component"/>
</keep-alive>
</transition>
</router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
import { useUserStore } from '@/store/user'
import { useRouteStore } from '@/store/router'
import { useAppStore } from '@/store/app'
import { useTagsStore } from '@/store/tags'
import LayerHeader from '@/layout/components/header.vue'
import { defineComponent, ref, onMounted, watch, reactive, computed, toRef } from 'vue'
import Tags from '@/layout/components/tags/index.vue'
import GAside from '@/layout/components/aside.vue'
import GHeader from '@/layout/components/header.vue'
export default defineComponent({
name: 'Layout',
components: { LayerHeader, Tags, GAside, GHeader },
setup (props) {
const userStore = useUserStore()
const appStore = useAppStore()
const tagStore = useTagsStore()
const leftWidth = computed(() => appStore.setting.sideIsCollapse ? '64px' : '210px')
const cachedTags = ref([])
cachedTags.value = tagStore.cached
return {
cachedTags,
leftWidth,
}
},
})
</script>
<style lang="scss" scoped>
.app-header {
background-color: #3f454b;
color: var(--basicWhite);
display: flex;
height: 50px;
}
.header-tags {
height: auto;
border-bottom: 1px solid #eee;
display: flex;
padding: 0;
}
.app-left {
height: 100%;
transition: width 0.5s;
}
.app-container {
min-height: 100vh;
}
</style>
+20
View File
@@ -0,0 +1,20 @@
import { createApp } from 'vue'
import 'element-plus/dist/index.css'
import App from './App.vue'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { router } from '@/router'
import 'normalize.css/normalize.css'
import { pinia } from '@/store'
import '@/permission'
import '@/styles/style.scss'
import * as ElementIcons from '@element-plus/icons'
const app = createApp(App)
app.use(ElementPlus, { locale: zhCn })
app.use(pinia)
app.use(router)
for (let icon in ElementIcons){
app.component("ElIcon" +icon ,ElementIcons[icon])
}
app.mount('#app')
+50
View File
@@ -0,0 +1,50 @@
import { router } from '@/router'
import { useRouteStore } from '@/store/router'
import { useUserStore } from '@/store/user'
import { getToken } from '@/utils/auth'
import { pinia } from '@/store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'
import { useAppStore } from '@/store/app' // progress bar style
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login']
const routeStore = useRouteStore(pinia)
const appStore = useAppStore(pinia)
router.beforeEach(async (to, from, next) => {
document.title = (to.meta?.title || 'Rust-api-web') + '-' + appStore.setting.title
NProgress.start()
const token = getToken()
if (!token) {
//无token,跳转到登录
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next(`/login?redirect=${to.path}`)
}
} else {
//有token
const userStore = useUserStore(pinia)
if (!userStore.route_names.length) {
const info = await userStore.info()
if (!info) {
userStore.logout()
next(`/login?redirect=${to.path}`)
} else {
next({ ...to, replace: true })
}
} else {
next()
}
}
})
router.afterEach(() => {
NProgress.done()
})
+118
View File
@@ -0,0 +1,118 @@
import { createRouter, createWebHashHistory } from 'vue-router'
const constantRoutes = [
{
path: '/login',
name: 'Login',
meta: { title: '登录' },
component: () => import('@/views/login/login.vue'),
},
{
path: '/404',
component: () => import('@/views/error-page/404.vue'),
hidden: true,
},
]
export const asyncRoutes = [
// {
// path: '/',
// name: 'Index',
// redirect: '/Home',
// meta: { title: '首页', icon: 'house' },
// component: () => import('@/layout/index.vue'),
// children: [
// {
// path: '/Home',
// name: 'Home',
// meta: { title: '首页', icon: 'house' },
// component: () => import('@/views/index/index.vue'),
// },
//
// ],
// },
{
path: '/my',
name: 'My',
redirect: '/my/tag/index',
meta: { title: '我的', icon: 'UserFilled' },
component: () => import('@/layout/index.vue'),
children: [
{
path: '/',
name: 'MyAddressBookList',
meta: { title: '地址簿管理', icon: 'Notebook' /*keepAlive: true*/ },
component: () => import('@/views/my/address_book/index.vue'),
},
{
path: 'tag/index',
name: 'MyTagList',
meta: { title: '标签管理', icon: 'CollectionTag' /*keepAlive: true*/ },
component: () => import('@/views/my/tag/index.vue'),
},
],
},
{
path: '/user',
name: 'User',
redirect: '/user/index',
meta: { title: '系统', icon: 'Setting' },
component: () => import('@/layout/index.vue'),
children: [
{
path: 'peer',
name: 'Peer',
meta: { title: '设备管理', icon: 'Monitor' /*keepAlive: true*/ },
component: () => import('@/views/peer/index.vue'),
},
{
path: 'group',
name: 'UserGroup',
meta: { title: '群组管理', icon: 'ChatRound' /*keepAlive: true*/ },
component: () => import('@/views/group/index.vue'),
},
{
path: 'index',
name: 'UserList',
meta: { title: '用户列表', icon: 'User' /*keepAlive: true*/ },
component: () => import('@/views/user/index.vue'),
},
{
path: 'add',
name: 'UserAdd',
meta: { title: '用户添加', hide: true },
component: () => import('@/views/user/edit.vue'),
},
{
path: 'edit/:id',
name: 'UserEdit',
meta: { title: '用户编辑', hide: true },
component: () => import('@/views/user/edit.vue'),
},
{
path: 'addressBook',
name: 'UserAddressBook',
meta: { title: '地址簿管理', icon: 'Notebook' /*keepAlive: true*/ },
component: () => import('@/views/address_book/index.vue'),
},
{
path: 'tag',
name: 'UserTag',
meta: { title: '标签管理', icon: 'CollectionTag' /*keepAlive: true*/ },
component: () => import('@/views/tag/index.vue'),
},
],
},
]
export const lastRoutes = [
{ path: '/:catchAll(.*)', redirect: '/404', meta: { hide: true } },
]
export const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes,
})
+23
View File
@@ -0,0 +1,23 @@
import { defineStore, acceptHMRUpdate } from 'pinia'
import logo from '@/assets/logo.png'
export const useAppStore = defineStore({
id: 'App',
state: () => ({
setting: {
title: 'Gwen-Admin',
sideIsCollapse: false,
logo,
},
}),
actions: {
sideCollapse () {
this.setting.sideIsCollapse = !this.setting.sideIsCollapse
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAppStore, import.meta.hot))
}
+3
View File
@@ -0,0 +1,3 @@
import { createPinia } from 'pinia'
export const pinia = createPinia()
+64
View File
@@ -0,0 +1,64 @@
import { defineStore, acceptHMRUpdate } from 'pinia'
import { lastRoutes, asyncRoutes, router } from '@/router'
function filterRoute (routes, enableNames) {
return routes.filter(route => {
if (route.children && route.children.length) {
return enableNames.includes(route.name) || route.children.some(r => enableNames.includes(r.name))
} else {
return enableNames.includes(route.name)
}
}).map(route => {
if (route.children && route.children.length) {
return {
...route,
children: filterRoute(route.children, enableNames),
}
} else {
return { ...route }
}
})
}
export const useRouteStore = defineStore({
id: 'router',
state: () => ({
routes: [],
activeRoute: '',
loaded: 0,
keepAlive: [],
}),
actions: {
addRoutes (accessRouteNames) {
if (accessRouteNames.includes('*')) {
this.routes = asyncRoutes
} else {
this.routes = filterRoute(asyncRoutes, accessRouteNames)
}
this.routes.forEach(route => {
router.addRoute(route)
})
lastRoutes.forEach(route => {
router.addRoute(route)
})
this.addKeepAlive(this.routes)
},
addKeepAlive (route) {
if (route instanceof Array) {
route.forEach(r => {
this.addKeepAlive(r)
})
} else if (route.children && route.children.length) {
this.addKeepAlive(route.children)
} else if (route.meta?.keepAlive && !this.keepAlive.includes(route.name)) {
this.keepAlive.push(route.name)
}
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useRouteStore, import.meta.hot))
}
+73
View File
@@ -0,0 +1,73 @@
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useTagsStore = defineStore({
id: 'tags',
state: () => ({
tags: [],
cached: [],
}),
actions: {
initTags () {
this.tags.push(
{
name: 'Home',
path: '/Home',
title: '首页',
active: false,
closeable: false,
keepAlive: false,
})
},
addTag (route) {
const tags = this.tags
if (tags.find(t => t.name === route.name)) {
tags.forEach(t => t.active = false)
tags.find(t => t.name === route.name).active = true
} else {
tags.forEach(t => t.active = false)
if (route.meta?.keepAlive) {
this.addCachedTag(route.name)
}
tags.push({
name: route.name,
path: route.fullPath,
title: route.meta?.title || route.name,
active: true,
closeable: true,
keepAlive: route.meta?.keepAlive,
})
}
this.$patch({ tags })
},
removeTag (tag) {
let tags = this.tags
if (tags.find(t => t.name === tag.name)) {
const index = tags.findIndex(t => t.name === tag.name)
if (index > -1) {
if (tags[index].keepAlive) {
this.removeCachedTag(tags[index].name)
}
tags.splice(index, 1)
}
}
this.$patch({ tags })
},
addCachedTag (name) {
if (!this.cached.includes(name)) {
this.cached.push(name)
}
},
removeCachedTag (name) {
if (this.cached.includes(name)) {
this.cached.splice(this.cached.indexOf(name), 1)
}
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useTagsStore, import.meta.hot))
}
+62
View File
@@ -0,0 +1,62 @@
import { defineStore, acceptHMRUpdate } from 'pinia'
import { current, login } from '@/api/user'
import { setToken, removeToken } from '@/utils/auth'
import { useRouteStore } from '@/store/router'
export const useUserStore = defineStore({
id: 'user',
state: () => ({
nickname: '',
username: '',
token: '',
role: '',
avatar: '',
route_names: [],
}),
actions: {
logout () {
removeToken()
this.$patch({
name: '',
role: {},
})
},
async login (form) {
const res = await login(form).catch(_ => false)
if (res) {
const userData = res.data
setToken(userData.token)
//
localStorage.setItem('user_info', JSON.stringify({ name: userData.username }))
this.$patch({
...userData,
})
if (userData.route_names && userData.route_names.length) {
useRouteStore().addRoutes(userData.route_names)
}
return userData
} else {
return false
}
},
async info () {
const res = await current().catch(_ => false)
if (res) {
const userData = res.data
setToken(userData.token)
this.$patch({
...userData,
})
useRouteStore().addRoutes(userData.route_names)
return userData
}
return false
},
},
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}
+20
View File
@@ -0,0 +1,20 @@
$basicBlack: #000000;
$basicWhite: #ffffff;
$primaryColor: #409eff;
$sideBarWidth: 210px;
:root {
--basicBlack: #000000;
--basicWhite: #ffffff;
--primaryColor: #409eff;
}
.list-body{
margin: 10px 0;
}
.dialog-form{
max-width: 600px;
margin: 20px auto;
}
+13
View File
@@ -0,0 +1,13 @@
const TokenKey = 'access_token'
export function getToken () {
return localStorage.getItem(TokenKey)
}
export function setToken (token) {
return localStorage.setItem(TokenKey, token)
}
export function removeToken () {
return localStorage.removeItem(TokenKey)
}
+4
View File
@@ -0,0 +1,4 @@
export const ENABLE_STATUS = 1
export const DISABLE_STATUS = 2
+34
View File
@@ -0,0 +1,34 @@
export function get_suffix(filename) {
var pos = filename.lastIndexOf('.')
var suffix = ''
if (pos !== -1) {
suffix = filename.substring(pos)
}
return suffix
}
export function random_string(len) {
len = len || 32
var chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
var maxPos = chars.length
var pwd = ''
for (let i = 0; i < len; i++) {
pwd += chars.charAt(Math.floor(Math.random() * maxPos))
}
return pwd
}
export function random_filename(filename) {
var suffix = get_suffix(filename)
var time = new Date()
var time2 = new Date('2020/01/01')
return Math.ceil((time.getTime() - time2.getTime()) / 1000) + '_' + random_string(10) + suffix
}
export function utf8_to_b64(str) {
return window.btoa(unescape(encodeURIComponent(str)))
}
export function b64_to_utf8(str) {
return decodeURIComponent(escape(window.atob(str)))
}
+4272
View File
File diff suppressed because it is too large Load Diff
+80
View File
@@ -0,0 +1,80 @@
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { getToken, removeToken } from '@/utils/auth'
import { useUserStore } from '@/store/user'
import { pinia } from '@/store'
// create an axios instance
const service = axios.create({
baseURL: import.meta.env.VITE_SERVER_API,
withCredentials: true, // send cookies when cross-domain requests
timeout: 50000, // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
const userStore = useUserStore(pinia)
const token = userStore.token || getToken()
if (token) {
if (!config.headers) {
config.headers = {}
}
config.headers['api-token'] = token
}
return config
},
error => {
// do something with request error
return Promise.reject(error)
},
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 20000, it is judged as an error.
if (res.code !== 0) {
ElMessage({
message: res.message || 'error',
type: 'error',
duration: 5 * 1000,
})
if (res.code === 403) {
removeToken()
window.location.reload()
}
return Promise.reject(res.message || 'error')
} else {
return res
}
},
error => {
if (error.code === 'ECONNABORTED'
&& error.message.indexOf('timeout') > -1) {
error.message = '请求超时!'
}
ElMessage({
message: error.message,
type: 'error',
duration: 5 * 1000,
})
return Promise.reject(error)
},
)
export default service
+26
View File
@@ -0,0 +1,26 @@
import { ref } from 'vue'
import { config } from '@/api/rustdesk'
export const toWebClientLink = (row) => {
window.open(`${rustdeskConfig.value.api_server}/webclient/#/?id=${row.id}`)
}
export function loadRustdeskConfig () {
const rustdeskConfig = ref({})
const fetchConfig = async () => {
const res = await config().catch(_ => false)
if (res) {
rustdeskConfig.value = res.data
localStorage.setItem('custom-rendezvous-server', res.data.id_server)
localStorage.setItem('key', res.data.key)
localStorage.setItem('api-server', res.data.api_server)
}
}
if (rustdeskConfig.value.id_server === undefined || rustdeskConfig.value.key === undefined) {
fetchConfig()
}
return {
rustdeskConfig,
}
}
const { rustdeskConfig } = loadRustdeskConfig()
+132
View File
@@ -0,0 +1,132 @@
import { onActivated, onMounted, reactive, ref, watch } from 'vue'
import { create, list, remove, update } from '@/api/address_book'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
export function useRepositories () {
const route = useRoute()
const user_id = route.query?.user_id
const listRes = reactive({
list: [], total: 0, loading: false,
})
const listQuery = reactive({
page: 1,
page_size: 10,
is_my: 0,
user_id: user_id ? parseInt(user_id) : null,
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
}
}
const del = async (row) => {
const cf = await ElMessageBox.confirm('确定删除么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = await remove({ row_id: row.row_id }).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
getList()
}
}
const platformList = [
{ label: 'Windows', value: 'Windows' },
{ label: 'Linux', value: 'Linux' },
{ label: 'Mac OS', value: 'Mac OS' },
{ label: 'Android', value: 'Android' },
]
const formVisible = ref(false)
const formData = reactive({
'row_id': 0,
'alias': '',
'force_always_relay': false,
'hash': '',
'hostname': '',
'id': '',
'login_name': '',
'online': false,
'password': '',
'platform': '',
'rdp_port': '',
'rdp_username': '',
'same_server': false,
'tags': [],
'user_id': null,
'username': '',
})
const toEdit = (row) => {
formVisible.value = true
//将row中的数据赋值给formData
Object.keys(formData).forEach(key => {
formData[key] = row[key]
})
}
const toAdd = () => {
formVisible.value = true
//重置formData
formData.row_id = 0
formData.alias = ''
formData.force_always_relay = false
formData.hash = ''
formData.hostname = ''
formData.id = ''
formData.login_name = ''
formData.online = false
formData.password = ''
formData.platform = ''
formData.rdp_port = ''
formData.rdp_username = ''
formData.same_server = false
formData.tags = []
formData.user_id = null
formData.username = ''
}
const submit = async () => {
const api = formData.row_id ? update : create
const res = await api(formData).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
formVisible.value = false
getList()
}
}
return {
listRes,
listQuery,
getList,
handlerQuery,
del,
platformList,
formVisible,
formData,
toEdit,
toAdd,
submit,
}
}
+212
View File
@@ -0,0 +1,212 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<el-form-item label="用户">
<el-select v-model="listQuery.user_id" clearable>
<el-option
v-for="item in allUsers"
:key="item.id"
:label="item.username"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<!-- <el-tag type="danger" style="margin-bottom: 10px">不建议在此操作地址簿可能会造成数据不同步</el-tag>-->
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="id" align="center"/>
<el-table-column label="所属用户" align="center">
<template #default="{row}">
<span v-if="row.user_id"> <el-tag>{{ allUsers?.find(u => u.id === row.user_id)?.username }}</el-tag> </span>
</template>
</el-table-column>
<el-table-column prop="username" label="用户名" align="center"/>
<el-table-column prop="hostname" label="主机名" align="center"/>
<el-table-column prop="alias" label="别名" align="center"/>
<el-table-column prop="platform" label="平台" align="center"/>
<el-table-column prop="hash" label="hash" align="center"/>
<el-table-column prop="tags" label="标签" align="center"/>
<!-- <el-table-column prop="created_at" label="创建时间" align="center"/>-->
<!-- <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
<el-table-column label="操作" align="center" width="400">
<template #default="{row}">
<el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" width="800" :title="!formData.row_id?'创建':'修改'">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item label="用户" prop="user_id" required>
<el-select v-model="formData.user_id" @change="changeUser">
<el-option
v-for="item in allUsers"
:key="item.id"
:label="item.username"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="id" prop="id" required>
<el-input v-model="formData.id"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username"></el-input>
</el-form-item>
<el-form-item label="别名" prop="alias">
<el-input v-model="formData.alias"></el-input>
</el-form-item>
<el-form-item label="hash" prop="hash">
<el-input v-model="formData.hash"></el-input>
</el-form-item>
<el-form-item label="主机名" prop="hostname">
<el-input v-model="formData.hostname"></el-input>
</el-form-item>
<el-form-item label="登录名" prop="login_name">
<el-input v-model="formData.login_name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password"></el-input>
</el-form-item>
<el-form-item label="平台" prop="platform">
<el-select v-model="formData.platform">
<el-option
v-for="item in platformList"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="标签" prop="tags">
<el-select v-model="formData.tags" multiple>
<el-option
v-for="item in tagList"
:key="item.name"
:label="item.name"
:value="item.name"
></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="强制中继" prop="force_always_relay" required>
<el-switch v-model="formData.force_always_relay"></el-switch>
</el-form-item>
<el-form-item label="在线" prop="online">
<el-switch v-model="formData.online"></el-switch>
</el-form-item>
<el-form-item label="rdp端口" prop="rdp_port">
<el-input v-model="formData.rdp_port"></el-input>
</el-form-item>
<el-form-item label="rdp用户名" prop="rdp_username">
<el-input v-model="formData.rdp_username"></el-input>
</el-form-item>
<el-form-item label="同一服务器" prop="same_server">
<el-switch v-model="formData.same_server"></el-switch>
</el-form-item>-->
<el-form-item>
<el-button @click="formVisible = false">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onActivated, onMounted, reactive, ref, watch } from 'vue'
import { create, list, remove, update } from '@/api/address_book'
import { list as fetchTagList } from '@/api/tag'
import { ElMessage, ElMessageBox } from 'element-plus'
import { loadAllUsers } from '@/global'
import { useRoute } from 'vue-router'
import { useRepositories } from '@/views/address_book/index'
import { toWebClientLink } from '@/utils/webclient'
const { allUsers, getAllUsers } = loadAllUsers()
getAllUsers()
const changeUser = (v) => {
formData.tags = []
fetchTagListData(v)
}
const tagList = ref([])
const fetchTagListData = async (user_id) => {
const res = await fetchTagList({ user_id }).catch(_ => false)
if (res) {
tagList.value = res.data.list
}
}
const {
listRes,
listQuery,
getList,
handlerQuery,
del,
formVisible,
platformList,
formData,
toEdit,
toAdd,
submit,
activeChange,
currentColor,
} = useRepositories()
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
</script>
<style scoped lang="scss">
.list-query .el-select {
--el-select-width: 160px;
}
.colors {
display: flex;
justify-content: center;
align-items: center;
.colorbox {
width: 50px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
.dot {
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
}
}
}
</style>
+13
View File
@@ -0,0 +1,13 @@
<template>
<h1>404</h1>
</template>
<script>
export default {
name: '404',
}
</script>
<style scoped>
</style>
+149
View File
@@ -0,0 +1,149 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<!-- <el-form-item label="名称">
<el-input v-model="listQuery.name"></el-input>
</el-form-item>-->
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="id" align="center"></el-table-column>
<el-table-column prop="name" label="名称" align="center"/>
<el-table-column prop="created_at" label="创建时间" align="center"/>
<el-table-column prop="updated_at" label="更新时间" align="center"/>
<el-table-column label="操作" align="center">
<template #default="{row}">
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" :title="!formData.id?'创建':'修改'" width="800">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item label="名称" prop="name" required>
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="类型" prop="type" required>
<el-radio-group v-model="formData.type">
<el-radio v-for="item in groupTypes" :key="item.value" :label="item.value" style="display: block">
{{ item.label }}
<span style="font-size: 12px;color: #999">{{item.note}}</span>
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button @click="formVisible = false">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, watch, ref, onActivated } from 'vue'
import { list, create, update, detail, remove } from '@/api/group'
import { ElMessage, ElMessageBox } from 'element-plus'
const listRes = reactive({
list: [], total: 0, loading: false,
})
const listQuery = reactive({
page: 1,
page_size: 10,
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
}
}
const del = async (row) => {
const cf = await ElMessageBox.confirm('确定删除么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = await remove({ id: row.id }).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
getList()
}
}
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const groupTypes = [
{ label: '普通组', value: 1, note: '只有管理员能看到小组成员和成员地址簿' },
{ label: '共享组', value: 2, note: '所有用户都能看到小组成员和成员地址簿' },
]
const formVisible = ref(false)
const formData = reactive({
id: 0,
name: '',
type: 1
})
const toEdit = (row) => {
formVisible.value = true
formData.id = row.id
formData.name = row.name
formData.type = row.type
}
const toAdd = () => {
formVisible.value = true
formData.id = 0
formData.name = ''
formData.type = 1
}
const submit = async () => {
const api = formData.id ? update : create
const res = await api(formData).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
formVisible.value = false
getList()
}
}
</script>
<style scoped lang="scss">
</style>
+75
View File
@@ -0,0 +1,75 @@
<template>
<div class="index">
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
name: 'Home',
setup () {
const todoList = ref([
{title:'修复bug'},
{title:'修复bug'},
{title:'修复bug'},
{title:'增加新功能'},
])
return {
todoList
}
},
})
</script>
<style scoped lang="scss">
.index {
.counts {
display: flex;
justify-content: space-between;
.item {
width: 32.5%;
.num {
font-size: 28px;
display: flex;
justify-content: space-around;
align-items: center;
.before, .after {
font-size: 18px;
.exp {
font-size: 12px;
margin-right: 10px;
}
.red {
color: red;
}
.green {
color: green;
}
}
.middle {
.exp {
font-size: 20px;
margin-right: 10px;
}
span + span {
font-weight: bold;
}
}
}
}
}
.lans, .todo{
height: 250px
}
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<div class="login">
<el-card class="login-card">
<h1>登录</h1>
<el-form label-width="60px">
<el-form-item label="用户名">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" @keyup.enter.native="login" show-password></el-input>
</el-form-item>
<el-form-item>
<el-button @click="login" type="primary">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script>
import { defineComponent, reactive } from 'vue'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
export default defineComponent({
setup (props) {
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const form = reactive({
username: 'admin',
password: 'admin',
})
const redirect = route.query?.redirect
const login = async () => {
const res = await userStore.login(form)
if (res) {
ElMessage.success('登录成功')
router.push({ path: redirect || '/', replace: true })
}
}
return {
redirect,
form,
login,
}
},
})
</script>
<style scoped lang="scss">
.login {
width: 100vw;
height: 100vh;
background-color: #2d3a4b;
padding-top: 200px;
box-sizing: border-box;
.tips {
font-size: 12px;
color: #fff;
margin-left: 60px;
}
.login-card {
width: 500px;
background-color: #283342;
color: #fff;
border: none;
margin: 0 auto;
.el-form-item {
::v-deep(.el-form-item__label) {
color: #fff;
}
.el-input {
::v-deep(.el-input__wrapper) {
border: 1px solid rgba(255, 255, 255, 0.1);
background: transparent;
}
::v-deep(input) {
color: #fff;
}
}
}
}
}
</style>
+204
View File
@@ -0,0 +1,204 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<!-- <el-tag type="danger" style="margin-bottom: 10px">不建议在此操作地址簿可能会造成数据不同步</el-tag>-->
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="id" align="center"/>
<el-table-column prop="username" label="用户名" align="center"/>
<el-table-column prop="hostname" label="主机名" align="center"/>
<el-table-column prop="alias" label="别名" align="center"/>
<el-table-column prop="platform" label="平台" align="center"/>
<el-table-column prop="hash" label="hash" align="center"/>
<el-table-column prop="tags" label="标签" align="center"/>
<!-- <el-table-column prop="created_at" label="创建时间" align="center"/>-->
<!-- <el-table-column prop="updated_at" label="更新时间" align="center"/>-->
<el-table-column label="操作" align="center" width="400">
<template #default="{row}">
<el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" width="800" :title="!formData.row_id?'创建':'修改'">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item label="id" prop="id" required>
<el-input v-model="formData.id"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username"></el-input>
</el-form-item>
<el-form-item label="别名" prop="alias">
<el-input v-model="formData.alias"></el-input>
</el-form-item>
<el-form-item label="hash" prop="hash">
<el-input v-model="formData.hash"></el-input>
</el-form-item>
<el-form-item label="主机名" prop="hostname">
<el-input v-model="formData.hostname"></el-input>
</el-form-item>
<el-form-item label="登录名" prop="login_name">
<el-input v-model="formData.login_name"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="formData.password"></el-input>
</el-form-item>
<el-form-item label="平台" prop="platform">
<el-select v-model="formData.platform">
<el-option
v-for="item in platformList"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="标签" prop="tags">
<el-select v-model="formData.tags" multiple>
<el-option
v-for="item in tagList"
:key="item.name"
:label="item.name"
:value="item.name"
></el-option>
</el-select>
</el-form-item>
<!-- <el-form-item label="强制中继" prop="force_always_relay" required>
<el-switch v-model="formData.force_always_relay"></el-switch>
</el-form-item>
<el-form-item label="在线" prop="online">
<el-switch v-model="formData.online"></el-switch>
</el-form-item>
<el-form-item label="rdp端口" prop="rdp_port">
<el-input v-model="formData.rdp_port"></el-input>
</el-form-item>
<el-form-item label="rdp用户名" prop="rdp_username">
<el-input v-model="formData.rdp_username"></el-input>
</el-form-item>
<el-form-item label="同一服务器" prop="same_server">
<el-switch v-model="formData.same_server"></el-switch>
</el-form-item>-->
<el-form-item>
<el-button @click="formVisible = false">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onActivated, onMounted, reactive, ref, watch } from 'vue'
import { create, list, remove, update } from '@/api/address_book'
import { list as fetchTagList } from '@/api/tag'
import { useRepositories } from '@/views/address_book'
import { toWebClientLink } from '@/utils/webclient'
const tagList = ref([])
const fetchTagListData = async () => {
const res = await fetchTagList({ is_my: 1 }).catch(_ => false)
if (res) {
tagList.value = res.data.list
}
}
fetchTagListData()
const {
listRes,
listQuery,
getList,
handlerQuery,
del,
formVisible,
platformList,
formData,
toEdit,
toAdd,
submit,
activeChange,
currentColor,
} = useRepositories()
listQuery.is_my = 1
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
watch(() => listRes.list, () => {
const peers = {}
listRes.list.map(item => {
peers[item.id] = {
'view-style': 'shrink',
tm: new Date().getTime(),
info: {
'id': item.id,
'username': item.username,
'hostname': item.hostname,
'alias': item.alias,
'platform': item.platform,
'hash': item.hash,
'tags': item.tags,
},
}
})
localStorage.setItem('peers', JSON.stringify(peers))
},
{
immediate: true,
})
</script>
<style scoped lang="scss">
.list-query .el-select {
--el-select-width: 160px;
}
.colors {
display: flex;
justify-content: center;
align-items: center;
.colorbox {
width: 50px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
.dot {
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
}
}
}
</style>
+133
View File
@@ -0,0 +1,133 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="id" align="center"/>
<el-table-column prop="name" label="名称" align="center"/>
<el-table-column prop="color" label="颜色" align="center">
<template #default="{row}">
<div class="colors">
<div style="background-color: #efeff2" class="colorbox">
<div :style="{backgroundColor: row.color}" class="dot">
</div>
</div>
<div style="background-color: #24252b" class="colorbox">
<div :style="{backgroundColor: row.color}" class="dot">
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" align="center"/>
<el-table-column prop="updated_at" label="更新时间" align="center"/>
<el-table-column label="操作" align="center">
<template #default="{row}">
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" :title="!formData.id?'创建':'修改'" width="800">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item label="名称" prop="name" required>
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="颜色" prop="color" required>
<el-color-picker v-model="formData.color" show-alpha @active-change="activeChange"></el-color-picker>
<br>
<div class="colors">
<div style="background-color: #efeff2" class="colorbox">
<div :style="{backgroundColor: currentColor}" class="dot">
</div>
</div>
<div style="background-color: #24252b" class="colorbox">
<div :style="{backgroundColor: currentColor}" class="dot">
</div>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button @click="formVisible = false">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, watch, onActivated } from 'vue'
import { useRepositories } from '@/views/tag'
const {
listRes,
listQuery,
getList,
handlerQuery,
del,
formVisible,
formData,
toEdit,
toAdd,
submit,
activeChange,
currentColor,
} = useRepositories()
listQuery.is_my = 1
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
</script>
<style scoped lang="scss">
.list-query .el-select {
--el-select-width: 160px;
}
.colors {
display: flex;
justify-content: center;
align-items: center;
.colorbox {
width: 50px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
.dot {
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
}
}
}
</style>
+211
View File
@@ -0,0 +1,211 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border size="small">
<el-table-column prop="id" label="id" align="center"/>
<el-table-column prop="cpu" label="cpu" align="center"/>
<el-table-column prop="hostname" label="主机名" align="center"/>
<el-table-column prop="memory" label="内存" align="center"/>
<el-table-column prop="os" label="系统" align="center"/>
<el-table-column prop="username" label="username" align="center"/>
<el-table-column prop="uuid" label="uuid" align="center"/>
<el-table-column prop="version" label="版本号" align="center"/>
<el-table-column prop="created_at" label="创建时间" align="center"/>
<el-table-column prop="updated_at" label="更新时间" align="center"/>
<el-table-column label="操作" align="center" width="400">
<template #default="{row}">
<el-button type="success" @click="toWebClientLink(row)">Web-Client</el-button>
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" :title="!formData.row_id?'创建':'修改'" width="800">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item label="id" prop="id" required>
<el-input v-model="formData.id"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username">
<el-input v-model="formData.username"></el-input>
</el-form-item>
<el-form-item label="主机名" prop="hostname">
<el-input v-model="formData.hostname"></el-input>
</el-form-item>
<el-form-item label="cpu" prop="cpu">
<el-input v-model="formData.cpu"></el-input>
</el-form-item>
<el-form-item label="内存" prop="memory">
<el-input v-model="formData.memory"></el-input>
</el-form-item>
<el-form-item label="系统" prop="os">
<el-input v-model="formData.os"></el-input>
</el-form-item>
<el-form-item label="uuid" prop="uuid">
<el-input v-model="formData.uuid"></el-input>
</el-form-item>
<el-form-item label="版本" prop="version">
<el-input v-model="formData.version"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="formVisible = false">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onActivated, onMounted, reactive, ref, watch } from 'vue'
import { create, list, remove, update } from '@/api/peer'
import { list as fetchTagList } from '@/api/tag'
import { ElMessage, ElMessageBox } from 'element-plus'
import { loadAllUsers } from '@/global'
import { toWebClientLink } from '@/utils/webclient'
const listRes = reactive({
list: [], total: 0, loading: false,
})
const listQuery = reactive({
page: 1,
page_size: 10,
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
}
}
const del = async (row) => {
const cf = await ElMessageBox.confirm('确定删除么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = await remove({ row_id: row.row_id }).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
getList()
}
}
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const platformList = [
{ label: 'Windows', value: 'Windows' },
{ label: 'Linux', value: 'Linux' },
{ label: 'Mac OS', value: 'Mac OS' },
{ label: 'Android', value: 'Android' },
]
const formVisible = ref(false)
const formData = reactive({
row_id: 0,
cpu: '',
hostname: '',
id: '',
memory: '',
os: '',
username: '',
uuid: '',
version: '',
})
const toEdit = (row) => {
formVisible.value = true
//将row中的数据赋值给formData
Object.keys(formData).forEach(key => {
formData[key] = row[key]
})
}
const toAdd = () => {
formVisible.value = true
//重置formData
formData.row_id = 0
formData.cpu = ''
formData.hostname = ''
formData.id = ''
formData.memory = ''
formData.os = ''
formData.username = ''
formData.uuid = ''
formData.version = ''
}
const submit = async () => {
const api = formData.row_id ? update : create
const res = await api(formData).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
formVisible.value = false
getList()
}
}
</script>
<style scoped lang="scss">
.list-query .el-select {
--el-select-width: 160px;
}
.colors {
display: flex;
justify-content: center;
align-items: center;
.colorbox {
width: 50px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
.dot {
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
}
}
}
</style>
+155
View File
@@ -0,0 +1,155 @@
import { onActivated, onMounted, reactive, ref, watch } from 'vue'
import { create, list, remove, update } from '@/api/tag'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRoute } from 'vue-router'
export function useRepositories () {
//获取query
const route = useRoute()
const user_id = route.query?.user_id
const listRes = reactive({
list: [], total: 0, loading: false,
})
const listQuery = reactive({
page: 1,
page_size: 10,
is_my: 0,
user_id: user_id ? parseInt(user_id) : null,
})
const flutterColor2rgba = (color) => {
// color 是十进制的数字,先转成16进制
let hex = color.toString(16)
console.log('hex', hex)
//前两位是透明度
let alpha = hex.slice(0, 2)
//后六位是颜色
let rgba = hex.slice(2)
return `rgba(${parseInt(rgba.slice(0, 2), 16)}, ${parseInt(rgba.slice(2, 4), 16)}, ${parseInt(rgba.slice(4, 6), 16)}, ${parseInt(alpha, 16) / 255})`
}
const rgba2flutterColor = (color) => {
console.log('color', color)
//rgba(133, 33, 33, 0.81)
let rgba = color.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)/)
console.log('rgba', rgba)
let alpha = Math.round(parseFloat(rgba[4]) * 255).toString(16)
let r = parseInt(rgba[1]).toString(16)
let g = parseInt(rgba[2]).toString(16)
let b = parseInt(rgba[3]).toString(16)
//如果是1位要补位
if (alpha.length === 1) {
alpha = '0' + alpha
}
if (r.length === 1) {
r = '0' + r
}
if (g.length === 1) {
g = '0' + g
}
if (b.length === 1) {
b = '0' + b
}
return parseInt(alpha + r + g + b, 16)
}
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list.map(item => {
item.color = flutterColor2rgba(item.color)
return item
})
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
}
}
const del = async (row) => {
const cf = await ElMessageBox.confirm('确定删除么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = await remove({ id: row.id }).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
getList()
}
}
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
const formVisible = ref(false)
const formData = reactive({
id: 0,
name: '',
color: 0,
user_id: 0,
})
const currentColor = ref('')
const activeChange = (c) => {
console.log(c)
currentColor.value = c
}
const toEdit = (row) => {
console.log('row', row)
formVisible.value = true
formData.id = row.id
formData.name = row.name
formData.color = row.color
formData.user_id = row.user_id
}
const toAdd = () => {
formVisible.value = true
formData.id = 0
formData.name = ''
formData.color = 0
formData.user_id = 0
}
const submit = async () => {
console.log(formData)
const api = formData.id ? update : create
const data = {
...formData,
color: rgba2flutterColor(formData.color),
}
console.log(data)
const res = await api(data).catch(_ => false)
if (res) {
ElMessage.success('操作成功')
formVisible.value = false
getList()
}
}
return {
listRes,
listQuery,
getList,
handlerQuery,
del,
formVisible,
formData,
toEdit,
toAdd,
submit,
activeChange,
currentColor,
}
}
+165
View File
@@ -0,0 +1,165 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<el-form-item label="用户">
<el-select v-model="listQuery.user_id" clearable>
<el-option
v-for="item in allUsers"
:key="item.id"
:label="item.username"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="id" align="center"/>
<el-table-column label="所属用户" align="center">
<template #default="{row}">
<span v-if="row.user_id"> <el-tag>{{ allUsers?.find(u => u.id === row.user_id)?.username }}</el-tag> </span>
</template>
</el-table-column>
<el-table-column prop="name" label="名称" align="center"/>
<el-table-column prop="color" label="颜色" align="center">
<template #default="{row}">
<div class="colors">
<div style="background-color: #efeff2" class="colorbox">
<div :style="{backgroundColor: row.color}" class="dot">
</div>
</div>
<div style="background-color: #24252b" class="colorbox">
<div :style="{backgroundColor: row.color}" class="dot">
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" align="center"/>
<el-table-column prop="updated_at" label="更新时间" align="center"/>
<el-table-column label="操作" align="center">
<template #default="{row}">
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
<el-dialog v-model="formVisible" :title="!formData.id?'创建':'修改'" width="800">
<el-form class="dialog-form" ref="form" :model="formData" label-width="120px">
<el-form-item label="名称" prop="name" required>
<el-input v-model="formData.name"></el-input>
</el-form-item>
<el-form-item label="颜色" prop="color" required>
<el-color-picker v-model="formData.color" show-alpha @active-change="activeChange"></el-color-picker>
<br>
<div class="colors">
<div style="background-color: #efeff2" class="colorbox">
<div :style="{backgroundColor: currentColor}" class="dot">
</div>
</div>
<div style="background-color: #24252b" class="colorbox">
<div :style="{backgroundColor: currentColor}" class="dot">
</div>
</div>
</div>
</el-form-item>
<el-form-item label="用户" prop="user_id" required>
<el-select v-model="formData.user_id">
<el-option
v-for="item in allUsers"
:key="item.id"
:label="item.username"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="formVisible = false">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, reactive, watch, ref, onActivated } from 'vue'
import { list, create, update, detail, remove } from '@/api/tag'
import { ElMessage, ElMessageBox } from 'element-plus'
import { loadAllUsers } from '@/global'
import { useRoute } from 'vue-router'
import { useRepositories } from '@/views/tag/index'
const { allUsers, getAllUsers } = loadAllUsers()
getAllUsers()
const {
listRes,
listQuery,
getList,
handlerQuery,
del,
formVisible,
formData,
toEdit,
toAdd,
submit,
activeChange,
currentColor,
} = useRepositories(0)
onMounted(getList)
onActivated(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
</script>
<style scoped lang="scss">
.list-query .el-select {
--el-select-width: 160px;
}
.colors {
display: flex;
justify-content: center;
align-items: center;
.colorbox {
width: 50px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
.dot {
width: 10px;
height: 10px;
display: block;
border-radius: 50%;
}
}
}
</style>
+86
View File
@@ -0,0 +1,86 @@
import { ref, onMounted, reactive, watch } from 'vue'
import { create, detail, update, remove } from '@/api/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
import { list as groups } from '@/api/group'
export function useGetDetail (id) {
let item = ref({}) //保留原始值
let form = ref({})
const groupsList = ref([])
const getDetail = async (id) => {
const res = await detail(id)
item.value = { ...res.data }
form.value = { ...res.data }
}
if (id > 0) {
onMounted(getDetail(id))
}
const getGroups = async () => {
const res = await groups({ page_size: 9999 }).catch(_ => false)
if (res) {
groupsList.value = res.data.list
}
}
onMounted(getGroups)
return {
form,
item,
getDetail,
groupsList
}
}
export function useSubmit (form, id) {
const root = ref(null)
const router = useRouter()
const rules = reactive({
username: [{ required: true, message: '用户名是必须的' }],
// nickname: [{ required: true, message: '昵称是必须的' }],
status: [{ required: true, message: '请选择状态' }],
})
const validate = async () => {
const res = await root.value.validate().catch(err => false)
return res
}
const submitCreate = async () => {
const res = await create(form.value).catch(_ => false)
return res.code === 0
}
const submitUpdate = async () => {
const res = await update(form.value).catch(_ => false)
return res.code === 0
}
const submitFunc = id > 0 ? submitUpdate : submitCreate
const submit = async () => {
const v = await validate()
if (!v) {
return
}
const res = await submitFunc()
if (res) {
ElMessage.success('操作成功')
router.back()
}
}
const cancel = () => {
router.back()
}
return {
root,
rules,
validate,
submit,
cancel,
}
}
+124
View File
@@ -0,0 +1,124 @@
import { onMounted, reactive, watch } from 'vue'
import { list, remove, changePwd } from '@/api/user'
import { list as groups } from '@/api/group'
import { useRouter } from 'vue-router'
import { ElMessageBox, ElMessage } from 'element-plus'
export function useRepositories () {
const listRes = reactive({
list: [], total: 0, loading: false,
groups: [],
})
const listQuery = reactive({
page: 1,
page_size: 10,
username: '',
})
const getList = async () => {
listRes.loading = true
const res = await list(listQuery).catch(_ => false)
listRes.loading = false
if (res) {
listRes.list = res.data.list
listRes.total = res.data.total
}
}
const handlerQuery = () => {
if (listQuery.page === 1) {
getList()
} else {
listQuery.page = 1
//由watch 触发
}
}
const getGroups = async () => {
const res = await groups({ page_size: 9999 }).catch(_ => false)
if (res) {
listRes.groups = res.data.list
}
}
onMounted(getGroups)
onMounted(getList)
watch(() => listQuery.page, getList)
watch(() => listQuery.page_size, handlerQuery)
return {
listRes,
listQuery,
handlerQuery,
getList,
getGroups,
}
}
export function useToEditOrAdd () {
const router = useRouter()
const toEdit = (row) => {
router.push('/user/edit/' + row.id)
}
const toAdd = () => {
router.push('/user/add')
}
const toTag = (row) => {
router.push('/user/tag/?user_id=' + row.id)
}
const toAddressBook = (row) => {
router.push('/user/addressBook/?user_id=' + row.id)
}
return {
toAdd,
toEdit,
toTag,
toAddressBook
}
}
export function useDel () {
const del = async (id) => {
const cf = await ElMessageBox.confirm('确定删除么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).catch(_ => false)
if (!cf) {
return false
}
const res = remove({ id }).catch(_ => false)
return res
}
return {
del,
}
}
export function useChangePwd () {
const changePass = async (admin) => {
const input = await ElMessageBox.prompt('请输入新密码', '重置密码', {
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(_ => false)
if (!input) {
return
}
const confirm = await ElMessageBox.confirm('确定重置密码么?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(_ => false)
if (!confirm) {
return
}
const res = await changePwd({ id: admin.id, password: input.value }).catch(_ => false)
if (!res) {
return
}
ElMessage.success('修改成功')
}
return { changePass }
}
+76
View File
@@ -0,0 +1,76 @@
<template>
<div class="form-card">
<el-form ref="root" label-width="120px" :model="form" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname"></el-input>
</el-form-item>
<el-form-item label="小组" prop="group_id">
<el-select v-model="form.group_id" placeholder="请选择小组">
<el-option
v-for="item in groupsList"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否是管理员" prop="is_admin">
<el-switch v-model="form.is_admin"
:active-value="true"
:inactive-value="false"
></el-switch>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch v-model="form.status"
:active-value="ENABLE_STATUS"
:inactive-value="DISABLE_STATUS"
></el-switch>
</el-form-item>
<el-form-item>
<el-button @click="cancel">取消</el-button>
<el-button @click="submit" type="primary">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { defineComponent, toRef } from 'vue'
import { useRoute } from 'vue-router'
import { useGetDetail, useSubmit } from '@/views/user/composables/edit'
import { ENABLE_STATUS, DISABLE_STATUS } from '@/utils/common_options'
export default defineComponent({
name: 'UserEdit',
props: {},
setup (props, context) {
const route = useRoute()
const { form, item, getDetail, groupsList } = useGetDetail(route.params.id)
const { root, rules, validate, submit, cancel } = useSubmit(form, route.params.id)
return {
form,
item,
getDetail,
rules,
validate,
root,
submit,
cancel,
groupsList,
ENABLE_STATUS, DISABLE_STATUS,
}
},
})
</script>
<style lang="scss" scoped>
.form-card {
}
</style>
+78
View File
@@ -0,0 +1,78 @@
<template>
<div>
<el-card class="list-query" shadow="hover">
<el-form inline label-width="60px">
<el-form-item label="用户名">
<el-input v-model="listQuery.username"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handlerQuery">筛选</el-button>
<el-button type="danger" @click="toAdd">添加</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="list-body" shadow="hover">
<el-table :data="listRes.list" v-loading="listRes.loading" border>
<el-table-column prop="id" label="id" align="center"></el-table-column>
<el-table-column prop="username" label="用户名" align="center"/>
<el-table-column prop="nickname" label="昵称" align="center"/>
<el-table-column label="所在小组" align="center">
<template #default="{row}">
<span v-if="row.group_id"> <el-tag>{{ listRes.groups?.find(g => g.id === row.group_id)?.name }} </el-tag> </span>
<span v-else> 未分组 </span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" align="center"/>
<el-table-column prop="updated_at" label="更新时间" align="center"/>
<el-table-column label="操作" align="center" width="550">
<template #default="{row}">
<el-button @click="toTag(row)">他的标签</el-button>
<el-button @click="toAddressBook(row)">他的地址簿</el-button>
<el-button @click="toEdit(row)">编辑</el-button>
<el-button type="warning" @click="changePass(row)">重置密码</el-button>
<el-button type="danger" @click="remove(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-card class="list-page" shadow="hover">
<el-pagination background
layout="prev, pager, next, sizes, jumper"
:page-sizes="[10,20,50,100]"
v-model:page-size="listQuery.page_size"
v-model:current-page="listQuery.page"
:total="listRes.total">
</el-pagination>
</el-card>
</div>
</template>
<script setup>
import { useRepositories, useDel, useToEditOrAdd, useChangePwd } from '@/views/user/composables'
//列表
const {
listRes,
listQuery,
handlerQuery,
getList,
} = useRepositories()
const { toEdit, toAdd, toAddressBook, toTag } = useToEditOrAdd()
const { changePass } = useChangePwd()
//删除
const { del } = useDel()
const remove = async (row) => {
const res = await del(row.id)
if (res) {
getList(listQuery)
}
}
</script>
<style scoped>
</style>
+82
View File
@@ -0,0 +1,82 @@
import { defineConfig } from 'vite'
import * as path from 'path'
import * as dotenv from 'dotenv'
import * as fs from 'fs'
import vue from '@vitejs/plugin-vue'
const NODE_ENV = process.env.NODE_ENV || 'development'
const envFile = `.env.${NODE_ENV}`
const envConfig = dotenv.parse(fs.readFileSync(envFile))
for (const k in envConfig) {
process.env[k] = envConfig[k]
}
let alias = {
'@': path.resolve(__dirname, './src'),
'vue$': 'vue/dist/vue.runtime.esm-bundler.js',
}
const conf = {
base: './', // index.html文件所在位置
root: './', // js导入的资源路径,src
server: {
open: true,
port: process.env.VITE_DEV_PORT,
proxy: {
[process.env.VITE_SERVER_API]: {
target: process.env.VITE_SERVER_PATH,
// rewrite: path => path.replace(/^\/api/, '/api'), //为了模拟
changeOrigin: true,
},
},
},
build: {
target: 'es2015',
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用 esbuild
manifest: false, // 是否产出maifest.json
sourcemap: false, // 是否产出soucemap.json
emptyOutDir: true,
outDir: 'dist', // 产出目录
rollupOptions: {
output: {
manualChunks (id) {
if (id.includes('node_modules')) {
const arr = id.toString().split('node_modules/')[1].split('/')
switch (arr[0]) {
case '@popperjs':
case '@vue':
case 'axios':
case 'element-plus':
case '@element-plus':
return '_' + arr[0]
default :
return '__vendor'
}
}else if(id.includes('Gwen-admin/src')){
//src 下的都打包到一起 不然很多小文件
return 'gwen'
}
},
chunkFileNames: 'static/chunk/[name]-[hash].js',
entryFileNames: 'static/entry/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
},
},
},
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
},
},
},
resolve: {
alias,
},
plugins: [
vue(),
],
}
// https://vitejs.dev/config/
export default defineConfig(conf)