初始化项目

This commit is contained in:
2023-01-10 11:52:47 +08:00
parent c74db0d2b9
commit 2180adecb0
142 changed files with 16480 additions and 0 deletions

191
src/view/about/index.vue Normal file
View File

@@ -0,0 +1,191 @@
<template>
<div>
<el-row :gutter="10">
<el-col :span="12">
<el-card>
<template #header>
<el-divider>gin-vue-admin</el-divider>
</template>
<div>
<el-row>
<el-col :span="8" :offset="8">
<a href="https://github.com/flipped-aurora/gin-vue-admin">
<img
class="org-img dom-center"
src="@/assets/logo.png"
alt="gin-vue-admin"
>
</a>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="8">
<a href="https://github.com/flipped-aurora/gin-vue-admin">
<img
class="dom-center"
src="https://img.shields.io/github/watchers/flipped-aurora/gin-vue-admin.svg?label=Watch"
alt=""
>
</a>
</el-col>
<el-col :span="8">
<a href="https://github.com/flipped-aurora/gin-vue-admin">
<img
class="dom-center"
src="https://img.shields.io/github/stars/flipped-aurora/gin-vue-admin.svg?style=social"
alt=""
>
</a>
</el-col>
<el-col :span="8">
<a href="https://github.com/flipped-aurora/gin-vue-admin">
<img
class="dom-center"
src="https://img.shields.io/github/forks/flipped-aurora/gin-vue-admin.svg?label=Fork"
alt=""
>
</a>
</el-col>
</el-row>
</div>
</el-card>
<el-card style="margin-top: 20px">
<template #header>
<div>flipped-aurora团队</div>
</template>
<div>
<el-row>
<el-col :span="8" :offset="8">
<a href="https://github.com/flipped-aurora">
<img
class="org-img dom-center"
src="@/assets/flipped-aurora.png"
alt="flipped-aurora"
>
</a>
</el-col>
</el-row>
<el-row style="margin-left: 40px" :gutter="20">
<el-col v-for="(item, index) in members" :key="index" :span="8">
<a :href="item.html_url">
<img class="avatar-img" :src="item.avatar_url">
<a class="author-name" style="">{{ item.login }}</a>
</a>
</el-col>
</el-row>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>
<div>提交记录</div>
</template>
<div>
<el-timeline>
<el-timeline-item
v-for="(item,index) in dataTimeline"
:key="index"
:timestamp="item.from"
placement="top"
>
<el-card>
<h4>{{ item.title }}</h4>
<p>{{ item.message }}</p>
</el-card>
</el-timeline-item>
</el-timeline>
</div>
<el-button
class="load-more"
type="primary"
link
@click="loadMore"
>Load more</el-button>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
export default {
name: 'About',
}
</script>
<script setup>
import { ref } from 'vue'
import { Commits, Members } from '@/api/github'
import { formatTimeToStr } from '@/utils/date'
const page = ref(0)
const loadMore = () => {
page.value++
loadCommits()
}
const dataTimeline = ref([])
const loadCommits = () => {
Commits(page.value).then(({ data }) => {
data.forEach((element) => {
if (element.commit.message) {
dataTimeline.value.push({
from: formatTimeToStr(element.commit.author.date, 'yyyy-MM-dd'),
title: element.commit.author.name,
showDayAndMonth: true,
message: element.commit.message,
})
}
})
})
}
const members = ref([])
const loadMembers = () => {
Members().then(({ data }) => {
members.value = data
members.value.sort()
})
}
loadCommits()
loadMembers()
</script>
<style scoped>
.load-more {
margin-left: 120px;
}
.avatar-img {
float: left;
height: 40px;
width: 40px;
border-radius: 50%;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
margin-top: 15px;
}
.org-img {
height: 150px;
width: 150px;
}
.author-name {
float: left;
line-height: 65px !important;
margin-left: 10px;
color: darkblue;
line-height: 100px;
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
"Lucida Sans Unicode", Geneva, Verdana, sans-serif;
}
.dom-center {
margin-left: 50%;
transform: translateX(-50%);
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="dashboard-line-box">
<div class="dashboard-line-title">
访问趋势
</div>
<div
ref="echart"
class="dashboard-line"
/>
</div>
</template>
<script setup>
import * as echarts from 'echarts'
import { nextTick, onMounted, onUnmounted, ref, shallowRef } from 'vue'
// import 'echarts/theme/macarons'
var dataAxis = []
for (var i = 1; i < 13; i++) {
dataAxis.push(`${i}`)
}
var data = [
220,
182,
191,
234,
290,
330,
310,
123,
442,
321,
90,
149,
]
var yMax = 500
var dataShadow = []
// eslint-disable-next-line no-redeclare
for (var i = 0; i < data.length; i++) {
dataShadow.push(yMax)
}
const chart = shallowRef(null)
const echart = ref(null)
const initChart = () => {
chart.value = echarts.init(echart.value /* 'macarons' */)
setOptions()
}
const setOptions = () => {
chart.value.setOption({
grid: {
left: '40',
right: '20',
top: '40',
bottom: '20',
},
xAxis: {
data: dataAxis,
axisTick: {
show: false,
},
axisLine: {
show: false,
},
z: 10,
},
yAxis: {
axisLine: {
show: false,
},
axisTick: {
show: false,
},
axisLabel: {
textStyle: {
color: '#999',
},
},
},
dataZoom: [
{
type: 'inside',
},
],
series: [
{
type: 'bar',
barWidth: '40%',
itemStyle: {
borderRadius: [5, 5, 0, 0],
color: '#188df0',
},
emphasis: {
itemStyle: {
color: '#188df0',
},
},
data: data,
},
],
})
}
onMounted(async() => {
await nextTick()
initChart()
})
onUnmounted(() => {
if (!chart.value) {
return
}
chart.value.dispose()
chart.value = null
})
</script>
<style lang="scss" scoped>
.dashboard-line-box {
.dashboard-line {
background-color: #fff;
height: 360px;
width: 100%;
}
.dashboard-line-title {
font-weight: 600;
margin-bottom: 12px;
}
}
</style>

View File

@@ -0,0 +1,112 @@
<template>
<div class="commit-table">
<div class="commit-table-title">
更新日志
</div>
<div class="log">
<div v-for="(item,key) in dataTimeline" :key="key" class="log-item">
<div class="flex-1 flex key-box">
<span class="key" :class="key<3&&'top'">{{ key+1 }}</span>
</div>
<div class="flex-5 flex message">{{ item.message }}</div>
<div class="flex-3 flex form">{{ item.from }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'DashboardTable',
}
</script>
<script setup>
import { Commits } from '@/api/github'
import { formatTimeToStr } from '@/utils/date.js'
import { ref } from 'vue'
const loading = ref(true)
const dataTimeline = ref([])
const loadCommits = () => {
Commits(0).then(({ data }) => {
loading.value = false
data.forEach((element, index) => {
if (element.commit.message && index < 10) {
dataTimeline.value.push({
from: formatTimeToStr(element.commit.author.date, 'yyyy-MM-dd'),
title: element.commit.author.name,
showDayAndMonth: true,
message: element.commit.message,
})
}
})
})
}
loadCommits()
</script>
<style lang="scss" scoped>
.commit-table{
background-color: #fff;
height: 400px;
&-title{
font-weight: 600;
margin-bottom: 12px;
}
.log{
&-item{
display: flex;
justify-content: space-between;
margin-top: 14px;
.key-box{
justify-content: center;
}
.key{
&.top{
background: #314659;
color: #FFFFFF;;
}
display: inline-flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: #F0F2F5;
text-align: center;
color:rgba($color: #000000, $alpha: 0.65)
}
.message{
color: rgba(0, 0, 0, 0.65);
}
.form{
color: rgba(0, 0, 0, 0.65);
margin-left: 12px;
}
.flex{
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.flex-1{
flex:1;
}
.flex-2{
flex:2;
}
.flex-3{
flex:3;
}
.flex-4{
flex:4;
}
.flex-5{
flex:5;
}
}
}
}
</style>

View File

@@ -0,0 +1,329 @@
<template>
<div class="page">
<div class="gva-card-box">
<div class="gva-card gva-top-card">
<div class="gva-top-card-left">
<div class="gva-top-card-left-title">早安管理员请开始一天的工作吧</div>
<div class="gva-top-card-left-dot">{{ weatherInfo }}</div>
<div class="gva-top-card-left-rows">
<el-row>
<el-col :span="8" :xs="24" :sm="8">
<div class="flex-center">
<el-icon class="dashboard-icon">
<sort />
</el-icon>
今日流量 (1231231)
</div>
</el-col>
<el-col :span="8" :xs="24" :sm="8">
<div class="flex-center">
<el-icon class="dashboard-icon">
<avatar />
</el-icon>
总用户数 (24001)
</div>
</el-col>
<el-col :span="8" :xs="24" :sm="8">
<div class="flex-center">
<el-icon class="dashboard-icon">
<comment />
</el-icon>
好评率 (99%)
</div>
</el-col>
</el-row>
</div>
<div>
<div class="gva-top-card-left-item">
使用教学
<a
style="color:#409EFF"
target="view_window"
href="https://www.bilibili.com/video/BV1Rg411u7xH/"
>https://www.bilibili.com/video/BV1Rg411u7xH</a>
</div>
<div class="gva-top-card-left-item">
插件仓库
<a
style="color:#409EFF"
target="view_window"
href="https://plugin.gin-vue-admin.com/#/layout/home"
>https://plugin.gin-vue-admin.com</a>
</div>
</div>
</div>
<img src="@/assets/dashboard.png" class="gva-top-card-right" alt>
</div>
</div>
<div class="gva-card-box">
<el-card class="gva-card quick-entrance">
<template #header>
<div class="card-header">
<span>快捷入口</span>
</div>
</template>
<el-row :gutter="20">
<el-col
v-for="(card, key) in toolCards"
:key="key"
:span="4"
:xs="8"
class="quick-entrance-items"
@click="toTarget(card.name)"
>
<div class="quick-entrance-item">
<div class="quick-entrance-item-icon" :style="{ backgroundColor: card.bg }">
<el-icon>
<component :is="card.icon" :style="{ color: card.color }" />
</el-icon>
</div>
<p>{{ card.label }}</p>
</div>
</el-col>
</el-row>
</el-card>
<!-- <div class="quick-entrance-title"></div> -->
</div>
<div class="gva-card-box">
<div class="gva-card">
<div class="card-header">
<span>数据统计</span>
</div>
<div class="echart-box">
<el-row :gutter="20">
<el-col :xs="24" :sm="18">
<echarts-line />
</el-col>
<el-col :xs="24" :sm="6">
<dashboard-table />
</el-col>
</el-row>
</div>
</div>
</div>
</div>
</template>
<script setup>
import EchartsLine from '@/view/dashboard/dashboardCharts/echartsLine.vue'
import DashboardTable from '@/view/dashboard/dashboardTable/dashboardTable.vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useWeatherInfo } from '@/view/dashboard/weather.js'
const weatherInfo = useWeatherInfo()
const toolCards = ref([
{
label: '用户管理',
icon: 'monitor',
name: 'user',
color: '#ff9c6e',
bg: 'rgba(255, 156, 110,.3)'
},
{
label: '角色管理',
icon: 'setting',
name: 'authority',
color: '#69c0ff',
bg: 'rgba(105, 192, 255,.3)'
},
{
label: '菜单管理',
icon: 'menu',
name: 'menu',
color: '#b37feb',
bg: 'rgba(179, 127, 235,.3)'
},
{
label: '代码生成器',
icon: 'cpu',
name: 'autoCode',
color: '#ffd666',
bg: 'rgba(255, 214, 102,.3)'
},
{
label: '表单生成器',
icon: 'document-checked',
name: 'formCreate',
color: '#ff85c0',
bg: 'rgba(255, 133, 192,.3)'
},
{
label: '关于我们',
icon: 'user',
name: 'about',
color: '#5cdbd3',
bg: 'rgba(92, 219, 211,.3)'
}
])
const router = useRouter()
const toTarget = (name) => {
router.push({ name })
}
</script>
<script>
export default {
name: 'Dashboard'
}
</script>
<style lang="scss" scoped>
@mixin flex-center {
display: flex;
align-items: center;
}
.page {
background: #f0f2f5;
padding: 0;
.gva-card-box{
padding: 12px 16px;
&+.gva-card-box{
padding-top: 0px;
}
}
.gva-card {
box-sizing: border-box;
background-color: #fff;
border-radius: 2px;
height: auto;
padding: 26px 30px;
overflow: hidden;
box-shadow: 0 0 7px 1px rgba(0, 0, 0, 0.03);
}
.gva-top-card {
height: 260px;
@include flex-center;
justify-content: space-between;
color: #777;
&-left {
height: 100%;
display: flex;
flex-direction: column;
&-title {
font-size: 22px;
color: #343844;
}
&-dot {
font-size: 16px;
color: #6B7687;
margin-top: 24px;
}
&-rows {
// margin-top: 15px;
margin-top: 18px;
color: #6B7687;
width: 600px;
align-items: center;
}
&-item{
+.gva-top-card-left-item{
margin-top: 24px;
}
margin-top: 14px;
}
}
&-right {
height: 600px;
width: 600px;
margin-top: 28px;
}
}
::v-deep(.el-card__header){
padding:0;
border-bottom: none;
}
.card-header{
padding-bottom: 20px;
border-bottom: 1px solid #e8e8e8;
}
.quick-entrance-title {
height: 30px;
font-size: 22px;
color: #333;
width: 100%;
border-bottom: 1px solid #eee;
}
.quick-entrance-items {
@include flex-center;
justify-content: center;
text-align: center;
color: #333;
.quick-entrance-item {
padding: 16px 28px;
margin-top: -16px;
margin-bottom: -16px;
border-radius: 4px;
transition: all 0.2s;
&:hover{
box-shadow: 0px 0px 7px 0px rgba(217, 217, 217, 0.55);
}
cursor: pointer;
height: auto;
text-align: center;
// align-items: center;
&-icon {
width: 50px;
height: 50px !important;
border-radius: 8px;
@include flex-center;
justify-content: center;
margin: 0 auto;
i {
font-size: 24px;
}
}
p {
margin-top: 10px;
}
}
}
.echart-box{
padding: 14px;
}
}
.dashboard-icon {
font-size: 20px;
color: rgb(85, 160, 248);
width: 30px;
height: 30px;
margin-right: 10px;
@include flex-center;
}
.flex-center {
@include flex-center;
}
//小屏幕不显示右侧,将登录框居中
@media (max-width: 750px) {
.gva-card {
padding: 20px 10px !important;
.gva-top-card {
height: auto;
&-left {
&-title {
font-size: 20px !important;
}
&-rows {
margin-top: 15px;
align-items: center;
}
}
&-right {
display: none;
}
}
.gva-middle-card {
&-item {
line-height: 20px;
}
}
.dashboard-icon {
font-size: 18px;
}
}
}
</style>

View File

@@ -0,0 +1,31 @@
import axios from 'axios'
import { ref } from 'vue'
const weatherInfo = ref('今日晴0℃ - 10℃天气寒冷注意添加衣物。')
const amapKey = '8e8baa8a7317586c29ec694895de6e0a'
export const useWeatherInfo = () => {
ip()
return weatherInfo
}
export const ip = async() => {
// key换成你自己的 https://console.amap.com/dev/index
if (amapKey === '') {
return false
}
const res = await axios.get('https://restapi.amap.com/v3/ip?key=' + amapKey)
if (res.data.adcode) {
getWeather(res.data.adcode)
}
}
const getWeather = async(code) => {
const response = await axios.get('https://restapi.amap.com/v3/weather/weatherInfo?key=' + amapKey + '&extensions=base&city=' + code)
if (response.data.status === '1') {
const s = response.data.lives[0]
weatherInfo.value = s.city + ' 天气:' + s.weather + ' 温度:' + s.temperature + '摄氏度 风向:' + s.winddirection + ' 风力:' + s.windpower + '级 空气湿度:' + s.humidity
}
}

45
src/view/error/index.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<div>
<div class="big">
<div class="inner">
<img src="../../assets/notFound.png">
<p>页面被神秘力量吸走了如果您是开源版请联系我们修复</p>
<p style="font-size:18px;line-height:40px;">常见问题为当前此角色无当前路由如果确定要使用本路由请到角色管理进行分配</p>
<p></p>
<img src="../../assets/qm.png" class="leftPic">
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Error'
}
</script>
<style lang="scss">
.big{
width: 100%;
height: calc(100vh - 220px);
background-color: rgb(244,244,244);
position: relative;
}
.inner{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.inner p {
text-align: center;
font-size: 24px;
}
.inner .leftPic{
width: 60px;
height: 60px;
margin-left: 44%;
margin-top: 20px;
}
</style>

14
src/view/error/reload.vue Normal file
View File

@@ -0,0 +1,14 @@
<template>
<div />
</template>
<script>
import { useRouter } from 'vue-router'
export default {
name: 'Reload'
}
</script>
<script setup>
const router = useRouter()
router.go(-1)
</script>

View File

@@ -0,0 +1,266 @@
<template>
<div class="break-point">
<div class="gva-table-box">
<el-divider content-position="left">大文件上传</el-divider>
<form id="fromCont" method="post">
<div class="fileUpload" @click="inputChange">
选择文件
<input v-show="false" id="file" ref="FileInput" multiple="multiple" type="file" @change="choseFile">
</div>
</form>
<el-button :disabled="limitFileSize" type="primary" size="small" class="uploadBtn" @click="getFile">上传文件</el-button>
<div class="el-upload__tip">请上传不超过5MB的文件</div>
<div class="list">
<transition name="list" tag="p">
<div v-if="file" class="list-item">
<el-icon>
<document />
</el-icon>
<span>{{ file.name }}</span>
<span class="percentage">{{ percentage }}%</span>
<el-progress :show-text="false" :text-inside="false" :stroke-width="2" :percentage="percentage" />
</div>
</transition>
</div>
<div class="tips">此版本为先行体验功能测试版样式美化和性能优化正在进行中上传切片文件和合成的完整文件分别再QMPlusserver目录的breakpointDir文件夹和fileDir文件夹</div>
</div>
</div>
</template>
<script setup>
import SparkMD5 from 'spark-md5'
import {
findFile,
breakpointContinueFinish,
removeChunk,
breakpointContinue
} from '@/api/breakpoint'
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
const file = ref(null)
const fileMd5 = ref('')
const formDataList = ref([])
const waitUpLoad = ref([])
const waitNum = ref(NaN)
const limitFileSize = ref(false)
const percentage = ref(0)
const percentageFlage = ref(true)
// 选中文件的函数
const choseFile = async(e) => {
const fileR = new FileReader() // 创建一个reader用来读取文件流
const fileInput = e.target.files[0] // 获取当前文件
const maxSize = 5 * 1024 * 1024
file.value = fileInput // file 丢全局方便后面用 可以改进为func传参形式
percentage.value = 0
if (file.value.size < maxSize) {
fileR.readAsArrayBuffer(file.value) // 把文件读成ArrayBuffer 主要为了保持跟后端的流一致
fileR.onload = async e => {
// 读成arrayBuffer的回调 e 为方法自带参数 相当于 dom的e 流存在e.target.result 中
const blob = e.target.result
const spark = new SparkMD5.ArrayBuffer() // 创建md5制造工具 md5用于检测文件一致性 这里不懂就打电话问我)
spark.append(blob) // 文件流丢进工具
fileMd5.value = spark.end() // 工具结束 产生一个a 总文件的md5
const FileSliceCap = 1 * 1024 * 1024 // 分片字节数
let start = 0 // 定义分片开始切的地方
let end = 0 // 每片结束切的地方a
let i = 0 // 第几片
formDataList.value = [] // 分片存储的一个池子 丢全局
while (end < file.value.size) {
// 当结尾数字大于文件总size的时候 结束切片
start = i * FileSliceCap // 计算每片开始位置
end = (i + 1) * FileSliceCap // 计算每片结束位置
var fileSlice = file.value.slice(start, end) // 开始切 file.slice 为 h5方法 对文件切片 参数为 起止字节数
const formData = new window.FormData() // 创建FormData用于存储传给后端的信息
formData.append('fileMd5', fileMd5.value) // 存储总文件的Md5 让后端知道自己是谁的切片
formData.append('file', fileSlice) // 当前的切片
formData.append('chunkNumber', i) // 当前是第几片
formData.append('fileName', file.value.name) // 当前文件的文件名 用于后端文件切片的命名 formData.appen 为 formData对象添加参数的方法
formDataList.value.push({ key: i, formData }) // 把当前切片信息 自己是第几片 存入我们方才准备好的池子
i++
}
const params = {
fileName: file.value.name,
fileMd5: fileMd5.value,
chunkTotal: formDataList.value.length
}
const res = await findFile(params)
// 全部切完以后 发一个请求给后端 拉当前文件后台存储的切片信息 用于检测有多少上传成功的切片
const finishList = res.data.file.ExaFileChunk // 上传成功的切片
const IsFinish = res.data.file.IsFinish // 是否是同文件不同命 文件md5相同 文件名不同 则默认是同一个文件但是不同文件名 此时后台数据库只需要拷贝一下数据库文件即可 不需要上传文件 即秒传功能)
if (!IsFinish) {
// 当是断点续传时候
waitUpLoad.value = formDataList.value.filter(all => {
return !(
finishList &&
finishList.some(fi => fi.FileChunkNumber === all.key)
) // 找出需要上传的切片
})
} else {
waitUpLoad.value = [] // 秒传则没有需要上传的切片
ElMessage.success('文件已秒传')
}
waitNum.value = waitUpLoad.value.length // 记录长度用于百分比展示
console.log(waitNum.value)
}
} else {
limitFileSize.value = true
ElMessage('请上传小于5M文件')
}
}
const getFile = () => {
// 确定按钮
if (file.value === null) {
ElMessage('请先上传文件')
return
}
if (percentage.value === 100) {
percentageFlage.value = false
}
sliceFile() // 上传切片
}
const sliceFile = () => {
waitUpLoad.value &&
waitUpLoad.value.forEach(item => {
// 需要上传的切片
item.formData.append('chunkTotal', formDataList.value.length) // 切片总数携带给后台 总有用的
const fileR = new FileReader() // 功能同上
const fileF = item.formData.get('file')
fileR.readAsArrayBuffer(fileF)
fileR.onload = e => {
const spark = new SparkMD5.ArrayBuffer()
spark.append(e.target.result)
item.formData.append('chunkMd5', spark.end()) // 获取当前切片md5 后端用于验证切片完整性
upLoadFileSlice(item)
}
})
}
watch(() => waitNum.value, () => { percentage.value = Math.floor(((formDataList.value.length - waitNum.value) / formDataList.value.length) * 100) })
const upLoadFileSlice = async(item) => {
// 切片上传
const fileRe = await breakpointContinue(item.formData)
if (fileRe.code !== 0) {
return
}
waitNum.value-- // 百分数增加
if (waitNum.value === 0) {
// 切片传完以后 合成文件
const params = {
fileName: file.value.name,
fileMd5: fileMd5.value
}
const res = await breakpointContinueFinish(params)
if (res.code === 0) {
// 合成文件过后 删除缓存切片
const params = {
fileName: file.value.name,
fileMd5: fileMd5.value,
filePath: res.data.filePath,
}
ElMessage.success('上传成功')
await removeChunk(params)
}
}
}
const FileInput = ref(null)
const inputChange = () => {
FileInput.value.dispatchEvent(new MouseEvent('click'))
}
</script>
<script>
export default {
name: 'BreakPoint'
}
</script>
<style lang='scss' scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
#fromCont{
display: inline-block;
}
.fileUpload{
padding: 3px 10px;
font-size: 12px;
height: 20px;
line-height: 20px;
position: relative;
cursor: pointer;
color: #000;
border: 1px solid #c1c1c1;
border-radius: 4px;
overflow: hidden;
display: inline-block;
input{
position: absolute;
font-size: 100px;
right: 0;
top: 0;
opacity: 0;
cursor: pointer;
}
}
.fileName{
display: inline-block;
vertical-align: top;
margin: 6px 15px 0 15px;
}
.uploadBtn{
position: relative;
top: -10px;
margin-left: 15px;
}
.tips{
margin-top: 30px;
font-size: 14px;
font-weight: 400;
color: #606266;
}
.el-divider{
margin: 0 0 30px 0;
}
.list{
margin-top:15px;
}
.list-item {
display: block;
margin-right: 10px;
color: #606266;
line-height: 25px;
margin-bottom: 5px;
width: 40%;
.percentage{
float: right;
}
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(-30px);
}
</style>

View File

@@ -0,0 +1,182 @@
<template>
<div>
<warning-bar title="在资源权限中将此角色的资源权限清空 或者不包含创建者的角色 即可屏蔽此客户资源的显示" />
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="openDialog">新增</el-button>
</div>
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
tooltip-effect="dark"
row-key="ID"
>
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="接入日期" width="180">
<template #default="scope">
<span>{{ formatDate(scope.row.CreatedAt) }}</span>
</template>
</el-table-column>
<el-table-column align="left" label="姓名" prop="customerName" width="120" />
<el-table-column align="left" label="电话" prop="customerPhoneData" width="120" />
<el-table-column align="left" label="接入人ID" prop="sysUserId" width="120" />
<el-table-column align="left" label="按钮组" min-width="160">
<template #default="scope">
<el-button size="small" type="primary" link icon="edit" @click="updateCustomer(scope.row)">变更</el-button>
<el-popover v-model="scope.row.visible" placement="top" width="160">
<p>确定要删除吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
<el-button type="primary" size="small" @click="deleteCustomer(scope.row)">确定</el-button>
</div>
<template #reference>
<el-button type="primary" link icon="delete" size="small" @click="scope.row.visible = true">删除</el-button>
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog v-model="dialogFormVisible" :before-close="closeDialog" title="客户">
<el-form :inline="true" :model="form" label-width="80px">
<el-form-item label="客户名">
<el-input v-model="form.customerName" autocomplete="off" />
</el-form-item>
<el-form-item label="客户电话">
<el-input v-model="form.customerPhoneData" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {
createExaCustomer,
updateExaCustomer,
deleteExaCustomer,
getExaCustomer,
getExaCustomerList
} from '@/api/customer'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/format'
const form = ref({
customerName: '',
customerPhoneData: ''
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getExaCustomerList({ page: page.value, pageSize: pageSize.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const dialogFormVisible = ref(false)
const type = ref('')
const updateCustomer = async(row) => {
const res = await getExaCustomer({ ID: row.ID })
type.value = 'update'
if (res.code === 0) {
form.value = res.data.customer
dialogFormVisible.value = true
}
}
const closeDialog = () => {
dialogFormVisible.value = false
form.value = {
customerName: '',
customerPhoneData: ''
}
}
const deleteCustomer = async(row) => {
row.visible = false
const res = await deleteExaCustomer({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
const enterDialog = async() => {
let res
switch (type.value) {
case 'create':
res = await createExaCustomer(form.value)
break
case 'update':
res = await updateExaCustomer(form.value)
break
default:
res = await createExaCustomer(form.value)
break
}
if (res.code === 0) {
closeDialog()
getTableData()
}
}
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
</script>
<script>
export default {
name: 'Customer'
}
</script>
<style></style>

View File

@@ -0,0 +1,21 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script>
export default {
name: 'Example'
}
</script>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
</script>

View File

@@ -0,0 +1,210 @@
<template>
<div v-loading.fullscreen.lock="fullscreenLoading">
<div class="gva-table-box">
<warning-bar
title="点击“文件名/备注”可以编辑文件名或者备注内容。"
/>
<div class="gva-btn-list">
<upload-common
v-model:imageCommon="imageCommon"
class="upload-btn"
@on-success="getTableData"
/>
<upload-image
v-model:imageUrl="imageUrl"
:file-size="512"
:max-w-h="1080"
class="upload-btn"
@on-success="getTableData"
/>
<el-form ref="searchForm" :inline="true" :model="search">
<el-form-item label="">
<el-input v-model="search.keyword" class="keyword" placeholder="请输入文件名或备注" />
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" icon="search" @click="getTableData">查询</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="tableData">
<el-table-column align="left" label="预览" width="100">
<template #default="scope">
<CustomPic pic-type="file" :pic-src="scope.row.url" />
</template>
</el-table-column>
<el-table-column align="left" label="日期" prop="UpdatedAt" width="180">
<template #default="scope">
<div>{{ formatDate(scope.row.UpdatedAt) }}</div>
</template>
</el-table-column>
<el-table-column align="left" label="文件名/备注" prop="name" width="180">
<template #default="scope">
<div class="name" @click="editFileNameFunc(scope.row)">{{ scope.row.name }}</div>
</template>
</el-table-column>
<el-table-column align="left" label="链接" prop="url" min-width="300" />
<el-table-column align="left" label="标签" prop="tag" width="100">
<template #default="scope">
<el-tag
:type="scope.row.tag === 'jpg' ? 'primary' : 'success'"
disable-transitions
>{{ scope.row.tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column align="left" label="操作" width="160">
<template #default="scope">
<el-button size="small" icon="download" type="primary" link @click="downloadFile(scope.row)">下载</el-button>
<el-button size="small" icon="delete" type="primary" link @click="deleteFileFunc(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:style="{ float: 'right', padding: '20px' }"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import { getFileList, deleteFile, editFileName } from '@/api/fileUploadAndDownload'
import { downloadImage } from '@/utils/downloadImg'
import CustomPic from '@/components/customPic/index.vue'
import UploadImage from '@/components/upload/image.vue'
import UploadCommon from '@/components/upload/common.vue'
import { formatDate } from '@/utils/format'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const path = ref(import.meta.env.VITE_BASE_API)
const imageUrl = ref('')
const imageCommon = ref('')
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const search = ref({})
const tableData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getFileList({ page: page.value, pageSize: pageSize.value, ...search.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const deleteFileFunc = async(row) => {
ElMessageBox.confirm('此操作将永久删除文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async() => {
const res = await deleteFile(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!',
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '已取消删除',
})
})
}
const downloadFile = (row) => {
if (row.url.indexOf('http://') > -1 || row.url.indexOf('https://') > -1) {
downloadImage(row.url, row.name)
} else {
debugger
downloadImage(path.value + '/' + row.url, row.name)
}
}
/**
* 编辑文件名或者备注
* @param row
* @returns {Promise<void>}
*/
const editFileNameFunc = async(row) => {
ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '不能为空',
inputValue: row.name
}).then(async({ value }) => {
row.name = value
// console.log(row)
const res = await editFileName(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '编辑成功!',
})
getTableData()
}
}).catch(() => {
ElMessage({
type: 'info',
message: '取消修改'
})
})
}
</script>
<script>
export default {
name: 'Upload',
}
</script>
<style scoped>
.name {
cursor: pointer;
}
.upload-btn + .upload-btn {
margin-left: 12px;
}
</style>

320
src/view/init/index.vue Normal file
View File

@@ -0,0 +1,320 @@
<template>
<div class="init_page">
<div class="init_page_panel">
<div v-if="hello < 2" id="hello" :class="[hello < 1 ? 'slide-in-fwd-top' : 'slide-out-right']" class="hello">
<div>
<div class="hello_title">GIN-VUE-ADMIN</div>
<p class="in-two a-fadeinT">初始化须知</p>
<p class="init_p">1.您需有用一定的VUE和GOLANG基础</p>
<p class="init_p">2.请您确认是否已经阅读过官方文档</p>
<p class="init_p">3.请您确认是否了解后续的配置流程</p>
<p class="init_p">开发组不为文档中书写过的内容提供无偿服务</p>
<p class="init_btn">
<el-button type="primary" @click="goDoc">
阅读文档
</el-button>
<el-button type="primary" @click="showNext">
我已确认
</el-button>
</p>
</div>
</div>
<div v-if="hello > 0 " :class="[(hello > 0 && !out)? 'slide-in-left' : '' , out ? 'slide-out-right' : '']"
class="form">
<el-form ref="formRef" :model="form" label-width="100px">
<el-form-item label="数据库类型">
<el-select v-model="form.dbType" placeholder="请选择" @change="changeDB">
<el-option key="mysql" label="mysql" value="mysql" />
<el-option key="pgsql" label="pgsql" value="pgsql" />
<el-option key="oracle" label="oracle" value="oracle" />
<el-option key="mssql" label="mssql" value="mssql" />
</el-select>
</el-form-item>
<el-form-item label="host">
<el-input v-model="form.host" placeholder="请输入数据库链接" />
</el-form-item>
<el-form-item label="port">
<el-input v-model="form.port" placeholder="请输入数据库端口" />
</el-form-item>
<el-form-item label="userName">
<el-input v-model="form.userName" placeholder="请输入数据库用户名" />
</el-form-item>
<el-form-item label="password">
<el-input v-model="form.password" placeholder="请输入数据库密码(没有则为空)" />
</el-form-item>
<el-form-item label="dbName">
<el-input v-model="form.dbName" placeholder="请输入数据库名称" />
</el-form-item>
<el-form-item>
<div style="text-align: right">
<el-button type="primary" @click="onSubmit">立即初始化</el-button>
</div>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Init',
}
</script>
<script setup>
// @ts-ignore
import { initDB } from '@/api/initdb'
import { reactive, ref } from 'vue'
import { ElLoading, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const hello = ref(0)
const showNext = () => {
hello.value = hello.value + 1
}
const goDoc = () => {
window.open('https://www.gin-vue-admin.com/guide/start-quickly/env.html')
}
const out = ref(false)
const form = reactive({
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
userName: 'root',
password: '',
dbName: 'gva',
})
const changeDB = (val) => {
switch (val) {
case 'mysql':
Object.assign(form, {
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
userName: 'root',
password: '',
dbName: 'gva',
})
break
case 'pgsql':
Object.assign(form, {
dbType: 'pgsql',
host: '127.0.0.1',
port: '5432',
userName: 'postgres',
password: '',
dbName: 'gva',
})
break
case 'oracle':
Object.assign(form, {
dbType: 'oracle',
host: '127.0.0.1',
port: '1521',
userName: 'oracle',
password: '',
dbName: 'gva',
})
break
case 'mssql':
Object.assign(form, {
dbType: 'mssql',
host: '127.0.0.1',
port: '1433',
userName: 'mssql',
password: '',
dbName: 'gva',
})
break
default:
Object.assign(form, {
dbType: 'mysql',
host: '127.0.0.1',
port: '3306',
userName: 'root',
password: '',
dbName: 'gva',
})
}
}
const onSubmit = async () => {
const loading = ElLoading.service({
lock: true,
text: '正在初始化数据库,请稍候',
spinner: 'loading',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const res = await initDB(form)
if (res.code === 0) {
out.value = true
ElMessage({
type: 'success',
message: res.msg,
})
router.push({ name: 'Login' })
}
loading.close()
} catch (err) {
loading.close()
}
}
</script>
<style lang="scss" scoped>
.init_page {
margin: 0;
padding: 0;
background-image: url('@/assets/login_background.jpg');
background-size: cover;
width: 100%;
height: 100%;
position: relative;
.init_page_panel {
position: absolute;
top: 3vh;
left: 2vw;
width: 96vw;
height: 94vh;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: space-evenly;
.hello {
position: absolute;
z-index: 2;
text-align: center;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.hello_title {
font-size: 32px;
line-height: 98px;
}
.in-two {
font-size: 22px;
}
.init_p {
margin-top: 20px;
color: #777777;
}
.init_btn {
margin-top: 20px;
}
}
.form {
position: absolute;
z-index: 3;
margin-top: 60px;
width: 600px;
height: auto;
padding: 20px;
border-radius: 6px;
}
}
}
.slide-in-fwd-top {
-webkit-animation: slide-in-fwd-top 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94)
both;
animation: slide-in-fwd-top 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.slide-out-right {
-webkit-animation: slide-out-right 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53)
both;
animation: slide-out-right 0.5s cubic-bezier(0.55, 0.085, 0.68, 0.53) both;
}
.slide-in-left {
-webkit-animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)
both;
animation: slide-in-left 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
@-webkit-keyframes slide-in-fwd-top {
0% {
-webkit-transform: translateZ(-1400px) translateY(-800px);
transform: translateZ(-1400px) translateY(-800px);
opacity: 0;
}
100% {
-webkit-transform: translateZ(0) translateY(0);
transform: translateZ(0) translateY(0);
opacity: 1;
}
}
@keyframes slide-in-fwd-top {
0% {
-webkit-transform: translateZ(-1400px) translateY(-800px);
transform: translateZ(-1400px) translateY(-800px);
opacity: 0;
}
100% {
-webkit-transform: translateZ(0) translateY(0);
transform: translateZ(0) translateY(0);
opacity: 1;
}
}
@-webkit-keyframes slide-out-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(1000px);
transform: translateX(1000px);
opacity: 0;
}
}
@keyframes slide-out-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
100% {
-webkit-transform: translateX(1000px);
transform: translateX(1000px);
opacity: 0;
}
}
@-webkit-keyframes slide-in-left {
0% {
-webkit-transform: translateX(-1000px);
transform: translateX(-1000px);
opacity: 0;
}
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
}
@keyframes slide-in-left {
0% {
-webkit-transform: translateX(-1000px);
transform: translateX(-1000px);
opacity: 0;
}
100% {
-webkit-transform: translateX(0);
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 750px) {
.form {
width: 94vw !important;
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,95 @@
<template>
<el-sub-menu ref="subMenu" :index="routerInfo.name">
<template #title>
<div v-if="!isCollapse" class="gva-subMenu">
<el-icon v-if="routerInfo.meta.icon">
<component :is="routerInfo.meta.icon" />
</el-icon>
<span>{{ routerInfo.meta.title }}</span>
</div>
<template v-else>
<el-icon v-if="routerInfo.meta.icon">
<component :is="routerInfo.meta.icon" />
</el-icon>
<span>{{ routerInfo.meta.title }}</span>
</template>
</template>
<slot />
</el-sub-menu>
</template>
<script>
export default {
name: 'AsyncSubmenu',
}
</script>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
routerInfo: {
default: function() {
return null
},
type: Object
},
isCollapse: {
default: function() {
return false
},
type: Boolean
},
theme: {
default: function() {
return {}
},
type: Object
}
})
const activeBackground = ref(props.theme.activeBackground)
const activeText = ref(props.theme.activeText)
const normalText = ref(props.theme.normalText)
// const hoverBackground = ref(props.theme.hoverBackground)
// const hoverText = ref(props.theme.hoverText)
watch(() => props.theme, () => {
activeBackground.value = props.theme.activeBackground
activeText.value = props.theme.activeText
normalText.value = props.theme.normalText
// hoverBackground.value = props.theme.hoverBackground
// hoverText.value = props.theme.hoverText
})
</script>
<style lang="scss" scoped>
.el-sub-menu{
::v-deep(.el-sub-menu__title){
padding: 6px;
color: v-bind(normalText);
}
}
.is-active:not(.is-opened){
::v-deep(.el-sub-menu__title) .gva-subMenu{
flex:1;
height: 100%;
line-height: 44px;
background: v-bind(activeBackground) !important;
border-radius: 4px;
box-shadow: 0 0 2px 1px v-bind(activeBackground) !important;
i{
color: v-bind(activeText);
}
span{
color: v-bind(activeText);
}
}
}
.gva-subMenu {
padding-left: 4px;
}
</style>

View File

@@ -0,0 +1,59 @@
<template>
<component
:is="menuComponent"
v-if="!routerInfo.hidden"
:is-collapse="isCollapse"
:theme="theme"
:router-info="routerInfo"
>
<template v-if="routerInfo.children&&routerInfo.children.length">
<AsideComponent
v-for="item in routerInfo.children"
:key="item.name"
:is-collapse="false"
:router-info="item"
:theme="theme"
/>
</template>
</component>
</template>
<script>
export default {
name: 'AsideComponent',
}
</script>
<script setup>
import MenuItem from './menuItem.vue'
import AsyncSubmenu from './asyncSubmenu.vue'
import { computed } from 'vue'
const props = defineProps({
routerInfo: {
type: Object,
default: () => null,
},
isCollapse: {
default: function() {
return false
},
type: Boolean
},
theme: {
default: function() {
return {}
},
type: Object
}
})
const menuComponent = computed(() => {
if (props.routerInfo.children && props.routerInfo.children.filter(item => !item.hidden).length) {
return AsyncSubmenu
} else {
return MenuItem
}
})
</script>

View File

@@ -0,0 +1,121 @@
<template>
<el-menu-item :index="routerInfo.name">
<template v-if="isCollapse">
<el-tooltip
class="box-item"
effect="light"
:content="routerInfo.meta.title"
placement="right"
>
<el-icon v-if="routerInfo.meta.icon">
<component :is="routerInfo.meta.icon" />
</el-icon>
</el-tooltip>
</template>
<template v-else>
<div class="gva-menu-item">
<el-icon v-if="routerInfo.meta.icon">
<component :is="routerInfo.meta.icon" />
</el-icon>
<span class="gva-menu-item-title">{{ routerInfo.meta.title }}</span>
</div>
</template>
</el-menu-item>
</template>
<script>
export default {
name: 'MenuItem',
}
</script>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
routerInfo: {
default: function() {
return null
},
type: Object
},
isCollapse: {
default: function() {
return false
},
type: Boolean
},
theme: {
default: function() {
return {}
},
type: Object
}
})
const activeBackground = ref(props.theme.activeBackground)
const activeText = ref(props.theme.activeText)
const normalText = ref(props.theme.normalText)
const hoverBackground = ref(props.theme.hoverBackground)
const hoverText = ref(props.theme.hoverText)
watch(() => props.theme, () => {
activeBackground.value = props.theme.activeBackground
activeText.value = props.theme.activeText
normalText.value = props.theme.normalText
hoverBackground.value = props.theme.hoverBackground
hoverText.value = props.theme.hoverText
})
</script>
<style lang="scss" scoped>
.gva-menu-item{
width: 100%;
flex:1;
height: 44px;
display: flex;
justify-content: flex-start;
align-items: center;
padding-left: 4px;
.gva-menu-item-title {
flex:1;
}
}
.el-menu--collapse{
.el-menu-item.is-active{
color: v-bind(activeBackground);
}
}
.el-menu-item{
color: v-bind(normalText);
}
.el-menu-item.is-active{
.gva-menu-item{
background: v-bind(activeBackground) !important;
border-radius: 4px;
box-shadow: 0 0 2px 1px v-bind(activeBackground) !important;
i{
color: v-bind(activeText);
}
span{
color: v-bind(activeText);
}
}
}
.el-menu-item:hover{
.gva-menu-item{
background: v-bind(hoverBackground);
border-radius: 4px;
box-shadow: 0 0 2px 1px v-bind(hoverBackground);
color: v-bind(hoverText);
}
}
</style>

View File

@@ -0,0 +1,363 @@
<template>
<div class="router-history">
<el-tabs
v-model="activeValue"
:closable="!(historys.length === 1 && $route.name === defaultRouter)"
type="card"
@contextmenu.prevent="openContextMenu($event)"
@tab-click="changeTab"
@tab-remove="removeTab"
>
<el-tab-pane
v-for="item in historys"
:key="name(item)"
:label="item.meta.title"
:name="name(item)"
:tab="item"
class="gva-tab"
>
<template #label>
<span
:tab="item"
:style="{
color: activeValue === name(item) ? userStore.activeColor : '#333',
}"
><i
class="dot"
:style="{
backgroundColor:
activeValue === name(item) ? userStore.activeColor : '#ddd',
}"
/>
{{ fmtTitle(item.meta.title,item) }}</span>
</template>
</el-tab-pane>
</el-tabs>
<!--自定义右键菜单html代码-->
<ul
v-show="contextMenuVisible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="closeAll">关闭所有</li>
<li @click="closeLeft">关闭左侧</li>
<li @click="closeRight">关闭右侧</li>
<li @click="closeOther">关闭其他</li>
</ul>
</div>
</template>
<script>
export default {
name: 'HistoryComponent',
}
</script>
<script setup>
import { emitter } from '@/utils/bus.js'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/pinia/modules/user'
import { fmtTitle } from '@/utils/fmtRouterTitle'
const route = useRoute()
const router = useRouter()
const getFmtString = (item) => {
return item.name + JSON.stringify(item.query) + JSON.stringify(item.params)
}
const historys = ref([])
const activeValue = ref('')
const contextMenuVisible = ref(false)
const userStore = useUserStore()
const name = (item) => {
return (
item.name + JSON.stringify(item.query) + JSON.stringify(item.params)
)
}
const left = ref(0)
const top = ref(0)
const isCollapse = ref(false)
const isMobile = ref(false)
const rightActive = ref('')
const defaultRouter = computed(() => userStore.userInfo.authority.defaultRouter)
const openContextMenu = (e) => {
if (
historys.value.length === 1 &&
route.name === defaultRouter.value
) {
return false
}
let id = ''
if (e.srcElement.nodeName === 'SPAN') {
id = e.srcElement.offsetParent.id
} else {
id = e.srcElement.id
}
if (id) {
contextMenuVisible.value = true
let width
if (isCollapse.value) {
width = 54
} else {
width = 220
}
if (isMobile.value) {
width = 0
}
left.value = e.clientX - width
top.value = e.clientY + 10
rightActive.value = id.substring(4)
}
}
const closeAll = () => {
historys.value = [
{
name: defaultRouter.value,
meta: {
title: '首页',
},
query: {},
params: {},
},
]
router.push({ name: defaultRouter.value })
contextMenuVisible.value = false
sessionStorage.setItem('historys', JSON.stringify(historys.value))
}
const closeLeft = () => {
let right
const rightIndex = historys.value.findIndex((item) => {
if (getFmtString(item) === rightActive.value) {
right = item
}
return getFmtString(item) === rightActive.value
})
const activeIndex = historys.value.findIndex(
(item) => getFmtString(item) === activeValue.value
)
historys.value.splice(0, rightIndex)
if (rightIndex > activeIndex) {
router.push(right)
}
sessionStorage.setItem('historys', JSON.stringify(historys.value))
}
const closeRight = () => {
let right
const leftIndex = historys.value.findIndex((item) => {
if (getFmtString(item) === rightActive.value) {
right = item
}
return getFmtString(item) === rightActive.value
})
const activeIndex = historys.value.findIndex(
(item) => getFmtString(item) === activeValue.value
)
historys.value.splice(leftIndex + 1, historys.value.length)
if (leftIndex < activeIndex) {
router.push(right)
}
sessionStorage.setItem('historys', JSON.stringify(historys.value))
}
const closeOther = () => {
let right
historys.value = historys.value.filter((item) => {
if (getFmtString(item) === rightActive.value) {
right = item
}
return getFmtString(item) === rightActive.value
})
router.push(right)
sessionStorage.setItem('historys', JSON.stringify(historys.value))
}
const isSame = (route1, route2) => {
if (route1.name !== route2.name) {
return false
}
if (Object.keys(route1.query).length !== Object.keys(route2.query).length || Object.keys(route1.params).length !== Object.keys(route2.params).length) {
return false
}
for (const key in route1.query) {
if (route1.query[key] !== route2.query[key]) {
return false
}
}
for (const key in route1.params) {
if (route1.params[key] !== route2.params[key]) {
return false
}
}
return true
}
const setTab = (route) => {
if (!historys.value.some((item) => isSame(item, route))) {
const obj = {}
obj.name = route.name
obj.meta = { ...route.meta }
delete obj.meta.matched
obj.query = route.query
obj.params = route.params
historys.value.push(obj)
}
window.sessionStorage.setItem('activeValue', getFmtString(route))
}
const historyMap = ref({})
const changeTab = (TabsPaneContext) => {
const name = TabsPaneContext?.props?.name
if (!name) return
const tab = historyMap.value[name]
router.push({
name: tab.name,
query: tab.query,
params: tab.params,
})
}
const removeTab = (tab) => {
const index = historys.value.findIndex(
(item) => getFmtString(item) === tab
)
if (getFmtString(route) === tab) {
if (historys.value.length === 1) {
router.push({ name: defaultRouter.value })
} else {
if (index < historys.value.length - 1) {
router.push({
name: historys.value[index + 1].name,
query: historys.value[index + 1].query,
params: historys.value[index + 1].params,
})
} else {
router.push({
name: historys.value[index - 1].name,
query: historys.value[index - 1].query,
params: historys.value[index - 1].params,
})
}
}
}
historys.value.splice(index, 1)
}
watch(() => contextMenuVisible.value, () => {
if (contextMenuVisible.value) {
document.body.addEventListener('click', () => {
contextMenuVisible.value = false
})
} else {
document.body.removeEventListener('click', () => {
contextMenuVisible.value = false
})
}
})
watch(() => route, (to, now) => {
if (to.name === 'Login' || to.name === 'Reload') {
return
}
historys.value = historys.value.filter((item) => !item.meta.closeTab)
setTab(to)
sessionStorage.setItem('historys', JSON.stringify(historys.value))
activeValue.value = window.sessionStorage.getItem('activeValue')
}, { deep: true })
watch(() => historys.value, () => {
sessionStorage.setItem('historys', JSON.stringify(historys.value))
historyMap.value = {}
historys.value.forEach((item) => {
historyMap.value[getFmtString(item)] = item
})
emitter.emit('setKeepAlive', historys.value)
}, {
deep: true
})
const initPage = () => {
// 全局监听 关闭当前页面函数
emitter.on('closeThisPage', () => {
removeTab(name(route))
})
// 全局监听 关闭所有页面函数
emitter.on('closeAllPage', () => {
closeAll()
})
emitter.on('mobile', (data) => {
isMobile.value = data
})
emitter.on('collapse', (data) => {
isCollapse.value = data
})
const initHistorys = [
{
name: defaultRouter.value,
meta: {
title: '首页',
},
query: {},
params: {},
},
]
historys.value =
JSON.parse(sessionStorage.getItem('historys')) || initHistorys
if (!window.sessionStorage.getItem('activeValue')) {
activeValue.value = getFmtString(route)
} else {
activeValue.value = window.sessionStorage.getItem('activeValue')
}
setTab(route)
if (window.sessionStorage.getItem('needCloseAll') === 'true') {
closeAll()
window.sessionStorage.removeItem('needCloseAll')
}
}
initPage()
onUnmounted(() => {
emitter.off('collapse')
emitter.off('mobile')
})
</script>
<style lang="scss">
.contextmenu {
width: 100px;
margin: 0;
border: 1px solid #ccc;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 14px;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.2);
}
.el-tabs__item .el-icon-close {
color: initial !important;
}
.el-tabs__item .dot {
content: "";
width: 9px;
height: 9px;
margin-right: 8px;
display: inline-block;
border-radius: 50%;
transition: background-color 0.2s;
}
.contextmenu li {
margin: 0;
padding: 7px 16px;
}
.contextmenu li:hover {
background: #f2f2f2;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div :style="{ background: userStore.sideMode }">
<el-scrollbar style="height: calc(100vh - 60px)">
<transition
:duration="{ enter: 800, leave: 100 }"
mode="out-in"
name="el-fade-in-linear"
>
<el-menu
:collapse="isCollapse"
:collapse-transition="false"
:default-active="active"
:background-color="theme.background"
:active-text-color="theme.active"
class="el-menu-vertical"
unique-opened
@select="selectMenuItem"
>
<template v-for="item in routerStore.asyncRouters[0].children">
<aside-component
v-if="!item.hidden"
:key="item.name"
:is-collapse="isCollapse"
:router-info="item"
:theme="theme"
/>
</template>
</el-menu>
</transition>
</el-scrollbar>
</div>
</template>
<script>
export default {
name: 'Aside',
}
</script>
<script setup>
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
import { emitter } from '@/utils/bus.js'
import { ref, watch, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '@/pinia/modules/user'
import { useRouterStore } from '@/pinia/modules/router'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const routerStore = useRouterStore()
const theme = ref({})
const getTheme = () => {
switch (userStore.sideMode) {
case '#fff':
theme.value = {
background: '#fff',
activeBackground: '#4D70FF',
activeText: '#fff',
normalText: '#333',
hoverBackground: 'rgba(64, 158, 255, 0.08)',
hoverText: '#333',
}
break
case '#191a23':
theme.value = {
background: '#191a23',
activeBackground: '#4D70FF',
activeText: '#fff',
normalText: '#fff',
hoverBackground: 'rgba(64, 158, 255, 0.08)',
hoverText: '#fff',
}
break
}
}
getTheme()
const active = ref('')
watch(() => route, () => {
active.value = route.meta.activeName || route.name
}, { deep: true })
watch(() => userStore.sideMode, () => {
getTheme()
})
const isCollapse = ref(false)
const initPage = () => {
active.value = route.meta.activeName || route.name
const screenWidth = document.body.clientWidth
if (screenWidth < 1000) {
isCollapse.value = !isCollapse.value
}
emitter.on('collapse', (item) => {
isCollapse.value = item
})
}
initPage()
onUnmounted(() => {
emitter.off('collapse')
})
const selectMenuItem = (index, _, ele, aaa) => {
const query = {}
const params = {}
routerStore.routeMap[index]?.parameters &&
routerStore.routeMap[index]?.parameters.forEach((item) => {
if (item.type === 'query') {
query[item.key] = item.value
} else {
params[item.key] = item.value
}
})
if (index === route.name) return
if (index.indexOf('http://') > -1 || index.indexOf('https://') > -1) {
window.open(index)
} else {
router.push({ name: index, query, params })
}
}
</script>
<style lang="scss">
.el-sub-menu__title:hover,
.el-menu-item:hover {
background: transparent;
}
.el-scrollbar {
.el-scrollbar__view {
height: 100%;
}
}
.menu-info {
.menu-contorl {
line-height: 52px;
font-size: 20px;
display: table-cell;
vertical-align: middle;
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="bottom-info">
<div>
<span>Powered by</span>
<span>
<a href="https://github.com/flipped-aurora/gin-vue-admin">{{ $GIN_VUE_ADMIN.appName }}</a>
</span>
<el-divider direction="vertical" />
<span>Copyright</span>
<span>
<a href="https://github.com/flipped-aurora">flipped-aurora团队</a>
</span>
</div>
</div>
</template>
<script>
// 此文件内容为版权信息如需改动请联系wx:shouzi_1994购买授权 未授权状态只需保留此代码 不影响任何正常使用
// 项目为apatch协议 请遵守版权协议内容
export default {
name: 'BottomInfo'
}
</script>
<style lang="scss">
.bottom-info {
color: #888;
height: 30px;
line-height: 12px;
a {
color: #888;
}
div {
display: flex;
justify-content: center;
span{
margin: 0 3px;
}
}
}
</style>

258
src/view/layout/index.vue Normal file
View File

@@ -0,0 +1,258 @@
<template>
<el-container class="layout-cont">
<el-container :class="[isSider?'openside':'hideside',isMobile ? 'mobile': '']">
<el-row :class="[isShadowBg?'shadowBg':'']" @click="changeShadow()" />
<el-aside class="main-cont main-left gva-aside">
<div class="tilte" :style="{background: backgroundColor}">
<img alt class="logoimg" :src="$GIN_VUE_ADMIN.appLogo">
<div v-if="isSider" class="tit-text" :style="{color:textColor}">{{ $GIN_VUE_ADMIN.appName }}</div>
</div>
<Aside class="aside" />
</el-aside>
<!-- 分块滑动功能 -->
<el-main class="main-cont main-right">
<transition :duration="{ enter: 800, leave: 100 }" mode="out-in" name="el-fade-in-linear">
<div
:style="{width: `calc(100% - ${isMobile?'0px':isCollapse?'54px':'220px'})`}"
class="topfix"
>
<el-row>
<el-col>
<el-header class="header-cont">
<el-row class="pd-0">
<el-col :xs="2" :lg="1" :md="1" :sm="1" :xl="1" style="z-index:100">
<div class="menu-total" @click="totalCollapse">
<div v-if="isCollapse" class="gvaIcon gvaIcon-arrow-double-right" />
<div v-else class="gvaIcon gvaIcon-arrow-double-left" />
</div>
</el-col>
<el-col :xs="10" :lg="14" :md="14" :sm="9" :xl="14" :pull="1">
<!-- 修改为手机端不显示顶部标签 -->
<el-breadcrumb v-show="!isMobile" class="breadcrumb">
<el-breadcrumb-item
v-for="item in matched.slice(1,matched.length)"
:key="item.path"
>{{ fmtTitle(item.meta.title,route) }}</el-breadcrumb-item>
</el-breadcrumb>
</el-col>
<el-col :xs="12" :lg="9" :md="9" :sm="14" :xl="9">
<div class="right-box">
<Search />
<el-dropdown>
<div class="dp-flex justify-content-center align-items height-full width-full">
<span class="header-avatar" style="cursor: pointer">
<CustomPic />
<span v-show="!isMobile" style="margin-left: 5px">{{ userStore.userInfo.nickName }}</span>
<el-icon>
<arrow-down />
</el-icon>
</span>
</div>
<template #dropdown>
<el-dropdown-menu class="dropdown-group">
<el-dropdown-item>
<span style="font-weight: 600;">
当前角色{{ userStore.userInfo.authority.authorityName }}
</span>
</el-dropdown-item>
<template v-if="userStore.userInfo.authorities">
<el-dropdown-item v-for="item in userStore.userInfo.authorities.filter(i=>i.authorityId!==userStore.userInfo.authorityId)" :key="item.authorityId" @click="changeUserAuth(item.authorityId)">
<span>
切换为{{ item.authorityName }}
</span>
</el-dropdown-item>
</template>
<el-dropdown-item icon="avatar" @click="toPerson">个人信息</el-dropdown-item>
<el-dropdown-item icon="reading-lamp" @click="userStore.LoginOut"> </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-col>
</el-row>
</el-header>
</el-col>
</el-row>
<!-- 当前面包屑用路由自动生成可根据需求修改 -->
<!--
:to="{ path: item.path }" 暂时注释不用-->
<HistoryComponent ref="layoutHistoryComponent" />
</div>
</transition>
<router-view
v-if="reloadFlag"
v-slot="{ Component }"
v-loading="loadingFlag"
element-loading-text="正在加载中"
class="admin-box"
>
<div>
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</div>
</router-view>
<BottomInfo />
<setting />
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
name: 'Layout',
}
</script>
<script setup>
import Aside from '@/view/layout/aside/index.vue'
import HistoryComponent from '@/view/layout/aside/historyComponent/history.vue'
import Search from '@/view/layout/search/search.vue'
import BottomInfo from '@/view/layout/bottomInfo/bottomInfo.vue'
import CustomPic from '@/components/customPic/index.vue'
import Setting from './setting/index.vue'
import { setUserAuthority } from '@/api/user'
import { emitter } from '@/utils/bus.js'
import { computed, ref, onMounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useRouterStore } from '@/pinia/modules/router'
import { fmtTitle } from '@/utils/fmtRouterTitle'
import { useUserStore } from '@/pinia/modules/user'
const router = useRouter()
const route = useRoute()
const routerStore = useRouterStore()
// 三种窗口适配
const isCollapse = ref(false)
const isSider = ref(true)
const isMobile = ref(false)
const initPage = () => {
const screenWidth = document.body.clientWidth
if (screenWidth < 1000) {
isMobile.value = true
isSider.value = false
isCollapse.value = true
} else if (screenWidth >= 1000 && screenWidth < 1200) {
isMobile.value = false
isSider.value = false
isCollapse.value = true
} else {
isMobile.value = false
isSider.value = true
isCollapse.value = false
}
}
initPage()
const loadingFlag = ref(false)
onMounted(() => {
// 挂载一些通用的事件
emitter.emit('collapse', isCollapse.value)
emitter.emit('mobile', isMobile.value)
emitter.on('reload', reload)
emitter.on('showLoading', () => {
loadingFlag.value = true
})
emitter.on('closeLoading', () => {
loadingFlag.value = false
})
window.onresize = () => {
return (() => {
initPage()
emitter.emit('collapse', isCollapse.value)
emitter.emit('mobile', isMobile.value)
})()
}
if (userStore.loadingInstance) {
userStore.loadingInstance.close()
}
})
const userStore = useUserStore()
const textColor = computed(() => {
if (userStore.sideMode === 'dark') {
return '#fff'
} else if (userStore.sideMode === 'light') {
return '#191a23'
} else {
return userStore.baseColor
}
})
const backgroundColor = computed(() => {
if (userStore.sideMode === 'dark') {
return '#191a23'
} else if (userStore.sideMode === 'light') {
return '#fff'
} else {
return userStore.sideMode
}
})
const matched = computed(() => route.meta.matched)
const changeUserAuth = async(id) => {
const res = await setUserAuthority({
authorityId: id
})
if (res.code === 0) {
window.sessionStorage.setItem('needCloseAll', 'true')
window.location.reload()
}
}
const reloadFlag = ref(true)
let reloadTimer = null
const reload = async() => {
if (reloadTimer) {
window.clearTimeout(reloadTimer)
}
reloadTimer = window.setTimeout(async() => {
if (route.meta.keepAlive) {
reloadFlag.value = false
await nextTick()
reloadFlag.value = true
} else {
const title = route.meta.title
router.push({ name: 'Reload', params: { title }})
}
}, 400)
}
const isShadowBg = ref(false)
const totalCollapse = () => {
isCollapse.value = !isCollapse.value
isSider.value = !isCollapse.value
isShadowBg.value = !isCollapse.value
emitter.emit('collapse', isCollapse.value)
}
const toPerson = () => {
router.push({ name: 'person' })
}
const changeShadow = () => {
isShadowBg.value = !isShadowBg.value
isSider.value = !!isCollapse.value
totalCollapse()
}
</script>
<style lang="scss">
@import '@/style/mobile.scss';
.dark{
background-color: #191a23 !important;
color: #fff !important;
}
.light{
background-color: #fff !important;
color: #000 !important;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div @click="clickFull">
<div v-if="isShow" class="gvaIcon gvaIcon-fullscreen-expand" />
<div v-else class="gvaIcon gvaIcon-fullscreen-shrink" />
</div>
</template>
<script>
export default {
name: 'Screenfull',
}
</script>
<script setup>
import screenfull from 'screenfull' // 引入screenfull
import { onMounted, onUnmounted, ref } from 'vue'
defineProps({
width: {
type: Number,
default: 22
},
height: {
type: Number,
default: 22
},
fill: {
type: String,
default: '#48576a'
}
})
onMounted(() => {
if (screenfull.isEnabled) {
screenfull.on('change', changeFullShow)
}
})
onUnmounted(() => {
screenfull.off('change')
})
const clickFull = () => {
if (screenfull.isEnabled) {
screenfull.toggle()
}
}
const isShow = ref(true)
const changeFullShow = () => {
isShow.value = !screenfull.isFullscreen
}
</script>
<style scoped lang="scss">
.screenfull-svg {
width: 16px;
height: 16px;
cursor: pointer;
vertical-align: middle;
margin-right: 32px;
fill: rgba(0, 0, 0, 0.45);
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<div class="search-component">
<div v-if="show" class="transition-box" style="display: inline-block;">
<el-select
ref="searchInput"
v-model="value"
filterable
placeholder="请选择"
@blur="hiddenSearch"
@change="changeRouter"
>
<el-option
v-for="item in routerStore.routerList"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div
v-if="btnShow"
class="user-box"
>
<div class="gvaIcon gvaIcon-refresh" :class="[reload ? 'reloading' : '']" @click="handleReload" />
</div>
<div
v-if="btnShow"
class="user-box"
>
<div class="gvaIcon gvaIcon-search" @click="showSearch" />
</div>
<div
v-if="btnShow"
class="user-box"
>
<Screenfull class="search-icon" :style="{cursor:'pointer'}" />
</div>
<div
v-if="btnShow"
class="user-box"
>
<div class="service gvaIcon-customer-service" @click="toService" />
</div>
</div>
</template>
<script>
export default {
name: 'BtnBox',
}
</script>
<script setup>
import Screenfull from '@/view/layout/screenfull/index.vue'
import { emitter } from '@/utils/bus.js'
import { ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { useRouterStore } from '@/pinia/modules/router'
const router = useRouter()
const routerStore = useRouterStore()
const value = ref('')
const changeRouter = () => {
router.push({ name: value.value })
value.value = ''
}
const show = ref(false)
const btnShow = ref(true)
const hiddenSearch = () => {
show.value = false
btnShow.value = true
}
const searchInput = ref(null)
const showSearch = async() => {
btnShow.value = false
show.value = true
await nextTick()
searchInput.value.focus()
}
const reload = ref(false)
const handleReload = () => {
reload.value = true
emitter.emit('reload')
setTimeout(() => {
reload.value = false
}, 500)
}
const toService = () => {
window.open('https://support.qq.com/product/371961')
}
</script>
<style scoped lang="scss">
.reload{
font-size: 18px;
}
.reloading{
animation:turn 0.5s linear infinite;
}
@keyframes turn {
0%{-webkit-transform:rotate(0deg);}
25%{-webkit-transform:rotate(90deg);}
50%{-webkit-transform:rotate(180deg);}
75%{-webkit-transform:rotate(270deg);}
100%{-webkit-transform:rotate(360deg);}
}
.service {
font-family: "gvaIcon" !important;
font-size: 16px;
font-style: normal;
font-weight: 800;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
//小屏幕不显示
@media (max-width: 750px) {
.service {
display: none;
}
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<div>
<el-button type="primary" class="drawer-container" icon="setting" @click="showSettingDrawer" />
<el-drawer
v-model="drawer"
title="系统配置"
:direction="direction"
:before-close="handleClose"
>
<div class="setting_body">
<div class="setting_card">
<div class="setting_content">
<div class="theme-box">
<div class="item" @click="changeMode('light')">
<div class="item-top">
<el-icon v-if="userStore.mode === 'light'" class="check">
<check />
</el-icon>
<img src="https://gw.alipayobjects.com/zos/antfincdn/NQ%24zoisaD2/jpRkZQMyYRryryPNtyIC.svg">
</div>
<p>
简约白
</p>
</div>
<div class="item" @click="changeMode('dark')">
<div class="item-top">
<el-icon v-if="userStore.mode === 'dark'" class="check">
<check />
</el-icon>
<img src="https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg">
</div>
<p>
商务黑
</p>
</div>
</div>
</div>
</div>
</div>
</el-drawer>
</div>
</template>
<script>
export default {
name: 'Setting',
}
</script>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/pinia/modules/user'
const drawer = ref(false)
const direction = ref('rtl')
const userStore = useUserStore()
const handleClose = () => {
drawer.value = false
}
const showSettingDrawer = () => {
drawer.value = true
}
const changeMode = (e) => {
if (e === null) {
userStore.changeSideMode('dark')
return
}
userStore.changeSideMode(e)
}
</script>
<style lang="scss" scoped>
.drawer-container {
transition: all 0.2s;
&:hover{
right: 0
}
position: fixed;
right: -20px;
bottom: 15%;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
color: #fff;
border-radius: 4px 0 0 4px;
cursor: pointer;
-webkit-box-shadow: inset 0 0 6px rgba(0 ,0 ,0, 10%);
}
.setting_body{
padding: 20px;
.setting_card{
margin-bottom: 20px;
}
.setting_content{
margin-top: 20px;
display: flex;
flex-direction: column;
>.theme-box{
display: flex;
}
>.color-box{
div{
display: flex;
flex-direction: column;
}
}
.item{
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin-right: 20px;
.item-top{
position: relative;
}
.check{
position: absolute;
font-size: 20px;
color: #00afff;
right:10px;
bottom: 10px;
}
p{
text-align: center;
font-size: 12px;
}
}
}
}
</style>

228
src/view/login/index.vue Normal file
View File

@@ -0,0 +1,228 @@
<template>
<div id="userLayout">
<div class="login_panel">
<div class="login_panel_form">
<div class="login_panel_form_title">
<img
class="login_panel_form_title_logo"
:src="$GIN_VUE_ADMIN.appLogo"
alt
>
<p class="login_panel_form_title_p">{{ $GIN_VUE_ADMIN.appName }}</p>
</div>
<el-form
ref="loginForm"
:model="loginFormData"
:rules="rules"
:validate-on-rule-change="false"
@keyup.enter="submitForm"
>
<el-form-item prop="username">
<el-input
v-model="loginFormData.username"
placeholder="请输入用户名"
>
<template #suffix>
<span class="input-icon">
<el-icon>
<user />
</el-icon>
</span>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginFormData.password"
:type="lock === 'lock' ? 'password' : 'text'"
placeholder="请输入密码"
>
<template #suffix>
<span class="input-icon">
<el-icon>
<component
:is="lock"
@click="changeLock"
/>
</el-icon>
</span>
</template>
</el-input>
</el-form-item>
<el-form-item prop="captcha" v-if="loginFormData.openCaptcha">
<div class="vPicBox">
<el-input
v-model="loginFormData.captcha"
placeholder="请输入验证码"
style="width: 60%"
/>
<div class="vPic">
<img
v-if="picPath"
:src="picPath"
alt="请输入验证码"
@click="loginVerify()"
>
</div>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
style="width: 46%"
size="large"
@click="checkInit"
>前往初始化</el-button>
<el-button
type="primary"
size="large"
style="width: 46%; margin-left: 8%"
@click="submitForm"
> </el-button>
</el-form-item>
</el-form>
</div>
<div class="login_panel_right" />
<div class="login_panel_foot">
<div class="links">
<a href="http://doc.henrongyi.top/" target="_blank">
<img src="@/assets/docs.png" class="link-icon">
</a>
<a href="https://support.qq.com/product/371961" target="_blank">
<img src="@/assets/kefu.png" class="link-icon">
</a>
<a
href="https://github.com/flipped-aurora/gin-vue-admin"
target="_blank"
>
<img src="@/assets/github.png" class="link-icon">
</a>
<a href="https://space.bilibili.com/322210472" target="_blank">
<img src="@/assets/video.png" class="link-icon">
</a>
</div>
<div class="copyright">
<BottomInfo />
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
}
</script>
<script setup>
import { captcha } from '@/api/user'
import { checkDB } from '@/api/initdb'
import BottomInfo from '@/view/layout/bottomInfo/bottomInfo.vue'
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/pinia/modules/user'
const router = useRouter()
// 验证函数
const checkUsername = (rule, value, callback) => {
if (value.length < 5) {
return callback(new Error('请输入正确的用户名'))
} else {
callback()
}
}
const checkPassword = (rule, value, callback) => {
if (value.length < 6) {
return callback(new Error('请输入正确的密码'))
} else {
callback()
}
}
// 获取验证码
const loginVerify = () => {
captcha({}).then(async(ele) => {
rules.captcha.push({
max: ele.data.captchaLength,
min: ele.data.captchaLength,
message: `请输入${ele.data.captchaLength}位验证码`,
trigger: 'blur',
})
picPath.value = ele.data.picPath
loginFormData.captchaId = ele.data.captchaId
loginFormData.openCaptcha = ele.data.openCaptcha
})
}
loginVerify()
// 登录相关操作
const lock = ref('lock')
const changeLock = () => {
lock.value = lock.value === 'lock' ? 'unlock' : 'lock'
}
const loginForm = ref(null)
const picPath = ref('')
const loginFormData = reactive({
username: 'admin',
password: '123456',
captcha: '',
captchaId: '',
openCaptcha: false,
})
const rules = reactive({
username: [{ validator: checkUsername, trigger: 'blur' }],
password: [{ validator: checkPassword, trigger: 'blur' }],
captcha: [
{
message: '验证码格式不正确',
trigger: 'blur',
},
],
})
const userStore = useUserStore()
const login = async() => {
return await userStore.LoginIn(loginFormData)
}
const submitForm = () => {
loginForm.value.validate(async(v) => {
if (v) {
const flag = await login()
if (!flag) {
loginVerify()
}
} else {
ElMessage({
type: 'error',
message: '请正确填写登录信息',
showClose: true,
})
loginVerify()
return false
}
})
}
// 跳转初始化
const checkInit = async() => {
const res = await checkDB()
if (res.code === 0) {
if (res.data?.needInit) {
userStore.NeedInit()
router.push({ name: 'Init' })
} else {
ElMessage({
type: 'info',
message: '已配置数据库信息,无法初始化',
})
}
}
}
</script>
<style lang="scss" scoped>
@import "@/style/newLogin.scss";
</style>

561
src/view/person/person.vue Normal file
View File

@@ -0,0 +1,561 @@
<template>
<div>
<el-row>
<el-col :span="6">
<div class="fl-left avatar-box">
<div class="user-card">
<div
class="user-headpic-update"
:style="{
'background-image': `url(${
userStore.userInfo.headerImg &&
userStore.userInfo.headerImg.slice(0, 4) !== 'http'
? path + userStore.userInfo.headerImg
: userStore.userInfo.headerImg
})`,
'background-repeat': 'no-repeat',
'background-size': 'cover',
}"
>
<span class="update" @click="openChooseImg">
<el-icon>
<edit />
</el-icon>
重新上传</span>
</div>
<div class="user-personality">
<p v-if="!editFlag" class="nickName">
{{ userStore.userInfo.nickName }}
<el-icon class="pointer" color="#66b1ff" @click="openEdit">
<edit />
</el-icon>
</p>
<p v-if="editFlag" class="nickName">
<el-input v-model="nickName" />
<el-icon class="pointer" color="#67c23a" @click="enterEdit">
<check />
</el-icon>
<el-icon class="pointer" color="#f23c3c" @click="closeEdit">
<close />
</el-icon>
</p>
<p class="person-info">这个家伙很懒什么都没有留下</p>
</div>
<div class="user-information">
<ul>
<li>
<el-icon>
<user />
</el-icon>
{{ userStore.userInfo.nickName }}
</li>
<el-tooltip
class="item"
effect="light"
content="北京反转极光科技有限公司-技术部-前端事业群"
placement="top"
>
<li>
<el-icon>
<data-analysis />
</el-icon>
北京反转极光科技有限公司-技术部-前端事业群
</li>
</el-tooltip>
<li>
<el-icon>
<video-camera />
</el-icon>
中国·北京市·朝阳区
</li>
<el-tooltip
class="item"
effect="light"
content="GoLang/JavaScript/Vue/Gorm"
placement="top"
>
<li>
<el-icon>
<medal />
</el-icon>
GoLang/JavaScript/Vue/Gorm
</li>
</el-tooltip>
</ul>
</div>
</div>
</div>
</el-col>
<el-col :span="18">
<div class="user-addcount">
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane label="账号绑定" name="second">
<ul>
<li>
<p class="title">密保手机</p>
<p class="desc">
已绑定手机:{{ userStore.userInfo.phone }}
<a href="javascript:void(0)" @click="changePhoneFlag = true">立即修改</a>
</p>
</li>
<li>
<p class="title">密保邮箱</p>
<p class="desc">
已绑定邮箱{{ userStore.userInfo.email }}
<a href="javascript:void(0)" @click="changeEmailFlag = true">立即修改</a>
</p>
</li>
<li>
<p class="title">密保问题</p>
<p class="desc">
未设置密保问题
<a href="javascript:void(0)">去设置</a>
</p>
</li>
<li>
<p class="title">修改密码</p>
<p class="desc">
修改个人密码
<a
href="javascript:void(0)"
@click="showPassword = true"
>修改密码</a>
</p>
</li>
</ul>
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
<ChooseImg ref="chooseImgRef" @enter-img="enterImg" />
<el-dialog
v-model="showPassword"
title="修改密码"
width="360px"
@close="clearPassword"
>
<el-form
ref="modifyPwdForm"
:model="pwdModify"
:rules="rules"
label-width="80px"
>
<el-form-item :minlength="6" label="原密码" prop="password">
<el-input v-model="pwdModify.password" show-password />
</el-form-item>
<el-form-item :minlength="6" label="新密码" prop="newPassword">
<el-input v-model="pwdModify.newPassword" show-password />
</el-form-item>
<el-form-item :minlength="6" label="确认密码" prop="confirmPassword">
<el-input v-model="pwdModify.confirmPassword" show-password />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button
size="small"
@click="showPassword = false"
> </el-button>
<el-button
size="small"
type="primary"
@click="savePassword"
> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="changePhoneFlag" title="绑定手机" width="600px">
<el-form :model="phoneForm">
<el-form-item label="手机号" label-width="120px">
<el-input v-model="phoneForm.phone" placeholder="请输入手机号" autocomplete="off" />
</el-form-item>
<el-form-item label="验证码" label-width="120px">
<div class="code-box">
<el-input v-model="phoneForm.code" autocomplete="off" placeholder="请自行设计短信服务,此处为模拟随便写" style="width:300px" />
<el-button size="small" type="primary" :disabled="time>0" @click="getCode">{{ time>0?`(${time}s)后重新获取`:'获取验证码' }}</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button
size="small"
@click="closeChangePhone"
>取消</el-button>
<el-button
type="primary"
size="small"
@click="changePhone"
>更改</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="changeEmailFlag" title="绑定邮箱" width="600px">
<el-form :model="emailForm">
<el-form-item label="邮箱" label-width="120px">
<el-input v-model="emailForm.email" placeholder="请输入邮箱" autocomplete="off" />
</el-form-item>
<el-form-item label="验证码" label-width="120px">
<div class="code-box">
<el-input v-model="emailForm.code" placeholder="请自行设计邮件服务,此处为模拟随便写" autocomplete="off" style="width:300px" />
<el-button size="small" type="primary" :disabled="emailTime>0" @click="getEmailCode">{{ emailTime>0?`(${emailTime}s)后重新获取`:'获取验证码' }}</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button
size="small"
@click="closeChangeEmail"
>取消</el-button>
<el-button
type="primary"
size="small"
@click="changeEmail"
>更改</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'Person',
}
</script>
<script setup>
import ChooseImg from '@/components/chooseImg/index.vue'
import { setSelfInfo, changePassword } from '@/api/user.js'
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/pinia/modules/user'
const path = ref(import.meta.env.VITE_BASE_API + '/')
const activeName = ref('second')
const rules = reactive({
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '最少6个字符', trigger: 'blur' },
],
newPassword: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '最少6个字符', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请输入确认密码', trigger: 'blur' },
{ min: 6, message: '最少6个字符', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (value !== pwdModify.value.newPassword) {
callback(new Error('两次密码不一致'))
} else {
callback()
}
},
trigger: 'blur',
},
],
})
const userStore = useUserStore()
const modifyPwdForm = ref(null)
const showPassword = ref(false)
const pwdModify = ref({})
const nickName = ref('')
const editFlag = ref(false)
const savePassword = async() => {
modifyPwdForm.value.validate((valid) => {
if (valid) {
changePassword({
password: pwdModify.value.password,
newPassword: pwdModify.value.newPassword,
}).then((res) => {
if (res.code === 0) {
ElMessage.success('修改密码成功!')
}
showPassword.value = false
})
} else {
return false
}
})
}
const clearPassword = () => {
pwdModify.value = {
password: '',
newPassword: '',
confirmPassword: '',
}
modifyPwdForm.value.clearValidate()
}
const chooseImgRef = ref(null)
const openChooseImg = () => {
chooseImgRef.value.open()
}
const enterImg = async(url) => {
const res = await setSelfInfo({ headerImg: url })
if (res.code === 0) {
userStore.ResetUserInfo({ headerImg: url })
ElMessage({
type: 'success',
message: '设置成功',
})
}
}
const openEdit = () => {
nickName.value = userStore.userInfo.nickName
editFlag.value = true
}
const closeEdit = () => {
nickName.value = ''
editFlag.value = false
}
const enterEdit = async() => {
const res = await setSelfInfo({
nickName: nickName.value
})
if (res.code === 0) {
userStore.ResetUserInfo({ nickName: nickName.value })
ElMessage({
type: 'success',
message: '设置成功',
})
}
nickName.value = ''
editFlag.value = false
}
const handleClick = (tab, event) => {
console.log(tab, event)
}
const changePhoneFlag = ref(false)
const time = ref(0)
const phoneForm = reactive({
phone: '',
code: ''
})
const getCode = async() => {
time.value = 60
let timer = setInterval(() => {
time.value--
if (time.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
const closeChangePhone = () => {
changePhoneFlag.value = false
phoneForm.phone = ''
phoneForm.code = ''
}
const changePhone = async() => {
const res = await setSelfInfo({ phone: phoneForm.phone })
if (res.code === 0) {
ElMessage.success('修改成功')
userStore.ResetUserInfo({ phone: phoneForm.phone })
closeChangePhone()
}
}
const changeEmailFlag = ref(false)
const emailTime = ref(0)
const emailForm = reactive({
email: '',
code: ''
})
const getEmailCode = async() => {
emailTime.value = 60
let timer = setInterval(() => {
emailTime.value--
if (emailTime.value <= 0) {
clearInterval(timer)
timer = null
}
}, 1000)
}
const closeChangeEmail = () => {
changeEmailFlag.value = false
emailForm.email = ''
emailForm.code = ''
}
const changeEmail = async() => {
const res = await setSelfInfo({ email: emailForm.email })
if (res.code === 0) {
ElMessage.success('修改成功')
userStore.ResetUserInfo({ email: emailForm.email })
closeChangeEmail()
}
}
</script>
<style lang="scss">
.avatar-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
.avatar-box {
box-shadow: -2px 0 20px -16px;
width: 80%;
height: 100%;
.user-card {
min-height: calc(90vh - 200px);
padding: 30px 20px;
text-align: center;
background-color: #fff;
border-radius: 8px;
flex-shrink: 0;
.el-avatar {
border-radius: 50%;
}
.user-personality {
padding: 24px 0;
text-align: center;
p {
font-size: 16px;
}
.nickName {
display: flex;
justify-content: center;
align-items: center;
font-size: 26px;
}
.person-info {
margin-top: 6px;
font-size: 14px;
color: #999;
}
}
.user-information {
width: 100%;
height: 100%;
text-align: left;
ul {
display: inline-block;
height: 100%;
width: 100%;
li {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
i {
margin-right: 8px;
}
padding: 20px 0;
font-size: 16px;
font-weight: 400;
color: #606266;
}
}
}
}
}
.user-addcount {
background-color: #fff;
padding: 20px;
border-radius: 8px;
ul {
li {
.title {
padding: 10px;
font-size: 18px;
color: #696969;
}
.desc {
font-size: 16px;
padding: 0 10px 20px 10px;
color: #a9a9a9;
a {
color: rgb(64, 158, 255);
float: right;
}
}
border-bottom: 2px solid #f0f2f5;
&:last-child{
border-bottom: none;
}
}
}
}
.user-headpic-update {
width: 120px;
height: 120px;
line-height: 120px;
margin: 0 auto;
display: flex;
justify-content: center;
border-radius: 20px;
&:hover {
color: #fff;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.15) 0%,
rgba(0, 0, 0, 0.15) 100%
),
radial-gradient(
at top center,
rgba(255, 255, 255, 0.4) 0%,
rgba(0, 0, 0, 0.4) 120%
)
#989898;
background-blend-mode: multiply, multiply;
.update {
color: #fff;
}
}
.update {
height: 120px;
width: 120px;
text-align: center;
color: transparent;
}
}
.pointer {
cursor: pointer;
}
.code-box{
display: flex;
justify-content: space-between;
}
</style>

23
src/view/routerHolder.vue Normal file
View File

@@ -0,0 +1,23 @@
<!-- 此路由可作为父类路由通用路由页面使用 如需自定义父类路由页面 请参考 @/view/superAdmin/index.vue -->
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script>
export default {
name: 'RouterHolder'
}
</script>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
</script>

View File

@@ -0,0 +1,391 @@
<template>
<div>
<div class="gva-search-box">
<el-form ref="searchForm" :inline="true" :model="searchInfo">
<el-form-item label="路径">
<el-input v-model="searchInfo.path" placeholder="路径" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="searchInfo.description" placeholder="描述" />
</el-form-item>
<el-form-item label="API组">
<el-input v-model="searchInfo.apiGroup" placeholder="api组" />
</el-form-item>
<el-form-item label="请求">
<el-select v-model="searchInfo.method" clearable placeholder="请选择">
<el-option
v-for="item in methodOptions"
:key="item.value"
:label="`${item.label}(${item.value})`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button size="small" icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="openDialog('addApi')">新增</el-button>
<el-popover v-model="deleteVisible" placement="top" width="160">
<p>确定要删除吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="deleteVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="onDelete">确定</el-button>
</div>
<template #reference>
<el-button icon="delete" size="small" :disabled="!apis.length" style="margin-left: 10px;" @click="deleteVisible = true">删除</el-button>
</template>
</el-popover>
</div>
<el-table :data="tableData" @sort-change="sortChange" @selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55"
/>
<el-table-column align="left" label="id" min-width="60" prop="ID" sortable="custom" />
<el-table-column align="left" label="API路径" min-width="150" prop="path" sortable="custom" />
<el-table-column align="left" label="API分组" min-width="150" prop="apiGroup" sortable="custom" />
<el-table-column align="left" label="API简介" min-width="150" prop="description" sortable="custom" />
<el-table-column align="left" label="请求" min-width="150" prop="method" sortable="custom">
<template #default="scope">
<div>
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
</div>
</template>
</el-table-column>
<el-table-column align="left" fixed="right" label="操作" width="200">
<template #default="scope">
<el-button
icon="edit"
size="small"
type="primary"
link
@click="editApiFunc(scope.row)"
>编辑</el-button>
<el-button
icon="delete"
size="small"
type="primary"
link
@click="deleteApiFunc(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog v-model="dialogFormVisible" :before-close="closeDialog" :title="dialogTitle">
<warning-bar title="新增API需要在角色管理内配置权限才可使用" />
<el-form ref="apiForm" :model="form" :rules="rules" label-width="80px">
<el-form-item label="路径" prop="path">
<el-input v-model="form.path" autocomplete="off" />
</el-form-item>
<el-form-item label="请求" prop="method">
<el-select v-model="form.method" placeholder="请选择" style="width:100%">
<el-option
v-for="item in methodOptions"
:key="item.value"
:label="`${item.label}(${item.value})`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="api分组" prop="apiGroup">
<el-input v-model="form.apiGroup" autocomplete="off" />
</el-form-item>
<el-form-item label="api简介" prop="description">
<el-input v-model="form.description" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'Api',
}
</script>
<script setup>
import {
getApiById,
getApiList,
createApi,
updateApi,
deleteApi,
deleteApisByIds
} from '@/api/api'
import { toSQLLine } from '@/utils/stringFun'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const methodFilter = (value) => {
const target = methodOptions.value.filter(item => item.value === value)[0]
return target && `${target.label}`
}
const apis = ref([])
const form = ref({
path: '',
apiGroup: '',
method: '',
description: ''
})
const methodOptions = ref([
{
value: 'POST',
label: '创建',
type: 'success'
},
{
value: 'GET',
label: '查看',
type: ''
},
{
value: 'PUT',
label: '更新',
type: 'warning'
},
{
value: 'DELETE',
label: '删除',
type: 'danger'
}
])
const type = ref('')
const rules = ref({
path: [{ required: true, message: '请输入api路径', trigger: 'blur' }],
apiGroup: [
{ required: true, message: '请输入组名称', trigger: 'blur' }
],
method: [
{ required: true, message: '请选择请求方式', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入api介绍', trigger: 'blur' }
]
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
const onReset = () => {
searchInfo.value = {}
}
// 搜索
const onSubmit = () => {
page.value = 1
pageSize.value = 10
getTableData()
}
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 排序
const sortChange = ({ prop, order }) => {
if (prop) {
if (prop === 'ID') {
prop = 'id'
}
searchInfo.value.orderKey = toSQLLine(prop)
searchInfo.value.desc = order === 'descending'
}
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getApiList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// 批量操作
const handleSelectionChange = (val) => {
apis.value = val
}
const deleteVisible = ref(false)
const onDelete = async() => {
const ids = apis.value.map(item => item.ID)
const res = await deleteApisByIds({ ids })
if (res.code === 0) {
ElMessage({
type: 'success',
message: res.msg
})
if (tableData.value.length === ids.length && page.value > 1) {
page.value--
}
deleteVisible.value = false
getTableData()
}
}
// 弹窗相关
const apiForm = ref(null)
const initForm = () => {
apiForm.value.resetFields()
form.value = {
path: '',
apiGroup: '',
method: '',
description: ''
}
}
const dialogTitle = ref('新增Api')
const dialogFormVisible = ref(false)
const openDialog = (key) => {
switch (key) {
case 'addApi':
dialogTitle.value = '新增Api'
break
case 'edit':
dialogTitle.value = '编辑Api'
break
default:
break
}
type.value = key
dialogFormVisible.value = true
}
const closeDialog = () => {
initForm()
dialogFormVisible.value = false
}
const editApiFunc = async(row) => {
const res = await getApiById({ id: row.ID })
form.value = res.data.api
openDialog('edit')
}
const enterDialog = async() => {
apiForm.value.validate(async valid => {
if (valid) {
switch (type.value) {
case 'addApi':
{
const res = await createApi(form.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '添加成功',
showClose: true
})
}
getTableData()
closeDialog()
}
break
case 'edit':
{
const res = await updateApi(form.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '编辑成功',
showClose: true
})
}
getTableData()
closeDialog()
}
break
default:
// eslint-disable-next-line no-lone-blocks
{
ElMessage({
type: 'error',
message: '未知操作',
showClose: true
})
}
break
}
}
})
}
const deleteApiFunc = async(row) => {
ElMessageBox.confirm('此操作将永久删除所有角色下该api, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async() => {
const res = await deleteApi(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
}
</script>
<style scoped lang="scss">
.button-box {
padding: 10px 20px;
.el-button {
float: right;
}
}
.warning {
color: #dc143c;
}
</style>

View File

@@ -0,0 +1,410 @@
<template>
<div class="authority">
<warning-bar title="注:右上角头像下拉可切换角色" />
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="addAuthority(0)">新增角色</el-button>
</div>
<el-table
:data="tableData"
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
row-key="authorityId"
style="width: 100%"
>
<el-table-column label="角色ID" min-width="180" prop="authorityId" />
<el-table-column align="left" label="角色名称" min-width="180" prop="authorityName" />
<el-table-column align="left" label="操作" width="460">
<template #default="scope">
<el-button
icon="setting"
size="small"
type="primary"
link
@click="opdendrawer(scope.row)"
>设置权限</el-button>
<el-button
icon="plus"
size="small"
type="primary"
link
@click="addAuthority(scope.row.authorityId)"
>新增子角色</el-button>
<el-button
icon="copy-document"
size="small"
type="primary"
link
@click="copyAuthorityFunc(scope.row)"
>拷贝</el-button>
<el-button
icon="edit"
size="small"
type="primary"
link
@click="editAuthority(scope.row)"
>编辑</el-button>
<el-button
icon="delete"
size="small"
type="primary"
link
@click="deleteAuth(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增角色弹窗 -->
<el-dialog v-model="dialogFormVisible" :title="dialogTitle">
<el-form ref="authorityForm" :model="form" :rules="rules" label-width="80px">
<el-form-item label="父级角色" prop="parentId">
<el-cascader
v-model="form.parentId"
style="width:100%"
:disabled="dialogType=='add'"
:options="AuthorityOption"
:props="{ checkStrictly: true,label:'authorityName',value:'authorityId',disabled:'disabled',emitPath:false}"
:show-all-levels="false"
filterable
/>
</el-form-item>
<el-form-item label="角色ID" prop="authorityId">
<el-input v-model="form.authorityId" :disabled="dialogType=='edit'" autocomplete="off" />
</el-form-item>
<el-form-item label="角色姓名" prop="authorityName">
<el-input v-model="form.authorityName" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
<el-drawer v-if="drawer" v-model="drawer" custom-class="auth-drawer" :with-header="false" size="40%" title="角色配置">
<el-tabs :before-leave="autoEnter" type="border-card">
<el-tab-pane label="角色菜单">
<Menus ref="menus" :row="activeRow" @changeRow="changeRow" />
</el-tab-pane>
<el-tab-pane label="角色api">
<Apis ref="apis" :row="activeRow" @changeRow="changeRow" />
</el-tab-pane>
<el-tab-pane label="资源权限">
<Datas ref="datas" :authority="tableData" :row="activeRow" @changeRow="changeRow" />
</el-tab-pane>
</el-tabs>
</el-drawer>
</div>
</template>
<script setup>
import {
getAuthorityList,
deleteAuthority,
createAuthority,
updateAuthority,
copyAuthority
} from '@/api/authority'
import Menus from '@/view/superAdmin/authority/components/menus.vue'
import Apis from '@/view/superAdmin/authority/components/apis.vue'
import Datas from '@/view/superAdmin/authority/components/datas.vue'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const mustUint = (rule, value, callback) => {
if (!/^[0-9]*[1-9][0-9]*$/.test(value)) {
return callback(new Error('请输入正整数'))
}
return callback()
}
const AuthorityOption = ref([
{
authorityId: 0,
authorityName: '根角色'
}
])
const drawer = ref(false)
const dialogType = ref('add')
const activeRow = ref({})
const dialogTitle = ref('新增角色')
const dialogFormVisible = ref(false)
const apiDialogFlag = ref(false)
const copyForm = ref({})
const form = ref({
authorityId: 0,
authorityName: '',
parentId: 0
})
const rules = ref({
authorityId: [
{ required: true, message: '请输入角色ID', trigger: 'blur' },
{ validator: mustUint, trigger: 'blur', message: '必须为正整数' }
],
authorityName: [
{ required: true, message: '请输入角色名', trigger: 'blur' }
],
parentId: [
{ required: true, message: '请选择父角色', trigger: 'blur' },
]
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(999)
const tableData = ref([])
const searchInfo = ref({})
// 查询
const getTableData = async() => {
const table = await getAuthorityList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const changeRow = (key, value) => {
activeRow.value[key] = value
}
const menus = ref(null)
const apis = ref(null)
const datas = ref(null)
const autoEnter = (activeName, oldActiveName) => {
const paneArr = [menus, apis, datas]
if (oldActiveName) {
if (paneArr[oldActiveName].value.needConfirm) {
paneArr[oldActiveName].value.enterAndNext()
paneArr[oldActiveName].value.needConfirm = false
}
}
}
// 拷贝角色
const copyAuthorityFunc = (row) => {
setOptions()
dialogTitle.value = '拷贝角色'
dialogType.value = 'copy'
for (const k in form.value) {
form.value[k] = row[k]
}
copyForm.value = row
dialogFormVisible.value = true
}
const opdendrawer = (row) => {
drawer.value = true
activeRow.value = row
}
// 删除角色
const deleteAuth = (row) => {
ElMessageBox.confirm('此操作将永久删除该角色, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async() => {
const res = await deleteAuthority({ authorityId: row.authorityId })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '已取消删除'
})
})
}
// 初始化表单
const authorityForm = ref(null)
const initForm = () => {
if (authorityForm.value) {
authorityForm.value.resetFields()
}
form.value = {
authorityId: 0,
authorityName: '',
parentId: 0
}
}
// 关闭窗口
const closeDialog = () => {
initForm()
dialogFormVisible.value = false
apiDialogFlag.value = false
}
// 确定弹窗
const enterDialog = () => {
form.value.authorityId = Number(form.value.authorityId)
if (form.value.authorityId === 0) {
ElMessage({
type: 'error',
message: '角色id不能为0'
})
return false
}
authorityForm.value.validate(async valid => {
if (valid) {
switch (dialogType.value) {
case 'add':
{
const res = await createAuthority(form.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '添加成功!'
})
getTableData()
closeDialog()
}
}
break
case 'edit':
{
const res = await updateAuthority(form.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '添加成功!'
})
getTableData()
closeDialog()
}
}
break
case 'copy': {
const data = {
authority: {
authorityId: 0,
authorityName: '',
datauthorityId: [],
parentId: 0
},
oldAuthorityId: 0
}
data.authority.authorityId = form.value.authorityId
data.authority.authorityName = form.value.authorityName
data.authority.parentId = form.value.parentId
data.authority.dataAuthorityId = copyForm.value.dataAuthorityId
data.oldAuthorityId = copyForm.value.authorityId
const res = await copyAuthority(data)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '复制成功!'
})
getTableData()
}
}
}
initForm()
dialogFormVisible.value = false
}
})
}
const setOptions = () => {
AuthorityOption.value = [
{
authorityId: 0,
authorityName: '根角色'
}
]
setAuthorityOptions(tableData.value, AuthorityOption.value, false)
}
const setAuthorityOptions = (AuthorityData, optionsData, disabled) => {
form.value.authorityId = String(form.value.authorityId)
AuthorityData &&
AuthorityData.forEach(item => {
if (item.children && item.children.length) {
const option = {
authorityId: item.authorityId,
authorityName: item.authorityName,
disabled: disabled || item.authorityId === form.value.authorityId,
children: []
}
setAuthorityOptions(
item.children,
option.children,
disabled || item.authorityId === form.value.authorityId
)
optionsData.push(option)
} else {
const option = {
authorityId: item.authorityId,
authorityName: item.authorityName,
disabled: disabled || item.authorityId === form.value.authorityId
}
optionsData.push(option)
}
})
}
// 增加角色
const addAuthority = (parentId) => {
initForm()
dialogTitle.value = '新增角色'
dialogType.value = 'add'
form.value.parentId = parentId
setOptions()
dialogFormVisible.value = true
}
// 编辑角色
const editAuthority = (row) => {
setOptions()
dialogTitle.value = '编辑角色'
dialogType.value = 'edit'
for (const key in form.value) {
form.value[key] = row[key]
}
setOptions()
dialogFormVisible.value = true
}
</script>
<script>
export default {
name: 'Authority'
}
</script>
<style lang="scss">
.authority {
.el-input-number {
margin-left: 15px;
span {
display: none;
}
}
}
.tree-content{
overflow: auto;
height: calc(100vh - 100px);
margin-top: 10px;
}
.auth-drawer{
.el-drawer__body{
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div>
<div class="clearfix sticky-button">
<el-input v-model="filterText" class="fitler" placeholder="筛选" />
<el-button class="fl-right" size="small" type="primary" @click="authApiEnter"> </el-button>
</div>
<div class="tree-content">
<el-tree
ref="apiTree"
:data="apiTreeData"
:default-checked-keys="apiTreeIds"
:props="apiDefaultProps"
default-expand-all
highlight-current
node-key="onlyId"
show-checkbox
:filter-node-method="filterNode"
@check="nodeChange"
/>
</div>
</div>
</template>
<script>
export default {
name: 'Apis',
}
</script>
<script setup>
import { getAllApis } from '@/api/api'
import { UpdateCasbin, getPolicyPathByAuthorityId } from '@/api/casbin'
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
row: {
default: function() {
return {}
},
type: Object
}
})
const apiDefaultProps = ref({
children: 'children',
label: 'description'
})
const filterText = ref('')
const apiTreeData = ref([])
const apiTreeIds = ref([])
const activeUserId = ref('')
const init = async() => {
const res2 = await getAllApis()
const apis = res2.data.apis
apiTreeData.value = buildApiTree(apis)
const res = await getPolicyPathByAuthorityId({
authorityId: props.row.authorityId
})
activeUserId.value = props.row.authorityId
apiTreeIds.value = []
res.data.paths && res.data.paths.forEach(item => {
apiTreeIds.value.push('p:' + item.path + 'm:' + item.method)
})
}
init()
const needConfirm = ref(false)
const nodeChange = () => {
needConfirm.value = true
}
// 暴露给外层使用的切换拦截统一方法
const enterAndNext = () => {
authApiEnter()
}
// 创建api树方法
const buildApiTree = (apis) => {
const apiObj = {}
apis &&
apis.forEach(item => {
item.onlyId = 'p:' + item.path + 'm:' + item.method
if (Object.prototype.hasOwnProperty.call(apiObj, item.apiGroup)) {
apiObj[item.apiGroup].push(item)
} else {
Object.assign(apiObj, { [item.apiGroup]: [item] })
}
})
const apiTree = []
for (const key in apiObj) {
const treeNode = {
ID: key,
description: key + '组',
children: apiObj[key]
}
apiTree.push(treeNode)
}
return apiTree
}
// 关联关系确定
const apiTree = ref(null)
const authApiEnter = async() => {
const checkArr = apiTree.value.getCheckedNodes(true)
var casbinInfos = []
checkArr && checkArr.forEach(item => {
var casbinInfo = {
path: item.path,
method: item.method
}
casbinInfos.push(casbinInfo)
})
const res = await UpdateCasbin({
authorityId: activeUserId.value,
casbinInfos
})
if (res.code === 0) {
ElMessage({ type: 'success', message: 'api设置成功' })
}
}
defineExpose({
needConfirm,
enterAndNext
})
const filterNode = (value, data) => {
if (!value) return true
return data.description.indexOf(value) !== -1
}
watch(filterText, (val) => {
apiTree.value.filter(val)
})
</script>
<style lang="scss" scoped>
@import "@/style/button.scss";
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div>
<div class="clearfix sticky-button" style="margin: 18px">
<el-button class="fl-right" size="small" type="primary" @click="authDataEnter"> </el-button>
<el-button class="fl-left" size="small" type="primary" @click="all">全选</el-button>
<el-button class="fl-left" size="small" type="primary" @click="self">本角色</el-button>
<el-button class="fl-left" size="small" type="primary" @click="selfAndChildren">本角色及子角色</el-button>
</div>
<div class="tree-content">
<el-checkbox-group v-model="dataAuthorityId" @change="selectAuthority">
<el-checkbox v-for="(item,key) in authoritys" :key="key" :label="item">{{ item.authorityName }}</el-checkbox>
</el-checkbox-group>
</div>
<warning-bar title="此功能仅用于创建角色和角色的many2many关系表具体使用还须自己结合表实现业务详情参考示例代码客户示例" />
</div>
</template>
<script>
export default {
name: 'Datas'
}
</script>
<script setup>
import { setDataAuthority } from '@/api/authority'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
row: {
default: function() {
return {}
},
type: Object
},
authority: {
default: function() {
return []
},
type: Array
}
})
const authoritys = ref([])
const needConfirm = ref(false)
// 平铺角色
const roundAuthority = (authoritysData) => {
authoritysData && authoritysData.forEach(item => {
const obj = {}
obj.authorityId = item.authorityId
obj.authorityName = item.authorityName
authoritys.value.push(obj)
if (item.children && item.children.length) {
roundAuthority(item.children)
}
})
}
const dataAuthorityId = ref([])
const init = () => {
roundAuthority(props.authority)
props.row.dataAuthorityId && props.row.dataAuthorityId.forEach(item => {
const obj = authoritys.value && authoritys.value.filter(au => au.authorityId === item.authorityId) && authoritys.value.filter(au => au.authorityId === item.authorityId)[0]
dataAuthorityId.value.push(obj)
})
}
init()
// 暴露给外层使用的切换拦截统一方法
const enterAndNext = () => {
authDataEnter()
}
const emit = defineEmits(['changeRow'])
const all = () => {
dataAuthorityId.value = [...authoritys.value]
emit('changeRow', 'dataAuthorityId', dataAuthorityId.value)
needConfirm.value = true
}
const self = () => {
dataAuthorityId.value = authoritys.value.filter(item => item.authorityId === props.row.authorityId)
emit('changeRow', 'dataAuthorityId', dataAuthorityId.value)
needConfirm.value = true
}
const selfAndChildren = () => {
const arrBox = []
getChildrenId(props.row, arrBox)
dataAuthorityId.value = authoritys.value.filter(item => arrBox.indexOf(item.authorityId) > -1)
emit('changeRow', 'dataAuthorityId', dataAuthorityId.value)
needConfirm.value = true
}
const getChildrenId = (row, arrBox) => {
arrBox.push(row.authorityId)
row.children && row.children.forEach(item => {
getChildrenId(item, arrBox)
})
}
// 提交
const authDataEnter = async() => {
const res = await setDataAuthority(props.row)
if (res.code === 0) {
ElMessage({ type: 'success', message: '资源设置成功' })
}
}
// 选择
const selectAuthority = () => {
emit('changeRow', 'dataAuthorityId', dataAuthorityId.value)
needConfirm.value = true
}
defineExpose({
enterAndNext,
needConfirm
})
</script>

View File

@@ -0,0 +1,225 @@
<template>
<div>
<div class="clearfix sticky-button">
<el-input v-model="filterText" class="fitler" placeholder="筛选" />
<el-button class="fl-right" size="small" type="primary" @click="relation"> </el-button>
</div>
<div class="tree-content">
<el-tree
ref="menuTree"
:data="menuTreeData"
:default-checked-keys="menuTreeIds"
:props="menuDefaultProps"
default-expand-all
highlight-current
node-key="ID"
show-checkbox
:filter-node-method="filterNode"
@check="nodeChange"
>
<template #default="{ node , data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span>
<el-button
type="primary"
link
size="small"
:style="{color:row.defaultRouter === data.name?'#E6A23C':'#85ce61'}"
:disabled="!node.checked"
@click="() => setDefault(data)"
>
{{ row.defaultRouter === data.name?"首页":"设为首页" }}
</el-button>
</span>
<span v-if="data.menuBtn.length">
<el-button
type="primary"
link
size="small"
@click="() => OpenBtn(data)"
>
分配按钮
</el-button>
</span>
</span>
</template>
</el-tree>
</div>
<el-dialog v-model="btnVisible" title="分配按钮" destroy-on-close>
<el-table
ref="btnTableRef"
:data="btnData"
row-key="ID"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="按钮名称" prop="name" />
<el-table-column label="按钮备注" prop="desc" />
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { getBaseMenuTree, getMenuAuthority, addMenuAuthority } from '@/api/menu'
import {
updateAuthority
} from '@/api/authority'
import { getAuthorityBtnApi, setAuthorityBtnApi } from '@/api/authorityBtn'
import { nextTick, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
const props = defineProps({
row: {
default: function() {
return {}
},
type: Object
}
})
const emit = defineEmits(['changeRow'])
const filterText = ref('')
const menuTreeData = ref([])
const menuTreeIds = ref([])
const needConfirm = ref(false)
const menuDefaultProps = ref({
children: 'children',
label: function(data) {
return data.meta.title
}
})
const init = async() => {
// 获取所有菜单树
const res = await getBaseMenuTree()
menuTreeData.value = res.data.menus
const res1 = await getMenuAuthority({ authorityId: props.row.authorityId })
const menus = res1.data.menus
const arr = []
menus.forEach(item => {
// 防止直接选中父级造成全选
if (!menus.some(same => same.parentId === item.menuId)) {
arr.push(Number(item.menuId))
}
})
menuTreeIds.value = arr
}
init()
const setDefault = async(data) => {
const res = await updateAuthority({ authorityId: props.row.authorityId, AuthorityName: props.row.authorityName, parentId: props.row.parentId, defaultRouter: data.name })
if (res.code === 0) {
ElMessage({ type: 'success', message: '设置成功' })
emit('changeRow', 'defaultRouter', res.data.authority.defaultRouter)
}
}
const nodeChange = () => {
needConfirm.value = true
}
// 暴露给外层使用的切换拦截统一方法
const enterAndNext = () => {
relation()
}
// 关联树 确认方法
const menuTree = ref(null)
const relation = async() => {
const checkArr = menuTree.value.getCheckedNodes(false, true)
const res = await addMenuAuthority({
menus: checkArr,
authorityId: props.row.authorityId
})
if (res.code === 0) {
ElMessage({
type: 'success',
message: '菜单设置成功!'
})
}
}
defineExpose({ enterAndNext, needConfirm })
const btnVisible = ref(false)
const btnData = ref([])
const multipleSelection = ref([])
const btnTableRef = ref()
let menuID = ''
const OpenBtn = async(data) => {
menuID = data.ID
const res = await getAuthorityBtnApi({ menuID: menuID, authorityId: props.row.authorityId })
if (res.code === 0) {
openDialog(data)
await nextTick()
if (res.data.selected) {
res.data.selected.forEach(id => {
btnData.value.some(item => {
if (item.ID === id) {
btnTableRef.value.toggleRowSelection(item, true)
}
})
})
}
}
}
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
const openDialog = (data) => {
btnVisible.value = true
btnData.value = data.menuBtn
}
const closeDialog = () => {
btnVisible.value = false
}
const enterDialog = async() => {
const selected = multipleSelection.value.map(item => item.ID)
const res = await setAuthorityBtnApi({
menuID,
selected,
authorityId: props.row.authorityId
})
if (res.code === 0) {
ElMessage({ type: 'success', message: '设置成功' })
btnVisible.value = false
}
}
const filterNode = (value, data) => {
if (!value) return true
// console.log(data.mate.title)
return data.meta.title.indexOf(value) !== -1
}
watch(filterText, (val) => {
menuTree.value.filter(val)
})
</script>
<script>
export default {
name: 'Menus'
}
</script>
<style lang="scss" scope>
@import "@/style/button.scss";
.custom-tree-node{
span+span{
margin-left: 12px;
}
}
</style>

View File

@@ -0,0 +1,375 @@
<template>
<div>
<warning-bar
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
/>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo">
<el-form-item label="字典名(中)">
<el-input v-model="searchInfo.name" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="字典名(英)">
<el-input v-model="searchInfo.type" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="searchInfo.status" clear placeholder="请选择">
<el-option key="true" label="是" value="true" />
<el-option key="false" label="否" value="false" />
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="searchInfo.desc" placeholder="搜索条件" />
</el-form-item>
<el-form-item>
<el-button
size="small"
type="primary"
icon="search"
@click="onSubmit"
>查询</el-button>
<el-button
size="small"
icon="refresh"
@click="onReset"
>重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button
size="small"
type="primary"
icon="plus"
@click="openDialog"
>新增</el-button>
</div>
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
tooltip-effect="dark"
row-key="ID"
>
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="日期" width="180">
<template #default="scope">{{
formatDate(scope.row.CreatedAt)
}}</template>
</el-table-column>
<el-table-column
align="left"
label="字典名(中)"
prop="name"
width="160"
/>
<el-table-column
align="left"
label="字典名(英)"
prop="type"
width="120"
/>
<el-table-column align="left" label="状态" prop="status" width="120">
<template #default="scope">{{
formatBoolean(scope.row.status)
}}</template>
</el-table-column>
<el-table-column align="left" label="描述" prop="desc" width="280" />
<el-table-column align="left" label="按钮组">
<template #default="scope">
<el-button
size="small"
icon="document"
type="primary"
link
@click="toDetail(scope.row)"
>详情</el-button>
<el-button
size="small"
icon="edit"
type="primary"
link
@click="updateSysDictionaryFunc(scope.row)"
>变更</el-button>
<el-popover
v-model="scope.row.visible"
placement="top"
width="160"
>
<p>确定要删除吗</p>
<div style="text-align: right; margin-top: 8px">
<el-button
size="small"
type="primary"
link
@click="scope.row.visible = false"
>取消</el-button>
<el-button
type="primary"
size="small"
@click="deleteSysDictionaryFunc(scope.row)"
>确定</el-button>
</div>
<template #reference>
<el-button
type="primary"
link
icon="delete"
size="small"
style="margin-left: 10px"
@click="scope.row.visible = true"
>删除</el-button>
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog
v-model="dialogFormVisible"
:before-close="closeDialog"
title="弹窗操作"
>
<el-form
ref="dialogForm"
:model="formData"
:rules="rules"
size="medium"
label-width="110px"
>
<el-form-item label="字典名(中)" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入字典名(中)"
clearable
:style="{ width: '100%' }"
/>
</el-form-item>
<el-form-item label="字典名(英)" prop="type">
<el-input
v-model="formData.type"
placeholder="请输入字典名(英)"
clearable
:style="{ width: '100%' }"
/>
</el-form-item>
<el-form-item label="状态" prop="status" required>
<el-switch
v-model="formData.status"
active-text="开启"
inactive-text="停用"
/>
</el-form-item>
<el-form-item label="描述" prop="desc">
<el-input
v-model="formData.desc"
placeholder="请输入描述"
clearable
:style="{ width: '100%' }"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button
size="small"
type="primary"
@click="enterDialog"
> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'SysDictionary',
}
</script>
<script setup>
import {
createSysDictionary,
deleteSysDictionary,
updateSysDictionary,
findSysDictionary,
getSysDictionaryList,
} from '@/api/sysDictionary' // 此处请自行替换地址
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { formatBoolean, formatDate } from '@/utils/format'
const router = useRouter()
const formData = ref({
name: null,
type: null,
status: true,
desc: null,
})
const rules = ref({
name: [
{
required: true,
message: '请输入字典名(中)',
trigger: 'blur',
},
],
type: [
{
required: true,
message: '请输入字典名(英)',
trigger: 'blur',
},
],
desc: [
{
required: true,
message: '请输入描述',
trigger: 'blur',
},
],
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
const onReset = () => {
searchInfo.value = {}
}
// 条件搜索前端看此方法
const onSubmit = () => {
page.value = 1
pageSize.value = 10
if (searchInfo.value.status === '') {
searchInfo.value.status = null
}
getTableData()
}
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getSysDictionaryList({
page: page.value,
pageSize: pageSize.value,
...searchInfo.value,
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const toDetail = (row) => {
router.push({
name: 'dictionaryDetail',
params: {
id: row.ID,
},
})
}
const dialogFormVisible = ref(false)
const type = ref('')
const updateSysDictionaryFunc = async(row) => {
const res = await findSysDictionary({ ID: row.ID, status: row.status })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data.resysDictionary
dialogFormVisible.value = true
}
}
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
name: null,
type: null,
status: true,
desc: null,
}
}
const deleteSysDictionaryFunc = async(row) => {
row.visible = false
const res = await deleteSysDictionary({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功',
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
const dialogForm = ref(null)
const enterDialog = async() => {
dialogForm.value.validate(async(valid) => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createSysDictionary(formData.value)
break
case 'update':
res = await updateSysDictionary(formData.value)
break
default:
res = await createSysDictionary(formData.value)
break
}
if (res.code === 0) {
ElMessage.success('操作成功')
closeDialog()
getTableData()
}
})
}
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
</script>
<style></style>

View File

@@ -0,0 +1,291 @@
<template>
<div>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo">
<el-form-item label="展示值">
<el-input v-model="searchInfo.label" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="字典值">
<el-input v-model="searchInfo.value" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="启用状态" prop="status">
<el-select v-model="searchInfo.status" placeholder="请选择">
<el-option key="true" label="是" value="true" />
<el-option key="false" label="否" value="false" />
</el-select>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button size="small" icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="openDialog">新增字典项</el-button>
</div>
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
tooltip-effect="dark"
row-key="ID"
>
<el-table-column type="selection" width="55" />
<el-table-column align="left" label="日期" width="180">
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
</el-table-column>
<el-table-column align="left" label="展示值" prop="label" width="120" />
<el-table-column align="left" label="字典值" prop="value" width="120" />
<el-table-column align="left" label="启用状态" prop="status" width="120">
<template #default="scope">{{ formatBoolean(scope.row.status) }}</template>
</el-table-column>
<el-table-column align="left" label="排序标记" prop="sort" width="120" />
<el-table-column align="left" label="按钮组">
<template #default="scope">
<el-button size="small" type="primary" link icon="edit" @click="updateSysDictionaryDetailFunc(scope.row)">变更</el-button>
<el-popover v-model="scope.row.visible" placement="top" width="160">
<p>确定要删除吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
<el-button type="primary" size="small" @click="deleteSysDictionaryDetailFunc(scope.row)">确定</el-button>
</div>
<template #reference>
<el-button type="primary" link icon="delete" size="small" @click="scope.row.visible = true">删除</el-button>
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog v-model="dialogFormVisible" :before-close="closeDialog" title="弹窗操作">
<el-form ref="dialogForm" :model="formData" :rules="rules" size="medium" label-width="110px">
<el-form-item label="展示值" prop="label">
<el-input
v-model="formData.label"
placeholder="请输入展示值"
clearable
:style="{width: '100%'}"
/>
</el-form-item>
<el-form-item label="字典值" prop="value">
<el-input-number
v-model.number="formData.value"
step-strictly
:step="1"
placeholder="请输入字典值"
clearable
:style="{width: '100%'}"
/>
</el-form-item>
<el-form-item label="启用状态" prop="status" required>
<el-switch v-model="formData.status" active-text="开启" inactive-text="停用" />
</el-form-item>
<el-form-item label="排序标记" prop="sort">
<el-input-number v-model.number="formData.sort" placeholder="排序标记" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'SysDictionaryDetail'
}
</script>
<script setup>
import {
createSysDictionaryDetail,
deleteSysDictionaryDetail,
updateSysDictionaryDetail,
findSysDictionaryDetail,
getSysDictionaryDetailList
} from '@/api/sysDictionaryDetail' // 此处请自行替换地址
import { ref } from 'vue'
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
import { ElMessage } from 'element-plus'
import { formatBoolean, formatDate } from '@/utils/format'
const route = useRoute()
onBeforeRouteUpdate((to, form) => {
if (to.name === 'dictionaryDetail') {
searchInfo.value.sysDictionaryID = to.params.id
getTableData()
}
})
const formData = ref({
label: null,
value: null,
status: true,
sort: null
})
const rules = ref({
label: [
{
required: true,
message: '请输入展示值',
trigger: 'blur'
}
],
value: [
{
required: true,
message: '请输入字典值',
trigger: 'blur'
}
],
sort: [
{
required: true,
message: '排序标记',
trigger: 'blur'
}
]
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({ sysDictionaryID: Number(route.params.id) })
const onReset = () => {
searchInfo.value = { sysDictionaryID: Number(route.params.id) }
}
// 条件搜索前端看此方法
const onSubmit = () => {
page.value = 1
pageSize.value = 10
if (searchInfo.value.status === '') {
searchInfo.value.status = null
}
getTableData()
}
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getSysDictionaryDetailList({
page: page.value,
pageSize: pageSize.value,
...searchInfo.value,
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const type = ref('')
const dialogFormVisible = ref(false)
const updateSysDictionaryDetailFunc = async(row) => {
const res = await findSysDictionaryDetail({ ID: row.ID })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data.reSysDictionaryDetail
dialogFormVisible.value = true
}
}
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
label: null,
value: null,
status: true,
sort: null,
sysDictionaryID: ''
}
}
const deleteSysDictionaryDetailFunc = async(row) => {
row.visible = false
const res = await deleteSysDictionaryDetail({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
const dialogForm = ref(null)
const enterDialog = async() => {
formData.value.sysDictionaryID = Number(route.params.id)
dialogForm.value.validate(async valid => {
if (!valid) return
let res
switch (type.value) {
case 'create':
res = await createSysDictionaryDetail(formData.value)
break
case 'update':
res = await updateSysDictionaryDetail(formData.value)
break
default:
res = await createSysDictionaryDetail(formData.value)
break
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: '创建/更改成功'
})
closeDialog()
getTableData()
}
})
}
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
</script>
<style>
</style>

View File

@@ -0,0 +1,21 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script>
export default {
name: 'SuperAdmin'
}
</script>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,529 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="addMenu('0')">新增根菜单</el-button>
</div>
<!-- 由于此处菜单跟左侧列表一一对应所以不需要分页 pageSize默认999 -->
<el-table :data="tableData" row-key="ID">
<el-table-column align="left" label="ID" min-width="100" prop="ID" />
<el-table-column align="left" label="展示名称" min-width="120" prop="authorityName">
<template #default="scope">
<span>{{ scope.row.meta.title }}</span>
</template>
</el-table-column>
<el-table-column align="left" label="图标" min-width="140" prop="authorityName">
<template #default="scope">
<div v-if="scope.row.meta.icon" class="icon-column">
<el-icon>
<component :is="scope.row.meta.icon" />
</el-icon>
<span>{{ scope.row.meta.icon }}</span>
</div>
</template>
</el-table-column>
<el-table-column align="left" label="路由Name" show-overflow-tooltip min-width="160" prop="name" />
<el-table-column align="left" label="路由Path" show-overflow-tooltip min-width="160" prop="path" />
<el-table-column align="left" label="是否隐藏" min-width="100" prop="hidden">
<template #default="scope">
<span>{{ scope.row.hidden?"隐藏":"显示" }}</span>
</template>
</el-table-column>
<el-table-column align="left" label="父节点" min-width="90" prop="parentId" />
<el-table-column align="left" label="排序" min-width="70" prop="sort" />
<el-table-column align="left" label="文件路径" min-width="360" prop="component" />
<el-table-column align="left" fixed="right" label="操作" width="300">
<template #default="scope">
<el-button
size="small"
type="primary"
link
icon="plus"
@click="addMenu(scope.row.ID)"
>添加子菜单</el-button>
<el-button
size="small"
type="primary"
link
icon="edit"
@click="editMenu(scope.row.ID)"
>编辑</el-button>
<el-button
size="small"
type="primary"
link
icon="delete"
@click="deleteMenu(scope.row.ID)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dialogFormVisible" :before-close="handleClose" :title="dialogTitle">
<warning-bar title="新增菜单,需要在角色管理内配置权限才可使用" />
<el-form
v-if="dialogFormVisible"
ref="menuForm"
:inline="true"
:model="form"
:rules="rules"
label-position="top"
label-width="85px"
>
<el-form-item label="路由Name" prop="path" style="width:30%">
<el-input
v-model="form.name"
autocomplete="off"
placeholder="唯一英文字符串"
@change="changeName"
/>
</el-form-item>
<el-form-item prop="path" style="width:30%">
<template #label>
<div style="display:inline-flex">
路由Path
<el-checkbox v-model="checkFlag" style="float:right;margin-left:20px;">添加参数</el-checkbox>
</div>
</template>
<el-input
v-model="form.path"
:disabled="!checkFlag"
autocomplete="off"
placeholder="建议只在后方拼接参数"
/>
</el-form-item>
<el-form-item label="是否隐藏" style="width:30%">
<el-select v-model="form.hidden" placeholder="是否在列表隐藏">
<el-option :value="false" label="否" />
<el-option :value="true" label="是" />
</el-select>
</el-form-item>
<el-form-item label="父节点ID" style="width:30%">
<el-cascader
v-model="form.parentId"
style="width:100%"
:disabled="!isEdit"
:options="menuOption"
:props="{ checkStrictly: true,label:'title',value:'ID',disabled:'disabled',emitPath:false}"
:show-all-levels="false"
filterable
/>
</el-form-item>
<el-form-item label="文件路径" prop="component" style="width:60%">
<el-input v-model="form.component" autocomplete="off" placeholder="页面:view/xxx/xx.vue 插件:plugin/xx/xx.vue" @blur="fmtComponent" />
<span style="font-size:12px;margin-right:12px;">如果菜单包含子菜单请创建router-view二级路由页面或者</span><el-button style="margin-top:4px" size="small" @click="form.component = 'view/routerHolder.vue'">点我设置</el-button>
</el-form-item>
<el-form-item label="展示名称" prop="meta.title" style="width:30%">
<el-input v-model="form.meta.title" autocomplete="off" />
</el-form-item>
<el-form-item label="图标" prop="meta.icon" style="width:30%">
<icon :meta="form.meta" style="width:100%" />
</el-form-item>
<el-form-item label="排序标记" prop="sort" style="width:30%">
<el-input v-model.number="form.sort" autocomplete="off" />
</el-form-item>
<el-form-item prop="meta.activeName" style="width:30%">
<template #label>
<div>
<span> 高亮菜单 </span>
<el-tooltip content="注当到达此路由时候指定左侧菜单指定name会处于活跃状态亮起可为空为空则为本路由Name。" placement="top" effect="light">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model="form.meta.activeName" :placeholder="form.name" autocomplete="off" />
</el-form-item>
<el-form-item label="KeepAlive" prop="meta.keepAlive" style="width:30%">
<el-select v-model="form.meta.keepAlive" style="width:100%" placeholder="是否keepAlive缓存页面">
<el-option :value="false" label="否" />
<el-option :value="true" label="是" />
</el-select>
</el-form-item>
<el-form-item label="CloseTab" prop="meta.closeTab" style="width:30%">
<el-select v-model="form.meta.closeTab" style="width:100%" placeholder="是否自动关闭tab">
<el-option :value="false" label="否" />
<el-option :value="true" label="是" />
</el-select>
</el-form-item>
<el-form-item style="width:30%">
<template #label>
<div>
<span> 是否为基础页面 </span>
<el-tooltip content="此项选择为是,则不会展示左侧菜单以及顶部信息。" placement="top" effect="light">
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</template>
<el-select v-model="form.meta.defaultMenu" style="width:100%" placeholder="是否为基础页面">
<el-option :value="false" label="否" />
<el-option :value="true" label="是" />
</el-select>
</el-form-item>
</el-form>
<div>
<el-button
size="small"
type="primary"
icon="edit"
@click="addParameter(form)"
>新增菜单参数</el-button>
<el-table :data="form.parameters" style="width: 100%">
<el-table-column align="left" prop="type" label="参数类型" width="180">
<template #default="scope">
<el-select v-model="scope.row.type" placeholder="请选择">
<el-option key="query" value="query" label="query" />
<el-option key="params" value="params" label="params" />
</el-select>
</template>
</el-table-column>
<el-table-column align="left" prop="key" label="参数key" width="180">
<template #default="scope">
<div>
<el-input v-model="scope.row.key" />
</div>
</template>
</el-table-column>
<el-table-column align="left" prop="value" label="参数值">
<template #default="scope">
<div>
<el-input v-model="scope.row.value" />
</div>
</template>
</el-table-column>
<el-table-column align="left">
<template #default="scope">
<div>
<el-button
type="danger"
size="small"
icon="delete"
@click="deleteParameter(form.parameters,scope.$index)"
>删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-button
style="margin-top:12px"
size="small"
type="primary"
icon="edit"
@click="addBtn(form)"
>新增可控按钮</el-button>
<el-table :data="form.menuBtn" style="width: 100%">
<el-table-column align="left" prop="name" label="按钮名称" width="180">
<template #default="scope">
<div>
<el-input v-model="scope.row.name" />
</div>
</template>
</el-table-column>
<el-table-column align="left" prop="name" label="备注" width="180">
<template #default="scope">
<div>
<el-input v-model="scope.row.desc" />
</div>
</template>
</el-table-column>
<el-table-column align="left">
<template #default="scope">
<div>
<el-button
type="danger"
size="small"
icon="delete"
@click="deleteBtn(form.menuBtn,scope.$index)"
>删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {
updateBaseMenu,
getMenuList,
addBaseMenu,
deleteBaseMenu,
getBaseMenuById
} from '@/api/menu'
import icon from '@/view/superAdmin/menu/icon.vue'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { canRemoveAuthorityBtnApi } from '@/api/authorityBtn'
import { reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const rules = reactive({
path: [{ required: true, message: '请输入菜单name', trigger: 'blur' }],
component: [
{ required: true, message: '请输入文件路径', trigger: 'blur' }
],
'meta.title': [
{ required: true, message: '请输入菜单展示名称', trigger: 'blur' }
]
})
const page = ref(1)
const total = ref(0)
const pageSize = ref(999)
const tableData = ref([])
const searchInfo = ref({})
// 查询
const getTableData = async() => {
const table = await getMenuList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// 新增参数
const addParameter = (form) => {
if (!form.parameters) {
form.parameters = []
}
form.parameters.push({
type: 'query',
key: '',
value: ''
})
}
const fmtComponent = () => {
form.value.component = form.value.component.replace(/\\/g, '/')
}
// 删除参数
const deleteParameter = (parameters, index) => {
parameters.splice(index, 1)
}
// 新增可控按钮
const addBtn = (form) => {
if (!form.menuBtn) {
form.menuBtn = []
}
form.menuBtn.push({
name: '',
desc: '',
})
}
// 删除可控按钮
const deleteBtn = async(btns, index) => {
const btn = btns[index]
if (btn.ID === 0) {
btns.splice(index, 1)
return
}
const res = await canRemoveAuthorityBtnApi({ id: btn.ID })
if (res.code === 0) {
btns.splice(index, 1)
return
}
}
const form = ref({
ID: 0,
path: '',
name: '',
hidden: false,
parentId: '',
component: '',
meta: {
activeName: '',
title: '',
icon: '',
defaultMenu: false,
closeTab: false,
keepAlive: false
},
parameters: [],
menuBtn: []
})
const changeName = () => {
form.value.path = form.value.name
}
const handleClose = (done) => {
initForm()
done()
}
// 删除菜单
const deleteMenu = (ID) => {
ElMessageBox.confirm('此操作将永久删除所有角色下该菜单, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async() => {
const res = await deleteBaseMenu({ ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
.catch(() => {
ElMessage({
type: 'info',
message: '已取消删除'
})
})
}
// 初始化弹窗内表格方法
const menuForm = ref(null)
const checkFlag = ref(false)
const initForm = () => {
checkFlag.value = false
menuForm.value.resetFields()
form.value = {
ID: 0,
path: '',
name: '',
hidden: false,
parentId: '',
component: '',
meta: {
title: '',
icon: '',
defaultMenu: false,
closeTab: false,
keepAlive: false
}
}
}
// 关闭弹窗
const dialogFormVisible = ref(false)
const closeDialog = () => {
initForm()
dialogFormVisible.value = false
}
// 添加menu
const enterDialog = async() => {
menuForm.value.validate(async valid => {
if (valid) {
let res
if (isEdit.value) {
res = await updateBaseMenu(form.value)
} else {
res = await addBaseMenu(form.value)
}
if (res.code === 0) {
ElMessage({
type: 'success',
message: isEdit.value ? '编辑成功' : '添加成功!'
})
getTableData()
}
initForm()
dialogFormVisible.value = false
}
})
}
const menuOption = ref([
{
ID: '0',
title: '根菜单'
}
])
const setOptions = () => {
menuOption.value = [
{
ID: '0',
title: '根目录'
}
]
setMenuOptions(tableData.value, menuOption.value, false)
}
const setMenuOptions = (menuData, optionsData, disabled) => {
menuData &&
menuData.forEach(item => {
if (item.children && item.children.length) {
const option = {
title: item.meta.title,
ID: String(item.ID),
disabled: disabled || item.ID === form.value.ID,
children: []
}
setMenuOptions(
item.children,
option.children,
disabled || item.ID === form.value.ID
)
optionsData.push(option)
} else {
const option = {
title: item.meta.title,
ID: String(item.ID),
disabled: disabled || item.ID === form.value.ID
}
optionsData.push(option)
}
})
}
// 添加菜单方法id为 0则为添加根菜单
const isEdit = ref(false)
const dialogTitle = ref('新增菜单')
const addMenu = (id) => {
dialogTitle.value = '新增菜单'
form.value.parentId = String(id)
isEdit.value = false
setOptions()
dialogFormVisible.value = true
}
// 修改菜单方法
const editMenu = async(id) => {
dialogTitle.value = '编辑菜单'
const res = await getBaseMenuById({ id })
form.value = res.data.menu
isEdit.value = true
setOptions()
dialogFormVisible.value = true
}
</script>
<script>
export default {
name: 'Menus',
}
</script>
<style scoped lang="scss">
.warning {
color: #dc143c;
}
.icon-column{
display: flex;
align-items: center;
.el-icon{
margin-right: 8px;
}
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div>
<div class="gva-search-box">
<el-form :inline="true" :model="searchInfo">
<el-form-item label="请求方法">
<el-input v-model="searchInfo.method" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="请求路径">
<el-input v-model="searchInfo.path" placeholder="搜索条件" />
</el-form-item>
<el-form-item label="结果状态码">
<el-input v-model="searchInfo.status" placeholder="搜索条件" />
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" icon="search" @click="onSubmit">查询</el-button>
<el-button size="small" icon="refresh" @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-popover v-model="deleteVisible" placement="top" width="160">
<p>确定要删除吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="deleteVisible = false">取消</el-button>
<el-button size="small" type="primary" @click="onDelete">确定</el-button>
</div>
<template #reference>
<el-button icon="delete" size="small" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="deleteVisible = true">删除</el-button>
</template>
</el-popover>
</div>
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
tooltip-effect="dark"
row-key="ID"
@selection-change="handleSelectionChange"
>
<el-table-column align="left" type="selection" width="55" />
<el-table-column align="left" label="操作人" width="140">
<template #default="scope">
<div>{{ scope.row.user.userName }}({{ scope.row.user.nickName }})</div>
</template>
</el-table-column>
<el-table-column align="left" label="日期" width="180">
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
</el-table-column>
<el-table-column align="left" label="状态码" prop="status" width="120">
<template #default="scope">
<div>
<el-tag type="success">{{ scope.row.status }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column align="left" label="请求IP" prop="ip" width="120" />
<el-table-column align="left" label="请求方法" prop="method" width="120" />
<el-table-column align="left" label="请求路径" prop="path" width="240" />
<el-table-column align="left" label="请求" prop="path" width="80">
<template #default="scope">
<div>
<el-popover v-if="scope.row.body" placement="left-start" trigger="click">
<div class="popover-box">
<pre>{{ fmtBody(scope.row.body) }}</pre>
</div>
<template #reference>
<el-icon style="cursor: pointer;"><warning /></el-icon>
</template>
</el-popover>
<span v-else></span>
</div>
</template>
</el-table-column>
<el-table-column align="left" label="响应" prop="path" width="80">
<template #default="scope">
<div>
<el-popover v-if="scope.row.resp" placement="left-start" trigger="click">
<div class="popover-box">
<pre>{{ fmtBody(scope.row.resp) }}</pre>
</div>
<template #reference>
<el-icon style="cursor: pointer;"><warning /></el-icon>
</template>
</el-popover>
<span v-else></span>
</div>
</template>
</el-table-column>
<el-table-column align="left" label="按钮组">
<template #default="scope">
<el-popover v-model="scope.row.visible" placement="top" width="160">
<p>确定要删除吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
<el-button size="small" type="primary" @click="deleteSysOperationRecordFunc(scope.row)">确定</el-button>
</div>
<template #reference>
<el-button icon="delete" size="small" type="primary" link @click="scope.row.visible = true">删除</el-button>
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</template>
<script setup>
import {
deleteSysOperationRecord,
getSysOperationRecordList,
deleteSysOperationRecordByIds
} from '@/api/sysOperationRecord' // 此处请自行替换地址
import { formatDate } from '@/utils/format'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
const searchInfo = ref({})
const onReset = () => {
searchInfo.value = {}
}
// 条件搜索前端看此方法
const onSubmit = () => {
page.value = 1
pageSize.value = 10
if (searchInfo.value.status === '') {
searchInfo.value.status = null
}
getTableData()
}
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getSysOperationRecordList({
page: page.value,
pageSize: pageSize.value,
...searchInfo.value,
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const deleteVisible = ref(false)
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
const onDelete = async() => {
const ids = []
multipleSelection.value &&
multipleSelection.value.forEach(item => {
ids.push(item.ID)
})
const res = await deleteSysOperationRecordByIds({ ids })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === ids.length && page.value > 1) {
page.value--
}
deleteVisible.value = false
getTableData()
}
}
const deleteSysOperationRecordFunc = async(row) => {
row.visible = false
const res = await deleteSysOperationRecord({ ID: row.ID })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
const fmtBody = (value) => {
try {
return JSON.parse(value)
} catch (err) {
return value
}
}
</script>
<script>
export default {
name: 'SysOperationRecord'
}
</script>
<style lang="scss">
.table-expand {
padding-left: 60px;
font-size: 0;
label {
width: 90px;
color: #99a9bf;
.el-form-item {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
}
}
.popover-box {
background: #112435;
color: #f08047;
height: 600px;
width: 420px;
overflow: auto;
}
.popover-box::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
</style>

View File

@@ -0,0 +1,446 @@
<template>
<div>
<warning-bar title="注:右上角头像下拉可切换角色" />
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="addUser">新增用户</el-button>
</div>
<el-table
:data="tableData"
row-key="ID"
>
<el-table-column align="left" label="头像" min-width="75">
<template #default="scope">
<CustomPic style="margin-top:8px" :pic-src="scope.row.headerImg" />
</template>
</el-table-column>
<el-table-column align="left" label="ID" min-width="50" prop="ID" />
<el-table-column align="left" label="用户名" min-width="150" prop="userName" />
<el-table-column align="left" label="昵称" min-width="150" prop="nickName" />
<el-table-column align="left" label="手机号" min-width="180" prop="phone" />
<el-table-column align="left" label="邮箱" min-width="180" prop="email" />
<el-table-column align="left" label="用户角色" min-width="200">
<template #default="scope">
<el-cascader
v-model="scope.row.authorityIds"
:options="authOptions"
:show-all-levels="false"
collapse-tags
:props="{ multiple:true,checkStrictly: true,label:'authorityName',value:'authorityId',disabled:'disabled',emitPath:false}"
:clearable="false"
@visible-change="(flag)=>{changeAuthority(scope.row,flag,0)}"
@remove-tag="(removeAuth)=>{changeAuthority(scope.row,false,removeAuth)}"
/>
</template>
</el-table-column>
<el-table-column align="left" label="启用" min-width="150">
<template #default="scope">
<el-switch
v-model="scope.row.enable"
inline-prompt
:active-value="1"
:inactive-value="2"
@change="()=>{switchEnable(scope.row)}"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="250" fixed="right">
<template #default="scope">
<el-popover v-model="scope.row.visible" placement="top" width="160">
<p>确定要删除此用户吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
<el-button type="primary" size="small" @click="deleteUserFunc(scope.row)">确定</el-button>
</div>
<template #reference>
<el-button type="primary" link icon="delete" size="small">删除</el-button>
</template>
</el-popover>
<el-button type="primary" link icon="edit" size="small" @click="openEdit(scope.row)">编辑</el-button>
<el-button type="primary" link icon="magic-stick" size="small" @click="resetPasswordFunc(scope.row)">重置密码</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-dialog
v-model="addUserDialog"
custom-class="user-dialog"
title="用户"
:show-close="false"
:close-on-press-escape="false"
:close-on-click-modal="false"
>
<div style="height:60vh;overflow:auto;padding:0 12px;">
<el-form ref="userForm" :rules="rules" :model="userInfo" label-width="80px">
<el-form-item v-if="dialogFlag === 'add'" label="用户名" prop="userName">
<el-input v-model="userInfo.userName" />
</el-form-item>
<el-form-item v-if="dialogFlag === 'add'" label="密码" prop="password">
<el-input v-model="userInfo.password" />
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input v-model="userInfo.nickName" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="userInfo.phone" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="userInfo.email" />
</el-form-item>
<el-form-item label="用户角色" prop="authorityId">
<el-cascader
v-model="userInfo.authorityIds"
style="width:100%"
:options="authOptions"
:show-all-levels="false"
:props="{ multiple:true,checkStrictly: true,label:'authorityName',value:'authorityId',disabled:'disabled',emitPath:false}"
:clearable="false"
/>
</el-form-item>
<el-form-item label="启用" prop="disabled">
<el-switch
v-model="userInfo.enable"
inline-prompt
:active-value="1"
:inactive-value="2"
/>
</el-form-item>
<el-form-item label="头像" label-width="80px">
<div style="display:inline-block" @click="openHeaderChange">
<img v-if="userInfo.headerImg" class="header-img-box" :src="(userInfo.headerImg && userInfo.headerImg.slice(0, 4) !== 'http')?path+userInfo.headerImg:userInfo.headerImg">
<div v-else class="header-img-box">从媒体库选择</div>
</div>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeAddUserDialog">取 消</el-button>
<el-button size="small" type="primary" @click="enterAddUserDialog">确 定</el-button>
</div>
</template>
</el-dialog>
<ChooseImg ref="chooseImg" :target="userInfo" :target-key="`headerImg`" />
</div>
</template>
<script>
export default {
name: 'User',
}
</script>
<script setup>
import {
getUserList,
setUserAuthorities,
register,
deleteUser
} from '@/api/user'
import { getAuthorityList } from '@/api/authority'
import CustomPic from '@/components/customPic/index.vue'
import ChooseImg from '@/components/chooseImg/index.vue'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { setUserInfo, resetPassword } from '@/api/user.js'
import { nextTick, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const path = ref(import.meta.env.VITE_BASE_API + '/')
// 初始化相关
const setAuthorityOptions = (AuthorityData, optionsData) => {
AuthorityData &&
AuthorityData.forEach(item => {
if (item.children && item.children.length) {
const option = {
authorityId: item.authorityId,
authorityName: item.authorityName,
children: []
}
setAuthorityOptions(item.children, option.children)
optionsData.push(option)
} else {
const option = {
authorityId: item.authorityId,
authorityName: item.authorityName
}
optionsData.push(option)
}
})
}
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getUserList({ page: page.value, pageSize: pageSize.value })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
watch(() => tableData.value, () => {
setAuthorityIds()
})
const initPage = async() => {
getTableData()
const res = await getAuthorityList({ page: 1, pageSize: 999 })
setOptions(res.data.list)
}
initPage()
const resetPasswordFunc = (row) => {
ElMessageBox.confirm(
'是否将此用户密码重置为123456?',
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async() => {
const res = await resetPassword({
ID: row.ID,
})
if (res.code === 0) {
ElMessage({
type: 'success',
message: res.msg,
})
} else {
ElMessage({
type: 'error',
message: res.msg,
})
}
})
}
const setAuthorityIds = () => {
tableData.value && tableData.value.forEach((user) => {
const authorityIds = user.authorities && user.authorities.map(i => {
return i.authorityId
})
user.authorityIds = authorityIds
})
}
const chooseImg = ref(null)
const openHeaderChange = () => {
chooseImg.value.open()
}
const authOptions = ref([])
const setOptions = (authData) => {
authOptions.value = []
setAuthorityOptions(authData, authOptions.value)
}
const deleteUserFunc = async(row) => {
const res = await deleteUser({ id: row.ID })
if (res.code === 0) {
ElMessage.success('删除成功')
row.visible = false
await getTableData()
}
}
// 弹窗相关
const userInfo = ref({
username: '',
password: '',
nickName: '',
headerImg: '',
authorityId: '',
authorityIds: [],
enable: 1,
})
const rules = ref({
userName: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 5, message: '最低5位字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入用户密码', trigger: 'blur' },
{ min: 6, message: '最低6位字符', trigger: 'blur' }
],
nickName: [
{ required: true, message: '请输入用户昵称', trigger: 'blur' }
],
phone: [
{ pattern: /^1([38][0-9]|4[014-9]|[59][0-35-9]|6[2567]|7[0-8])\d{8}$/, message: "请输入合法手机号", trigger: "blur" },
],
email: [
{ pattern: /^([0-9A-Za-z\-_\.]+)@([0-9a-z]+\.[a-z]{2,3}(\.[a-z]{2})?)$/g, message: "请输入正确的邮箱", trigger: "blur" },
],
authorityId: [
{ required: true, message: '请选择用户角色', trigger: 'blur' }
]
})
const userForm = ref(null)
const enterAddUserDialog = async() => {
userInfo.value.authorityId = userInfo.value.authorityIds[0]
userForm.value.validate(async valid => {
if (valid) {
const req = {
...userInfo.value
}
if (dialogFlag.value === 'add') {
const res = await register(req)
if (res.code === 0) {
ElMessage({ type: 'success', message: '创建成功' })
await getTableData()
closeAddUserDialog()
}
}
if (dialogFlag.value === 'edit') {
const res = await setUserInfo(req)
if (res.code === 0) {
ElMessage({ type: 'success', message: '编辑成功' })
await getTableData()
closeAddUserDialog()
}
}
}
})
}
const addUserDialog = ref(false)
const closeAddUserDialog = () => {
userForm.value.resetFields()
userInfo.value.headerImg = ''
userInfo.value.authorityIds = []
addUserDialog.value = false
}
const dialogFlag = ref('add')
const addUser = () => {
dialogFlag.value = 'add'
addUserDialog.value = true
}
const tempAuth = {}
const changeAuthority = async(row, flag, removeAuth) => {
if (flag) {
if (!removeAuth) {
tempAuth[row.ID] = [...row.authorityIds]
}
return
}
await nextTick()
const res = await setUserAuthorities({
ID: row.ID,
authorityIds: row.authorityIds
})
if (res.code === 0) {
ElMessage({ type: 'success', message: '角色设置成功' })
} else {
if (!removeAuth) {
row.authorityIds = [...tempAuth[row.ID]]
delete tempAuth[row.ID]
} else {
row.authorityIds = [removeAuth, ...row.authorityIds]
}
}
}
const openEdit = (row) => {
dialogFlag.value = 'edit'
userInfo.value = JSON.parse(JSON.stringify(row))
addUserDialog.value = true
}
const switchEnable = async(row) => {
userInfo.value = JSON.parse(JSON.stringify(row))
await nextTick()
const req = {
...userInfo.value
}
const res = await setUserInfo(req)
if (res.code === 0) {
ElMessage({ type: 'success', message: `${req.enable === 2 ? '禁用' : '启用'}成功` })
await getTableData()
userInfo.value.headerImg = ''
userInfo.value.authorityIds = []
}
}
</script>
<style lang="scss">
.user-dialog {
.header-img-box {
width: 200px;
height: 200px;
border: 1px dashed #ccc;
border-radius: 20px;
text-align: center;
line-height: 200px;
cursor: pointer;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
border: 1px dashed #d9d9d9 !important;
border-radius: 6px;
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
}
.nickName{
display: flex;
justify-content: flex-start;
align-items: center;
}
.pointer{
cursor: pointer;
font-size: 16px;
margin-left: 2px;
}
</style>

183
src/view/system/state.vue Normal file
View File

@@ -0,0 +1,183 @@
<template>
<div>
<el-row :gutter="15" class="system_state">
<el-col :span="12">
<el-card v-if="state.os" class="card_item">
<template #header>
<div>Runtime</div>
</template>
<div>
<el-row :gutter="10">
<el-col :span="12">os:</el-col>
<el-col :span="12" v-text="state.os.goos" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">cpu nums:</el-col>
<el-col :span="12" v-text="state.os.numCpu" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">compiler:</el-col>
<el-col :span="12" v-text="state.os.compiler" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">go version:</el-col>
<el-col :span="12" v-text="state.os.goVersion" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">goroutine nums:</el-col>
<el-col :span="12" v-text="state.os.numGoroutine" />
</el-row>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card v-if="state.disk" class="card_item">
<template #header>
<div>Disk</div>
</template>
<div>
<el-row :gutter="10">
<el-col :span="12">
<el-row :gutter="10">
<el-col :span="12">total (MB)</el-col>
<el-col :span="12" v-text="state.disk.totalMb" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">used (MB)</el-col>
<el-col :span="12" v-text="state.disk.usedMb" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">total (GB)</el-col>
<el-col :span="12" v-text="state.disk.totalGb" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">used (GB)</el-col>
<el-col :span="12" v-text="state.disk.usedGb" />
</el-row>
</el-col>
<el-col :span="12">
<el-progress
type="dashboard"
:percentage="state.disk.usedPercent"
:color="colors"
/>
</el-col>
</el-row>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="15" class="system_state">
<el-col :span="12">
<el-card
v-if="state.cpu"
class="card_item"
:body-style="{ height: '180px', 'overflow-y': 'scroll' }"
>
<template #header>
<div>CPU</div>
</template>
<div>
<el-row :gutter="10">
<el-col :span="12">physical number of cores:</el-col>
<el-col :span="12" v-text="state.cpu.cores" />
</el-row>
<el-row v-for="(item, index) in state.cpu.cpus" :key="index" :gutter="10">
<el-col :span="12">core {{ index }}:</el-col>
<el-col
:span="12"
><el-progress
type="line"
:percentage="+item.toFixed(0)"
:color="colors"
/></el-col>
</el-row>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card v-if="state.ram" class="card_item">
<template #header>
<div>Ram</div>
</template>
<div>
<el-row :gutter="10">
<el-col :span="12">
<el-row :gutter="10">
<el-col :span="12">total (MB)</el-col>
<el-col :span="12" v-text="state.ram.totalMb" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">used (MB)</el-col>
<el-col :span="12" v-text="state.ram.usedMb" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">total (GB)</el-col>
<el-col :span="12" v-text="state.ram.totalMb / 1024" />
</el-row>
<el-row :gutter="10">
<el-col :span="12">used (GB)</el-col>
<el-col
:span="12"
v-text="(state.ram.usedMb / 1024).toFixed(2)"
/>
</el-row>
</el-col>
<el-col :span="12">
<el-progress
type="dashboard"
:percentage="state.ram.usedPercent"
:color="colors"
/>
</el-col>
</el-row>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { getSystemState } from '@/api/system'
import { onUnmounted, ref } from 'vue'
const timer = ref(null)
const state = ref({})
const colors = ref([
{ color: '#5cb87a', percentage: 20 },
{ color: '#e6a23c', percentage: 40 },
{ color: '#f56c6c', percentage: 80 }
])
const reload = async() => {
const { data } = await getSystemState()
state.value = data.server
}
reload()
timer.value = setInterval(() => {
reload()
}, 1000 * 10)
onUnmounted(() => {
clearInterval(timer.value)
timer.value = null
})
</script>
<script>
export default {
name: 'State',
}
</script>
<style>
.system_state {
padding: 10px;
}
.card_item {
height: 280px;
}
</style>

View File

@@ -0,0 +1,234 @@
<template>
<div>
<warning-bar title="id , created_at , updated_at , deleted_at 会自动生成请勿重复创建。搜索时如果条件为LIKE只支持字符串" />
<el-form
ref="fieldDialogFrom"
:model="middleDate"
label-width="120px"
label-position="right"
:rules="rules"
class="grid-form"
>
<el-form-item label="Field名称" prop="fieldName">
<el-input v-model="middleDate.fieldName" autocomplete="off" style="width:80%" />
<el-button size="small" style="width:18%;margin-left:2%" @click="autoFill">
<span style="font-size: 12px">自动填充</span>
</el-button>
</el-form-item>
<el-form-item label="Field中文名" prop="fieldDesc">
<el-input v-model="middleDate.fieldDesc" autocomplete="off" />
</el-form-item>
<el-form-item label="FieldJSON" prop="fieldJson">
<el-input v-model="middleDate.fieldJson" autocomplete="off" />
</el-form-item>
<el-form-item label="数据库字段名" prop="columnName">
<el-input v-model="middleDate.columnName" autocomplete="off" />
</el-form-item>
<el-form-item label="数据库字段描述" prop="comment">
<el-input v-model="middleDate.comment" autocomplete="off" />
</el-form-item>
<el-form-item label="Field数据类型" prop="fieldType">
<el-select
v-model="middleDate.fieldType"
style="width:100%"
placeholder="请选择field数据类型"
clearable
@change="clearOther"
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="middleDate.fieldType === 'enum' ? '枚举值' : '类型长度'" prop="dataTypeLong">
<el-input v-model="middleDate.dataTypeLong" :placeholder="middleDate.fieldType === 'enum'?`例:'北京','天津'`:'数据库类型长度'" />
</el-form-item>
<el-form-item label="Field查询条件" prop="fieldSearchType">
<el-select
v-model="middleDate.fieldSearchType"
style="width:100%"
placeholder="请选择Field查询条件"
clearable
>
<el-option
v-for="item in typeSearchOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="
(middleDate.fieldType!=='string'&&item.value==='LIKE')||
((middleDate.fieldType!=='int'&&middleDate.fieldType!=='time.Time'&&middleDate.fieldType!=='float64')&&(item.value==='BETWEEN' || item.value==='NOT BETWEEN'))
"
/>
</el-select>
</el-form-item>
<el-form-item label="关联字典" prop="dictType">
<el-select
v-model="middleDate.dictType"
style="width:100%"
:disabled="middleDate.fieldType!=='int'"
placeholder="请选择字典"
clearable
>
<el-option
v-for="item in dictOptions"
:key="item.type"
:label="`${item.type}(${item.name})`"
:value="item.type"
/>
</el-select>
</el-form-item>
<el-form-item label="是否排序">
<el-switch v-model="middleDate.sort" />
</el-form-item>
<el-form-item label="是否必填">
<el-switch v-model="middleDate.require" />
</el-form-item>
<el-form-item label="是否可清空">
<el-switch v-model="middleDate.clearable" />
</el-form-item>
<el-form-item label="校验失败文案">
<el-input v-model="middleDate.errorText" />
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { toLowerCase, toSQLLine } from '@/utils/stringFun'
import { getSysDictionaryList } from '@/api/sysDictionary'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ref } from 'vue'
const props = defineProps({
dialogMiddle: {
type: Object,
default: function() {
return {}
}
}
})
const middleDate = ref({})
const dictOptions = ref([])
const typeSearchOptions = ref([
{
label: '=',
value: '='
},
{
label: '<>',
value: '<>'
},
{
label: '>',
value: '>'
},
{
label: '<',
value: '<'
},
{
label: 'LIKE',
value: 'LIKE'
},
{
label: 'BETWEEN',
value: 'BETWEEN'
},
{
label: 'NOT BETWEEN',
value: 'NOT BETWEEN'
}
])
const typeOptions = ref([
{
label: '字符串',
value: 'string'
},
{
label: '整型',
value: 'int'
},
{
label: '布尔值',
value: 'bool'
},
{
label: '浮点型',
value: 'float64'
},
{
label: '时间',
value: 'time.Time'
},
{
label: '枚举',
value: 'enum'
}
])
const rules = ref({
fieldName: [
{ required: true, message: '请输入field英文名', trigger: 'blur' }
],
fieldDesc: [
{ required: true, message: '请输入field中文名', trigger: 'blur' }
],
fieldJson: [
{ required: true, message: '请输入field格式化json', trigger: 'blur' }
],
columnName: [
{ required: true, message: '请输入数据库字段', trigger: 'blur' }
],
fieldType: [
{ required: true, message: '请选择field数据类型', trigger: 'blur' }
]
})
const init = async() => {
middleDate.value = props.dialogMiddle
const dictRes = await getSysDictionaryList({
page: 1,
pageSize: 999999
})
dictOptions.value = dictRes.data.list
}
init()
const autoFill = () => {
middleDate.value.fieldJson = toLowerCase(middleDate.value.fieldName)
middleDate.value.columnName = toSQLLine(middleDate.value.fieldJson)
}
const clearOther = () => {
middleDate.value.fieldSearchType = ''
middleDate.value.dictType = ''
}
const fieldDialogFrom = ref(null)
defineExpose({ fieldDialogFrom })
</script>
<script>
export default {
name: 'FieldDialog'
}
</script>
<style scoped>
.grid-form{
display: grid;
grid-template-columns: 1fr 1fr;
}
.click-text{
color: #0d84ff;
font-size: 13px;
cursor: pointer;
user-select: none;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div class="previewCode">
<el-tabs v-model="activeName">
<el-tab-pane v-for="(item, key) in previewCode" :key="key" :label="key" :name="key">
<div :id="key" class="tab-info" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import marked from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/atelier-plateau-light.css'
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue'
const props = defineProps({
previewCode: {
type: Object,
default() {
return {}
}
}
})
const activeName = ref('')
onMounted(() => {
marked.setOptions({
renderer: new marked.Renderer(),
highlight: function(code) {
return hljs.highlightAuto(code).value
},
pedantic: false,
gfm: true,
tables: true,
breaks: false,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false
})
for (const key in props.previewCode) {
if (activeName.value === '') {
activeName.value = key
}
document.getElementById(key).innerHTML = marked(props.previewCode[key])
}
})
const selectText = () => {
const element = document.getElementById(activeName.value)
if (document.body.createTextRange) {
const range = document.body.createTextRange()
range.moveToElementText(element)
range.select()
} else if (window.getSelection) {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(element)
selection.removeAllRanges()
selection.addRange(range)
} else {
alert('none')
}
}
const copy = () => {
selectText()
document.execCommand('copy')
ElMessage.success('复制成功')
}
defineExpose({ copy })
</script>
<script>
export default {
}
</script>
<style lang="scss">
.previewCode {
.tab-info {
height: 50vh;
background: #fff;
padding: 0 20px;
overflow-y: scroll;
}
}
</style>

View File

@@ -0,0 +1,723 @@
<template>
<div>
<warning-bar href="https://www.bilibili.com/video/BV1kv4y1g7nT?p=3" title="此功能为开发环境使用不建议发布到生产具体使用效果请看视频https://www.bilibili.com/video/BV1kv4y1g7nT?p=3" />
<!-- 从数据库直接获取字段 -->
<div class="gva-search-box">
<el-collapse v-model="activeNames" style="margin-bottom:12px">
<el-collapse-item name="1">
<template #title>
<div :style="{fontSize:'16px',paddingLeft:'20px'}">
点这里从现有数据库创建代码
<el-icon class="header-icon ">
<pointer />
</el-icon>
</div>
</template>
<el-form ref="getTableForm" style="margin-top:24px" :inline="true" :model="dbform" label-width="120px">
<el-form-item label="业务库" prop="selectDBtype">
<template #label>
<el-tooltip content="注需要提前到db-list自行配置多数据库如未配置需配置后重启服务方可使用。此处可选择对应库表可理解为从哪个库选择表" placement="bottom" effect="light">
<div> 业务库 <el-icon><QuestionFilled /></el-icon> </div>
</el-tooltip>
</template>
<el-select v-model="dbform.businessDB" clearable style="width:194px" placeholder="选择业务库" @change="getDbFunc">
<el-option
v-for="item in dbList"
:key="item.aliasName"
:value="item.aliasName"
:label="item.aliasName"
:disabled="item.disable"
>
<div>
<span>{{ item.aliasName }}</span>
<span style="float:right;color:#8492a6;font-size:13px">{{ item.dbName }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item label="数据库名" prop="structName">
<el-select v-model="dbform.dbName" clearable filterable placeholder="请选择数据库" @change="getTableFunc">
<el-option
v-for="item in dbOptions"
:key="item.database"
:label="item.database"
:value="item.database"
/>
</el-select>
</el-form-item>
<el-form-item label="表名" prop="structName">
<el-select
v-model="dbform.tableName"
:disabled="!dbform.dbName"
filterable
placeholder="请选择表"
>
<el-option
v-for="item in tableOptions"
:key="item.tableName"
:label="item.tableName"
:value="item.tableName"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button size="small" type="primary" @click="getColumnFunc">使用此表创建</el-button>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
</div>
<div class="gva-search-box">
<!-- 初始版本自动化代码工具 -->
<el-form ref="autoCodeForm" :rules="rules" :model="form" label-width="120px" :inline="true">
<el-form-item label="Struct名称" prop="structName">
<el-input v-model="form.structName" placeholder="首字母自动转换大写" />
</el-form-item>
<el-form-item label="TableName" prop="tableName">
<el-input v-model="form.tableName" placeholder="指定表名(非必填)" />
</el-form-item>
<el-form-item label="Struct简称" prop="abbreviation">
<el-input v-model="form.abbreviation" placeholder="简称会作为入参对象名和路由group" />
</el-form-item>
<el-form-item label="Struct中文名称" prop="description">
<el-input v-model="form.description" placeholder="中文描述作为自动api描述" />
</el-form-item>
<el-form-item label="文件名称" prop="packageName">
<el-input v-model="form.packageName" placeholder="生成文件的默认名称(建议为驼峰格式,首字母小写,如sysXxxXxxx)" @blur="toLowerCaseFunc(form,'packageName')" />
</el-form-item>
<el-form-item label="Package" prop="package">
<el-select v-model="form.package" style="width:194px">
<el-option v-for="item in pkgs" :key="item.ID" :value="item.packageName" :label="item.packageName" />
</el-select>
<el-icon class="auto-icon" @click="getPkgs"><refresh /></el-icon>
<el-icon class="auto-icon" @click="goPkgs"><document-add /></el-icon>
</el-form-item>
<el-form-item label="业务库" prop="businessDB">
<template #label>
<el-tooltip content="注需要提前到db-list自行配置多数据库此项为空则会使用gva本库创建自动化代码(global.GVA_DB),填写后则会创建指定库的代码(global.MustGetGlobalDBByDBName(dbname))" placement="bottom" effect="light">
<div> 业务库 <el-icon><QuestionFilled /></el-icon> </div>
</el-tooltip>
</template>
<el-select
v-model="form.businessDB"
style="width:194px"
placeholder="选择业务库"
>
<el-option
v-for="item in dbList"
:key="item.aliasName"
:value="item.aliasName"
:label="item.aliasName"
:disabled="item.disable"
>
<div>
<span>{{ item.aliasName }}</span>
<span style="float:right;color:#8492a6;font-size:13px">{{ item.dbName }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<template #label>
<el-tooltip content="注:会自动在结构体添加 created_by updated_by deleted_by方便用户进行资源权限控制" placement="bottom" effect="light">
<div> 创建资源标识 <el-icon><QuestionFilled /></el-icon> </div>
</el-tooltip>
</template>
<el-checkbox v-model="form.autoCreateResource" />
</el-form-item>
<el-form-item>
<template #label>
<el-tooltip content="注把自动生成的API注册进数据库" placement="bottom" effect="light">
<div> 自动创建API </div>
</el-tooltip>
</template>
<el-checkbox v-model="form.autoCreateApiToSql" />
</el-form-item>
<el-form-item>
<template #label>
<el-tooltip content="注自动迁移生成的文件到yaml配置的对应位置" placement="bottom" effect="light">
<div> 自动移动文件 </div>
</el-tooltip>
</template>
<el-checkbox v-model="form.autoMoveFile" />
</el-form-item>
</el-form>
</div>
<!-- 组件列表 -->
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" @click="editAndAddField()">新增Field</el-button>
</div>
<el-table :data="form.fields">
<el-table-column align="left" type="index" label="序列" width="60" />
<el-table-column align="left" prop="fieldName" label="Field名" width="160">
<template #default="{row}">
<el-input v-model="row.fieldName" />
</template>
</el-table-column>
<el-table-column align="left" prop="fieldDesc" label="中文名" width="160">
<template #default="{row}">
<el-input v-model="row.fieldDesc" />
</template>
</el-table-column>
<el-table-column align="left" prop="require" label="必填">
<template #default="{row}"> <el-checkbox v-model="row.require" /></template>
</el-table-column>
<el-table-column align="left" prop="sort" label="排序">
<template #default="{row}"> <el-checkbox v-model="row.sort" /> </template>
</el-table-column>
<el-table-column align="left" prop="fieldJson" width="160px" label="FieldJson">
<template #default="{row}">
<el-input v-model="row.fieldJson" />
</template>
</el-table-column>
<el-table-column align="left" prop="fieldType" label="Field数据类型" width="160">
<template #default="{row}">
<el-select
v-model="row.fieldType"
style="width:100%"
placeholder="请选择field数据类型"
clearable
>
<el-option
v-for="item in typeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column align="left" prop="dataTypeLong" label="数据库字段长度" width="160">
<template #default="{row}">
<el-input v-model="row.dataTypeLong" />
</template>
</el-table-column>
<el-table-column align="left" prop="columnName" label="数据库字段" width="160">
<template #default="{row}">
<el-input v-model="row.columnName" />
</template>
</el-table-column>
<el-table-column align="left" prop="comment" label="数据库字段描述" width="160">
<template #default="{row}">
<el-input v-model="row.columnName" />
</template>
</el-table-column>
<el-table-column align="left" prop="fieldSearchType" label="搜索条件" width="130">
<template #default="{row}">
<el-select
v-model="row.fieldSearchType"
style="width:100%"
placeholder="请选择Field查询条件"
clearable
>
<el-option
v-for="item in typeSearchOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="
(row.fieldType!=='string'&&item.value==='LIKE')||
((row.fieldType!=='int'&&row.fieldType!=='time.Time'&&row.fieldType!=='float64')&&(item.value==='BETWEEN' || item.value==='NOT BETWEEN'))
"
/>
</el-select>
</template>
</el-table-column>
<el-table-column align="left" label="操作" width="300" fixed="right">
<template #default="scope">
<el-button
size="small"
type="primary"
link
icon="edit"
@click="editAndAddField(scope.row)"
>高级编辑</el-button>
<el-button
size="small"
type="primary"
link
:disabled="scope.$index === 0"
@click="moveUpField(scope.$index)"
>上移</el-button>
<el-button
size="small"
type="primary"
link
:disabled="(scope.$index + 1) === form.fields.length"
@click="moveDownField(scope.$index)"
>下移</el-button>
<el-popover v-model="scope.row.visible" placement="top">
<p>确定删除吗</p>
<div style="text-align: right; margin-top: 8px;">
<el-button size="small" type="primary" link @click="scope.row.visible = false">取消</el-button>
<el-button type="primary" size="small" @click="deleteField(scope.$index)">确定</el-button>
</div>
<template #reference>
<el-button size="small" type="primary" link icon="delete" @click="scope.row.visible = true">删除</el-button>
</template>
</el-popover>
</template>
</el-table-column>
</el-table>
<!-- 组件列表 -->
<div class="gva-btn-list justify-content-flex-end auto-btn-list">
<el-button size="small" type="primary" @click="enterForm(true)">预览代码</el-button>
<el-button size="small" type="primary" @click="enterForm(false)">生成代码</el-button>
</div>
</div>
<!-- 组件弹窗 -->
<el-dialog v-model="dialogFlag" width="70%" title="组件内容">
<FieldDialog v-if="dialogFlag" ref="fieldDialogNode" :dialog-middle="dialogMiddle" />
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
<el-dialog v-model="previewFlag">
<template #header>
<div class="previewCodeTool">
<p>操作栏</p>
<el-button size="small" type="primary" @click="selectText">全选</el-button>
<el-button size="small" type="primary" @click="copy">复制</el-button>
</div>
</template>
<PreviewCodeDialog v-if="previewFlag" ref="previewNode" :preview-code="preViewCode" />
<template #footer>
<div class="dialog-footer" style="padding-top:14px;padding-right:14px">
<el-button size="small" type="primary" @click="previewFlag = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import FieldDialog from '@/view/systemTools/autoCode/component/fieldDialog.vue'
import PreviewCodeDialog from '@/view/systemTools/autoCode/component/previewCodeDialg.vue'
import { toUpperCase, toHump, toSQLLine, toLowerCase } from '@/utils/stringFun'
import { createTemp, getDB, getTable, getColumn, preview, getMeta, getPackageApi } from '@/api/autoCode'
import { getDict } from '@/utils/dictionary'
import { ref, getCurrentInstance, reactive, watch, toRaw } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import WarningBar from '@/components/warningBar/warningBar.vue'
const typeOptions = ref([
{
label: '字符串',
value: 'string'
},
{
label: '整型',
value: 'int'
},
{
label: '布尔值',
value: 'bool'
},
{
label: '浮点型',
value: 'float64'
},
{
label: '时间',
value: 'time.Time'
},
{
label: '枚举',
value: 'enum'
}
])
const typeSearchOptions = ref([
{
label: '=',
value: '='
},
{
label: '<>',
value: '<>'
},
{
label: '>',
value: '>'
},
{
label: '<',
value: '<'
},
{
label: 'LIKE',
value: 'LIKE'
},
{
label: 'BETWEEN',
value: 'BETWEEN'
},
{
label: 'NOT BETWEEN',
value: 'NOT BETWEEN'
}
])
const fieldTemplate = {
fieldName: '',
fieldDesc: '',
fieldType: '',
dataType: '',
fieldJson: '',
columnName: '',
dataTypeLong: '',
comment: '',
require: false,
sort: false,
errorText: '',
clearable: true,
fieldSearchType: '',
dictType: ''
}
const route = useRoute()
const router = useRouter()
const activeNames = reactive([])
const preViewCode = ref({})
const dbform = ref({
businessDB: '',
dbName: '',
tableName: ''
})
const tableOptions = ref([])
const addFlag = ref('')
const fdMap = ref({})
const form = ref({
structName: '',
tableName: '',
packageName: '',
package: '',
abbreviation: '',
description: '',
businessDB: '',
autoCreateApiToSql: true,
autoMoveFile: true,
autoCreateResource: false,
fields: []
})
const rules = ref({
structName: [
{ required: true, message: '请输入结构体名称', trigger: 'blur' }
],
abbreviation: [
{ required: true, message: '请输入结构体简称', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入结构体描述', trigger: 'blur' }
],
packageName: [
{
required: true,
message: '文件名称sysXxxxXxxx',
trigger: 'blur'
}
],
package: [
{ required: true, message: '请选择package', trigger: 'blur' }
]
})
const dialogMiddle = ref({})
const bk = ref({})
const dialogFlag = ref(false)
const previewFlag = ref(false)
const toLowerCaseFunc = (form, key) => {
form[key] = toLowerCase(form[key])
}
const previewNode = ref(null)
const selectText = () => {
previewNode.value.selectText()
}
const copy = () => {
previewNode.value.copy()
}
const editAndAddField = (item) => {
dialogFlag.value = true
if (item) {
addFlag.value = 'edit'
bk.value = JSON.parse(JSON.stringify(item))
dialogMiddle.value = item
} else {
addFlag.value = 'add'
dialogMiddle.value = JSON.parse(JSON.stringify(fieldTemplate))
}
}
const moveUpField = (index) => {
if (index === 0) {
return
}
const oldUpField = form.value.fields[index - 1]
form.value.fields.splice(index - 1, 1)
form.value.fields.splice(index, 0, oldUpField)
}
const moveDownField = (index) => {
const fCount = form.value.fields.length
if (index === fCount - 1) {
return
}
const oldDownField = form.value.fields[index + 1]
form.value.fields.splice(index + 1, 1)
form.value.fields.splice(index, 0, oldDownField)
}
const currentInstance = getCurrentInstance()
const enterDialog = () => {
currentInstance.refs.fieldDialogNode.fieldDialogFrom.validate(valid => {
if (valid) {
dialogMiddle.value.fieldName = toUpperCase(
dialogMiddle.value.fieldName
)
if (addFlag.value === 'add') {
form.value.fields.push(dialogMiddle.value)
}
dialogFlag.value = false
} else {
return false
}
})
}
const closeDialog = () => {
if (addFlag.value === 'edit') {
dialogMiddle.value = bk.value
}
dialogFlag.value = false
}
const deleteField = (index) => {
form.value.fields.splice(index, 1)
}
const autoCodeForm = ref(null)
const enterForm = async(isPreview) => {
if (form.value.fields.length <= 0) {
ElMessage({
type: 'error',
message: '请填写至少一个field'
})
return false
}
if (
form.value.fields.some(item => item.fieldName === form.value.structName)
) {
ElMessage({
type: 'error',
message: '存在与结构体同名的字段'
})
return false
}
autoCodeForm.value.validate(async valid => {
if (valid) {
for (const key in form.value) {
if (typeof form.value[key] === 'string') {
form.value[key] = form.value[key].trim()
}
}
form.value.structName = toUpperCase(form.value.structName)
form.value.tableName = form.value.tableName.replace(' ', '')
if (!form.value.tableName) {
form.value.tableName = toSQLLine(toLowerCase(form.value.structName))
}
if (form.value.structName === form.value.abbreviation) {
ElMessage({
type: 'error',
message: 'structName和struct简称不能相同'
})
return false
}
form.value.humpPackageName = toSQLLine(form.value.packageName)
if (isPreview) {
const data = await preview(form.value)
preViewCode.value = data.data.autoCode
previewFlag.value = true
} else {
const data = await createTemp(form.value)
if (data.headers?.success === 'false') {
return
} else {
if (form.value.autoMoveFile) {
ElMessage({
type: 'success',
message: '自动化代码创建成功,自动移动成功'
})
return
}
ElMessage({
type: 'success',
message: '自动化代码创建成功,正在下载'
})
}
const blob = new Blob([data])
const fileName = 'ginvueadmin.zip'
if ('download' in document.createElement('a')) {
// 不是IE浏览器
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // 下载完成移除元素
window.URL.revokeObjectURL(url) // 释放掉blob对象
} else {
// IE 10+
window.navigator.msSaveBlob(blob, fileName)
}
}
} else {
return false
}
})
}
const dbList = ref([])
const dbOptions = ref([])
const getDbFunc = async() => {
dbform.value.dbName = ''
dbform.value.tableName = ''
const res = await getDB({ businessDB: dbform.value.businessDB })
if (res.code === 0) {
dbOptions.value = res.data.dbs
dbList.value = res.data.dbList
}
}
const getTableFunc = async() => {
const res = await getTable({ businessDB: dbform.value.businessDB, dbName: dbform.value.dbName })
if (res.code === 0) {
tableOptions.value = res.data.tables
}
dbform.value.tableName = ''
}
const getColumnFunc = async() => {
const gormModelList = ['id', 'created_at', 'updated_at', 'deleted_at']
const res = await getColumn(dbform.value)
if (res.code === 0) {
let dbtype = ''
if (dbform.value.businessDB !== '') {
const dbtmp = dbList.value.find(item => item.aliasName === dbform.value.businessDB)
console.log(dbtmp)
const dbraw = toRaw(dbtmp)
console.log(dbraw)
dbtype = dbraw.dbtype
}
const tbHump = toHump(dbform.value.tableName)
form.value.structName = toUpperCase(tbHump)
form.value.tableName = dbform.value.tableName
form.value.packageName = tbHump
form.value.abbreviation = tbHump
form.value.description = tbHump + '表'
form.value.autoCreateApiToSql = true
form.value.autoMoveFile = true
form.value.fields = []
res.data.columns &&
res.data.columns.forEach(item => {
if (!gormModelList.some(gormfd => gormfd === item.columnName)) {
const fbHump = toHump(item.columnName)
form.value.fields.push({
fieldName: toUpperCase(fbHump),
fieldDesc: item.columnComment || fbHump + '字段',
fieldType: fdMap.value[item.dataType],
dataType: item.dataType,
fieldJson: fbHump,
dataTypeLong: item.dataTypeLong && item.dataTypeLong.split(',')[0],
columnName: dbtype == 'oracle' ? item.columnName.toUpperCase() : item.columnName,
comment: item.columnComment,
require: false,
errorText: '',
clearable: true,
fieldSearchType: '',
dictType: ''
})
}
})
}
}
const setFdMap = async() => {
const fdTypes = ['string', 'int', 'bool', 'float64', 'time.Time']
fdTypes.forEach(async fdtype => {
const res = await getDict(fdtype)
res && res.forEach(item => {
fdMap.value[item.label] = fdtype
})
})
}
const getAutoCodeJson = async(id) => {
const res = await getMeta({ id: Number(id) })
if (res.code === 0) {
form.value = JSON.parse(res.data.meta)
}
}
const pkgs = ref([])
const getPkgs = async() => {
const res = await getPackageApi()
if (res.code === 0) {
pkgs.value = res.data.pkgs
}
}
const goPkgs = () => {
router.push({ name: 'autoPkg' })
}
const init = () => {
getDbFunc()
setFdMap()
getPkgs()
const id = route.params.id
if (id) {
getAutoCodeJson(id)
}
}
init()
watch(() => route.params.id, (id) => {
if (route.name === 'autoCodeEdit') {
init()
}
})
</script>
<script>
export default {
name: 'AutoCode'
}
</script>
<style scoped lang="scss">
.previewCodeTool {
display: flex;
align-items: center;
padding: 5px 0;
}
.button-box {
padding: 10px 20px;
.el-button {
margin-right: 20px;
float: right;
}
}
.auto-btn-list{
margin-top: 16px;
}
.auto-icon{
margin-left: 6px;
color: #666;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,188 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="goAutoCode(null)">新增</el-button>
</div>
<el-table :data="tableData">
<el-table-column
type="selection"
width="55"
/>
<el-table-column align="left" label="id" width="60" prop="ID" />
<el-table-column align="left" label="日期" width="180">
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
</el-table-column>
<el-table-column align="left" label="结构体名" min-width="150" prop="structName" />
<el-table-column align="left" label="结构体描述" min-width="150" prop="structCNName" />
<el-table-column align="left" label="表名称" min-width="150" prop="tableName" />
<el-table-column align="left" label="回滚标记" min-width="150" prop="flag">
<template #default="scope">
<el-tag
v-if="scope.row.flag"
type="danger"
size="small"
effect="dark"
>
已回滚
</el-tag>
<el-tag
v-else
size="small"
type="success"
effect="dark"
>
未回滚
</el-tag>
</template>
</el-table-column>
<el-table-column align="left" label="操作" min-width="240">
<template #default="scope">
<div>
<el-button size="small" type="primary" link :disabled="scope.row.flag === 1" @click="rollbackFunc(scope.row,true)">回滚(删表)</el-button>
<el-button size="small" type="primary" link :disabled="scope.row.flag === 1" @click="rollbackFunc(scope.row,false)">回滚(不删表)</el-button>
<el-button size="small" type="primary" link @click="goAutoCode(scope.row)">复用</el-button>
<el-button size="small" type="primary" link @click="deleteRow(scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AutoCodeAdmin',
}
</script>
<script setup>
import { getSysHistory, rollback, delSysHistory } from '@/api/autoCode.js'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref } from 'vue'
import { formatDate } from '@/utils/format'
const router = useRouter()
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
// 分页
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
// 查询
const getTableData = async() => {
const table = await getSysHistory({
page: page.value,
pageSize: pageSize.value
})
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
const deleteRow = async(row) => {
ElMessageBox.confirm('此操作将删除本历史, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const res = await delSysHistory({ id: Number(row.ID) })
if (res.code === 0) {
ElMessage.success('删除成功')
getTableData()
}
})
}
const rollbackFunc = async(row, flag) => {
if (flag) {
ElMessageBox.confirm(`此操作将删除自动创建的文件和api会删除表, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
ElMessageBox.confirm(`此操作将删除自动创建的文件和api会删除表, 请继续确认!!!`, '会删除表', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
ElMessageBox.confirm(`此操作将删除自动创建的文件和api会删除表, 请继续确认!!!`, '会删除表', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const res = await rollback({ id: Number(row.ID), deleteTable: flag })
if (res.code === 0) {
ElMessage.success('回滚成功')
getTableData()
}
})
})
})
} else {
ElMessageBox.confirm(`此操作将删除自动创建的文件和api, 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async() => {
const res = await rollback({ id: Number(row.ID), deleteTable: flag })
if (res.code === 0) {
ElMessage.success('回滚成功')
getTableData()
}
})
}
}
const goAutoCode = (row) => {
if (row) {
router.push({ name: 'autoCodeEdit', params: {
id: row.ID
}})
} else {
router.push({ name: 'autoCode' })
}
}
</script>
<style scoped lang="scss">
.button-box {
padding: 10px 20px;
.el-button {
float: right;
}
}
.el-tag--mini {
margin-left: 5px;
}
.warning {
color: #dc143c;
}
</style>

View File

@@ -0,0 +1,160 @@
<template>
<div>
<warning-bar href="https://www.bilibili.com/video/BV1kv4y1g7nT?p=3" title="此功能为开发环境使用不建议发布到生产具体使用效果请看视频https://www.bilibili.com/video/BV1kv4y1g7nT?p=3" />
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button size="small" type="primary" icon="plus" @click="openDialog('addApi')">新增</el-button>
</div>
<el-table :data="tableData">
<el-table-column align="left" label="id" width="60" prop="ID" />
<el-table-column align="left" label="包名" width="150" prop="packageName" />
<el-table-column align="left" label="展示名" width="150" prop="label" />
<el-table-column align="left" label="描述" min-width="150" prop="desc" />
<el-table-column align="left" label="操作" width="200">
<template #default="scope">
<el-button
icon="delete"
size="small"
type="primary"
link
@click="deleteApiFunc(scope.row)"
>删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog v-model="dialogFormVisible" :before-close="closeDialog" title="创建Package">
<warning-bar title="新增Pkg用于自动化代码使用" />
<el-form ref="pkgForm" :model="form" :rules="rules" label-width="80px">
<el-form-item label="包名" prop="packageName">
<el-input v-model="form.packageName" autocomplete="off" />
</el-form-item>
<el-form-item label="展示名" prop="label">
<el-input v-model="form.label" autocomplete="off" />
</el-form-item>
<el-form-item label="描述" prop="desc">
<el-input v-model="form.desc" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button size="small" @click="closeDialog"> </el-button>
<el-button size="small" type="primary" @click="enterDialog"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'AutoPkg',
}
</script>
<script setup>
import {
createPackageApi,
getPackageApi,
deletePackageApi,
} from '@/api/autoCode'
import { ref } from 'vue'
import WarningBar from '@/components/warningBar/warningBar.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const form = ref({
packageName: '',
label: '',
desc: '',
})
const validateNum = (rule, value, callback) => {
if ((/^\d+$/.test(value[0]))) {
callback(new Error('不能够以数字开头'))
} else {
callback()
}
}
const rules = ref({
packageName: [
{ required: true, message: '请输入包名', trigger: 'blur' },
{ validator: validateNum, trigger: 'blur' }
],
})
const dialogFormVisible = ref(false)
const openDialog = () => {
dialogFormVisible.value = true
}
const closeDialog = () => {
dialogFormVisible.value = false
form.value = {
packageName: '',
label: '',
desc: '',
}
}
const pkgForm = ref(null)
const enterDialog = async() => {
pkgForm.value.validate(async valid => {
if (valid) {
const res = await createPackageApi(form.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '添加成功',
showClose: true
})
}
getTableData()
closeDialog()
}
})
}
const tableData = ref([])
const getTableData = async() => {
const table = await getPackageApi()
if (table.code === 0) {
tableData.value = table.data.pkgs
}
}
const deleteApiFunc = async(row) => {
ElMessageBox.confirm('此操作仅删除数据库中的pkg存储后端相应目录结构请自行删除与数据库保持一致', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async() => {
const res = await deletePackageApi(row)
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功!'
})
getTableData()
}
})
}
getTableData()
</script>
<style scoped lang="scss">
.button-box {
padding: 10px 20px;
.el-button {
float: right;
}
}
.warning {
color: #dc143c;
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div>
<div class="gva-table-box">
<el-form label-width="140px" class="plug-form">
<el-form-item label="插件名">
<el-input v-model="form.plugName" placeholder="必填(英文大写字母开头)" @blur="titleCase" />
</el-form-item>
<el-form-item label="路由组">
<el-input v-model="form.routerGroup" placeholder="将会作为插件路由组使用" />
</el-form-item>
<el-form-item label="使用全局属性">
<el-checkbox v-model="form.hasGlobal" />
</el-form-item>
<el-form-item v-if="form.hasGlobal" label="全局属性">
<div v-for="(i,k) in form.global" :key="k" class="plug-row">
<span>
<el-input v-model="i.key" placeholder="key 必填" />
</span>
<span>
<el-select v-model="i.type" placeholder="type 必填">
<el-option label="string" value="string" />
<el-option label="int" value="int" />
<el-option label="float32" value="float32" />
<el-option label="float64" value="float64" />
<el-option label="bool" value="bool" />
</el-select>
</span>
<span>
<el-input v-model="i.desc" placeholder="备注 必填" />
</span>
<span>
<el-button :icon="Plus" circle @click="addkv(form.global)" />
</span>
<span>
<el-button :icon="Minus" circle @click="minkv(form.global,k)" />
</span>
</div>
</el-form-item>
<el-form-item label="使用Request">
<el-checkbox v-model="form.hasRequest" />
</el-form-item>
<el-form-item v-if="form.hasRequest" label="Request">
<div v-for="(i,k) in form.request" :key="k" class="plug-row">
<span>
<el-input v-model="i.key" placeholder="key 必填" />
</span>
<span>
<el-select v-model="i.type" placeholder="type 必填">
<el-option label="string" value="string" />
<el-option label="int" value="int" />
<el-option label="float32" value="float32" />
<el-option label="float64" value="float64" />
<el-option label="bool" value="bool" />
</el-select>
</span>
<span>
<el-input v-model="i.desc" placeholder="备注 必填" />
</span>
<span>
<el-button :icon="Plus" circle @click="addkv(form.request)" />
</span>
<span>
<el-button :icon="Minus" circle @click="minkv(form.request,k)" />
</span>
</div>
</el-form-item>
<el-form-item label="使用Response">
<el-checkbox v-model="form.hasResponse" />
</el-form-item>
<el-form-item v-if="form.hasResponse" label="Response">
<div v-for="(i,k) in form.response" :key="k" class="plug-row">
<span>
<el-input v-model="i.key" placeholder="key 必填" />
</span>
<span>
<el-select v-model="i.type" placeholder="type 必填">
<el-option label="string" value="string" />
<el-option label="int" value="int" />
<el-option label="float32" value="float32" />
<el-option label="float64" value="float64" />
<el-option label="bool" value="bool" />
</el-select>
</span>
<span>
<el-input v-model="i.desc" placeholder="备注 必填" />
</span>
<span>
<el-button :icon="Plus" circle @click="addkv(form.response)" />
</span>
<span>
<el-button :icon="Minus" circle @click="minkv(form.response,k)" />
</span>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="createPlug">创建</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { toUpperCase } from '@/utils/stringFun'
import {
Plus,
Minus
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { createPlugApi } from '@/api/autoCode.js'
import { reactive } from 'vue'
const form = reactive({
plugName: '',
routerGroup: '',
hasGlobal: true,
hasRequest: true,
hasResponse: true,
global: [{
key: '',
type: '',
desc: '',
}],
request: [{
key: '',
type: '',
desc: '',
}],
response: [{
key: '',
type: '',
desc: '',
}]
})
const titleCase = () => {
form.plugName = toUpperCase(form.plugName)
}
const createPlug = async() => {
if (!form.plugName || !form.routerGroup) {
ElMessage.error('插件名称和插件路由组为必填项')
return
}
if (form.hasGlobal) {
const intercept = form.global.some(i => {
if (!i.key || !i.type) {
return true
}
})
if (intercept) {
ElMessage.error('全局属性的key和type为必填项')
return
}
}
if (form.hasRequest) {
const intercept = form.request.some(i => {
if (!i.key || !i.type) {
return true
}
})
if (intercept) {
ElMessage.error('请求属性的key和type为必填项')
return
}
}
if (form.hasResponse) {
const intercept = form.response.some(i => {
if (!i.key || !i.type) {
return true
}
})
if (intercept) {
ElMessage.error('响应属性的key和type为必填项')
return
}
}
const res = await createPlugApi(form)
if (res.code === 0) {
ElMessageBox('创建成功插件已自动写入后端plugin目录下请按照自己的逻辑进行创造')
}
}
const addkv = (arr) => {
arr.push({
key: '',
value: '',
})
}
const minkv = (arr, key) => {
if (arr.length === 1) {
ElMessage.warning('至少有一个全局属性')
return
}
arr.splice(key, 1)
}
</script>
<style lang="scss" scoped>
.plug-form{
width: 680px;
}
.plug-row{
display: flex;
align-items: center;
width: 100%;
&+&{
margin-top: 12px;
}
&>span{
margin-left: 8px;
}
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<div style="height:80vh">
<iframe width="100%" height="100%" :src="`${basePath}:${basePort}/form-generator/#/`" frameborder="0" />
</div>
</template>
<script>
export default {
name: 'FormGenerator'
}
</script>
<script setup>
import { ref } from 'vue'
const basePath = ref(import.meta.env.VITE_BASE_PATH)
const basePort = ref(import.meta.env.VITE_SERVER_PORT)
</script>

View File

@@ -0,0 +1,21 @@
<template>
<div>
<router-view v-slot="{ Component }">
<transition mode="out-in" name="el-fade-in-linear">
<keep-alive :include="routerStore.keepAliveRouters">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script>
export default {
name: 'System'
}
</script>
<script setup>
import { useRouterStore } from '@/pinia/modules/router'
const routerStore = useRouterStore()
</script>

View File

@@ -0,0 +1,44 @@
<template>
<div>
<el-upload
class="upload-demo"
drag
:action="`${path}/autoCode/installPlugin`"
:headers="{'x-token':userStore.token}"
:show-file-list="false"
:on-success="handleSuccess"
:on-error="handleSuccess"
name="plug"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
拖拽或<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
请把安装包的zip拖拽至此处上传
</div>
</template>
</el-upload>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useUserStore } from '@/pinia/modules/user'
import { ElMessage } from 'element-plus'
const userStore = useUserStore()
const path = ref(import.meta.env.VITE_BASE_API)
const handleSuccess = (res) => {
if (res.code === 0) {
let msg = ``
res.data && res.data.forEach((item, index) => {
msg += `${index + 1}.${item.msg}\n`
})
alert(msg)
} else {
ElMessage.error(res.msg)
}
}
</script>

View File

@@ -0,0 +1,463 @@
<template>
<div class="system">
<el-form ref="form" :model="config" label-width="240px">
<!-- System start -->
<el-collapse v-model="activeNames">
<el-collapse-item title="系统配置" name="1">
<el-form-item label="环境值">
<!-- <el-input v-model="config.system.env" />-->
<el-select v-model="config.system.env" style="width:100%">
<el-option value="public" />
<el-option value="develop" />
</el-select>
</el-form-item>
<el-form-item label="端口值">
<el-input v-model.number="config.system.addr" />
</el-form-item>
<el-form-item label="数据库类型">
<el-select v-model="config.system['db-type']" style="width:100%">
<el-option value="mysql" />
<el-option value="pgsql" />
</el-select>
</el-form-item>
<el-form-item label="Oss类型">
<el-select v-model="config.system['oss-type']" style="width:100%">
<el-option value="local" />
<el-option value="qiniu" />
<el-option value="tencent-cos" />
<el-option value="aliyun-oss" />
<el-option value="huawei-obs" />
</el-select>
</el-form-item>
<el-form-item label="多点登录拦截">
<el-checkbox v-model="config.system['use-multipoint']">开启</el-checkbox>
</el-form-item>
<el-form-item label="开启redis">
<el-checkbox v-model="config.system['use-redis']">开启</el-checkbox>
</el-form-item>
<el-form-item label="限流次数">
<el-input-number v-model.number="config.system['iplimit-count']" />
</el-form-item>
<el-form-item label="限流时间">
<el-input-number v-model.number="config.system['iplimit-time']" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="jwt签名" name="2">
<el-form-item label="jwt签名">
<el-input v-model="config.jwt['signing-key']" />
</el-form-item>
<el-form-item label="有效期">
<el-input v-model="config.jwt['expires-time']" />
</el-form-item>
<el-form-item label="缓冲期">
<el-input v-model="config.jwt['buffer-time']" />
</el-form-item>
<el-form-item label="签发者">
<el-input v-model="config.jwt.issuer" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="Zap日志配置" name="3">
<el-form-item label="级别">
<el-input v-model.number="config.zap.level" />
</el-form-item>
<el-form-item label="输出">
<el-input v-model="config.zap.format" />
</el-form-item>
<el-form-item label="日志前缀">
<el-input v-model="config.zap.prefix" />
</el-form-item>
<el-form-item label="日志文件夹">
<el-input v-model="config.zap.director" />
</el-form-item>
<el-form-item label="编码级">
<el-input v-model="config.zap['encode-level']" />
</el-form-item>
<el-form-item label="栈名">
<el-input v-model="config.zap['stacktrace-key']" />
</el-form-item>
<el-form-item label="日志留存时间(默认以天为单位)">
<el-input v-model.number="config.zap['max-age']" />
</el-form-item>
<el-form-item label="显示行">
<el-checkbox v-model="config.zap['show-line']" />
</el-form-item>
<el-form-item label="输出控制台">
<el-checkbox v-model="config.zap['log-in-console']" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="Redis admin数据库配置" name="4">
<el-form-item label="库">
<el-input v-model.number="config.redis.db" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="config.redis.addr" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="config.redis.password" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="邮箱配置" name="5">
<el-form-item label="接收者邮箱">
<el-input v-model="config.email.to" placeholder="可多个,以逗号分隔" />
</el-form-item>
<el-form-item label="端口">
<el-input v-model.number="config.email.port" />
</el-form-item>
<el-form-item label="发送者邮箱">
<el-input v-model="config.email.from" />
</el-form-item>
<el-form-item label="host">
<el-input v-model="config.email.host" />
</el-form-item>
<el-form-item label="是否为ssl">
<el-checkbox v-model="config.email['is-ssl']" />
</el-form-item>
<el-form-item label="secret">
<el-input v-model="config.email.secret" />
</el-form-item>
<el-form-item label="测试邮件">
<el-button @click="email">测试邮件</el-button>
</el-form-item>
</el-collapse-item>
<el-collapse-item title="验证码配置" name="7">
<el-form-item label="字符长度">
<el-input v-model.number="config.captcha['key-long']" />
</el-form-item>
<el-form-item label="平台宽度">
<el-input v-model.number="config.captcha['img-width']" />
</el-form-item>
<el-form-item label="图片高度">
<el-input v-model.number="config.captcha['img-height']" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="数据库配置" name="9">
<template v-if="config.system['db-type'] === 'mysql'">
<el-form-item label="用户名">
<el-input v-model="config.mysql.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="config.mysql.password" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="config.mysql.path" />
</el-form-item>
<el-form-item label="数据库">
<el-input v-model="config.mysql['db-name']" />
</el-form-item>
<el-form-item label="前缀">
<el-input v-model="config.mysql['refix']" />
</el-form-item>
<el-form-item label="复数表">
<el-switch v-model="config.mysql['singular']" />
</el-form-item>
<el-form-item label="引擎">
<el-input v-model="config.mysql['engine']" />
</el-form-item>
<el-form-item label="maxIdleConns">
<el-input v-model.number="config.mysql['max-idle-conns']" />
</el-form-item>
<el-form-item label="maxOpenConns">
<el-input v-model.number="config.mysql['max-open-conns']" />
</el-form-item>
<el-form-item label="写入日志">
<el-checkbox v-model="config.mysql['log-zap']" />
</el-form-item>
<el-form-item label="日志模式">
<el-input v-model="config.mysql['log-mode']" />
</el-form-item>
</template>
<template v-if="config.system.dbType === 'pgsql'">
<el-form-item label="用户名">
<el-input v-model="config.pgsql.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="config.pgsql.password" />
</el-form-item>
<el-form-item label="地址">
<el-input v-model="config.pgsql.path" />
</el-form-item>
<el-form-item label="数据库">
<el-input v-model="config.pgsql.dbname" />
</el-form-item>
<el-form-item label="前缀">
<el-input v-model="config.pgsql['refix']" />
</el-form-item>
<el-form-item label="复数表">
<el-switch v-model="config.pgsql['singular']" />
</el-form-item>
<el-form-item label="引擎">
<el-input v-model="config.pgsql['engine']" />
</el-form-item>
<el-form-item label="maxIdleConns">
<el-input v-model.number="config.pgsql['max-idle-conns']" />
</el-form-item>
<el-form-item label="maxOpenConns">
<el-input v-model.number="config.pgsql['max-open-conns']" />
</el-form-item>
<el-form-item label="写入日志">
<el-checkbox v-model="config.pgsql['log-zap']" />
</el-form-item>
<el-form-item label="日志模式">
<el-input v-model="config.pgsql['log-mode']" />
</el-form-item>
</template>
</el-collapse-item>
<el-collapse-item title="oss配置" name="10">
<template v-if="config.system['oss-type'] === 'local'">
<h2>本地文件配置</h2>
<el-form-item label="本地文件访问路径">
<el-input v-model="config.local.path" />
</el-form-item>
<el-form-item label="本地文件存储路径">
<el-input v-model="config.local['store-path']" />
</el-form-item>
</template>
<template v-if="config.system['oss-type'] === 'qiniu'">
<h2>qiniu上传配置</h2>
<el-form-item label="存储区域">
<el-input v-model="config.qiniu.zone" />
</el-form-item>
<el-form-item label="空间名称">
<el-input v-model="config.qiniu.bucket" />
</el-form-item>
<el-form-item label="CDN加速域名">
<el-input v-model="config.qiniu['img-path']" />
</el-form-item>
<el-form-item label="是否使用https">
<el-checkbox v-model="config.qiniu['use-https']">开启</el-checkbox>
</el-form-item>
<el-form-item label="accessKey">
<el-input v-model="config.qiniu['access-key']" />
</el-form-item>
<el-form-item label="secretKey">
<el-input v-model="config.qiniu['secret-key']" />
</el-form-item>
<el-form-item label="上传是否使用CDN上传加速">
<el-checkbox v-model="config.qiniu['use-cdn-domains']">开启</el-checkbox>
</el-form-item>
</template>
<template v-if="config.system['oss-type'] === 'tencent-cos'">
<h2>腾讯云COS上传配置</h2>
<el-form-item label="存储桶名称">
<el-input v-model="config['tencent-cos']['bucket']" />
</el-form-item>
<el-form-item label="所属地域">
<el-input v-model="config['tencent-cos'].region" />
</el-form-item>
<el-form-item label="secretID">
<el-input v-model="config['tencent-cos']['secret-id']" />
</el-form-item>
<el-form-item label="secretKey">
<el-input v-model="config['tencent-cos']['secret-key']" />
</el-form-item>
<el-form-item label="路径前缀">
<el-input v-model="config['tencent-cos']['path-prefix']" />
</el-form-item>
<el-form-item label="访问域名">
<el-input v-model="config['tencent-cos']['base-url']" />
</el-form-item>
</template>
<template v-if="config.system['oss-type'] === 'aliyun-oss'">
<h2>阿里云OSS上传配置</h2>
<el-form-item label="区域">
<el-input v-model="config['aliyun-oss'].endpoint" />
</el-form-item>
<el-form-item label="accessKeyId">
<el-input v-model="config['aliyun-oss']['access-key-id']" />
</el-form-item>
<el-form-item label="accessKeySecret">
<el-input v-model="config['aliyun-oss']['access-key-secret']" />
</el-form-item>
<el-form-item label="存储桶名称">
<el-input v-model="config['aliyun-oss']['bucket-name']" />
</el-form-item>
<el-form-item label="访问域名">
<el-input v-model="config['aliyun-oss']['bucket-url']" />
</el-form-item>
</template>
<template v-if="config.system['oss-type'] === 'huawei-obs'">
<h2>华为云Obs上传配置</h2>
<el-form-item label="路径">
<el-input v-model="config['hua-wei-obs'].path" />
</el-form-item>
<el-form-item label="存储桶名称">
<el-input v-model="config['hua-wei-obs'].bucket" />
</el-form-item>
<el-form-item label="区域">
<el-input v-model="config['hua-wei-obs'].endpoint" />
</el-form-item>
<el-form-item label="accessKey">
<el-input v-model="config['hua-wei-obs']['access-key']" />
</el-form-item>
<el-form-item label="secretKey">
<el-input v-model="config['hua-wei-obs']['secret-key']" />
</el-form-item>
</template>
</el-collapse-item>
<el-collapse-item title="Excel上传配置" name="11">
<el-form-item label="合成目标地址">
<el-input v-model="config.excel.dir" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="自动化代码配置" name="12">
<el-form-item label="是否自动重启(linux)">
<el-checkbox v-model="config.autocode['transfer-restart']" />
</el-form-item>
<el-form-item label="root(项目根路径)">
<el-input v-model="config.autocode.root" disabled />
</el-form-item>
<el-form-item label="Server(后端代码地址)">
<el-input v-model="config.autocode['transfer-restart']" />
</el-form-item>
<el-form-item label="SApi(后端api文件夹地址)">
<el-input v-model="config.autocode['server-api']" />
</el-form-item>
<el-form-item label="SInitialize(后端Initialize文件夹)">
<el-input v-model="config.autocode['server-initialize']" />
</el-form-item>
<el-form-item label="SModel(后端Model文件地址)">
<el-input v-model="config.autocode['server-model']" />
</el-form-item>
<el-form-item label="SRequest(后端Request文件夹地址)">
<el-input v-model="config.autocode['server-request']" />
</el-form-item>
<el-form-item label="SRouter(后端Router文件夹地址)">
<el-input v-model="config.autocode['server-router']" />
</el-form-item>
<el-form-item label="SService(后端Service文件夹地址)">
<el-input v-model="config.autocode['server-service']" />
</el-form-item>
<el-form-item label="Web(前端文件夹地址)">
<el-input v-model="config.autocode.web" />
</el-form-item>
<el-form-item label="WApi(后端WApi文件夹地址)">
<el-input v-model="config.autocode['web-api']" />
</el-form-item>
<el-form-item label="WForm(后端WForm文件夹地址)">
<el-input v-model="config.autocode['web-form']" />
</el-form-item>
<el-form-item label="WTable(后端WTable文件夹地址)">
<el-input v-model="config.autocode['web-table']" />
</el-form-item>
</el-collapse-item>
<el-collapse-item title="Timer(定时任务)" name="13">
<el-form-item label="Start是否启用">
<el-checkbox v-model="config.timer['start']" />
</el-form-item>
<el-form-item label="Spec(CRON表达式)">
<el-input v-model="config.timer.spec" />
</el-form-item>
<template v-for="(item,k) in config.timer.detail">
<div v-for="(key,k2) in item" :key="k2">
<el-form-item :key="k+k2" :label="k2">
<el-input v-model="item[k2]" />
</el-form-item>
</div>
</template>
</el-collapse-item>
</el-collapse>
</el-form>
<div class="gva-btn-list">
<el-button type="primary" size="small" @click="update">立即更新</el-button>
<el-button type="primary" size="small" @click="reload">重启服务开发中</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'Config'
}
</script>
<script setup>
import { getSystemConfig, setSystemConfig } from '@/api/system'
import { emailTest } from '@/api/email'
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const activeNames = reactive([])
const config = ref({
system: {
'iplimit-count': 0,
'iplimit-time': 0
},
jwt: {},
mysql: {},
pgsql: {},
excel: {},
autocode: {},
redis: {},
qiniu: {},
'tencent-cos': {},
'aliyun-oss': {},
'hua-wei-obs': {},
captcha: {},
zap: {},
local: {},
email: {},
timer: {
detail: {}
}
})
const initForm = async() => {
const res = await getSystemConfig()
if (res.code === 0) {
config.value = res.data.config
}
}
initForm()
const reload = () => {}
const update = async() => {
const res = await setSystemConfig({ config: config.value })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '配置文件设置成功'
})
await initForm()
}
}
const email = async() => {
const res = await emailTest()
if (res.code === 0) {
ElMessage({
type: 'success',
message: '邮件发送成功'
})
await initForm()
} else {
ElMessage({
type: 'error',
message: '邮件发送失败'
})
}
}
</script>
<style lang="scss">
.system {
background: #fff;
padding:36px;
border-radius: 2px;
h2 {
padding: 10px;
margin: 10px 0;
font-size: 16px;
box-shadow: -4px 0px 0px 0px #e7e8e8;
}
::v-deep(.el-input-number__increase){
top:5px !important;
}
.gva-btn-list{
margin-top:16px;
}
}
</style>