فهرست منبع

add 公告新增 websocket 实时显示公告信息

bzd_lxf 2 ماه پیش
والد
کامیت
1d05695c9e

+ 6 - 0
pm-admin/pom.xml

@@ -61,6 +61,12 @@
             <artifactId>pm-generator</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.12.0</version>
+        </dependency>
+
     </dependencies>
 
     <build>

+ 45 - 0
pm-admin/src/main/java/com/pm/web/controller/system/SysMenuController.java

@@ -1,6 +1,9 @@
 package com.pm.web.controller.system;
 
 import java.util.List;
+
+import com.pm.common.config.PageData;
+import com.pm.common.utils.DateUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
@@ -139,4 +142,46 @@ public class SysMenuController extends BaseController
         }
         return toAjax(menuService.deleteMenuById(menuId));
     }
+
+    /**
+     * 获取公告列表
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:query')")
+    @GetMapping("/listNoticeRead")
+    public AjaxResult listNoticeRead()
+    {
+        PageData pd = this.getPageData();
+        pd.put("userId",getUserId());
+        List<PageData> menus = menuService.listNoticeRead(pd);
+        return success(menus);
+    }
+
+    /**
+     * 读取 公告信息
+     * @param pd
+     * @return
+     */
+    @PreAuthorize("@ss.hasPermi('system:notice:query')")
+    @Log(title = "菜单管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/show")
+    public AjaxResult show(@Validated @RequestBody PageData pd)
+    {
+        pd.put("createTime", DateUtils.getTime());
+        pd.put("user_id", getUserId());
+        int i = menuService.listNoticeReadShow(pd);
+        return success(i);
+    }
+
+    /**
+     * 获取首页所有的统计信息
+     * @return
+     */
+    @GetMapping("/showAllCountData")
+    public AjaxResult showAllCountData()
+    {
+        List<PageData> menus = menuService.showAllCountData(this.getPageData());
+        return success(menus);
+    }
+
+
 }

+ 5 - 0
pm-admin/src/main/java/com/pm/web/controller/system/SysNoticeController.java

@@ -2,6 +2,7 @@ package com.pm.web.controller.system;
 
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.messaging.simp.SimpMessagingTemplate;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.DeleteMapping;
@@ -32,6 +33,9 @@ public class SysNoticeController extends BaseController
     @Autowired
     private ISysNoticeService noticeService;
 
+    @Autowired
+    private SimpMessagingTemplate messagingTemplate;
+
     /**
      * 获取通知公告列表
      */
@@ -63,6 +67,7 @@ public class SysNoticeController extends BaseController
     public AjaxResult add(@Validated @RequestBody SysNotice notice)
     {
         notice.setCreateBy(getUsername());
+        messagingTemplate.convertAndSend("/topic/announcements", notice);
         return toAjax(noticeService.insertNotice(notice));
     }
 

+ 23 - 0
pm-common/src/main/java/com/pm/common/config/WebSocketConfig.java

@@ -0,0 +1,23 @@
+package com.pm.common.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+@Configuration
+@EnableWebSocketMessageBroker
+public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        registry.addEndpoint("/ws").setAllowedOriginPatterns("*");
+    }
+
+    @Override
+    public void configureMessageBroker(MessageBrokerRegistry registry) {
+        registry.enableSimpleBroker("/topic");
+        registry.setApplicationDestinationPrefixes("/app");
+    }
+}

+ 2 - 0
pm-framework/src/main/java/com/pm/framework/config/SecurityConfig.java

@@ -110,6 +110,8 @@ public class SecurityConfig
             // 注解标记允许匿名访问的url
             .authorizeHttpRequests((requests) -> {
                 permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
+                // 放行 WebSocket 路径
+                requests.antMatchers("/ws/**","/topic/**").permitAll();
                 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                 requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
                     // 静态资源,可匿名访问

+ 163 - 43
pm_ui/src/layout/components/Navbar.vue

@@ -1,9 +1,9 @@
 <template>
   <div class="navbar">
     <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container"
-      @toggleClick="toggleSideBar" />
-    <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
-    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
+               @toggleClick="toggleSideBar"/>
+    <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container"/>
+    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container"/>
 
     <div class="right-menu">
       <template v-if="appStore.device !== 'mobile'">
@@ -11,15 +11,15 @@
         <el-tooltip :content="noticeContent" effect="dark" placement="bottom">
           <div class="right-menu-item" style="height: 70%">
             <el-badge :value="noticeCount" :max="99" :offset="[0, 10]" :hidden="noticeCount == 0 ? true : false"
-              class="badge-container">
+                      class="badge-container">
               <el-icon @click="showDetail" style="cursor: pointer;">
-                <BellFilled />
+                <BellFilled/>
               </el-icon>
             </el-badge>
           </div>
         </el-tooltip>
         <!-- 弹框组件 -->
-        <el-dialog v-model="dialogVisible" title="通知" width="30%" :before-close="close">
+        <el-dialog v-model="dialogVisible2" title="通知" width="30%" :before-close="closeDetailDialog">
           <el-row :gutter="20">
             <el-col :span="24">
               <div class="dialog-item">
@@ -30,13 +30,13 @@
             <el-col :span="24">
               <div class="dialog-item">
                 <span class="label">类型:</span>
-                  <el-select v-model="noticeType" disabled placeholder="请选择公告类型" style="width: 90%;">
-                    <el-option
-                        v-for="dict in sys_notice_type"
-                        :key="dict.value"
-                        :label="dict.label"
-                        :value="dict.value"
-                    ></el-option>
+                <el-select v-model="noticeType" disabled placeholder="请选择公告类型" style="width: 90%;">
+                  <el-option
+                      v-for="dict in sys_notice_type"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="dict.value"
+                  ></el-option>
                 </el-select>
               </div>
             </el-col>
@@ -49,28 +49,64 @@
           </el-row>
           <template #footer>
             <div class="dialog-footer">
-              <el-button type="primary" @click="close">已 读</el-button>
+              <el-button type="primary" @click="readInfo2">已 读</el-button>
             </div>
           </template>
         </el-dialog>
-        <header-search id="header-search" class="right-menu-item" />
-        <screenfull id="screenfull" class="right-menu-item hover-effect" />
+        <el-dialog v-model="dialogVisible" title="通知" width="50%" :before-close="closeListDialog">
+          <el-table v-loading="loading" :data="noticeList">
+            <el-table-column type="selection" width="55" align="center"/>
+            <!--            <el-table-column label="序号" align="center" prop="notice_id" width="100" />-->
+            <el-table-column
+                label="公告标题"
+                align="center"
+                prop="notice_title"
+                :show-overflow-tooltip="true"
+            />
+            <el-table-column label="公告类型" align="center" prop="notice_type" width="100">
+              <template #default="scope">
+                <dict-tag :options="sys_notice_type" :value="scope.row.notice_type"/>
+              </template>
+            </el-table-column>
+            <!--            <el-table-column label="状态" align="center" prop="status" width="100">
+                          <template #default="scope">
+                            <dict-tag :options="sys_notice_status" :value="scope.row.status" />
+                          </template>
+                        </el-table-column>-->
+            <el-table-column label="创建者" align="center" prop="create_by" width="100"/>
+            <el-table-column label="创建时间" align="center" prop="create_time" width="100">
+              <template #default="scope">
+                <span>{{ parseTime(scope.row.create_time, '{y}-{m}-{d}') }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180px">
+              <template #default="scope">
+                <el-button link type="primary" icon="View" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:query']">查看</el-button>
+                <el-button link type="primary" icon="Check" @click="readInfo(scope.row)" >已读</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-dialog>
+        <header-search id="header-search" class="right-menu-item"/>
+        <screenfull id="screenfull" class="right-menu-item hover-effect"/>
         <el-tooltip content="主题模式" effect="dark" placement="bottom">
           <div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
-            <svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
-            <svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
+            <svg-icon v-if="settingsStore.isDark" icon-class="sunny"/>
+            <svg-icon v-if="!settingsStore.isDark" icon-class="moon"/>
           </div>
         </el-tooltip>
 
         <el-tooltip content="布局大小" effect="dark" placement="bottom">
-          <size-select id="size-select" class="right-menu-item hover-effect" />
+          <size-select id="size-select" class="right-menu-item hover-effect"/>
         </el-tooltip>
       </template>
       <div class="avatar-container">
         <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
           <div class="avatar-wrapper">
-            <img :src="userStore.avatar" class="user-avatar" />
-            <el-icon><caret-bottom /></el-icon>
+            <img :src="userStore.avatar" class="user-avatar"/>
+            <el-icon>
+              <caret-bottom/>
+            </el-icon>
           </div>
           <template #dropdown>
             <el-dropdown-menu>
@@ -92,7 +128,7 @@
 </template>
 
 <script setup>
-import { ElMessageBox } from 'element-plus'
+import {ElMessageBox} from 'element-plus'
 import Breadcrumb from '@/components/Breadcrumb'
 import TopNav from '@/components/TopNav'
 import Hamburger from '@/components/Hamburger'
@@ -101,18 +137,59 @@ import SizeSelect from '@/components/SizeSelect'
 import HeaderSearch from '@/components/HeaderSearch'
 import pmGit from '@/components/pm/Git'
 import pmDoc from '@/components/pm/Doc'
-import { listNoticeRead } from "@/api/system/menu";
-import { getNotice, show } from "@/api/system/notice";
+import {listNoticeRead} from "@/api/system/menu";
+import {getNotice, show} from "@/api/system/notice";
 import useAppStore from '@/store/modules/app'
 import useUserStore from '@/store/modules/user'
 import useSettingsStore from '@/store/modules/settings'
-const { proxy } = getCurrentInstance();
-const { sys_notice_status, sys_notice_type } = proxy.useDict("sys_notice_status", "sys_notice_type");
+
+const {proxy} = getCurrentInstance();
+const {sys_notice_status, sys_notice_type} = proxy.useDict("sys_notice_status", "sys_notice_type");
+
+import {connectToWebSocket} from "@/layout/components/websocket";
+import {ElNotification} from 'element-plus'
+
+const announcements = ref([])
+
+connectToWebSocket((newAnnounce) => {
+  announcements.value.unshift(newAnnounce)
+  if (newAnnounce) {
+    // 显示右侧提示信息
+    // ElNotification({
+    //   title: '新公告',
+    //   message: '您有新的公告,请注意查收',
+    //   type: 'warning',
+    //   duration: 5000,
+    //   position: 'top-right'
+    // });
+    txt("新公告", "您有新的公告,请注意查收",'warning',6000);
+    getlistNotice()
+  }
+})
+
+const getNoticeTypeLabel = (type) => {
+  const dict = sys_notice_type.value.find(item => item.value === type)
+  return dict ? dict.label : type
+}
+
+const markAsRead = (noticeId) => {
+  show({noticeId}).then(response => {
+    getlistNotice()
+  })
+}
+
+function showDetail() {
+  //getlistNotice()
+  dialogVisible.value = true
+}
+
 
 const appStore = useAppStore()
 const userStore = useUserStore()
 const settingsStore = useSettingsStore()
 
+const noticeList = ref([]);
+const loading = ref(true);
 // 定义响应式变量
 const noticeContentV = ref('暂无消息'); // 通知内容
 const noticeContent = ref(''); // 通知内容
@@ -120,6 +197,7 @@ const noticeCount = ref(0); // 通知数量
 const intervalId = ref(null); // 定时器 ID
 const notice_id = ref(null); // 通知 ID
 const dialogVisible = ref(false); // 控制弹框显示/隐藏
+const dialogVisible2 = ref(false); // 控制弹框显示/隐藏
 const noticeTitle = ref(''); // 通知标题
 const noticeType = ref(''); // 通知类型
 const uniqueKey = ref(0); // 新增的唯一键属性
@@ -140,6 +218,7 @@ function handleCommand(command) {
       break;
   }
 }
+
 function logout() {
   ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
     confirmButtonText: '确定',
@@ -149,39 +228,70 @@ function logout() {
     userStore.logOut().then(() => {
       location.href = '/index';
     })
-  }).catch(() => { });
+  }).catch(() => {
+  });
 }
-function showDetail() {
-  if (noticeCount.value === 0) {
+
+// function close() {
+//   dialogVisible.value = false;
+//   show({ noticeId: notice_id.value }).then(response => {
+//     getlistNotice();
+//   })
+// }
+function readInfo(row) {
+  // dialogVisible.value = false;     // 关闭公告列表弹框
+  // dialogVisible2.value = false;    // 同时关闭公告详情弹框
+  show({noticeId: row.notice_id}).then(response => {
     getlistNotice();
-    return;
-  } else {
-    dialogVisible.value = true;
-  }
+  });
+  txt("已读公告信息", "您已成功读取公告信息",'success',3000);
 }
-function close() {
-  dialogVisible.value = false;
-  show({ noticeId: notice_id.value }).then(response => {
+function readInfo2() {
+  //dialogVisible.value = false;     // 关闭公告列表弹框
+  dialogVisible2.value = false;    // 同时关闭公告详情弹框
+  show({noticeId: notice_id.value}).then(response => {
     getlistNotice();
-  })
+  });
+  txt("已读公告信息", "您已成功读取公告信息",'success',3000);
+}
+
+function closeListDialog() {
+  dialogVisible.value = false;
 }
+
+function closeDetailDialog() {
+  dialogVisible2.value = false;
+}
+
 getlistNotice()
+
 function getlistNotice() {
+  loading.value = true;
   listNoticeRead().then((response) => {
     // 更新唯一键以强制重新渲染
     noticeCount.value = response.data.length;
-    if (response.data.length > 0) {
-      noticeContentV.value = response.data[0].notice_content;
-      noticeTitle.value = response.data[0].notice_title;
-      noticeType.value = response.data[0].notice_type;
-      notice_id.value = response.data[0].notice_id;
-    }
+
+    loading.value = false;
+    noticeList.value = response.data;
     noticeContent.value = "您有" + noticeCount.value + "条未读的信息 (点击铃铛查看消息)";
   });
 }
 
+function handleUpdate(row) {
+  const noticeId = row.notice_id || ids.value;
+  getNotice(noticeId).then(response => {
+    //form.value = response.data;
+    dialogVisible2.value = true;
+    noticeContentV.value = response.data.noticeContent;
+    noticeTitle.value = response.data.noticeTitle;
+    noticeType.value = response.data.noticeType;
+    notice_id.value = response.data.noticeId;
+  });
+}
+
 
 const emits = defineEmits(['setLayout'])
+
 function setLayout() {
   emits('setLayout');
 }
@@ -189,6 +299,16 @@ function setLayout() {
 function toggleTheme() {
   settingsStore.toggleTheme()
 }
+
+function txt(title,msg,type,time) {
+  ElNotification({
+    title: title,
+    message: msg,
+    type: type,
+    duration: time,
+    position: 'top-right'
+  });
+}
 </script>
 
 <style lang='scss' scoped>

+ 27 - 0
pm_ui/src/layout/components/websocket.js

@@ -0,0 +1,27 @@
+import { Client } from '@stomp/stompjs';
+
+let stompClient = null;
+
+export function connectToWebSocket(onMessageReceived) {
+    const socket = new WebSocket('ws://'+__LOCAL_IP__+':8010/ws'); // 后端 WebSocket 地址
+    stompClient = new Client({
+        webSocketFactory: () => socket,
+        reconnectDelay: 5000,
+        heartbeatIncoming: 4000,
+        heartbeatOutgoing: 4000,
+    });
+
+    stompClient.onConnect = () => {
+
+        stompClient.subscribe('/topic/announcements', (message) => {
+            const announcement = JSON.parse(message.body);
+            onMessageReceived(announcement);
+        });
+    };
+
+    stompClient.onStompError = (error) => {
+        console.error('STOMP Error:', error.headers.message);
+    };
+
+    stompClient.activate();
+}

+ 10 - 0
pm_ui/src/main.js

@@ -80,3 +80,13 @@ app.use(ElementPlus, {
 })
 
 app.mount('#app')
+
+// 屏蔽 DOMNodeInserted 警告(前端项目入口文件)
+const originalAddEventListener = EventTarget.prototype.addEventListener;
+EventTarget.prototype.addEventListener = function(type, listener, options) {
+  if (type === 'DOMNodeInserted' || type === 'DOMNodeRemoved') {
+    //console.warn(`Blocked deprecated mutation event: ${type}`);
+    return; // 直接跳过,不执行监听
+  }
+  originalAddEventListener.call(this, type, listener, options);
+};

+ 111 - 64
pm_ui/src/views/index.vue

@@ -1,14 +1,15 @@
 <template>
   <div class="app-container home">
     <el-row :gutter="20">
+      <!-- 左侧内容 -->
       <el-col :sm="24" :lg="12" style="padding-left: 20px">
         <h2>IBMS后台管理框架</h2>
-
         <p>
           <b>当前版本:</b> <span>v{{ version }}</span>
         </p>
       </el-col>
 
+      <!-- 右侧技术选型 -->
       <el-col :sm="24" :lg="12" style="padding-left: 50px">
         <el-row>
           <el-col :span="12">
@@ -19,107 +20,153 @@
           <el-col :span="6">
             <h4>后端技术</h4>
             <ul>
-              <li>SpringBoot</li>
-              <li>Spring Security</li>
-              <li>JWT</li>
-              <li>MyBatis</li>
-              <li>Druid</li>
-              <li>Fastjson</li>
-              <li>...</li>
+              <li v-for="(item, index) in backendTech" :key="index">{{ item }}</li>
             </ul>
           </el-col>
           <el-col :span="6">
             <h4>前端技术</h4>
             <ul>
-              <li>Vue</li>
-              <li>Vuex</li>
-              <li>Element-ui</li>
-              <li>Axios</li>
-              <li>Sass</li>
-              <li>Quill</li>
-              <li>...</li>
+              <li v-for="(item, index) in frontendTech" :key="index">{{ item }}</li>
             </ul>
           </el-col>
         </el-row>
       </el-col>
     </el-row>
-    <el-divider />
-    <el-row :gutter="20">
 
+    <el-divider />
 
+    <!-- 其他内容 -->
+    <el-row :gutter="20">
+      <el-col :span="24">
+        <h2>更新日志</h2>
+        <ol class="update-log">
+          <li v-for="(log, index) in updateLogs" :key="index">{{ log }}</li>
+        </ol>
+      </el-col>
     </el-row>
   </div>
 </template>
 
 <script setup name="Index">
-const version = ref('3.8.9')
-
-function goTarget(url) {
-  window.open(url, '__blank')
-}
+import { ref } from 'vue';
+
+const version = ref('3.8.9');
+
+// 后端技术列表
+const backendTech = [
+  'SpringBoot',
+  'Spring Security',
+  'JWT',
+  'MyBatis',
+  'Druid',
+  'Fastjson',
+  '...'
+];
+
+// 前端技术列表
+const frontendTech = [
+  'Vue',
+  'Vuex',
+  'Element-ui',
+  'Axios',
+  'Sass',
+  'Quill',
+  '...'
+];
+
+// 更新日志
+const updateLogs = [
+  '修复了一些已知问题',
+  '优化了性能',
+  '新增了用户管理模块',
+  '...'
+];
 </script>
 
 <style scoped lang="scss">
 .home {
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
-  }
-  hr {
-    margin-top: 20px;
-    margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
-  }
-  .col-item {
-    margin-bottom: 20px;
-  }
+  background: #f0f4f8;
+  color: #333;
+  font-family: 'Roboto', sans-serif;
+  min-height: auto;
+  padding: 20px;
 
-  ul {
-    padding: 0;
-    margin: 0;
+  h2 {
+    font-size: 26px;
+    font-weight: bold;
+    margin-bottom: 15px;
   }
 
-  font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
-  font-size: 13px;
-  color: #676a6c;
-  overflow-x: hidden;
+  p {
+    font-size: 16px;
+    margin-top: 10px;
 
-  ul {
-    list-style-type: none;
+    b {
+      color: #409eff;
+      font-weight: bold;
+    }
   }
 
-  h4 {
-    margin-top: 0px;
+  .el-row {
+    margin-bottom: 20px;
   }
 
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
+  .el-col {
+    background: #ffffff;
+    border-radius: 10px;
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+    padding: 20px;
+    transition: transform 0.3s ease;
+
+    &:hover {
+      transform: translateY(-5px);
+      box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
+    }
   }
 
-  p {
-    margin-top: 10px;
+  ul {
+    list-style-type: none;
+    padding: 0;
 
-    b {
-      font-weight: 700;
+    li {
+      font-size: 14px;
+      line-height: 24px;
+      position: relative;
+      padding-left: 20px;
+
+      &::before {
+        content: '•';
+        position: absolute;
+        left: 0;
+        color: #409eff;
+        font-size: 16px;
+      }
     }
   }
 
   .update-log {
     ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
+      padding-left: 20px;
+
+      li {
+        font-size: 14px;
+        line-height: 24px;
+        position: relative;
+
+        &::before {
+          content: counter(list-item) '.';
+          position: absolute;
+          left: -20px;
+          color: #409eff;
+          font-weight: bold;
+        }
+      }
     }
   }
+
+  .el-divider {
+    background-color: #ddd;
+    margin: 30px 0;
+  }
 }
 </style>
-

+ 58 - 34
pm_ui/src/views/login.vue

@@ -4,35 +4,35 @@
       <h3 class="title">{{ title }}</h3>
       <el-form-item prop="username">
         <el-input
-          v-model="loginForm.username"
-          type="text"
-          size="large"
-          auto-complete="off"
-          placeholder="账号"
+            v-model="loginForm.username"
+            type="text"
+            size="large"
+            auto-complete="off"
+            placeholder="账号"
         >
           <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
         </el-input>
       </el-form-item>
       <el-form-item prop="password">
         <el-input
-          v-model="loginForm.password"
-          type="password"
-          size="large"
-          auto-complete="off"
-          placeholder="密码"
-          @keyup.enter="handleLogin"
+            v-model="loginForm.password"
+            type="password"
+            size="large"
+            auto-complete="off"
+            placeholder="密码"
+            @keyup.enter="handleLogin"
         >
           <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
         </el-input>
       </el-form-item>
       <el-form-item prop="code" v-if="captchaEnabled">
         <el-input
-          v-model="loginForm.code"
-          size="large"
-          auto-complete="off"
-          placeholder="验证码"
-          style="width: 63%"
-          @keyup.enter="handleLogin"
+            v-model="loginForm.code"
+            size="large"
+            auto-complete="off"
+            placeholder="验证码"
+            style="width: 63%"
+            @keyup.enter="handleLogin"
         >
           <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
         </el-input>
@@ -43,11 +43,11 @@
       <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
       <el-form-item style="width:100%;">
         <el-button
-          :loading="loading"
-          size="large"
-          type="primary"
-          style="width:100%;"
-          @click.prevent="handleLogin"
+            :loading="loading"
+            size="large"
+            type="primary"
+            style="width:100%;"
+            @click.prevent="handleLogin"
         >
           <span v-if="!loading">登 录</span>
           <span v-else>登 录 中...</span>
@@ -99,7 +99,7 @@ const register = ref(false);
 const redirect = ref(undefined);
 
 watch(route, (newRoute) => {
-    redirect.value = newRoute.query && newRoute.query.redirect;
+  redirect.value = newRoute.query && newRoute.query.redirect;
 }, { immediate: true });
 
 function handleLogin() {
@@ -169,37 +169,58 @@ getCookie();
   justify-content: center;
   align-items: center;
   height: 100%;
-  background-image: url("../assets/images/login-background.jpg");
+  background: linear-gradient(to bottom, #0f0c29, #302b63, #24243e);
   background-size: cover;
 }
+
 .title {
   margin: 0px auto 30px auto;
   text-align: center;
-  color: #707070;
+  color: #ffffff;
+  font-family: 'Courier New', Courier, monospace;
+  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
 }
 
 .login-form {
-  border-radius: 6px;
-  background: #ffffff;
+  border-radius: 10px;
+  background: rgba(255, 255, 255, 0.1);
+  box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
+  backdrop-filter: blur( 4px );
+  -webkit-backdrop-filter: blur( 4px );
+  border: 1px solid rgba(255, 255, 255, 0.18);
   width: 400px;
   padding: 25px 25px 5px 25px;
   .el-input {
     height: 40px;
     input {
       height: 40px;
+      background: transparent;
+      color: #ffffff;
+      border: none;
+      border-bottom: 1px solid #ffffff;
+      &:focus {
+        border-color: #409EFF;
+      }
     }
   }
   .input-icon {
     height: 39px;
     width: 14px;
     margin-left: 0px;
+    color: #ffffff;
+  }
+  .el-checkbox {
+    color: #ffffff;
+  }
+  .el-button--primary {
+    background: linear-gradient(45deg, #409EFF, #79bbff);
+    border: none;
+    &:hover {
+      background: linear-gradient(45deg, #79bbff, #409EFF);
+    }
   }
 }
-.login-tip {
-  font-size: 13px;
-  text-align: center;
-  color: #bfbfbf;
-}
+
 .login-code {
   width: 33%;
   height: 40px;
@@ -209,6 +230,7 @@ getCookie();
     vertical-align: middle;
   }
 }
+
 .el-login-footer {
   height: 40px;
   line-height: 40px;
@@ -216,11 +238,13 @@ getCookie();
   bottom: 0;
   width: 100%;
   text-align: center;
-  color: #fff;
-  font-family: Arial;
+  color: #ffffff;
+  font-family: 'Courier New', Courier, monospace;
   font-size: 12px;
   letter-spacing: 1px;
+  background: rgba(0, 0, 0, 0.5);
 }
+
 .login-code-img {
   height: 40px;
   padding-left: 12px;