refactor(frontend): 重命名前端项目为 gym-manage-web
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<div class="exception-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索操作人或异常信息"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="操作人"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="operation"
|
||||
label="操作模块"
|
||||
sortable="custom"
|
||||
width="150"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="method"
|
||||
label="请求方法"
|
||||
sortable="custom"
|
||||
width="200"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="errorMsg"
|
||||
label="异常信息"
|
||||
:show-overflow-tooltip="true"
|
||||
width="250"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="ip"
|
||||
label="IP地址"
|
||||
sortable="custom"
|
||||
width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="异常时间"
|
||||
sortable="custom"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="120"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleViewDetail(row)"
|
||||
>
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="异常详情"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ currentDetail.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作人">
|
||||
{{ currentDetail.username }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作模块">
|
||||
{{ currentDetail.operation }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方法">
|
||||
{{ currentDetail.method }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数">
|
||||
<pre style="max-height: 200px; overflow: auto;">{{ currentDetail.params }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常信息">
|
||||
<div style="color: #f56c6c; word-break: break-all;">
|
||||
{{ currentDetail.errorMsg }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常堆栈">
|
||||
<pre style="max-height: 300px; overflow: auto; font-size: 12px;">{{ currentDetail.exceptionStack }}</pre>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">
|
||||
{{ currentDetail.ip }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="异常时间">
|
||||
{{ currentDetail.createTime }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { exceptionLogApi, ExceptionLog } from '@/api/exceptionLog'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<ExceptionLog[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const currentDetail = ref<ExceptionLog>({})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await exceptionLogApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: ExceptionLog) => {
|
||||
currentDetail.value = { ...row }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.exception-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<div class="login-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户名或IP地址"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="ip"
|
||||
label="IP地址"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="location"
|
||||
label="登录地点"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="browser"
|
||||
label="浏览器"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="os"
|
||||
label="操作系统"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="loginTime"
|
||||
label="登录时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.loginTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/logs/login/page', {
|
||||
params: {
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
}
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.login-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<div class="operation-log">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索操作人或操作模块"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="handleExport"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="操作人"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="operation"
|
||||
label="操作模块"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="operation-cell">
|
||||
<el-icon class="operation-icon">
|
||||
<component :is="getOperationIcon(row.operation)" />
|
||||
</el-icon>
|
||||
<span>{{ row.operation }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="method"
|
||||
label="请求方法"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="params"
|
||||
label="请求参数"
|
||||
:show-overflow-tooltip="true"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
v-if="row.params"
|
||||
class="params-content"
|
||||
>
|
||||
<el-popover
|
||||
placement="top"
|
||||
:width="500"
|
||||
trigger="hover"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="params-preview">
|
||||
{{ formatParams(row.params) }}
|
||||
</div>
|
||||
</template>
|
||||
<pre class="params-detail">{{ formatParams(row.params) }}</pre>
|
||||
</el-popover>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="duration"
|
||||
label="耗时(ms)"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="操作时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search, User, Document, Setting, Lock, View, Edit, Delete, Plus, Download } from '@element-plus/icons-vue'
|
||||
import { operationLogApi, OperationLog } from '@/api/operationLog'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<OperationLog[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sort: 'id',
|
||||
order: 'asc'
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await operationLogApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = new URLSearchParams()
|
||||
if (searchKeyword.value) {
|
||||
params.append('keyword', searchKeyword.value)
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/logs/operation/export?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('导出失败')
|
||||
}
|
||||
|
||||
const blob = await response.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `operation_logs_${new Date().toISOString().slice(0, 19).replace(/[:-]/g, '')}.xlsx`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sort = prop
|
||||
sortInfo.order = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const formatParams = (params: string) => {
|
||||
if (!params) return ''
|
||||
try {
|
||||
const parsed = JSON.parse(params)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
const getOperationIcon = (operation: string) => {
|
||||
if (!operation) return Document
|
||||
const op = operation.toLowerCase()
|
||||
if (op.includes('登录') || op.includes('login')) return User
|
||||
if (op.includes('删除') || op.includes('delete')) return Delete
|
||||
if (op.includes('编辑') || op.includes('修改') || op.includes('update') || op.includes('edit')) return Edit
|
||||
if (op.includes('查看') || op.includes('查询') || op.includes('view') || op.includes('get')) return View
|
||||
if (op.includes('新增') || op.includes('创建') || op.includes('add') || op.includes('create')) return Plus
|
||||
if (op.includes('下载') || op.includes('download')) return Download
|
||||
if (op.includes('设置') || op.includes('配置') || op.includes('setting') || op.includes('config')) return Setting
|
||||
if (op.includes('密码') || op.includes('password')) return Lock
|
||||
return Document
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.operation-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.operation-icon {
|
||||
font-size: 16px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.params-content {
|
||||
.params-preview {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.params-detail {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<div class="config-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>参数配置</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增配置
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="configName"
|
||||
label="参数名称"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="configKey"
|
||||
label="参数键名"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="configValue"
|
||||
label="参数值"
|
||||
/>
|
||||
<el-table-column label="类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.configType === 'Y' ? 'info' : 'success'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.configType === 'Y' ? '内置' : '自定义' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="参数名称">
|
||||
<el-input v-model="formState.configName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键名">
|
||||
<el-input v-model="formState.configKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数值">
|
||||
<el-input v-model="formState.configValue" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, configName: '', configKey: '', configValue: '' })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/config')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增配置'
|
||||
Object.assign(formState, { id: null, configName: '', configKey: '', configValue: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑配置'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该配置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/config/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
await request.put(`/config/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/config', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.config-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="dict-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>字典管理</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增字典
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictName"
|
||||
label="字典名称"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictType"
|
||||
label="字典类型"
|
||||
/>
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="200"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="字典名称">
|
||||
<el-input v-model="formState.dictName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型">
|
||||
<el-input v-model="formState.dictType" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="0"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="1"
|
||||
label="停用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="formState.remark"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, dictName: '', dictType: '', status: '0', remark: '' })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/dict/types')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增字典'
|
||||
Object.assign(formState, { id: null, dictName: '', dictType: '', status: '0', remark: '' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑字典'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该字典吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/dict/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除字典失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
await request.put(`/dict/types/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/dict/types', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.dict-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="file-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>文件管理</span>
|
||||
<el-upload
|
||||
:before-upload="handleUpload"
|
||||
:show-file-list="false"
|
||||
>
|
||||
<el-button type="primary">
|
||||
<el-icon><Upload /></el-icon> 上传文件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="filteredDataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fileName"
|
||||
label="文件名"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fileSize"
|
||||
label="文件大小"
|
||||
/>
|
||||
<el-table-column label="文件类型">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getFileTypeTag(row.fileType)">
|
||||
{{ getFileTypeName(row.fileType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="storageType"
|
||||
label="存储方式"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="上传时间"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createBy"
|
||||
label="上传人"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDownload(row)"
|
||||
>
|
||||
下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload, Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const filteredDataSource = computed(() => {
|
||||
if (!searchKeyword.value) {
|
||||
return dataSource.value
|
||||
}
|
||||
return dataSource.value.filter((item: any) =>
|
||||
item.fileName.toLowerCase().includes(searchKeyword.value.toLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/files')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
try {
|
||||
await request.post('/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
ElMessage.success('上传成功')
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleDownload = (row: any) => {
|
||||
const downloadUrl = `/api/files/${row.id}/download`
|
||||
const link = document.createElement('a')
|
||||
link.href = downloadUrl
|
||||
link.download = row.fileName
|
||||
link.click()
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该文件吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/files/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getFileTypeName = (fileType: string) => {
|
||||
if (!fileType) return '未知'
|
||||
if (fileType.startsWith('image/')) return '图片'
|
||||
if (fileType.startsWith('video/')) return '视频'
|
||||
if (fileType.startsWith('audio/')) return '音频'
|
||||
if (fileType.includes('pdf')) return 'PDF'
|
||||
if (fileType.includes('word') || fileType.includes('document')) return 'Word'
|
||||
if (fileType.includes('excel') || fileType.includes('spreadsheet')) return 'Excel'
|
||||
return '其他'
|
||||
}
|
||||
|
||||
const getFileTypeTag = (fileType: string): '' | 'success' | 'warning' | 'danger' | 'info' => {
|
||||
if (!fileType) return 'info'
|
||||
if (fileType.startsWith('image/')) return 'success'
|
||||
if (fileType.startsWith('video/')) return 'danger'
|
||||
if (fileType.startsWith('audio/')) return 'warning'
|
||||
if (fileType.includes('pdf')) return 'danger'
|
||||
if (fileType.includes('word')) return ''
|
||||
if (fileType.includes('excel')) return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.file-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="notice-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>通知公告</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增公告
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="noticeTitle"
|
||||
label="公告标题"
|
||||
/>
|
||||
<el-table-column
|
||||
label="公告类型"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.noticeType === '1' ? 'info' : 'success'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.noticeType === '1' ? '通知' : '公告' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === '0' ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="发布时间"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="公告标题">
|
||||
<el-input v-model="formState.noticeTitle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公告类型">
|
||||
<el-select v-model="formState.noticeType">
|
||||
<el-option
|
||||
value="1"
|
||||
label="通知"
|
||||
/>
|
||||
<el-option
|
||||
value="2"
|
||||
label="公告"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="公告内容">
|
||||
<el-input
|
||||
v-model="formState.noticeContent"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="0"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="1"
|
||||
label="停用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, noticeTitle: '', noticeType: '1', noticeContent: '', status: '0' })
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/notices')
|
||||
dataSource.value = res
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增公告'
|
||||
Object.assign(formState, { id: null, noticeTitle: '', noticeType: '1', noticeContent: '', status: '0' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑公告'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该公告吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/notice/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除通知失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
await request.put(`/notices/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/notices', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.notice-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card user-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="用户总数"
|
||||
:value="stats.userCount"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon user-icon">
|
||||
<User />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card role-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="角色总数"
|
||||
:value="stats.roleCount"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon role-icon">
|
||||
<UserFilled />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card login-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="今日登录"
|
||||
:value="stats.todayLogin"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon login-icon">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
class="stat-card log-card"
|
||||
>
|
||||
<el-statistic
|
||||
title="操作日志"
|
||||
:value="stats.operationLog"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon class="stat-icon log-icon">
|
||||
<Document />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row
|
||||
:gutter="16"
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
title="最近登录"
|
||||
class="recent-login-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">最近登录</span>
|
||||
<el-icon class="header-icon">
|
||||
<Clock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="item in recentLogins"
|
||||
:key="item.id"
|
||||
:type="item.status === '0' ? 'success' : 'danger'"
|
||||
:timestamp="formatDateTime(item.loginTime)"
|
||||
placement="top"
|
||||
>
|
||||
<div class="login-item">
|
||||
<div class="login-user">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ item.username }}</span>
|
||||
</div>
|
||||
<div class="login-ip">
|
||||
<el-icon><Location /></el-icon>
|
||||
<span>{{ item.ip }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item
|
||||
v-if="recentLogins.length === 0"
|
||||
placement="top"
|
||||
>
|
||||
<div class="empty-tip">
|
||||
暂无登录记录
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
v-loading="loading"
|
||||
title="系统信息"
|
||||
class="system-info-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">系统信息</span>
|
||||
<el-icon class="header-icon">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions
|
||||
:column="1"
|
||||
border
|
||||
>
|
||||
<el-descriptions-item label="系统版本">
|
||||
<div class="info-item">
|
||||
<el-icon><Star /></el-icon>
|
||||
<span>{{ systemInfo.version }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="Java版本">
|
||||
<div class="info-item">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span>{{ systemInfo.javaVersion }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="前端框架">
|
||||
<div class="info-item">
|
||||
<el-icon><Monitor /></el-icon>
|
||||
<span>{{ systemInfo.frontendFramework }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库">
|
||||
<div class="info-item">
|
||||
<el-icon><Coin /></el-icon>
|
||||
<span>{{ systemInfo.database }}</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { User, UserFilled, ArrowRight, Document, Clock, Location, Setting, Star, Cpu, Monitor, Coin } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = reactive({
|
||||
userCount: 0,
|
||||
roleCount: 0,
|
||||
todayLogin: 0,
|
||||
operationLog: 0
|
||||
})
|
||||
|
||||
const recentLogins = ref<any[]>([])
|
||||
const systemInfo = reactive({
|
||||
version: '1.0.0',
|
||||
javaVersion: '21',
|
||||
frontendFramework: 'Vue 3 + Element Plus',
|
||||
database: 'PostgreSQL'
|
||||
})
|
||||
|
||||
const fetchStats = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [userCountRes, roleCountRes, todayLoginRes, operationLogRes] = await Promise.allSettled([
|
||||
request.get('/users/count'),
|
||||
request.get('/roles/count'),
|
||||
request.get('/logs/login/today/count'),
|
||||
request.get('/logs/operation/count')
|
||||
])
|
||||
|
||||
stats.userCount = userCountRes.status === 'fulfilled' ? (userCountRes.value || 0) : 0
|
||||
stats.roleCount = roleCountRes.status === 'fulfilled' ? (roleCountRes.value || 0) : 0
|
||||
stats.todayLogin = todayLoginRes.status === 'fulfilled' ? (todayLoginRes.value || 0) : 0
|
||||
stats.operationLog = operationLogRes.status === 'fulfilled' ? (operationLogRes.value || 0) : 0
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRecentLogins = async () => {
|
||||
try {
|
||||
const res: any = await request.get('/logs/login/recent?limit=10')
|
||||
recentLogins.value = res || []
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch recent logins:', error)
|
||||
recentLogins.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchRecentLogins()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.dashboard {
|
||||
padding: 16px;
|
||||
|
||||
.stat-card {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&.user-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.role-card {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.login-card {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&.log-card {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
color: white;
|
||||
|
||||
:deep(.el-statistic__head) {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recent-login-card,
|
||||
.system-info-card {
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: 600;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
color: #409eff;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-item {
|
||||
.login-user,
|
||||
.login-ip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 4px 0;
|
||||
font-size: 14px;
|
||||
|
||||
.el-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.login-user {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.login-ip {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.el-icon {
|
||||
color: #409eff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="forbidden-container">
|
||||
<el-result
|
||||
icon="warning"
|
||||
title="403"
|
||||
sub-title="抱歉,您没有权限访问此页面"
|
||||
>
|
||||
<template #extra>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="goBack"
|
||||
>
|
||||
返回上一页
|
||||
</el-button>
|
||||
<el-button @click="goHome">
|
||||
返回首页
|
||||
</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const goHome = () => {
|
||||
router.push('/dashboard')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.forbidden-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<h2>登录 - Novalon 管理系统</h2>
|
||||
</template>
|
||||
<el-form
|
||||
:model="formState"
|
||||
label-position="top"
|
||||
@submit.prevent="onFinish"
|
||||
>
|
||||
<el-form-item
|
||||
label="用户名"
|
||||
prop="username"
|
||||
:rules="[{ required: true, message: '请输入用户名', trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.username"
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="密码"
|
||||
prop="password"
|
||||
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="loading"
|
||||
style="width: 100%"
|
||||
>
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
import { onMounted } from 'vue'
|
||||
import { jwtDecode } from 'jwt-decode'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const formState = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
document.title = '登录 - Novalon 管理系统'
|
||||
})
|
||||
|
||||
interface JwtPayload {
|
||||
userId: number
|
||||
username: string
|
||||
roles: string[]
|
||||
exp: number
|
||||
iat: number
|
||||
}
|
||||
|
||||
const onFinish = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.post('/auth/login', formState)
|
||||
|
||||
if (!res || !res.token) {
|
||||
ElMessage.error('登录失败:未收到有效响应')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('token', res.token)
|
||||
if (res.userId) {
|
||||
localStorage.setItem('userId', String(res.userId))
|
||||
}
|
||||
if (res.username) {
|
||||
localStorage.setItem('username', res.username)
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwtDecode<JwtPayload>(res.token)
|
||||
if (decoded.roles && Array.isArray(decoded.roles)) {
|
||||
localStorage.setItem('roles', JSON.stringify(decoded.roles))
|
||||
}
|
||||
} catch (decodeError) {
|
||||
console.warn('解析Token中的角色信息失败:', decodeError)
|
||||
}
|
||||
|
||||
try {
|
||||
await permissionStore.fetchUserMenus()
|
||||
} catch (menuError) {
|
||||
console.error('获取用户菜单失败:', menuError)
|
||||
}
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
|
||||
await router.push('/')
|
||||
} catch (error: any) {
|
||||
console.error('登录错误:', error)
|
||||
ElMessage.error(error.response?.data?.message || error.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: var(--el-color-primary-light-9);
|
||||
|
||||
.login-card {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="menu-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>菜单管理</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增菜单
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column
|
||||
prop="menuName"
|
||||
label="菜单名称"
|
||||
/>
|
||||
<el-table-column
|
||||
label="菜单类型"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.menuType === 'M' ? 'info' : row.menuType === 'C' ? 'success' : 'warning'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.menuType === 'M' ? '目录' : row.menuType === 'C' ? '菜单' : '按钮' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="perms"
|
||||
label="权限标识"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="component"
|
||||
label="组件"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="80"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="80"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
{{ row.status === '0' ? '显示' : '隐藏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item
|
||||
label="菜单名称"
|
||||
prop="menuName"
|
||||
>
|
||||
<el-input v-model="formState.menuName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级菜单">
|
||||
<el-tree-select
|
||||
v-model="formState.parentId"
|
||||
:data="menuTree"
|
||||
placeholder="请选择父级菜单"
|
||||
clearable
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="菜单类型"
|
||||
prop="menuType"
|
||||
>
|
||||
<el-select v-model="formState.menuType">
|
||||
<el-option
|
||||
value="M"
|
||||
label="目录"
|
||||
/>
|
||||
<el-option
|
||||
value="C"
|
||||
label="菜单"
|
||||
/>
|
||||
<el-option
|
||||
value="F"
|
||||
label="按钮"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formState.menuType !== 'F'"
|
||||
label="路由地址"
|
||||
prop="perms"
|
||||
>
|
||||
<el-input v-model="formState.perms" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formState.menuType === 'C'"
|
||||
label="组件路径"
|
||||
prop="component"
|
||||
>
|
||||
<el-input v-model="formState.component" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="排序"
|
||||
prop="orderNum"
|
||||
>
|
||||
<el-input-number v-model="formState.orderNum" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="0"
|
||||
label="显示"
|
||||
/>
|
||||
<el-option
|
||||
value="1"
|
||||
label="隐藏"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const menuTree = ref<any[]>([])
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const formState = reactive({
|
||||
id: null, menuName: '', parentId: 0, menuType: 'C', perms: '', component: '', orderNum: 0, status: '0'
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
menuName: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '菜单名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
menuType: [
|
||||
{ required: true, message: '请选择菜单类型', trigger: 'change' }
|
||||
],
|
||||
perms: [
|
||||
{ required: true, message: '请输入路由地址', trigger: 'blur' },
|
||||
{ pattern: /^\/[a-zA-Z0-9/_-]*$/, message: '路由地址格式不正确,应以/开头', trigger: 'blur' }
|
||||
],
|
||||
component: [
|
||||
{ required: true, message: '请输入组件路径', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9/-]+$/, message: '组件路径格式不正确', trigger: 'blur' }
|
||||
],
|
||||
orderNum: [
|
||||
{ required: true, message: '请输入排序', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序必须大于等于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/menus')
|
||||
dataSource.value = res
|
||||
menuTree.value = buildTreeSelect(res)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const buildTreeSelect = (menus: any[]): any[] => {
|
||||
return menus.map(m => ({ value: m.id, label: m.menuName, children: m.children ? buildTreeSelect(m.children) : undefined }))
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增菜单'
|
||||
Object.assign(formState, { id: null, menuName: '', parentId: 0, menuType: 'C', perms: '', component: '', orderNum: 0, status: '0' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑菜单'
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/menus/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除菜单失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
await request.put(`/menus/${formState.id}`, formState)
|
||||
} else {
|
||||
await request.post('/menus', formState)
|
||||
}
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.menu-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<div class="role-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索角色名称或标识"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增角色
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
width="80"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="roleName"
|
||||
label="角色名称"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="roleKey"
|
||||
label="角色标识"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="roleSort"
|
||||
label="显示顺序"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === RoleStatus.ACTIVE ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === RoleStatus.ACTIVE ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="创建时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="250"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
@click="handleAssignPermissions(row)"
|
||||
>
|
||||
分配权限
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item
|
||||
label="角色名称"
|
||||
prop="roleName"
|
||||
required
|
||||
>
|
||||
<el-input v-model="formState.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="角色标识"
|
||||
prop="roleKey"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.roleKey"
|
||||
:disabled="!!formState.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="显示顺序"
|
||||
prop="roleSort"
|
||||
required
|
||||
>
|
||||
<el-input-number
|
||||
v-model="formState.roleSort"
|
||||
:min="1"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
value="ACTIVE"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
value="INACTIVE"
|
||||
label="禁用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="permissionDialogVisible"
|
||||
title="分配权限"
|
||||
width="600px"
|
||||
>
|
||||
<el-tree
|
||||
ref="permissionTreeRef"
|
||||
:data="permissionTree"
|
||||
:props="{ label: 'name', children: 'children' }"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-checked-keys="selectedPermissions"
|
||||
check-strictly
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="permissionDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAssignPermissionsOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { roleApi, type Role, type CreateRoleRequest, type UpdateRoleRequest, type Permission } from '@/api/role.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { RoleStatus } from '@/constants/status'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<Role[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const formState = reactive<CreateRoleRequest & { id?: number; status?: RoleStatus }>({
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: 1,
|
||||
permissions: [],
|
||||
status: RoleStatus.ACTIVE
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
roleName: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '角色名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
roleKey: [
|
||||
{ required: true, message: '请输入角色标识', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '角色标识长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '角色标识只能包含字母、数字、下划线和横线', trigger: 'blur' }
|
||||
],
|
||||
roleSort: [
|
||||
{ required: true, message: '请输入显示顺序', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '显示顺序必须大于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const permissionDialogVisible = ref(false)
|
||||
const permissionTreeRef = ref()
|
||||
const permissionTree = ref<any[]>([])
|
||||
const selectedPermissions = ref<number[]>([])
|
||||
const currentRoleId = ref<number | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await roleApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sortBy: sortInfo.sortBy,
|
||||
sortOrder: sortInfo.sortOrder,
|
||||
name: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sortBy = prop
|
||||
sortInfo.sortOrder = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增角色'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
roleSort: 1,
|
||||
permissions: [],
|
||||
status: RoleStatus.ACTIVE
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: Role) => {
|
||||
modalTitle.value = '编辑角色'
|
||||
Object.assign(formState, {
|
||||
id: row.id,
|
||||
roleName: row.roleName,
|
||||
roleKey: row.roleKey,
|
||||
roleSort: row.roleSort,
|
||||
status: row.status,
|
||||
permissions: []
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: Role) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该角色吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await roleApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
const updateData: UpdateRoleRequest = {
|
||||
roleName: formState.roleName,
|
||||
roleKey: formState.roleKey,
|
||||
roleSort: formState.roleSort,
|
||||
status: formState.status
|
||||
}
|
||||
await roleApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
const createData: CreateRoleRequest = {
|
||||
roleName: formState.roleName,
|
||||
roleKey: formState.roleKey,
|
||||
roleSort: formState.roleSort,
|
||||
permissions: formState.permissions
|
||||
}
|
||||
await roleApi.create(createData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
modalVisible.value = false
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssignPermissions = async (row: Role) => {
|
||||
currentRoleId.value = row.id
|
||||
try {
|
||||
const allPermissions = await roleApi.getAllPermissions()
|
||||
permissionTree.value = buildPermissionTree(allPermissions)
|
||||
|
||||
const rolePermissions = await roleApi.getPermissions(row.id)
|
||||
selectedPermissions.value = rolePermissions.map((p: Permission) => p.id)
|
||||
|
||||
permissionDialogVisible.value = true
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const buildPermissionTree = (permissions: Permission[]): any[] => {
|
||||
const tree: any[] = []
|
||||
const resourceMap = new Map<string, Permission[]>()
|
||||
|
||||
permissions.forEach(p => {
|
||||
if (!resourceMap.has(p.resource)) {
|
||||
resourceMap.set(p.resource, [])
|
||||
}
|
||||
resourceMap.get(p.resource)!.push(p)
|
||||
})
|
||||
|
||||
resourceMap.forEach((perms, resource) => {
|
||||
tree.push({
|
||||
id: `resource-${resource}`,
|
||||
name: resource,
|
||||
children: perms.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
const handleAssignPermissionsOk = async () => {
|
||||
if (!currentRoleId.value) return
|
||||
|
||||
try {
|
||||
const checkedNodes = permissionTreeRef.value.getCheckedNodes()
|
||||
const permissionIds = checkedNodes
|
||||
.filter((node: any) => typeof node.id === 'number')
|
||||
.map((node: any) => node.id)
|
||||
|
||||
await roleApi.assignPermissions(currentRoleId.value, permissionIds)
|
||||
ElMessage.success('权限分配成功')
|
||||
permissionDialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.role-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,460 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="search-section">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户名或邮箱"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
新增用户
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="dataSource"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
width="80"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="nickname"
|
||||
label="昵称"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="邮箱"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="phone"
|
||||
label="手机号"
|
||||
sortable="custom"
|
||||
/>
|
||||
<el-table-column
|
||||
label="状态"
|
||||
width="100"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.status === 1 ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
style="font-weight: 500; font-size: 14px;"
|
||||
>
|
||||
{{ row.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
label="创建时间"
|
||||
sortable="custom"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="250"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
link
|
||||
@click="handleAssignRoles(row)"
|
||||
>
|
||||
分配角色
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item
|
||||
label="用户名"
|
||||
prop="username"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.username"
|
||||
:disabled="!!formState.id"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="!formState.id"
|
||||
label="密码"
|
||||
prop="password"
|
||||
required
|
||||
>
|
||||
<el-input
|
||||
v-model="formState.password"
|
||||
type="password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="formState.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="formState.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="formState.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formState.status">
|
||||
<el-option
|
||||
:value="UserStatus.ACTIVE"
|
||||
label="正常"
|
||||
/>
|
||||
<el-option
|
||||
:value="UserStatus.INACTIVE"
|
||||
label="禁用"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleModalCancel">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleModalOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="roleDialogVisible"
|
||||
title="分配角色"
|
||||
width="500px"
|
||||
>
|
||||
<el-transfer
|
||||
v-model="selectedRoles"
|
||||
:data="allRoles"
|
||||
:titles="['可选角色', '已分配角色']"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="roleDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleAssignRolesOk"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { userApi, type User, type CreateUserRequest, type UpdateUserRequest } from '@/api/user.api'
|
||||
import { roleApi, type Role } from '@/api/role.api'
|
||||
import { handleApiError } from '@/utils/errorHandler'
|
||||
import { UserStatus, StatusHelper } from '@/constants/status'
|
||||
import { formatDateTime } from '@/utils/dateFormat'
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref<User[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const sortInfo = reactive({
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc' as 'asc' | 'desc'
|
||||
})
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
const formState = reactive<CreateUserRequest & { id?: number; status?: UserStatus }>({
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roles: [],
|
||||
status: UserStatus.ACTIVE
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '用户名只能包含字母、数字、下划线和横线', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 8, max: 20, message: '密码长度在 8 到 20 个字符', trigger: 'blur' },
|
||||
{ pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: '密码必须包含大小写字母和数字', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
{ max: 100, message: '邮箱长度不能超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const roleDialogVisible = ref(false)
|
||||
const selectedRoles = ref<number[]>([])
|
||||
const allRoles = ref<{ key: number; label: string }[]>([])
|
||||
const currentUserId = ref<number | null>(null)
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await userApi.getPage({
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sortBy: sortInfo.sortBy,
|
||||
sortOrder: sortInfo.sortOrder,
|
||||
keyword: searchKeyword.value || undefined
|
||||
})
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleSortChange = ({ prop, order }: any) => {
|
||||
sortInfo.sortBy = prop
|
||||
sortInfo.sortOrder = order === 'ascending' ? 'asc' : 'desc'
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增用户'
|
||||
Object.assign(formState, {
|
||||
id: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
nickname: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
roles: [],
|
||||
status: 'ACTIVE'
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row: User) => {
|
||||
modalTitle.value = '编辑用户'
|
||||
Object.assign(formState, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
nickname: row.nickname,
|
||||
email: row.email,
|
||||
phone: row.phone,
|
||||
status: row.status,
|
||||
roles: []
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (row: User) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await userApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
|
||||
if (formState.id) {
|
||||
const updateData: UpdateUserRequest = {
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
status: formState.status
|
||||
}
|
||||
await userApi.update(formState.id, updateData)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
const createData: CreateUserRequest = {
|
||||
username: formState.username,
|
||||
password: formState.password,
|
||||
nickname: formState.nickname,
|
||||
email: formState.email,
|
||||
phone: formState.phone,
|
||||
roles: formState.roles
|
||||
}
|
||||
await userApi.create(createData)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
modalVisible.value = false
|
||||
if (error !== 'cancel') {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
const handleAssignRoles = async (row: User) => {
|
||||
currentUserId.value = row.id
|
||||
try {
|
||||
const roles = await roleApi.getAll()
|
||||
allRoles.value = roles.map((role: Role) => ({
|
||||
key: role.id,
|
||||
label: role.roleName
|
||||
}))
|
||||
selectedRoles.value = row.roles || []
|
||||
roleDialogVisible.value = true
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssignRolesOk = async () => {
|
||||
if (!currentUserId.value) return
|
||||
|
||||
try {
|
||||
await userApi.assignRoles(currentUserId.value, selectedRoles.value)
|
||||
ElMessage.success('角色分配成功')
|
||||
roleDialogVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
handleApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="css">
|
||||
.user-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user