zs 2 years ago
parent
commit
1ad3f915a6
60 changed files with 2829 additions and 478 deletions
  1. 7 0
      .env.development
  2. 7 0
      .env.production
  3. 2 2
      .prettierrc.json
  4. 1 46
      README.md
  5. 4 0
      env.d.ts
  6. 13 10
      index.html
  7. 2 2
      package-lock.json
  8. 1 1
      package.json
  9. 14 73
      src/App.vue
  10. 12 28
      src/assets/main.css
  11. 0 40
      src/components/HelloWorld.vue
  12. 0 86
      src/components/TheWelcome.vue
  13. 0 86
      src/components/WelcomeItem.vue
  14. 61 0
      src/components/admin-frame/home.vue
  15. 75 0
      src/components/admin-frame/parts/Header.vue
  16. 108 0
      src/components/admin-frame/parts/Sidebar.vue
  17. 23 0
      src/components/admin-frame/parts/breadcrumb.vue
  18. 26 0
      src/components/frame/c-button.vue
  19. 29 0
      src/components/frame/c-dialog.vue
  20. 236 0
      src/components/frame/c-form.vue
  21. 53 0
      src/components/frame/c-pages.vue
  22. 169 0
      src/components/frame/c-search.vue
  23. 215 0
      src/components/frame/c-table.vue
  24. 108 0
      src/components/frame/c-upload.vue
  25. 69 0
      src/components/frame/wang-editor.vue
  26. 0 7
      src/components/icons/IconCommunity.vue
  27. 0 7
      src/components/icons/IconDocumentation.vue
  28. 0 7
      src/components/icons/IconEcosystem.vue
  29. 0 7
      src/components/icons/IconSupport.vue
  30. 0 19
      src/components/icons/IconTooling.vue
  31. 15 0
      src/components/index.ts
  32. 70 0
      src/layout/site.ts
  33. 29 4
      src/main.ts
  34. 99 10
      src/router/index.ts
  35. 9 10
      src/stores/counter.ts
  36. 52 0
      src/stores/dict/dictData.ts
  37. 52 0
      src/stores/dict/dictType.ts
  38. 6 0
      src/stores/user/mutations.ts
  39. 1 0
      src/stores/user/state.ts
  40. 63 0
      src/stores/users/admin.ts
  41. 150 0
      src/util/axios-wrapper.ts
  42. 29 0
      src/util/types.util.ts
  43. 0 15
      src/views/AboutView.vue
  44. 0 9
      src/views/HomeView.vue
  45. 38 0
      src/views/home/index.vue
  46. 137 0
      src/views/login/index.vue
  47. 38 0
      src/views/match/course/index.vue
  48. 38 0
      src/views/match/enroll/index.vue
  49. 38 0
      src/views/match/match/index.vue
  50. 38 0
      src/views/match/ranking/index.vue
  51. 184 0
      src/views/system/dict/index.vue
  52. 181 0
      src/views/system/dictData/index.vue
  53. 38 0
      src/views/team/examine/index.vue
  54. 38 0
      src/views/team/team/index.vue
  55. 70 0
      src/views/updatepd/index.vue
  56. 38 0
      src/views/users/match/detail.vue
  57. 38 0
      src/views/users/match/index.vue
  58. 38 0
      src/views/users/team/detail.vue
  59. 38 0
      src/views/users/team/index.vue
  60. 29 9
      vite.config.ts

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+VITE_BASE_URL = "/balladmin"
+VITE_OUT_DIR = "balladmin"
+VITE_REQUEST_BASE = ''
+VITE_APP_HOST="https://broadcast.waityou24.cn"
+VITE_APP_PAGE_SIZE=15
+VITE_APP_ROUTER="admin"
+VITE_APP_ENV = 'development'

+ 7 - 0
.env.production

@@ -0,0 +1,7 @@
+VITE_BASE_URL = "/balladmin"
+VITE_OUT_DIR = "balladmin"
+VITE_REQUEST_BASE = ''
+VITE_APP_HOST="https://broadcast.waityou24.cn"
+VITE_APP_PAGE_SIZE=15
+VITE_APP_ROUTER="admin"
+VITE_APP_ENV = 'production'

+ 2 - 2
.prettierrc.json

@@ -1,8 +1,8 @@
 {
   "$schema": "https://json.schemastore.org/prettierrc",
-  "semi": false,
+  "semi": true,
   "tabWidth": 2,
   "singleQuote": true,
   "printWidth": 100,
   "trailingComma": "none"
-}
+}

+ 1 - 46
README.md

@@ -1,46 +1 @@
-# admin
-
-This template should help get you started developing with Vue 3 in Vite.
-
-## Recommended IDE Setup
-
-[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
-
-## Type Support for `.vue` Imports in TS
-
-TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
-
-If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
-
-1. Disable the built-in TypeScript Extension
-    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
-    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
-2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
-
-## Customize configuration
-
-See [Vite Configuration Reference](https://vitejs.dev/config/).
-
-## Project Setup
-
-```sh
-npm install
-```
-
-### Compile and Hot-Reload for Development
-
-```sh
-npm run dev
-```
-
-### Type-Check, Compile and Minify for Production
-
-```sh
-npm run build
-```
-
-### Lint with [ESLint](https://eslint.org/)
-
-```sh
-npm run lint
-```
+# ball_admin

+ 4 - 0
env.d.ts

@@ -1 +1,5 @@
 /// <reference types="vite/client" />
+interface ImportMetaEnv {
+  VITE_BASE_URL: string
+  VITE_OUT_DIR: string
+}

+ 13 - 10
index.html

@@ -1,13 +1,16 @@
 <!DOCTYPE html>
 <html lang="en">
-  <head>
-    <meta charset="UTF-8">
-    <link rel="icon" href="/favicon.ico">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Vite App</title>
-  </head>
-  <body>
-    <div id="app"></div>
-    <script type="module" src="/src/main.ts"></script>
-  </body>
+
+<head>
+  <meta charset="UTF-8">
+  <link rel="icon" href="/favicon.ico">
+  <meta name="viewport" content="width=device-width,user-scalable=yes,initial-scale=0.1">
+  <title>加载中...</title>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+</body>
+
 </html>

+ 2 - 2
package-lock.json

@@ -1,11 +1,11 @@
 {
-  "name": "admin",
+  "name": "ball_admin",
   "version": "0.0.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
-      "name": "admin",
+      "name": "ball_admin",
       "version": "0.0.0",
       "dependencies": {
         "@element-plus/icons-vue": "^2.1.0",

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "admin",
+  "name": "ball_admin",
   "version": "0.0.0",
   "private": true,
   "scripts": {

+ 14 - 73
src/App.vue

@@ -1,85 +1,26 @@
-<script setup lang="ts">
-import { RouterLink, RouterView } from 'vue-router'
-import HelloWorld from './components/HelloWorld.vue'
-</script>
+<script setup lang="ts"></script>
 
 <template>
-  <header>
-    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
-
-    <div class="wrapper">
-      <HelloWorld msg="You did it!" />
-
-      <nav>
-        <RouterLink to="/">Home</RouterLink>
-        <RouterLink to="/about">About</RouterLink>
-      </nav>
-    </div>
-  </header>
-
   <RouterView />
 </template>
 
-<style scoped>
-header {
-  line-height: 1.5;
-  max-height: 100vh;
-}
-
-.logo {
-  display: block;
-  margin: 0 auto 2rem;
-}
-
-nav {
-  width: 100%;
-  font-size: 12px;
-  text-align: center;
-  margin-top: 2rem;
-}
-
-nav a.router-link-exact-active {
-  color: var(--color-text);
-}
-
-nav a.router-link-exact-active:hover {
-  background-color: transparent;
+<style>
+body {
+  margin: 0;
 }
-
-nav a {
-  display: inline-block;
-  padding: 0 1rem;
-  border-left: 1px solid var(--color-border);
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
 }
 
-nav a:first-of-type {
-  border: 0;
+p {
+  margin: 0;
+  padding: 0;
 }
 
-@media (min-width: 1024px) {
-  header {
-    display: flex;
-    place-items: center;
-    padding-right: calc(var(--section-gap) / 2);
-  }
-
-  .logo {
-    margin: 0 2rem 0 0;
-  }
-
-  header .wrapper {
-    display: flex;
-    place-items: flex-start;
-    flex-wrap: wrap;
-  }
-
-  nav {
-    text-align: left;
-    margin-left: -1rem;
-    font-size: 1rem;
-
-    padding: 1rem 0;
-    margin-top: 1rem;
-  }
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 </style>

+ 12 - 28
src/assets/main.css

@@ -1,35 +1,19 @@
-@import './base.css';
-
-#app {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-
-  font-weight: normal;
+body {
+  margin: 0;
 }
 
-a,
-.green {
-  text-decoration: none;
-  color: hsla(160, 100%, 37%, 1);
-  transition: 0.4s;
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
 }
 
-@media (hover: hover) {
-  a:hover {
-    background-color: hsla(160, 100%, 37%, 0.2);
-  }
+p {
+  margin: 0;
+  padding: 0;
 }
 
-@media (min-width: 1024px) {
-  body {
-    display: flex;
-    place-items: center;
-  }
-
-  #app {
-    display: grid;
-    grid-template-columns: 1fr 1fr;
-    padding: 0 2rem;
-  }
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }

+ 0 - 40
src/components/HelloWorld.vue

@@ -1,40 +0,0 @@
-<script setup lang="ts">
-defineProps<{
-  msg: string
-}>()
-</script>
-
-<template>
-  <div class="greetings">
-    <h1 class="green">{{ msg }}</h1>
-    <h3>
-      You’ve successfully created a project with
-      <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
-      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
-    </h3>
-  </div>
-</template>
-
-<style scoped>
-h1 {
-  font-weight: 500;
-  font-size: 2.6rem;
-  top: -10px;
-}
-
-h3 {
-  font-size: 1.2rem;
-}
-
-.greetings h1,
-.greetings h3 {
-  text-align: center;
-}
-
-@media (min-width: 1024px) {
-  .greetings h1,
-  .greetings h3 {
-    text-align: left;
-  }
-}
-</style>

+ 0 - 86
src/components/TheWelcome.vue

@@ -1,86 +0,0 @@
-<script setup lang="ts">
-import WelcomeItem from './WelcomeItem.vue'
-import DocumentationIcon from './icons/IconDocumentation.vue'
-import ToolingIcon from './icons/IconTooling.vue'
-import EcosystemIcon from './icons/IconEcosystem.vue'
-import CommunityIcon from './icons/IconCommunity.vue'
-import SupportIcon from './icons/IconSupport.vue'
-</script>
-
-<template>
-  <WelcomeItem>
-    <template #icon>
-      <DocumentationIcon />
-    </template>
-    <template #heading>Documentation</template>
-
-    Vue’s
-    <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
-    provides you with all information you need to get started.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <ToolingIcon />
-    </template>
-    <template #heading>Tooling</template>
-
-    This project is served and bundled with
-    <a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
-    recommended IDE setup is
-    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
-    <a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
-    you need to test your components and web pages, check out
-    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
-    <a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
-
-    <br />
-
-    More instructions are available in <code>README.md</code>.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <EcosystemIcon />
-    </template>
-    <template #heading>Ecosystem</template>
-
-    Get official tools and libraries for your project:
-    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
-    <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
-    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
-    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
-    you need more resources, we suggest paying
-    <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
-    a visit.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <CommunityIcon />
-    </template>
-    <template #heading>Community</template>
-
-    Got stuck? Ask your question on
-    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
-    Discord server, or
-    <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
-      >StackOverflow</a
-    >. You should also subscribe to
-    <a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
-    the official
-    <a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
-    twitter account for latest news in the Vue world.
-  </WelcomeItem>
-
-  <WelcomeItem>
-    <template #icon>
-      <SupportIcon />
-    </template>
-    <template #heading>Support Vue</template>
-
-    As an independent project, Vue relies on community backing for its sustainability. You can help
-    us by
-    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
-  </WelcomeItem>
-</template>

+ 0 - 86
src/components/WelcomeItem.vue

@@ -1,86 +0,0 @@
-<template>
-  <div class="item">
-    <i>
-      <slot name="icon"></slot>
-    </i>
-    <div class="details">
-      <h3>
-        <slot name="heading"></slot>
-      </h3>
-      <slot></slot>
-    </div>
-  </div>
-</template>
-
-<style scoped>
-.item {
-  margin-top: 2rem;
-  display: flex;
-}
-
-.details {
-  flex: 1;
-  margin-left: 1rem;
-}
-
-i {
-  display: flex;
-  place-items: center;
-  place-content: center;
-  width: 32px;
-  height: 32px;
-
-  color: var(--color-text);
-}
-
-h3 {
-  font-size: 1.2rem;
-  font-weight: 500;
-  margin-bottom: 0.4rem;
-  color: var(--color-heading);
-}
-
-@media (min-width: 1024px) {
-  .item {
-    margin-top: 0;
-    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
-  }
-
-  i {
-    top: calc(50% - 25px);
-    left: -26px;
-    position: absolute;
-    border: 1px solid var(--color-border);
-    background: var(--color-background);
-    border-radius: 8px;
-    width: 50px;
-    height: 50px;
-  }
-
-  .item:before {
-    content: ' ';
-    border-left: 1px solid var(--color-border);
-    position: absolute;
-    left: 0;
-    bottom: calc(50% + 25px);
-    height: calc(50% - 25px);
-  }
-
-  .item:after {
-    content: ' ';
-    border-left: 1px solid var(--color-border);
-    position: absolute;
-    left: 0;
-    top: calc(50% + 25px);
-    height: calc(50% - 25px);
-  }
-
-  .item:first-of-type:before {
-    display: none;
-  }
-
-  .item:last-of-type:after {
-    display: none;
-  }
-}
-</style>

+ 61 - 0
src/components/admin-frame/home.vue

@@ -0,0 +1,61 @@
+<template>
+  <el-container class="main">
+    <el-header :style="{ padding: 0 }"> <component :is="cHeader"></component></el-header>
+    <el-container>
+      <el-aside width="200px" :style="{ 'background-color': '#242f42' }"><component :is="cAside"></component></el-aside>
+      <el-main>
+        <div class="content-box" :class="{ 'content-collapse': collapse }">
+          <el-col :span="24" class="content">
+            <transition name="move" mode="out-in">
+              <el-row>
+                <component :is="cBreadcrumb" :breadcrumbTitle="route.meta.title"></component>
+                <el-col :span="24" class="container" :style="{ padding: '10px 0' }"><router-view :style="testInfo"></router-view></el-col>
+              </el-row>
+            </transition>
+            <el-backtop target=".content"></el-backtop>
+          </el-col>
+        </div>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script setup lang="ts">
+// 组件
+import cHeader from './parts/Header.vue';
+import cAside from './parts/Sidebar.vue';
+import cBreadcrumb from './parts/breadcrumb.vue';
+import { useRoute } from 'vue-router';
+import type { Ref } from 'vue';
+import { ref } from 'vue';
+
+// 路由
+const route = useRoute();
+// const breadcrumbTitle: Ref<any> = ref();
+let collapse: Ref<any> = ref(false);
+const testInfo: Ref<any> = ref({
+  height: '85vh',
+  background: '#ffffff',
+  'overflow-x': 'hidden',
+  'overflow-y': 'auto',
+  border: '1px solid #f0f0f0',
+  padding: '10px'
+});
+</script>
+<style scoped lang="scss">
+.main {
+  height: 100vh;
+  background-color: #f0f0f0;
+  .el-header {
+    border-bottom: 1px solid;
+  }
+  .el-aside {
+    height: 93.3vh;
+    overflow-x: auto;
+    overflow-y: auto;
+  }
+  .el-main {
+    padding: 10px;
+  }
+}
+</style>

+ 75 - 0
src/components/admin-frame/parts/Header.vue

@@ -0,0 +1,75 @@
+<template>
+  <div id="Header">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="main header">
+          <el-col :span="24" class="one">
+            <el-col :span="12" class="left">
+              <span>
+                <i v-if="!collapse" class="el-icon-s-fold"></i>
+                <i v-else class="el-icon-s-unfold"></i>
+              </span>
+              <span>{{ siteInfo.zhTitle }}-管理中心</span>
+            </el-col>
+            <el-col :span="12" class="right">
+              <span>
+                <el-icon><UserFilled /></el-icon>
+                {{ user && user._id ? user.name : '游客' }}
+              </span>
+              <el-button type="danger" size="small" @click="logout">退出登录</el-button>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { siteInfo } from '@/layout/site';
+import store from '@/stores/counter';
+import type { Ref } from 'vue';
+import { ref } from 'vue';
+let collapse: Ref<any> = ref(false);
+let user: Ref<any> = ref(store.state.user);
+// 退出登录
+const logout = () => {
+  localStorage.removeItem('token');
+  window.location.href = `${import.meta.env.VITE_APP_HOST}${import.meta.env.VITE_BASE_URL}`;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  background-color: #242f42;
+  .one {
+    height: 60px;
+    border-bottom: 1px solid #f1f1f1;
+    padding: 0 10px;
+    display: flex;
+    .left {
+      line-height: 60px;
+      span {
+        display: inline-block;
+        margin: 0 10px;
+        font-size: 22px;
+        color: #ffffff;
+        font-weight: bold;
+        font-family: cursive;
+      }
+    }
+    .right {
+      text-align: right;
+      padding: 16px 0;
+      span {
+        padding: 0 8px;
+        color: #ffffff;
+        position: relative;
+        top: 1px;
+      }
+      .el-icon {
+        top: 2px;
+      }
+    }
+  }
+}
+</style>

+ 108 - 0
src/components/admin-frame/parts/Sidebar.vue

@@ -0,0 +1,108 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <div id="Sidebar">
+    <el-row class="sidebar">
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="one">
+          <el-menu
+            class="sidebar-el-menu"
+            :default-active="onRoutes"
+            unique-opened
+            router
+            :background-color="styleInfo.backColor"
+            :text-color="styleInfo.textColor"
+            :active-text-color="styleInfo.activeColor"
+          >
+            <template v-for="item in items">
+              <template v-if="item.type === '0'">
+                <el-sub-menu :index="item._id" :key="item._id">
+                  <template #title>
+                    <i :class="['iconfont', item.icon]"></i>
+                    <span>{{ item.name }}</span>
+                  </template>
+                  <template v-for="subItem in item.children">
+                    <el-sub-menu v-if="subItem.children && subItem.children.length > 0" :index="subItem._id" :key="subItem._id">
+                      <template #title>
+                        <i :class="['iconfont', subItem.icon]"></i>
+                        <span>{{ subItem.name }}</span>
+                      </template>
+                      <el-menu-item v-for="(threeItem, i) in subItem.children" :key="i" :index="threeItem.path">
+                        <i :class="['iconfont', threeItem.icon]"></i>
+                        <span>{{ threeItem.name }}</span>
+                      </el-menu-item>
+                    </el-sub-menu>
+                    <el-menu-item v-else :index="subItem.path" :key="subItem.path">
+                      <i :class="['iconfont', subItem.icon]"></i>
+                      <span>{{ subItem.name }}</span>
+                    </el-menu-item>
+                  </template>
+                </el-sub-menu>
+              </template>
+              <template v-else>
+                <el-menu-item :index="item.path" :key="item.path">
+                  <i :class="['iconfont', item.icon]"></i>
+                  <span>{{ item.name }}</span>
+                </el-menu-item>
+              </template>
+            </template>
+          </el-menu>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { menuInfo } from '@/layout/site';
+import { ElMessage } from 'element-plus';
+import store from '@/stores/counter';
+import type { Ref } from 'vue';
+import { ref, onMounted, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
+const route = useRoute();
+
+let onRoutes = route.path;
+let user: Ref<any> = ref({});
+const styleInfo: Ref<any> = ref(menuInfo.info);
+let items: Ref<any> = ref(menuInfo.menuList);
+onMounted(async () => {
+  user.value = store.state.user;
+});
+
+const getMenu = async () => {
+  // console.log('1');
+};
+
+watch(
+  user,
+  (newVal) => {
+    if (newVal && newVal._id) {
+      if (newVal && newVal._id) getMenu();
+      else ElMessage({ message: `暂无用户信息,无法获取菜单信息`, type: 'error' });
+    }
+  },
+  {
+    deep: true
+  }
+);
+</script>
+<style scoped lang="scss">
+.sidebar::-webkit-scrollbar {
+  width: 0;
+}
+.sidebar-el-menu:not(.el-menu--collapse) {
+  width: 201px;
+}
+.sidebar > ul {
+  height: 100%;
+}
+.main {
+  .one {
+    .iconfont {
+      font-size: 18px;
+      margin: 0 5px 0 0;
+    }
+  }
+}
+</style>

+ 23 - 0
src/components/admin-frame/parts/breadcrumb.vue

@@ -0,0 +1,23 @@
+<template>
+  <div id="breadcrumb">
+    <el-row>
+      <el-col :span="24" class="crumbs">
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item> <i class="el-icon-s-grid"></i> {{ breadcrumbTitle }} </el-breadcrumb-item>
+        </el-breadcrumb>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { toRefs } from 'vue';
+// #region 参数传递
+const props = defineProps({
+  breadcrumbTitle: { type: String, default: () => '' }
+});
+const { breadcrumbTitle } = toRefs(props);
+
+// #endregion
+</script>
+<style lang="scss" scoped></style>

+ 26 - 0
src/components/frame/c-button.vue

@@ -0,0 +1,26 @@
+<template>
+  <div id="c-button">
+    <el-button type="primary" @click="toAdd()" v-if="isAdd">新增</el-button>
+    <el-button type="warning" v-if="isEdit">修改</el-button>
+    <el-button type="danger" v-if="isDel">删除</el-button>
+    <el-button type="warning" @click="toExport()" v-if="isExport">导出</el-button>
+    <slot name="custom"></slot>
+  </div>
+</template>
+<script setup lang="ts">
+import { toRefs } from 'vue';
+const props = defineProps({
+  isAdd: { type: Boolean, default: () => true },
+  isEdit: { type: Boolean, default: () => false },
+  isDel: { type: Boolean, default: () => false },
+  isExport: { type: Boolean, default: () => false }
+});
+const { isAdd } = toRefs(props);
+const emit = defineEmits(['toAdd', 'toExport']);
+const toAdd = () => {
+  emit('toAdd');
+};
+const toExport = () => {
+  emit('toExport');
+};
+</script>

+ 29 - 0
src/components/frame/c-dialog.vue

@@ -0,0 +1,29 @@
+<template>
+  <div id="e-dialog">
+    <el-dialog :title="dialog.title" v-model="dialog.show" :width="width" :before-close="toClose" :close-on-click-modal="false" :append-to-body="true">
+      <el-col :span="24" class="dialogInfo" :style="{ 'max-height': height }"><slot name="info"></slot></el-col>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { toRefs } from 'vue';
+const props = defineProps({
+  dialog: { type: Object, default: () => {} },
+  width: { type: String, default: '40%' },
+  height: { type: String, default: '400px' }
+});
+const { dialog } = toRefs(props);
+const { width } = toRefs(props);
+const emit = defineEmits(['toClose']);
+const toClose = () => {
+  emit('toClose');
+};
+</script>
+
+<style lang="scss" scoped>
+.dialogInfo {
+  min-height: 30px;
+  overflow-y: auto;
+}
+</style>

+ 236 - 0
src/components/frame/c-form.vue

@@ -0,0 +1,236 @@
+<template>
+  <div id="c-form">
+    <el-row type="flex" justify="space-around">
+      <el-col>
+        <el-form ref="formRef" :model="form" :rules="rules" :label-width="labelWidth" class="form" @submit.prevent :disabled="disabled">
+          <!-- <template > -->
+          <el-col :span="span" v-for="(item, index) in fields" :key="index">
+            <el-form-item v-if="display(item)" :key="'form-field-' + index" :label="getField('label', item)" :prop="item.model" :required="item.required">
+              <template v-if="!item.custom">
+                <template v-if="item.type === 'textarea'">
+                  <el-input
+                    clearable
+                    v-model="form[item.model]"
+                    :type="item.type"
+                    :placeholder="getField('placeholder', item)"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                    show-word-limit
+                  ></el-input>
+                </template>
+                <template v-else-if="item.type === 'numbers'">
+                  <el-input-number
+                    v-model="form[item.model]"
+                    :placeholder="getField('placeholder', item)"
+                    @change="dataChange(item.model)"
+                    style="width: 100%"
+                  />
+                </template>
+                <template v-else-if="item.type === 'radio'">
+                  <el-radio-group v-model="form[item.model]" :type="item.type" v-bind="item.options" @change="dataChange(item.model)">
+                    <slot :name="item.model" v-bind="{ item }"></slot>
+                  </el-radio-group>
+                </template>
+                <template v-else-if="item.type === 'checkbox'">
+                  <el-checkbox-group v-model="form[item.model]" :type="item.type" v-bind="item.options">
+                    <slot :name="item.model" v-bind="{ item }"></slot>
+                  </el-checkbox-group>
+                </template>
+                <template v-else-if="item.type === 'select'">
+                  <el-select
+                    clearable
+                    filterable
+                    allow-create
+                    default-first-option
+                    v-model="form[item.model]"
+                    :type="item.type"
+                    :placeholder="getField('selectplaceholder', item)"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                    style="width: 100%"
+                  >
+                    <slot :name="item.model" v-bind="{ item }"></slot>
+                  </el-select>
+                </template>
+                <template v-else-if="item.type === 'selectMany'">
+                  <el-select
+                    filterable
+                    clearable
+                    multiple
+                    collapse-tags
+                    v-model="form[item.model]"
+                    :type="item.type"
+                    :placeholder="getField('selectplaceholder', item)"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                    style="width: 100%"
+                  >
+                    <slot :name="item.model" v-bind="{ item }"></slot>
+                  </el-select>
+                </template>
+                <template
+                  v-else-if="
+                    item.type === `year` ||
+                    item.type == 'month' ||
+                    item.type == 'date' ||
+                    item.type == 'daterange' ||
+                    item.type == 'datetime' ||
+                    item.type == 'datetimerange'
+                  "
+                >
+                  <el-date-picker
+                    v-model="form[item.model]"
+                    :type="item.type"
+                    :placeholder="getField('selectplaceholder', item)"
+                    :format="getDateFormat(item.type)"
+                    :value-format="getDateFormat(item.type)"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                    range-separator="至"
+                    style="width: 100%"
+                  >
+                  </el-date-picker>
+                </template>
+                <template v-else-if="item.type === `time`">
+                  <el-time-picker
+                    v-model="form[item.model]"
+                    :placeholder="getField('selectplaceholder', item)"
+                    :format="getDateFormat(item.type)"
+                    :value-format="getDateFormat(item.type)"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                    style="width: 100%"
+                  >
+                  </el-time-picker>
+                </template>
+                <template v-else-if="item.type === `inputnumber`">
+                  <el-input-number
+                    v-model="form[item.model]"
+                    :placeholder="getField('placeholder', item)"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                    style="width: 100%"
+                  ></el-input-number>
+                </template>
+                <template v-else>
+                  <el-input
+                    clearable
+                    v-model="form[item.model]"
+                    :type="getField('type', item)"
+                    :placeholder="getField('placeholder', item)"
+                    :show-password="getField('type', item) === 'password'"
+                    v-bind="item.options"
+                    @change="dataChange(item.model)"
+                  ></el-input>
+                </template>
+              </template>
+              <template v-else>
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </template>
+            </el-form-item>
+          </el-col>
+          <!-- </template> -->
+          <el-col :span="24" label="" class="btn" v-if="isSave">
+            <slot name="submit">
+              <el-button type="primary" @click="save(formRef)">{{ submitText }}</el-button>
+            </slot>
+          </el-col>
+        </el-form>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, toRefs } from 'vue';
+import type { FormInstance } from 'element-plus';
+import _ from 'lodash';
+// #region 传递
+interface fieldsItem {
+  model: string;
+  type: string;
+  options: object;
+  custom: string;
+  required: string;
+  limit: number | undefined;
+  url: string;
+}
+const props = defineProps({
+  fields: { type: Array<fieldsItem>, default: () => [] },
+  rules: { type: Object, default: () => {} },
+  labelWidth: { type: String, default: '120px' },
+  submitText: { type: String, default: '提交保存' },
+  form: { type: Object, default: () => {} },
+  reset: { type: Boolean, default: false },
+  isSave: { type: Boolean, default: true },
+  span: { type: Number, default: 24 }, // 限制两侧的距离,24就是整行全用
+  disabled: { type: Boolean, default: false }
+});
+const { fields } = toRefs(props);
+const { rules } = toRefs(props);
+const { labelWidth } = toRefs(props);
+const { submitText } = toRefs(props);
+const { form } = toRefs(props);
+const { reset } = toRefs(props);
+const { isSave } = toRefs(props);
+const { span } = toRefs(props);
+const { disabled } = toRefs(props);
+const formRef = ref<FormInstance>();
+const getField = (item: string, data: any) => {
+  let res: string | null | boolean = _.get(data, item, null);
+  if (item === 'type') res = res === null ? `text` : res;
+  if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res;
+  if (item === `selectplaceholder`) res = res === null ? `请选择${data.label}` : res;
+  if (item === 'required') res = res === null ? false : res;
+  if (item === `error`) res = res === null ? `${data.label}错误` : res;
+  return res;
+};
+
+const getDateFormat = (e: string) => {
+  if (e === 'year') return 'YYYY';
+  if (e === 'month') return 'MM';
+  if (e === 'date') return 'YYYY-MM-DD';
+  if (e === 'daterange') return 'YYYY-MM-DD';
+  if (e === 'datetime') return 'YYYY-MM-DD HH:mm:ss';
+  if (e === 'datetimerange') return 'YYYY-MM-DD HH:mm:ss';
+  if (e === 'time') return 'HH:mm:ss';
+};
+const emit = defineEmits(['save', 'dataChange']);
+const clear = ref<any>();
+// 提交
+const save = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      emit('save', form.value);
+      if (reset.value) clear.value.resetFields();
+    } else {
+      console.log('error submit!', fields);
+    }
+  });
+};
+const display = (field: any) => {
+  let dis = _.get(field, `display`);
+  if (!_.isFunction(dis)) return true;
+  else return dis(field, form);
+};
+const dataChange = (model: string) => {
+  const value = form.value[model];
+  emit('dataChange', { model, value });
+};
+const sss = () => {
+  console.log(1);
+};
+defineExpose({ sss });
+</script>
+
+<style scoped>
+.btn {
+  text-align: center;
+}
+.form {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+</style>

+ 53 - 0
src/components/frame/c-pages.vue

@@ -0,0 +1,53 @@
+<template>
+  <div id="c-pages">
+    <el-row justify="center" class="c-pages" v-loading="loading">
+      <el-pagination
+        background
+        layout="total, prev, pager, next"
+        :page-sizes="[10, 20, 50, 100, 200]"
+        :total="total"
+        :page-size="limit"
+        v-model:current-page="currentPage"
+        @current-change="changePage"
+        @size-change="sizeChange"
+      >
+      </el-pagination>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, toRefs } from 'vue';
+
+const props = defineProps({
+  total: { type: Number, default: () => 0 },
+  limit: { type: Number, default: () => 10 }
+});
+const { total } = toRefs(props);
+const { limit } = toRefs(props);
+
+const emit = defineEmits(['toSelect', 'query']);
+
+// 加载中
+const loading: Ref<any> = ref(false);
+
+const currentPage: Ref<number> = ref(1);
+
+// 分页
+const changePage = (page: number = currentPage.value) => {
+  emit('query', { skip: (page - 1) * limit.value, limit: limit.value });
+};
+const sizeChange = (limits: number) => {
+  console.log(limits);
+};
+
+// 请求
+onMounted(async () => {
+  loading.value = true;
+
+  loading.value = false;
+});
+</script>
+<style scoped lang="scss"></style>

+ 169 - 0
src/components/frame/c-search.vue

@@ -0,0 +1,169 @@
+<template>
+  <div id="c-search">
+    <el-row>
+      <el-col :span="24" class="title" v-if="is_title">
+        <el-col :span="24" class="title_1">
+          <span>{{ title || $route.meta.title }}</span>
+          <span>{{ tip }}</span>
+        </el-col>
+        <el-col :span="24" class="title_2">
+          <span>{{ remark }}</span>
+        </el-col>
+      </el-col>
+      <el-col :span="24" class="search" v-if="is_search">
+        <el-form ref="formRef" :model="form" label-width="auto">
+          <el-col :span="24" class="form">
+            <template v-for="(item, index) in fields">
+              <el-col :span="8" class="form_1" :key="'form-field-' + index" v-if="item.isSearch == true">
+                <el-form-item :label="getField('label', item)" :prop="item.model">
+                  <template v-if="!item.custom">
+                    <template v-if="item.type === 'select'">
+                      <el-select v-model="form[item.model]" v-bind="item.options" filterable clearable @change="dataChange(item.model)">
+                        <slot :name="item.model" v-bind="{ item }"></slot>
+                      </el-select>
+                    </template>
+                    <template v-else>
+                      <el-input
+                        v-model="form[item.model]"
+                        :type="getField('type', item)"
+                        :placeholder="getField('place', item)"
+                        clearable
+                        v-bind="item.options"
+                        @change="dataChange(item.model)"
+                      ></el-input>
+                    </template>
+                  </template>
+                  <template v-else>
+                    <slot :name="item.model" v-bind="{ item }"></slot>
+                    <!-- <el-input v-model="form[item.model]" clearable :placeholder="`输入${item.label}`"></el-input> -->
+                  </template>
+                </el-form-item>
+              </el-col>
+            </template>
+          </el-col>
+          <el-col :span="24" class="btn">
+            <el-button type="primary" @click="toSubmit()">查询</el-button>
+            <el-button type="danger" @click="toReset()">重置</el-button>
+          </el-col>
+        </el-form>
+      </el-col>
+      <el-col :span="24" class="back" v-if="is_back">
+        <el-button type="primary" @click="toBack()">返回</el-button>
+      </el-col>
+      <el-col :span="24" class="slot"><slot name="isslot"></slot></el-col>
+    </el-row>
+  </div>
+</template>
+<script lang="ts" setup>
+import { ref, toRefs } from 'vue';
+import type { Ref } from 'vue';
+import _ from 'lodash';
+interface fieldsItem {
+  model: string;
+  type: string;
+  // readonly: string;
+  options: object;
+  custom: string;
+  // required: string;
+  // limit: number | undefined;
+  isSearch: boolean;
+}
+const props = defineProps({
+  is_title: { type: Boolean, default: true },
+  is_search: { type: Boolean, default: false },
+  is_back: { type: Boolean, default: false },
+  fields: { type: Array<fieldsItem> },
+  title: { type: String },
+  tip: { type: String },
+  remark: { type: String }
+});
+const { is_title } = toRefs(props);
+const { is_search } = toRefs(props);
+const { is_back } = toRefs(props);
+const { fields } = toRefs(props);
+const { title } = toRefs(props);
+const { tip } = toRefs(props);
+const { remark } = toRefs(props);
+
+let form: Ref<{}> = ref({});
+const emit = defineEmits(['search', 'toReset', 'toBack', 'dataChange']);
+const toSubmit = () => {
+  const obj = _.pickBy(form.value);
+  emit('search', { ...obj });
+};
+// 重置
+const toReset = () => {
+  form.value = {};
+  emit('search', form.value);
+};
+// 文字描述
+const getField = (item: any, data: any) => {
+  let res = _.get(data, item, null);
+  if (item === 'type') res = res === null ? `text` : res;
+  if (item === 'place') res = res === null ? `请输入${data.label}` : res;
+  if (item === 'required') res = res === null ? false : res;
+  if (item === `error`) res = res === null ? `${data.label}错误` : res;
+  return res;
+};
+// 获取输入值
+const dataChange = (model: string) => {
+  const value = form.value[model];
+  emit('dataChange', { model, value });
+};
+// 返回
+const toBack = () => {
+  emit('toBack');
+};
+</script>
+
+<style lang="scss" scoped>
+.title {
+  margin: 0 0 5px 0;
+  .title_1 {
+    margin: 0 0 5px 0;
+    span:first-child {
+      font-size: 20px;
+      font-weight: 700;
+      margin-right: 10px;
+    }
+    span:last-child {
+      font-size: 14px;
+      color: #979797;
+    }
+  }
+  .title_2 {
+    span {
+      color: #8baae7;
+      font-size: 14px;
+      margin-top: 10px;
+    }
+  }
+}
+.search {
+  margin: 0 0 10px 0;
+  .form {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    .form_1 {
+      padding: 0 0 0 10px;
+      .el-form-item {
+        float: left;
+        width: 100%;
+        margin: 0 0 10px 0;
+      }
+      .el-select {
+        width: 100%;
+      }
+    }
+  }
+
+  .btn {
+    text-align: right;
+  }
+}
+.back {
+  text-align: left;
+  margin: 0 0 10px 0;
+}
+</style>

+ 215 - 0
src/components/frame/c-table.vue

@@ -0,0 +1,215 @@
+<template>
+  <el-table :data="list" border stripe @selection-change="toSelect">
+    <el-table-column type="selection" width="55" v-if="select"> </el-table-column>
+    <template v-for="(item, index) in fields">
+      <template v-if="item.custom">
+        <el-table-column :key="index" :label="item.label" v-bind="item.options" align="center" :show-overflow-tooltip="item.showTip || true">
+          <template v-slot="{ row }">
+            <slot :name="item.model" v-bind="{ item, row }"></slot>
+          </template>
+        </el-table-column>
+      </template>
+      <template v-else>
+        <el-table-column
+          :key="index"
+          :label="item.label"
+          :prop="item.model"
+          :formatter="toFormatter"
+          v-bind="item.options"
+          :sortable="true"
+          align="center"
+          :show-overflow-tooltip="item.showTip === false ? item.showTip : true"
+        ></el-table-column>
+      </template>
+    </template>
+    <template v-if="opera.length > 0">
+      <el-table-column label="操作" fixed="right" width="110" align="center">
+        <template v-slot="{ row, $index }">
+          <template v-for="(item, index) in opera">
+            <template v-if="display(item, row)">
+              <template v-if="vOpera">
+                <el-link
+                  :key="`${item.model}-column-${index}`"
+                  :type="item.type || 'primary'"
+                  size="small"
+                  :underline="false"
+                  class="link"
+                  v-opera="item.method"
+                  @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index, item.confirmWord)"
+                >
+                  {{ item.label }}
+                </el-link>
+              </template>
+              <template v-else>
+                <el-link
+                  :key="`${item.model}-column-${index}`"
+                  :type="item.type || 'primary'"
+                  size="small"
+                  :underline="false"
+                  class="link"
+                  @click="handleOpera(row, item.method, item.confirm, item.methodZh, item.label, $index, item.confirmWord)"
+                >
+                  {{ item.label }}
+                </el-link>
+              </template>
+            </template>
+          </template>
+        </template>
+      </el-table-column>
+    </template>
+  </el-table>
+  <el-row justify="end" class="page" v-if="usePage">
+    <!-- sizes -->
+    <el-pagination
+      background
+      layout="total, prev, pager, next"
+      :page-sizes="[10, 20, 50, 100, 200]"
+      :total="total"
+      :page-size="limit"
+      v-model:current-page="currentPage"
+      @current-change="changePage"
+      @size-change="sizeChange"
+    >
+    </el-pagination>
+  </el-row>
+</template>
+
+<script setup lang="ts">
+// 基础
+import _ from 'lodash';
+import type { Ref } from 'vue';
+import { onMounted, toRefs, getCurrentInstance, ref } from 'vue';
+import { ElMessageBox } from 'element-plus';
+const { proxy } = getCurrentInstance() as any;
+const emit = defineEmits(['toSelect', 'query']);
+
+// 参数
+interface fieldsItem {
+  // 是否自定义
+  custom: string;
+  // 名称
+  label: string;
+  // 绑定键值
+  model: string;
+  // 其他
+  options: string;
+  // 是否超出隐藏
+  showTip: boolean;
+}
+interface operaItem {
+  // 名称
+  label: string;
+  model: string;
+  // 绑定键值
+  method: string;
+  // 颜色
+  type: string;
+  // 是否开启弹框
+  confirm: boolean;
+  // 是否自定义文字说明
+  confirmWord: string;
+  // 简短文字
+  methodZh: string;
+}
+interface dataItem {
+  _id?: string;
+}
+
+const props = defineProps({
+  // 列表配置
+  fields: { type: Array<fieldsItem>, required: true },
+  // 操作配置
+  opera: { type: Array<operaItem>, required: true },
+  // 列表数据
+  list: { type: Array<dataItem>, required: true },
+  // 是否开启多选
+  select: { type: Boolean, default: false },
+  // selected: { type: Array, default: () => [] },
+  vOpera: { type: Boolean, default: false },
+  // 分页
+  usePage: { type: Boolean, default: true },
+  total: { type: Number, default: 0 }
+});
+const { fields } = toRefs(props);
+const { opera } = toRefs(props);
+const { list } = toRefs(props);
+const { select } = toRefs(props);
+// const { selected } = toRefs(props);
+const { vOpera } = toRefs(props);
+const { usePage } = toRefs(props);
+const { total } = toRefs(props);
+
+let limit: number = proxy.$limit;
+let currentPage: Ref<number> = ref(1);
+// 请求
+onMounted(async () => {});
+// 多选
+const toSelect = (val: Array<[]>) => {
+  emit(`toSelect`, val);
+};
+// 格式化内容
+const toFormatter = (row: any, column: { property: string }, cellValue: string) => {
+  let this_fields = fields.value.filter((fil) => fil.model === column.property);
+  if (this_fields.length > 0) {
+    let format: any = _.get(this_fields[0], `format`, false);
+    if (format) {
+      let res;
+      if (_.isFunction(format)) {
+        res = format(cellValue);
+      } else {
+        res = toFormat({ model: this_fields[0].model, value: cellValue });
+      }
+      return res;
+    } else {
+      return cellValue;
+    }
+  }
+};
+const toFormat = (e: { model: string; value: string }) => {
+  console.log(e);
+};
+// 过滤
+const display = (item: operaItem, row: any) => {
+  let display: any = _.get(item, `display`, true);
+  if (display === true) return true;
+  else {
+    let res = display(row);
+    return res;
+  }
+};
+// 操作
+const handleOpera = (data: string, method: any, confirm = false, methodZh: string, label: string, index: string, confirmWord: string) => {
+  let self = true;
+  if (_.isFunction(methodZh)) methodZh = methodZh(data);
+  else if (!_.isString(methodZh)) {
+    methodZh = label;
+    self = false;
+  }
+  if (confirm) {
+    let word = self ? methodZh : `您确认${methodZh}该数据?`;
+    if (confirmWord) word = confirmWord;
+    ElMessageBox.confirm(word, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
+      .then(() => {
+        emit(method, data, index);
+      })
+      .catch(() => {});
+  } else emit(method, data, index);
+};
+// 分页
+const changePage = (page: number = currentPage.value) => {
+  emit('query', { skip: (page - 1) * limit, limit: limit });
+};
+const sizeChange = (limits: number) => {
+  limit = limits;
+  currentPage.value = 1;
+  emit('query', { skip: 0, limit: limit });
+};
+</script>
+<style scoped lang="scss">
+.link {
+  padding: 0 5px 0 0;
+}
+.page {
+  margin: 10px 0 0 0;
+}
+</style>

+ 108 - 0
src/components/frame/c-upload.vue

@@ -0,0 +1,108 @@
+<template>
+  <div id="c-upload">
+    <el-upload
+      v-if="url"
+      ref="upload"
+      :action="url"
+      :limit="limit"
+      :accept="accept"
+      :file-list="list"
+      :list-type="listType"
+      :on-exceed="outLimit"
+      :on-preview="filePreview"
+      :on-success="onSuccess"
+      :before-remove="onRemove"
+    >
+      <el-button type="primary">选择文件</el-button>
+      <template #tip v-if="tip">
+        <p style="color: #ff0000">{{ tip }}</p>
+      </template>
+    </el-upload>
+    <el-dialog v-model="dialog.show" append-to-body>
+      <img width="100%" :src="dialog.url" alt="" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Ref } from 'vue';
+import { ref, toRefs } from 'vue';
+import { ElMessage } from 'element-plus';
+import _ from 'lodash';
+
+// #region
+interface ListItem {
+  errcode?: string | number;
+  errmsg?: string;
+  uri?: string;
+  name?: string;
+  url?: string;
+  id?: any;
+}
+let dialog: Ref<{ show: boolean; url: string }> = ref({ show: false, url: '' });
+const props = defineProps({
+  url: { type: String, default: () => '' },
+  limit: { type: Number, default: () => 6 },
+  accept: { type: String, default: () => 'image/png, image/jpeg' },
+  listType: { type: String, default: () => 'text' }, //'text' | 'picture' | 'picture-card'
+  tip: { type: String, default: () => undefined },
+  list: { type: Array<ListItem>, default: () => [] },
+  model: { type: String, default: () => '' }
+});
+// 图片上传地址
+const { url } = toRefs(props);
+// 可上传文件数目
+const { limit } = toRefs(props);
+// 接收上传的文件类型
+const { accept } = toRefs(props);
+// 文件列表的类型--picture-card---picture
+const { listType } = toRefs(props);
+// 文件提醒
+const { tip } = toRefs(props);
+// 已有数据,赋值,预览
+const { list } = toRefs(props);
+const { model } = toRefs(props);
+// const list = ref<UploadUserFile[]>([]);
+
+const emit = defineEmits(['change']);
+// 图片预览
+const filePreview = (file: { url: string }) => {
+  // this.dialog = { show: true, url: file.url };
+  window.open(file.url);
+};
+// 只允许上传多少个文件
+const outLimit = () => {
+  ElMessage.error(`只允许上传${limit.value}个文件`);
+};
+// 上传成功,response:成功信息,file:图片信息,fileList:图片列表
+const onSuccess = (response: { errcode: string | number; errmsg: string; uri: string }, file: { name: string }) => {
+  if (response.errcode !== 0) {
+    ElMessage({ type: 'error', message: '删除成功' });
+    return;
+  }
+  let ponse = _.omit(response, ['errcode', 'errmsg']);
+  let arr: Ref<ListItem[]> = _.cloneDeep(list);
+  if (_.isArray(list.value)) {
+    arr.value.push({ ...ponse, name: file.name, url: `${import.meta.env.VITE_APP_HOST}${response.uri}` });
+  } else {
+    arr.value = [{ ...ponse, name: file.name, url: `${import.meta.env.VITE_APP_HOST}${response.uri}` }];
+  }
+  emit('change', { model: model.value, value: arr.value });
+};
+// 删除图片
+// file: { id: any; uri: string }, fileList: any
+const onRemove = () => {
+  // let arr: Ref<ListItem[]> = _.cloneDeep(list);
+  // let info = arr.value.filter((f) => f.id != file.id);
+  // emit('change', info);
+  return true;
+};
+
+// #endregion
+</script>
+
+<style lang="scss" scoped>
+#c-upload {
+  width: 100%;
+}
+</style>

+ 69 - 0
src/components/frame/wang-editor.vue

@@ -0,0 +1,69 @@
+<template>
+  <div id="editor">
+    <div style="border: 1px solid #ccc">
+      <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
+      <Editor style="height: 500px; overflow-y: hidden" v-model="valueHtml" :defaultConfig="editorConfig" :mode="mode" @onCreated="onCreated" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import '@wangeditor/editor/dist/css/style.css'; // 引入 css
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+import type { Ref } from 'vue';
+import { ref, toRefs, onBeforeUnmount, shallowRef, computed } from 'vue';
+interface EmitEvent {
+  (e: 'update:modelValue', params: string): void;
+}
+// #region 参数传递
+const props = defineProps({
+  modelValue: { type: String, default: () => '' },
+  mode: { type: String, default: () => 'default' },
+  url: { type: String, default: () => '' }
+});
+const { modelValue } = toRefs(props);
+const { mode } = toRefs(props);
+const { url } = toRefs(props);
+// #endregion
+
+const editorRef = shallowRef();
+const onCreated = (editor: string) => {
+  editorRef.value = Object.seal(editor); // 一定要用 Object.seal() ,否则会报错
+};
+
+const emit = defineEmits<EmitEvent>();
+const valueHtml = computed({
+  get() {
+    return modelValue.value;
+  },
+  set(value: string) {
+    emit('update:modelValue', value);
+  }
+});
+const customPicInsert = (result: { errcode: number; uri: string; name: string }, insertFn: any) => {
+  const { errcode, uri, name } = result;
+  const url = `${import.meta.env.VITE_APP_HOST}${uri}`;
+  if (errcode === 0) {
+    insertFn(url, name);
+  }
+};
+let editorConfig: Ref<object> = ref({
+  placeholder: '请输入内容...',
+  MENU_CONF: { uploadImage: { server: url, customInsert: customPicInsert } }
+});
+let toolbarConfig: Ref<object> = ref({
+  // excludeKeys: ['insertImage', 'insertVideo', 'uploadVideo', 'video'],
+});
+onBeforeUnmount(() => {
+  const editor = editorRef.value;
+  if (editor == null) return;
+  editor.destroy();
+});
+</script>
+
+<style src="@wangeditor/editor/dist/css/style.css"></style>
+<style scoped>
+.editor {
+  overflow-y: hidden;
+}
+</style>

File diff suppressed because it is too large
+ 0 - 7
src/components/icons/IconCommunity.vue


File diff suppressed because it is too large
+ 0 - 7
src/components/icons/IconDocumentation.vue


File diff suppressed because it is too large
+ 0 - 7
src/components/icons/IconEcosystem.vue


+ 0 - 7
src/components/icons/IconSupport.vue

@@ -1,7 +0,0 @@
-<template>
-  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
-    <path
-      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
-    />
-  </svg>
-</template>

+ 0 - 19
src/components/icons/IconTooling.vue

@@ -1,19 +0,0 @@
-<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
-<template>
-  <svg
-    xmlns="http://www.w3.org/2000/svg"
-    xmlns:xlink="http://www.w3.org/1999/xlink"
-    aria-hidden="true"
-    role="img"
-    class="iconify iconify--mdi"
-    width="24"
-    height="24"
-    preserveAspectRatio="xMidYMid meet"
-    viewBox="0 0 24 24"
-  >
-    <path
-      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
-      fill="currentColor"
-    ></path>
-  </svg>
-</template>

+ 15 - 0
src/components/index.ts

@@ -0,0 +1,15 @@
+import type { Component } from 'vue';
+import cButton from './frame/c-button.vue';
+import cDialog from './frame/c-dialog.vue';
+import cSearch from './frame/c-search.vue';
+import cForm from './frame/c-form.vue';
+import cTable from './frame/c-table.vue';
+import cUpload from './frame/c-upload.vue';
+import cPages from './frame/c-pages.vue';
+import cEditor from './frame/wang-editor.vue';
+
+const components: {
+  [propName: string]: Component;
+} = { cButton, cDialog, cSearch, cForm, cTable, cUpload, cPages, cEditor };
+
+export default components;

+ 70 - 0
src/layout/site.ts

@@ -0,0 +1,70 @@
+// 网站基本设置
+export const siteInfo = {
+  display: false,
+  zhTitle: '羽毛球比赛',
+};
+// 菜单设置
+export const menuInfo = {
+  info: {
+    display: false,
+    mode: 'horizontal',
+    backColor: '#242f42',
+    textColor: '#ffffff'
+  },
+  menuList: [
+    { icon: 'iconshouye', _id: 'admin_1', path: '/homeIndex', name: '系统首页' },
+    {
+      icon: 'iconshouye',
+      _id: 'admin_2',
+      name: '账号管理',
+      index: '2',
+      type: '0',
+      children: [
+        { icon: 'iconshouye', _id: 'admin_2_1', path: '/users/match', name: '比赛人员' },
+        { icon: 'iconshouye', _id: 'admin_2_2', path: '/users/team', name: '团队管理人员' }
+      ]
+    },
+    {
+      icon: 'iconshouye',
+      _id: 'admin_3',
+      name: '团队管理',
+      index: '3',
+      type: '0',
+      children: [
+        { icon: 'iconshouye', _id: 'admin_3_1', path: '/team/examine', name: '信息审批' },
+        { icon: 'iconshouye', _id: 'admin_3_2', path: '/team/team', name: '信息列表' }
+      ]
+    },
+    {
+      icon: 'iconshouye',
+      _id: 'admin_4',
+      name: '比赛管理',
+      index: '4',
+      type: '0',
+      children: [
+        { icon: 'iconshouye', _id: 'admin_3_1', path: '/match/match', name: '比赛管理' },
+        { icon: 'iconshouye', _id: 'admin_3_1', path: '/match/enroll', name: '团队报名' },
+        { icon: 'iconshouye', _id: 'admin_3_2', path: '/match/course', name: '赛程安排' },
+        { icon: 'iconshouye', _id: 'admin_3_2', path: '/match/ranking', name: '团队排名' }
+      ]
+    },
+    {
+      icon: 'iconshouye',
+      _id: 'admin_5',
+      name: '账号管理',
+      index: '5',
+      type: '0',
+      children: [
+        { icon: 'iconshouye', _id: 'admin_4_1', path: '/acccount/updatepd', name: '修改密码' }
+      ]
+    },
+    {
+      icon: 'iconshouye',
+      _id: 'admin_6',
+      name: '系统设置',
+      index: '6',
+      type: '0',
+      children: [{ icon: 'iconshouye', _id: 'admin_5_1', path: '/system/dict', name: '字典管理' }]
+    }
+  ]
+}

+ 29 - 4
src/main.ts

@@ -1,14 +1,39 @@
-import './assets/main.css'
-
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
-
 import App from './App.vue'
 import router from './router'
+// 样式
+import '@/assets/main.css'
+// 动画
+import 'animate.css'
+// element
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import locale from 'element-plus/lib/locale/lang/zh-cn'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
 
-const app = createApp(App)
+// 图标
+import '@/assets/icon/iconfont.css'
 
+// moment
+import moment from 'moment'
+// lodash
+// import _ from 'lodash';
+
+// 组件
+import frameComponents from '@/components/index'
+const app = createApp(App)
 app.use(createPinia())
 app.use(router)
+app.use(ElementPlus, { locale })
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+app.config.globalProperties.$moment = moment
+
+for (const componentItme in frameComponents) {
+  app.component(componentItme, frameComponents[componentItme])
+}
+app.config.globalProperties.$limit = parseInt(import.meta.env.VITE_APP_PAGE_SIZE) || 10
 
 app.mount('#app')

+ 99 - 10
src/router/index.ts

@@ -1,23 +1,112 @@
 import { createRouter, createWebHistory } from 'vue-router'
-import HomeView from '../views/HomeView.vue'
-
+import store from '@/stores/counter'
+import axios from 'axios'
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
     {
       path: '/',
-      name: 'home',
-      component: HomeView
+      redirect: '/login'
+    },
+    {
+      path: '/login',
+      name: 'login',
+      meta: { title: '羽毛球比赛-账号登录' },
+      component: () => import('@/views/login/index.vue')
     },
     {
-      path: '/about',
-      name: 'about',
-      // route level code-splitting
-      // this generates a separate chunk (About.[hash].js) for this route
-      // which is lazy-loaded when the route is visited.
-      component: () => import('../views/AboutView.vue')
+      path: '/homeIndex',
+      meta: { title: '羽毛球比赛-系统首页' },
+      component: () => import('@/components/admin-frame/home.vue'),
+      children: [
+        {
+          path: '/homeIndex',
+          meta: { title: '羽毛球比赛-系统首页' },
+          component: () => import('@/views/home/index.vue')
+        },
+        {
+          path: '/users/match',
+          meta: { title: '比赛人员管理' },
+          component: () => import('@/views/users/match/index.vue')
+        },
+        {
+          path: '/users/match/detail',
+          meta: { title: '信息修改' },
+          component: () => import('@/views/users/match/detail.vue')
+        },
+        {
+          path: '/users/team',
+          meta: { title: '团队管理人员管理' },
+          component: () => import('@/views/users/team/index.vue')
+        },
+        {
+          path: '/users/detail',
+          meta: { title: '团队管理人员信息管理' },
+          component: () => import('@/views/users/team/detail.vue')
+        },
+        {
+          path: '/team/examine',
+          meta: { title: '信息审批' },
+          component: () => import('@/views/team/examine/index.vue')
+        },
+        {
+          path: '/team/team',
+          meta: { title: '信息列表' },
+          component: () => import('@/views/team/examine/index.vue')
+        },
+        {
+          path: '/match/match',
+          meta: { title: '信息列表' },
+          component: () => import('@/views/match/match/index.vue')
+        },
+        {
+          path: '/match/enroll',
+          meta: { title: '信息列表' },
+          component: () => import('@/views/match/enroll/index.vue')
+        },
+        {
+          path: '/match/course',
+          meta: { title: '信息列表' },
+          component: () => import('@/views/match/course/index.vue')
+        },
+        {
+          path: '/acccount/updatepd',
+          meta: { title: '修改密码' },
+          component: () => import('@/views/updatepd/index.vue')
+        },
+        {
+          path: '/system/dict',
+          meta: { title: '字典管理' },
+          component: () => import('@/views/system/dict/index.vue')
+        },
+        {
+          path: '/system/dictData',
+          meta: { title: '字典数据管理' },
+          component: () => import('@/views/system/dictData/index.vue')
+        }
+      ]
     }
   ]
 })
+router.beforeEach(async (to, from, next) => {
+  document.title = `${to.meta.title} `
+  const token = localStorage.getItem('token')
+  if (to.name != 'login') {
+    if (token) {
+      const res = await axios.request({
+        method: 'get',
+        url: '/ball/v1/api/token/tokenView',
+        responseType: 'json',
+        headers: {
+          token: token
+        }
+      })
+      if (res.data.errcode == '0') {
+        store.commit('setUser', res.data.data, { root: true })
+      }
+      next()
+    } else next('/login')
+  } else next()
+})
 
 export default router

+ 9 - 10
src/stores/counter.ts

@@ -1,12 +1,11 @@
-import { ref, computed } from 'vue'
-import { defineStore } from 'pinia'
+import * as ustate from './user/state'
+import * as umutations from './user/mutations'
 
-export const useCounterStore = defineStore('counter', () => {
-  const count = ref(0)
-  const doubleCount = computed(() => count.value * 2)
-  function increment() {
-    count.value++
-  }
-
-  return { count, doubleCount, increment }
+import { createStore } from 'vuex'
+const store = createStore({
+  state: { ...ustate },
+  mutations: { ...umutations },
+  actions: {},
+  modules: {}
 })
+export default store

+ 52 - 0
src/stores/dict/dictData.ts

@@ -0,0 +1,52 @@
+import { ref, computed } from 'vue';
+import { defineStore } from 'pinia';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+import _ from 'lodash';
+
+import type { IQueryType, IQueryResult, IQueryParams } from '@/util/types.util';
+const axios = new AxiosWrapper();
+const api = {
+  url: `/ball/v1/api/dictData`
+};
+export const DictDataStore = defineStore('dictData', () => {
+  const count = ref(0);
+  const doubleCount = computed(() => count.value * 2);
+  function increment() {
+    count.value++;
+  }
+  const query = async ({ skip = 0, limit = undefined, ...info }: IQueryParams = {}): Promise<IQueryResult> => {
+    let cond: IQueryType = {};
+    if (skip) cond.skip = skip;
+    if (limit) cond.limit = limit;
+    cond = { ...cond, ...info };
+    const res = await axios.$get(`${api.url}`, cond);
+    return res;
+  };
+  const fetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/${payload}`);
+    return res;
+  };
+  const create = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}`, payload);
+    return res;
+  };
+  const update = async (payload: any): Promise<IQueryResult> => {
+    const id = _.get(payload, 'id', _.get(payload, '_id'));
+    const res = await axios.$post(`${api.url}/${id}`, payload);
+    return res;
+  };
+  const del = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${api.url}/${payload}`);
+    return res;
+  };
+  return {
+    count,
+    doubleCount,
+    increment,
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 52 - 0
src/stores/dict/dictType.ts

@@ -0,0 +1,52 @@
+import { ref, computed } from 'vue';
+import { defineStore } from 'pinia';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+import _ from 'lodash';
+
+import type { IQueryType, IQueryResult, IQueryParams } from '@/util/types.util';
+const axios = new AxiosWrapper();
+const api = {
+  url: `/ball/v1/api/dictType`
+}
+export const DictTypeStore = defineStore('dictType', () => {
+  const count = ref(0);
+  const doubleCount = computed(() => count.value * 2);
+  function increment() {
+    count.value++;
+  }
+  const query = async ({ skip = 0, limit = undefined, ...info }: IQueryParams = {}): Promise<IQueryResult> => {
+    let cond: IQueryType = {};
+    if (skip) cond.skip = skip;
+    if (limit) cond.limit = limit;
+    cond = { ...cond, ...info };
+    const res = await axios.$get(`${api.url}`, cond);
+    return res;
+  };
+  const fetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/${payload}`);
+    return res;
+  };
+  const create = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}`, payload);
+    return res;
+  };
+  const update = async (payload: any): Promise<IQueryResult> => {
+    const id = _.get(payload, 'id', _.get(payload, '_id'));
+    const res = await axios.$post(`${api.url}/${id}`, payload);
+    return res;
+  };
+  const del = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${api.url}/${payload}`);
+    return res;
+  };
+  return {
+    count,
+    doubleCount,
+    increment,
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 6 - 0
src/stores/user/mutations.ts

@@ -0,0 +1,6 @@
+export const setUser = (state: { user: any }, payload: any) => {
+  state.user = payload
+}
+export const setIsInAct = (state: any, payload: any) => {
+  state.isInAct = payload
+}

+ 1 - 0
src/stores/user/state.ts

@@ -0,0 +1 @@
+export const user = {}

+ 63 - 0
src/stores/users/admin.ts

@@ -0,0 +1,63 @@
+import { ref, computed } from 'vue';
+import { defineStore } from 'pinia';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+import _ from 'lodash';
+
+import type { IQueryType, IQueryResult, IQueryParams } from '@/util/types.util';
+const axios = new AxiosWrapper();
+const api = {
+  url: `/friend_school/v1/api/admin`
+};
+export const AdminStore = defineStore('admin', () => {
+  const count = ref(0);
+  const doubleCount = computed(() => count.value * 2);
+  function increment() {
+    count.value++;
+  }
+  const query = async ({ skip = 0, limit = undefined, ...info }: IQueryParams = {}): Promise<IQueryResult> => {
+    let cond: IQueryType = {};
+    if (skip) cond.skip = skip;
+    if (limit) cond.limit = limit;
+    cond = { ...cond, ...info };
+    const res = await axios.$get(`${api.url}`, cond);
+    return res;
+  };
+  const fetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/${payload}`);
+    return res;
+  };
+  const create = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}`, payload);
+    return res;
+  };
+  const update = async (payload: any): Promise<IQueryResult> => {
+    const id = _.get(payload, 'id', _.get(payload, '_id'));
+    const res = await axios.$post(`${api.url}/${id}`, payload);
+    return res;
+  };
+  const del = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${api.url}/${payload}`);
+    return res;
+  };
+  const login = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}/login`, payload);
+    return res;
+  };
+  //password
+  const rp = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}/rp`, payload);
+    return res;
+  };
+  return {
+    count,
+    doubleCount,
+    increment,
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    login,
+    rp
+  };
+});

+ 150 - 0
src/util/axios-wrapper.ts

@@ -0,0 +1,150 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import type { IOptionsType, IQueryType, IRequestResult } from './types.util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+// // 参数类型设置
+// type valueType = string | number | object | boolean | Array<any>;
+// type queryType = string | number | boolean;
+
+// export interface IQueryType {
+//   [props: string]: queryType;
+// }
+// export interface IOptionsType {
+//   [props: string]: valueType;
+// }
+
+// export interface IRequestResult {
+//   errcode: string | number;
+//   errmsg: string | number;
+//   details?: string;
+//   [props: string]: any;
+// }
+
+export class AxiosWrapper {
+  constructor({ baseUrl = import.meta.env.VITE_REQUEST_BASE, unwrap = true } = {}) {
+    this.baseUrl = baseUrl;
+    this.unwrap = unwrap;
+  }
+  baseUrl: string;
+  unwrap: boolean;
+
+  // 替换uri中的参数变量
+  static merge(uri: string, query: IQueryType) {
+    if (!uri.includes(':')) {
+      return uri;
+    }
+    const keys = [];
+    const regexp = /\/:([a-z0-9_]+)/gi;
+    let res;
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1]);
+    }
+    keys.forEach((key) => {
+      const val = _.get(query, key);
+      if (!isNullOrUndefined(val)) {
+        uri = uri.replace(`:${key}`, `${val}`);
+      }
+    });
+    return uri;
+  }
+
+  $get(uri: string, query?: IQueryType, options?: IOptionsType) {
+    return this.$request(uri, undefined, query, options);
+  }
+
+  $post(uri: string, data: object = {}, query?: IQueryType, options?: IOptionsType) {
+    return this.$request(uri, data, query, options);
+  }
+  $delete(uri: string, data: object = {}, query?: IQueryType, options: IOptionsType = {}) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options);
+  }
+  async $request(uri: string, data?: object, query?: IQueryType, options?: IOptionsType) {
+    if (query && _.isObject(query)) {
+      const keys = Object.keys(query);
+      for (const key of keys) {
+        const val = _.get(query, key);
+        if (val === '') {
+          delete query[key];
+        }
+      }
+    }
+    if (_.isObject(query) && _.isObject(options)) {
+      options = { ...options, params: query, method: 'get' };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    if (!options) options = {};
+    if (options.params) options.params = trimData(options.params, null, null);
+    const params = _.get(options, 'params');
+    const url = AxiosWrapper.merge(uri, params as IQueryType);
+    currentRequests += 1;
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+    try {
+      let returnData: any;
+      const axios = Axios.create({
+        baseURL: this.baseUrl
+      });
+      // if (util.token && util.token !== null) axios.defaults.headers.common.Authorization = util.token;
+      const token = localStorage.getItem('token');
+      const apiToken = localStorage.getItem('apiToken');
+      if (token) axios.defaults.headers.common['token'] = token;
+      if (apiToken) axios.defaults.headers.common['api-token'] = apiToken;
+      const res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options
+      });
+      const returnRes: IRequestResult = res.data;
+      const { errcode, errmsg, details } = returnRes;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return returnRes;
+      }
+      // unwrap data
+      if (this.unwrap) {
+        returnData = returnRes;
+      }
+      // 处理apiToken
+      const { apiToken: at, ...others } = returnData;
+      if (at) localStorage.setItem('apiToken', at);
+      return others;
+    } catch (err: any) {
+      let errmsg = '接口请求失败,请稍后重试';
+      if (err.response) {
+        const { status } = err.response;
+        if (status === 401) errmsg = '用户认证失败,请重新登录';
+        if (status === 403) errmsg = '当前用户不允许执行该操作';
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      );
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message };
+    } finally {
+      /* eslint-disable */
+      currentRequests -= 1;
+      if (currentRequests <= 0) {
+        currentRequests = 0;
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 29 - 0
src/util/types.util.ts

@@ -0,0 +1,29 @@
+// 参数类型设置
+type valueType = string | number | Object | boolean | Array<any>;
+type queryType = string | number | boolean;
+
+export interface IQueryType {
+  [props: string]: queryType;
+}
+export interface IOptionsType {
+  [props: string]: valueType;
+}
+
+export interface IRequestResult {
+  errcode: string | number;
+  errmsg: string | number;
+  details?: string;
+  [props: string]: any;
+}
+export interface IQueryResult {
+  errcode?: string | number;
+  errmsg?: string | number;
+  data: valueType;
+  total: number;
+}
+
+export interface IQueryParams {
+  skip?: number;
+  limit?: number;
+  [props: string]: any;
+}

+ 0 - 15
src/views/AboutView.vue

@@ -1,15 +0,0 @@
-<template>
-  <div class="about">
-    <h1>This is an about page</h1>
-  </div>
-</template>
-
-<style>
-@media (min-width: 1024px) {
-  .about {
-    min-height: 100vh;
-    display: flex;
-    align-items: center;
-  }
-}
-</style>

+ 0 - 9
src/views/HomeView.vue

@@ -1,9 +0,0 @@
-<script setup lang="ts">
-import TheWelcome from '../components/TheWelcome.vue'
-</script>
-
-<template>
-  <main>
-    <TheWelcome />
-  </main>
-</template>

+ 38 - 0
src/views/home/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,137 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight">
+        <el-col :span="24" class="one w_1200">
+          <el-col :span="24" class="one_1">羽毛球比赛</el-col>
+          <el-col :span="24" class="one_2">
+            <div class="login">
+              <el-col :span="24" class="login_1"> 管理登录 </el-col>
+              <el-col :span="24" class="login_2">
+                <el-col :span="24" class="form">
+                  <el-form ref="formRef" :model="form" :rules="rules" label-width="auto">
+                    <el-form-item label="" prop="account">
+                      <el-input v-model="form.account" placeholder="请输入登录账号">
+                        <template #prefix>
+                          <el-icon><User /></el-icon>
+                        </template>
+                      </el-input>
+                    </el-form-item>
+                    <el-form-item label="" prop="password">
+                      <el-input v-model="form.password" type="password" show-password placeholder="请输入登录密码">
+                        <template #prefix>
+                          <el-icon><Unlock /></el-icon>
+                        </template>
+                      </el-input>
+                    </el-form-item>
+                    <el-col :span="24" class="btn">
+                      <el-button type="primary" @click="toSave(formRef)">提交登录</el-button>
+                    </el-col>
+                  </el-form>
+                </el-col>
+              </el-col>
+            </div>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+
+import type { Ref } from 'vue';
+import { onMounted, ref, reactive } from 'vue';
+import type { FormInstance, FormRules } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+
+// 接口
+import { AdminStore } from '@/stores/users/admin';
+import type { IQueryResult } from '@/util/types.util';
+const adminAxios = AdminStore();
+
+// 路由
+const router = useRouter();
+
+// 表单
+const formRef = ref<FormInstance>();
+const form: Ref<any> = ref({});
+const rules = reactive<FormRules>({
+  account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入账号密码', trigger: 'blur' }]
+});
+
+// 请求
+onMounted(async () => {});
+
+// 提交登录
+const toSave = async (formEl: any) => {
+  if (!formEl) return;
+  await formEl.validate((valid: any) => {
+    if (valid) {
+      toLogin(form.value);
+    } else {
+      console.log('error submit!');
+    }
+  });
+};
+const toLogin = async (data: any) => {
+  let res: IQueryResult = await adminAxios.login(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: `登录成功`, type: 'success' });
+    localStorage.setItem('token', `${res.data}`);
+    router.push({ path: '/homeIndex' });
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+</script>
+<style scoped lang="scss">
+.main {
+  display: flex;
+  flex-direction: column;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  background: url('@/assets/bglogin.jpg');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+  .one {
+    .one_1 {
+      text-align: center;
+      color: #ffffff;
+      font-size: 30px;
+      margin: 5% 0 40px 0;
+    }
+    .one_2 {
+      text-align: center;
+      display: flex;
+      justify-content: center;
+      .login {
+        width: 500px;
+        height: 500px;
+        background-color: #ffffff;
+        border-radius: 5px;
+        padding: 10px;
+        .login_1 {
+          text-align: center;
+          margin: 30px 0;
+          font-size: 25px;
+          color: #409eff;
+          font-weight: 700;
+        }
+        .login_2 {
+          .type {
+            margin: 0 0 10px 0;
+          }
+        }
+      }
+    }
+  }
+}
+:deep(.el-input-group__append, .el-input-group__prepend) {
+  padding: 0;
+}
+</style>

+ 38 - 0
src/views/match/course/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/match/enroll/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/match/match/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/match/ranking/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 184 - 0
src/views/system/dict/index.vue

@@ -0,0 +1,184 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">
+          <cSearch :is_title="false" :is_search="true" :fields="fields" @search="toSearch"> </cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cButton @toAdd="toAdd()"> </cButton>
+        </el-col>
+        <el-col :span="24" class="thr">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel">
+            <template #type="{ item, row }">
+              <template v-if="item.model === 'type'">
+                <el-link size="small" type="primary" @click="toType(row)">{{ getProps(row, item.model) }}</el-link>
+              </template>
+            </template>
+          </cTable>
+        </el-col>
+      </el-col>
+    </el-row>
+    <cDialog :dialog="dialog" @toClose="toClose">
+      <template v-slot:info>
+        <el-col :span="24" class="dialog_one" v-if="dialog.type == '1'">
+          <cForm :span="24" :fields="formFields" :form="form" :rules="rules" @save="onSubmit">
+            <template #is_use>
+              <el-radio v-for="(i, index) in is_useList" :key="index" :label="i.value">{{ i.label }}</el-radio>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+<script lang="ts" setup>
+// 基础
+import _ from 'lodash';
+import type { FormRules } from 'element-plus';
+import type { Ref } from 'vue';
+import { ref, onMounted, getCurrentInstance, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+
+// 接口
+import { DictDataStore } from '@/stores/dict/dictData'; //
+import { DictTypeStore } from '@/stores/dict/dictType'; //
+import type { IQueryResult } from '@/util/types.util';
+const dictType = DictTypeStore();
+const dictData = DictDataStore();
+
+// 路由
+const router = useRouter();
+
+const { proxy } = getCurrentInstance() as any;
+
+// 加载中
+const loading = ref(false);
+
+// 列表数据
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit: number = proxy.$limit;
+let fields: Ref<any[]> = ref([
+  { label: '字典名称', model: 'title', isSearch: true },
+  { label: '字典类型', model: 'type', custom: true },
+  { label: '是否使用', model: 'is_use', format: (i: any) => getDict(i, 'is_use') },
+  { label: '备注', model: 'remark' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+// 弹框
+const dialog: Ref<{ type: string; show: boolean; title: string }> = ref({ type: '1', show: false, title: '信息管理' });
+let form: Ref<{}> = ref({});
+// 表单
+let formFields: Ref<any[]> = ref([
+  { label: '字典名称', model: 'title' },
+  { label: '字典类型', model: 'type' },
+  { label: '是否使用', model: 'is_use', type: 'radio' },
+  { label: '备注', model: 'remark', type: 'textarea' }
+]);
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '字典名称', trigger: 'blur' }],
+  type: [{ required: true, message: '字典类型', trigger: 'blur' }]
+});
+
+// 查询数据
+let searchForm: Ref<any> = ref({});
+
+// 字典表
+let is_useList: Ref<any> = ref([]);
+
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+// 查询
+const search = async (e: { skip: number; limit: number }) => {
+  const { skip, limit } = e;
+  const condition = _.cloneDeep(searchForm.value);
+  let info = { limit: limit, skip: skip, ...condition };
+  let res: IQueryResult = await dictType.query(info);
+  if (res.errcode == 0) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const getDict = (value, model) => {
+  console.log(value);
+  if (model == 'is_use') {
+    if (value) {
+      let data = is_useList.value.find((i) => i.value == value);
+      if (data) return data.label;
+      else return '暂无';
+    }
+  }
+};
+const getProps = (data, prop) => {
+  return _.get(data, prop);
+};
+// 新增
+const toAdd = () => {
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 修改
+const toEdit = async (data) => {
+  let res: IQueryResult = await dictType.fetch(data._id);
+  if (res.errcode == 0) {
+    form.value = res.data as {};
+    dialog.value = { title: '信息管理', show: true, type: '1' };
+  }
+};
+// 提交
+const onSubmit = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await dictType.update(data);
+  else res = await dictType.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toClose();
+  }
+};
+// 删除
+const toDel = async (data) => {
+  let res: IQueryResult = await dictType.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+// 弹框关闭
+const toClose = () => {
+  form.value = {};
+  searchForm.value = {};
+  dialog.value = { title: '信息管理', show: false, type: '1' };
+  search({ skip, limit });
+};
+// 字典类型跳转
+const toType = (data) => {
+  router.push({ path: '/system/dictData', query: { id: data._id, type: data.type } });
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult = await dictData.query({ type: 'common_use' });
+  if (res.errcode == 0) is_useList.value = res.data;
+};
+</script>
+<style lang="scss" scoped>
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 181 - 0
src/views/system/dictData/index.vue

@@ -0,0 +1,181 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">
+          <cSearch :is_title="false" :is_search="true" :fields="fields" @search="toSearch"> </cSearch>
+        </el-col>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd()">
+          <template v-slot:custom>
+            <el-button type="primary" @click="toBack">返回</el-button>
+          </template>
+        </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel"> </cTable>
+      </el-col>
+    </el-row>
+    <cDialog :dialog="dialog" @toClose="toClose">
+      <template v-slot:info>
+        <el-col :span="24" class="dialog_one" v-if="dialog.type == '1'">
+          <cForm :span="24" :fields="formFields" :form="form" :rules="rules" @save="toSave">
+            <template #is_use>
+              <el-radio v-for="i in is_useList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import _ from 'lodash';
+import type { FormRules } from 'element-plus';
+import type { Ref } from 'vue';
+import { ref, onMounted, getCurrentInstance, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRoute, useRouter } from 'vue-router';
+
+// 接口
+import { DictDataStore } from '@/stores/dict/dictData';
+import type { IQueryResult } from '@/util/types.util';
+const dictData = DictDataStore();
+
+// 路由
+const route = useRoute();
+const router = useRouter();
+
+const { proxy } = getCurrentInstance() as any;
+
+// 加载中
+const loading: Ref<any> = ref(false);
+
+// 列表数据
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit: number = proxy.$limit;
+let fields: Ref<any[]> = ref([
+  { label: '类型', model: 'type' },
+  { label: '标签', model: 'label', isSearch: true },
+  { label: '键值', model: 'value', isSearch: true },
+  { label: '排序', model: 'sort', type: 'number' },
+  { label: '是否使用', model: 'is_use', format: (i: any) => getDict(i, 'is_use') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+// 弹框
+const dialog: Ref<{ type: string; show: boolean; title: string }> = ref({ type: '1', show: false, title: '信息管理' });
+let form: Ref<{}> = ref({});
+// 表单
+let formFields: Ref<any[]> = ref([
+  { label: '类型', model: 'type', options: { disabled: true } },
+  { label: '标签', model: 'label' },
+  { label: '键值', model: 'value' },
+  { label: '排序', model: 'sort', type: 'number' },
+  { label: '是否使用', model: 'is_use', type: 'radio' }
+]);
+const rules = reactive<FormRules>({
+  label: [{ required: true, message: '标签', trigger: 'blur' }],
+  value: [{ required: true, message: '键值', trigger: 'blur' }]
+});
+
+// 查询数据
+let searchForm: Ref<any> = ref({});
+
+// 字典表
+let is_useList: Ref<any> = ref([]);
+
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const { skip, limit } = e;
+  const condition = _.cloneDeep(searchForm.value);
+  let type = route.query.type;
+  let info = { limit: limit, skip: skip, ...condition, type: type };
+  let res: IQueryResult = await dictData.query(info);
+  if (res.errcode == 0) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const getDict = (value, model) => {
+  if (model == 'is_use') {
+    if (value) {
+      let data = is_useList.value.find((i) => i.value == value);
+      if (data) return data.label;
+      else return '暂无';
+    }
+  }
+};
+// 新增
+const toAdd = () => {
+  let type = route.query.type;
+  form.value = { type: type };
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 修改
+const toEdit = async (data) => {
+  form.value = data;
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 提交保存
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await dictData.update(data);
+  else res = await dictData.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toClose();
+  }
+};
+// 删除
+const toDel = async (data) => {
+  let res: IQueryResult = await dictData.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+// 弹框关闭
+const toClose = () => {
+  form.value = {};
+  dialog.value = { title: '信息管理', show: false, type: '1' };
+  search({ skip, limit });
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 是否使用
+  res = await dictData.query({ type: 'common_use' });
+  if (res.errcode == '0') is_useList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  router.push({ path: '/system/dict' });
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 38 - 0
src/views/team/examine/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/team/team/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 70 - 0
src/views/updatepd/index.vue

@@ -0,0 +1,70 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">
+          <cForm :span="12" :fields="fields" :form="form" :rules="rules" @save="toSave"> </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import store from '@/stores/counter';
+import type { Ref } from 'vue';
+import { onMounted, ref } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+// 接口
+import { AdminStore } from '@/stores/users/admin'; // 管理员
+import type { IQueryResult } from '@/util/types.util';
+
+const adminAxios = AdminStore();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 路由
+const router = useRouter();
+// 表单
+const form: Ref<any> = ref({});
+const fields: Ref<any> = ref([
+  { label: '新密码', model: 'password', type: 'password' },
+  { label: '确认新密码', model: 'ispassword', type: 'password' }
+]);
+const rules: Ref<any> = ref({
+  password: [{ required: true, message: '请输入新密码' }],
+  ispassword: [
+    { required: true, message: '请输入确认新密码' },
+    {
+      trigger: 'blur',
+      validator: (rule, value, callback) => {
+        if (form.value.password !== value) {
+          callback(new Error('两次输入的密码不一致'));
+        } else {
+          callback();
+        }
+      }
+    }
+  ]
+});
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  loading.value = false;
+});
+// 提交保存
+const toSave = async (data) => {
+  let user = store.state.user;
+  let res: IQueryResult = await adminAxios.rp({ _id: user._id, password: data.password });
+  if (res.errcode == '0') {
+    ElMessage({ type: 'success', message: '修改密码成功' });
+    // 退出登录
+    localStorage.removeItem('token');
+    router.push('/login');
+  } else {
+    ElMessage({ type: 'error', message: `${res.errmsg}` });
+  }
+};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/users/match/detail.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/users/match/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/users/team/detail.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 38 - 0
src/views/users/team/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">系统首页</el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+// reactive,
+import { onMounted, ref, getCurrentInstance } from 'vue';
+// 接口
+//import { TestStore } from '@common/src/stores/test';
+//import type { IQueryResult } from '@/util/types.util';
+//const testAxios = TestStore();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 分页数据
+//  const skip = 0;
+//  const limit = proxy.limit;;
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  //  await search({ skip, limit });
+  loading.value = false;
+});
+//const search = async (e: { skip: number; limit: number }) => {
+//  const info = { skip: e.skip, limit: e.limit, ...searchInfo.value  };
+//  const res: IQueryResult = await testAxios.query(info);
+//  console.log(res);
+//};
+</script>
+<style scoped lang="scss"></style>

+ 29 - 9
vite.config.ts

@@ -1,14 +1,34 @@
 import { fileURLToPath, URL } from 'node:url'
-
-import { defineConfig } from 'vite'
+import { defineConfig, loadEnv } from 'vite'
 import vue from '@vitejs/plugin-vue'
-
-// https://vitejs.dev/config/
-export default defineConfig({
-  plugins: [vue()],
-  resolve: {
-    alias: {
-      '@': fileURLToPath(new URL('./src', import.meta.url))
+export default defineConfig(({ mode }) => {
+  const env = loadEnv(mode, __dirname)
+  return {
+    // 静态路径
+    base: env.VITE_BASE_URL,
+    // 打包名称
+    build: {
+      outDir: env.VITE_OUT_DIR
+    },
+    plugins: [vue()],
+    server: {
+      port: 8001,
+      proxy: {
+        '/files': {
+          target: 'https://broadcast.waityou24.cn'
+        },
+        '/ball/v1/api': {
+          target: 'http://192.168.1.113:13005',
+          changeOrigin: true,
+          ws: false
+        }
+      },
+      fs: { strict: false }
+    },
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url))
+      }
     }
   }
 })