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
+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>