feat: add system quality improvement plan and implementation
This commit is contained in:
@@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<a-config-provider :locale="locale">
|
||||
<router-view />
|
||||
</a-config-provider>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { locale } = useI18n()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,712 @@
|
||||
:root {
|
||||
--el-color-primary: #409eff;
|
||||
--el-color-primary-light-9: #53a8ff;
|
||||
--el-color-primary-light-3: #79bbff;
|
||||
--el-color-primary-dark-2: #337ecc;
|
||||
--el-color-success: #67c23a;
|
||||
--el-color-success-light-9: #85ce61;
|
||||
--el-color-success-light-3: #a0daee;
|
||||
--el-color-success-dark-2: #529b2e;
|
||||
--el-color-warning: #e6a23c;
|
||||
--el-color-warning-light-9: #ebb563;
|
||||
--el-color-warning-light-3: #f0c78a;
|
||||
--el-color-warning-dark-2: #b88230;
|
||||
--el-color-danger: #f56c6c;
|
||||
--el-color-danger-light-9: #f78989;
|
||||
--el-color-danger-light-3: #dd6161;
|
||||
--el-color-danger-dark-2: #c45656;
|
||||
--el-color-info: #909399;
|
||||
--el-color-info-light-9: #a6a9ad;
|
||||
--el-color-info-light-3: #c8c9cc;
|
||||
--el-color-info-dark-2: #73767a;
|
||||
|
||||
--border-radius-base: 8px;
|
||||
--border-radius-large: 12px;
|
||||
--border-radius-small: 4px;
|
||||
--border-radius-circle: 50%;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-card:hover {
|
||||
box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.el-button {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-button--primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.el-input__wrapper {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-input__wrapper:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.el-select .el-input__wrapper {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-table th {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-table tr {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-table tr:hover {
|
||||
background-color: #f0f9ff !important;
|
||||
}
|
||||
|
||||
.el-table .el-table__cell {
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.el-pagination {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-pagination .el-pager li {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-pagination .el-pager li:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.el-dialog {
|
||||
border-radius: var(--border-radius-large) !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
border-radius: var(--border-radius-large) var(--border-radius-large) 0 0 !important;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
margin: 4px 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-menu-item:hover {
|
||||
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-statistic {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-statistic:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-timeline-item__tail {
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.el-timeline-item__node {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-tag {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
padding: 4px 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-tag--success {
|
||||
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tag--warning {
|
||||
background: linear-gradient(135deg, #e6a23c 0%, #d93026 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tag--info {
|
||||
background: linear-gradient(135deg, #909399 0%, #73767a 100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-descriptions {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-descriptions__label {
|
||||
font-weight: 600;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
border-radius: var(--border-radius-large) !important;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.el-notification {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-drawer {
|
||||
border-radius: var(--border-radius-large) var(--border-radius-large) 0 0 !important;
|
||||
}
|
||||
|
||||
.el-switch {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-checkbox {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-radio {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-upload {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-upload-dragger {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
border: 2px dashed #dcdfe6;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-upload-dragger:hover {
|
||||
border-color: #667eea;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-progress-bar {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-rate__icon {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-slider__runway {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-slider__button {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-avatar {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-badge__content {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-breadcrumb {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.el-breadcrumb__item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-breadcrumb__item:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-empty {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-result {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-alert {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-alert--success {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
}
|
||||
|
||||
.el-alert--warning {
|
||||
background: linear-gradient(135deg, #fdf6ec 0%, #fef0f0 100%);
|
||||
}
|
||||
|
||||
.el-alert--error {
|
||||
background: linear-gradient(135deg, #fef0f0 0%, #fde2e2 100%);
|
||||
}
|
||||
|
||||
.el-alert--info {
|
||||
background: linear-gradient(135deg, #f4f4f5 0%, #e9e9eb 100%);
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0 !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-tabs__item:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-collapse {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-collapse-item__header:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-popover {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-tooltip__popper {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-dropdown-menu {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-dropdown-menu__item:hover {
|
||||
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.el-tree-node__content {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-tree-node__content:hover {
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.el-transfer-panel {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-cascader-panel {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-color-picker__panel {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-date-picker {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-picker-panel {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-calendar {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-image {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-skeleton {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-backtop {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-affix {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-space {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-tour {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-segmented {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-timeline {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-timeline-item {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-scrollbar {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-footer {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-row {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-col {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-form {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-input-number {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-option {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-option:hover {
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.el-option-group {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-option-group__title {
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0 !important;
|
||||
}
|
||||
|
||||
.el-checkbox-group {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-checkbox-button {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-radio-button {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-check-tag {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-select-v2 {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown-v2 {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown-v2__item {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-select-dropdown-v2__item:hover {
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.el-table-v2 {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-table-column {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-table-v2__row {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.el-table-v2__row:hover {
|
||||
background: #f0f9ff !important;
|
||||
}
|
||||
|
||||
.el-page-header {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-overlay {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-notification__group {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-notification__icon {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-notification__content {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-notification__closeBtn {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.el-message__icon {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-message__content {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-message-box__btns {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-menu-item-group {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-menu-item-group__title {
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0 !important;
|
||||
}
|
||||
|
||||
.el-mention {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-loading-spinner {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-link {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-input-tag {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-infinite-scroll {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-image-viewer {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-result__icon {
|
||||
border-radius: var(--border-radius-circle) !important;
|
||||
}
|
||||
|
||||
.el-result__title {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-result__subtitle {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-result__extra {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-result__content {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-descriptions-item {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-descriptions-item__label {
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0 !important;
|
||||
}
|
||||
|
||||
.el-descriptions-item__content {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-descriptions-item__cell {
|
||||
border-radius: var(--border-radius-base) !important;
|
||||
}
|
||||
|
||||
.el-date-picker__header {
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0 !important;
|
||||
}
|
||||
|
||||
.el-date-picker__header-label {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-date-picker__time-header {
|
||||
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0 !important;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option {
|
||||
border-radius: var(--border-radius-small) !important;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option:hover {
|
||||
background: #f0f9ff;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option.is-selected {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option.is-disabled {
|
||||
background: #f5f7fa;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option.is-disabled:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option.is-disabled.is-selected {
|
||||
background: #f5f7fa;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.el-date-picker__time-picker-option.is-disabled.is-selected:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
@@ -1,87 +1,109 @@
|
||||
<template>
|
||||
<a-layout class="default-layout">
|
||||
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
|
||||
<el-container class="default-layout">
|
||||
<el-aside :width="collapsed ? '64px' : '200px'" class="aside">
|
||||
<div class="logo">
|
||||
<span v-if="!collapsed">Novalon</span>
|
||||
<span v-else>N</span>
|
||||
</div>
|
||||
<a-menu v-model:selectedKeys="selectedKeys" theme="dark" mode="inline">
|
||||
<a-menu-item key="dashboard" @click="router.push('/dashboard')">
|
||||
<template #icon><DashboardOutlined /></template>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
class="menu"
|
||||
:collapse="collapsed"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409eff"
|
||||
router
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><Odometer /></el-icon>
|
||||
<span>仪表盘</span>
|
||||
</a-menu-item>
|
||||
<a-sub-menu key="system">
|
||||
<template #icon><SettingOutlined /></template>
|
||||
<template #title>系统管理</template>
|
||||
<a-menu-item key="users" @click="router.push('/users')">用户管理</a-menu-item>
|
||||
<a-menu-item key="roles" @click="router.push('/roles')">角色管理</a-menu-item>
|
||||
<a-menu-item key="menus" @click="router.push('/menus')">菜单管理</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="config">
|
||||
<template #icon><ToolOutlined /></template>
|
||||
<template #title>系统配置</template>
|
||||
<a-menu-item key="dict" @click="router.push('/dict')">字典管理</a-menu-item>
|
||||
<a-menu-item key="sysconfig" @click="router.push('/sysconfig')">参数配置</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="audit">
|
||||
<template #icon><AuditOutlined /></template>
|
||||
<template #title>审计中心</template>
|
||||
<a-menu-item key="loginlog" @click="router.push('/loginlog')">登录日志</a-menu-item>
|
||||
<a-menu-item key="oplog" @click="router.push('/oplog')">操作日志</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="notify">
|
||||
<template #icon><BellOutlined /></template>
|
||||
<template #title>通知中心</template>
|
||||
<a-menu-item key="notice" @click="router.push('/notice')">通知公告</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu key="file">
|
||||
<template #icon><FolderOutlined /></template>
|
||||
<template #title>文件管理</template>
|
||||
<a-menu-item key="files" @click="router.push('/files')">文件列表</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</a-menu>
|
||||
</a-layout-sider>
|
||||
<a-layout>
|
||||
<a-layout-header class="header">
|
||||
<menu-unfold-outlined v-if="collapsed" class="trigger" @click="collapsed = !collapsed" />
|
||||
<menu-fold-outlined v-else class="trigger" @click="collapsed = !collapsed" />
|
||||
</el-menu-item>
|
||||
<el-sub-menu index="system">
|
||||
<template #title>
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span>系统管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/users">用户管理</el-menu-item>
|
||||
<el-menu-item index="/roles">角色管理</el-menu-item>
|
||||
<el-menu-item index="/menus">菜单管理</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="config">
|
||||
<template #title>
|
||||
<el-icon><Tools /></el-icon>
|
||||
<span>系统配置</span>
|
||||
</template>
|
||||
<el-menu-item index="/dict">字典管理</el-menu-item>
|
||||
<el-menu-item index="/sysconfig">参数配置</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="audit">
|
||||
<template #title>
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>审计中心</span>
|
||||
</template>
|
||||
<el-menu-item index="/loginlog">登录日志</el-menu-item>
|
||||
<el-menu-item index="/oplog">操作日志</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="notify">
|
||||
<template #title>
|
||||
<el-icon><Bell /></el-icon>
|
||||
<span>通知中心</span>
|
||||
</template>
|
||||
<el-menu-item index="/notice">通知公告</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="file">
|
||||
<template #title>
|
||||
<el-icon><Folder /></el-icon>
|
||||
<span>文件管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/files">文件列表</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<el-icon class="trigger" @click="collapsed = !collapsed">
|
||||
<Fold v-if="!collapsed" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
<div class="header-right">
|
||||
<a-dropdown>
|
||||
<a-avatar>{{ username }}</a-avatar>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="profile">个人中心</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" @click="handleLogout">退出登录</a-menu-item>
|
||||
</a-menu>
|
||||
<el-dropdown @command="handleCommand">
|
||||
<el-avatar :size="32">{{ username }}</el-avatar>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
<a-layout-content class="content">
|
||||
</el-header>
|
||||
<el-main class="content">
|
||||
<router-view />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
DashboardOutlined, SettingOutlined, ToolOutlined,
|
||||
AuditOutlined, BellOutlined, FolderOutlined,
|
||||
MenuUnfoldOutlined, MenuFoldOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
Odometer, Setting, Tools, Document, Bell, Folder,
|
||||
Fold, Expand
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const collapsed = ref(false)
|
||||
const selectedKeys = ref<string[]>([])
|
||||
const username = ref(localStorage.getItem('username') || 'Admin')
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.clear()
|
||||
router.push('/login')
|
||||
const activeMenu = computed(() => route.path)
|
||||
|
||||
const handleCommand = (command: string) => {
|
||||
if (command === 'logout') {
|
||||
localStorage.clear()
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -97,6 +119,12 @@ onMounted(() => {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.aside {
|
||||
background-color: #304156;
|
||||
transition: width 0.3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
@@ -107,6 +135,10 @@ onMounted(() => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -119,7 +151,7 @@ onMounted(() => {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
&:hover { color: #1890ff; }
|
||||
&:hover { color: #409eff; }
|
||||
}
|
||||
|
||||
.header-right {
|
||||
@@ -132,6 +164,6 @@ onMounted(() => {
|
||||
margin: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
min-height: 280px;
|
||||
min-height: calc(100vh - 96px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Antd from 'ant-design-vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
import App from './App.vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import './styles/index.scss'
|
||||
import './assets/styles.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
app.use(Antd)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -31,6 +31,36 @@ const routes: RouteRecordRaw[] = [
|
||||
path: 'menus',
|
||||
name: 'MenuManagement',
|
||||
component: () => import('@/views/system/MenuManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
name: 'ConfigManagement',
|
||||
component: () => import('@/views/config/ConfigManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'dict',
|
||||
name: 'DictManagement',
|
||||
component: () => import('@/views/config/DictManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'FileManagement',
|
||||
component: () => import('@/views/file/FileManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'notices',
|
||||
name: 'NoticeManagement',
|
||||
component: () => import('@/views/notify/NoticeManagement.vue')
|
||||
},
|
||||
{
|
||||
path: 'login-logs',
|
||||
name: 'LoginLog',
|
||||
component: () => import('@/views/audit/LoginLog.vue')
|
||||
},
|
||||
{
|
||||
path: 'operation-logs',
|
||||
name: 'OperationLog',
|
||||
component: () => import('@/views/audit/OperationLog.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,49 +1,129 @@
|
||||
<template>
|
||||
<div class="login-log">
|
||||
<a-card>
|
||||
<template #title>登录日志</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '成功' : '失败' }}
|
||||
</a-tag>
|
||||
<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 :data="dataSource" v-loading="loading" 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'">
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
<el-table-column prop="loginTime" label="登录时间" sortable="custom" />
|
||||
</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"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: 'IP地址', dataIndex: 'ip', key: 'ip' },
|
||||
{ title: '登录地点', dataIndex: 'location', key: 'location' },
|
||||
{ title: '浏览器', dataIndex: 'browser', key: 'browser' },
|
||||
{ title: '操作系统', dataIndex: 'os', key: 'os' },
|
||||
{ title: '状态', key: 'status' },
|
||||
{ title: '登录时间', dataIndex: 'loginTime', key: 'loginTime' }
|
||||
]
|
||||
|
||||
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')
|
||||
dataSource.value = res
|
||||
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="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.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>
|
||||
|
||||
@@ -1,49 +1,129 @@
|
||||
<template>
|
||||
<div class="operation-log">
|
||||
<a-card>
|
||||
<template #title>操作日志</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '成功' : '失败' }}
|
||||
</a-tag>
|
||||
<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 :data="dataSource" v-loading="loading" 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" />
|
||||
<el-table-column prop="method" label="请求方法" sortable="custom" />
|
||||
<el-table-column prop="params" label="请求参数" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
{{ row.status === '0' ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="耗时(ms)" sortable="custom" />
|
||||
<el-table-column prop="createdAt" label="操作时间" sortable="custom" />
|
||||
</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"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '操作人', dataIndex: 'username', key: 'username' },
|
||||
{ title: '操作模块', dataIndex: 'operation', key: 'operation' },
|
||||
{ title: '请求方法', dataIndex: 'method', key: 'method' },
|
||||
{ title: '请求参数', dataIndex: 'params', key: 'params', ellipsis: true },
|
||||
{ title: '状态', key: 'status' },
|
||||
{ title: '耗时(ms)', dataIndex: 'duration', key: 'duration' },
|
||||
{ title: '操作时间', dataIndex: 'createdAt', key: 'createdAt' }
|
||||
]
|
||||
|
||||
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/operation')
|
||||
dataSource.value = res
|
||||
const res: any = await request.get('/logs/operation/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="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.operation-log {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,59 +1,58 @@
|
||||
<template>
|
||||
<div class="config-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>参数配置</span>
|
||||
<a-button type="primary" @click="handleAdd">新增配置</a-button>
|
||||
<el-button type="primary" @click="handleAdd">新增配置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'configType'">
|
||||
<a-tag :color="record.configType === 'Y' ? 'blue' : 'orange'">
|
||||
{{ record.configType === 'Y' ? '内置' : '自定义' }}
|
||||
</a-tag>
|
||||
<el-table :data="dataSource" v-loading="loading" 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'">
|
||||
{{ row.configType === 'Y' ? '内置' : '自定义' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</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>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
|
||||
<a-form :model="formState" :label-col="{ span: 6 }">
|
||||
<a-form-item label="参数名称" name="configName">
|
||||
<a-input v-model:value="formState.configName" />
|
||||
</a-form-item>
|
||||
<a-form-item label="参数键名" name="configKey">
|
||||
<a-input v-model:value="formState.configKey" />
|
||||
</a-form-item>
|
||||
<a-form-item label="参数值" name="configValue">
|
||||
<a-input v-model:value="formState.configValue" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<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 { message } from 'ant-design-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '参数名称', dataIndex: 'configName', key: 'configName' },
|
||||
{ title: '参数键名', dataIndex: 'configKey', key: 'configKey' },
|
||||
{ title: '参数值', dataIndex: 'configValue', key: 'configValue' },
|
||||
{ title: '类型', key: 'configType' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
@@ -76,18 +75,23 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑配置'
|
||||
Object.assign(formState, record)
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/config/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该配置吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/config/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch { message.error('删除失败') }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
@@ -97,10 +101,12 @@ const handleModalOk = async () => {
|
||||
} else {
|
||||
await request.post('/config', formState)
|
||||
}
|
||||
message.success('操作成功')
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch { message.error('操作失败') }
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
|
||||
@@ -1,65 +1,64 @@
|
||||
<template>
|
||||
<div class="dict-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>字典管理</span>
|
||||
<a-button type="primary" @click="handleAdd">新增字典</a-button>
|
||||
<el-button type="primary" @click="handleAdd">新增字典</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '正常' : '停用' }}
|
||||
</a-tag>
|
||||
<el-table :data="dataSource" v-loading="loading" 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'">
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</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>
|
||||
</a-table>
|
||||
</template>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
|
||||
<a-form :model="formState" :label-col="{ span: 6 }">
|
||||
<a-form-item label="字典名称" name="dictName">
|
||||
<a-input v-model:value="formState.dictName" />
|
||||
</a-form-item>
|
||||
<a-form-item label="字典类型" name="dictType">
|
||||
<a-input v-model:value="formState.dictType" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status">
|
||||
<a-select-option value="0">正常</a-select-option>
|
||||
<a-select-option value="1">停用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea v-model:value="formState.remark" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<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 { message } from 'ant-design-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '字典名称', dataIndex: 'dictName', key: 'dictName' },
|
||||
{ title: '字典类型', dataIndex: 'dictType', key: 'dictType' },
|
||||
{ title: '状态', key: 'status' },
|
||||
{ title: '备注', dataIndex: 'remark', key: 'remark' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
@@ -82,18 +81,23 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑字典'
|
||||
Object.assign(formState, record)
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/dict/types/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该字典吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/dict/types/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch { message.error('删除失败') }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
@@ -103,10 +107,12 @@ const handleModalOk = async () => {
|
||||
} else {
|
||||
await request.post('/dict/types', formState)
|
||||
}
|
||||
message.success('操作成功')
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch { message.error('操作失败') }
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
|
||||
@@ -1,51 +1,46 @@
|
||||
<template>
|
||||
<div class="file-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>文件管理</span>
|
||||
<a-upload :before-upload="handleUpload" :show-upload-list="false">
|
||||
<a-button type="primary">
|
||||
<upload-outlined /> 上传文件
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<el-upload :before-upload="handleUpload" :show-file-list="false">
|
||||
<el-button type="primary">
|
||||
<el-icon><Upload /></el-icon> 上传文件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'fileType'">
|
||||
<a-tag :color="getFileTypeColor(record.fileType)">
|
||||
{{ getFileTypeName(record.fileType) }}
|
||||
</a-tag>
|
||||
<el-table :data="dataSource" v-loading="loading" 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>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleDownload(record)">下载</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storageType" label="存储方式" />
|
||||
<el-table-column prop="createdAt" 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>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { UploadOutlined } from '@ant-design/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '文件名', dataIndex: 'fileName', key: 'fileName' },
|
||||
{ title: '文件大小', dataIndex: 'fileSize', key: 'fileSize' },
|
||||
{ title: '文件类型', key: 'fileType' },
|
||||
{ title: '存储方式', dataIndex: 'storageType', key: 'storageType' },
|
||||
{ title: '上传时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
|
||||
@@ -66,22 +61,29 @@ const handleUpload = async (file: File) => {
|
||||
await request.post('/files/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
message.success('上传成功')
|
||||
ElMessage.success('上传成功')
|
||||
fetchData()
|
||||
} catch { message.error('上传失败') }
|
||||
} catch {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleDownload = (record: any) => {
|
||||
window.open(record.filePath)
|
||||
const handleDownload = (row: any) => {
|
||||
window.open(row.filePath)
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/files/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该文件吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/files/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch { message.error('删除失败') }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const getFileTypeName = (fileType: string) => {
|
||||
@@ -95,15 +97,15 @@ const getFileTypeName = (fileType: string) => {
|
||||
return '其他'
|
||||
}
|
||||
|
||||
const getFileTypeColor = (fileType: string) => {
|
||||
if (!fileType) return 'default'
|
||||
if (fileType.startsWith('image/')) return 'pink'
|
||||
if (fileType.startsWith('video/')) return 'purple'
|
||||
if (fileType.startsWith('audio/')) return 'cyan'
|
||||
if (fileType.includes('pdf')) return 'red'
|
||||
if (fileType.includes('word')) return 'blue'
|
||||
if (fileType.includes('excel')) return 'green'
|
||||
return 'orange'
|
||||
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())
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
<template>
|
||||
<div class="notice-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>通知公告</span>
|
||||
<a-button type="primary" @click="handleAdd">新增公告</a-button>
|
||||
<el-button type="primary" @click="handleAdd">新增公告</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'noticeType'">
|
||||
<a-tag :color="record.noticeType === '1' ? 'blue' : 'green'">
|
||||
{{ record.noticeType === '1' ? '通知' : '公告' }}
|
||||
</a-tag>
|
||||
<el-table :data="dataSource" v-loading="loading" 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' ? '' : 'success'">
|
||||
{{ row.noticeType === '1' ? '通知' : '公告' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '正常' : '停用' }}
|
||||
</a-tag>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
{{ row.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="发布时间" />
|
||||
<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>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
|
||||
<a-form :model="formState" :label-col="{ span: 6 }">
|
||||
<a-form-item label="公告标题" name="noticeTitle">
|
||||
<a-input v-model:value="formState.noticeTitle" />
|
||||
</a-form-item>
|
||||
<a-form-item label="公告类型" name="noticeType">
|
||||
<a-select v-model:value="formState.noticeType">
|
||||
<a-select-option value="1">通知</a-select-option>
|
||||
<a-select-option value="2">公告</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="公告内容" name="noticeContent">
|
||||
<a-textarea v-model:value="formState.noticeContent" :rows="4" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status">
|
||||
<a-select-option value="0">正常</a-select-option>
|
||||
<a-select-option value="1">停用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<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 { message } from 'ant-design-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '公告标题', dataIndex: 'noticeTitle', key: 'noticeTitle' },
|
||||
{ title: '公告类型', key: 'noticeType', width: 100 },
|
||||
{ title: '状态', key: 'status', width: 80 },
|
||||
{ title: '发布时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const modalVisible = ref(false)
|
||||
@@ -90,18 +90,23 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑公告'
|
||||
Object.assign(formState, record)
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/notices/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该公告吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/notices/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch { message.error('删除失败') }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
@@ -111,10 +116,12 @@ const handleModalOk = async () => {
|
||||
} else {
|
||||
await request.post('/notices', formState)
|
||||
}
|
||||
message.success('操作成功')
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch { message.error('操作失败') }
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
|
||||
@@ -1,56 +1,78 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="用户总数" :value="stats.userCount" :prefix="h(UserOutlined)" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="角色总数" :value="stats.roleCount" :prefix="h(TeamOutlined)" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="今日登录" :value="stats.todayLogin" :prefix="h(LogoutOutlined)" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="操作日志" :value="stats.operationLog" :prefix="h(FileTextOutlined)" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row :gutter="16" style="margin-top: 16px">
|
||||
<a-col :span="12">
|
||||
<a-card title="最近登录">
|
||||
<a-timeline>
|
||||
<a-timeline-item v-for="item in recentLogins" :key="item.id" :color="item.status === '0' ? 'green' : 'red'">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading">
|
||||
<el-statistic title="用户总数" :value="stats.userCount">
|
||||
<template #prefix>
|
||||
<el-icon><User /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading">
|
||||
<el-statistic title="角色总数" :value="stats.roleCount">
|
||||
<template #prefix>
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading">
|
||||
<el-statistic title="今日登录" :value="stats.todayLogin">
|
||||
<template #prefix>
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card v-loading="loading">
|
||||
<el-statistic title="操作日志" :value="stats.operationLog">
|
||||
<template #prefix>
|
||||
<el-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 title="最近登录" v-loading="loading">
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="item in recentLogins"
|
||||
:key="item.id"
|
||||
:type="item.status === '0' ? 'success' : 'danger'"
|
||||
>
|
||||
<p>{{ item.username }} - {{ item.ip }}</p>
|
||||
<p>{{ item.loginTime }}</p>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-card title="系统信息">
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label="系统版本">1.0.0</a-descriptions-item>
|
||||
<a-descriptions-item label="Java版本">21</a-descriptions-item>
|
||||
<a-descriptions-item label="前端框架">Vue 3 + Ant Design Vue</a-descriptions-item>
|
||||
<a-descriptions-item label="数据库">PostgreSQL</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card title="系统信息" v-loading="loading">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="系统版本">{{ systemInfo.version }}</el-descriptions-item>
|
||||
<el-descriptions-item label="Java版本">{{ systemInfo.javaVersion }}</el-descriptions-item>
|
||||
<el-descriptions-item label="前端框架">{{ systemInfo.frontendFramework }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据库">{{ systemInfo.database }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h } from 'vue'
|
||||
import { UserOutlined, TeamOutlined, LogoutOutlined, FileTextOutlined } from '@ant-design/icons-vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { User, UserFilled, ArrowRight, Document } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const loading = ref(false)
|
||||
const stats = reactive({
|
||||
userCount: 0,
|
||||
roleCount: 0,
|
||||
@@ -59,6 +81,50 @@ const stats = reactive({
|
||||
})
|
||||
|
||||
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: any = await request.get('/users/count')
|
||||
stats.userCount = userCountRes || 0
|
||||
|
||||
const roleCountRes: any = await request.get('/roles/count')
|
||||
stats.roleCount = roleCountRes || 0
|
||||
|
||||
const todayLoginRes: any = await request.get('/logs/login/today/count')
|
||||
stats.todayLogin = todayLoginRes || 0
|
||||
|
||||
const operationLogRes: any = await request.get('/logs/operation/count')
|
||||
stats.operationLog = operationLogRes || 0
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRecentLogins = async () => {
|
||||
loading.value = true
|
||||
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)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchStats()
|
||||
fetchRecentLogins()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<a-card class="login-card">
|
||||
<template #title>
|
||||
<el-card class="login-card">
|
||||
<template #header>
|
||||
<h2>Novalon 管理系统</h2>
|
||||
</template>
|
||||
<a-form
|
||||
<el-form
|
||||
:model="formState"
|
||||
@finish="onFinish"
|
||||
layout="vertical"
|
||||
@submit.prevent="onFinish"
|
||||
label-position="top"
|
||||
>
|
||||
<a-form-item
|
||||
<el-form-item
|
||||
label="用户名"
|
||||
name="username"
|
||||
:rules="[{ required: true, message: '请输入用户名' }]"
|
||||
prop="username"
|
||||
:rules="[{ required: true, message: '请输入用户名', trigger: 'blur' }]"
|
||||
>
|
||||
<a-input v-model:value="formState.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item
|
||||
<el-input v-model="formState.username" placeholder="请输入用户名" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="密码"
|
||||
name="password"
|
||||
:rules="[{ required: true, message: '请输入密码' }]"
|
||||
prop="password"
|
||||
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
|
||||
>
|
||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" :loading="loading" block>
|
||||
<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%">
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</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 { message } from 'ant-design-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -53,10 +53,10 @@ const onFinish = async () => {
|
||||
const res: any = await request.post('/auth/login', formState)
|
||||
localStorage.setItem('token', res.token)
|
||||
localStorage.setItem('userId', res.userId)
|
||||
message.success('登录成功')
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error: any) {
|
||||
message.error(error.response?.data?.message || '登录失败')
|
||||
ElMessage.error(error.response?.data?.message || '登录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
<template>
|
||||
<div class="menu-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-title">
|
||||
<span>菜单管理</span>
|
||||
<a-button type="primary" @click="handleAdd">新增菜单</a-button>
|
||||
<el-button type="primary" @click="handleAdd">新增菜单</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading" :pagination="false" row-key="id">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'menuType'">
|
||||
<a-tag :color="record.menuType === 'M' ? 'blue' : record.menuType === 'C' ? 'green' : 'orange'">
|
||||
{{ record.menuType === 'M' ? '目录' : record.menuType === 'C' ? '菜单' : '按钮' }}
|
||||
</a-tag>
|
||||
<el-table :data="dataSource" v-loading="loading" :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' ? '' : row.menuType === 'C' ? 'success' : 'warning'">
|
||||
{{ row.menuType === 'M' ? '目录' : row.menuType === 'C' ? '菜单' : '按钮' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '显示' : '隐藏' }}
|
||||
</a-tag>
|
||||
</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>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</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>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
|
||||
<a-form :model="formState" :label-col="{ span: 6 }">
|
||||
<a-form-item label="菜单名称" name="menuName">
|
||||
<a-input v-model:value="formState.menuName" />
|
||||
</a-form-item>
|
||||
<a-form-item label="父级菜单" name="parentId">
|
||||
<a-tree-select v-model:value="formState.parentId" :tree-data="menuTree" placeholder="请选择父级菜单" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="菜单类型" name="menuType">
|
||||
<a-select v-model:value="formState.menuType">
|
||||
<a-select-option value="M">目录</a-select-option>
|
||||
<a-select-option value="C">菜单</a-select-option>
|
||||
<a-select-option value="F">按钮</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="路由地址" name="perms" v-if="formState.menuType !== 'F'">
|
||||
<a-input v-model:value="formState.perms" />
|
||||
</a-form-item>
|
||||
<a-form-item label="组件路径" name="component" v-if="formState.menuType === 'C'">
|
||||
<a-input v-model:value="formState.component" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="orderNum">
|
||||
<a-input-number v-model:value="formState.orderNum" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status">
|
||||
<a-select-option value="0">显示</a-select-option>
|
||||
<a-select-option value="1">隐藏</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<el-dialog v-model="modalVisible" :title="modalTitle" width="500px">
|
||||
<el-form :model="formState" label-width="100px">
|
||||
<el-form-item label="菜单名称">
|
||||
<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="菜单类型">
|
||||
<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 label="路由地址" v-if="formState.menuType !== 'F'">
|
||||
<el-input v-model="formState.perms" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组件路径" v-if="formState.menuType === 'C'">
|
||||
<el-input v-model="formState.component" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<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 { message } from 'ant-design-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: '菜单名称', dataIndex: 'menuName', key: 'menuName' },
|
||||
{ title: '菜单类型', key: 'menuType', width: 100 },
|
||||
{ title: '权限标识', dataIndex: 'perms', key: 'perms' },
|
||||
{ title: '组件', dataIndex: 'component', key: 'component' },
|
||||
{ title: '排序', dataIndex: 'orderNum', key: 'orderNum', width: 80 },
|
||||
{ title: '状态', key: 'status', width: 80 },
|
||||
{ title: '操作', key: 'action', width: 150 }
|
||||
]
|
||||
|
||||
const loading = ref(false)
|
||||
const dataSource = ref([])
|
||||
const menuTree = ref<any[]>([])
|
||||
@@ -109,18 +109,23 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑菜单'
|
||||
Object.assign(formState, record)
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/menus/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/menus/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch { message.error('删除失败') }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
@@ -130,10 +135,12 @@ const handleModalOk = async () => {
|
||||
} else {
|
||||
await request.post('/menus', formState)
|
||||
}
|
||||
message.success('操作成功')
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch { message.error('操作失败') }
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
|
||||
@@ -1,68 +1,104 @@
|
||||
<template>
|
||||
<div class="role-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>角色管理</span>
|
||||
<a-button type="primary" @click="handleAdd">新增角色</a-button>
|
||||
<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>
|
||||
<a-table :columns="columns" :data-source="dataSource" :loading="loading">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
<el-table :data="dataSource" v-loading="loading" style="width: 100%" @sort-change="handleSortChange">
|
||||
<el-table-column prop="id" label="ID" 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="状态">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
|
||||
{{ row.status === '0' ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" sortable="custom" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</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"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<a-modal v-model:open="modalVisible" :title="modalTitle" @ok="handleModalOk">
|
||||
<a-form :model="formState" :label-col="{ span: 6 }">
|
||||
<a-form-item label="角色名称" name="roleName">
|
||||
<a-input v-model:value="formState.roleName" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色标识" name="roleKey">
|
||||
<a-input v-model:value="formState.roleKey" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="roleSort">
|
||||
<a-input-number v-model:value="formState.roleSort" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status">
|
||||
<a-select-option value="0">正常</a-select-option>
|
||||
<a-select-option value="1">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<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.roleName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色标识">
|
||||
<el-input v-model="formState.roleKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="formState.roleSort" />
|
||||
</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 { message } from 'ant-design-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '角色名称', dataIndex: 'roleName', key: 'roleName' },
|
||||
{ title: '角色标识', dataIndex: 'roleKey', key: 'roleKey' },
|
||||
{ title: '排序', dataIndex: 'roleSort', key: 'roleSort' },
|
||||
{ title: '状态', key: 'status' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
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 modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({ id: null, roleName: '', roleKey: '', roleSort: 0, status: '0' })
|
||||
@@ -70,31 +106,65 @@ const formState = reactive({ id: null, roleName: '', roleKey: '', roleSort: 0, s
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/roles')
|
||||
dataSource.value = res
|
||||
const res: any = await request.get('/roles/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()
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增角色'
|
||||
Object.assign(formState, { id: null, roleName: '', roleKey: '', roleSort: 0, status: '0' })
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑角色'
|
||||
Object.assign(formState, record)
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/roles/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该角色吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/roles/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch { message.error('删除失败') }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
@@ -104,19 +174,30 @@ const handleModalOk = async () => {
|
||||
} else {
|
||||
await request.post('/roles', formState)
|
||||
}
|
||||
message.success('操作成功')
|
||||
ElMessage.success('操作成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch { message.error('操作失败') }
|
||||
} catch {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-management .card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.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>
|
||||
|
||||
@@ -1,89 +1,116 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<a-card>
|
||||
<template #title>
|
||||
<div class="card-title">
|
||||
<span>用户管理</span>
|
||||
<a-button type="primary" @click="handleAdd">新增用户</a-button>
|
||||
<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>
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@change="handleTableChange"
|
||||
<el-table
|
||||
:data="dataSource"
|
||||
v-loading="loading"
|
||||
style="width: 100%"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '0' ? 'green' : 'red'">
|
||||
{{ record.status === '0' ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||
<el-table-column prop="username" 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 === '0' ? 'success' : 'danger'">
|
||||
{{ row.status === '0' ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleEdit(record)">编辑</a-button>
|
||||
<a-button type="link" danger @click="handleDelete(record)">删除</a-button>
|
||||
</a-space>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" sortable="custom" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</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"
|
||||
@current-change="handleTableChange"
|
||||
@size-change="handleSizeChange"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
width="500px"
|
||||
>
|
||||
<a-form
|
||||
<el-form
|
||||
:model="formState"
|
||||
:label-col="{ span: 6 }"
|
||||
layout="horizontal"
|
||||
label-width="80px"
|
||||
>
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formState.username" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formState.email" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="formState.phone" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-select v-model:value="formState.status">
|
||||
<a-select-option value="0">正常</a-select-option>
|
||||
<a-select-option value="1">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="formState.username" />
|
||||
</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="0" label="正常" />
|
||||
<el-option value="1" 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const columns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '状态', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
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 modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formState = reactive({
|
||||
@@ -97,21 +124,39 @@ const formState = reactive({
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/users', {
|
||||
const res: any = await request.get('/users/page', {
|
||||
params: {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize
|
||||
page: pagination.current - 1,
|
||||
size: pagination.pageSize,
|
||||
sort: sortInfo.sort,
|
||||
order: sortInfo.order,
|
||||
keyword: searchKeyword.value || undefined
|
||||
}
|
||||
})
|
||||
dataSource.value = res.data
|
||||
pagination.total = res.total
|
||||
dataSource.value = res.content
|
||||
pagination.total = res.totalElements
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTableChange = (pag: any) => {
|
||||
pagination.current = pag.current
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -121,19 +166,24 @@ const handleAdd = () => {
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record: any) => {
|
||||
const handleEdit = (row: any) => {
|
||||
modalTitle.value = '编辑用户'
|
||||
Object.assign(formState, record)
|
||||
Object.assign(formState, row)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = async (record: any) => {
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await request.delete(`/users/${record.id}`)
|
||||
message.success('删除成功')
|
||||
await ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
await request.delete(`/users/${row.id}`)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} catch {
|
||||
message.error('删除失败')
|
||||
// 用户取消或删除失败
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,15 +191,15 @@ const handleModalOk = async () => {
|
||||
try {
|
||||
if (formState.id) {
|
||||
await request.put(`/users/${formState.id}`, formState)
|
||||
message.success('更新成功')
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await request.post('/users', formState)
|
||||
message.success('创建成功')
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch {
|
||||
message.error('操作失败')
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,10 +214,17 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-management {
|
||||
.card-title {
|
||||
.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