Browse Source

增加权限

zhuliu 1 month ago
parent
commit
0b6a944945

+ 25 - 0
package-lock.json

@@ -11,6 +11,8 @@
         "axios": "^1.11.0",
         "date-fns": "^4.1.0",
         "element-plus": "^2.10.6",
+        "js-cookie": "^3.0.5",
+        "nprogress": "^0.2.0",
         "pinia": "^3.0.3",
         "vue": "^3.5.18",
         "vue-router": "^4.5.1"
@@ -3713,6 +3715,14 @@
         "jiti": "lib/jiti-cli.mjs"
       }
     },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -4117,6 +4127,11 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/nprogress": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz",
+      "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
+    },
     "node_modules/nth-check": {
       "version": "2.1.1",
       "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",
@@ -7718,6 +7733,11 @@
       "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
       "dev": true
     },
+    "js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
+    },
     "js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8010,6 +8030,11 @@
         }
       }
     },
+    "nprogress": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz",
+      "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
+    },
     "nth-check": {
       "version": "2.1.1",
       "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz",

+ 2 - 0
package.json

@@ -19,6 +19,8 @@
     "axios": "^1.11.0",
     "date-fns": "^4.1.0",
     "element-plus": "^2.10.6",
+    "js-cookie": "^3.0.5",
+    "nprogress": "^0.2.0",
     "pinia": "^3.0.3",
     "vue": "^3.5.18",
     "vue-router": "^4.5.1"

+ 10 - 61
src/App.vue

@@ -1,30 +1,16 @@
 <template>
   <div id="app">
-    <el-container>
-      <el-header>
-        <div class="header-content">
-          <h1>资讯管理系统</h1>
-        </div>
-      </el-header>
-
-      <el-container>
-        <el-aside :width="isCollapse ? '64px' : '220px'">
-          <Sidebar v-model:collapse="isCollapse" />
-        </el-aside>
-
-        <el-main class="main-content">
-          <router-view />
-        </el-main>
-      </el-container>
-    </el-container>
+    <router-view />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
-import Sidebar from '@/components/Sidebar.vue'
+import { onMounted } from 'vue'
+import { useRouter } from 'vue-router'
 
-const isCollapse = ref(false)
+const router = useRouter()
+
+onMounted(async () => {})
 </script>
 
 <style scoped>
@@ -32,46 +18,9 @@ const isCollapse = ref(false)
   height: 100vh;
   width: 100vw;
 }
-
-.el-container {
-  height: 100%;
-}
-
-.el-header {
-  background-color: #409eff;
-  color: white;
-  padding: 0;
-  flex-shrink: 0;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-
-.header-content {
-  display: flex;
-  align-items: center;
-  height: 100%;
-  padding: 0 20px;
-}
-
-.header-content h1 {
-  margin: 0;
-  font-size: 20px;
-  font-weight: 600;
-}
-
-.el-aside {
-  background-color: #ffffff;
-  flex-shrink: 0;
-  position: relative;
-  transition: all 0.3s ease;
-  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
-  height: calc(100vh - 60px);
-  border-right: 1px solid #ebeef5;
-}
-
-.main-content {
-  background-color: #f5f7fa;
-  padding: 0;
-  height: calc(100vh - 60px);
-  overflow: auto;
+</style>
+<style>
+.height_100 {
+  height: 100% !important;
 }
 </style>

+ 22 - 4
src/components/Sidebar.vue

@@ -23,28 +23,44 @@
       active-text-color="#409eff"
       :collapse-transition="false"
     >
-      <el-menu-item index="/news" class="menu-item">
+      <el-menu-item
+        v-if="hasPermission.hasPermission('/xiaoshi/ppa/articleInfo')"
+        index="/news"
+        class="menu-item"
+      >
         <el-icon><Document /></el-icon>
         <template #title>
           <div class="menu-title">资讯管理</div>
         </template>
       </el-menu-item>
 
-      <el-menu-item index="/reports" class="menu-item">
+      <el-menu-item
+        v-if="hasPermission.hasPermission('/xiaoshi/ppa/report')"
+        index="/reports"
+        class="menu-item"
+      >
         <el-icon><Folder /></el-icon>
         <template #title>
           <div class="menu-title">报告管理</div>
         </template>
       </el-menu-item>
 
-      <el-menu-item index="/sources" class="menu-item">
+      <el-menu-item
+        v-if="hasPermission.hasPermission('/xiaoshi/ppa/sourceInfo')"
+        index="/sources"
+        class="menu-item"
+      >
         <el-icon><Link /></el-icon>
         <template #title>
           <div class="menu-title">来源管理</div>
         </template>
       </el-menu-item>
 
-      <el-menu-item index="/categories" class="menu-item">
+      <el-menu-item
+        v-if="hasPermission.hasPermission('/xiaoshi/ppa/category')"
+        index="/categories"
+        class="menu-item"
+      >
         <el-icon><Collection /></el-icon>
         <template #title>
           <div class="menu-title">分类管理</div>
@@ -64,6 +80,8 @@ import { ref, watch } from 'vue'
 import { useRoute } from 'vue-router'
 import { Document, Folder, Link, Collection, Fold, Expand, Reading } from '@element-plus/icons-vue'
 
+import hasPermission from '@/utils/permissions'
+
 const route = useRoute()
 const activeMenu = ref(route.path)
 const isCollapse = defineModel('collapse', { type: Boolean, default: false })

+ 7 - 3
src/main.ts

@@ -2,15 +2,19 @@ import './assets/main.css'
 
 import { createApp } from 'vue'
 import App from './App.vue'
+const app = createApp(App)
+
+import { createPinia } from 'pinia'
+const pinia = createPinia()
+app.use(pinia)
+
 import router from './router'
 
 // Element Plus
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
 
-const app = createApp(App)
-
 app.use(ElementPlus)
 app.use(router)
 
-app.mount('#app')
+app.mount('#app')

+ 130 - 38
src/router/index.ts

@@ -1,45 +1,137 @@
 import { createRouter, createWebHistory } from 'vue-router'
-import NewsList from '@/views/NewsList.vue'
-import ReportList from '@/views/ReportList.vue'
-import ReportDetail from '@/views/ReportDetail.vue'
-import SourceList from '@/views/SourceList.vue'
-import CategoryList from '@/views/CategoryList.vue'
 
-const router = createRouter({
-  history: createWebHistory(import.meta.env.BASE_URL),
-  routes: [
-    {
-      path: '/',
-      name: 'home',
-      redirect: '/news'
-    },
-    {
-      path: '/news',
-      name: 'news',
-      component: NewsList
-    },
-    {
-      path: '/reports',
-      name: 'reports',
-      component: ReportList
-    },
-    {
-      path: '/reports/:id',
-      name: 'report-detail',
-      component: ReportDetail,
-      props: true
+import Cookies from 'js-cookie'
+
+import NProgress from 'nprogress' // Progress 进度条
+import 'nprogress/nprogress.css' // Progress 进度条样式
+
+import { userPermission } from '@/stores/permission'
+
+import hasPermission from '@/utils/permissions'
+
+const routes = [
+  {
+    path: '/',
+    meta: {
+      notLogin: true,
     },
-    {
-      path: '/sources',
-      name: 'sources',
-      component: SourceList
+    component: () => import('@/views/index/index.vue'),
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    meta: {
+      title: '系统登录',
+      notLogin: true,
     },
-    {
-      path: '/categories',
-      name: 'categories',
-      component: CategoryList
+    component: () => import('@/views/login/index.vue'),
+  },
+  {
+    path: '/news',
+    component: () => import('@/views/layout/index.vue'),
+    children: [
+      {
+        path: '/news',
+        meta: {
+          title: '资讯清单',
+          permission: '/xiaoshi/ppa/articleInfo',
+        },
+        component: () => import('@/views/news/index.vue'),
+      },
+      {
+        path: '/reports',
+        name: 'reports',
+        meta: {
+          title: '报告清单',
+          permission: '/xiaoshi/ppa/report',
+        },
+        component: () => import('@/views/report/index.vue'),
+      },
+      {
+        path: '/reports/:id',
+        name: 'report-detail',
+        meta: {
+          title: '报告详情',
+        },
+        component: () => import('@/views/report/ReportDetail.vue'),
+        props: true,
+      },
+      {
+        path: '/sources',
+        name: 'sources',
+        meta: {
+          title: '来源清单',
+          permission: '/xiaoshi/ppa/sourceInfo',
+        },
+        component: () => import('@/views/source/index.vue'),
+      },
+      {
+        path: '/categories',
+        name: 'categories',
+        meta: {
+          title: '类别清单',
+          permission: '/xiaoshi/ppa/category',
+        },
+        component: () => import('@/views/category/index.vue'),
+      },
+    ],
+  },
+]
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: routes,
+})
+
+router.beforeEach((to, form, next) => {
+  NProgress.start()
+  if (to.meta.title) {
+    document.title = to.meta.title as string
+  }
+  const permissionStore = userPermission()
+  if (to.meta.notLogin) {
+    if (to.path == '/login') {
+      Cookies.remove('token')
+      permissionStore.updataPermissionList(null)
+    }
+    next()
+  } else {
+    const tokenStr = Cookies.get('token')
+    switch (tokenStr) {
+      case undefined:
+        if (to.meta.title) {
+          document.title = to.meta.title as string
+        }
+        next({ name: 'Login' })
+        break
+      default:
+        const permissionSign = to.meta.permission as string
+        if (!permissionStore.load) {
+          permissionStore
+            .getPersonPermission()
+            .then((response) => {
+              if (permissionSign && !hasPermission.hasPermission(permissionSign)) {
+                next({ path: '/news' })
+              } else {
+                next()
+              }
+            })
+            .catch(() => {
+              next()
+            })
+        } else {
+          if (permissionSign && !hasPermission.hasPermission(permissionSign)) {
+            next({ path: '/news' })
+          } else {
+            next()
+          }
+        }
+        break
     }
-  ]
+  }
+})
+
+router.afterEach((to, from) => {
+  NProgress.done()
 })
 
-export default router
+export default router

+ 11 - 38
src/services/api.ts

@@ -1,32 +1,7 @@
 // api.ts
-import axios from 'axios'
 import type { Category, Source, NewsItem, Report } from '@/types'
 
-const API_BASE_URL = '/api'
-
-// Create an axios instance
-const apiClient = axios.create({
-  baseURL: API_BASE_URL,
-  timeout: 10000,
-  headers: {
-    'Content-Type': 'application/json',
-  },
-})
-
-// Response interceptor to handle errors
-apiClient.interceptors.response.use(
-  (response) => response.data,
-  (error) => {
-    console.error('API Error:', error)
-    return Promise.reject(error)
-  },
-)
-
-interface ApiResponse<T> {
-  data: T
-  message: string
-  code: boolean
-}
+import apiClient from './axios'
 
 // Categories API
 export const categoryApi = {
@@ -74,23 +49,22 @@ export const configApi = {
   getConfigurations: async (params: Record<string, any>): Promise<unknown> => {
     try {
       const url = `/xiaoshi/ppa/sourceInfo/selectConfigurationList`
-      const response = await apiClient.get<unknown>(url, {params})
+      const response = await apiClient.get<unknown>(url, { params })
       return response
     } catch (error) {
       console.error(`Error from export report:`, error)
       throw error
     }
   },
-
 }
 
 // Sources API
 export const sourceApi = {
   //获取来源列表
-  getSources: async (params?: Record<string,any>): Promise<unknown> => {
+  getSources: async (params?: Record<string, any>): Promise<unknown> => {
     try {
       const url = `/xiaoshi/ppa/sourceInfo/selectSourceInfoList`
-      const response = await apiClient.post<unknown>(url,params)
+      const response = await apiClient.post<unknown>(url, params)
       return response
     } catch (error) {
       console.error('Error fetching sources:', error)
@@ -99,10 +73,10 @@ export const sourceApi = {
   },
 
   //获取来源详情
-  getSource: async (params?: Record<string,any>): Promise<unknown> => {
+  getSource: async (params?: Record<string, any>): Promise<unknown> => {
     try {
       const url = `/xiaoshi/ppa/sourceInfo/selectSourceInfoDetail`
-      const response = await apiClient.post<unknown>(url,params)
+      const response = await apiClient.post<unknown>(url, params)
       return response
     } catch (error) {
       console.error(`Error fetching source:`, error)
@@ -111,10 +85,10 @@ export const sourceApi = {
   },
 
   // Create source
-  createSource: async (params?: Record<string,any>): Promise<unknown> => {
+  createSource: async (params?: Record<string, any>): Promise<unknown> => {
     try {
       const url = `/xiaoshi/ppa/sourceInfo/addOrEditSourceInfo`
-      const response = await apiClient.post<unknown>(url,params)
+      const response = await apiClient.post<unknown>(url, params)
       return response
     } catch (error) {
       console.error('Error creating source:', error)
@@ -122,8 +96,7 @@ export const sourceApi = {
     }
   },
 
-
-   // 删除来源
+  // 删除来源
   deleteSource: async (params: Record<string, any>): Promise<unknown> => {
     try {
       const url = `/xiaoshi/ppa/sourceInfo/deleteSourceInfo`
@@ -281,14 +254,14 @@ export const reportApi = {
   exportReportInfo: async (params: Record<string, any>): Promise<unknown> => {
     try {
       const url = `/xiaoshi/ppa/report/exportReport`
-      const response = await apiClient.get<unknown>(url, {params})
+      const response = await apiClient.get<unknown>(url, { params })
       return response
     } catch (error) {
       console.error(`Error from export report:`, error)
       throw error
     }
   },
-  
+
   //一键导出上月资讯报告
   oneClickExportReport: async (): Promise<unknown> => {
     try {

+ 23 - 0
src/services/axios.ts

@@ -0,0 +1,23 @@
+import axios from 'axios'
+
+const API_BASE_URL = '/api'
+
+// Create an axios instance
+const apiClient = axios.create({
+  baseURL: API_BASE_URL,
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json',
+  },
+})
+
+// Response interceptor to handle errors
+apiClient.interceptors.response.use(
+  (response) => response.data,
+  (error) => {
+    console.error('API Error:', error)
+    return Promise.reject(error)
+  },
+)
+
+export default apiClient

+ 56 - 0
src/services/permission.ts

@@ -0,0 +1,56 @@
+import apiClient from './axios'
+
+export const permissionApi = {
+  /**
+   * 获取验证码
+   */
+  getVerifyCode: async (): Promise<unknown> => {
+    try {
+      const url = `/permission/api/admin/verifyCode`
+      const response = await apiClient.get<unknown>(url)
+      return response
+    } catch (error) {
+      console.error('获取验证码失败:', error)
+      throw error
+    }
+  },
+  /**
+   * 获取用户权限
+   */
+  getPermissionList: async (params: Record<string, any>): Promise<unknown> => {
+    try {
+      const url = `/permission/api/system/getPermissionList`
+      const response = await apiClient.get<unknown>(url, { params })
+      return response
+    } catch (error) {
+      console.error('获取用户权限失败:', error)
+      throw error
+    }
+  },
+  /**
+   * 用户登录
+   */
+  adminLogin: async (params: Record<string, any>): Promise<unknown> => {
+    try {
+      const url = `/permission/api/admin/login`
+      const response = await apiClient.post<unknown>(url, params)
+      return response
+    } catch (error) {
+      console.error('获取用户权限失败:', error)
+      throw error
+    }
+  },
+  /**
+   * 用户信息
+   */
+  getUserInfo: async (): Promise<unknown> => {
+    try {
+      const url = `/permission/api/system/userinfo`
+      const response = await apiClient.get<unknown>(url)
+      return response
+    } catch (error) {
+      console.error('获取用户信息失败:', error)
+      throw error
+    }
+  },
+}

+ 0 - 12
src/stores/counter.ts

@@ -1,12 +0,0 @@
-import { ref, computed } from 'vue'
-import { defineStore } from 'pinia'
-
-export const useCounterStore = defineStore('counter', () => {
-  const count = ref(0)
-  const doubleCount = computed(() => count.value * 2)
-  function increment() {
-    count.value++
-  }
-
-  return { count, doubleCount, increment }
-})

+ 37 - 0
src/stores/permission.ts

@@ -0,0 +1,37 @@
+import { defineStore } from 'pinia'
+import { permissionApi } from '@/services/permission'
+
+export const userPermission = defineStore('permission', {
+  state: () => ({
+    permissionList: [],
+    userinfo: {},
+    load: false,
+  }),
+  getters: {},
+  actions: {
+    updataPermissionList(arr: any) {
+      this.permissionList = arr
+    },
+    async getPersonPermission() {
+      const userinfo = await permissionApi.getUserInfo()
+      if (userinfo.code == 200) {
+        this.userinfo = userinfo.data
+      }
+      return new Promise(async (resolve, reject) => {
+        const params = {
+          code: '61e170a5624f401dbfb6fe5260f13f77',
+        }
+        const response = await permissionApi.getPermissionList(params)
+        if (response.code == 200) {
+          this.permissionList = response.data
+          this.load = true
+          resolve(response.data)
+        } else {
+          this.permissionList = []
+          this.load = true
+          reject('获取权限失败')
+        }
+      })
+    },
+  },
+})

+ 17 - 0
src/utils/permissions.ts

@@ -0,0 +1,17 @@
+import { userPermission } from '@/stores/permission'
+
+export default {
+  hasPermission: (sign: string) => {
+    const permissionStore = userPermission()
+    const userinfo = { ...permissionStore.userinfo }
+    if (userinfo.roleType == 1) {
+      return true
+    }
+    const permissionList = [...permissionStore.permissionList]
+    console.log(permissionList, sign)
+    if (!permissionList) {
+      return false
+    }
+    return permissionList.includes(sign)
+  },
+}

src/views/CategoryList.vue → src/views/category/CategoryList.vue


+ 9 - 0
src/views/category/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="height_100">
+    <CategoryList></CategoryList>
+  </div>
+</template>
+
+<script setup lang="ts">
+import CategoryList from './CategoryList.vue'
+</script>

+ 16 - 0
src/views/index/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <div></div>
+</template>
+<script setup lang="ts">
+import { onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+// Router
+const router = useRouter()
+
+onMounted(() => {
+  console.log(1)
+  router.push({
+    name: 'Login',
+  })
+})
+</script>

+ 78 - 0
src/views/layout/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <div id="layout">
+    <el-container>
+      <el-header>
+        <div class="header-content">
+          <h1>资讯管理系统</h1>
+        </div>
+      </el-header>
+
+      <el-container>
+        <el-aside :width="isCollapse ? '64px' : '220px'">
+          <Sidebar v-model:collapse="isCollapse" />
+        </el-aside>
+
+        <el-main class="main-content">
+          <router-view />
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import Sidebar from '@/components/Sidebar.vue'
+
+const isCollapse = ref(false)
+</script>
+
+<style scoped>
+#layout {
+  height: 100%;
+  min-width: 1200px;
+  width: 100%;
+}
+
+.el-container {
+  height: 100%;
+}
+
+.el-header {
+  background-color: #409eff;
+  color: white;
+  padding: 0;
+  flex-shrink: 0;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.header-content {
+  display: flex;
+  align-items: center;
+  height: 100%;
+  padding: 0 20px;
+}
+
+.header-content h1 {
+  margin: 0;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+.el-aside {
+  background-color: #ffffff;
+  flex-shrink: 0;
+  position: relative;
+  transition: all 0.3s ease;
+  box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
+  height: calc(100vh - 60px);
+  border-right: 1px solid #ebeef5;
+}
+
+.main-content {
+  background-color: #f5f7fa;
+  padding: 0;
+  height: calc(100vh - 60px);
+  overflow: auto;
+}
+</style>

+ 234 - 0
src/views/login/index.vue

@@ -0,0 +1,234 @@
+<template>
+  <div class="login">
+    <div class="form_warp">
+      <div class="header">
+        <div class="title">咨询管理系统</div>
+        <div class="title2">知识产权资讯采集与管理平台</div>
+      </div>
+      <div class="form_content">
+        <el-form
+          ref="ruleFormRef"
+          :model="form"
+          :rules="rules"
+          label-width="auto"
+          label-position="top"
+        >
+          <el-form-item label="用户名" prop="username">
+            <el-input
+              v-model="form.username"
+              clearable
+              autocomplete="off"
+              @keyup.enter="submitForm(ruleFormRef)"
+              placeholder="请输入用户名"
+            >
+              <template #prepend></template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="密码" prop="password">
+            <el-input
+              type="password"
+              v-model="form.password"
+              autocomplete="off"
+              :show-password="true"
+              @keyup.enter="submitForm(ruleFormRef)"
+              placeholder="请输入密码"
+            >
+              <template #prepend></template>
+            </el-input>
+          </el-form-item>
+          <el-form-item label="验证码" prop="code" class="margin-left_0">
+            <div class="code_content">
+              <div class="code_input">
+                <el-input
+                  type="text"
+                  maxlength="4"
+                  placeholder="请输入验证码"
+                  v-model="form.code"
+                  clearable
+                  autocomplete="off"
+                  @keyup.enter="submitForm(ruleFormRef)"
+                >
+                  <template #prepend></template>
+                </el-input>
+              </div>
+              <div class="login-code">
+                <img
+                  @click="getCode"
+                  class="login-code-img"
+                  style="margin-top: 0"
+                  :src="captcha"
+                  title="点击获取新的验证码"
+                />
+              </div>
+            </div>
+          </el-form-item>
+          <el-form-item>
+            <el-button
+              type="primary"
+              size="large"
+              class="login-submit"
+              @click="submitForm(ruleFormRef)"
+              :loading="btnLoading"
+            >
+              <span>登 录</span>
+            </el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, reactive } from 'vue'
+import cookie from 'js-cookie'
+import { permissionApi } from '@/services/permission.ts'
+
+import { ElMessage } from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+
+import { useRouter } from 'vue-router'
+const router = useRouter()
+
+interface RuleForm {
+  username: string
+  password: string
+  code: string
+  uuid: string
+}
+
+const ruleFormRef = ref<FormInstance>()
+const form = ref<RuleForm>({
+  username: '',
+  password: '',
+  code: '',
+  uuid: '',
+})
+const rules = reactive<FormRules<RuleForm>>({
+  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+  code: [
+    { required: true, message: '请输入验证码', trigger: 'blur' },
+    { min: 4, max: 4, message: '长度为4', trigger: 'blur' },
+  ],
+})
+const captcha = ref('')
+const btnLoading = ref(false)
+
+const getCode = async () => {
+  const response = await permissionApi.getVerifyCode()
+  form.value.uuid = response.data.uuid
+  captcha.value = response.data.captcha
+}
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      btnLoading.value = true
+      const response = await permissionApi.adminLogin(form.value)
+      btnLoading.value = false
+      if (response.code != 200) {
+        ElMessage.error(response.message)
+        return
+      }
+      localStorage.setItem('username', form.value.username)
+      localStorage.setItem('password', form.value.password)
+      cookie.set('token', response.data.token)
+      sessionStorage.clear()
+      router.push({
+        path: '/news',
+      })
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+
+onMounted(async () => {
+  getCode()
+  const username = localStorage.getItem('username')
+  const password = localStorage.getItem('password')
+  if (username) {
+    form.value.username = username
+  }
+  if (password) {
+    form.value.password = password
+  }
+})
+</script>
+
+<style scoped>
+.login {
+  height: 100%;
+  width: 100%;
+  background: #d3eafc;
+  position: relative;
+}
+.login .form_warp {
+  width: 700px;
+  height: 480px;
+  position: absolute;
+  inset: 0;
+  margin: auto;
+  background: white;
+  border-radius: 10px;
+}
+.login .form_warp .header {
+  background: #1f8eea;
+  text-align: center;
+  color: white;
+  padding: 20px;
+  border-radius: 10px 10px 0 0;
+}
+.login .form_warp .header .title {
+  font-size: 40px;
+}
+.login .form_warp .header .title2 {
+  font-size: 20px;
+  color: rgb(230, 225, 225);
+}
+.login .form_warp .form_content {
+  padding: 15px;
+}
+.login .form_warp .form_content .code_content {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  flex: 1;
+}
+.login .form_warp .form_content .code_content .code_input {
+  width: 100%;
+}
+.login .form_warp .form_content .login-code {
+  display: flex;
+  align-items: center;
+  justify-content: space-around;
+  margin: 0 0 0 10px;
+  user-select: none;
+  width: 140px;
+}
+.login .form_warp .form_content .login-code-img {
+  margin-top: 2px;
+  width: 100px;
+  height: 32px;
+  border: 1px solid #dcdfe6;
+  color: #333;
+  font-size: 14px;
+  font-weight: 700;
+  letter-spacing: 5px;
+  line-height: 38px;
+  text-indent: 5px;
+  text-align: center;
+  cursor: pointer;
+  transition: all ease 0.2s;
+  border-radius: 4px;
+}
+.login .form_warp .form_content .login-code-img:hover {
+  border-color: #c0c4cc;
+  transition: all ease 0.2s;
+}
+.login .form_warp .form_content .login-submit {
+  width: 100%;
+}
+</style>

src/views/NewsList.vue → src/views/news/NewsList.vue


+ 9 - 0
src/views/news/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="height_100">
+    <NewsList></NewsList>
+  </div>
+</template>
+
+<script setup lang="ts">
+import NewsList from './NewsList.vue'
+</script>

src/views/ReportDetail.vue → src/views/report/ReportDetail.vue


src/views/ReportList.vue → src/views/report/ReportList.vue


+ 9 - 0
src/views/report/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="height_100">
+    <ReportList></ReportList>
+  </div>
+</template>
+
+<script setup lang="ts">
+import ReportList from './ReportList.vue'
+</script>

src/views/SourceList.vue → src/views/source/SourceList.vue


+ 9 - 0
src/views/source/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div class="height_100">
+    <SourceList></SourceList>
+  </div>
+</template>
+
+<script setup lang="ts">
+import SourceList from './SourceList.vue'
+</script>