✨ init Project
This commit is contained in:
166
src/view/about/index.vue
Normal file
166
src/view/about/index.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<div class="flex flex-col md:flex-row gap-4">
|
||||
<div class="w-full md:w-1/2">
|
||||
<el-card class="min-w-96">
|
||||
<template #header>
|
||||
<el-divider>gin-vue-admin</el-divider>
|
||||
</template>
|
||||
<div>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
<div class="w-full flex items-center justify-around">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="min-w-96 mt-5">
|
||||
<template #header>
|
||||
<div>flipped-aurora团队</div>
|
||||
</template>
|
||||
<div>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<a href="https://github.com/flipped-aurora">
|
||||
<img
|
||||
class="org-img dom-center"
|
||||
src="@/assets/flipped-aurora.png"
|
||||
alt="flipped-aurora"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4 mt-4">
|
||||
<div v-for="(item, index) in members" :key="index" class="min-h-10 flex items-center">
|
||||
<a :href="item.html_url" class="flex items-center group">
|
||||
<img class="w-8 h-8 rounded-full" :src="item.avatar_url" />
|
||||
<el-link
|
||||
class="text-blue-700 ml-2 text-lg font-bold font-sans break-all"
|
||||
>{{ item.login }}</el-link
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div>提交记录</div>
|
||||
</template>
|
||||
<div class="h-[calc(100vh-300px)] overflow-y-auto">
|
||||
<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>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<el-button class="load-more" type="primary" link @click="loadMore">
|
||||
Load more
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Commits, Members } from '@/api/github'
|
||||
import { formatTimeToStr } from '@/utils/date'
|
||||
const page = ref(0)
|
||||
|
||||
defineOptions({
|
||||
name: 'About'
|
||||
})
|
||||
|
||||
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>
|
||||
.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;
|
||||
}
|
||||
|
||||
.dom-center {
|
||||
margin-left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
</style>
|
||||
411
src/view/bot/bot/bot.vue
Normal file
411
src/view/bot/bot/bot.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" :rules="searchRule" @keyup.enter="onSubmit">
|
||||
<el-form-item label="创建日期" prop="createdAt">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-date-picker v-model="searchInfo.startCreatedAt" type="datetime" placeholder="开始日期" :disabled-date="time=> searchInfo.endCreatedAt ? time.getTime() > searchInfo.endCreatedAt.getTime() : false"></el-date-picker>
|
||||
—
|
||||
<el-date-picker v-model="searchInfo.endCreatedAt" type="datetime" placeholder="结束日期" :disabled-date="time=> searchInfo.startCreatedAt ? time.getTime() < searchInfo.startCreatedAt.getTime() : false"></el-date-picker>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<template v-if="showAllQuery">
|
||||
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
<el-button link type="primary" icon="arrow-down" @click="showAllQuery=true" v-if="!showAllQuery">展开</el-button>
|
||||
<el-button link type="primary" icon="arrow-up" @click="showAllQuery=false" v-else>收起</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog()">新增</el-button>
|
||||
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="onDelete">删除</el-button>
|
||||
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="createdAt"width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="left" label="关键词" prop="keyword" width="120" />
|
||||
<el-table-column label="内容" prop="content" width="200">
|
||||
<template #default="scope">
|
||||
[富文本内容]
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon style="margin-right: 5px"><InfoFilled /></el-icon>查看</el-button>
|
||||
<el-button type="primary" link icon="edit" class="table-button" @click="updateBotFunc(scope.row)">编辑</el-button>
|
||||
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{type==='create'?'新增':'编辑'}}</span>
|
||||
<div>
|
||||
<el-button :loading="btnLoading" type="primary" @click="enterDialog">确 定</el-button>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="rule" label-width="80px">
|
||||
<el-form-item label="关键词:" prop="keyword" >
|
||||
<el-input v-model="formData.keyword" :clearable="true" placeholder="请输入关键词" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容:" prop="content" >
|
||||
<RichEdit v-model="formData.content"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="detailShow" :show-close="true" :before-close="closeDetailShow" title="查看">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="关键词">
|
||||
{{ detailFrom.keyword }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="内容">
|
||||
<RichView v-model="detailFrom.content" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createBot,
|
||||
deleteBot,
|
||||
deleteBotByIds,
|
||||
updateBot,
|
||||
findBot,
|
||||
getBotList
|
||||
} from '@/api/bot/bot'
|
||||
// 富文本组件
|
||||
import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||
import RichView from '@/components/richtext/rich-view.vue'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { getDictFunc, formatDate, formatBoolean, filterDict ,filterDataSource, returnArrImg, onDownloadFile } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useAppStore } from "@/pinia"
|
||||
|
||||
|
||||
|
||||
|
||||
defineOptions({
|
||||
name: 'Bot'
|
||||
})
|
||||
|
||||
// 提交按钮loading
|
||||
const btnLoading = ref(false)
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 控制更多查询条件显示/隐藏状态
|
||||
const showAllQuery = ref(false)
|
||||
|
||||
// 自动化生成的字典(可能为空)以及字段
|
||||
const formData = ref({
|
||||
keyword: '',
|
||||
content: '',
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
keyword : [{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input','blur'],
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur'],
|
||||
}
|
||||
],
|
||||
content : [{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input','blur'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const searchRule = reactive({
|
||||
createdAt: [
|
||||
{ validator: (rule, value, callback) => {
|
||||
if (searchInfo.value.startCreatedAt && !searchInfo.value.endCreatedAt) {
|
||||
callback(new Error('请填写结束日期'))
|
||||
} else if (!searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt) {
|
||||
callback(new Error('请填写开始日期'))
|
||||
} else if (searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt && (searchInfo.value.startCreatedAt.getTime() === searchInfo.value.endCreatedAt.getTime() || searchInfo.value.startCreatedAt.getTime() > searchInfo.value.endCreatedAt.getTime())) {
|
||||
callback(new Error('开始日期应当早于结束日期'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'change' }
|
||||
],
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
// =========== 表格控制部分 ===========
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async(valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 修改页面容量
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async() => {
|
||||
const table = await getBotList({ 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 setOptions = async () =>{
|
||||
}
|
||||
|
||||
// 获取需要的字典 可能为空 按需保留
|
||||
setOptions()
|
||||
|
||||
|
||||
// 多选数据
|
||||
const multipleSelection = ref([])
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteBotFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const onDelete = async() => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
const IDs = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map(item => {
|
||||
IDs.push(item.ID)
|
||||
})
|
||||
const res = await deleteBotByIds({ IDs })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 行为控制标记(弹窗内部需要增还是改)
|
||||
const type = ref('')
|
||||
|
||||
// 更新行
|
||||
const updateBotFunc = async(row) => {
|
||||
const res = await findBot({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除行
|
||||
const deleteBotFunc = async (row) => {
|
||||
const res = await deleteBot({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗控制标记
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
keyword: '',
|
||||
content: '',
|
||||
}
|
||||
}
|
||||
// 弹窗确定
|
||||
const enterDialog = async () => {
|
||||
btnLoading.value = true
|
||||
elFormRef.value?.validate( async (valid) => {
|
||||
if (!valid) return btnLoading.value = false
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createBot(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateBot(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createBot(formData.value)
|
||||
break
|
||||
}
|
||||
btnLoading.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const detailFrom = ref({})
|
||||
|
||||
// 查看详情控制标记
|
||||
const detailShow = ref(false)
|
||||
|
||||
|
||||
// 打开详情弹窗
|
||||
const openDetailShow = () => {
|
||||
detailShow.value = true
|
||||
}
|
||||
|
||||
|
||||
// 打开详情
|
||||
const getDetails = async (row) => {
|
||||
// 打开弹窗
|
||||
const res = await findBot({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
detailFrom.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailFrom.value = {}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
118
src/view/bot/bot/botForm.vue
Normal file
118
src/view/bot/bot/botForm.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-form-box">
|
||||
<el-form :model="formData" ref="elFormRef" label-position="right" :rules="rule" label-width="80px">
|
||||
<el-form-item label="关键词:" prop="keyword">
|
||||
<el-input v-model="formData.keyword" :clearable="true" placeholder="请输入关键词" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容:" prop="content">
|
||||
<RichEdit v-model="formData.content"/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :loading="btnLoading" type="primary" @click="save">保存</el-button>
|
||||
<el-button type="primary" @click="back">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createBot,
|
||||
updateBot,
|
||||
findBot
|
||||
} from '@/api/bot/bot'
|
||||
|
||||
defineOptions({
|
||||
name: 'BotForm'
|
||||
})
|
||||
|
||||
// 自动获取字典
|
||||
import { getDictFunc } from '@/utils/format'
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
// 富文本组件
|
||||
import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 提交按钮loading
|
||||
const btnLoading = ref(false)
|
||||
|
||||
const type = ref('')
|
||||
const formData = ref({
|
||||
keyword: '',
|
||||
content: '',
|
||||
})
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
keyword : [{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input','blur'],
|
||||
}],
|
||||
content : [{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input','blur'],
|
||||
}],
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
|
||||
// 初始化方法
|
||||
const init = async () => {
|
||||
// 建议通过url传参获取目标数据ID 调用 find方法进行查询数据操作 从而决定本页面是create还是update 以下为id作为url参数示例
|
||||
if (route.query.id) {
|
||||
const res = await findBot({ ID: route.query.id })
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
type.value = 'update'
|
||||
}
|
||||
} else {
|
||||
type.value = 'create'
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
// 保存按钮
|
||||
const save = async() => {
|
||||
btnLoading.value = true
|
||||
elFormRef.value?.validate( async (valid) => {
|
||||
if (!valid) return btnLoading.value = false
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createBot(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateBot(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createBot(formData.value)
|
||||
break
|
||||
}
|
||||
btnLoading.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回按钮
|
||||
const back = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
438
src/view/category/category/category.vue
Normal file
438
src/view/category/category/category.vue
Normal file
@@ -0,0 +1,438 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" :rules="searchRule" @keyup.enter="onSubmit">
|
||||
<el-form-item label="创建日期" prop="createdAt">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-date-picker v-model="searchInfo.startCreatedAt" type="datetime" placeholder="开始日期" :disabled-date="time=> searchInfo.endCreatedAt ? time.getTime() > searchInfo.endCreatedAt.getTime() : false"></el-date-picker>
|
||||
—
|
||||
<el-date-picker v-model="searchInfo.endCreatedAt" type="datetime" placeholder="结束日期" :disabled-date="time=> searchInfo.startCreatedAt ? time.getTime() < searchInfo.startCreatedAt.getTime() : false"></el-date-picker>
|
||||
</el-form-item>
|
||||
|
||||
|
||||
<template v-if="showAllQuery">
|
||||
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
<el-button link type="primary" icon="arrow-down" @click="showAllQuery=true" v-if="!showAllQuery">展开</el-button>
|
||||
<el-button link type="primary" icon="arrow-up" @click="showAllQuery=false" v-else>收起</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog()">新增</el-button>
|
||||
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="onDelete">删除</el-button>
|
||||
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
@sort-change="sortChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="createdAt"width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column sortable align="left" label="名称" prop="name" width="120" />
|
||||
<el-table-column sortable align="left" label="排序" prop="order" width="120" />
|
||||
<el-table-column align="left" label="状态" prop="active" width="120">
|
||||
<template #default="scope">{{ formatBoolean(scope.row.active) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="父ID" prop="parentId" width="120" />
|
||||
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon style="margin-right: 5px"><InfoFilled /></el-icon>查看</el-button>
|
||||
<el-button type="primary" link icon="edit" class="table-button" @click="updateCategoryFunc(scope.row)">编辑</el-button>
|
||||
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{type==='create'?'新增':'编辑'}}</span>
|
||||
<div>
|
||||
<el-button :loading="btnLoading" type="primary" @click="enterDialog">确 定</el-button>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="rule" label-width="80px">
|
||||
<el-form-item label="名称:" prop="name" >
|
||||
<el-input v-model="formData.name" :clearable="false" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序:" prop="order" >
|
||||
<el-input v-model.number="formData.order" :clearable="false" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态:" prop="active" >
|
||||
<el-switch v-model="formData.active" active-color="#13ce66" inactive-color="#ff4949" active-text="是" inactive-text="否" clearable ></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="父ID:" prop="parentId" >
|
||||
<el-input v-model.number="formData.parentId" :clearable="false" placeholder="请输入父ID" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="detailShow" :show-close="true" :before-close="closeDetailShow" title="查看">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="名称">
|
||||
{{ detailFrom.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="排序">
|
||||
{{ detailFrom.order }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
{{ detailFrom.active }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="父ID">
|
||||
{{ detailFrom.parentId }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createCategory,
|
||||
deleteCategory,
|
||||
deleteCategoryByIds,
|
||||
updateCategory,
|
||||
findCategory,
|
||||
getCategoryList
|
||||
} from '@/api/category/category'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { getDictFunc, formatDate, formatBoolean, filterDict ,filterDataSource, returnArrImg, onDownloadFile } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useAppStore } from "@/pinia"
|
||||
|
||||
|
||||
|
||||
|
||||
defineOptions({
|
||||
name: 'Category'
|
||||
})
|
||||
|
||||
// 提交按钮loading
|
||||
const btnLoading = ref(false)
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 控制更多查询条件显示/隐藏状态
|
||||
const showAllQuery = ref(false)
|
||||
|
||||
// 自动化生成的字典(可能为空)以及字段
|
||||
const formData = ref({
|
||||
name: '',
|
||||
order: 0,
|
||||
active: false,
|
||||
parentId: 0,
|
||||
})
|
||||
|
||||
|
||||
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
name : [{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input','blur'],
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur'],
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
const searchRule = reactive({
|
||||
createdAt: [
|
||||
{ validator: (rule, value, callback) => {
|
||||
if (searchInfo.value.startCreatedAt && !searchInfo.value.endCreatedAt) {
|
||||
callback(new Error('请填写结束日期'))
|
||||
} else if (!searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt) {
|
||||
callback(new Error('请填写开始日期'))
|
||||
} else if (searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt && (searchInfo.value.startCreatedAt.getTime() === searchInfo.value.endCreatedAt.getTime() || searchInfo.value.startCreatedAt.getTime() > searchInfo.value.endCreatedAt.getTime())) {
|
||||
callback(new Error('开始日期应当早于结束日期'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'change' }
|
||||
],
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
// =========== 表格控制部分 ===========
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
// 排序
|
||||
const sortChange = ({ prop, order }) => {
|
||||
const sortMap = {
|
||||
name: 'name',
|
||||
order: 'order',
|
||||
}
|
||||
|
||||
let sort = sortMap[prop]
|
||||
if(!sort){
|
||||
sort = prop.replace(/[A-Z]/g, match => `_${match.toLowerCase()}`)
|
||||
}
|
||||
|
||||
searchInfo.value.sort = sort
|
||||
searchInfo.value.order = order
|
||||
getTableData()
|
||||
}
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async(valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
if (searchInfo.value.active === ""){
|
||||
searchInfo.value.active=null
|
||||
}
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 修改页面容量
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async() => {
|
||||
const table = await getCategoryList({ 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 setOptions = async () =>{
|
||||
}
|
||||
|
||||
// 获取需要的字典 可能为空 按需保留
|
||||
setOptions()
|
||||
|
||||
|
||||
// 多选数据
|
||||
const multipleSelection = ref([])
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteCategoryFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const onDelete = async() => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
const IDs = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map(item => {
|
||||
IDs.push(item.ID)
|
||||
})
|
||||
const res = await deleteCategoryByIds({ IDs })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 行为控制标记(弹窗内部需要增还是改)
|
||||
const type = ref('')
|
||||
|
||||
// 更新行
|
||||
const updateCategoryFunc = async(row) => {
|
||||
const res = await findCategory({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 删除行
|
||||
const deleteCategoryFunc = async (row) => {
|
||||
const res = await deleteCategory({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗控制标记
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
name: '',
|
||||
order: 0,
|
||||
active: false,
|
||||
parentId: 0,
|
||||
}
|
||||
}
|
||||
// 弹窗确定
|
||||
const enterDialog = async () => {
|
||||
btnLoading.value = true
|
||||
elFormRef.value?.validate( async (valid) => {
|
||||
if (!valid) return btnLoading.value = false
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createCategory(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateCategory(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createCategory(formData.value)
|
||||
break
|
||||
}
|
||||
btnLoading.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const detailFrom = ref({})
|
||||
|
||||
// 查看详情控制标记
|
||||
const detailShow = ref(false)
|
||||
|
||||
|
||||
// 打开详情弹窗
|
||||
const openDetailShow = () => {
|
||||
detailShow.value = true
|
||||
}
|
||||
|
||||
|
||||
// 打开详情
|
||||
const getDetails = async (row) => {
|
||||
// 打开弹窗
|
||||
const res = await findCategory({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
detailFrom.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailFrom.value = {}
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
119
src/view/category/category/categoryForm.vue
Normal file
119
src/view/category/category/categoryForm.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-form-box">
|
||||
<el-form :model="formData" ref="elFormRef" label-position="right" :rules="rule" label-width="80px">
|
||||
<el-form-item label="名称:" prop="name">
|
||||
<el-input v-model="formData.name" :clearable="false" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序:" prop="order">
|
||||
<el-input v-model.number="formData.order" :clearable="false" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态:" prop="active">
|
||||
<el-switch v-model="formData.active" active-color="#13ce66" inactive-color="#ff4949" active-text="是" inactive-text="否" clearable ></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="父ID:" prop="parentId">
|
||||
<el-input v-model.number="formData.parentId" :clearable="false" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button :loading="btnLoading" type="primary" @click="save">保存</el-button>
|
||||
<el-button type="primary" @click="back">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createCategory,
|
||||
updateCategory,
|
||||
findCategory
|
||||
} from '@/api/category/category'
|
||||
|
||||
defineOptions({
|
||||
name: 'CategoryForm'
|
||||
})
|
||||
|
||||
// 自动获取字典
|
||||
import { getDictFunc } from '@/utils/format'
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 提交按钮loading
|
||||
const btnLoading = ref(false)
|
||||
|
||||
const type = ref('')
|
||||
const formData = ref({
|
||||
name: '',
|
||||
order: 0,
|
||||
active: false,
|
||||
parentId: 0,
|
||||
})
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
name : [{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input','blur'],
|
||||
}],
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
|
||||
// 初始化方法
|
||||
const init = async () => {
|
||||
// 建议通过url传参获取目标数据ID 调用 find方法进行查询数据操作 从而决定本页面是create还是update 以下为id作为url参数示例
|
||||
if (route.query.id) {
|
||||
const res = await findCategory({ ID: route.query.id })
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
type.value = 'update'
|
||||
}
|
||||
} else {
|
||||
type.value = 'create'
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
// 保存按钮
|
||||
const save = async() => {
|
||||
btnLoading.value = true
|
||||
elFormRef.value?.validate( async (valid) => {
|
||||
if (!valid) return btnLoading.value = false
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createCategory(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateCategory(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createCategory(formData.value)
|
||||
break
|
||||
}
|
||||
btnLoading.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 返回按钮
|
||||
const back = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
43
src/view/dashboard/components/banner.vue
Normal file
43
src/view/dashboard/components/banner.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<el-carousel class="-mt-2">
|
||||
<el-carousel-item
|
||||
class="cursor-pointer lg:h-40"
|
||||
v-for="(item, index) in banners"
|
||||
:key="index"
|
||||
@click="openLink(item.link)"
|
||||
>
|
||||
<el-image class="h-full w-full" :src="item.img" fit="fill"></el-image>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import banner from '@/assets/banner.jpg'
|
||||
import banner2 from '@/assets/banner2.jpg'
|
||||
|
||||
const openLink = (link) => {
|
||||
window.open(link, '_blank')
|
||||
}
|
||||
|
||||
const banners = [
|
||||
{
|
||||
img: banner,
|
||||
link: 'https://gin-vue-admin.com/empower/index.html'
|
||||
},
|
||||
{
|
||||
img: banner2,
|
||||
link: 'https://plugin.gin-vue-admin.com'
|
||||
},
|
||||
{
|
||||
img: 'https://qmplusimg.henrongyi.top/gvaDemo/k8s.jpg',
|
||||
link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=42'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
46
src/view/dashboard/components/card.vue
Normal file
46
src/view/dashboard/components/card.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-900 text-gray-800 dark:text-gray-400 rounded shadow"
|
||||
:class="[customClass || '', withoutPadding ? 'p-0' : 'p-4']"
|
||||
>
|
||||
<div v-if="title" class="flex justify-between items-center">
|
||||
<div class="text-base font-bold">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div v-if="showAction" class="text-sm text-active cursor-pointer">
|
||||
查看更多
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
withoutPadding: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
189
src/view/dashboard/components/charts-content-numbers.vue
Normal file
189
src/view/dashboard/components/charts-content-numbers.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<!--
|
||||
本组件参考 arco-pro 的实现 将 ts 改为 js 写法
|
||||
https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/views/dashboard/workplace/components/content-chart.vue
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<Chart :height="height" :option="chartOption" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Chart from '@/components/charts/index.vue'
|
||||
import useChartOption from '@/hooks/charts'
|
||||
import { graphic } from 'echarts'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config } = storeToRefs(appStore)
|
||||
defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: '128px'
|
||||
}
|
||||
})
|
||||
const dotColor = computed(() => {
|
||||
return appStore.isDark ? '#333' : '#E5E8EF'
|
||||
})
|
||||
const graphicFactory = (side) => {
|
||||
return {
|
||||
type: 'text',
|
||||
bottom: '8',
|
||||
...side,
|
||||
style: {
|
||||
text: '',
|
||||
textAlign: 'center',
|
||||
fill: '#4E5969',
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
const xAxis = ref([
|
||||
'2024-1',
|
||||
'2024-2',
|
||||
'2024-3',
|
||||
'2024-4',
|
||||
'2024-5',
|
||||
'2024-6',
|
||||
'2024-7',
|
||||
'2024-8'
|
||||
])
|
||||
const chartsData = ref([12, 22, 32, 45, 32, 78, 89, 92])
|
||||
const graphicElements = ref([
|
||||
graphicFactory({ left: '5%' }),
|
||||
graphicFactory({ right: 0 })
|
||||
])
|
||||
const { chartOption } = useChartOption(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: '40',
|
||||
right: '0',
|
||||
top: '10',
|
||||
bottom: '30'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
offset: 2,
|
||||
data: xAxis.value,
|
||||
boundaryGap: false,
|
||||
axisLabel: {
|
||||
color: '#4E5969',
|
||||
formatter(value, idx) {
|
||||
if (idx === 0) return ''
|
||||
if (idx === xAxis.value.length - 1) return ''
|
||||
return `${value}`
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
interval: (idx) => {
|
||||
if (idx === 0) return false
|
||||
if (idx === xAxis.value.length - 1) return false
|
||||
return true
|
||||
},
|
||||
lineStyle: {
|
||||
color: dotColor.value
|
||||
}
|
||||
},
|
||||
axisPointer: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: `${config.value.primaryColor}FF`,
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
formatter(value, idx) {
|
||||
if (idx === 0) return value
|
||||
return `${value}k`
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: dotColor.value
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter(params) {
|
||||
const [firstElement] = params
|
||||
return `<div>
|
||||
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
|
||||
<div class="content-panel"><span>总内容量</span><span class="tooltip-value">${(
|
||||
Number(firstElement.value) * 10000
|
||||
).toLocaleString()}</span></div>
|
||||
</div>`
|
||||
},
|
||||
className: 'echarts-tooltip-diy'
|
||||
},
|
||||
graphic: {
|
||||
elements: graphicElements.value
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: chartsData.value,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
// symbol: 'circle',
|
||||
symbolSize: 12,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: new graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `${config.value.primaryColor}80`
|
||||
},
|
||||
{
|
||||
offset: 0.5,
|
||||
color: `${config.value.primaryColor}92`
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `${config.value.primaryColor}FF`
|
||||
}
|
||||
])
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `${config.value.primaryColor}20`
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `${config.value.primaryColor}08`
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
138
src/view/dashboard/components/charts-people-numbers.vue
Normal file
138
src/view/dashboard/components/charts-people-numbers.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<!--
|
||||
本组件参考 arco-pro 的实现 将 ts 改为 js 写法
|
||||
https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/views/dashboard/workplace/components/content-chart.vue
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
@desc: 人数统计图表
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<Chart :height="height" :option="chartOption" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Chart from '@/components/charts/index.vue'
|
||||
import useChartOption from '@/hooks/charts'
|
||||
import { graphic } from 'echarts'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '@/pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config } = storeToRefs(appStore)
|
||||
|
||||
const prop = defineProps({
|
||||
height: {
|
||||
type: String,
|
||||
default: '128px'
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const graphicFactory = (side) => {
|
||||
return {
|
||||
type: 'text',
|
||||
bottom: '8',
|
||||
...side,
|
||||
style: {
|
||||
text: '',
|
||||
textAlign: 'center',
|
||||
fill: '#4E5969',
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
const graphicElements = ref([
|
||||
graphicFactory({ left: '5%' }),
|
||||
graphicFactory({ right: 0 })
|
||||
])
|
||||
const { chartOption } = useChartOption(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: '40',
|
||||
right: '0',
|
||||
top: '10',
|
||||
bottom: '30'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
offset: 2,
|
||||
show: false,
|
||||
boundaryGap: false,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisTick: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false,
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
show: false
|
||||
},
|
||||
splitLine: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
graphic: {
|
||||
elements: graphicElements.value
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: prop.data,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbolSize: 12,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
color: new graphic.LinearGradient(0, 0, 1, 0, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `${config.value.primaryColor}32`
|
||||
},
|
||||
{
|
||||
offset: 0.5,
|
||||
color: `${config.value.primaryColor}64`
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `${config.value.primaryColor}FF`
|
||||
}
|
||||
])
|
||||
},
|
||||
showSymbol: false,
|
||||
areaStyle: {
|
||||
opacity: 0.8,
|
||||
color: new graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: `${config.value.primaryColor}20`
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: `${config.value.primaryColor}08`
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
54
src/view/dashboard/components/charts.vue
Normal file
54
src/view/dashboard/components/charts.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div class="">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div v-if="title" class="font-bold">
|
||||
{{ title }}
|
||||
</div>
|
||||
<slot v-else name="title" />
|
||||
</div>
|
||||
<div class="w-full relative">
|
||||
<div v-if="type !== 4">
|
||||
<div class="mt-4 text-gray-600 text-3xl font-mono">
|
||||
<el-statistic :value="268500" />
|
||||
</div>
|
||||
<div class="mt-2 text-green-600 text-sm font-bold font-mono">
|
||||
+80% <el-icon><TopRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-0 right-2 w-[50%] h-20">
|
||||
<charts-people-number v-if="type === 1" :data="data[0]" height="100%" />
|
||||
<charts-people-number v-if="type === 2" :data="data[1]" height="100%" />
|
||||
<charts-people-number v-if="type === 3" :data="data[2]" height="100%" />
|
||||
</div>
|
||||
<charts-content-number v-if="type === 4" height="14rem" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import chartsPeopleNumber from './charts-people-numbers.vue'
|
||||
import chartsContentNumber from './charts-content-numbers.vue'
|
||||
defineProps({
|
||||
type: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const data = [
|
||||
[12, 22, 32, 45, 32, 78, 89, 92],
|
||||
[1, 2, 43, 5, 67, 78, 89, 12],
|
||||
[12, 22, 32, 45, 32, 78, 89, 92]
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
19
src/view/dashboard/components/index.js
Normal file
19
src/view/dashboard/components/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import GvaBanner from './banner.vue'
|
||||
import GvaCard from './card.vue'
|
||||
import GvaChart from './charts.vue'
|
||||
import GvaTable from './table.vue'
|
||||
import GvaNotice from './notice.vue'
|
||||
import GvaQuickLink from './quickLinks.vue'
|
||||
import GvaWiki from './wiki.vue'
|
||||
import GvaPluginTable from './pluginTable.vue'
|
||||
|
||||
export {
|
||||
GvaBanner,
|
||||
GvaCard,
|
||||
GvaChart,
|
||||
GvaTable,
|
||||
GvaNotice,
|
||||
GvaQuickLink,
|
||||
GvaWiki,
|
||||
GvaPluginTable
|
||||
}
|
||||
80
src/view/dashboard/components/notice.vue
Normal file
80
src/view/dashboard/components/notice.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<div
|
||||
v-for="(item, index) in notices"
|
||||
:key="index"
|
||||
class="flex items-center mb-1.5 gap-3"
|
||||
>
|
||||
<el-tag :type="item.type" size="small">
|
||||
{{ item.typeTitle }}
|
||||
</el-tag>
|
||||
<el-tooltip effect="light" :content="item.title" placement="top">
|
||||
<div class="text-xs text-gray-700 dark:text-gray-300 line-clamp-1">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const notices = [
|
||||
{
|
||||
type: 'primary',
|
||||
typeTitle: '公告',
|
||||
title: '授权费将在从六月一日起结束第一价格梯度,进入第二价格梯度。'
|
||||
},
|
||||
{
|
||||
type: 'success',
|
||||
typeTitle: '通知',
|
||||
title: '授权后将进入专属飞书群,获取官方辅助。'
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
typeTitle: '警告',
|
||||
title: '授权可获得插件市场极大优惠价格。'
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
typeTitle: '违规',
|
||||
title: '未授权商用将有可能被资源采集工具爬取并追责。'
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
typeTitle: '信息',
|
||||
title: '再次感谢您对开源事业的支持'
|
||||
},
|
||||
{
|
||||
type: 'primary',
|
||||
typeTitle: '公告',
|
||||
title: '让创意更有价值。'
|
||||
},
|
||||
{
|
||||
type: 'success',
|
||||
typeTitle: '通知',
|
||||
title: '让劳动更有意义。'
|
||||
},
|
||||
{
|
||||
type: 'warning',
|
||||
typeTitle: '警告',
|
||||
title: '让思维更有深度。'
|
||||
},
|
||||
{
|
||||
type: 'danger',
|
||||
typeTitle: '错误',
|
||||
title: '让生活更有趣味。'
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
typeTitle: '信息',
|
||||
title: '让公司更有活力。'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
66
src/view/dashboard/components/pluginTable.vue
Normal file
66
src/view/dashboard/components/pluginTable.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="tableData" stripe style="width: 100%">
|
||||
<el-table-column prop="ranking" label="排名" width="80" align="center" />
|
||||
<el-table-column prop="title" label="插件标题" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<a class="text-active" :href="row.link" target="_blank">{{
|
||||
row.title
|
||||
}}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="click_num" label="关注度" width="100" />
|
||||
<el-table-column prop="hot" label="热度值" width="100" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const tableData = [
|
||||
{
|
||||
ranking: 1,
|
||||
title: '组织管理插件:更方便管理组织,分配资源权限。',
|
||||
click_num: 523,
|
||||
hot: 263,
|
||||
link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=36'
|
||||
},
|
||||
{
|
||||
ranking: 2,
|
||||
title:
|
||||
'Kubernetes容器管理:,Kubernetes 原生资源管理,提供炫酷的YAML 编辑,Pod 终端,方便运维兄弟管理k8s资源',
|
||||
click_num: 416,
|
||||
hot: 223,
|
||||
link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=42'
|
||||
},
|
||||
{
|
||||
ranking: 3,
|
||||
title:
|
||||
'定时任务配置化管理:本插件用于对系统内部的定时任务进行配置化管理,可以配置自定义的函数和HTTP,可以配置cron和remark等等',
|
||||
click_num: 337,
|
||||
hot: 176,
|
||||
link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=67'
|
||||
},
|
||||
{
|
||||
ranking: 4,
|
||||
title:
|
||||
'官网CMS系统:基于Gin-Vue-Admin 和 插件市场客户端开发基座开发的企业官网类(cms)系统',
|
||||
click_num: 292,
|
||||
hot: 145,
|
||||
link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=69'
|
||||
},
|
||||
{
|
||||
ranking: 5,
|
||||
title: '微信支付插件:提供扫码支付功能(需自行对接业务)',
|
||||
click_num: 173,
|
||||
hot: 110,
|
||||
link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=28'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
111
src/view/dashboard/components/quickLinks.vue
Normal file
111
src/view/dashboard/components/quickLinks.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
<template>
|
||||
<div class="mt-8 w-full">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 3xl:grid-cols-4">
|
||||
<div
|
||||
v-for="(item, index) in shortcuts"
|
||||
:key="index"
|
||||
class="flex flex-col items-center mb-3 group cursor-pointer"
|
||||
@click="toPath(item)"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded bg-gray-200 dark:bg-slate-500 flex items-center justify-center group-hover:bg-blue-400 group-hover:text-white"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="text-xs mt-2 text-gray-700 dark:text-gray-300">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 mt-8">
|
||||
<div
|
||||
v-for="(item, index) in recentVisits"
|
||||
:key="index"
|
||||
class="flex flex-col items-center mb-3 group cursor-pointer"
|
||||
@click="openLink(item)"
|
||||
>
|
||||
<div
|
||||
class="w-8 h-8 rounded bg-gray-200 dark:bg-slate-500 flex items-center justify-center group-hover:bg-blue-400 group-hover:text-white"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="text-xs mt-2 text-gray-700 dark:text-gray-300">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Menu,
|
||||
Link,
|
||||
User,
|
||||
Service,
|
||||
Document,
|
||||
Reading,
|
||||
Files,
|
||||
Memo
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
const toPath = (item) => {
|
||||
router.push({ name: item.path })
|
||||
}
|
||||
|
||||
const openLink = (item) => {
|
||||
window.open(item.path, '_blank')
|
||||
}
|
||||
const shortcuts = [
|
||||
{
|
||||
icon: Menu,
|
||||
title: '菜单管理',
|
||||
path: 'menu'
|
||||
},
|
||||
{
|
||||
icon: Link,
|
||||
title: 'API管理',
|
||||
path: 'api'
|
||||
},
|
||||
{
|
||||
icon: Service,
|
||||
title: '角色管理',
|
||||
path: 'authority'
|
||||
},
|
||||
{
|
||||
icon: User,
|
||||
title: '用户管理',
|
||||
path: 'user'
|
||||
},
|
||||
{
|
||||
icon: Files,
|
||||
title: '自动化包',
|
||||
path: 'autoPkg'
|
||||
},
|
||||
{
|
||||
icon: Memo,
|
||||
title: '自动代码',
|
||||
path: 'autoCode'
|
||||
}
|
||||
]
|
||||
|
||||
const recentVisits = [
|
||||
{
|
||||
icon: Reading,
|
||||
title: '授权购买',
|
||||
path: 'https://gin-vue-admin.com/empower/index.html'
|
||||
},
|
||||
{
|
||||
icon: Document,
|
||||
title: '插件市场',
|
||||
path: 'https://plugin.gin-vue-admin.com/#/layout/home'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
52
src/view/dashboard/components/table.vue
Normal file
52
src/view/dashboard/components/table.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="tableData" stripe style="width: 100%">
|
||||
<el-table-column prop="ranking" label="排名" width="80" align="center" />
|
||||
<el-table-column prop="title" label="内容标题" show-overflow-tooltip />
|
||||
<el-table-column prop="click_num" label="关注度" width="100" />
|
||||
<el-table-column prop="hot" label="热度值" width="100" />
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const tableData = [
|
||||
{
|
||||
ranking: 1,
|
||||
title: '更简洁的使用界面,更快速的操作体验',
|
||||
click_num: 523,
|
||||
hot: 263
|
||||
},
|
||||
{
|
||||
ranking: 2,
|
||||
title: '更优质的服务,更便捷的使用体验',
|
||||
click_num: 416,
|
||||
hot: 223
|
||||
},
|
||||
{
|
||||
ranking: 3,
|
||||
title: '更快速的创意实现,更高效的工作效率',
|
||||
click_num: 337,
|
||||
hot: 176
|
||||
},
|
||||
{
|
||||
ranking: 4,
|
||||
title: '更多的创意资源,更多的创意灵感',
|
||||
click_num: 292,
|
||||
hot: 145
|
||||
},
|
||||
{
|
||||
ranking: 5,
|
||||
title: '更合理的代码结构,更清晰的代码逻辑',
|
||||
click_num: 173,
|
||||
hot: 110
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
45
src/view/dashboard/components/wiki.vue
Normal file
45
src/view/dashboard/components/wiki.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/8
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<a
|
||||
v-for="item in wikis"
|
||||
:key="item.url"
|
||||
:href="item.url"
|
||||
class="text-sm text-gray-700 dark:text-gray-300 no-underline hover:text-active"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const wikis = [
|
||||
{
|
||||
title: 'Vue3',
|
||||
url: 'https://v3.cn.vuejs.org/guide/introduction.html'
|
||||
},
|
||||
{
|
||||
title: 'GIN 文档',
|
||||
url: 'https://gin-gonic.com/'
|
||||
},
|
||||
{
|
||||
title: 'GVA 文档',
|
||||
url: 'https://www.gin-vue-admin.com/'
|
||||
},
|
||||
{
|
||||
title: '插件市场',
|
||||
url: 'https://plugin.gin-vue-admin.com/'
|
||||
},
|
||||
{
|
||||
title: 'github 仓库',
|
||||
url: 'https://github.com/flipped-aurora/gin-vue-admin'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
77
src/view/dashboard/index.vue
Normal file
77
src/view/dashboard/index.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div
|
||||
class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-7 py-2 gap-4 md:gap-2 gva-container2"
|
||||
>
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||
<gva-chart :type="1" title="访问人数" />
|
||||
</gva-card>
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||
<gva-chart :type="2" title="新增客户" />
|
||||
</gva-card>
|
||||
<gva-card custom-class="col-span-1 lg:col-span-2 ">
|
||||
<gva-chart :type="3" title="解决数量" />
|
||||
</gva-card>
|
||||
<gva-card
|
||||
title="快捷功能"
|
||||
show-action
|
||||
custom-class="col-start-1 md:col-start-3 lg:col-start-7 row-span-2 "
|
||||
>
|
||||
<gva-quick-link />
|
||||
</gva-card>
|
||||
<gva-card
|
||||
title="内容数据"
|
||||
custom-class="col-span-1 md:col-span-2 md:row-start-2 lg:col-span-6 col-start-1 row-span-2"
|
||||
>
|
||||
<gva-chart :type="4" />
|
||||
</gva-card>
|
||||
<gva-card
|
||||
title="文档"
|
||||
show-action
|
||||
custom-class="md:row-start-8 md:col-start-3 lg:row-start-3 lg:col-start-7"
|
||||
>
|
||||
<gva-wiki />
|
||||
</gva-card>
|
||||
|
||||
<gva-card
|
||||
title="最新更新"
|
||||
custom-class="col-span-1 md:col-span-3 row-span-2"
|
||||
>
|
||||
<gva-table />
|
||||
</gva-card>
|
||||
<gva-card
|
||||
title="最新插件"
|
||||
custom-class="col-span-1 md:col-span-3 row-span-2"
|
||||
>
|
||||
<gva-plugin-table />
|
||||
</gva-card>
|
||||
|
||||
<gva-card title="公告" show-action custom-class="col-span-1 lg:col-start-7">
|
||||
<gva-notice />
|
||||
</gva-card>
|
||||
|
||||
<gva-card
|
||||
without-padding
|
||||
custom-class="overflow-hidden lg:h-40 col-span-1 md:col-start-2 md:col-span-1 lg:col-start-7"
|
||||
>
|
||||
<gva-banner />
|
||||
</gva-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
GvaPluginTable,
|
||||
GvaTable,
|
||||
GvaChart,
|
||||
GvaWiki,
|
||||
GvaNotice,
|
||||
GvaQuickLink,
|
||||
GvaCard,
|
||||
GvaBanner
|
||||
} from './components'
|
||||
defineOptions({
|
||||
name: 'Dashboard'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
37
src/view/error/index.vue
Normal file
37
src/view/error/index.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="w-full h-screen bg-gray-50 flex items-center justify-center">
|
||||
<div class="flex flex-col items-center text-2xl gap-4">
|
||||
<img class="w-1/3" src="../../assets/404.png" />
|
||||
<p class="text-lg">页面被神秘力量吸走了</p>
|
||||
<p class="text-lg">
|
||||
常见问题为当前此角色无当前路由,如果确定要使用本路由,请到角色管理进行分配
|
||||
</p>
|
||||
<p>
|
||||
项目地址:<a
|
||||
href="https://github.com/flipped-aurora/gin-vue-admin"
|
||||
target="_blank"
|
||||
class="text-blue-600"
|
||||
>https://github.com/flipped-aurora/gin-vue-admin</a
|
||||
>
|
||||
</p>
|
||||
<el-button @click="toDashboard">返回首页</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'Error'
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const toDashboard = () => {
|
||||
router.push({ name: userStore.userInfo.authority.defaultRouter })
|
||||
}
|
||||
</script>
|
||||
14
src/view/error/reload.vue
Normal file
14
src/view/error/reload.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'Reload'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
router.go(-1)
|
||||
</script>
|
||||
340
src/view/example/breakpoint/breakpoint.vue
Normal file
340
src/view/example/breakpoint/breakpoint.vue
Normal file
@@ -0,0 +1,340 @@
|
||||
<template>
|
||||
<div class="break-point">
|
||||
<div class="gva-table-box">
|
||||
<el-divider content-position="left">大文件上传</el-divider>
|
||||
<form id="fromCont" method="post">
|
||||
<!-- 新增按钮容器,使用 Flexbox 对齐按钮 -->
|
||||
<div class="button-container">
|
||||
<div class="fileUpload" @click="inputChange">
|
||||
<span class="takeFile">选择文件</span>
|
||||
<input
|
||||
v-show="false"
|
||||
id="file"
|
||||
ref="FileInput"
|
||||
multiple="multiple"
|
||||
type="file"
|
||||
@change="choseFile"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
:disabled="limitFileSize"
|
||||
type="primary"
|
||||
class="uploadBtn"
|
||||
@click="getFile"
|
||||
>上传文件</el-button>
|
||||
</div>
|
||||
</form>
|
||||
<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'
|
||||
|
||||
defineOptions({
|
||||
name: 'BreakPoint'
|
||||
})
|
||||
|
||||
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) => {
|
||||
// 点击选择文件后取消 直接return
|
||||
if (!e.target.files.length) {
|
||||
return
|
||||
}
|
||||
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 // 记录长度用于百分比展示
|
||||
}
|
||||
} else {
|
||||
limitFileSize.value = true
|
||||
ElMessage('请上传小于5M文件!')
|
||||
}
|
||||
}
|
||||
|
||||
const getFile = () => {
|
||||
// 确定按钮
|
||||
if (file.value === null) {
|
||||
ElMessage('请先上传文件!')
|
||||
return
|
||||
}
|
||||
// 检查文件上传进度
|
||||
if (percentage.value === 100) {
|
||||
ElMessage.success('上传已完成!') // 添加提示消息
|
||||
percentageFlage.value = false
|
||||
return // 如果进度已完成,阻止继续执行后续代码
|
||||
}
|
||||
// 如果文件未上传完成,继续上传切片
|
||||
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>
|
||||
|
||||
<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;
|
||||
}
|
||||
|
||||
.gva-table-box {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fileUpload,
|
||||
.uploadBtn {
|
||||
width: 90px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fileUpload {
|
||||
padding: 0 15px;
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease-in-out;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.uploadBtn {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.fileUpload:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.uploadBtn:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
|
||||
.fileUpload:active,
|
||||
.uploadBtn:active {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.fileUpload input {
|
||||
position: relative;
|
||||
font-size: 100px;
|
||||
right: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.fileName {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin: 6px 15px 0 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>
|
||||
215
src/view/example/customer/customer.vue
Normal file
215
src/view/example/customer/customer.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
title="在资源权限中将此角色的资源权限清空 或者不包含创建者的角色 即可屏蔽此客户资源的显示"
|
||||
/>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDrawer"
|
||||
>新增</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
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="updateCustomer(scope.row)"
|
||||
>变更</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteCustomer(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-drawer
|
||||
v-model="drawerFormVisible"
|
||||
:before-close="closeDrawer"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">客户</span>
|
||||
<div>
|
||||
<el-button @click="closeDrawer">取 消</el-button>
|
||||
<el-button type="primary" @click="enterDrawer">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<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>
|
||||
</el-drawer>
|
||||
</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, ElMessageBox } from 'element-plus'
|
||||
import { formatDate } from '@/utils/format'
|
||||
|
||||
defineOptions({
|
||||
name: 'Customer'
|
||||
})
|
||||
|
||||
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 drawerFormVisible = 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
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
}
|
||||
const closeDrawer = () => {
|
||||
drawerFormVisible.value = false
|
||||
form.value = {
|
||||
customerName: '',
|
||||
customerPhoneData: ''
|
||||
}
|
||||
}
|
||||
const deleteCustomer = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
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 enterDrawer = 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) {
|
||||
closeDrawer()
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
const openDrawer = () => {
|
||||
type.value = 'create'
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
19
src/view/example/index.vue
Normal file
19
src/view/example/index.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<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 setup>
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
const routerStore = useRouterStore()
|
||||
defineOptions({
|
||||
name: 'Example'
|
||||
})
|
||||
</script>
|
||||
245
src/view/example/upload/scanUpload.vue
Normal file
245
src/view/example/upload/scanUpload.vue
Normal file
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="flex justify-center w-full pt-2">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
class="h5-uploader"
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
|
||||
accept="image/*"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
:headers="{ 'x-token': token }"
|
||||
:data="{'classId': classId}"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-icon class="h5-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full h-auto p-0 pt-4">
|
||||
<!-- 左侧编辑区 -->
|
||||
<div class="flex-1 min-h-[60vh]">
|
||||
<div class="w-screen h-[calc(100vh-175px)] rounded">
|
||||
<template v-if="isCrop">
|
||||
<VueCropper
|
||||
ref="cropperRef"
|
||||
:img="imgSrc"
|
||||
mode="contain"
|
||||
outputType="jpeg"
|
||||
:autoCrop="true"
|
||||
:autoCropWidth="cropWidth"
|
||||
:autoCropHeight="cropHeight"
|
||||
:fixedBox="false"
|
||||
:fixed="fixedRatio"
|
||||
:fixedNumber="fixedNumber"
|
||||
:centerBox="true"
|
||||
:canMoveBox="true"
|
||||
:full="false"
|
||||
:maxImgSize="windowWidth"
|
||||
:original="true"
|
||||
></VueCropper>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex justify-center items-center w-full h-[calc(100vh-175px)]">
|
||||
<el-image v-if="imgSrc" :src="imgSrc" class="max-w-full max-h-full" mode="cover" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button-group v-if="isCrop">
|
||||
<el-tooltip content="向左旋转">
|
||||
<el-button @click="rotate(-90)" :icon="RefreshLeft" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="向右旋转">
|
||||
<el-button @click="rotate(90)" :icon="RefreshRight" />
|
||||
</el-tooltip>
|
||||
<el-button :icon="Plus" @click="changeScale(1)"></el-button>
|
||||
<el-button :icon="Minus" @click="changeScale(-1)"></el-button>
|
||||
</el-button-group>
|
||||
|
||||
|
||||
<el-switch
|
||||
size="large"
|
||||
v-model="isCrop"
|
||||
inline-prompt
|
||||
active-text="裁剪"
|
||||
inactive-text="裁剪"
|
||||
/>
|
||||
|
||||
<el-button type="primary" @click="handleUpload" :loading="uploading"> {{ uploading ? '上传中...' : '上 传' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, getCurrentInstance, onMounted } from 'vue'
|
||||
import { ElLoading, ElMessage } from 'element-plus'
|
||||
import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue'
|
||||
import 'vue-cropper/dist/index.css'
|
||||
import { VueCropper } from 'vue-cropper'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'scanUpload'
|
||||
})
|
||||
|
||||
const classId = ref(0)
|
||||
const token = ref('')
|
||||
const isCrop = ref(false)
|
||||
|
||||
const windowWidth = ref(300)
|
||||
|
||||
// 获取屏幕宽度
|
||||
const getWindowResize = function() {
|
||||
windowWidth.value = window.innerWidth
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getWindowResize()
|
||||
window.addEventListener('resize', getWindowResize)
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
router.isReady().then(() => {
|
||||
let query = router.currentRoute.value.query
|
||||
//console.log(query)
|
||||
classId.value = query.id
|
||||
token.value = query.token
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
|
||||
const uploadRef = ref(null)
|
||||
// 响应式数据
|
||||
const imgSrc = ref('')
|
||||
const cropperRef = ref(null)
|
||||
const { proxy } = getCurrentInstance()
|
||||
const previews = ref({})
|
||||
const uploading = ref(false)
|
||||
|
||||
// 缩放控制
|
||||
const changeScale = (value) => {
|
||||
proxy.$refs.cropperRef.changeScale(value)
|
||||
}
|
||||
|
||||
const fixedNumber = ref([1, 1])
|
||||
const cropWidth = ref(300)
|
||||
const cropHeight = ref(300)
|
||||
|
||||
const fixedRatio = ref(false)
|
||||
|
||||
// 文件处理
|
||||
const handleFileChange = (file) => {
|
||||
const isImage = file.raw.type.includes('image')
|
||||
if (!isImage) {
|
||||
ElMessage.error('请选择图片文件')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.raw.size / 1024 / 1024 > 8) {
|
||||
ElMessage.error('文件大小不能超过8MB!')
|
||||
return false
|
||||
}
|
||||
|
||||
const loading = ElLoading.service({
|
||||
lock: true,
|
||||
text: '请稍后',
|
||||
background: 'rgba(0, 0, 0, 0.7)',
|
||||
})
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
imgSrc.value = e.target.result
|
||||
loading.close()
|
||||
}
|
||||
reader.readAsDataURL(file.raw)
|
||||
}
|
||||
|
||||
// 旋转控制
|
||||
const rotate = (degree) => {
|
||||
if (degree === -90) {
|
||||
proxy.$refs.cropperRef.rotateLeft()
|
||||
} else {
|
||||
proxy.$refs.cropperRef.rotateRight()
|
||||
}
|
||||
}
|
||||
|
||||
// 上传处理
|
||||
const handleUpload = () => {
|
||||
uploading.value = true
|
||||
if(isCrop.value === false){
|
||||
uploadRef.value.submit()
|
||||
return true
|
||||
}
|
||||
proxy.$refs.cropperRef.getCropBlob((blob) => {
|
||||
try {
|
||||
const file = new File([blob], `${Date.now()}.jpg`, { type: 'image/jpeg' })
|
||||
uploadRef.value.clearFiles()
|
||||
uploadRef.value.handleStart(file)
|
||||
uploadRef.value.submit()
|
||||
|
||||
} catch (error) {
|
||||
uploading.value = false
|
||||
ElMessage.error('上传失败: ' + error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleImageSuccess = (res) => {
|
||||
const { data } = res
|
||||
if (data) {
|
||||
imgSrc.value = null
|
||||
uploading.value = false
|
||||
previews.value = {}
|
||||
ElMessage.success('上传成功')
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
/* 工具栏(固定在底部) */
|
||||
.toolbar {
|
||||
@apply fixed bottom-0 m-0 rounded-none p-2.5 shadow-[0_-2px_10px_rgba(0,0,0,0.1)] z-[1000] flex justify-between w-screen bg-slate-900;
|
||||
|
||||
/* 按钮组适配 */
|
||||
.el-button-group {
|
||||
@apply flex gap-2;
|
||||
|
||||
.el-button {
|
||||
@apply p-2 w-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.vue-cropper) {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.h5-uploader .el-upload {
|
||||
@apply rounded cursor-pointer relative overflow-hidden;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
}
|
||||
|
||||
.h5-uploader .el-upload:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon.h5-uploader-icon {
|
||||
@apply text-2xl text-gray-500 w-32 h-32 text-center;
|
||||
}
|
||||
</style>
|
||||
448
src/view/example/upload/upload.vue
Normal file
448
src/view/example/upload/upload.vue
Normal file
@@ -0,0 +1,448 @@
|
||||
<template>
|
||||
<div v-loading.fullscreen.lock="fullscreenLoading">
|
||||
<div class="flex gap-4 p-2">
|
||||
<div class="flex-none w-64 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4">
|
||||
<el-scrollbar style="height: calc(100vh - 300px)">
|
||||
<el-tree
|
||||
:data="categories"
|
||||
node-key="id"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
default-expand-all
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
|
||||
<el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900">
|
||||
<div class="gva-table-box mt-0 mb-0">
|
||||
<warning-bar title="点击“文件名”可以编辑;选择的类别即是上传的类别。" />
|
||||
<div class="gva-btn-list gap-3">
|
||||
<upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
|
||||
<cropper-image :classId="search.classId" @on-success="onSuccess" />
|
||||
<QRCodeUpload :classId="search.classId" @on-success="onSuccess" />
|
||||
<upload-image
|
||||
:image-url="imageUrl"
|
||||
:file-size="512"
|
||||
:max-w-h="1080"
|
||||
:classId="search.classId"
|
||||
@on-success="onSuccess"
|
||||
/>
|
||||
<el-button type="primary" icon="upload" @click="importUrlFunc">
|
||||
导入URL
|
||||
</el-button>
|
||||
<el-input
|
||||
v-model="search.keyword"
|
||||
class="w-72"
|
||||
placeholder="请输入文件名或备注"
|
||||
/>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询
|
||||
</el-button
|
||||
>
|
||||
</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" preview/>
|
||||
</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="cursor-pointer" @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?.toLowerCase() === 'jpg' ? 'info' : '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
|
||||
icon="download"
|
||||
type="primary"
|
||||
link
|
||||
@click="downloadFile(scope.row)"
|
||||
>下载
|
||||
</el-button
|
||||
>
|
||||
<el-button
|
||||
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>
|
||||
</div>
|
||||
|
||||
<!-- 添加分类弹窗 -->
|
||||
<el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
|
||||
:title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
|
||||
draggable
|
||||
>
|
||||
<el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
|
||||
<el-form-item label="上级分类">
|
||||
<el-tree-select
|
||||
v-model="categoryFormData.pid"
|
||||
:data="categories"
|
||||
check-strictly
|
||||
:props="defaultProps"
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="closeAddCategoryDialog">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getFileList,
|
||||
deleteFile,
|
||||
editFileName,
|
||||
importURL
|
||||
} 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 {CreateUUID, formatDate} from '@/utils/format'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
|
||||
import {ref} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {addCategory, deleteCategory, getCategoryList} from "@/api/attachmentCategory";
|
||||
import CropperImage from "@/components/upload/cropper.vue";
|
||||
import QRCodeUpload from "@/components/upload/QR-code.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'Upload'
|
||||
})
|
||||
|
||||
const fullscreenLoading = ref(false)
|
||||
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({
|
||||
keyword: null,
|
||||
classId: 0
|
||||
})
|
||||
const tableData = ref([])
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
search.value.classId = 0
|
||||
page.value = 1
|
||||
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--
|
||||
}
|
||||
await 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 {
|
||||
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: '编辑成功!'
|
||||
})
|
||||
await getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '取消修改'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入URL
|
||||
*/
|
||||
const importUrlFunc = () => {
|
||||
ElMessageBox.prompt('格式:文件名|链接或者仅链接。', '导入', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder:
|
||||
'我的图片|https://my-oss.com/my.png\nhttps://my-oss.com/my_1.png',
|
||||
inputPattern: /\S/,
|
||||
inputErrorMessage: '不能为空'
|
||||
})
|
||||
.then(async ({value}) => {
|
||||
let data = value.split('\n')
|
||||
let importData = []
|
||||
data.forEach((item) => {
|
||||
let oneData = item.trim().split('|')
|
||||
let url, name
|
||||
if (oneData.length > 1) {
|
||||
name = oneData[0].trim()
|
||||
url = oneData[1]
|
||||
} else {
|
||||
url = oneData[0].trim()
|
||||
let str = url.substring(url.lastIndexOf('/') + 1)
|
||||
name = str.substring(0, str.lastIndexOf('.'))
|
||||
}
|
||||
if (url) {
|
||||
importData.push({
|
||||
name: name,
|
||||
url: url,
|
||||
classId: search.value.classId,
|
||||
tag: url.substring(url.lastIndexOf('.') + 1),
|
||||
key: CreateUUID()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const res = await importURL(importData)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '导入成功!'
|
||||
})
|
||||
await getTableData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '取消导入'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const onSuccess = () => {
|
||||
search.value.keyword = null
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: 'name',
|
||||
value: 'ID'
|
||||
}
|
||||
|
||||
const categories = ref([])
|
||||
const fetchCategories = async () => {
|
||||
const res = await getCategoryList()
|
||||
let data = {
|
||||
name: '全部分类',
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
children:[]
|
||||
}
|
||||
if (res.code === 0) {
|
||||
categories.value = res.data || []
|
||||
categories.value.unshift(data)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNodeClick = (node) => {
|
||||
search.value.keyword = null
|
||||
search.value.classId = node.ID
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const categoryDialogVisible = ref(false)
|
||||
const categoryFormData = ref({
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const categoryForm = ref(null)
|
||||
const rules = ref({
|
||||
name: [
|
||||
{required: true, message: '请输入分类名称', trigger: 'blur'},
|
||||
{max: 20, message: '最多20位字符', trigger: 'blur'}
|
||||
]
|
||||
})
|
||||
|
||||
const addCategoryFun = (category) => {
|
||||
categoryDialogVisible.value = true
|
||||
categoryFormData.value.ID = 0
|
||||
categoryFormData.value.pid = category.ID
|
||||
}
|
||||
|
||||
const editCategory = (category) => {
|
||||
categoryFormData.value = {
|
||||
ID: category.ID,
|
||||
pid: category.pid,
|
||||
name: category.name
|
||||
}
|
||||
categoryDialogVisible.value = true
|
||||
}
|
||||
|
||||
const deleteCategoryFun = async (id) => {
|
||||
const res = await deleteCategory({id: id})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success({type: 'success', message: '删除成功'})
|
||||
await fetchCategories()
|
||||
}
|
||||
}
|
||||
|
||||
const confirmAddCategory = async () => {
|
||||
categoryForm.value.validate(async valid => {
|
||||
if (valid) {
|
||||
const res = await addCategory(categoryFormData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({type: 'success', message: '操作成功'})
|
||||
await fetchCategories()
|
||||
closeAddCategoryDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const closeAddCategoryDialog = () => {
|
||||
categoryDialogVisible.value = false
|
||||
categoryFormData.value = {
|
||||
ID: 0,
|
||||
pid: 0,
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
|
||||
fetchCategories()
|
||||
</script>
|
||||
368
src/view/init/index.vue
Normal file
368
src/view/init/index.vue
Normal file
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<div
|
||||
class="rounded-lg flex items-center justify-evenly w-full h-full relative md:w-screen md:h-screen md:bg-[#194bfb] overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="rounded-md w-full h-full flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="oblique h-[130%] w-3/5 bg-white dark:bg-slate-900 transform -rotate-12 absolute -ml-80"
|
||||
/>
|
||||
<div
|
||||
v-if="!page.showForm"
|
||||
:class="[page.showReadme ? 'slide-out-right' : 'slide-in-fwd-top']"
|
||||
>
|
||||
<div class="text-lg">
|
||||
<div
|
||||
class="font-sans text-4xl font-bold text-center mb-4 dark:text-white"
|
||||
>
|
||||
GIN-VUE-ADMIN
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">初始化须知</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">
|
||||
1.您需有用一定的VUE和GOLANG基础
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">
|
||||
2.请您确认是否已经阅读过<a
|
||||
class="text-blue-600 font-bold"
|
||||
href="https://www.gin-vue-admin.com"
|
||||
target="_blank"
|
||||
>官方文档</a
|
||||
>
|
||||
<a
|
||||
class="text-blue-600 font-bold"
|
||||
href="https://www.bilibili.com/video/BV1kv4y1g7nT?p=2"
|
||||
target="_blank"
|
||||
>初始化视频</a
|
||||
>
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">
|
||||
3.请您确认是否了解后续的配置流程
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">
|
||||
4.如果您使用mysql数据库,请确认数据库引擎为<span
|
||||
class="text-red-600 font-bold text-3xl ml-2"
|
||||
>innoDB</span
|
||||
>
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-300 mb-2">
|
||||
注:开发组不为文档中书写过的内容提供无偿服务
|
||||
</p>
|
||||
<p class="flex items-center justify-between mt-8">
|
||||
<el-button type="primary" size="large" @click="goDoc">
|
||||
阅读文档
|
||||
</el-button>
|
||||
<el-button type="primary" size="large" @click="showNext">
|
||||
我已确认
|
||||
</el-button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="page.showForm"
|
||||
:class="[page.showForm ? 'slide-in-left' : 'slide-out-right']"
|
||||
class="w-96"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" label-width="100px" size="large">
|
||||
<el-form-item label="管理员密码">
|
||||
<el-input
|
||||
v-model="form.adminPassword"
|
||||
placeholder="admin账号的默认密码"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="数据库类型">
|
||||
<el-select
|
||||
v-model="form.dbType"
|
||||
placeholder="请选择"
|
||||
class="w-full"
|
||||
@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-option key="sqlite" label="sqlite" value="sqlite" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.dbType !== 'sqlite'" label="host">
|
||||
<el-input v-model="form.host" placeholder="请输入数据库链接" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.dbType !== 'sqlite'" label="port">
|
||||
<el-input v-model="form.port" placeholder="请输入数据库端口" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.dbType !== 'sqlite'" label="userName">
|
||||
<el-input
|
||||
v-model="form.userName"
|
||||
placeholder="请输入数据库用户名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.dbType !== 'sqlite'" 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 v-if="form.dbType === 'sqlite'" label="dbPath">
|
||||
<el-input
|
||||
v-model="form.dbPath"
|
||||
placeholder="请输入sqlite数据库文件存放路径"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.dbType === 'pgsql'" label="template">
|
||||
<el-input
|
||||
v-model="form.template"
|
||||
placeholder="请输入postgresql指定template"
|
||||
/>
|
||||
</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 class="hidden md:block w-1/2 h-full float-right bg-[#194bfb]">
|
||||
<img class="h-full" src="@/assets/login_right_banner.jpg" alt="banner" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
defineOptions({
|
||||
name: 'Init'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const page = reactive({
|
||||
showReadme: false,
|
||||
showForm: false
|
||||
})
|
||||
|
||||
const showNext = () => {
|
||||
page.showReadme = false
|
||||
setTimeout(() => {
|
||||
page.showForm = true
|
||||
}, 20)
|
||||
}
|
||||
|
||||
const goDoc = () => {
|
||||
window.open('https://www.gin-vue-admin.com/guide/start-quickly/env.html')
|
||||
}
|
||||
|
||||
const out = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
adminPassword: '123456',
|
||||
dbType: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: '3306',
|
||||
userName: 'root',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: ''
|
||||
})
|
||||
|
||||
const changeDB = (val) => {
|
||||
switch (val) {
|
||||
case 'mysql':
|
||||
Object.assign(form, {
|
||||
adminPassword: '123456',
|
||||
reAdminPassword: '',
|
||||
dbType: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: '3306',
|
||||
userName: 'root',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: ''
|
||||
})
|
||||
break
|
||||
case 'pgsql':
|
||||
Object.assign(form, {
|
||||
adminPassword: '123456',
|
||||
dbType: 'pgsql',
|
||||
host: '127.0.0.1',
|
||||
port: '5432',
|
||||
userName: 'postgres',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: '',
|
||||
template: 'template0'
|
||||
})
|
||||
break
|
||||
case 'oracle':
|
||||
Object.assign(form, {
|
||||
adminPassword: '123456',
|
||||
dbType: 'oracle',
|
||||
host: '127.0.0.1',
|
||||
port: '1521',
|
||||
userName: 'oracle',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: ''
|
||||
})
|
||||
break
|
||||
case 'mssql':
|
||||
Object.assign(form, {
|
||||
adminPassword: '123456',
|
||||
dbType: 'mssql',
|
||||
host: '127.0.0.1',
|
||||
port: '1433',
|
||||
userName: 'mssql',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: ''
|
||||
})
|
||||
break
|
||||
case 'sqlite':
|
||||
Object.assign(form, {
|
||||
adminPassword: '123456',
|
||||
dbType: 'sqlite',
|
||||
host: '',
|
||||
port: '',
|
||||
userName: '',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: ''
|
||||
})
|
||||
break
|
||||
default:
|
||||
Object.assign(form, {
|
||||
adminPassword: '123456',
|
||||
dbType: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: '3306',
|
||||
userName: 'root',
|
||||
password: '',
|
||||
dbName: 'gva',
|
||||
dbPath: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
const onSubmit = async () => {
|
||||
if (form.adminPassword.length < 6) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '密码长度不能小于6位'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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 (_) {
|
||||
loading.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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% {
|
||||
transform: translateZ(-1400px) translateY(-800px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateZ(0) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slide-in-fwd-top {
|
||||
0% {
|
||||
transform: translateZ(-1400px) translateY(-800px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateZ(0) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes slide-out-right {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(1000px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes slide-out-right {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(1000px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes slide-in-left {
|
||||
0% {
|
||||
transform: translateX(-1000px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes slide-in-left {
|
||||
0% {
|
||||
transform: translateX(-1000px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 750px) {
|
||||
.form {
|
||||
width: 94vw !important;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
src/view/layout/aside/asideComponent/asyncSubmenu.vue
Normal file
66
src/view/layout/aside/asideComponent/asyncSubmenu.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<el-sub-menu
|
||||
ref="subMenu"
|
||||
:index="routerInfo.name"
|
||||
class="gva-sub-menu dark:text-slate-300 relative"
|
||||
>
|
||||
<template #title>
|
||||
<div
|
||||
v-if="!isCollapse"
|
||||
class="flex items-center"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<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 setup>
|
||||
import { inject, computed } from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'AsyncSubmenu'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
routerInfo: {
|
||||
default: function () {
|
||||
return null
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const isCollapse = inject('isCollapse', {
|
||||
default: false
|
||||
})
|
||||
|
||||
const sideHeight = computed(() => {
|
||||
return config.value.layout_side_item_height + 'px'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.gva-sub-menu {
|
||||
.el-sub-menu__title {
|
||||
height: v-bind('sideHeight') !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
47
src/view/layout/aside/asideComponent/index.vue
Normal file
47
src/view/layout/aside/asideComponent/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<component
|
||||
:is="menuComponent"
|
||||
v-if="!routerInfo.hidden"
|
||||
:router-info="routerInfo"
|
||||
>
|
||||
<template v-if="routerInfo.children && routerInfo.children.length">
|
||||
<AsideComponent
|
||||
v-for="item in routerInfo.children"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MenuItem from './menuItem.vue'
|
||||
import AsyncSubmenu from './asyncSubmenu.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'AsideComponent'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
routerInfo: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'vertical'
|
||||
}
|
||||
})
|
||||
|
||||
const menuComponent = computed(() => {
|
||||
if (
|
||||
props.routerInfo.children &&
|
||||
props.routerInfo.children.filter((item) => !item.hidden).length
|
||||
) {
|
||||
return AsyncSubmenu
|
||||
} else {
|
||||
return MenuItem
|
||||
}
|
||||
})
|
||||
</script>
|
||||
43
src/view/layout/aside/asideComponent/menuItem.vue
Normal file
43
src/view/layout/aside/asideComponent/menuItem.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<el-menu-item
|
||||
:index="routerInfo.name"
|
||||
class="dark:text-slate-300 overflow-hidden"
|
||||
:style="{
|
||||
height: sideHeight
|
||||
}"
|
||||
>
|
||||
<el-icon v-if="routerInfo.meta.icon">
|
||||
<component :is="routerInfo.meta.icon" />
|
||||
</el-icon>
|
||||
<template #title>
|
||||
{{ routerInfo.meta.title }}
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'MenuItem'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
routerInfo: {
|
||||
default: function () {
|
||||
return null
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const sideHeight = computed(() => {
|
||||
return config.value.layout_side_item_height + 'px'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
146
src/view/layout/aside/combinationMode.vue
Normal file
146
src/view/layout/aside/combinationMode.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-if="mode === 'head'"
|
||||
class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="routerStore.topActive"
|
||||
mode="horizontal"
|
||||
class="border-r-0 border-b-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
|
||||
unique-opened
|
||||
@select="(index, _, ele) => selectMenuItem(index, _, ele, true)"
|
||||
>
|
||||
<template v-for="item in routerStore.topMenu">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
mode="horizontal"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div
|
||||
v-if="mode === 'normal'"
|
||||
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
|
||||
:class="isCollapse ? '' : ' px-2'"
|
||||
:style="{
|
||||
width: layoutSideWidth + 'px'
|
||||
}"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="(index, _, ele) => selectMenuItem(index, _, ele, false)"
|
||||
>
|
||||
<template v-for="item in routerStore.leftMenu">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<div
|
||||
class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer"
|
||||
:class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<el-icon v-if="!isCollapse">
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaAside'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'normal'
|
||||
}
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
const layoutSideWidth = computed(() => {
|
||||
if (!isCollapse.value) {
|
||||
return config.value.layout_side_width
|
||||
} else {
|
||||
return config.value.layout_side_collapsed_width
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
const selectMenuItem = (index, _, ele, top) => {
|
||||
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, '_blank')
|
||||
return
|
||||
}
|
||||
|
||||
if (!top) {
|
||||
router.push({ name: index, query, params })
|
||||
return
|
||||
}
|
||||
const leftMenu = routerStore.setLeftMenu(index)
|
||||
if (!leftMenu) {
|
||||
router.push({ name: index, query, params })
|
||||
return;
|
||||
}
|
||||
const firstMenu = leftMenu.find((item) => !item.hidden && item.path.indexOf("http://") === -1 && item.path.indexOf("https://") === -1)
|
||||
router.push({ name: firstMenu.name, query, params })
|
||||
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
</script>
|
||||
106
src/view/layout/aside/headMode.vue
Normal file
106
src/view/layout/aside/headMode.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto"
|
||||
>
|
||||
<el-menu
|
||||
:default-active="active"
|
||||
mode="horizontal"
|
||||
class="border-r-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
|
||||
unique-opened
|
||||
@select="selectMenuItem"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0].children">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
mode="horizontal"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { device } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaAside'
|
||||
})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
const selectMenuItem = (index) => {
|
||||
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) {
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-menu--horizontal.el-menu,
|
||||
.el-menu--horizontal > .el-menu-item.is-active {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--el-color-primary-light-8) !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--el-color-primary-bg) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
src/view/layout/aside/index.vue
Normal file
34
src/view/layout/aside/index.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<normal-mode
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
"
|
||||
/>
|
||||
<head-mode v-if="config.side_mode === 'head' && device !== 'mobile'" />
|
||||
<combination-mode
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
:mode="mode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NormalMode from './normalMode.vue'
|
||||
import HeadMode from './headMode.vue'
|
||||
import CombinationMode from './combinationMode.vue'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'normal'
|
||||
}
|
||||
})
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '@/pinia'
|
||||
const appStore = useAppStore()
|
||||
const { config, device } = storeToRefs(appStore)
|
||||
</script>
|
||||
120
src/view/layout/aside/normalMode.vue
Normal file
120
src/view/layout/aside/normalMode.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
|
||||
:class="isCollapse ? '' : ' px-2'"
|
||||
:style="{
|
||||
width: layoutSideWidth + 'px'
|
||||
}"
|
||||
>
|
||||
<el-scrollbar>
|
||||
<el-menu
|
||||
:collapse="isCollapse"
|
||||
:collapse-transition="false"
|
||||
:default-active="active"
|
||||
class="border-r-0 w-full"
|
||||
unique-opened
|
||||
@select="selectMenuItem"
|
||||
>
|
||||
<template v-for="item in routerStore.asyncRouters[0]?.children || []">
|
||||
<aside-component
|
||||
v-if="!item.hidden"
|
||||
:key="item.name"
|
||||
:router-info="item"
|
||||
/>
|
||||
</template>
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
<div
|
||||
class="absolute bottom-8 right-2 w-8 h-8 bg-gray-50 dark:bg-slate-800 flex items-center justify-center rounded cursor-pointer"
|
||||
:class="isCollapse ? 'right-0 left-0 mx-auto' : 'right-2'"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<el-icon v-if="!isCollapse">
|
||||
<DArrowLeft />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<DArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AsideComponent from '@/view/layout/aside/asideComponent/index.vue'
|
||||
import { ref, provide, watchEffect, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaAside'
|
||||
})
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const routerStore = useRouterStore()
|
||||
const isCollapse = ref(false)
|
||||
const active = ref('')
|
||||
const layoutSideWidth = computed(() => {
|
||||
if (!isCollapse.value) {
|
||||
return config.value.layout_side_width
|
||||
} else {
|
||||
return config.value.layout_side_collapsed_width
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
if (route.name === 'Iframe') {
|
||||
active.value = decodeURIComponent(route.query.url)
|
||||
return
|
||||
}
|
||||
active.value = route.meta.activeName || route.name
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (device.value === 'mobile') {
|
||||
isCollapse.value = true
|
||||
} else {
|
||||
isCollapse.value = false
|
||||
}
|
||||
})
|
||||
|
||||
provide('isCollapse', isCollapse)
|
||||
|
||||
const selectMenuItem = (index) => {
|
||||
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) {
|
||||
if (index === 'Iframe') {
|
||||
query.url = decodeURIComponent(index)
|
||||
router.push({
|
||||
name: 'Iframe',
|
||||
query,
|
||||
params
|
||||
})
|
||||
return
|
||||
} else {
|
||||
window.open(index, '_blank')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
router.push({ name: index, query, params })
|
||||
}
|
||||
}
|
||||
|
||||
const toggleCollapse = () => {
|
||||
isCollapse.value = !isCollapse.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
140
src/view/layout/header/index.vue
Normal file
140
src/view/layout/header/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/7
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex justify-between fixed top-0 left-0 right-0 z-10 h-16 bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700 items-center px-2"
|
||||
>
|
||||
<div class="flex items-center cursor-pointer flex-1">
|
||||
<div
|
||||
class="flex items-center cursor-pointer"
|
||||
:class="isMobile ? '' : 'min-w-48'"
|
||||
@click="router.push({ path: '/' })"
|
||||
>
|
||||
<img
|
||||
alt
|
||||
class="h-12 bg-white rounded-full"
|
||||
:src="$GIN_VUE_ADMIN.appLogo"
|
||||
/>
|
||||
<div
|
||||
v-if="!isMobile"
|
||||
class="inline-flex font-bold text-2xl ml-2"
|
||||
:class="
|
||||
(config.side_mode === 'head' ||
|
||||
config.side_mode === 'combination') &&
|
||||
'min-w-fit'
|
||||
"
|
||||
>
|
||||
{{ $GIN_VUE_ADMIN.appName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-breadcrumb
|
||||
v-show="!isMobile"
|
||||
v-if="config.side_mode !== 'head' && config.side_mode !== 'combination'"
|
||||
class="ml-4"
|
||||
>
|
||||
<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>
|
||||
<gva-aside
|
||||
v-if="config.side_mode === 'head' && !isMobile"
|
||||
class="flex-1"
|
||||
/>
|
||||
<gva-aside
|
||||
v-if="config.side_mode === 'combination' && !isMobile"
|
||||
mode="head"
|
||||
class="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-2 flex items-center">
|
||||
<tools />
|
||||
<el-dropdown>
|
||||
<div class="flex justify-center items-center h-full w-full">
|
||||
<span
|
||||
class="cursor-pointer flex justify-center items-center text-black dark:text-gray-100"
|
||||
>
|
||||
<CustomPic />
|
||||
<span v-show="!isMobile" class="w-16">{{
|
||||
userStore.userInfo.nickName
|
||||
}}</span>
|
||||
<el-icon>
|
||||
<arrow-down />
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<span class="font-bold">
|
||||
当前角色:{{ 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import tools from './tools.vue'
|
||||
import CustomPic from '@/components/customPic/index.vue'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { setUserAuthority } from '@/api/user'
|
||||
import { fmtTitle } from '@/utils/fmtRouterTitle'
|
||||
import gvaAside from '@/view/layout/aside/index.vue'
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
const { device, config } = storeToRefs(appStore)
|
||||
const isMobile = computed(() => {
|
||||
return device.value === 'mobile'
|
||||
})
|
||||
const toPerson = () => {
|
||||
router.push({ name: 'person' })
|
||||
}
|
||||
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.sessionStorage.setItem('needToHome', 'true')
|
||||
window.location.reload()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
192
src/view/layout/header/tools.vue
Normal file
192
src/view/layout/header/tools.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/7
|
||||
!-->
|
||||
|
||||
<template>
|
||||
<div class="flex items-center mx-4 gap-4">
|
||||
<el-tooltip class="" effect="dark" content="视频教程" placement="bottom">
|
||||
<el-dropdown @command="toDoc">
|
||||
<el-icon
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
>
|
||||
<Film />
|
||||
</el-icon>
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in videoList"
|
||||
:key="item.link"
|
||||
:command="item.link"
|
||||
>{{ item.title }}</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="" effect="dark" content="搜索" placement="bottom">
|
||||
<el-icon
|
||||
@click="handleCommand"
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
>
|
||||
<Search />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="" effect="dark" content="系统设置" placement="bottom">
|
||||
<el-icon
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
@click="toggleSetting"
|
||||
>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip class="" effect="dark" content="刷新" placement="bottom">
|
||||
<el-icon
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
|
||||
:class="showRefreshAnmite ? 'animate-spin' : ''"
|
||||
@click="toggleRefresh"
|
||||
>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
class=""
|
||||
effect="dark"
|
||||
content="切换主题"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-icon
|
||||
v-if="appStore.isDark"
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-600 cursor-pointer border-solid"
|
||||
@click="appStore.toggleTheme(false)"
|
||||
>
|
||||
<Sunny />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
v-else
|
||||
class="w-8 h-8 shadow rounded-full border border-gray-200 cursor-pointer border-solid"
|
||||
@click="appStore.toggleTheme(true)"
|
||||
>
|
||||
<Moon />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
|
||||
<gva-setting v-model:drawer="showSettingDrawer"></gva-setting>
|
||||
<command-menu ref="command" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/pinia'
|
||||
import GvaSetting from '@/view/layout/setting/index.vue'
|
||||
import { ref } from 'vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import CommandMenu from '@/components/commandMenu/index.vue'
|
||||
import { toDoc } from '@/utils/doc'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const showSettingDrawer = ref(false)
|
||||
const showRefreshAnmite = ref(false)
|
||||
const toggleRefresh = () => {
|
||||
showRefreshAnmite.value = true
|
||||
emitter.emit('reload')
|
||||
setTimeout(() => {
|
||||
showRefreshAnmite.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const toggleSetting = () => {
|
||||
showSettingDrawer.value = true
|
||||
}
|
||||
|
||||
const first = ref('')
|
||||
const command = ref()
|
||||
|
||||
const handleCommand = () => {
|
||||
command.value.open()
|
||||
}
|
||||
const initPage = () => {
|
||||
// 判断当前用户的操作系统
|
||||
if (window.localStorage.getItem('osType') === 'WIN') {
|
||||
first.value = 'Ctrl'
|
||||
} else {
|
||||
first.value = '⌘'
|
||||
}
|
||||
// 当用户同时按下ctrl和k键的时候
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.ctrlKey && e.key === 'k') {
|
||||
// 阻止浏览器默认事件
|
||||
e.preventDefault()
|
||||
handleCommand()
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
|
||||
initPage()
|
||||
|
||||
const videoList = [
|
||||
{
|
||||
title: '1.clone项目和安装依赖',
|
||||
link: 'https://www.bilibili.com/video/BV1jx4y1s7xx'
|
||||
},
|
||||
{
|
||||
title: '2.初始化项目',
|
||||
link: 'https://www.bilibili.com/video/BV1sr421K7sv'
|
||||
},
|
||||
{
|
||||
title: '3.开启调试工具+创建初始化包',
|
||||
link: 'https://www.bilibili.com/video/BV1iH4y1c7Na'
|
||||
},
|
||||
{
|
||||
title: '4.手动使用自动化创建功能',
|
||||
link: 'https://www.bilibili.com/video/BV1UZ421T7fV'
|
||||
},
|
||||
{
|
||||
title: '5.使用已有表格创建业务',
|
||||
link: 'https://www.bilibili.com/video/BV1NE4m1977s'
|
||||
},
|
||||
{
|
||||
title: '6.使用AI创建业务和创建数据源模式的可选项',
|
||||
link: 'https://www.bilibili.com/video/BV17i421a7DE'
|
||||
},
|
||||
{
|
||||
title: '7.创建自己的后端方法',
|
||||
link: 'https://www.bilibili.com/video/BV1Yw4m1k7fg'
|
||||
},
|
||||
{
|
||||
title: '8.新增一个前端页面',
|
||||
link: 'https://www.bilibili.com/video/BV12y411i7oE'
|
||||
},
|
||||
{
|
||||
title: '9.配置一个前端二级页面',
|
||||
link: 'https://www.bilibili.com/video/BV1ZM4m1y7i3'
|
||||
},
|
||||
{
|
||||
title: '10.配置一个前端菜单参数',
|
||||
link: 'https://www.bilibili.com/video/BV1WS42197DZ'
|
||||
},
|
||||
{
|
||||
title: '11.菜单参数实战+动态菜单标题+菜单高亮配置',
|
||||
link: 'https://www.bilibili.com/video/BV1NE4m1979c'
|
||||
},
|
||||
{
|
||||
title: '12.增加菜单可控按钮',
|
||||
link: 'https://www.bilibili.com/video/BV1Sw4m1k746'
|
||||
},
|
||||
{
|
||||
title: '14.新增客户角色和其相关配置教学',
|
||||
link: 'https://www.bilibili.com/video/BV1Ki421a7X2'
|
||||
},
|
||||
{
|
||||
title: '15.发布项目上线',
|
||||
link: 'https://www.bilibili.com/video/BV1Lx4y1s77D'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
73
src/view/layout/iframe.vue
Normal file
73
src/view/layout/iframe.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800 w-screen h-screen"
|
||||
>
|
||||
<iframe
|
||||
v-if="reloadFlag"
|
||||
id="gva-base-load-dom"
|
||||
class="gva-body-h bg-gray-50 dark:bg-slate-800 w-full border-t border-gray-200 dark:border-slate-700"
|
||||
:src="url"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useResponsive from '@/hooks/responsive'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, onMounted, nextTick, reactive, watchEffect } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
const appStore = useAppStore()
|
||||
const { isDark } = storeToRefs(appStore)
|
||||
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaLayoutIframe'
|
||||
})
|
||||
|
||||
useResponsive(true)
|
||||
const font = reactive({
|
||||
color: 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
font.color = isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const url = route.query.url || 'https://www.gin-vue-admin.com'
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载一些通用的事件
|
||||
emitter.on('reload', reload)
|
||||
if (userStore.loadingInstance) {
|
||||
userStore.loadingInstance.close()
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
114
src/view/layout/index.vue
Normal file
114
src/view/layout/index.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800 w-screen h-screen"
|
||||
>
|
||||
<el-watermark
|
||||
v-if="config.show_watermark"
|
||||
:font="font"
|
||||
:z-index="9999"
|
||||
:gap="[180, 150]"
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
:content="userStore.userInfo.nickName"
|
||||
/>
|
||||
<gva-header />
|
||||
<div class="flex flex-row w-full gva-container pt-16 box-border h-full">
|
||||
<gva-aside
|
||||
v-if="
|
||||
config.side_mode === 'normal' ||
|
||||
(device === 'mobile' && config.side_mode == 'head') ||
|
||||
(device === 'mobile' && config.side_mode == 'combination')
|
||||
"
|
||||
/>
|
||||
<gva-aside
|
||||
v-if="config.side_mode === 'combination' && device !== 'mobile'"
|
||||
mode="normal"
|
||||
/>
|
||||
<div class="flex-1 px-2 w-0 h-full">
|
||||
<gva-tabs v-if="config.showTabs" />
|
||||
<div
|
||||
class="overflow-auto"
|
||||
:class="config.showTabs ? 'gva-container2' : 'gva-container pt-1'"
|
||||
>
|
||||
<router-view v-if="reloadFlag" v-slot="{ Component, route }">
|
||||
<div
|
||||
id="gva-base-load-dom"
|
||||
class="gva-body-h bg-gray-50 dark:bg-slate-800"
|
||||
>
|
||||
<transition mode="out-in" :name="config.transition_type">
|
||||
<keep-alive :include="routerStore.keepAliveRouters">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</div>
|
||||
</router-view>
|
||||
<BottomInfo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import GvaAside from '@/view/layout/aside/index.vue'
|
||||
import GvaHeader from '@/view/layout/header/index.vue'
|
||||
import useResponsive from '@/hooks/responsive'
|
||||
import GvaTabs from './tabs/index.vue'
|
||||
import BottomInfo from '@/components/bottomInfo/bottomInfo.vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { ref, onMounted, nextTick, reactive, watchEffect } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import '@/style/transition.scss'
|
||||
const appStore = useAppStore()
|
||||
const { config, isDark, device } = storeToRefs(appStore)
|
||||
|
||||
defineOptions({
|
||||
name: 'GvaLayout'
|
||||
})
|
||||
|
||||
useResponsive(true)
|
||||
const font = reactive({
|
||||
color: 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
font.color = isDark.value ? 'rgba(255,255,255, .15)' : 'rgba(0, 0, 0, .15)'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 挂载一些通用的事件
|
||||
emitter.on('reload', reload)
|
||||
if (userStore.loadingInstance) {
|
||||
userStore.loadingInstance.close()
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss"></style>
|
||||
62
src/view/layout/screenfull/index.vue
Normal file
62
src/view/layout/screenfull/index.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div @click="clickFull">
|
||||
<div v-if="isShow" class="gvaIcon gvaIcon-fullscreen-expand" />
|
||||
<div v-else class="gvaIcon gvaIcon-fullscreen-shrink" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import screenfull from 'screenfull' // 引入screenfull
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Screenfull'
|
||||
})
|
||||
|
||||
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>
|
||||
98
src/view/layout/search/search.vue
Normal file
98
src/view/layout/search/search.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="search-component items-center">
|
||||
<div
|
||||
class="gvaIcon gvaIcon-refresh"
|
||||
:class="[reload ? 'reloading' : '']"
|
||||
@click="handleReload"
|
||||
/>
|
||||
<Screenfull class="search-icon" />
|
||||
<div class="gvaIcon gvaIcon-customer-service" @click="toService" />
|
||||
<el-switch
|
||||
v-model="isDark"
|
||||
:active-action-icon="Moon"
|
||||
:inactive-action-icon="Sunny"
|
||||
@change="handleDarkSwitch"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Screenfull from '@/view/layout/screenfull/index.vue'
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { Sunny, Moon } from '@element-plus/icons-vue'
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'BtnBox'
|
||||
})
|
||||
const isDark = ref(localStorage.getItem('isDark') === 'true' || true)
|
||||
|
||||
watchEffect(() => {
|
||||
if (isDark.value) {
|
||||
document.documentElement.classList.add('dark')
|
||||
localStorage.setItem('isDark', true)
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.setItem('isDark', false)
|
||||
}
|
||||
})
|
||||
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')
|
||||
}
|
||||
|
||||
const handleDarkSwitch = (e) => {
|
||||
isDark.value = e
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.search-component {
|
||||
@apply inline-flex overflow-hidden text-center gap-5 mr-5 text-black dark:text-gray-100;
|
||||
div {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
.el-input__inner {
|
||||
@apply border-b border-solid border-gray-300;
|
||||
}
|
||||
.el-dropdown-link {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.reload {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.reloading {
|
||||
animation: turn 0.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes turn {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
212
src/view/layout/setting/index.vue
Normal file
212
src/view/layout/setting/index.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="drawer"
|
||||
title="系统配置"
|
||||
direction="rtl"
|
||||
:size="width"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">系统配置</span>
|
||||
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-8">
|
||||
<Title title="默认主题"></Title>
|
||||
<div class="mt-2 text-sm p-2 flex items-center justify-center gap-2">
|
||||
<el-segmented
|
||||
v-model="config.darkMode"
|
||||
:options="options"
|
||||
size="default"
|
||||
@change="appStore.toggleDarkMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<Title title="主题色"></Title>
|
||||
<div class="mt-2 text-sm p-2 flex items-center gap-2 justify-center">
|
||||
<div
|
||||
v-for="item in colors"
|
||||
:key="item"
|
||||
class="w-5 h-5 rounded cursor-pointer flex items-center justify-center"
|
||||
:style="`background:${item}`"
|
||||
@click="appStore.togglePrimaryColor(item)"
|
||||
>
|
||||
<el-icon v-if="config.primaryColor === item">
|
||||
<Select />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-color-picker
|
||||
v-model="customColor"
|
||||
@change="appStore.togglePrimaryColor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<Title title="主题配置"></Title>
|
||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>展示水印</div>
|
||||
<el-switch
|
||||
v-model="config.show_watermark"
|
||||
@change="appStore.toggleConfigWatermark"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>灰色模式</div>
|
||||
<el-switch v-model="config.grey" @change="appStore.toggleGrey" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>色弱模式</div>
|
||||
<el-switch
|
||||
v-model="config.weakness"
|
||||
@change="appStore.toggleWeakness"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>菜单模式</div>
|
||||
<el-segmented
|
||||
v-model="config.side_mode"
|
||||
:options="sideModes"
|
||||
size="default"
|
||||
@change="appStore.toggleSideMode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>显示标签页</div>
|
||||
<el-switch
|
||||
v-model="config.showTabs"
|
||||
@change="appStore.toggleTabs"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex-shrink-0">页面切换动画</div>
|
||||
<el-select
|
||||
v-model="config.transition_type"
|
||||
@change="appStore.toggleTransition"
|
||||
class="w-40"
|
||||
>
|
||||
<el-option value="fade" label="淡入淡出" />
|
||||
<el-option value="slide" label="滑动" />
|
||||
<el-option value="zoom" label="缩放" />
|
||||
<el-option value="none" label="无动画" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<Title title="layout 大小配置"></Title>
|
||||
<div class="mt-2 text-md p-2 flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏展开宽度</div>
|
||||
<el-input-number
|
||||
v-model="config.layout_side_width"
|
||||
:min="150"
|
||||
:max="400"
|
||||
:step="10"
|
||||
></el-input-number>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏收缩宽度</div>
|
||||
<el-input-number
|
||||
v-model="config.layout_side_collapsed_width"
|
||||
:min="60"
|
||||
:max="100"
|
||||
></el-input-number>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div>侧边栏子项高度</div>
|
||||
<el-input-number
|
||||
v-model="config.layout_side_item_height"
|
||||
:min="30"
|
||||
:max="50"
|
||||
></el-input-number>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <el-alert type="warning" :closable="false">
|
||||
请注意,所有配置请保存到本地文件的
|
||||
<el-tag>config.json</el-tag> 文件中,否则刷新页面后会丢失配置
|
||||
</el-alert>-->
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAppStore } from '@/pinia'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { setSelfSetting } from '@/api/user'
|
||||
import Title from './title.vue'
|
||||
const appStore = useAppStore()
|
||||
const { config, device } = storeToRefs(appStore)
|
||||
defineOptions({
|
||||
name: 'GvaSetting'
|
||||
})
|
||||
|
||||
const width = computed(() => {
|
||||
return device.value === 'mobile' ? '100%' : '500px'
|
||||
})
|
||||
|
||||
const colors = [
|
||||
'#EB2F96',
|
||||
'#3b82f6',
|
||||
'#2FEB54',
|
||||
'#EBEB2F',
|
||||
'#EB2F2F',
|
||||
'#2FEBEB'
|
||||
]
|
||||
|
||||
const drawer = defineModel('drawer', {
|
||||
default: true,
|
||||
type: Boolean
|
||||
})
|
||||
|
||||
const options = ['dark', 'light', 'auto']
|
||||
const sideModes = [
|
||||
{
|
||||
label: '正常模式',
|
||||
value: 'normal'
|
||||
},
|
||||
{
|
||||
label: '顶部菜单栏模式',
|
||||
value: 'head'
|
||||
},
|
||||
{
|
||||
label: '组合模式',
|
||||
value: 'combination'
|
||||
}
|
||||
]
|
||||
|
||||
const saveConfig = async () => {
|
||||
/*const input = document.createElement("textarea");
|
||||
input.value = JSON.stringify(config.value);
|
||||
// 添加回车
|
||||
input.value = input.value.replace(/,/g, ",\n");
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(input);
|
||||
ElMessage.success("复制成功, 请自行保存到本地文件中");*/
|
||||
const res = await setSelfSetting(config.value)
|
||||
if (res.code === 0) {
|
||||
localStorage.setItem('originSetting', JSON.stringify(config.value))
|
||||
ElMessage.success('保存成功')
|
||||
drawer.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const customColor = ref('')
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep(.el-drawer__header) {
|
||||
@apply border-gray-400 dark:border-gray-600;
|
||||
}
|
||||
</style>
|
||||
34
src/view/layout/setting/title.vue
Normal file
34
src/view/layout/setting/title.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="title relative my-2">
|
||||
<div class="flex-shrink-0 text-center text-xl text-gray-600">
|
||||
{{ title }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'layoutSettingTitle'
|
||||
})
|
||||
|
||||
defineProps({
|
||||
title: String
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.title::before,
|
||||
.title::after {
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: #e3e3e3;
|
||||
}
|
||||
</style>
|
||||
425
src/view/layout/tabs/index.vue
Normal file
425
src/view/layout/tabs/index.vue
Normal file
@@ -0,0 +1,425 @@
|
||||
<!--
|
||||
@auther: bypanghu<bypanghu@163.com>
|
||||
@date: 2024/5/7
|
||||
!-->
|
||||
<template>
|
||||
<div class="gva-tabs">
|
||||
<el-tabs
|
||||
v-model="activeValue"
|
||||
:closable="!(historys.length === 1 && $route.name === defaultRouter)"
|
||||
type="card"
|
||||
class="bg-white text-slate-700 dark:text-slate-500 dark:bg-slate-900"
|
||||
@contextmenu.prevent="openContextMenu($event)"
|
||||
@tab-click="changeTab"
|
||||
@tab-remove="removeTab"
|
||||
@click.middle.prevent="middleCloseTab($event)"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in historys"
|
||||
:key="getFmtString(item)"
|
||||
:label="item.meta.title"
|
||||
:name="getFmtString(item)"
|
||||
:tab="item"
|
||||
class="border-none"
|
||||
>
|
||||
<template #label>
|
||||
<span
|
||||
:tab="item"
|
||||
:class="
|
||||
activeValue === getFmtString(item)
|
||||
? 'text-active'
|
||||
: 'text-gray-600 dark:text-slate-400 '
|
||||
"
|
||||
><i
|
||||
:class="
|
||||
activeValue === getFmtString(item)
|
||||
? 'text-active'
|
||||
: 'text-gray-600 dark:text-slate-400'
|
||||
"
|
||||
/>
|
||||
{{ 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 setup>
|
||||
import { emitter } from '@/utils/bus.js'
|
||||
import { computed, onUnmounted, ref, watch, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import { fmtTitle } from '@/utils/fmtRouterTitle'
|
||||
|
||||
defineOptions({
|
||||
name: 'HistoryComponent'
|
||||
})
|
||||
|
||||
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 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
|
||||
|
||||
left.value = e.clientX
|
||||
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) => {
|
||||
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(getFmtString(route))
|
||||
})
|
||||
// 全局监听 关闭所有页面函数
|
||||
emitter.on('closeAllPage', () => {
|
||||
closeAll()
|
||||
})
|
||||
emitter.on('mobile', (data) => {
|
||||
isMobile.value = data
|
||||
})
|
||||
emitter.on('collapse', (data) => {
|
||||
isCollapse.value = data
|
||||
})
|
||||
|
||||
emitter.on('setQuery', (data) => {
|
||||
const index = historys.value.findIndex(
|
||||
(item) => getFmtString(item) === activeValue.value
|
||||
)
|
||||
historys.value[index].query = data
|
||||
activeValue.value = getFmtString(historys.value[index])
|
||||
const currentUrl = window.location.href.split('?')[0]
|
||||
const currentSearchParams = new URLSearchParams(data).toString()
|
||||
window.history.replaceState(
|
||||
{},
|
||||
'',
|
||||
`${currentUrl}?${currentSearchParams}`
|
||||
)
|
||||
sessionStorage.setItem('historys', JSON.stringify(historys.value))
|
||||
})
|
||||
|
||||
emitter.on('switchTab', async (data) => {
|
||||
const index = historys.value.findIndex((item) => item.name === data.name)
|
||||
if (index < 0) {
|
||||
return
|
||||
}
|
||||
for (const key in data.query) {
|
||||
data.query[key] = String(data.query[key])
|
||||
}
|
||||
for (const key in data.params) {
|
||||
data.params[key] = String(data.params[key])
|
||||
}
|
||||
|
||||
historys.value[index].query = data.query || {}
|
||||
historys.value[index].params = data.params || {}
|
||||
await nextTick()
|
||||
router.push(historys.value[index])
|
||||
})
|
||||
const initHistorys = [
|
||||
{
|
||||
name: defaultRouter.value,
|
||||
meta: {
|
||||
title: '首页'
|
||||
},
|
||||
query: {},
|
||||
params: {}
|
||||
}
|
||||
]
|
||||
setTab(route)
|
||||
historys.value =
|
||||
JSON.parse(sessionStorage.getItem('historys')) || initHistorys
|
||||
if (!window.sessionStorage.getItem('activeValue')) {
|
||||
activeValue.value = getFmtString(route)
|
||||
} else {
|
||||
activeValue.value = window.sessionStorage.getItem('activeValue')
|
||||
}
|
||||
if (window.sessionStorage.getItem('needCloseAll') === 'true') {
|
||||
closeAll()
|
||||
window.sessionStorage.removeItem('needCloseAll')
|
||||
}
|
||||
}
|
||||
initPage()
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off('collapse')
|
||||
emitter.off('mobile')
|
||||
})
|
||||
|
||||
const middleCloseTab = (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) {
|
||||
removeTab(id.substring(4))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.contextmenu {
|
||||
@apply bg-white dark:bg-slate-900 w-28 m-0 py-2.5 px-0 border border-gray-200 text-sm shadow-md rounded absolute z-50 border-solid dark:border-slate-800;
|
||||
}
|
||||
|
||||
.contextmenu li {
|
||||
@apply text-slate-700 dark:text-slate-200 text-base list-none px-4 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer;
|
||||
}
|
||||
|
||||
$base-tag-item-height: 4rem;
|
||||
|
||||
.gva-tabs {
|
||||
::v-deep(.el-tabs--card > .el-tabs__header) {
|
||||
border: none;
|
||||
}
|
||||
::v-deep(.el-tabs__nav-scroll) {
|
||||
padding: 4px 4px;
|
||||
}
|
||||
|
||||
::v-deep(.el-tabs__nav) {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
::v-deep(.el-tabs__header) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
::v-deep(.el-tabs__item) {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--el-border-color-darker);
|
||||
border-radius: 2px;
|
||||
margin-right: 5px;
|
||||
margin-left: 2px;
|
||||
transition: padding 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !important;
|
||||
height: 34px;
|
||||
&.is-active {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
::v-deep(.el-tabs__item):first-child {
|
||||
border: 1px solid var(--el-border-color-darker);
|
||||
&.is-active {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
238
src/view/login/index.vue
Normal file
238
src/view/login/index.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div id="userLayout" class="w-full h-full relative">
|
||||
<div
|
||||
class="rounded-lg flex items-center justify-evenly w-full h-full md:w-screen md:h-screen md:bg-[#194bfb] bg-white"
|
||||
>
|
||||
<div class="md:w-3/5 w-10/12 h-full flex items-center justify-evenly">
|
||||
<div
|
||||
class="oblique h-[130%] w-3/5 bg-white dark:bg-slate-900 transform -rotate-12 absolute -ml-52"
|
||||
/>
|
||||
<!-- 分割斜块 -->
|
||||
<div
|
||||
class="z-[999] pt-12 pb-10 md:w-96 w-full rounded-lg flex flex-col justify-between box-border"
|
||||
>
|
||||
<div>
|
||||
<div class="flex items-center justify-center">
|
||||
<img class="w-24" :src="$GIN_VUE_ADMIN.appLogo" alt />
|
||||
</div>
|
||||
<div class="mb-9">
|
||||
<p class="text-center text-4xl font-bold">
|
||||
{{ $GIN_VUE_ADMIN.appName }}
|
||||
</p>
|
||||
<p class="text-center text-sm font-normal text-gray-500 mt-2.5">
|
||||
A management platform using Golang and Vue
|
||||
</p>
|
||||
</div>
|
||||
<el-form
|
||||
ref="loginForm"
|
||||
:model="loginFormData"
|
||||
:rules="rules"
|
||||
:validate-on-rule-change="false"
|
||||
@keyup.enter="submitForm"
|
||||
>
|
||||
<el-form-item prop="username" class="mb-6">
|
||||
<el-input
|
||||
v-model="loginFormData.username"
|
||||
size="large"
|
||||
placeholder="请输入用户名"
|
||||
suffix-icon="user"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password" class="mb-6">
|
||||
<el-input
|
||||
v-model="loginFormData.password"
|
||||
show-password
|
||||
size="large"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="loginFormData.openCaptcha"
|
||||
prop="captcha"
|
||||
class="mb-6"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<el-input
|
||||
v-model="loginFormData.captcha"
|
||||
placeholder="请输入验证码"
|
||||
size="large"
|
||||
class="flex-1 mr-5"
|
||||
/>
|
||||
<div class="w-1/3 h-11 bg-[#c3d4f2] rounded">
|
||||
<img
|
||||
v-if="picPath"
|
||||
class="w-full h-full"
|
||||
:src="picPath"
|
||||
alt="请输入验证码"
|
||||
@click="loginVerify()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item class="mb-6">
|
||||
<el-button
|
||||
class="shadow shadow-active h-11 w-full"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="submitForm"
|
||||
>登 录</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item class="mb-6">
|
||||
<el-button
|
||||
class="shadow shadow-active h-11 w-full"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="checkInit"
|
||||
>前往初始化</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:block w-1/2 h-full float-right bg-[#194bfb]">
|
||||
<img
|
||||
class="h-full"
|
||||
src="@/assets/login_right_banner.jpg"
|
||||
alt="banner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomInfo class="left-0 right-0 absolute bottom-3 mx-auto w-full z-20">
|
||||
<div class="links items-center justify-center gap-2 hidden md:flex">
|
||||
<a href="https://www.gin-vue-admin.com/" target="_blank">
|
||||
<img src="@/assets/docs.png" class="w-8 h-8" alt="文档" />
|
||||
</a>
|
||||
<a href="https://support.qq.com/product/371961" target="_blank">
|
||||
<img src="@/assets/kefu.png" class="w-8 h-8" alt="客服" />
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/flipped-aurora/gin-vue-admin"
|
||||
target="_blank"
|
||||
>
|
||||
<img src="@/assets/github.png" class="w-8 h-8" alt="github" />
|
||||
</a>
|
||||
<a href="https://space.bilibili.com/322210472" target="_blank">
|
||||
<img src="@/assets/video.png" class="w-8 h-8" alt="视频站" />
|
||||
</a>
|
||||
</div>
|
||||
</BottomInfo>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { captcha } from '@/api/user'
|
||||
import { checkDB } from '@/api/initdb'
|
||||
import BottomInfo from '@/components/bottomInfo/bottomInfo.vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
|
||||
defineOptions({
|
||||
name: 'Login'
|
||||
})
|
||||
|
||||
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 = async () => {
|
||||
const ele = await captcha()
|
||||
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 loginForm = ref(null)
|
||||
const picPath = ref('')
|
||||
const loginFormData = reactive({
|
||||
username: 'admin',
|
||||
password: '',
|
||||
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) {
|
||||
// 未通过前端静态验证
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请正确填写登录信息',
|
||||
showClose: true
|
||||
})
|
||||
await loginVerify()
|
||||
return false
|
||||
}
|
||||
|
||||
// 通过验证,请求登陆
|
||||
const flag = await login()
|
||||
|
||||
// 登陆失败,刷新验证码
|
||||
if (!flag) {
|
||||
await loginVerify()
|
||||
return false
|
||||
}
|
||||
|
||||
// 登陆成功
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转初始化
|
||||
const checkInit = async () => {
|
||||
const res = await checkDB()
|
||||
if (res.code === 0) {
|
||||
if (res.data?.needInit) {
|
||||
userStore.NeedInit()
|
||||
await router.push({ name: 'Init' })
|
||||
} else {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '已配置数据库信息,无法初始化'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
631
src/view/person/person.vue
Normal file
631
src/view/person/person.vue
Normal file
@@ -0,0 +1,631 @@
|
||||
<template>
|
||||
<div class="profile-container">
|
||||
<!-- 顶部个人信息卡片 -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm mb-8">
|
||||
<!-- 顶部背景图 -->
|
||||
<div class="h-48 bg-blue-50 dark:bg-slate-600 relative">
|
||||
<div class="absolute inset-0 bg-pattern opacity-7"></div>
|
||||
</div>
|
||||
|
||||
<!-- 个人信息区 -->
|
||||
<div class="px-8 -mt-20 pb-8">
|
||||
<div class="flex flex-col lg:flex-row items-start gap-8">
|
||||
<!-- 左侧头像 -->
|
||||
<div class="profile-avatar-wrapper flex-shrink-0 mx-auto lg:mx-0">
|
||||
<SelectImage
|
||||
v-model="userStore.userInfo.headerImg"
|
||||
file-type="image"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 右侧信息 -->
|
||||
<div class="flex-1 pt-12 lg:pt-20 w-full">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row items-start lg:items-start justify-between gap-4"
|
||||
>
|
||||
<div class="lg:mt-4">
|
||||
<div class="flex items-center gap-4 mb-4">
|
||||
<div
|
||||
v-if="!editFlag"
|
||||
class="text-2xl font-bold flex items-center gap-3 text-gray-800 dark:text-gray-100"
|
||||
>
|
||||
{{ userStore.userInfo.nickName }}
|
||||
<el-icon
|
||||
class="cursor-pointer text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors duration-200"
|
||||
@click="openEdit"
|
||||
>
|
||||
<edit />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-else class="flex items-center">
|
||||
<el-input v-model="nickName" class="w-48 mr-4" />
|
||||
<el-button type="primary" plain @click="enterEdit">
|
||||
确认
|
||||
</el-button>
|
||||
<el-button type="danger" plain @click="closeEdit">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-col lg:flex-row items-start lg:items-center gap-4 lg:gap-8 text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><location /></el-icon>
|
||||
<span>中国·北京市·朝阳区</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><office-building /></el-icon>
|
||||
<span>北京翻转极光科技有限公司</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><user /></el-icon>
|
||||
<span>技术部·前端事业群</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 mt-4">
|
||||
<el-button type="primary" plain icon="message">
|
||||
发送消息
|
||||
</el-button>
|
||||
<el-button icon="share"> 分享主页 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要内容区 -->
|
||||
<div class="grid lg:grid-cols-12 md:grid-cols-1 gap-8">
|
||||
<!-- 左侧信息栏 -->
|
||||
<div class="lg:col-span-4">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 rounded-xl p-6 mb-6 profile-card"
|
||||
>
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<el-icon class="text-blue-500"><info-filled /></el-icon>
|
||||
基本信息
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
class="flex items-center gap-1 lg:gap-3 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<el-icon class="text-blue-500"><phone /></el-icon>
|
||||
<span class="font-medium">手机号码:</span>
|
||||
<span>{{ userStore.userInfo.phone || '未设置' }}</span>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="ml-auto"
|
||||
@click="changePhoneFlag = true"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1 lg:gap-3 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<el-icon class="text-green-500"><message /></el-icon>
|
||||
<span class="font-medium flex-shrink-0">邮箱地址:</span>
|
||||
<span>{{ userStore.userInfo.email || '未设置' }}</span>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="ml-auto"
|
||||
@click="changeEmailFlag = true"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1 lg:gap-3 text-gray-600 dark:text-gray-300"
|
||||
>
|
||||
<el-icon class="text-purple-500"><lock /></el-icon>
|
||||
<span class="font-medium">账号密码:</span>
|
||||
<span>已设置</span>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
class="ml-auto"
|
||||
@click="showPassword = true"
|
||||
>
|
||||
修改
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 profile-card">
|
||||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<el-icon class="text-blue-500"><medal /></el-icon>
|
||||
技能特长
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<el-tag effect="plain" type="success">GoLang</el-tag>
|
||||
<el-tag effect="plain" type="warning">JavaScript</el-tag>
|
||||
<el-tag effect="plain" type="danger">Vue</el-tag>
|
||||
<el-tag effect="plain" type="info">Gorm</el-tag>
|
||||
<el-button link class="text-sm">
|
||||
<el-icon><plus /></el-icon>
|
||||
添加技能
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="lg:col-span-8">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 profile-card">
|
||||
<el-tabs class="custom-tabs">
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><data-line /></el-icon>
|
||||
数据统计
|
||||
</div>
|
||||
</template>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 lg:gap-6 py-6">
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-blue-500 mb-2"
|
||||
>
|
||||
138
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">项目参与</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-green-500 mb-2"
|
||||
>
|
||||
2.3k
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">代码提交</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-purple-500 mb-2"
|
||||
>
|
||||
95%
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">任务完成</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div
|
||||
class="text-2xl lg:text-4xl font-bold text-yellow-500 mb-2"
|
||||
>
|
||||
12
|
||||
</div>
|
||||
<div class="text-gray-500 text-sm">获得勋章</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane>
|
||||
<template #label>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-icon><calendar /></el-icon>
|
||||
近期动态
|
||||
</div>
|
||||
</template>
|
||||
<div class="py-6">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="(activity, index) in activities"
|
||||
:key="index"
|
||||
:type="activity.type"
|
||||
:timestamp="activity.timestamp"
|
||||
:hollow="true"
|
||||
class="pb-6"
|
||||
>
|
||||
<h3 class="text-base font-medium mb-1">
|
||||
{{ activity.title }}
|
||||
</h3>
|
||||
<p class="text-gray-500 text-sm">{{ activity.content }}</p>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<el-dialog
|
||||
v-model="showPassword"
|
||||
title="修改密码"
|
||||
width="400px"
|
||||
class="custom-dialog"
|
||||
@close="clearPassword"
|
||||
>
|
||||
<el-form
|
||||
ref="modifyPwdForm"
|
||||
:model="pwdModify"
|
||||
:rules="rules"
|
||||
label-width="90px"
|
||||
class="py-4"
|
||||
>
|
||||
<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 @click="showPassword = false">取 消</el-button>
|
||||
<el-button type="primary" @click="savePassword">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="changePhoneFlag"
|
||||
title="修改手机号"
|
||||
width="400px"
|
||||
class="custom-dialog"
|
||||
>
|
||||
<el-form :model="phoneForm" label-width="80px" class="py-4">
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="phoneForm.phone" placeholder="请输入新的手机号码">
|
||||
<template #prefix>
|
||||
<el-icon><phone /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<div class="flex gap-4">
|
||||
<el-input
|
||||
v-model="phoneForm.code"
|
||||
placeholder="请输入验证码[模拟]"
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><key /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="time > 0"
|
||||
class="w-32"
|
||||
@click="getCode"
|
||||
>
|
||||
{{ time > 0 ? `${time}s` : '获取验证码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeChangePhone">取 消</el-button>
|
||||
<el-button type="primary" @click="changePhone">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="changeEmailFlag"
|
||||
title="修改邮箱"
|
||||
width="400px"
|
||||
class="custom-dialog"
|
||||
>
|
||||
<el-form :model="emailForm" label-width="80px" class="py-4">
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="emailForm.email" placeholder="请输入新的邮箱地址">
|
||||
<template #prefix>
|
||||
<el-icon><message /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码">
|
||||
<div class="flex gap-4">
|
||||
<el-input
|
||||
v-model="emailForm.code"
|
||||
placeholder="请输入验证码[模拟]"
|
||||
class="flex-1"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><key /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="emailTime > 0"
|
||||
class="w-32"
|
||||
@click="getEmailCode"
|
||||
>
|
||||
{{ emailTime > 0 ? `${emailTime}s` : '获取验证码' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeChangeEmail">取 消</el-button>
|
||||
<el-button type="primary" @click="changeEmail">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { setSelfInfo, changePassword } from '@/api/user.js'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useUserStore } from '@/pinia/modules/user'
|
||||
import SelectImage from '@/components/selectImage/selectImage.vue'
|
||||
defineOptions({
|
||||
name: 'Person'
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const modifyPwdForm = ref(null)
|
||||
const showPassword = ref(false)
|
||||
const pwdModify = ref({})
|
||||
const nickName = ref('')
|
||||
const editFlag = ref(false)
|
||||
|
||||
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 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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clearPassword = () => {
|
||||
pwdModify.value = {
|
||||
password: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
modifyPwdForm.value?.clearValidate()
|
||||
}
|
||||
|
||||
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.success('修改成功')
|
||||
}
|
||||
nickName.value = ''
|
||||
editFlag.value = false
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => userStore.userInfo.headerImg, async(val) => {
|
||||
const res = await setSelfInfo({ headerImg: val })
|
||||
if (res.code === 0) {
|
||||
userStore.ResetUserInfo({ headerImg: val })
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '设置成功',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 添加活动数据
|
||||
const activities = [
|
||||
{
|
||||
timestamp: '2024-01-10',
|
||||
title: '完成项目里程碑',
|
||||
content: '成功完成第三季度主要项目开发任务,获得团队一致好评',
|
||||
type: 'primary'
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-11',
|
||||
title: '代码审核完成',
|
||||
content: '完成核心模块代码审核,提出多项改进建议并获采纳',
|
||||
type: 'success'
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-12',
|
||||
title: '技术分享会',
|
||||
content: '主持团队技术分享会,分享前端性能优化经验',
|
||||
type: 'warning'
|
||||
},
|
||||
{
|
||||
timestamp: '2024-01-13',
|
||||
title: '新功能上线',
|
||||
content: '成功上线用户反馈的新特性,显著提升用户体验',
|
||||
type: 'danger'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.profile-container {
|
||||
@apply p-4 lg:p-6 min-h-screen bg-gray-50 dark:bg-slate-900;
|
||||
|
||||
.bg-pattern {
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23000000' fill-opacity='0.1'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
@apply shadow-sm hover:shadow-md transition-shadow duration-300;
|
||||
}
|
||||
|
||||
.profile-action-btn {
|
||||
@apply bg-white/10 hover:bg-white/20 border-white/20;
|
||||
.el-icon {
|
||||
@apply mr-1;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@apply p-4 lg:p-6 rounded-lg bg-gray-50 dark:bg-slate-700/50 text-center hover:shadow-md transition-all duration-300;
|
||||
}
|
||||
|
||||
.custom-tabs {
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
@apply h-0.5 bg-gray-100 dark:bg-gray-700;
|
||||
}
|
||||
:deep(.el-tabs__active-bar) {
|
||||
@apply h-0.5 bg-blue-500;
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
@apply text-base font-medium px-6;
|
||||
.el-icon {
|
||||
@apply mr-1 text-lg;
|
||||
}
|
||||
&.is-active {
|
||||
@apply text-blue-500;
|
||||
}
|
||||
}
|
||||
:deep(.el-timeline-item__node--normal) {
|
||||
@apply left-[-2px];
|
||||
}
|
||||
:deep(.el-timeline-item__wrapper) {
|
||||
@apply pl-8;
|
||||
}
|
||||
:deep(.el-timeline-item__timestamp) {
|
||||
@apply text-gray-400 text-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dialog {
|
||||
:deep(.el-dialog__header) {
|
||||
@apply mb-0 pb-4 border-b border-gray-100 dark:border-gray-700;
|
||||
}
|
||||
:deep(.el-dialog__footer) {
|
||||
@apply mt-0 pt-4 border-t border-gray-100 dark:border-gray-700;
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
@apply shadow-none;
|
||||
}
|
||||
:deep(.el-input__prefix) {
|
||||
@apply mr-2;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
:deep(.el-input__wrapper) {
|
||||
@apply bg-white/10 border-white/20 shadow-none;
|
||||
input {
|
||||
@apply text-white;
|
||||
&::placeholder {
|
||||
@apply text-white/60;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
22
src/view/routerHolder.vue
Normal file
22
src/view/routerHolder.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<!-- 此路由可作为父类路由通用路由页面使用 如需自定义父类路由页面 请参考 @/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">
|
||||
<div>
|
||||
<component :is="Component" />
|
||||
</div>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineOptions({
|
||||
name: 'RouterHolder'
|
||||
})
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
const routerStore = useRouterStore()
|
||||
</script>
|
||||
832
src/view/superAdmin/api/api.vue
Normal file
832
src/view/superAdmin/api/api.vue
Normal file
@@ -0,0 +1,832 @@
|
||||
<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-select
|
||||
v-model="searchInfo.apiGroup"
|
||||
clearable
|
||||
placeholder="请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiGroupOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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 type="primary" icon="search" @click="onSubmit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog('addApi')">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button icon="delete" :disabled="!apis.length" @click="onDelete">
|
||||
删除
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="onFresh"> 刷新缓存 </el-button>
|
||||
<el-button icon="Compass" @click="onSync"> 同步API </el-button>
|
||||
<ExportTemplate template-id="api" />
|
||||
<ExportExcel template-id="api" :limit="9999" />
|
||||
<ImportExcel template-id="api" @on-success="getTableData" />
|
||||
</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="操作" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
icon="edit"
|
||||
type="primary"
|
||||
link
|
||||
@click="editApiFunc(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="delete"
|
||||
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-drawer
|
||||
v-model="syncApiFlag"
|
||||
:size="appStore.drawerSize"
|
||||
:before-close="closeSyncDialog"
|
||||
:show-close="false"
|
||||
>
|
||||
<warning-bar
|
||||
title="同步API,不输入路由分组将不会被自动同步,如果api不需要参与鉴权,可以按忽略按钮进行忽略。"
|
||||
/>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">同步路由</span>
|
||||
<div>
|
||||
<el-button :loading="apiCompletionLoading" @click="closeSyncDialog">
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="syncing || apiCompletionLoading"
|
||||
@click="enterSyncDialog"
|
||||
>
|
||||
确 定
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<h4>
|
||||
新增路由
|
||||
<span class="text-xs text-gray-500 mx-2 font-normal"
|
||||
>存在于当前路由中,但是不存在于api表</span
|
||||
>
|
||||
<el-button type="primary" size="small" @click="apiCompletion">
|
||||
<el-icon size="18">
|
||||
<ai-gva />
|
||||
</el-icon>
|
||||
自动填充
|
||||
</el-button>
|
||||
</h4>
|
||||
<el-table
|
||||
v-loading="syncing || apiCompletionLoading"
|
||||
element-loading-text="小淼正在思考..."
|
||||
:data="syncApiData.newApis"
|
||||
>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.apiGroup"
|
||||
placeholder="请选择或新增"
|
||||
allow-create
|
||||
filterable
|
||||
default-first-option
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiGroupOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.description" autocomplete="off" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button icon="plus" type="primary" link @click="addApiFunc(row)">
|
||||
单条新增
|
||||
</el-button>
|
||||
<el-button
|
||||
icon="sunrise"
|
||||
type="primary"
|
||||
link
|
||||
@click="ignoreApiFunc(row, true)"
|
||||
>
|
||||
忽略
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h4>
|
||||
已删除路由
|
||||
<span class="text-xs text-gray-500 ml-2 font-normal"
|
||||
>已经不存在于当前项目的路由中,确定同步后会自动从apis表删除</span
|
||||
>
|
||||
</h4>
|
||||
<el-table :data="syncApiData.deleteApis">
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<h4>
|
||||
忽略路由
|
||||
<span class="text-xs text-gray-500 ml-2 font-normal"
|
||||
>忽略路由不参与api同步,常见为不需要进行鉴权行为的路由</span
|
||||
>
|
||||
</h4>
|
||||
<el-table :data="syncApiData.ignoreApis">
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API路径"
|
||||
min-width="150"
|
||||
prop="path"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API分组"
|
||||
min-width="150"
|
||||
prop="apiGroup"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="API简介"
|
||||
min-width="150"
|
||||
prop="description"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="请求"
|
||||
min-width="150"
|
||||
prop="method"
|
||||
>
|
||||
<template #default="scope">
|
||||
<div>
|
||||
{{ scope.row.method }} / {{ methodFilter(scope.row.method) }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
icon="sunny"
|
||||
type="primary"
|
||||
link
|
||||
@click="ignoreApiFunc(row, false)"
|
||||
>
|
||||
取消忽略
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
v-model="dialogFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:before-close="closeDialog"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ dialogTitle }}</span>
|
||||
<div>
|
||||
<el-button @click="closeDialog"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDialog"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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-select
|
||||
v-model="form.apiGroup"
|
||||
placeholder="请选择或新增"
|
||||
allow-create
|
||||
filterable
|
||||
default-first-option
|
||||
>
|
||||
<el-option
|
||||
v-for="item in apiGroupOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="api简介" prop="description">
|
||||
<el-input v-model="form.description" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getApiById,
|
||||
getApiList,
|
||||
createApi,
|
||||
updateApi,
|
||||
deleteApi,
|
||||
deleteApisByIds,
|
||||
freshCasbin,
|
||||
syncApi,
|
||||
getApiGroups,
|
||||
ignoreApi,
|
||||
enterSyncApi
|
||||
} 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'
|
||||
import ExportExcel from '@/components/exportExcel/exportExcel.vue'
|
||||
import ExportTemplate from '@/components/exportExcel/exportTemplate.vue'
|
||||
import ImportExcel from '@/components/exportExcel/importExcel.vue'
|
||||
import { butler } from '@/api/autoCode'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'Api'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
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 apiGroupOptions = ref([])
|
||||
const apiGroupMap = ref({})
|
||||
|
||||
const getGroup = async () => {
|
||||
const res = await getApiGroups()
|
||||
if (res.code === 0) {
|
||||
const groups = res.data.groups
|
||||
apiGroupOptions.value = groups.map((item) => ({
|
||||
label: item,
|
||||
value: item
|
||||
}))
|
||||
apiGroupMap.value = res.data.apiGroupMap
|
||||
}
|
||||
}
|
||||
|
||||
const ignoreApiFunc = async (row, flag) => {
|
||||
const res = await ignoreApi({ path: row.path, method: row.method, flag })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
if (flag) {
|
||||
syncApiData.value.newApis = syncApiData.value.newApis.filter(
|
||||
(item) => !(item.path === row.path && item.method === row.method)
|
||||
)
|
||||
syncApiData.value.ignoreApis.push(row)
|
||||
return
|
||||
}
|
||||
syncApiData.value.ignoreApis = syncApiData.value.ignoreApis.filter(
|
||||
(item) => !(item.path === row.path && item.method === row.method)
|
||||
)
|
||||
syncApiData.value.newApis.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
const addApiFunc = async (row) => {
|
||||
if (!row.apiGroup) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请先选择API分组'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!row.description) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请先填写API描述'
|
||||
})
|
||||
return
|
||||
}
|
||||
const res = await createApi(row)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功',
|
||||
showClose: true
|
||||
})
|
||||
syncApiData.value.newApis = syncApiData.value.newApis.filter(
|
||||
(item) => !(item.path === row.path && item.method === row.method)
|
||||
)
|
||||
}
|
||||
getTableData()
|
||||
getGroup()
|
||||
}
|
||||
|
||||
const closeSyncDialog = () => {
|
||||
syncApiFlag.value = false
|
||||
}
|
||||
|
||||
const syncing = ref(false)
|
||||
|
||||
const enterSyncDialog = async () => {
|
||||
if (
|
||||
syncApiData.value.newApis.some(
|
||||
(item) => !item.apiGroup || !item.description
|
||||
)
|
||||
) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '存在API未分组或未填写描述'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
syncing.value = true
|
||||
const res = await enterSyncApi(syncApiData.value)
|
||||
syncing.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
syncApiFlag.value = false
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
// 搜索
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
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()
|
||||
getGroup()
|
||||
// 批量操作
|
||||
const handleSelectionChange = (val) => {
|
||||
apis.value = val
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(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--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const onFresh = async () => {
|
||||
ElMessageBox.confirm('确定要刷新缓存吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await freshCasbin()
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: res.msg
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const syncApiData = ref({
|
||||
newApis: [],
|
||||
deleteApis: [],
|
||||
ignoreApis: []
|
||||
})
|
||||
|
||||
const syncApiFlag = ref(false)
|
||||
|
||||
const onSync = async () => {
|
||||
const res = await syncApi()
|
||||
if (res.code === 0) {
|
||||
res.data.newApis.forEach((item) => {
|
||||
item.apiGroup = apiGroupMap.value[item.path.split('/')[1]]
|
||||
})
|
||||
|
||||
syncApiData.value = res.data
|
||||
syncApiFlag.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗相关
|
||||
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()
|
||||
getGroup()
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
break
|
||||
case 'edit':
|
||||
{
|
||||
const res = await updateApi(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '编辑成功',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
getTableData()
|
||||
closeDialog()
|
||||
}
|
||||
break
|
||||
default:
|
||||
{
|
||||
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()
|
||||
getGroup()
|
||||
}
|
||||
})
|
||||
}
|
||||
const apiCompletionLoading = ref(false)
|
||||
const apiCompletion = async () => {
|
||||
apiCompletionLoading.value = true
|
||||
const routerPaths = syncApiData.value.newApis
|
||||
.filter((item) => !item.apiGroup || !item.description)
|
||||
.map((item) => item.path)
|
||||
const res = await butler({ data: routerPaths, command: 'apiCompletion' })
|
||||
apiCompletionLoading.value = false
|
||||
if (res.code === 0) {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
syncApiData.value.newApis.forEach((item) => {
|
||||
const target = data.find((d) => d.path === item.path)
|
||||
if (target) {
|
||||
if (!item.apiGroup) {
|
||||
item.apiGroup = target.apiGroup
|
||||
}
|
||||
if (!item.description) {
|
||||
item.description = target.description
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (_) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: 'AI自动填充失败,请重新生成'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.warning {
|
||||
color: #dc143c;
|
||||
}
|
||||
</style>
|
||||
422
src/view/superAdmin/authority/authority.vue
Normal file
422
src/view/superAdmin/authority/authority.vue
Normal file
@@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<div class="authority">
|
||||
<warning-bar title="注:右上角头像下拉可切换角色" />
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button 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"
|
||||
type="primary"
|
||||
link
|
||||
@click="openDrawer(scope.row)"
|
||||
>设置权限</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="plus"
|
||||
type="primary"
|
||||
link
|
||||
@click="addAuthority(scope.row.authorityId)"
|
||||
>新增子角色</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="copy-document"
|
||||
type="primary"
|
||||
link
|
||||
@click="copyAuthorityFunc(scope.row)"
|
||||
>拷贝</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="edit"
|
||||
type="primary"
|
||||
link
|
||||
@click="editAuthority(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteAuth(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 新增角色弹窗 -->
|
||||
<el-drawer v-model="authorityFormVisible" :size="appStore.drawerSize" :show-close="false">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ authorityTitleForm }}</span>
|
||||
<div>
|
||||
<el-button @click="closeAuthorityForm">取 消</el-button>
|
||||
<el-button type="primary" @click="submitAuthorityForm"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<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"
|
||||
maxlength="15"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色姓名" prop="authorityName">
|
||||
<el-input v-model="form.authorityName" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
v-if="drawer"
|
||||
v-model="drawer"
|
||||
:size="appStore.drawerSize"
|
||||
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'
|
||||
import { useAppStore } from "@/pinia"
|
||||
|
||||
defineOptions({
|
||||
name: 'Authority'
|
||||
})
|
||||
|
||||
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 appStore = useAppStore()
|
||||
|
||||
const authorityTitleForm = ref('新增角色')
|
||||
const authorityFormVisible = 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 tableData = ref([])
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getAuthorityList()
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
authorityTitleForm.value = '拷贝角色'
|
||||
dialogType.value = 'copy'
|
||||
for (const k in form.value) {
|
||||
form.value[k] = row[k]
|
||||
}
|
||||
copyForm.value = row
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
const openDrawer = (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: '删除成功!'
|
||||
})
|
||||
|
||||
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 closeAuthorityForm = () => {
|
||||
initForm()
|
||||
authorityFormVisible.value = false
|
||||
apiDialogFlag.value = false
|
||||
}
|
||||
// 确定弹窗
|
||||
|
||||
const submitAuthorityForm = () => {
|
||||
authorityForm.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
form.value.authorityId = Number(form.value.authorityId)
|
||||
switch (dialogType.value) {
|
||||
case 'add':
|
||||
{
|
||||
const res = await createAuthority(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功!'
|
||||
})
|
||||
getTableData()
|
||||
closeAuthorityForm()
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'edit':
|
||||
{
|
||||
const res = await updateAuthority(form.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '添加成功!'
|
||||
})
|
||||
getTableData()
|
||||
closeAuthorityForm()
|
||||
}
|
||||
}
|
||||
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()
|
||||
authorityFormVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const setOptions = () => {
|
||||
AuthorityOption.value = [
|
||||
{
|
||||
authorityId: 0,
|
||||
authorityName: '根角色(严格模式下为当前用户角色)'
|
||||
}
|
||||
]
|
||||
setAuthorityOptions(tableData.value, AuthorityOption.value, false)
|
||||
}
|
||||
const setAuthorityOptions = (AuthorityData, optionsData, disabled) => {
|
||||
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()
|
||||
authorityTitleForm.value = '新增角色'
|
||||
dialogType.value = 'add'
|
||||
form.value.parentId = parentId
|
||||
setOptions()
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
// 编辑角色
|
||||
const editAuthority = (row) => {
|
||||
setOptions()
|
||||
authorityTitleForm.value = '编辑角色'
|
||||
dialogType.value = 'edit'
|
||||
for (const key in form.value) {
|
||||
form.value[key] = row[key]
|
||||
}
|
||||
setOptions()
|
||||
authorityForm.value && authorityForm.value.clearValidate()
|
||||
authorityFormVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.authority {
|
||||
.el-input-number {
|
||||
margin-left: 15px;
|
||||
span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.tree-content {
|
||||
margin-top: 10px;
|
||||
height: calc(100vh - 158px);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
174
src/view/superAdmin/authority/components/apis.vue
Normal file
174
src/view/superAdmin/authority/components/apis.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="sticky top-0.5 z-10 flex space-x-2">
|
||||
<el-input
|
||||
v-model="filterTextName"
|
||||
class="flex-1"
|
||||
placeholder="筛选名字"
|
||||
/>
|
||||
<el-input
|
||||
v-model="filterTextPath"
|
||||
class="flex-1"
|
||||
placeholder="筛选路径"
|
||||
/>
|
||||
<el-button class="float-right" type="primary" @click="authApiEnter"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="tree-content">
|
||||
<el-scrollbar>
|
||||
<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"
|
||||
>
|
||||
<template #default="{ _, data }">
|
||||
<div class="flex items-center justify-between w-full pr-1">
|
||||
<span>{{ data.description }} </span>
|
||||
<el-tooltip :content="data.path">
|
||||
<span
|
||||
class="max-w-[240px] break-all overflow-ellipsis overflow-hidden"
|
||||
>{{ data.path }}</span
|
||||
>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getAllApis } from '@/api/api'
|
||||
import { UpdateCasbin, getPolicyPathByAuthorityId } from '@/api/casbin'
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'Apis'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
row: {
|
||||
default: function () {
|
||||
return {}
|
||||
},
|
||||
type: Object
|
||||
}
|
||||
})
|
||||
|
||||
const apiDefaultProps = ref({
|
||||
children: 'children',
|
||||
label: 'description'
|
||||
})
|
||||
const filterTextName = ref('')
|
||||
const filterTextPath = 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 (!filterTextName.value && !filterTextPath.value) return true
|
||||
let matchesName, matchesPath
|
||||
if (!filterTextName.value) {
|
||||
matchesName = true
|
||||
} else {
|
||||
matchesName =
|
||||
data.description && data.description.includes(filterTextName.value)
|
||||
}
|
||||
if (!filterTextPath.value) {
|
||||
matchesPath = true
|
||||
} else {
|
||||
matchesPath = data.path && data.path.includes(filterTextPath.value)
|
||||
}
|
||||
return matchesName && matchesPath
|
||||
}
|
||||
watch([filterTextName, filterTextPath], () => {
|
||||
apiTree.value.filter('')
|
||||
})
|
||||
</script>
|
||||
145
src/view/superAdmin/authority/components/datas.vue
Normal file
145
src/view/superAdmin/authority/components/datas.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
title="此功能仅用于创建角色和角色的many2many关系表,具体使用还须自己结合表实现业务,详情参考示例代码(客户示例)。此功能不建议使用,建议使用插件市场【组织管理功能(点击前往)】来管理资源权限。"
|
||||
href="https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=36"
|
||||
/>
|
||||
<div class="sticky top-0.5 z-10 my-4">
|
||||
<el-button class="float-left" type="primary" @click="all">全选</el-button>
|
||||
<el-button class="float-left" type="primary" @click="self"
|
||||
>本角色</el-button
|
||||
>
|
||||
<el-button class="float-left" type="primary" @click="selfAndChildren"
|
||||
>本角色及子角色</el-button
|
||||
>
|
||||
<el-button class="float-right" type="primary" @click="authDataEnter"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="clear-both pt-4">
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { setDataAuthority } from '@/api/authority'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'Datas'
|
||||
})
|
||||
|
||||
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 = () => {
|
||||
dataAuthorityId.value = dataAuthorityId.value.filter((item) => item)
|
||||
emit('changeRow', 'dataAuthorityId', dataAuthorityId.value)
|
||||
needConfirm.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
enterAndNext,
|
||||
needConfirm
|
||||
})
|
||||
</script>
|
||||
233
src/view/superAdmin/authority/components/menus.vue
Normal file
233
src/view/superAdmin/authority/components/menus.vue
Normal file
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="sticky top-0.5 z-10">
|
||||
<el-input v-model="filterText" class="w-3/5" placeholder="筛选" />
|
||||
<el-button class="float-right" type="primary" @click="relation"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="tree-content clear-both">
|
||||
<el-scrollbar>
|
||||
<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 v-if="node.checked">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:style="{
|
||||
color:
|
||||
row.defaultRouter === data.name ? '#E6A23C' : '#85ce61'
|
||||
}"
|
||||
@click="() => setDefault(data)"
|
||||
>
|
||||
{{ row.defaultRouter === data.name ? '首页' : '设为首页' }}
|
||||
</el-button>
|
||||
</span>
|
||||
<span v-if="data.menuBtn.length">
|
||||
<el-button type="primary" link @click="() => OpenBtn(data)">
|
||||
分配按钮
|
||||
</el-button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</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 @click="closeDialog">取 消</el-button>
|
||||
<el-button 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'
|
||||
|
||||
defineOptions({
|
||||
name: 'Menus'
|
||||
})
|
||||
|
||||
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
|
||||
},
|
||||
disabled: function (data) {
|
||||
return props.row.defaultRouter === data.name
|
||||
}
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-tree-node {
|
||||
span + span {
|
||||
@apply ml-3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
255
src/view/superAdmin/dictionary/sysDictionary.vue
Normal file
255
src/view/superAdmin/dictionary/sysDictionary.vue
Normal file
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
title="获取字典且缓存方法已在前端utils/dictionary 已经封装完成 不必自己书写 使用方法查看文件内注释"
|
||||
/>
|
||||
<div class="flex gap-4 p-2">
|
||||
<div
|
||||
class="flex-none w-52 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded p-4"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text font-bold">字典列表</span>
|
||||
<el-button type="primary" @click="openDrawer"> 新增 </el-button>
|
||||
</div>
|
||||
<el-scrollbar class="mt-4" style="height: calc(100vh - 300px)">
|
||||
<div
|
||||
v-for="dictionary in dictionaryData"
|
||||
:key="dictionary.ID"
|
||||
class="rounded flex justify-between items-center px-2 py-4 cursor-pointer mt-2 hover:bg-blue-50 dark:hover:bg-blue-900 bg-gray-50 dark:bg-gray-800 gap-4"
|
||||
:class="
|
||||
selectID === dictionary.ID
|
||||
? 'text-active'
|
||||
: 'text-slate-700 dark:text-slate-50'
|
||||
"
|
||||
@click="toDetail(dictionary)"
|
||||
>
|
||||
<span class="max-w-[160px] truncate">{{ dictionary.name }}</span>
|
||||
<div class="min-w-[40px]">
|
||||
<el-icon
|
||||
class="text-blue-500"
|
||||
@click.stop="updateSysDictionaryFunc(dictionary)"
|
||||
>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
class="ml-2 text-red-500"
|
||||
@click="deleteSysDictionaryFunc(dictionary)"
|
||||
>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div
|
||||
class="flex-1 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900"
|
||||
>
|
||||
<sysDictionaryDetail :sys-dictionary-i-d="selectID" />
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer
|
||||
v-model="drawerFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:before-close="closeDrawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{
|
||||
type === 'create' ? '添加字典' : '修改字典'
|
||||
}}</span>
|
||||
<div>
|
||||
<el-button @click="closeDrawer"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDrawer"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form
|
||||
ref="drawerForm"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
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>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysDictionary,
|
||||
deleteSysDictionary,
|
||||
updateSysDictionary,
|
||||
findSysDictionary,
|
||||
getSysDictionaryList
|
||||
} from '@/api/sysDictionary' // 此处请自行替换地址
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import sysDictionaryDetail from './sysDictionaryDetail.vue'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'SysDictionary'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const selectID = ref(0)
|
||||
|
||||
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 dictionaryData = ref([])
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const res = await getSysDictionaryList()
|
||||
if (res.code === 0) {
|
||||
dictionaryData.value = res.data
|
||||
selectID.value = res.data[0].ID
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
const toDetail = (row) => {
|
||||
selectID.value = row.ID
|
||||
}
|
||||
|
||||
const drawerFormVisible = 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
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
}
|
||||
const closeDrawer = () => {
|
||||
drawerFormVisible.value = false
|
||||
formData.value = {
|
||||
name: null,
|
||||
type: null,
|
||||
status: true,
|
||||
desc: null
|
||||
}
|
||||
}
|
||||
const deleteSysDictionaryFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteSysDictionary({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const drawerForm = ref(null)
|
||||
const enterDrawer = async () => {
|
||||
drawerForm.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('操作成功')
|
||||
closeDrawer()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const openDrawer = () => {
|
||||
type.value = 'create'
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dict-box {
|
||||
height: calc(100vh - 240px);
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--el-color-primary) !important;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
321
src/view/superAdmin/dictionary/sysDictionaryDetail.vue
Normal file
321
src/view/superAdmin/dictionary/sysDictionaryDetail.vue
Normal file
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list justify-between">
|
||||
<span class="text font-bold">字典详细内容</span>
|
||||
<el-button type="primary" icon="plus" @click="openDrawer">
|
||||
新增字典项
|
||||
</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" />
|
||||
|
||||
<el-table-column align="left" label="字典值" prop="value" />
|
||||
|
||||
<el-table-column align="left" label="扩展值" prop="extend" />
|
||||
|
||||
<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="操作" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="updateSysDictionaryDetailFunc(scope.row)"
|
||||
>
|
||||
变更
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteSysDictionaryDetailFunc(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-drawer
|
||||
v-model="drawerFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:before-close="closeDrawer"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{
|
||||
type === 'create' ? '添加字典项' : '修改字典项'
|
||||
}}</span>
|
||||
<div>
|
||||
<el-button @click="closeDrawer"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDrawer"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form
|
||||
ref="drawerForm"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
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
|
||||
v-model="formData.value"
|
||||
placeholder="请输入字典值"
|
||||
clearable
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="扩展值" prop="extend">
|
||||
<el-input
|
||||
v-model="formData.extend"
|
||||
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>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysDictionaryDetail,
|
||||
deleteSysDictionaryDetail,
|
||||
updateSysDictionaryDetail,
|
||||
findSysDictionaryDetail,
|
||||
getSysDictionaryDetailList
|
||||
} from '@/api/sysDictionaryDetail' // 此处请自行替换地址
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { formatBoolean, formatDate } from '@/utils/format'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'SysDictionaryDetail'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
sysDictionaryID: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
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 handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
if (!props.sysDictionaryID) return
|
||||
const table = await getSysDictionaryDetailList({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
sysDictionaryID: props.sysDictionaryID
|
||||
})
|
||||
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 drawerFormVisible = ref(false)
|
||||
const updateSysDictionaryDetailFunc = async (row) => {
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
const res = await findSysDictionaryDetail({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data.reSysDictionaryDetail
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const closeDrawer = () => {
|
||||
drawerFormVisible.value = false
|
||||
formData.value = {
|
||||
label: null,
|
||||
value: null,
|
||||
status: true,
|
||||
sort: null,
|
||||
sysDictionaryID: props.sysDictionaryID
|
||||
}
|
||||
}
|
||||
const deleteSysDictionaryDetailFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
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 drawerForm = ref(null)
|
||||
const enterDrawer = async () => {
|
||||
drawerForm.value.validate(async (valid) => {
|
||||
formData.value.sysDictionaryID = props.sysDictionaryID
|
||||
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: '创建/更改成功'
|
||||
})
|
||||
closeDrawer()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const openDrawer = () => {
|
||||
type.value = 'create'
|
||||
drawerForm.value && drawerForm.value.clearValidate()
|
||||
drawerFormVisible.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.sysDictionaryID,
|
||||
() => {
|
||||
getTableData()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
20
src/view/superAdmin/index.vue
Normal file
20
src/view/superAdmin/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<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 setup>
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
defineOptions({
|
||||
name: 'SuperAdmin'
|
||||
})
|
||||
</script>
|
||||
131
src/view/superAdmin/menu/components/components-cascader.vue
Normal file
131
src/view/superAdmin/menu/components/components-cascader.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="flex justify-between items-center gap-2 w-full">
|
||||
<el-cascader
|
||||
v-if="pathIsSelect"
|
||||
placeholder="请选择文件路径"
|
||||
:options="pathOptions"
|
||||
v-model="activeComponent"
|
||||
filterable
|
||||
class="w-full"
|
||||
clearable
|
||||
@change="emitChange"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="tempPath"
|
||||
placeholder="页面:view/xxx/xx.vue 插件:plugin/xx/xx.vue"
|
||||
@change="emitChange"
|
||||
/>
|
||||
<el-button @click="togglePathIsSelect"
|
||||
>{{ pathIsSelect ? '手动输入' : '快捷选择' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import pathInfo from '@/pathInfo.json'
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['change'])
|
||||
|
||||
const pathOptions = ref([])
|
||||
const tempPath = ref('')
|
||||
const activeComponent = ref([])
|
||||
const pathIsSelect = ref(true)
|
||||
|
||||
const togglePathIsSelect = () => {
|
||||
if (pathIsSelect.value) {
|
||||
tempPath.value = activeComponent.value?.join('/') || ''
|
||||
} else {
|
||||
activeComponent.value = tempPath.value?.split('/') || []
|
||||
}
|
||||
|
||||
pathIsSelect.value = !pathIsSelect.value
|
||||
emitChange()
|
||||
}
|
||||
|
||||
function convertToCascaderOptions(data) {
|
||||
const result = []
|
||||
|
||||
for (const path in data) {
|
||||
const label = data[path]
|
||||
const parts = path.split('/').filter(Boolean)
|
||||
|
||||
// 如果第一个部分是 'src',则从第二个部分开始处理
|
||||
const startIndex = parts[0] === 'src' ? 1 : 0
|
||||
|
||||
let currentLevel = result
|
||||
|
||||
for (let i = startIndex; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
let node = currentLevel.find((item) => item.value === part)
|
||||
|
||||
if (!node) {
|
||||
node = {
|
||||
value: part,
|
||||
label: part,
|
||||
children: []
|
||||
}
|
||||
currentLevel.push(node)
|
||||
}
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
// 如果是路径的最后一部分,设置标签并移除 children
|
||||
node.label = label
|
||||
delete node.children
|
||||
}
|
||||
|
||||
currentLevel = node.children || []
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.component,
|
||||
(value) => {
|
||||
initCascader(value)
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
pathOptions.value = convertToCascaderOptions(pathInfo)
|
||||
initCascader(props.component)
|
||||
})
|
||||
|
||||
const initCascader = (value) => {
|
||||
// 新增的时候
|
||||
if (value === '') {
|
||||
pathIsSelect.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 编辑的时候,根据路径判断是选择框还是输入框
|
||||
if (pathInfo[`/src/${value}`]) {
|
||||
activeComponent.value = value.split('/').filter(Boolean)
|
||||
tempPath.value = ''
|
||||
pathIsSelect.value = true
|
||||
return
|
||||
}
|
||||
tempPath.value = value
|
||||
activeComponent.value = []
|
||||
pathIsSelect.value = false
|
||||
}
|
||||
|
||||
const emitChange = () => {
|
||||
emits(
|
||||
'change',
|
||||
pathIsSelect.value ? activeComponent.value?.join('/') : tempPath.value
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
1179
src/view/superAdmin/menu/icon.vue
Normal file
1179
src/view/superAdmin/menu/icon.vue
Normal file
File diff suppressed because it is too large
Load Diff
680
src/view/superAdmin/menu/menu.vue
Normal file
680
src/view/superAdmin/menu/menu.vue
Normal file
@@ -0,0 +1,680 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button 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="操作" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="plus"
|
||||
@click="addMenu(scope.row.ID)"
|
||||
>
|
||||
添加子菜单
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="editMenu(scope.row.ID)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteMenu(scope.row.ID)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-drawer
|
||||
v-model="dialogFormVisible"
|
||||
:size="appStore.drawerSize"
|
||||
:before-close="handleClose"
|
||||
:show-close="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ dialogTitle }}</span>
|
||||
<div>
|
||||
<el-button @click="closeDialog"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDialog"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<warning-bar title="新增菜单,需要在角色管理内配置权限才可使用" />
|
||||
<el-form
|
||||
v-if="dialogFormVisible"
|
||||
ref="menuForm"
|
||||
:inline="true"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="16">
|
||||
<el-form-item label="文件路径" prop="component">
|
||||
<components-cascader
|
||||
:component="form.component"
|
||||
@change="fmtComponent"
|
||||
/>
|
||||
<span style="font-size: 12px; margin-right: 12px"
|
||||
>如果菜单包含子菜单,请创建router-view二级路由页面或者</span
|
||||
>
|
||||
<el-button
|
||||
style="margin-top: 4px"
|
||||
@click="form.component = 'view/routerHolder.vue'"
|
||||
>
|
||||
点我设置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="展示名称" prop="meta.title">
|
||||
<el-input v-model="form.meta.title" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="路由Name" prop="path">
|
||||
<el-input
|
||||
v-model="form.name"
|
||||
autocomplete="off"
|
||||
placeholder="唯一英文字符串"
|
||||
@change="changeName"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="path">
|
||||
<template #label>
|
||||
<span style="display: inline-flex; align-items: center">
|
||||
<span>路由Path</span>
|
||||
<el-checkbox
|
||||
v-model="checkFlag"
|
||||
style="margin-left: 12px; height: auto"
|
||||
>添加参数</el-checkbox
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-model="form.path"
|
||||
:disabled="!checkFlag"
|
||||
autocomplete="off"
|
||||
placeholder="建议只在后方拼接参数"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="是否隐藏">
|
||||
<el-select
|
||||
v-model="form.hidden"
|
||||
style="width: 100%"
|
||||
placeholder="是否在列表隐藏"
|
||||
>
|
||||
<el-option :value="false" label="否" />
|
||||
<el-option :value="true" label="是" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="父节点ID">
|
||||
<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-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="图标" prop="meta.icon">
|
||||
<icon v-model="form.meta.icon" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排序标记" prop="sort">
|
||||
<el-input v-model.number="form.sort" autocomplete="off" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="8">
|
||||
<el-form-item prop="meta.activeName">
|
||||
<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-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="KeepAlive" prop="meta.keepAlive">
|
||||
<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-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="CloseTab" prop="meta.closeTab">
|
||||
<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-col>
|
||||
</el-row>
|
||||
<el-row class="w-full">
|
||||
<el-col :span="8">
|
||||
<el-form-item>
|
||||
<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-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<el-button type="primary" icon="edit" @click="addParameter(form)">
|
||||
新增菜单参数
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="form.parameters" style="width: 100%; margin-top: 12px">
|
||||
<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"
|
||||
icon="delete"
|
||||
@click="deleteParameter(form.parameters, scope.$index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="flex items-center gap-2 mt-3">
|
||||
<el-button type="primary" icon="edit" @click="addBtn(form)">
|
||||
新增可控按钮
|
||||
</el-button>
|
||||
<el-icon
|
||||
class="cursor-pointer"
|
||||
@click="
|
||||
toDoc('https://www.gin-vue-admin.com/guide/web/button-auth.html')
|
||||
"
|
||||
>
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<el-table :data="form.menuBtn" style="width: 100%; margin-top: 12px">
|
||||
<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"
|
||||
icon="delete"
|
||||
@click="deleteBtn(form.menuBtn, scope.$index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</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'
|
||||
import { QuestionFilled } from '@element-plus/icons-vue'
|
||||
import { toDoc } from '@/utils/doc'
|
||||
import { toLowerCase } from '@/utils/stringFun'
|
||||
import ComponentsCascader from '@/view/superAdmin/menu/components/components-cascader.vue'
|
||||
|
||||
import pathInfo from '@/pathInfo.json'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'Menus'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const rules = reactive({
|
||||
path: [{ required: true, message: '请输入菜单name', trigger: 'blur' }],
|
||||
component: [{ required: true, message: '请输入文件路径', trigger: 'blur' }],
|
||||
'meta.title': [
|
||||
{ required: true, message: '请输入菜单展示名称', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getMenuList()
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// 新增参数
|
||||
const addParameter = (form) => {
|
||||
if (!form.parameters) {
|
||||
form.parameters = []
|
||||
}
|
||||
form.parameters.push({
|
||||
type: 'query',
|
||||
key: '',
|
||||
value: ''
|
||||
})
|
||||
}
|
||||
|
||||
const fmtComponent = (component) => {
|
||||
form.value.component = component.replace(/\\/g, '/')
|
||||
form.value.name = toLowerCase(pathInfo['/src/' + component])
|
||||
form.value.path = form.value.name
|
||||
}
|
||||
|
||||
// 删除参数
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const form = ref({
|
||||
ID: 0,
|
||||
path: '',
|
||||
name: '',
|
||||
hidden: false,
|
||||
parentId: 0,
|
||||
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: '删除成功!'
|
||||
})
|
||||
|
||||
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: 0,
|
||||
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: 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: 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 = 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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.warning {
|
||||
color: #dc143c;
|
||||
}
|
||||
.icon-column {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
277
src/view/superAdmin/operation/sysOperationRecord.vue
Normal file
277
src/view/superAdmin/operation/sysOperationRecord.vue
Normal file
@@ -0,0 +1,277 @@
|
||||
<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 type="primary" icon="search" @click="onSubmit"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button
|
||||
icon="delete"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button
|
||||
>
|
||||
</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"
|
||||
:width="444"
|
||||
>
|
||||
<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"
|
||||
:width="444"
|
||||
>
|
||||
<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-button
|
||||
icon="delete"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteSysOperationRecordFunc(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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
deleteSysOperationRecord,
|
||||
getSysOperationRecordList,
|
||||
deleteSysOperationRecordByIds
|
||||
} from '@/api/sysOperationRecord' // 此处请自行替换地址
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'SysOperationRecord'
|
||||
})
|
||||
|
||||
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
|
||||
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 multipleSelection = ref([])
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(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--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
const deleteSysOperationRecordFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
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 (_) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
</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>
|
||||
604
src/view/superAdmin/params/sysParams.vue
Normal file
604
src/view/superAdmin/params/sysParams.vue
Normal file
@@ -0,0 +1,604 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar title="获取参数且缓存方法已在前端utils/params 已经封装完成 不必自己书写 使用方法查看文件内注释" />
|
||||
<div class="gva-search-box">
|
||||
<el-form
|
||||
ref="elSearchFormRef"
|
||||
:inline="true"
|
||||
:model="searchInfo"
|
||||
class="demo-form-inline"
|
||||
:rules="searchRule"
|
||||
@keyup.enter="onSubmit"
|
||||
>
|
||||
<el-form-item label="创建日期" prop="createdAt">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip
|
||||
content="搜索范围是开始日期(包含)至结束日期(不包含)"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-date-picker
|
||||
v-model="searchInfo.startCreatedAt"
|
||||
type="datetime"
|
||||
placeholder="开始日期"
|
||||
:disabled-date="
|
||||
(time) =>
|
||||
searchInfo.endCreatedAt
|
||||
? time.getTime() > searchInfo.endCreatedAt.getTime()
|
||||
: false
|
||||
"
|
||||
></el-date-picker>
|
||||
—
|
||||
<el-date-picker
|
||||
v-model="searchInfo.endCreatedAt"
|
||||
type="datetime"
|
||||
placeholder="结束日期"
|
||||
:disabled-date="
|
||||
(time) =>
|
||||
searchInfo.startCreatedAt
|
||||
? time.getTime() < searchInfo.startCreatedAt.getTime()
|
||||
: false
|
||||
"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="参数名称" prop="name">
|
||||
<el-input v-model="searchInfo.name" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键" prop="key">
|
||||
<el-input v-model="searchInfo.key" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="showAllQuery">
|
||||
<!-- 将需要控制显示状态的查询条件添加到此范围内 -->
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="arrow-down"
|
||||
@click="showAllQuery = true"
|
||||
v-if="!showAllQuery"
|
||||
>展开</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="arrow-up"
|
||||
@click="showAllQuery = false"
|
||||
v-else
|
||||
>收起</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="delete"
|
||||
style="margin-left: 10px"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="createdAt" width="180">
|
||||
<template #default="scope">{{
|
||||
formatDate(scope.row.CreatedAt)
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="参数名称"
|
||||
prop="name"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column align="left" label="参数键" prop="key" width="120" />
|
||||
<el-table-column align="left" label="参数值" prop="value" width="120" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="参数说明"
|
||||
prop="desc"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="240"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="table-button"
|
||||
@click="getDetails(scope.row)"
|
||||
><el-icon style="margin-right: 5px"><InfoFilled /></el-icon
|
||||
>查看详情</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
class="table-button"
|
||||
@click="updateSysParamsFunc(scope.row)"
|
||||
>变更</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteRow(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer
|
||||
destroy-on-close
|
||||
size="800"
|
||||
v-model="dialogFormVisible"
|
||||
:show-close="false"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ type === 'create' ? '添加' : '修改' }}</span>
|
||||
<div>
|
||||
<el-button type="primary" @click="enterDialog">确 定</el-button>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-position="top"
|
||||
ref="elFormRef"
|
||||
:rules="rule"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="参数名称:" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键:" prop="key">
|
||||
<el-input
|
||||
v-model="formData.key"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数键"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数值:" prop="value">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
v-model="formData.value"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数值"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数说明:" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
:clearable="true"
|
||||
placeholder="请输入参数说明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div
|
||||
class="usage-instructions bg-gray-100 border border-gray-300 rounded-lg p-4 mt-5"
|
||||
>
|
||||
<h3 class="mb-3 text-lg text-gray-800">使用说明</h3>
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
前端可以通过引入
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>import { getParams } from '@/utils/params'</code
|
||||
>
|
||||
然后通过
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>await getParams("{{ formData.key }}")</code
|
||||
>
|
||||
来获取对应的参数。
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">
|
||||
后端需要提前
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>import
|
||||
"github.com/flipped-aurora/gin-vue-admin/server/service/system"</code
|
||||
>
|
||||
</p>
|
||||
<p class="mb-2 text-sm text-gray-600">
|
||||
然后调用
|
||||
<code class="bg-blue-100 px-1 py-0.5 rounded"
|
||||
>new(system.SysParamsService).GetSysParam("{{
|
||||
formData.key
|
||||
}}")</code
|
||||
>
|
||||
来获取对应的 value 值。
|
||||
</p>
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
destroy-on-close
|
||||
size="800"
|
||||
v-model="detailShow"
|
||||
:show-close="true"
|
||||
:before-close="closeDetailShow"
|
||||
>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="参数名称">
|
||||
{{ detailFrom.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数键">
|
||||
{{ detailFrom.key }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数值">
|
||||
{{ detailFrom.value }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="参数说明">
|
||||
{{ detailFrom.desc }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysParams,
|
||||
deleteSysParams,
|
||||
deleteSysParamsByIds,
|
||||
updateSysParams,
|
||||
findSysParams,
|
||||
getSysParamsList
|
||||
} from '@/api/sysParams'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import WarningBar from "@/components/warningBar/warningBar.vue";
|
||||
|
||||
defineOptions({
|
||||
name: 'SysParams'
|
||||
})
|
||||
|
||||
// 控制更多查询条件显示/隐藏状态
|
||||
const showAllQuery = ref(false)
|
||||
|
||||
// 自动化生成的字典(可能为空)以及字段
|
||||
const formData = ref({
|
||||
name: '',
|
||||
key: '',
|
||||
value: '',
|
||||
desc: ''
|
||||
})
|
||||
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const searchRule = reactive({
|
||||
createdAt: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (
|
||||
searchInfo.value.startCreatedAt &&
|
||||
!searchInfo.value.endCreatedAt
|
||||
) {
|
||||
callback(new Error('请填写结束日期'))
|
||||
} else if (
|
||||
!searchInfo.value.startCreatedAt &&
|
||||
searchInfo.value.endCreatedAt
|
||||
) {
|
||||
callback(new Error('请填写开始日期'))
|
||||
} else if (
|
||||
searchInfo.value.startCreatedAt &&
|
||||
searchInfo.value.endCreatedAt &&
|
||||
(searchInfo.value.startCreatedAt.getTime() ===
|
||||
searchInfo.value.endCreatedAt.getTime() ||
|
||||
searchInfo.value.startCreatedAt.getTime() >
|
||||
searchInfo.value.endCreatedAt.getTime())
|
||||
) {
|
||||
callback(new Error('开始日期应当早于结束日期'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
// =========== 表格控制部分 ===========
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 修改页面容量
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getSysParamsList({
|
||||
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 setOptions = async () => {}
|
||||
|
||||
// 获取需要的字典 可能为空 按需保留
|
||||
setOptions()
|
||||
|
||||
// 多选数据
|
||||
const multipleSelection = ref([])
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteSysParamsFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const IDs = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map((item) => {
|
||||
IDs.push(item.ID)
|
||||
})
|
||||
const res = await deleteSysParamsByIds({ IDs })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === IDs.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 行为控制标记(弹窗内部需要增还是改)
|
||||
const type = ref('')
|
||||
|
||||
// 更新行
|
||||
const updateSysParamsFunc = async (row) => {
|
||||
const res = await findSysParams({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteSysParamsFunc = async (row) => {
|
||||
const res = await deleteSysParams({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗控制标记
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
name: '',
|
||||
key: '',
|
||||
value: '',
|
||||
desc: ''
|
||||
}
|
||||
}
|
||||
// 弹窗确定
|
||||
const enterDialog = async () => {
|
||||
elFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createSysParams(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateSysParams(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await createSysParams(formData.value)
|
||||
break
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const detailFrom = ref({})
|
||||
|
||||
// 查看详情控制标记
|
||||
const detailShow = ref(false)
|
||||
|
||||
// 打开详情弹窗
|
||||
const openDetailShow = () => {
|
||||
detailShow.value = true
|
||||
}
|
||||
|
||||
// 打开详情
|
||||
const getDetails = async (row) => {
|
||||
// 打开弹窗
|
||||
const res = await findSysParams({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
detailFrom.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailFrom.value = {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
524
src/view/superAdmin/user/user.vue
Normal file
524
src/view/superAdmin/user/user.vue
Normal file
@@ -0,0 +1,524 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar title="注:右上角头像下拉可切换角色" />
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="searchForm" :inline="true" :model="searchInfo">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="searchInfo.username" placeholder="用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="searchInfo.nickname" placeholder="昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="searchInfo.phone" placeholder="手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="searchInfo.email" placeholder="邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="refresh" @click="onReset"> 重置 </el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button 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="appStore.operateMinWith" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteUserFunc(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
@click="openEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="magic-stick"
|
||||
@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-drawer
|
||||
v-model="addUserDialog"
|
||||
:size="appStore.drawerSize"
|
||||
:show-close="false"
|
||||
:close-on-press-escape="false"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">用户</span>
|
||||
<div>
|
||||
<el-button @click="closeAddUserDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="enterAddUserDialog"
|
||||
>确 定</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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">
|
||||
<SelectImage v-model="userInfo.headerImg" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getUserList,
|
||||
setUserAuthorities,
|
||||
register,
|
||||
deleteUser
|
||||
} from '@/api/user'
|
||||
|
||||
import { getAuthorityList } from '@/api/authority'
|
||||
import CustomPic from '@/components/customPic/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'
|
||||
import SelectImage from '@/components/selectImage/selectImage.vue'
|
||||
import { useAppStore } from "@/pinia";
|
||||
|
||||
defineOptions({
|
||||
name: 'User'
|
||||
})
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const searchInfo = ref({
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
})
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {
|
||||
username: '',
|
||||
nickname: '',
|
||||
phone: '',
|
||||
email: ''
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
// 初始化相关
|
||||
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,
|
||||
...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
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => tableData.value,
|
||||
() => {
|
||||
setAuthorityIds()
|
||||
}
|
||||
)
|
||||
|
||||
const initPage = async () => {
|
||||
getTableData()
|
||||
const res = await getAuthorityList()
|
||||
setOptions(res.data)
|
||||
}
|
||||
|
||||
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) => {
|
||||
user.authorityIds =
|
||||
user.authorities &&
|
||||
user.authorities.map((i) => {
|
||||
return i.authorityId
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const authOptions = ref([])
|
||||
const setOptions = (authData) => {
|
||||
authOptions.value = []
|
||||
setAuthorityOptions(authData, authOptions.value)
|
||||
}
|
||||
|
||||
const deleteUserFunc = async (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteUser({ id: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
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">
|
||||
.header-img-box {
|
||||
@apply w-52 h-52 border border-solid border-gray-300 rounded-xl flex justify-center items-center cursor-pointer;
|
||||
}
|
||||
</style>
|
||||
192
src/view/system/state.vue
Normal file
192
src/view/system/state.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="15" class="py-1">
|
||||
<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">{{ state.os.goos }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">cpu nums:</el-col>
|
||||
<el-col :span="12">{{ state.os.numCpu }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">compiler:</el-col>
|
||||
<el-col :span="12">{{ state.os.compiler }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">go version:</el-col>
|
||||
<el-col :span="12">{{ state.os.goVersion }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">goroutine nums:</el-col>
|
||||
<el-col :span="12">{{ state.os.numGoroutine }}</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
v-if="state.disk"
|
||||
class="card_item"
|
||||
:body-style="{ height: '180px', 'overflow-y': 'scroll' }"
|
||||
>
|
||||
<template #header>
|
||||
<div>Disk</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-row
|
||||
v-for="(item, index) in state.disk"
|
||||
:key="index"
|
||||
:gutter="10"
|
||||
style="margin-bottom: 2rem"
|
||||
>
|
||||
<el-col :span="12">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">MountPoint</el-col>
|
||||
<el-col :span="12">{{ item.mountPoint }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">total (MB)</el-col>
|
||||
<el-col :span="12">{{ item.totalMb }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">used (MB)</el-col>
|
||||
<el-col :span="12">{{ item.usedMb }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">total (GB)</el-col>
|
||||
<el-col :span="12">{{ item.totalGb }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">used (GB)</el-col>
|
||||
<el-col :span="12">{{ item.usedGb }}</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-progress
|
||||
type="dashboard"
|
||||
:percentage="item.usedPercent"
|
||||
:color="colors"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="15" class="py-1">
|
||||
<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">{{ state.cpu.cores }}</el-col>
|
||||
</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">{{ state.ram.totalMb }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">used (MB)</el-col>
|
||||
<el-col :span="12">{{ state.ram.usedMb }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">total (GB)</el-col>
|
||||
<el-col :span="12">{{ state.ram.totalMb / 1024 }}</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">used (GB)</el-col>
|
||||
<el-col :span="12">{{
|
||||
(state.ram.usedMb / 1024).toFixed(2)
|
||||
}}</el-col>
|
||||
</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'
|
||||
|
||||
defineOptions({
|
||||
name: 'State'
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
<style>
|
||||
.card_item {
|
||||
@apply h-80 text-xl p-6 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-800 rounded m-2;
|
||||
}
|
||||
</style>
|
||||
502
src/view/systemTools/autoCode/component/fieldDialog.vue
Normal file
502
src/view/systemTools/autoCode/component/fieldDialog.vue
Normal file
@@ -0,0 +1,502 @@
|
||||
<template>
|
||||
<div>
|
||||
<warning-bar
|
||||
title="id , created_at , updated_at , deleted_at 会自动生成请勿重复创建。搜索时如果条件为LIKE只支持字符串"
|
||||
/>
|
||||
<el-form
|
||||
ref="fieldDialogForm"
|
||||
:model="middleDate"
|
||||
label-width="120px"
|
||||
label-position="right"
|
||||
:rules="rules"
|
||||
class="grid grid-cols-2"
|
||||
>
|
||||
<el-form-item label="字段名称" prop="fieldName">
|
||||
<el-input
|
||||
v-model="middleDate.fieldName"
|
||||
autocomplete="off"
|
||||
style="width: 80%"
|
||||
/>
|
||||
<el-button style="width: 18%; margin-left: 2%" @click="autoFill">
|
||||
<span style="font-size: 12px">自动填充</span>
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="字段中文名" prop="fieldDesc">
|
||||
<el-input v-model="middleDate.fieldDesc" autocomplete="off" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字段JSON" 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="字段类型" prop="fieldType">
|
||||
<el-select
|
||||
v-model="middleDate.fieldType"
|
||||
style="width: 100%"
|
||||
placeholder="请选择字段类型"
|
||||
clearable
|
||||
@change="clearOther"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:disabled="item.disabled"
|
||||
/>
|
||||
</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="字段查询条件" prop="fieldSearchType">
|
||||
<el-select
|
||||
v-model="middleDate.fieldSearchType"
|
||||
:disabled="middleDate.fieldType === 'json'"
|
||||
style="width: 100%"
|
||||
placeholder="请选择字段查询条件"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeSearchOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:disabled="canSelect(item.value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联字典" prop="dictType">
|
||||
<el-select
|
||||
v-model="middleDate.dictType"
|
||||
style="width: 100%"
|
||||
:disabled="middleDate.fieldType !== 'string' && middleDate.fieldType !== 'array'"
|
||||
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-input
|
||||
v-model="middleDate.defaultValue"
|
||||
placeholder="请输入默认值"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="主键">
|
||||
<el-checkbox v-model="middleDate.primaryKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="索引类型" prop="fieldIndexType">
|
||||
<el-select
|
||||
v-model="middleDate.fieldIndexType"
|
||||
:disabled="middleDate.fieldType === 'json'"
|
||||
style="width: 100%"
|
||||
placeholder="请选择字段索引类型"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeIndexOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:disabled="canSelect(item.value)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="前端新建/编辑">
|
||||
<el-switch v-model="middleDate.form" />
|
||||
</el-form-item>
|
||||
<el-form-item label="前端表格列">
|
||||
<el-switch v-model="middleDate.table" />
|
||||
</el-form-item>
|
||||
<el-form-item label="前端详情">
|
||||
<el-switch v-model="middleDate.desc" />
|
||||
</el-form-item>
|
||||
<el-form-item label="导入/导出">
|
||||
<el-switch v-model="middleDate.excel" />
|
||||
</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-switch
|
||||
v-model="middleDate.fieldSearchHide"
|
||||
:disabled="!middleDate.fieldSearchType"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="校验失败文案">
|
||||
<el-input v-model="middleDate.errorText" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item
|
||||
title="数据源配置(此配置为高级配置,如编程基础不牢,可能导致自动化代码不可用)"
|
||||
name="1"
|
||||
>
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="4">
|
||||
<el-select
|
||||
v-model="middleDate.dataSource.dbName"
|
||||
placeholder="数据库【不填则为GVA库】"
|
||||
@change="dbNameChange"
|
||||
clearable
|
||||
>
|
||||
<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-col>
|
||||
<el-col :span="4">
|
||||
<el-select
|
||||
v-model="middleDate.dataSource.association"
|
||||
placeholder="关联模式"
|
||||
@change="associationChange"
|
||||
>
|
||||
<el-option label="一对一" :value="1" />
|
||||
<el-option label="一对多" :value="2" />
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-select
|
||||
v-model="middleDate.dataSource.table"
|
||||
placeholder="请选择数据源表"
|
||||
filterable
|
||||
allow-create
|
||||
clearable
|
||||
@focus="getDBTableList"
|
||||
@change="selectDB"
|
||||
@clear="clearAccress"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dbTableList"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-select
|
||||
v-model="middleDate.dataSource.value"
|
||||
placeholder="请先选择需要存储的数据"
|
||||
>
|
||||
<template #label="{ value }">
|
||||
<span>存储: </span>
|
||||
<span style="font-weight: bold">{{ value }}</span>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in dbColumnList"
|
||||
:key="item.columnName"
|
||||
:value="item.columnName"
|
||||
>
|
||||
<span style="float: left">
|
||||
<el-tag :type="item.isPrimary ? 'primary' : 'info'">
|
||||
{{ item.isPrimary ? '主 键' : '非主键' }}
|
||||
</el-tag>
|
||||
{{ item.columnName }}</span
|
||||
>
|
||||
<span
|
||||
style="
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
"
|
||||
>
|
||||
类型:{{ item.type }}
|
||||
<block v-if="item.comment != ''"
|
||||
>,字段说明:{{ item.comment }}</block
|
||||
>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<el-col :span="5">
|
||||
<el-select
|
||||
v-model="middleDate.dataSource.label"
|
||||
placeholder="请先选择需要展示的数据"
|
||||
>
|
||||
<template #label="{ value }">
|
||||
<span>展示: </span>
|
||||
<span style="font-weight: bold">{{ value }}</span>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in dbColumnList"
|
||||
:key="item.columnName"
|
||||
:value="item.columnName"
|
||||
>
|
||||
<span style="float: left">
|
||||
<el-tag :type="item.isPrimary ? 'primary' : 'info'">
|
||||
{{ item.isPrimary ? '主 键' : '非主键' }}
|
||||
</el-tag>
|
||||
{{ item.columnName }}</span
|
||||
>
|
||||
<span
|
||||
style="
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
"
|
||||
>
|
||||
类型:{{ item.type }}
|
||||
<span v-if="item.comment != ''"
|
||||
>,字段说明:{{ item.comment }}</span
|
||||
>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<!-- <el-input v-model="middleDate.dataSource.label" placeholder="展示用字段" /> -->
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { toLowerCase, toSQLLine } from '@/utils/stringFun'
|
||||
import { getSysDictionaryList } from '@/api/sysDictionary'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { getColumn, getDB, getTable } from '@/api/autoCode'
|
||||
|
||||
defineOptions({
|
||||
name: 'FieldDialog'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
dialogMiddle: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
typeOptions: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
typeSearchOptions: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
},
|
||||
typeIndexOptions: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const activeNames = ref([])
|
||||
|
||||
const middleDate = ref({})
|
||||
const dictOptions = ref([])
|
||||
|
||||
const dbList = ref([])
|
||||
|
||||
const getDbFunc = async () => {
|
||||
const res = await getDB()
|
||||
if (res.code === 0) {
|
||||
dbList.value = res.data.dbList
|
||||
}
|
||||
}
|
||||
|
||||
const validateDataTypeLong = (rule, value, callback) => {
|
||||
const regex = /^('([^']*)'(?:,'([^']+)'*)*)$/
|
||||
if (middleDate.value.fieldType == 'enum' && !regex.test(value)) {
|
||||
callback(new Error('枚举值校验错误'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = ref({
|
||||
fieldName: [
|
||||
{ required: true, message: '请输入字段英文名', trigger: 'blur' }
|
||||
],
|
||||
fieldDesc: [
|
||||
{ required: true, message: '请输入字段中文名', trigger: 'blur' }
|
||||
],
|
||||
fieldJson: [
|
||||
{ required: true, message: '请输入字段格式化json', trigger: 'blur' }
|
||||
],
|
||||
columnName: [
|
||||
{ required: true, message: '请输入数据库字段', trigger: 'blur' }
|
||||
],
|
||||
fieldType: [{ required: true, message: '请选择字段类型', trigger: 'blur' }],
|
||||
dataTypeLong: [{ validator: validateDataTypeLong, trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const init = async () => {
|
||||
middleDate.value = props.dialogMiddle
|
||||
const dictRes = await getSysDictionaryList({
|
||||
page: 1,
|
||||
pageSize: 999999
|
||||
})
|
||||
|
||||
dictOptions.value = dictRes.data
|
||||
}
|
||||
init()
|
||||
|
||||
const autoFill = () => {
|
||||
middleDate.value.fieldJson = toLowerCase(middleDate.value.fieldName)
|
||||
middleDate.value.columnName = toSQLLine(middleDate.value.fieldJson)
|
||||
}
|
||||
|
||||
const canSelect = (item) => {
|
||||
const fieldType = middleDate.value.fieldType;
|
||||
|
||||
if (fieldType === 'richtext') {
|
||||
return item !== 'LIKE';
|
||||
}
|
||||
|
||||
if (fieldType !== 'string' && item === 'LIKE') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nonNumericTypes = ['int', 'time.Time', 'float64'];
|
||||
if (!nonNumericTypes.includes(fieldType) && ['BETWEEN', 'NOT BETWEEN'].includes(item)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const clearOther = () => {
|
||||
middleDate.value.fieldSearchType = ''
|
||||
middleDate.value.dictType = ''
|
||||
}
|
||||
|
||||
const associationChange = (val) => {
|
||||
if (val === 2) {
|
||||
ElMessageBox.confirm(
|
||||
'一对多关联模式下,数据类型会改变为数组,后端表现为json,具体表现为数组模式,是否继续?',
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '继续',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
middleDate.value.fieldType = 'array'
|
||||
})
|
||||
.catch(() => {
|
||||
middleDate.value.dataSource.association = 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clearAccress = () => {
|
||||
middleDate.value.dataSource.value = ''
|
||||
middleDate.value.dataSource.label = ''
|
||||
}
|
||||
|
||||
const clearDataSourceTable = () => {
|
||||
middleDate.value.dataSource.table = ''
|
||||
}
|
||||
|
||||
const dbNameChange = () => {
|
||||
getDBTableList()
|
||||
clearDataSourceTable()
|
||||
clearAccress()
|
||||
}
|
||||
|
||||
const dbTableList = ref([])
|
||||
|
||||
const getDBTableList = async () => {
|
||||
const res = await getTable({
|
||||
businessDB: middleDate.value.dataSource.dbName
|
||||
})
|
||||
if (res.code === 0) {
|
||||
let list = res.data.tables // 确保这里正确获取到 tables 数组
|
||||
dbTableList.value = list.map((item) => ({
|
||||
tableName: item.tableName,
|
||||
value: item.tableName // 这里假设 value 也是 tableName,如果不同请调整
|
||||
}))
|
||||
}
|
||||
clearAccress()
|
||||
}
|
||||
|
||||
const dbColumnList = ref([])
|
||||
const selectDB = async (val, isInit) => {
|
||||
middleDate.value.dataSource.hasDeletedAt = false
|
||||
middleDate.value.dataSource.table = val
|
||||
const res = await getColumn({
|
||||
businessDB: middleDate.value.dataSource.dbName,
|
||||
tableName: val
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
let list = res.data.columns // 确保这里正确获取到 tables 数组
|
||||
dbColumnList.value = list.map((item) => {
|
||||
if (item.columnName === 'deleted_at') {
|
||||
middleDate.value.dataSource.hasDeletedAt = true
|
||||
}
|
||||
return {
|
||||
columnName: item.columnName,
|
||||
value: item.columnName,
|
||||
type: item.dataType,
|
||||
isPrimary: item.primaryKey,
|
||||
comment: item.columnComment
|
||||
}
|
||||
})
|
||||
if (dbColumnList.value.length > 0 && !isInit) {
|
||||
middleDate.value.dataSource.label = dbColumnList.value[0].columnName
|
||||
middleDate.value.dataSource.value = dbColumnList.value[0].columnName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fieldDialogForm = ref(null)
|
||||
defineExpose({ fieldDialogForm })
|
||||
|
||||
onMounted(() => {
|
||||
getDbFunc()
|
||||
if (middleDate.value.dataSource.table) {
|
||||
selectDB(middleDate.value.dataSource.table, true)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
119
src/view/systemTools/autoCode/component/previewCodeDialog.vue
Normal file
119
src/view/systemTools/autoCode/component/previewCodeDialog.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<el-tabs
|
||||
v-model="activeName"
|
||||
tab-position="left"
|
||||
class="h-[calc(100vh-110px)]"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="(item, key) in useCode"
|
||||
:key="key"
|
||||
:label="key"
|
||||
:name="key"
|
||||
>
|
||||
<div :id="key" class="h-[calc(100vh-110px)] px-5 overflow-y-scroll"></div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Marked } from 'marked'
|
||||
import { markedHighlight } from 'marked-highlight'
|
||||
import hljs from 'highlight.js'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref, watchEffect } from 'vue'
|
||||
import { useAppStore } from '@/pinia'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const useCode = ref({})
|
||||
|
||||
const createKey = [
|
||||
'enter.go',
|
||||
'gorm_biz.go',
|
||||
'router_biz.go',
|
||||
'api',
|
||||
'router',
|
||||
'initialize',
|
||||
'gen.go'
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
const isDarkMode = appStore.config.darkMode === 'dark'
|
||||
if (isDarkMode) {
|
||||
import('highlight.js/styles/atom-one-dark.css')
|
||||
} else {
|
||||
import('highlight.js/styles/atom-one-light.css')
|
||||
}
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
previewCode: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
isAdd: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
for (const key in props.previewCode) {
|
||||
if (
|
||||
props.isAdd &&
|
||||
createKey.some((createKeyItem) => key.includes(createKeyItem))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
useCode.value[key] = props.previewCode[key]
|
||||
}
|
||||
})
|
||||
|
||||
const activeName = ref('')
|
||||
onMounted(() => {
|
||||
const marked = new Marked(
|
||||
markedHighlight({
|
||||
langPrefix: 'hljs language-',
|
||||
highlight(code, lang) {
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
||||
if (lang === 'vue') {
|
||||
return hljs.highlight(code, { language: 'html' }).value
|
||||
}
|
||||
return hljs.highlight(code, { language }).value
|
||||
}
|
||||
})
|
||||
)
|
||||
for (const key in useCode.value) {
|
||||
if (activeName.value === '') {
|
||||
activeName.value = key
|
||||
}
|
||||
document.getElementById(key).innerHTML = marked.parse(useCode.value[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, selectText })
|
||||
</script>
|
||||
1685
src/view/systemTools/autoCode/index.vue
Normal file
1685
src/view/systemTools/autoCode/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
620
src/view/systemTools/autoCodeAdmin/index.vue
Normal file
620
src/view/systemTools/autoCodeAdmin/index.vue
Normal file
@@ -0,0 +1,620 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button 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="description"
|
||||
/>
|
||||
<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" effect="dark">
|
||||
已回滚
|
||||
</el-tag>
|
||||
<el-tag v-else 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
|
||||
type="primary"
|
||||
link
|
||||
:disabled="scope.row.flag === 1"
|
||||
@click="addFuncBtn(scope.row)"
|
||||
>
|
||||
增加方法
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="goAutoCode(scope.row, 1)">
|
||||
增加字段
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
:disabled="scope.row.flag === 1"
|
||||
@click="openDialog(scope.row)"
|
||||
>
|
||||
回滚
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="goAutoCode(scope.row)">
|
||||
复用
|
||||
</el-button>
|
||||
<el-button 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>
|
||||
<el-dialog
|
||||
v-model="dialogFormVisible"
|
||||
:title="dialogFormTitle"
|
||||
:before-close="closeDialog"
|
||||
width="600px"
|
||||
>
|
||||
<el-form :inline="true" :model="formData" label-width="80px">
|
||||
<el-form-item label="选项:">
|
||||
<el-checkbox v-model="formData.deleteApi" label="删除接口" />
|
||||
<el-checkbox v-model="formData.deleteMenu" label="删除菜单" />
|
||||
<el-checkbox
|
||||
v-model="formData.deleteTable"
|
||||
label="删除表"
|
||||
@change="deleteTableCheck"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="closeDialog"> 取 消 </el-button>
|
||||
<el-popconfirm
|
||||
title="此操作将回滚生成文件和勾选项目, 是否继续?"
|
||||
@confirm="enterDialog"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button type="primary"> 确 定 </el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer
|
||||
v-model="funcFlag"
|
||||
size="60%"
|
||||
:show-close="false"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">操作栏</span>
|
||||
<div>
|
||||
<el-button type="primary" @click="runFunc" :loading="aiLoading">
|
||||
生成
|
||||
</el-button>
|
||||
<el-button type="primary" @click="closeFunc" :loading="aiLoading">
|
||||
取消
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="">
|
||||
<el-form
|
||||
v-loading="aiLoading"
|
||||
label-position="top"
|
||||
element-loading-text="小淼正在思考,请稍候..."
|
||||
:model="autoFunc"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="包名:">
|
||||
<el-input
|
||||
v-model="autoFunc.package"
|
||||
placeholder="请输入包名"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="结构体名:">
|
||||
<el-input
|
||||
v-model="autoFunc.structName"
|
||||
placeholder="请输入结构体名"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="前端文件名:">
|
||||
<el-input
|
||||
v-model="autoFunc.packageName"
|
||||
placeholder="请输入文件名"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="后端文件名:">
|
||||
<el-input
|
||||
v-model="autoFunc.humpPackageName"
|
||||
placeholder="请输入文件名"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="描述:">
|
||||
<el-input
|
||||
v-model="autoFunc.description"
|
||||
placeholder="请输入描述"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="缩写:">
|
||||
<el-input
|
||||
v-model="autoFunc.abbreviation"
|
||||
placeholder="请输入缩写"
|
||||
disabled
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="是否AI填充:">
|
||||
<el-switch v-model="autoFunc.isAi" />
|
||||
<span class="text-sm text-red-600 p-2"
|
||||
>当前ai帮写存在不稳定因素,生成代码后请注意手动调整部分内容</span
|
||||
>
|
||||
</el-form-item>
|
||||
<template v-if="autoFunc.isAi">
|
||||
<el-form-item label="Ai帮写:">
|
||||
<div class="relative w-full">
|
||||
<el-input
|
||||
type="textarea"
|
||||
placeholder="AI帮写功能,输入提示信息,自动生成代码"
|
||||
v-model="autoFunc.prompt"
|
||||
:rows="5"
|
||||
@input="autoFunc.router = autoFunc.router.replace(/\//g, '')"
|
||||
/>
|
||||
<el-button
|
||||
@click="aiAddFunc"
|
||||
type="primary"
|
||||
class="absolute right-2 bottom-2"
|
||||
><ai-gva />帮写</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Api方法:">
|
||||
<v-ace-editor
|
||||
v-model:value="autoFunc.apiFunc"
|
||||
lang="golang"
|
||||
theme="github_dark"
|
||||
class="h-80 w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Server方法:">
|
||||
<v-ace-editor
|
||||
v-model:value="autoFunc.serverFunc"
|
||||
lang="golang"
|
||||
theme="github_dark"
|
||||
class="h-80 w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="前端JSAPI方法:">
|
||||
<v-ace-editor
|
||||
v-model:value="autoFunc.jsFunc"
|
||||
lang="javascript"
|
||||
theme="github_dark"
|
||||
class="h-80 w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="方法介绍:">
|
||||
<div class="flex w-full gap-2">
|
||||
<el-input
|
||||
class="flex-1"
|
||||
v-model="autoFunc.funcDesc"
|
||||
placeholder="请输入方法介绍"
|
||||
/>
|
||||
<el-button type="primary" @click="autoComplete"
|
||||
><ai-gva />补全</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="方法名:">
|
||||
<el-input
|
||||
@blur="autoFunc.funcName = toUpperCase(autoFunc.funcName)"
|
||||
v-model="autoFunc.funcName"
|
||||
placeholder="请输入方法名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="方法:">
|
||||
<el-select v-model="autoFunc.method" placeholder="请选择方法">
|
||||
<el-option
|
||||
v-for="item in ['GET', 'POST', 'PUT', 'DELETE']"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否鉴权:">
|
||||
<el-switch
|
||||
v-model="autoFunc.isAuth"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由path:">
|
||||
<el-input
|
||||
v-model="autoFunc.router"
|
||||
placeholder="路由path"
|
||||
@input="autoFunc.router = autoFunc.router.replace(/\//g, '')"
|
||||
/>
|
||||
<div>
|
||||
API路径: [{{ autoFunc.method }}] /{{ autoFunc.abbreviation }}/{{
|
||||
autoFunc.router
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getSysHistory,
|
||||
rollback,
|
||||
delSysHistory,
|
||||
addFunc,
|
||||
butler
|
||||
} from '@/api/autoCode.js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { toUpperCase } from '@/utils/stringFun'
|
||||
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
import 'ace-builds/src-noconflict/mode-javascript'
|
||||
import 'ace-builds/src-noconflict/mode-golang'
|
||||
import 'ace-builds/src-noconflict/theme-github_dark'
|
||||
|
||||
defineOptions({
|
||||
name: 'AutoCodeAdmin'
|
||||
})
|
||||
|
||||
const aiLoading = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
deleteApi: true,
|
||||
deleteMenu: true,
|
||||
deleteTable: false
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const dialogFormVisible = ref(false)
|
||||
const dialogFormTitle = ref('')
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
|
||||
const activeInfo = ref('')
|
||||
|
||||
const autoFunc = ref({
|
||||
package: '',
|
||||
funcName: '',
|
||||
structName: '',
|
||||
packageName: '',
|
||||
description: '',
|
||||
abbreviation: '',
|
||||
humpPackageName: '',
|
||||
businessDB: '',
|
||||
method: '',
|
||||
funcDesc: '',
|
||||
isAuth: false,
|
||||
isAi: false,
|
||||
apiFunc: '',
|
||||
serverFunc: '',
|
||||
jsFunc: ''
|
||||
})
|
||||
|
||||
const addFuncBtn = (row) => {
|
||||
const req = JSON.parse(row.request)
|
||||
activeInfo.value = row.request
|
||||
autoFunc.value.package = req.package
|
||||
autoFunc.value.structName = req.structName
|
||||
autoFunc.value.packageName = req.packageName
|
||||
autoFunc.value.description = req.description
|
||||
autoFunc.value.abbreviation = req.abbreviation
|
||||
autoFunc.value.humpPackageName = req.humpPackageName
|
||||
autoFunc.value.businessDB = req.businessDB
|
||||
autoFunc.value.method = ''
|
||||
autoFunc.value.funcName = ''
|
||||
autoFunc.value.router = ''
|
||||
autoFunc.value.funcDesc = ''
|
||||
autoFunc.value.isAuth = false
|
||||
autoFunc.value.isAi = false
|
||||
autoFunc.value.apiFunc = ''
|
||||
autoFunc.value.serverFunc = ''
|
||||
autoFunc.value.jsFunc = ''
|
||||
funcFlag.value = true
|
||||
}
|
||||
|
||||
const funcFlag = ref(false)
|
||||
|
||||
const closeFunc = () => {
|
||||
funcFlag.value = false
|
||||
}
|
||||
|
||||
const runFunc = async () => {
|
||||
// 首字母自动转换为大写
|
||||
autoFunc.value.funcName = toUpperCase(autoFunc.value.funcName)
|
||||
|
||||
if (!autoFunc.value.funcName) {
|
||||
ElMessage.error('请输入方法名')
|
||||
return
|
||||
}
|
||||
if (!autoFunc.value.method) {
|
||||
ElMessage.error('请选择方法')
|
||||
return
|
||||
}
|
||||
if (!autoFunc.value.router) {
|
||||
ElMessage.error('请输入路由')
|
||||
return
|
||||
}
|
||||
if (!autoFunc.value.funcDesc) {
|
||||
ElMessage.error('请输入方法介绍')
|
||||
return
|
||||
}
|
||||
|
||||
if (autoFunc.value.isAi) {
|
||||
if (
|
||||
!autoFunc.value.apiFunc ||
|
||||
!autoFunc.value.serverFunc ||
|
||||
!autoFunc.value.jsFunc
|
||||
) {
|
||||
ElMessage.error('请先使用AI帮写完成基础代码,如果生成失败请重新调用')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const res = await addFunc(autoFunc.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('增加方法成功')
|
||||
closeFunc()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
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 openDialog = (row) => {
|
||||
dialogFormTitle.value = '回滚:' + row.structName
|
||||
formData.value.id = row.ID
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
deleteApi: true,
|
||||
deleteMenu: true,
|
||||
deleteTable: false
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除表
|
||||
const deleteTableCheck = (flag) => {
|
||||
if (flag) {
|
||||
ElMessageBox.confirm(
|
||||
`此操作将删除自动创建的文件和api(会删除表!!!), 是否继续?`,
|
||||
'提示',
|
||||
{
|
||||
closeOnClickModal: false,
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
ElMessageBox.confirm(
|
||||
`此操作将删除自动创建的文件和api(会删除表!!!), 请继续确认!!!`,
|
||||
'会删除表',
|
||||
{
|
||||
closeOnClickModal: false,
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).catch(() => {
|
||||
formData.value.deleteTable = false
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
formData.value.deleteTable = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const enterDialog = async () => {
|
||||
const res = await rollback(formData.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('回滚成功')
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const goAutoCode = (row, isAdd) => {
|
||||
if (row) {
|
||||
router.push({
|
||||
name: 'autoCodeEdit',
|
||||
params: {
|
||||
id: row.ID
|
||||
},
|
||||
query: {
|
||||
isAdd: isAdd
|
||||
}
|
||||
})
|
||||
} else {
|
||||
router.push({ name: 'autoCode' })
|
||||
}
|
||||
}
|
||||
|
||||
const aiAddFunc = async () => {
|
||||
aiLoading.value = true
|
||||
autoFunc.value.apiFunc = ''
|
||||
autoFunc.value.serverFunc = ''
|
||||
autoFunc.value.jsFunc = ''
|
||||
|
||||
if (!autoFunc.value.prompt) {
|
||||
ElMessage.error('请输入提示信息')
|
||||
return
|
||||
}
|
||||
|
||||
const res = await addFunc({ ...autoFunc.value, isPreview: true })
|
||||
if (res.code !== 0) {
|
||||
aiLoading.value = false
|
||||
ElMessage.error(res.msg)
|
||||
return
|
||||
}
|
||||
|
||||
const aiRes = await butler({
|
||||
structInfo: activeInfo.value,
|
||||
template: JSON.stringify(res.data),
|
||||
prompt: autoFunc.value.prompt,
|
||||
command: 'addFunc'
|
||||
})
|
||||
aiLoading.value = false
|
||||
if (aiRes.code === 0) {
|
||||
try {
|
||||
const aiData = JSON.parse(aiRes.data)
|
||||
autoFunc.value.apiFunc = aiData.api
|
||||
autoFunc.value.serverFunc = aiData.server
|
||||
autoFunc.value.jsFunc = aiData.js
|
||||
autoFunc.value.method = aiData.method
|
||||
autoFunc.value.funcName = aiData.funcName
|
||||
const routerArr = aiData.router.split('/')
|
||||
autoFunc.value.router = routerArr[routerArr.length - 1]
|
||||
autoFunc.value.funcDesc = autoFunc.value.prompt
|
||||
} catch (_) {
|
||||
ElMessage.error('小淼忙碌,请重新调用')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const autoComplete = async () => {
|
||||
aiLoading.value = true
|
||||
const aiRes = await butler({
|
||||
prompt: autoFunc.value.funcDesc,
|
||||
command: 'autoCompleteFunc'
|
||||
})
|
||||
aiLoading.value = false
|
||||
if (aiRes.code === 0) {
|
||||
try {
|
||||
const aiData = JSON.parse(aiRes.data)
|
||||
autoFunc.value.method = aiData.method
|
||||
autoFunc.value.funcName = aiData.funcName
|
||||
autoFunc.value.router = aiData.router
|
||||
autoFunc.value.prompt = autoFunc.value.funcDesc
|
||||
} catch (_) {
|
||||
ElMessage.error('小淼开小差了,请重新调用')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
205
src/view/systemTools/autoPkg/autoPkg.vue
Normal file
205
src/view/systemTools/autoPkg/autoPkg.vue
Normal file
@@ -0,0 +1,205 @@
|
||||
<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 gap-3 flex items-center">
|
||||
<el-button type="primary" icon="plus" @click="openDialog('addApi')">
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="tableData">
|
||||
<el-table-column align="left" label="id" width="120" prop="ID" />
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="包名"
|
||||
width="150"
|
||||
prop="packageName"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="模板"
|
||||
width="150"
|
||||
prop="template"
|
||||
/>
|
||||
<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"
|
||||
type="primary"
|
||||
link
|
||||
@click="deleteApiFunc(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-drawer v-model="dialogFormVisible" size="40%" :show-close="false">
|
||||
<warning-bar
|
||||
title="模板package会创建集成于项目本体中的代码包,模板plugin会创建插件包"
|
||||
/>
|
||||
<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="template">
|
||||
<el-select v-model="form.template">
|
||||
<el-option
|
||||
v-for="template in templatesOptions"
|
||||
:label="template"
|
||||
:value="template"
|
||||
:key="template"
|
||||
/>
|
||||
</el-select>
|
||||
</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 #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">创建Package</span>
|
||||
<div>
|
||||
<el-button @click="closeDialog"> 取 消 </el-button>
|
||||
<el-button type="primary" @click="enterDialog"> 确 定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createPackageApi,
|
||||
getPackageApi,
|
||||
deletePackageApi,
|
||||
getTemplatesApi
|
||||
} from '@/api/autoCode'
|
||||
import { ref } from 'vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'AutoPkg'
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
packageName: '',
|
||||
template: '',
|
||||
label: '',
|
||||
desc: ''
|
||||
})
|
||||
const templatesOptions = ref([])
|
||||
|
||||
const getTemplates = async () => {
|
||||
const res = await getTemplatesApi()
|
||||
if (res.code === 0) {
|
||||
templatesOptions.value = res.data
|
||||
}
|
||||
}
|
||||
|
||||
getTemplates()
|
||||
|
||||
const validateData = (rule, value, callback) => {
|
||||
if (/[\u4E00-\u9FA5]/g.test(value)) {
|
||||
callback(new Error('不能为中文'))
|
||||
} else if (/^\d+$/.test(value[0])) {
|
||||
callback(new Error('不能够以数字开头'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = ref({
|
||||
packageName: [
|
||||
{ required: true, message: '请输入包名', trigger: 'blur' },
|
||||
{ validator: validateData, trigger: 'blur' }
|
||||
],
|
||||
template: [
|
||||
{ required: true, message: '请选择模板', trigger: 'change' },
|
||||
{ validator: validateData, trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const dialogFormVisible = ref(false)
|
||||
const openDialog = () => {
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
form.value = {
|
||||
packageName: '',
|
||||
template: '',
|
||||
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>
|
||||
32
src/view/systemTools/exportTemplate/code.js
Normal file
32
src/view/systemTools/exportTemplate/code.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export const getCode = (templateID) => {
|
||||
return `<template>
|
||||
<!-- 导出组件 -->
|
||||
<ExportExcel templateId="${templateID}" :condition="condition" :limit="limit" :offset="offset" :order="order" />
|
||||
|
||||
<!-- 导入组件 handleSuccess为导入成功后的回调函数 -->
|
||||
<ImportExcel templateId="${templateID}" @on-success="handleSuccess" />
|
||||
|
||||
<!-- 导出模板 -->
|
||||
<ExportTemplate templateId="${templateID}" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
// 导出组件
|
||||
import ExportExcel from '@/components/exportExcel/exportExcel.vue';
|
||||
// 导入组件
|
||||
import ImportExcel from '@/components/exportExcel/importExcel.vue';
|
||||
// 导出模板组件
|
||||
import ExportTemplate from '@/components/exportExcel/exportTemplate.vue';
|
||||
|
||||
const condition = ref({}); // 查询条件
|
||||
const limit = ref(10); // 最大条数限制
|
||||
const offset = ref(0); // 偏移量
|
||||
const order = ref('id desc'); // 排序条件
|
||||
|
||||
const handleSuccess = (res) => {
|
||||
console.log(res);
|
||||
// 导入成功的回调函数
|
||||
};
|
||||
</script>`
|
||||
}
|
||||
996
src/view/systemTools/exportTemplate/exportTemplate.vue
Normal file
996
src/view/systemTools/exportTemplate/exportTemplate.vue
Normal file
@@ -0,0 +1,996 @@
|
||||
<template>
|
||||
<div>
|
||||
<WarningBar
|
||||
title="本功能提供同步的表格导出功能,大数据量的异步表格导出功能,可以选择点我定制"
|
||||
href="https://flipped-aurora.feishu.cn/docx/KwjxdnvatozgwIxGV0rcpkZSn4d"
|
||||
/>
|
||||
<div class="gva-search-box">
|
||||
<el-form
|
||||
ref="elSearchFormRef"
|
||||
:inline="true"
|
||||
:model="searchInfo"
|
||||
class="demo-form-inline"
|
||||
:rules="searchRule"
|
||||
@keyup.enter="onSubmit"
|
||||
>
|
||||
<el-form-item label="创建日期" prop="createdAt">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip
|
||||
content="搜索范围是开始日期(包含)至结束日期(不包含)"
|
||||
>
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-date-picker
|
||||
v-model="searchInfo.startCreatedAt"
|
||||
type="datetime"
|
||||
placeholder="开始日期"
|
||||
:disabled-date="
|
||||
(time) =>
|
||||
searchInfo.endCreatedAt
|
||||
? time.getTime() > searchInfo.endCreatedAt.getTime()
|
||||
: false
|
||||
"
|
||||
/>
|
||||
—
|
||||
<el-date-picker
|
||||
v-model="searchInfo.endCreatedAt"
|
||||
type="datetime"
|
||||
placeholder="结束日期"
|
||||
:disabled-date="
|
||||
(time) =>
|
||||
searchInfo.startCreatedAt
|
||||
? time.getTime() < searchInfo.startCreatedAt.getTime()
|
||||
: false
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="searchInfo.name" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="表名称" prop="tableName">
|
||||
<el-input v-model="searchInfo.tableName" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板标识" prop="templateID">
|
||||
<el-input v-model="searchInfo.templateID" placeholder="搜索条件" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit"
|
||||
>查询</el-button
|
||||
>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog"
|
||||
>新增</el-button
|
||||
>
|
||||
|
||||
<el-button
|
||||
icon="delete"
|
||||
style="margin-left: 10px"
|
||||
:disabled="!multipleSelection.length"
|
||||
@click="onDelete"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<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="数据库" width="120">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.dbName || 'GVA库' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="模板标识"
|
||||
prop="templateID"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="模板名称"
|
||||
prop="name"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="表名称"
|
||||
prop="tableName"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
align="left"
|
||||
label="模板信息"
|
||||
prop="templateInfo"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column align="left" label="操作" min-width="280">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="documentCopy"
|
||||
class="table-button"
|
||||
@click="copyFunc(scope.row)"
|
||||
>复制</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit-pen"
|
||||
class="table-button"
|
||||
@click="showCode(scope.row)"
|
||||
>代码</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="edit"
|
||||
class="table-button"
|
||||
@click="updateSysExportTemplateFunc(scope.row)"
|
||||
>变更</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
icon="delete"
|
||||
@click="deleteRow(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<el-drawer
|
||||
v-model="dialogFormVisible"
|
||||
size="60%"
|
||||
:before-close="closeDialog"
|
||||
:title="type === 'create' ? '添加' : '修改'"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ type === 'create' ? '添加' : '修改' }}</span>
|
||||
<div>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
<el-button type="primary" @click="enterDialog">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
ref="elFormRef"
|
||||
:model="formData"
|
||||
label-position="right"
|
||||
:rules="rule"
|
||||
label-width="100px"
|
||||
v-loading="aiLoading"
|
||||
element-loading-text="小淼正在思考..."
|
||||
>
|
||||
<el-form-item label="业务库" prop="dbName">
|
||||
<template #label>
|
||||
<el-tooltip
|
||||
content="注:需要提前到db-list自行配置多数据库,如未配置需配置后重启服务方可使用。若无法选择,请到config.yaml中设置disabled:false,选择导入导出的目标库。"
|
||||
placement="bottom"
|
||||
effect="light"
|
||||
>
|
||||
<div>
|
||||
业务库 <el-icon><QuestionFilled /></el-icon>
|
||||
</div>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select
|
||||
v-model="formData.dbName"
|
||||
clearable
|
||||
@change="dbNameChange"
|
||||
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 label="需用到的表" prop="tables">
|
||||
<el-select
|
||||
multiple
|
||||
v-model="tables"
|
||||
clearable
|
||||
placeholder="使用AI的情况下请选择"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tableOptions"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="AI帮写:" prop="ai">
|
||||
<div class="relative w-full">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="prompt"
|
||||
:clearable="true"
|
||||
:rows="5"
|
||||
placeholder="试试描述你要做的导出功能让AI帮你完成,在此之前请选择你需要导出的表所在的业务库,如不做选择,则默认使用gva库"
|
||||
/>
|
||||
<el-button
|
||||
class="absolute bottom-2 right-2"
|
||||
type="primary"
|
||||
@click="autoExport"
|
||||
><el-icon><ai-gva /></el-icon>帮写</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="表名称:" clearable prop="tableName">
|
||||
<div class="w-full flex gap-4">
|
||||
<el-select
|
||||
v-model="formData.tableName"
|
||||
class="flex-1"
|
||||
filterable
|
||||
placeholder="请选择表"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in tableOptions"
|
||||
:key="item.tableName"
|
||||
:label="item.tableName"
|
||||
:value="item.tableName"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
:disabled="!formData.tableName"
|
||||
type="primary"
|
||||
@click="getColumnFunc(true)"
|
||||
><el-icon><ai-gva /></el-icon>自动补全</el-button
|
||||
>
|
||||
<el-button
|
||||
:disabled="!formData.tableName"
|
||||
type="primary"
|
||||
@click="getColumnFunc(false)"
|
||||
>自动生成模板</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模板名称:" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
:clearable="true"
|
||||
placeholder="请输入模板名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模板标识:" prop="templateID">
|
||||
<el-input
|
||||
v-model="formData.templateID"
|
||||
:clearable="true"
|
||||
placeholder="模板标识为前端组件需要挂在的标识属性"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="关联条件:">
|
||||
<div
|
||||
v-for="(join, key) in formData.joinTemplate"
|
||||
:key="key"
|
||||
class="flex gap-4 w-full mb-2"
|
||||
>
|
||||
<el-select v-model="join.joins" placeholder="请选择关联方式">
|
||||
<el-option label="LEFT JOIN" value="LEFT JOIN" />
|
||||
<el-option label="INNER JOIN" value="INNER JOIN" />
|
||||
<el-option label="RIGHT JOIN" value="RIGHT JOIN" />
|
||||
</el-select>
|
||||
<el-input v-model="join.table" placeholder="请输入关联表" />
|
||||
<el-input
|
||||
v-model="join.on"
|
||||
placeholder="关联条件 table1.a = table2.b"
|
||||
/>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="delete"
|
||||
@click="() => formData.joinTemplate.splice(key, 1)"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-end w-full">
|
||||
<el-button type="primary" icon="plus" @click="addJoin"
|
||||
>添加条件</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模板信息:" prop="templateInfo">
|
||||
<el-input
|
||||
v-model="formData.templateInfo"
|
||||
type="textarea"
|
||||
:rows="12"
|
||||
:clearable="true"
|
||||
:placeholder="templatePlaceholder"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认导出条数:">
|
||||
<el-input-number
|
||||
v-model="formData.limit"
|
||||
:step="1"
|
||||
:step-strictly="true"
|
||||
:precision="0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认排序条件:">
|
||||
<el-input v-model="formData.order" placeholder="例:id desc" />
|
||||
</el-form-item>
|
||||
<el-form-item label="导出条件:">
|
||||
<div
|
||||
v-for="(condition, key) in formData.conditions"
|
||||
:key="key"
|
||||
class="flex gap-4 w-full mb-2"
|
||||
>
|
||||
<el-input
|
||||
v-model="condition.from"
|
||||
placeholder="需要从查询条件取的json key"
|
||||
/>
|
||||
<el-input v-model="condition.column" placeholder="表对应的column" />
|
||||
<el-select
|
||||
v-model="condition.operator"
|
||||
placeholder="请选择查询条件"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in typeSearchOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="delete"
|
||||
@click="() => formData.conditions.splice(key, 1)"
|
||||
>删除</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="flex justify-end w-full">
|
||||
<el-button type="primary" icon="plus" @click="addCondition"
|
||||
>添加条件</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer
|
||||
v-model="codeVisible"
|
||||
size="60%"
|
||||
:before-close="closeDialog"
|
||||
:title="type === 'create' ? '添加' : '修改'"
|
||||
:show-close="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">模板</span>
|
||||
<div>
|
||||
<el-button type="primary" @click="closeDialog">确 定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<v-ace-editor
|
||||
v-model:value="webCode"
|
||||
lang="vue"
|
||||
theme="github_dark"
|
||||
class="h-full"
|
||||
/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
createSysExportTemplate,
|
||||
deleteSysExportTemplate,
|
||||
deleteSysExportTemplateByIds,
|
||||
updateSysExportTemplate,
|
||||
findSysExportTemplate,
|
||||
getSysExportTemplateList
|
||||
} from '@/api/exportTemplate.js'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { getDB, getTable, getColumn, butler } from '@/api/autoCode'
|
||||
import { getCode } from './code'
|
||||
import { VAceEditor } from 'vue3-ace-editor'
|
||||
|
||||
import 'ace-builds/src-noconflict/mode-vue'
|
||||
import 'ace-builds/src-noconflict/theme-github_dark'
|
||||
|
||||
defineOptions({
|
||||
name: 'ExportTemplate'
|
||||
})
|
||||
|
||||
const templatePlaceholder = `模板信息格式:key标识数据库column列名称(在join模式下需要写为 table.column),value标识导出excel列名称,如key为数据库关键字或函数,请按照关键字的处理模式处理,当前以mysql为例,如下:
|
||||
{
|
||||
"table_column1":"第一列",
|
||||
"table_column3":"第三列",
|
||||
"table_column4":"第四列",
|
||||
"\`rows\`":"我属于数据库关键字或函数",
|
||||
}
|
||||
如果增加了JOINS导出key应该列为 {table_name1.table_column1:"第一列",table_name2.table_column2:"第二列"}
|
||||
如果有重复的列名导出格式应为 {table_name1.table_column1 as key:"第一列",table_name2.table_column2 as key2:"第二列"}
|
||||
JOINS模式下不支持导入
|
||||
`
|
||||
|
||||
// 自动化生成的字典(可能为空)以及字段
|
||||
const formData = ref({
|
||||
name: '',
|
||||
tableName: '',
|
||||
dbName: '',
|
||||
templateID: '',
|
||||
templateInfo: '',
|
||||
limit: 0,
|
||||
order: '',
|
||||
conditions: [],
|
||||
joinTemplate: []
|
||||
})
|
||||
|
||||
const prompt = ref('')
|
||||
const tables = 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 addCondition = () => {
|
||||
formData.value.conditions.push({
|
||||
from: '',
|
||||
column: '',
|
||||
operator: ''
|
||||
})
|
||||
}
|
||||
|
||||
const addJoin = () => {
|
||||
formData.value.joinTemplate.push({
|
||||
joins: 'LEFT JOIN',
|
||||
table: '',
|
||||
on: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 验证规则
|
||||
const rule = reactive({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
tableName: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
templateID: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
templateInfo: [
|
||||
{
|
||||
required: true,
|
||||
message: '',
|
||||
trigger: ['input', 'blur']
|
||||
},
|
||||
{
|
||||
whitespace: true,
|
||||
message: '不能只输入空格',
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const searchRule = reactive({
|
||||
createdAt: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (
|
||||
searchInfo.value.startCreatedAt &&
|
||||
!searchInfo.value.endCreatedAt
|
||||
) {
|
||||
callback(new Error('请填写结束日期'))
|
||||
} else if (
|
||||
!searchInfo.value.startCreatedAt &&
|
||||
searchInfo.value.endCreatedAt
|
||||
) {
|
||||
callback(new Error('请填写开始日期'))
|
||||
} else if (
|
||||
searchInfo.value.startCreatedAt &&
|
||||
searchInfo.value.endCreatedAt &&
|
||||
(searchInfo.value.startCreatedAt.getTime() ===
|
||||
searchInfo.value.endCreatedAt.getTime() ||
|
||||
searchInfo.value.startCreatedAt.getTime() >
|
||||
searchInfo.value.endCreatedAt.getTime())
|
||||
) {
|
||||
callback(new Error('开始日期应当早于结束日期'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
// =========== 表格控制部分 ===========
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
|
||||
const dbList = ref([])
|
||||
const tableOptions = ref([])
|
||||
const aiLoading = ref(false)
|
||||
|
||||
const getTablesCloumn = async () => {
|
||||
const tablesMap = {}
|
||||
const promises = tables.value.map(async (item) => {
|
||||
const res = await getColumn({
|
||||
businessDB: formData.value.dbName,
|
||||
tableName: item
|
||||
})
|
||||
if (res.code === 0) {
|
||||
tablesMap[item] = res.data.columns
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
return tablesMap
|
||||
}
|
||||
|
||||
const autoExport = async () => {
|
||||
if (tables.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请先选择需要参与导出的表'
|
||||
})
|
||||
return
|
||||
}
|
||||
aiLoading.value = true
|
||||
const tableMap = await getTablesCloumn()
|
||||
const aiRes = await butler({
|
||||
prompt: prompt.value,
|
||||
businessDB: formData.value.dbName || '',
|
||||
tableMap: tableMap,
|
||||
command: 'autoExportTemplate'
|
||||
})
|
||||
aiLoading.value = false
|
||||
if (aiRes.code === 0) {
|
||||
const aiData = JSON.parse(aiRes.data)
|
||||
formData.value.name = aiData.name
|
||||
formData.value.tableName = aiData.tableName
|
||||
formData.value.templateID = aiData.templateID
|
||||
formData.value.templateInfo = JSON.stringify(aiData.templateInfo, null, 2)
|
||||
formData.value.joinTemplate = aiData.joinTemplate
|
||||
}
|
||||
}
|
||||
|
||||
const getDbFunc = async () => {
|
||||
const res = await getDB()
|
||||
if (res.code === 0) {
|
||||
dbList.value = res.data.dbList
|
||||
}
|
||||
}
|
||||
|
||||
getDbFunc()
|
||||
|
||||
const dbNameChange = () => {
|
||||
formData.value.tableName = ''
|
||||
formData.value.templateInfo = ''
|
||||
tables.value = []
|
||||
getTableFunc()
|
||||
}
|
||||
|
||||
const getTableFunc = async () => {
|
||||
const res = await getTable({ businessDB: formData.value.dbName })
|
||||
if (res.code === 0) {
|
||||
tableOptions.value = res.data.tables
|
||||
}
|
||||
formData.value.tableName = ''
|
||||
}
|
||||
getTableFunc()
|
||||
const getColumnFunc = async (aiFLag) => {
|
||||
if (!formData.value.tableName) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请先选择业务库及选择表后再进行操作'
|
||||
})
|
||||
return
|
||||
}
|
||||
formData.value.templateInfo = ''
|
||||
aiLoading.value = true
|
||||
const res = await getColumn({
|
||||
businessDB: formData.value.dbName,
|
||||
tableName: formData.value.tableName
|
||||
})
|
||||
if (res.code === 0) {
|
||||
if (aiFLag) {
|
||||
const aiRes = await butler({
|
||||
data: res.data.columns,
|
||||
command: 'exportCompletion'
|
||||
})
|
||||
if (aiRes.code === 0) {
|
||||
const aiData = JSON.parse(aiRes.data)
|
||||
aiLoading.value = false
|
||||
formData.value.templateInfo = JSON.stringify(
|
||||
aiData.templateInfo,
|
||||
null,
|
||||
2
|
||||
)
|
||||
formData.value.name = aiData.name
|
||||
formData.value.templateID = aiData.templateID
|
||||
return
|
||||
}
|
||||
ElMessage.warning('AI自动补全失败,已调整为逻辑填写')
|
||||
}
|
||||
|
||||
// 把返回值的data.columns做尊换,制作一组JSON数据,columnName做key,columnComment做value
|
||||
const templateInfo = {}
|
||||
res.data.columns.forEach((item) => {
|
||||
templateInfo[item.columnName] = item.columnComment || item.columnName
|
||||
})
|
||||
formData.value.templateInfo = JSON.stringify(templateInfo, null, 2)
|
||||
}
|
||||
aiLoading.value = false
|
||||
}
|
||||
|
||||
// 重置
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
// 分页
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 修改页面容量
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 查询
|
||||
const getTableData = async () => {
|
||||
const table = await getSysExportTemplateList({
|
||||
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 setOptions = async () => {}
|
||||
|
||||
// 获取需要的字典 可能为空 按需保留
|
||||
setOptions()
|
||||
|
||||
// 多选数据
|
||||
const multipleSelection = ref([])
|
||||
// 多选
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteSysExportTemplateFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
// 多选删除
|
||||
const onDelete = async () => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const ids = []
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: '请选择要删除的数据'
|
||||
})
|
||||
return
|
||||
}
|
||||
multipleSelection.value &&
|
||||
multipleSelection.value.map((item) => {
|
||||
ids.push(item.ID)
|
||||
})
|
||||
const res = await deleteSysExportTemplateByIds({ ids })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === ids.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 行为控制标记(弹窗内部需要增还是改)
|
||||
const type = ref('')
|
||||
|
||||
// 复制
|
||||
const copyFunc = async (row) => {
|
||||
let copyData
|
||||
const res = await findSysExportTemplate({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
copyData = JSON.parse(JSON.stringify(res.data.resysExportTemplate))
|
||||
if (!copyData.conditions) {
|
||||
copyData.conditions = []
|
||||
}
|
||||
if (!copyData.joinTemplate) {
|
||||
copyData.joinTemplate = []
|
||||
}
|
||||
delete copyData.ID
|
||||
delete copyData.CreatedAt
|
||||
delete copyData.UpdatedAt
|
||||
copyData.templateID = copyData.templateID + '_copy'
|
||||
copyData.name = copyData.name + '_copy'
|
||||
formData.value = copyData
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 更新行
|
||||
const updateSysExportTemplateFunc = async (row) => {
|
||||
const res = await findSysExportTemplate({ ID: row.ID })
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = res.data.resysExportTemplate
|
||||
if (!formData.value.conditions) {
|
||||
formData.value.conditions = []
|
||||
}
|
||||
if (!formData.value.joinTemplate) {
|
||||
formData.value.joinTemplate = []
|
||||
}
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 删除行
|
||||
const deleteSysExportTemplateFunc = async (row) => {
|
||||
const res = await deleteSysExportTemplate({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '删除成功'
|
||||
})
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
const codeVisible = ref(false)
|
||||
// 弹窗控制标记
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
const webCode = ref('')
|
||||
|
||||
const showCode = (row) => {
|
||||
webCode.value = getCode(row.templateID)
|
||||
codeVisible.value = true
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const closeDialog = () => {
|
||||
codeVisible.value = false
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
name: '',
|
||||
tableName: '',
|
||||
templateID: '',
|
||||
templateInfo: '',
|
||||
limit: 0,
|
||||
order: '',
|
||||
conditions: [],
|
||||
joinTemplate: []
|
||||
}
|
||||
}
|
||||
// 弹窗确定
|
||||
const enterDialog = async () => {
|
||||
// 判断 formData.templateInfo 是否为标准json格式 如果不是标准json 则辅助调整
|
||||
try {
|
||||
JSON.parse(formData.value.templateInfo)
|
||||
} catch (_) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '模板信息格式不正确,请检查'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const reqData = JSON.parse(JSON.stringify(formData.value))
|
||||
for (let i = 0; i < reqData.conditions.length; i++) {
|
||||
if (
|
||||
!reqData.conditions[i].from ||
|
||||
!reqData.conditions[i].column ||
|
||||
!reqData.conditions[i].operator
|
||||
) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请填写完整的导出条件'
|
||||
})
|
||||
return
|
||||
}
|
||||
reqData.conditions[i].templateID = reqData.templateID
|
||||
}
|
||||
|
||||
for (let i = 0; i < reqData.joinTemplate.length; i++) {
|
||||
if (!reqData.joinTemplate[i].joins || !reqData.joinTemplate[i].on) {
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: '请填写完整的关联'
|
||||
})
|
||||
return
|
||||
}
|
||||
reqData.joinTemplate[i].templateID = reqData.templateID
|
||||
}
|
||||
|
||||
elFormRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await createSysExportTemplate(reqData)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateSysExportTemplate(reqData)
|
||||
break
|
||||
default:
|
||||
res = await createSysExportTemplate(reqData)
|
||||
break
|
||||
}
|
||||
if (res.code === 0) {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '创建/更改成功'
|
||||
})
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
18
src/view/systemTools/formCreate/index.vue
Normal file
18
src/view/systemTools/formCreate/index.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<fc-designer ref="designer" :config="config" height="calc(100vh - 160px)" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import FcDesigner from '@form-create/designer'
|
||||
|
||||
defineOptions({
|
||||
name: 'FormGenerator'
|
||||
})
|
||||
|
||||
const designer = ref(null)
|
||||
|
||||
const config = {
|
||||
fieldReadonly: false
|
||||
}
|
||||
</script>
|
||||
20
src/view/systemTools/index.vue
Normal file
20
src/view/systemTools/index.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<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 setup>
|
||||
import { useRouterStore } from '@/pinia/modules/router'
|
||||
const routerStore = useRouterStore()
|
||||
|
||||
defineOptions({
|
||||
name: 'System'
|
||||
})
|
||||
</script>
|
||||
42
src/view/systemTools/installPlugin/index.vue
Normal file
42
src/view/systemTools/installPlugin/index.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="gva-form-box">
|
||||
<el-upload
|
||||
drag
|
||||
:action="`${getBaseUrl()}/autoCode/installPlugin`"
|
||||
:show-file-list="false"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleSuccess"
|
||||
:headers="{'x-token': token}"
|
||||
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 { ElMessage } from 'element-plus'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useUserStore } from "@/pinia";
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const token = userStore.token
|
||||
|
||||
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>
|
||||
225
src/view/systemTools/pubPlug/pubPlug.vue
Normal file
225
src/view/systemTools/pubPlug/pubPlug.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<div class="gva-form-box">
|
||||
<div class="p-4 bg-white dark:bg-slate-900">
|
||||
<WarningBar
|
||||
title="目前只支持标准插件(通过插件模板生成的标准目录插件),非标准插件请自行打包"
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<el-input v-model="plugName" placeholder="插件模板处填写的【插件名】" />
|
||||
</div>
|
||||
<el-card class="mt-2 text-center">
|
||||
<WarningBar title="穿梭框请只选择子级菜单即可" />
|
||||
<el-input
|
||||
v-model="parentMenu"
|
||||
placeholder="请输入菜单组名,例:公告管理"
|
||||
class="mb-2"
|
||||
></el-input>
|
||||
<el-transfer
|
||||
v-model="menus"
|
||||
:props="{
|
||||
key: 'ID'
|
||||
}"
|
||||
class="plugin-transfer"
|
||||
:data="menusData"
|
||||
filterable
|
||||
:filter-method="filterMenuMethod"
|
||||
filter-placeholder="请输入菜单名称/路径"
|
||||
:titles="['可选菜单', '使用菜单']"
|
||||
:button-texts="['移除', '选中']"
|
||||
>
|
||||
<template #default="{ option }">
|
||||
{{ option.meta.title }} {{ option.component }}
|
||||
</template>
|
||||
</el-transfer>
|
||||
<div class="flex justify-end mt-2">
|
||||
<el-button type="primary" @click="fmtInitMenu">
|
||||
定义安装菜单
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card class="mt-2 text-center">
|
||||
<el-transfer
|
||||
v-model="apis"
|
||||
:props="{
|
||||
key: 'ID'
|
||||
}"
|
||||
class="plugin-transfer"
|
||||
:data="apisData"
|
||||
filterable
|
||||
:filter-method="filterApiMethod"
|
||||
filter-placeholder="请输入API描述/PATH"
|
||||
:titles="['可选API', '使用API']"
|
||||
:button-texts="['移除', '选中']"
|
||||
>
|
||||
<template #default="{ option }">
|
||||
{{ option.description }} {{ option.path }}
|
||||
</template>
|
||||
</el-transfer>
|
||||
<div class="flex justify-end mt-2">
|
||||
<el-button type="primary" @click="fmtInitAPI">
|
||||
定义安装API
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<el-button type="primary" @click="pubPlugin"> 打包插件 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import WarningBar from '@/components/warningBar/warningBar.vue'
|
||||
import { pubPlug, initMenu, initAPI } from '@/api/autoCode.js'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { getAllApis } from '@/api/api'
|
||||
import { getMenuList } from '@/api/menu'
|
||||
|
||||
const plugName = ref('')
|
||||
|
||||
const menus = ref([])
|
||||
const menusData = ref([])
|
||||
const apis = ref([])
|
||||
const apisData = ref([])
|
||||
const parentMenu = ref('')
|
||||
|
||||
const fmtMenu = (menus) => {
|
||||
// 如果menu存在children,递归展开到一级
|
||||
const res = []
|
||||
menus.forEach((item) => {
|
||||
if (item.children) {
|
||||
res.push(...fmtMenu(item.children))
|
||||
} else {
|
||||
res.push(item)
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const initData = async () => {
|
||||
const menuRes = await getMenuList()
|
||||
if (menuRes.code === 0) {
|
||||
menusData.value = fmtMenu(menuRes.data)
|
||||
}
|
||||
const apiRes = await getAllApis()
|
||||
if (apiRes.code === 0) {
|
||||
apisData.value = apiRes.data.apis
|
||||
}
|
||||
}
|
||||
|
||||
const filterMenuMethod = (query, item) => {
|
||||
return (
|
||||
item.meta.title.indexOf(query) > -1 || item.component.indexOf(query) > -1
|
||||
)
|
||||
}
|
||||
|
||||
const filterApiMethod = (query, item) => {
|
||||
return item.description.indexOf(query) > -1 || item.path.indexOf(query) > -1
|
||||
}
|
||||
|
||||
initData()
|
||||
|
||||
const pubPlugin = async () => {
|
||||
ElMessageBox.confirm(
|
||||
`请检查server下的/plugin/${plugName.value}/plugin.go是否已放开需要的 initialize.Api(ctx) 和 initialize.Menu(ctx)?`,
|
||||
'打包',
|
||||
{
|
||||
confirmButtonText: '打包',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
const res = await pubPlug({ plugName: plugName.value })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(res.msg)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '关闭打包'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const fmtInitMenu = () => {
|
||||
if (!parentMenu.value) {
|
||||
ElMessage.error('请填写菜单组名')
|
||||
return
|
||||
}
|
||||
if (menus.value.length === 0) {
|
||||
ElMessage.error('请至少选择一个菜单')
|
||||
return
|
||||
}
|
||||
if (plugName.value === '') {
|
||||
ElMessage.error('请填写插件名')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`点击后将会覆盖server下的/plugin/${plugName.value}/initialize/menu. 是否继续?`,
|
||||
'生成初始菜单',
|
||||
{
|
||||
confirmButtonText: '生成',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const req = {
|
||||
plugName: plugName.value,
|
||||
parentMenu: parentMenu.value,
|
||||
menus: menus.value
|
||||
}
|
||||
initMenu(req)
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '关闭生成菜单'
|
||||
})
|
||||
})
|
||||
}
|
||||
const fmtInitAPI = () => {
|
||||
if (apis.value.length === 0) {
|
||||
ElMessage.error('请至少选择一个API')
|
||||
return
|
||||
}
|
||||
if (plugName.value === '') {
|
||||
ElMessage.error('请填写插件名')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`点击后将会覆盖server下的/plugin/${plugName.value}/initialize/api. 是否继续?`,
|
||||
'生成初始API',
|
||||
{
|
||||
confirmButtonText: '生成',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const req = {
|
||||
plugName: plugName.value,
|
||||
apis: apis.value
|
||||
}
|
||||
initAPI(req)
|
||||
console.log(req)
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage({
|
||||
type: 'info',
|
||||
message: '关闭生成API'
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.plugin-transfer {
|
||||
.el-transfer-panel {
|
||||
width: 400px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1085
src/view/systemTools/system/system.vue
Normal file
1085
src/view/systemTools/system/system.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user