zs 1 năm trước cách đây
commit
2294915b73
78 tập tin đã thay đổi với 14619 bổ sung0 xóa
  1. 7 0
      .env.development
  2. 7 0
      .env.production
  3. 28 0
      .eslintrc.cjs
  4. 29 0
      .gitignore
  5. 8 0
      .prettierrc.json
  6. 3 0
      .vscode/extensions.json
  7. 1 0
      README.md
  8. 5 0
      env.d.ts
  9. 16 0
      index.html
  10. 9124 0
      package-lock.json
  11. 48 0
      package.json
  12. BIN
      public/favicon.ico
  13. 26 0
      src/App.vue
  14. 73 0
      src/assets/base.css
  15. BIN
      src/assets/bglogin.jpg
  16. 95 0
      src/assets/icon/iconfont.css
  17. 1 0
      src/assets/icon/iconfont.js
  18. 149 0
      src/assets/icon/iconfont.json
  19. BIN
      src/assets/icon/iconfont.ttf
  20. BIN
      src/assets/icon/iconfont.woff
  21. BIN
      src/assets/icon/iconfont.woff2
  22. 1 0
      src/assets/logo.svg
  23. 19 0
      src/assets/main.css
  24. 61 0
      src/components/admin-frame/home.vue
  25. 75 0
      src/components/admin-frame/parts/Header.vue
  26. 108 0
      src/components/admin-frame/parts/Sidebar.vue
  27. 23 0
      src/components/admin-frame/parts/breadcrumb.vue
  28. 26 0
      src/components/frame/c-button.vue
  29. 29 0
      src/components/frame/c-dialog.vue
  30. 236 0
      src/components/frame/c-form.vue
  31. 53 0
      src/components/frame/c-pages.vue
  32. 169 0
      src/components/frame/c-search.vue
  33. 215 0
      src/components/frame/c-table.vue
  34. 108 0
      src/components/frame/c-upload.vue
  35. 69 0
      src/components/frame/wang-editor.vue
  36. 15 0
      src/components/index.ts
  37. 67 0
      src/layout/site.ts
  38. 39 0
      src/main.ts
  39. 147 0
      src/router/index.ts
  40. 52 0
      src/stores/config.ts
  41. 11 0
      src/stores/counter.ts
  42. 52 0
      src/stores/dict/dictData.ts
  43. 52 0
      src/stores/dict/dictType.ts
  44. 52 0
      src/stores/match/application.ts
  45. 57 0
      src/stores/match/course.ts
  46. 52 0
      src/stores/match/match.ts
  47. 56 0
      src/stores/team/team.ts
  48. 6 0
      src/stores/user/mutations.ts
  49. 1 0
      src/stores/user/state.ts
  50. 63 0
      src/stores/users/admin.ts
  51. 63 0
      src/stores/users/user.ts
  52. 150 0
      src/util/axios-wrapper.ts
  53. 29 0
      src/util/types.util.ts
  54. 38 0
      src/views/home/index.vue
  55. 134 0
      src/views/login/index.vue
  56. 125 0
      src/views/match/course/detail.vue
  57. 229 0
      src/views/match/course/index.vue
  58. 92 0
      src/views/match/detail.vue
  59. 109 0
      src/views/match/enroll/detail.vue
  60. 131 0
      src/views/match/enroll/index.vue
  61. 144 0
      src/views/match/index.vue
  62. 49 0
      src/views/match/ranking/index.vue
  63. 93 0
      src/views/system/config/index.vue
  64. 175 0
      src/views/system/dict/index.vue
  65. 172 0
      src/views/system/dictData/index.vue
  66. 128 0
      src/views/team/examine/detail.vue
  67. 157 0
      src/views/team/examine/index.vue
  68. 128 0
      src/views/team/team/detail.vue
  69. 121 0
      src/views/team/team/index.vue
  70. 69 0
      src/views/updatepd/index.vue
  71. 154 0
      src/views/users/match/detail.vue
  72. 163 0
      src/views/users/match/index.vue
  73. 156 0
      src/views/users/team/detail.vue
  74. 162 0
      src/views/users/team/index.vue
  75. 20 0
      tsconfig.app.json
  76. 44 0
      tsconfig.json
  77. 16 0
      tsconfig.node.json
  78. 34 0
      vite.config.ts

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+VITE_BASE_URL = "/admin"
+VITE_OUT_DIR = "admin"
+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 = "/admin"
+VITE_OUT_DIR = "admin"
+VITE_REQUEST_BASE = ''
+VITE_APP_HOST="https://broadcast.waityou24.cn"
+VITE_APP_PAGE_SIZE=15
+VITE_APP_ROUTER="admin"
+VITE_APP_ENV = 'production'

+ 28 - 0
.eslintrc.cjs

@@ -0,0 +1,28 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution');
+
+module.exports = {
+  root: true,
+  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier'],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  },
+  rules: {
+    'vue/multi-word-component-names': 0,
+    'max-len': [
+      'warn',
+      {
+        code: 10000
+      }
+    ],
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 160
+      }
+    ]
+  }
+};

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+*.history

+ 8 - 0
.prettierrc.json

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

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# ball_admin

+ 5 - 0
env.d.ts

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

+ 16 - 0
index.html

@@ -0,0 +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,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>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 9124 - 0
package-lock.json


+ 48 - 0
package.json

@@ -0,0 +1,48 @@
+{
+  "name": "ball_admin",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite --host",
+    "build": "run-p type-check build-only",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "2.1.0",
+    "@wangeditor/editor": "5.1.23",
+    "@wangeditor/editor-for-vue": "5.1.12",
+    "animate.css": "4.1.1",
+    "axios": "1.4.0",
+    "element-plus": "2.3.6",
+    "lodash": "4.17.21",
+    "moment": "2.29.4",
+    "naf-core": "0.1.2",
+    "pinia": "2.0.36",
+    "vue": "3.3.2",
+    "vue-router": "4.2.0",
+    "vuex": "4.1.0"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "1.2.0",
+    "@tsconfig/node18": "2.0.1",
+    "@types/node": "18.16.8",
+    "@vitejs/plugin-vue": "4.2.3",
+    "@vue/eslint-config-prettier": "7.1.0",
+    "@vue/eslint-config-typescript": "11.0.3",
+    "@vue/tsconfig": "0.4.0",
+    "eslint": "8.39.0",
+    "eslint-plugin-vue": "9.11.0",
+    "npm-run-all": "4.1.5",
+    "prettier": "2.8.8",
+    "sass": "1.62.1",
+    "sass-loader": "13.3.1",
+    "typescript": "~5.0.4",
+    "vite": "4.3.5",
+    "vue-tsc": "1.6.4"
+  }
+  
+}

BIN
public/favicon.ico


+ 26 - 0
src/App.vue

@@ -0,0 +1,26 @@
+<script setup lang="ts"></script>
+
+<template>
+  <RouterView />
+</template>
+
+<style>
+body {
+  margin: 0;
+}
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
+}
+
+p {
+  margin: 0;
+  padding: 0;
+}
+
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 73 - 0
src/assets/base.css

@@ -0,0 +1,73 @@
+/* color palette from <https://github.com/vuejs/theme> */
+:root {
+  --vt-c-white: #ffffff;
+  --vt-c-white-soft: #f8f8f8;
+  --vt-c-white-mute: #f2f2f2;
+
+  --vt-c-black: #181818;
+  --vt-c-black-soft: #222222;
+  --vt-c-black-mute: #282828;
+
+  --vt-c-indigo: #2c3e50;
+
+  --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
+  --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
+  --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
+  --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
+
+  --vt-c-text-light-1: var(--vt-c-indigo);
+  --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
+  --vt-c-text-dark-1: var(--vt-c-white);
+  --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
+}
+
+/* semantic color variables for this project */
+:root {
+  --color-background: var(--vt-c-white);
+  --color-background-soft: var(--vt-c-white-soft);
+  --color-background-mute: var(--vt-c-white-mute);
+
+  --color-border: var(--vt-c-divider-light-2);
+  --color-border-hover: var(--vt-c-divider-light-1);
+
+  --color-heading: var(--vt-c-text-light-1);
+  --color-text: var(--vt-c-text-light-1);
+
+  --section-gap: 160px;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --color-background: var(--vt-c-black);
+    --color-background-soft: var(--vt-c-black-soft);
+    --color-background-mute: var(--vt-c-black-mute);
+
+    --color-border: var(--vt-c-divider-dark-2);
+    --color-border-hover: var(--vt-c-divider-dark-1);
+
+    --color-heading: var(--vt-c-text-dark-1);
+    --color-text: var(--vt-c-text-dark-2);
+  }
+}
+
+*,
+*::before,
+*::after {
+  box-sizing: border-box;
+  margin: 0;
+  font-weight: normal;
+}
+
+body {
+  min-height: 100vh;
+  color: var(--color-text);
+  background: var(--color-background);
+  transition: color 0.5s, background-color 0.5s;
+  line-height: 1.6;
+  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+    Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+  font-size: 15px;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}

BIN
src/assets/bglogin.jpg


+ 95 - 0
src/assets/icon/iconfont.css

@@ -0,0 +1,95 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 4079354 */
+  src: url('iconfont.woff2?t=1685509924120') format('woff2'),
+       url('iconfont.woff?t=1685509924120') format('woff'),
+       url('iconfont.ttf?t=1685509924120') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-dengji:before {
+  content: "\e610";
+}
+
+.icon-shenhezhong:before {
+  content: "\e682";
+}
+
+.icon-dkw_shenheweitongguo:before {
+  content: "\e604";
+}
+
+.icon-shenhetongguo:before {
+  content: "\e668";
+}
+
+.icon-hearts-fill:before {
+  content: "\e702";
+}
+
+.icon-xiaoyouhui:before {
+  content: "\e601";
+}
+
+.icon-youjiantou-copy:before {
+  content: "\e654";
+}
+
+.icon-6ruxueshijian:before {
+  content: "\e88a";
+}
+
+.icon-zhuanyezhuanyeke:before {
+  content: "\e6a1";
+}
+
+.icon-commpany:before {
+  content: "\e612";
+}
+
+.icon-zhiwuguanli:before {
+  content: "\e60f";
+}
+
+.icon-guanzhu:before {
+  content: "\e611";
+}
+
+.icon-guanzhu1:before {
+  content: "\e600";
+}
+
+.icon-edu-line:before {
+  content: "\e63a";
+}
+
+.icon-gender:before {
+  content: "\e63e";
+}
+
+.icon-xingming:before {
+  content: "\e640";
+}
+
+.icon-jibenxinxi:before {
+  content: "\e67c";
+}
+
+.icon-iocn_be_concern:before {
+  content: "\e607";
+}
+
+.icon-tupianshangchuan:before {
+  content: "\e639";
+}
+
+.icon-wodeguanzhu:before {
+  content: "\e8bc";
+}
+

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
src/assets/icon/iconfont.js


+ 149 - 0
src/assets/icon/iconfont.json

@@ -0,0 +1,149 @@
+{
+  "id": "4079354",
+  "name": "校友信息登记",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "1327399",
+      "name": "登记",
+      "font_class": "dengji",
+      "unicode": "e610",
+      "unicode_decimal": 58896
+    },
+    {
+      "icon_id": "1480896",
+      "name": "审核中",
+      "font_class": "shenhezhong",
+      "unicode": "e682",
+      "unicode_decimal": 59010
+    },
+    {
+      "icon_id": "2078809",
+      "name": "dkw_审核未通过",
+      "font_class": "dkw_shenheweitongguo",
+      "unicode": "e604",
+      "unicode_decimal": 58884
+    },
+    {
+      "icon_id": "10087856",
+      "name": "审核通过",
+      "font_class": "shenhetongguo",
+      "unicode": "e668",
+      "unicode_decimal": 58984
+    },
+    {
+      "icon_id": "35094512",
+      "name": "关心",
+      "font_class": "hearts-fill",
+      "unicode": "e702",
+      "unicode_decimal": 59138
+    },
+    {
+      "icon_id": "2726796",
+      "name": "校友会",
+      "font_class": "xiaoyouhui",
+      "unicode": "e601",
+      "unicode_decimal": 58881
+    },
+    {
+      "icon_id": "10515596",
+      "name": "右箭头",
+      "font_class": "youjiantou-copy",
+      "unicode": "e654",
+      "unicode_decimal": 58964
+    },
+    {
+      "icon_id": "518145",
+      "name": "6 入学时间",
+      "font_class": "6ruxueshijian",
+      "unicode": "e88a",
+      "unicode_decimal": 59530
+    },
+    {
+      "icon_id": "16365912",
+      "name": "专业 专业课",
+      "font_class": "zhuanyezhuanyeke",
+      "unicode": "e6a1",
+      "unicode_decimal": 59041
+    },
+    {
+      "icon_id": "376346",
+      "name": "工作单位",
+      "font_class": "commpany",
+      "unicode": "e612",
+      "unicode_decimal": 58898
+    },
+    {
+      "icon_id": "11672365",
+      "name": "职务管理",
+      "font_class": "zhiwuguanli",
+      "unicode": "e60f",
+      "unicode_decimal": 58895
+    },
+    {
+      "icon_id": "8712978",
+      "name": "关注",
+      "font_class": "guanzhu",
+      "unicode": "e611",
+      "unicode_decimal": 58897
+    },
+    {
+      "icon_id": "9714399",
+      "name": "关注",
+      "font_class": "guanzhu1",
+      "unicode": "e600",
+      "unicode_decimal": 58880
+    },
+    {
+      "icon_id": "6119296",
+      "name": "学历",
+      "font_class": "edu-line",
+      "unicode": "e63a",
+      "unicode_decimal": 58938
+    },
+    {
+      "icon_id": "6183156",
+      "name": "性别",
+      "font_class": "gender",
+      "unicode": "e63e",
+      "unicode_decimal": 58942
+    },
+    {
+      "icon_id": "20764666",
+      "name": "姓名",
+      "font_class": "xingming",
+      "unicode": "e640",
+      "unicode_decimal": 58944
+    },
+    {
+      "icon_id": "6856906",
+      "name": "基本信息",
+      "font_class": "jibenxinxi",
+      "unicode": "e67c",
+      "unicode_decimal": 59004
+    },
+    {
+      "icon_id": "1048854",
+      "name": "被关注",
+      "font_class": "iocn_be_concern",
+      "unicode": "e607",
+      "unicode_decimal": 58887
+    },
+    {
+      "icon_id": "2506206",
+      "name": "图片上传",
+      "font_class": "tupianshangchuan",
+      "unicode": "e639",
+      "unicode_decimal": 58937
+    },
+    {
+      "icon_id": "11372718",
+      "name": "我的关注",
+      "font_class": "wodeguanzhu",
+      "unicode": "e8bc",
+      "unicode_decimal": 59580
+    }
+  ]
+}

BIN
src/assets/icon/iconfont.ttf


BIN
src/assets/icon/iconfont.woff


BIN
src/assets/icon/iconfont.woff2


+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 19 - 0
src/assets/main.css

@@ -0,0 +1,19 @@
+body {
+  margin: 0;
+}
+
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
+}
+
+p {
+  margin: 0;
+  padding: 0;
+}
+
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}

+ 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>

+ 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;

+ 67 - 0
src/layout/site.ts

@@ -0,0 +1,67 @@
+// 网站基本设置
+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',
+      path: '/match/index'
+    },
+    {
+      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_2', path: '/system/config', name: '基础设置' },
+        { icon: 'iconshouye', _id: 'admin_5_1', path: '/system/dict', name: '字典管理' }
+      ]
+    }
+  ]
+};

+ 39 - 0
src/main.ts

@@ -0,0 +1,39 @@
+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';
+
+// 图标
+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');

+ 147 - 0
src/router/index.ts

@@ -0,0 +1,147 @@
+import { createRouter, createWebHistory } from 'vue-router';
+import store from '@/stores/counter';
+import axios from 'axios';
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      redirect: '/login'
+    },
+    {
+      path: '/login',
+      name: 'login',
+      meta: { title: '羽毛球比赛-账号登录' },
+      component: () => import('@/views/login/index.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/team/detail',
+          meta: { title: '团队管理人员信息管理' },
+          component: () => import('@/views/users/team/detail.vue')
+        },
+        {
+          path: '/team/examine',
+          meta: { title: '团队信息审批' },
+          component: () => import('@/views/team/examine/index.vue')
+        },
+        {
+          path: '/team/examine/detail',
+          meta: { title: '团队审批信息管理' },
+          component: () => import('@/views/team/examine/detail.vue')
+        },
+        {
+          path: '/team/team',
+          meta: { title: '团队信息列表' },
+          component: () => import('@/views/team/team/index.vue')
+        },
+        {
+          path: '/team/team/detail',
+          meta: { title: '团队信息查看' },
+          component: () => import('@/views/team/team/detail.vue')
+        },
+        {
+          path: '/match/index',
+          meta: { title: '比赛管理' },
+          component: () => import('@/views/match/index.vue')
+        },
+        {
+          path: '/match/detail',
+          meta: { title: '信息管理' },
+          component: () => import('@/views/match/detail.vue')
+        },
+        {
+          path: '/match/enroll',
+          meta: { title: '团队报名情况' },
+          component: () => import('@/views/match/enroll/index.vue')
+        },
+        {
+          path: '/match/enroll/detail',
+          meta: { title: '团队报名查看' },
+          component: () => import('@/views/match/enroll/detail.vue')
+        },
+        {
+          path: '/match/course',
+          meta: { title: '赛程安排' },
+          component: () => import('@/views/match/course/index.vue')
+        },
+        {
+          path: '/match/course/detail',
+          meta: { title: '赛程安排信息管理' },
+          component: () => import('@/views/match/course/detail.vue')
+        },
+        {
+          path: '/match/ranking',
+          meta: { title: '团队排名' },
+          component: () => import('@/views/match/ranking/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')
+        },
+        {
+          path: '/system/config',
+          meta: { title: '基础设置' },
+          component: () => import('@/views/system/config/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;

+ 52 - 0
src/stores/config.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/config`
+};
+export const ConfigStore = defineStore('config', () => {
+  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
+  };
+});

+ 11 - 0
src/stores/counter.ts

@@ -0,0 +1,11 @@
+import * as ustate from './user/state'
+import * as umutations from './user/mutations'
+
+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
+  };
+});

+ 52 - 0
src/stores/match/application.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/application`
+};
+export const ApplicationStore = defineStore('application', () => {
+  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
+  };
+});

+ 57 - 0
src/stores/match/course.ts

@@ -0,0 +1,57 @@
+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/course`
+};
+export const CourseStore = defineStore('course', () => {
+  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 ranking = async ({ ...info }: IQueryParams = {}): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/ranking`, info);
+    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,
+    ranking,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 52 - 0
src/stores/match/match.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/match`
+};
+export const MatchStore = defineStore('match', () => {
+  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
+  };
+});

+ 56 - 0
src/stores/team/team.ts

@@ -0,0 +1,56 @@
+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/team`
+};
+export const TeamStore = defineStore('team', () => {
+  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: `/ball/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
+  };
+});

+ 63 - 0
src/stores/users/user.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: `/ball/v1/api/user`
+};
+export const UserStore = defineStore('user', () => {
+  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;
+}

+ 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>

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

@@ -0,0 +1,134 @@
+<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">{{ siteInfo.zhTitle }}</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 { siteInfo } from '@/layout/site';
+
+// 接口
+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>

+ 125 - 0
src/views/match/course/detail.vue

@@ -0,0 +1,125 @@
+<template>
+  <div id="detail">
+    <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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="rules" @save="toSave" label-width="auto">
+            <template #red_team_id>
+              <el-option v-for="i in applicationList" :key="i.team_id" :label="i.team_name" :value="i.team_id"></el-option>
+            </template>
+            <template #blue_team_id>
+              <el-option v-for="i in applicationList" :key="i.team_id" :label="i.team_name" :value="i.team_id"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
+import type { FormRules } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import { useRoute } from 'vue-router';
+// 接口
+import { MatchStore } from '@/stores/match/match';
+import { CourseStore } from '@/stores/match/course';
+import { ApplicationStore } from '@/stores/match/application';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const courseAxios = CourseStore();
+const applicationAxios = ApplicationStore();
+const matchAxios = MatchStore();
+const dictAxios = DictDataStore();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let match_id: Ref<any> = ref(route.query.match_id);
+const rules = reactive<FormRules>({
+  red_team_id: [{ required: true, message: '红方团队名称', trigger: 'blur' }],
+  blue_team_id: [{ required: true, message: '蓝方团队名称', trigger: 'blur' }],
+  match_time: [{ required: true, message: '比赛时间', trigger: 'blur' }]
+});
+let fields: Ref<any[]> = ref([
+  { label: '比赛名称', model: 'match_name', options: { disabled: true } },
+  { label: '红方团队名称', model: 'red_team_id', type: 'select' },
+  { label: '蓝方团队名称', model: 'blue_team_id', type: 'select' },
+  { label: '比赛时间', model: 'match_time', type: 'datetime' }
+]);
+// 字典表
+const statusList: Ref<any> = ref([]);
+const applicationList: Ref<any> = ref([]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    const list = [
+      { label: '比赛名称', model: 'match_name', options: { disabled: true } },
+      { label: '红方团队名称', model: 'red_team_id', type: 'select' },
+      { label: '蓝方团队名称', model: 'blue_team_id', type: 'select' },
+      { label: '比赛时间', model: 'match_time', type: 'datetime' }
+    ];
+    fields.value = list;
+    let res: IQueryResult = await courseAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      form.value = info;
+    }
+  } else {
+    let res: any = await matchAxios.fetch(match_id.value);
+    if (res.errcode == '0') {
+      form.value.match_id = res.data._id;
+      form.value.match_name = res.data.name;
+    }
+  }
+};
+// 保存
+const toSave = async (data) => {
+  const red = applicationList.value.find((i) => i.team_id == data.red_team_id);
+  if (red) {
+    data.red_team_name = red.team_name;
+    data.red_person = red.user_id;
+  }
+  const blue = applicationList.value.find((i) => i.team_id == data.blue_team_id);
+  if (blue) {
+    data.blue_team_name = blue.team_name;
+    data.blue_person = blue.user_id;
+  }
+  let res: IQueryResult;
+  if (data._id) res = await courseAxios.update(data);
+  else res = await courseAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 状态
+  res = await dictAxios.query({ type: 'course_status' });
+  if (res.errcode == '0') statusList.value = res.data;
+  // 报名情况
+  res = await applicationAxios.query({ match_id: match_id.value, status: '1' });
+  if (res.errcode == '0') applicationList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,229 @@
+<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">
+            <template #status>
+              <el-option v-for="(i, index) in statusList" :key="index" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cSearch>
+        </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" @status="toStatus" @edit="toEdit" @del="toDel"> </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="{}" @save="toSave" @dataChange="dataChange" label-width="auto">
+            <template #winner>
+              <el-option v-for="item in winnerList" :key="item._id" :label="item.name" :value="item._id">
+                <span style="float: left">{{ item.name }}</span>
+                <span style="float: right; color: #8492a6; font-size: 13px">{{ item.color }}</span>
+              </el-option>
+            </template>
+            <template #status>
+              <el-option v-for="i in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter, useRoute } from 'vue-router';
+// 接口
+import { CourseStore } from '@/stores/match/course';
+import { TeamStore } from '@/stores/team/team';
+import { ApplicationStore } from '@/stores/match/application';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const courseAxios = CourseStore();
+const dictAxios = DictDataStore();
+const teamAxios = TeamStore();
+const applicationAxios = ApplicationStore();
+const { proxy } = getCurrentInstance() as any;
+// 路由
+const router = useRouter();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+let id: Ref<any> = ref(route.query.id);
+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: 'red_team_name', isSearch: true },
+  { label: '红方分数', model: 'red_score' },
+  { label: '蓝方团队名称', model: 'blue_team_name' },
+  { label: '蓝方分数', model: 'blue_score' },
+  { label: '比赛时间', model: 'match_time' },
+  { label: '胜者', model: 'winner', format: (i: any) => getDict(i, 'winner') },
+  { label: '状态', model: 'status', type: 'select', isSearch: true, format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '赛程状态', method: 'status' },
+  { label: '修改', method: 'edit', display: (i) => i.status == '0' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status == '0' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const statusList: Ref<any> = ref([]);
+const applicaList: Ref<any> = ref([]);
+const winnerList: Ref<any> = ref([]);
+// 弹框
+const dialog: Ref<any> = ref({ title: '赛程状态', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFields: Ref<any> = ref([{ label: '状态', model: 'status', type: 'select' }]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, match_id: id.value };
+  const res: IQueryResult = await courseAxios.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+// 选择
+const dataChange = ({ model, value }) => {
+  if (model == 'status') {
+    if (value == '-1') {
+      formFields.value.splice(
+        3,
+        0,
+        { label: '红方分数', model: 'red_score' },
+        { label: '蓝方分数', model: 'blue_score' },
+        { label: '胜者', model: 'winner', type: 'select' }
+      );
+    } else {
+      formFields.value = [{ label: '状态', model: 'status', type: 'select' }];
+    }
+  }
+};
+const getDict = (e, model) => {
+  if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  } else if (model == 'winner') {
+    let name = '暂无';
+    if (e) {
+      let data: any = applicaList.value.find((i: any) => i.team_id == e);
+      if (data) name = data.team_name;
+    }
+    return name;
+  }
+};
+
+// 赛程状态
+const toStatus = async (data) => {
+  let res: any = await courseAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    // 胜者
+    const { red_team_id, blue_team_id } = res.data;
+    const red: any = await teamAxios.fetch(red_team_id);
+    const blue: any = await teamAxios.fetch(blue_team_id);
+    let list = [];
+    red.data.color = '红方';
+    blue.data.color = '蓝方';
+    list.push(red.data);
+    list.push(blue.data);
+    winnerList.value = list;
+    if (res.data.status == '-1') {
+      formFields.value.splice(
+        3,
+        0,
+        { label: '红方分数', model: 'red_score' },
+        { label: '蓝方分数', model: 'blue_score' },
+        { label: '胜者', model: 'winner', type: 'select' }
+      );
+    } else {
+      formFields.value = [{ label: '状态', model: 'status', type: 'select' }];
+    }
+    dialog.value = { title: '赛程状态', show: true, type: '1' };
+  }
+};
+// 添加
+const toAdd = () => {
+  router.push({ path: '/match/course/detail', query: { match_id: id.value } });
+};
+// 修改
+const toEdit = (data) => {
+  router.push({ path: '/match/course/detail', query: { id: data._id, match_id: id.value } });
+};
+// 删除
+const toDel = async (data: any) => {
+  let res: IQueryResult = await courseAxios.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 状态
+  res = await dictAxios.query({ type: 'course_status' });
+  if (res.errcode == '0') statusList.value = res.data;
+  // 报名团队
+  res = await applicationAxios.query({ match_id: id.value });
+  if (res.errcode == '0') applicaList.value = res.data;
+};
+// 提交保存
+const toSave = async (data) => {
+  let res: IQueryResult = await courseAxios.update(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: '信息审核成功', type: 'success' });
+    toClose();
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  formFields.value = [{ label: '状态', model: 'status', type: 'select' }];
+  dialog.value = { show: false };
+  search({ skip, limit });
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 92 - 0
src/views/match/detail.vue

@@ -0,0 +1,92 @@
+<template>
+  <div id="detail">
+    <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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="rules" @save="toSave" label-width="auto">
+            <template #information>
+              <cEditor v-model="form.information" url="/files/ball/match/upload"></cEditor>
+            </template>
+            <template #status>
+              <el-option v-for="i in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
+import type { FormRules } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import { useRoute } from 'vue-router';
+// 接口
+import { MatchStore } from '@/stores/match/match';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const matchAxios = MatchStore();
+const dictAxios = DictDataStore();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+const rules = reactive<FormRules>({});
+let fields: Ref<any[]> = ref([
+  { label: '比赛名称', model: 'name' },
+  { label: '开始时间', model: 'start_time', type: 'datetime' },
+  { label: '结束时间', model: 'end_time', type: 'datetime' },
+  { label: '比赛地点', model: 'address' },
+  { label: '报名截止时间', model: 'sign_deadline', type: 'datetime' },
+  { label: '比赛信息', model: 'information', custom: true },
+  { label: '状态', model: 'status', type: 'select' }
+]);
+// 字典表
+const statusList: Ref<any> = ref([]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    let res: IQueryResult = await matchAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      form.value = info;
+    }
+  }
+};
+// 保存
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await matchAxios.update(data);
+  else res = await matchAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 状态
+  res = await dictAxios.query({ type: 'match_status' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

+ 109 - 0
src/views/match/enroll/detail.vue

@@ -0,0 +1,109 @@
+<template>
+  <div id="detail">
+    <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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="{}" :isSave="false" label-width="auto" :disabled="true">
+            <template #user_id>
+              <cTable :fields="memberfields" :opera="[]" :list="form.user_id" :usePage="false"> </cTable>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+// 接口
+import { UserStore } from '@/stores/users/user';
+import { ApplicationStore } from '@/stores/match/application';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const applicationAxios = ApplicationStore();
+const dictAxios = DictDataStore();
+const userAxios = UserStore();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '比赛名称', model: 'match_name' },
+  { label: '团队名称', model: 'team_name', isSearch: true },
+  { label: '参赛人数', model: 'num' },
+  { label: '报名时间', model: 'apply_time' },
+  { label: '分数', model: 'score' },
+  { label: '团队成员', model: 'user_id', custom: true }
+]);
+// 字典表
+const genderList: Ref<any> = ref([]);
+const userList: Ref<any> = ref([]);
+
+const memberfields: Ref<any> = ref([
+  { label: '姓名', model: 'name' },
+  { label: '性别', model: 'gender', format: (i: any) => getDict(i, 'gender') },
+  { label: '年龄', model: 'age' },
+  { label: '手机号', model: 'phone' }
+]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    let res: IQueryResult = await applicationAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      let memberList = [];
+      for (const val of info.user_id) {
+        for (const as of userList.value) {
+          if (val == as._id)
+            memberList.push({
+              _id: val,
+              name: as.name || '暂无',
+              age: as.age || '暂无',
+              phone: as.phone || '暂无',
+              gender: as.gender || '暂无'
+            });
+        }
+      }
+      info.user_id = memberList;
+      form.value = info;
+    }
+  }
+};
+const getDict = (e, model) => {
+  if (model == 'gender') {
+    let data: any = genderList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 管理员
+  res = await userAxios.query({ status: '1' });
+  if (res.errcode == '0') userList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,131 @@
+<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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @exam="toExam" @view="toView"> </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="{}" @save="toSave" label-width="auto">
+            <template #status>
+              <el-option v-for="i in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter, useRoute } from 'vue-router';
+// 接口
+import { ApplicationStore } from '@/stores/match/application';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const applicationAxios = ApplicationStore();
+const dictAxios = DictDataStore();
+const route = useRoute();
+// 路由
+const router = useRouter();
+const { proxy } = getCurrentInstance() as any;
+// 加载中
+const loading: Ref<any> = ref(false);
+let id: Ref<any> = ref(route.query.id);
+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: 'team_name', isSearch: true },
+  { label: '参赛人数', model: 'num' },
+  { label: '报名时间', model: 'apply_time' },
+  { label: '分数', model: 'score' },
+  { label: '状态', model: 'status', format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '审核', method: 'exam', type: 'warning', display: (i) => i.status == '0' },
+  { label: '查看', method: 'view' }
+]);
+// 字典表
+const statusList: Ref<any> = ref([]);
+// 弹框
+const dialog: Ref<any> = ref({ title: '审核管理', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFields: Ref<any> = ref([{ label: '状态', model: 'status', type: 'select' }]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  id.value = route.query.id;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, match_id: id.value };
+  const res: IQueryResult = await applicationAxios.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const getDict = (e, model) => {
+  if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 保存
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await applicationAxios.update(data);
+  else res = await applicationAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toClose();
+  }
+};
+// 审核
+const toExam = async (data) => {
+  let res: IQueryResult = await applicationAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    dialog.value = { title: '审核管理', show: true, type: '1' };
+  }
+};
+// 查看
+const toView = (data) => {
+  router.push({ path: '/match/enroll/detail', query: { id: data._id } });
+};
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value = { show: false };
+  search({ skip, limit });
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 状态
+  res = await dictAxios.query({ type: 'status' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,144 @@
+<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">
+            <template #status>
+              <el-option v-for="(i, index) in statusList" :key="index" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </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"
+            @enroll="toEnroll"
+            @course="toCourse"
+            @rank="toRank"
+          >
+          </cTable>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+// 接口
+import { MatchStore } from '@/stores/match/match';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const matchAxios = MatchStore();
+const dictAxios = DictDataStore();
+const { proxy } = getCurrentInstance() as any;
+// 路由
+const router = useRouter();
+// 加载中
+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: 'name', isSearch: true },
+  { label: '开始时间', model: 'start_time' },
+  { label: '结束时间', model: 'end_time' },
+  { label: '比赛地点', model: 'address' },
+  { label: '报名截止时间', model: 'sign_deadline' },
+  { label: '状态', model: 'status', type: 'select', isSearch: true, format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '团队报名', method: 'enroll', display: (i) => i.status == '0' },
+  { label: '赛程安排', method: 'course', display: (i) => i.status == '1' || i.status == '2' || i.status == '-1' },
+  { label: '排名', method: 'rank', display: (i) => i.status == '-1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const statusList: 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 info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await matchAxios.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 = (e, model) => {
+  if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 添加
+const toAdd = () => {
+  router.push({ path: '/match/detail' });
+};
+// 查看
+const toEdit = (data) => {
+  router.push({ path: '/match/detail', query: { id: data._id } });
+};
+// 删除
+const toDel = async (data: any) => {
+  let res: IQueryResult = await matchAxios.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+// 报名情况
+const toEnroll = async (data: any) => {
+  router.push({ path: '/match/enroll', query: { id: data._id } });
+};
+// 赛程
+const toCourse = async (data: any) => {
+  router.push({ path: '/match/course', query: { id: data._id } });
+};
+// 排名
+const toRank = async (data: any) => {
+  router.push({ path: '/match/ranking', query: { id: data._id } });
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 状态
+  res = await dictAxios.query({ type: 'match_status' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

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

@@ -0,0 +1,49 @@
+<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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cTable :fields="fields" :opera="[]" :list="list" :usePage="false"> </cTable>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+// 接口
+import { CourseStore } from '@/stores/match/course';
+const courseAxios = CourseStore();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let list: Ref<any> = ref([]);
+let id: Ref<any> = ref(route.query.id);
+let fields: Ref<any[]> = ref([
+  { label: '团队名称', model: 'name' },
+  { label: '分数', model: 'score' }
+]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let res: any = await courseAxios.ranking({ match_id: id.value });
+  if (res.errcode == '0') list.value = res.data.scoreList;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

+ 93 - 0
src/views/system/config/index.vue

@@ -0,0 +1,93 @@
+<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"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="rules" @save="toSave" label-width="auto">
+            <template #logo_url="{ item }">
+              <cUpload
+                :model="item.model"
+                :limit="1"
+                url="/files/ball/config/upload"
+                :list="form[item.model]"
+                listType="picture-card"
+                @change="onUpload"
+              ></cUpload>
+            </template>
+            <template #longlogo_url="{ item }">
+              <cUpload
+                :model="item.model"
+                :limit="1"
+                url="/files/ball/config/upload"
+                :list="form[item.model]"
+                listType="picture-card"
+                @change="onUpload"
+              ></cUpload>
+            </template>
+            <template #admin_url="{ item }">
+              <cUpload
+                :model="item.model"
+                :limit="1"
+                url="/files/ball/config/upload"
+                :list="form[item.model]"
+                listType="picture-card"
+                @change="onUpload"
+              ></cUpload>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import type { FormRules } from 'element-plus';
+// 接口
+import { ConfigStore } from '@/stores/config';
+import type { IQueryResult } from '@/util/types.util';
+const configAxios = ConfigStore();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: 'logo', model: 'logo_url', custom: true },
+  { label: 'logo长图', model: 'longlogo_url', custom: true },
+  { label: '管理员二维码', model: 'admin_url', custom: true }
+]);
+const rules = reactive<FormRules>({});
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let res: IQueryResult = await configAxios.query();
+  if (res.errcode == '0') {
+    form.value = res.data;
+  }
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await configAxios.update(data);
+  else res = await configAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    search();
+  }
+};
+</script>
+<style scoped lang="scss"></style>

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

@@ -0,0 +1,175 @@
+<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: any) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const getDict = (value: any, model: any) => {
+  if (model == 'is_use') {
+    if (value) {
+      let data = is_useList.value.find((i: any) => i.value == value);
+      if (data) return data.label;
+      else return '暂无';
+    }
+  }
+};
+const getProps = (data: any, prop: any) => {
+  return _.get(data, prop);
+};
+// 新增
+const toAdd = () => {
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 修改
+const toEdit = async (data: any) => {
+  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: any) => {
+  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: any) => {
+  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: any) => {
+  router.push({ path: '/system/dictData', query: { id: data._id, type: data.type } });
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult = await dictData.query({ type: 'is_use' });
+  if (res.errcode == 0) is_useList.value = res.data;
+};
+</script>
+<style lang="scss" scoped>
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

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

@@ -0,0 +1,172 @@
+<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()">
+            <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-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: any) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const getDict = (value: any, model: any) => {
+  if (model == 'is_use') {
+    if (value) {
+      let data = is_useList.value.find((i: any) => 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: any) => {
+  form.value = data;
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 提交保存
+const toSave = async (data: any) => {
+  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: any) => {
+  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: 'is_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>

+ 128 - 0
src/views/team/examine/detail.vue

@@ -0,0 +1,128 @@
+<template>
+  <div id="detail">
+    <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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="{}" :isSave="false" label-width="auto" :disabled="true">
+            <template #administrator>
+              <el-option v-for="i in userList" :key="i._id" :label="i.name" :value="i._id"></el-option>
+            </template>
+            <template #logo>
+              <el-image class="image" v-for="i in form.logo" :key="i.uri" :src="i.url" @click="imgView(i.url)"></el-image>
+            </template>
+            <template #member>
+              <cTable :fields="memberfields" :opera="[]" :list="form.member" :usePage="false"> </cTable>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+// 接口
+import { TeamStore } from '@/stores/team/team';
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const teamAxios = TeamStore();
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '所属管理员', model: 'administrator', type: 'select' },
+  { label: '团队名称', model: 'name' },
+  { label: '成立时间', model: 'create_time', type: 'date' },
+  { label: '单位地址', model: 'address' },
+  { label: '手机号', model: 'phone' },
+  { label: '团队人数', model: 'number' },
+  { label: '团队成员', model: 'member', custom: true },
+  { label: '团队logo', model: 'logo', custom: true }
+]);
+// 字典表
+const userList: Ref<any> = ref([]);
+const genderList: Ref<any> = ref([]);
+
+const memberfields: Ref<any> = ref([
+  { label: '姓名', model: 'name' },
+  { label: '性别', model: 'gender', format: (i: any) => getDict(i, 'gender') },
+  { label: '年龄', model: 'age' },
+  { label: '手机号', model: 'phone' }
+]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    let res: IQueryResult = await teamAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      let memberList = [];
+      for (const val of info.member) {
+        for (const as of userList.value) {
+          if (val == as._id)
+            memberList.push({
+              _id: val,
+              name: as.name || '暂无',
+              age: as.age || '暂无',
+              phone: as.phone || '暂无',
+              gender: as.gender || '暂无'
+            });
+        }
+      }
+      info.member = memberList;
+      form.value = info;
+    }
+  }
+};
+const getDict = (e, model) => {
+  if (model == 'gender') {
+    let data: any = genderList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 管理员
+  res = await userAxios.query({ status: '1' });
+  if (res.errcode == '0') userList.value = res.data;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+};
+// 图片预览
+const imgView = (url: any) => {
+  window.open(url);
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.image {
+  width: 120px;
+  height: 120px;
+  overflow: hidden;
+  border-radius: 5px;
+}
+</style>

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

@@ -0,0 +1,157 @@
+<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">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @view="toView" @exam="toExam"> </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="{}" @save="toSave" label-width="auto">
+            <template #status>
+              <el-option v-for="i in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+// 接口
+import { TeamStore } from '@/stores/team/team';
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const teamAxios = TeamStore();
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+const { proxy } = getCurrentInstance() as any;
+// 路由
+const router = useRouter();
+// 加载中
+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: 'administrator', format: (i: any) => getDict(i, 'administrator') },
+  { label: '团队名称', model: 'name', isSearch: true },
+  { label: '单位地址', model: 'address' },
+  { label: '手机号', model: 'phone', isSearch: true },
+  { label: '团队人数', model: 'number' },
+  { label: '成立时间', model: 'create_time' },
+  { label: '状态', model: 'status', format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '查看', method: 'view', tpye: 'Info' },
+  { label: '审核', method: 'exam', type: 'warning', display: (i) => i.status == '0' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+const statusList: Ref<any> = ref([]);
+const userList: Ref<any> = ref([]);
+// 弹框
+const dialog: Ref<any> = ref({ title: '审核管理', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFields: Ref<any> = ref([{ label: '状态', model: 'status', type: 'select' }]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, status: '0' };
+  const res: IQueryResult = await teamAxios.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 = (e, model) => {
+  if (model == 'administrator') {
+    let data: any = userList.value.find((i: any) => i._id == e);
+    if (data) return data.name;
+    else return '暂无';
+  } else if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 查看
+const toView = (data) => {
+  router.push({ path: '/team/examine/detail', query: { id: data._id } });
+};
+// 审核
+const toExam = async (data) => {
+  let res: IQueryResult = await teamAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    dialog.value = { title: '审核管理', show: true, type: '1' };
+  }
+};
+// 提交保存
+const toSave = async (data) => {
+  let res: IQueryResult = await teamAxios.update(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: '信息审核成功', type: 'success' });
+    toClose();
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value = { show: false };
+  search({ skip, limit });
+};
+
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类别
+  res = await dictAxios.query({ type: 'type' });
+  if (res.errcode == '0') typeList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'status' });
+  if (res.errcode == '0') statusList.value = res.data;
+  // 管理员
+  res = await userAxios.query({ status: '1' });
+  if (res.errcode == '0') userList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 128 - 0
src/views/team/team/detail.vue

@@ -0,0 +1,128 @@
+<template>
+  <div id="detail">
+    <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_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="{}" :isSave="false" label-width="auto" :disabled="true">
+            <template #administrator>
+              <el-option v-for="i in userList" :key="i._id" :label="i.name" :value="i._id"></el-option>
+            </template>
+            <template #logo>
+              <el-image class="image" v-for="i in form.logo" :key="i.uri" :src="i.url" @click="imgView(i.url)"></el-image>
+            </template>
+            <template #member>
+              <cTable :fields="memberfields" :opera="[]" :list="form.member" :usePage="false"> </cTable>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, onMounted } from 'vue';
+import { useRoute } from 'vue-router';
+// 接口
+import { TeamStore } from '@/stores/team/team';
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const teamAxios = TeamStore();
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '所属管理员', model: 'administrator', type: 'select' },
+  { label: '团队名称', model: 'name' },
+  { label: '成立时间', model: 'create_time', type: 'date' },
+  { label: '单位地址', model: 'address' },
+  { label: '手机号', model: 'phone' },
+  { label: '团队人数', model: 'number' },
+  { label: '团队成员', model: 'member', custom: true },
+  { label: '团队logo', model: 'logo', custom: true }
+]);
+// 字典表
+const userList: Ref<any> = ref([]);
+const genderList: Ref<any> = ref([]);
+
+const memberfields: Ref<any> = ref([
+  { label: '姓名', model: 'name' },
+  { label: '性别', model: 'gender', format: (i: any) => getDict(i, 'gender') },
+  { label: '年龄', model: 'age' },
+  { label: '手机号', model: 'phone' }
+]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    let res: IQueryResult = await teamAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      let memberList = [];
+      for (const val of info.member) {
+        for (const as of userList.value) {
+          if (val == as._id)
+            memberList.push({
+              _id: val,
+              name: as.name || '暂无',
+              age: as.age || '暂无',
+              phone: as.phone || '暂无',
+              gender: as.gender || '暂无'
+            });
+        }
+      }
+      info.member = memberList;
+      form.value = info;
+    }
+  }
+};
+const getDict = (e, model) => {
+  if (model == 'gender') {
+    let data: any = genderList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 管理员
+  res = await userAxios.query({ status: '1' });
+  if (res.errcode == '0') userList.value = res.data;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+};
+// 图片预览
+const imgView = (url: any) => {
+  window.open(url);
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.image {
+  width: 120px;
+  height: 120px;
+  overflow: hidden;
+  border-radius: 5px;
+}
+</style>

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

@@ -0,0 +1,121 @@
+<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">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @view="toView"> </cTable>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { useRouter } from 'vue-router';
+// 接口
+import { TeamStore } from '@/stores/team/team';
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const teamAxios = TeamStore();
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+const { proxy } = getCurrentInstance() as any;
+// 路由
+const router = useRouter();
+// 加载中
+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: 'administrator', format: (i: any) => getDict(i, 'administrator') },
+  { label: '团队名称', model: 'name', isSearch: true },
+  { label: '单位地址', model: 'address' },
+  { label: '手机号', model: 'phone', isSearch: true },
+  { label: '团队人数', model: 'number' },
+  { label: '成立时间', model: 'create_time' },
+  { label: '状态', model: 'status', format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([{ label: '查看', method: 'view', tpye: 'Info' }]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+const statusList: Ref<any> = ref([]);
+const userList: 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 info = { skip: e.skip, limit: e.limit, ...searchForm.value, status: '1' };
+  const res: IQueryResult = await teamAxios.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 = (e, model) => {
+  if (model == 'administrator') {
+    let data: any = userList.value.find((i: any) => i._id == e);
+    if (data) return data.name;
+    else return '暂无';
+  } else if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 查看
+const toView = (data) => {
+  router.push({ path: '/team/team/detail', query: { id: data._id } });
+};
+
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类别
+  res = await dictAxios.query({ type: 'type' });
+  if (res.errcode == '0') typeList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'status' });
+  if (res.errcode == '0') statusList.value = res.data;
+  // 管理员
+  res = await userAxios.query({ status: '1' });
+  if (res.errcode == '0') userList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+.dialog_one {
+  .image {
+    width: 120px;
+    height: 120px;
+    border-radius: 5px;
+  }
+}
+</style>

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

@@ -0,0 +1,69 @@
+<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: any, value: any, callback: any) => {
+        if (form.value.password !== value) {
+          callback(new Error('两次输入的密码不一致'));
+        } else {
+          callback();
+        }
+      }
+    }
+  ]
+});
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  loading.value = false;
+});
+// 提交保存
+const toSave = async (data: any) => {
+  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>

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

@@ -0,0 +1,154 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">
+          <cSearch :is_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="rules" @save="toSave" label-width="auto">
+            <template #gender>
+              <el-option v-for="i in genderList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+            <template #type>
+              <el-option v-for="i in typeList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+            <template #icon>
+              <cUpload :model="`${'icon'}`" listType="picture-card" :limit="1" url="/files/ball/match/upload" :list="form.icon" @change="onUpload"></cUpload>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import type { FormRules } from 'element-plus';
+import { useRoute } from 'vue-router';
+// 接口
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+// 路由
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '微信用户标识', model: 'openid', options: { disabled: true } },
+  { label: '账号', model: 'account', options: { disabled: true } },
+  { label: '名称', model: 'name' },
+  { label: '手机号', model: 'phone' },
+  { label: '电子邮箱', model: 'email' },
+  { label: '性别', model: 'gender', type: 'select' },
+  { label: '年龄', model: 'age' },
+  { label: '工作单位', model: 'work' },
+  { label: '头像', model: 'icon', custom: true }
+]);
+const rules = reactive<FormRules>({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    let res: IQueryResult = await userAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      form.value = info;
+    }
+  }
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// 保存
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await userAxios.update(data);
+  else res = await userAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类别
+  res = await dictAxios.query({ type: 'type' });
+  if (res.errcode == '0') typeList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.study {
+  width: 100%;
+  .study_1 {
+    margin: 0 0 10px 0;
+    span {
+      font-size: 16px;
+    }
+    span:first-child {
+      color: #ff0000;
+    }
+  }
+  .study_2 {
+    .study_2_info {
+      width: 100%;
+      display: flex;
+      .info_1 {
+        position: relative;
+        max-width: 24%;
+        border: 1px solid #67c23a;
+        padding: 0 10px;
+        border-radius: 5px;
+        margin: 0 10px 0 0;
+        .txt {
+          position: absolute;
+          top: -15px;
+          left: 10px;
+          span {
+            display: inline-block;
+            padding: 6px 15px;
+            background: #67c23a;
+            color: #fff;
+            border-radius: 5px;
+            font-size: 16px;
+            font-weight: bold;
+            line-height: 1;
+          }
+        }
+        .info {
+          margin: 15px 0 0 0;
+          .label {
+            margin: 0 0 10px 0;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,163 @@
+<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">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @exam="toExam" @edit="toEdit" @del="toDel"> </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="{}" @save="toSave" label-width="auto">
+            <template #status>
+              <el-option v-for="i in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+// 接口
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+const { proxy } = getCurrentInstance() as any;
+// 路由
+const router = useRouter();
+// 加载中
+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: 'openid' },
+  { label: '用户账号', model: 'account', isSearch: true },
+  { label: '姓名', model: 'name', isSearch: true },
+  { label: '性别', model: 'gender', format: (i: any) => getDict(i, 'gender') },
+  { label: '年龄', model: 'age' },
+  { label: '手机号', model: 'phone', isSearch: true },
+  { label: '电子邮箱', model: 'email' },
+  { label: '工作单位', model: 'work' },
+  { label: '状态', model: 'status', format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '审核', method: 'exam', type: 'warning', display: (i) => i.status == '0' },
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+const statusList: Ref<any> = ref([]);
+// 弹框
+const dialog: Ref<any> = ref({ title: '审核管理', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFields: Ref<any> = ref([{ label: '状态', model: 'status', type: 'select' }]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, type: '0' };
+  const res: IQueryResult = await userAxios.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 = (e, model) => {
+  if (model == 'gender') {
+    let data: any = genderList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  } else if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 审核
+const toExam = async (data) => {
+  let res: IQueryResult = await userAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    dialog.value = { title: '审核管理', show: true, type: '1' };
+  }
+};
+// 提交保存
+const toSave = async (data) => {
+  let res: IQueryResult = await userAxios.update(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: '信息审核成功', type: 'success' });
+    toClose();
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+// 修改
+const toEdit = (data) => {
+  router.push({ path: '/users/match/detail', query: { id: data._id } });
+};
+// 删除
+const toDel = async (data: any) => {
+  let res: IQueryResult = await userAxios.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value = { show: false };
+  search({ skip, limit });
+};
+
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类别
+  res = await dictAxios.query({ type: 'type' });
+  if (res.errcode == '0') typeList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'status' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

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

@@ -0,0 +1,156 @@
+<template>
+  <div id="detail">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight" v-loading="loading">
+        <el-col :span="24" class="one">
+          <cSearch :is_back="true" @toBack="toBack"></cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cForm :span="24" :fields="fields" :form="form" :rules="rules" @save="toSave" label-width="auto">
+            <template #gender>
+              <el-option v-for="i in genderList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+            <template #type>
+              <el-option v-for="i in typeList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+            <template #icon>
+              <cUpload :model="`${'icon'}`" listType="picture-card" :limit="1" url="/files/ball/match/upload" :list="form.icon" @change="onUpload"></cUpload>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import type { FormRules } from 'element-plus';
+import { useRoute } from 'vue-router';
+// 接口
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+// 路由
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '微信用户标识', model: 'openid', options: { disabled: true } },
+  { label: '账号', model: 'account', options: { disabled: true } },
+  // { label: '类别', model: 'type', type: 'select', options: { disabled: true } },
+  { label: '姓名', model: 'name' },
+  { label: '性别', model: 'gender', type: 'select' },
+  { label: '年龄', model: 'age' },
+  { label: '手机号', model: 'phone' },
+  { label: '电子邮箱', model: 'email' },
+  { label: '工作单位', model: 'work' },
+  { label: '头像', model: 'icon', custom: true }
+]);
+const rules = reactive<FormRules>({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = route.query.id;
+  if (id) {
+    let res: IQueryResult = await userAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      form.value = info;
+    }
+  }
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// 保存
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await userAxios.update(data);
+  else res = await userAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类别
+  res = await dictAxios.query({ type: 'type' });
+  if (res.errcode == '0') typeList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.study {
+  width: 100%;
+  .study_1 {
+    margin: 0 0 10px 0;
+    span {
+      font-size: 16px;
+    }
+    span:first-child {
+      color: #ff0000;
+    }
+  }
+  .study_2 {
+    .study_2_info {
+      width: 100%;
+      display: flex;
+      .info_1 {
+        position: relative;
+        max-width: 24%;
+        border: 1px solid #67c23a;
+        padding: 0 10px;
+        border-radius: 5px;
+        margin: 0 10px 0 0;
+        .txt {
+          position: absolute;
+          top: -15px;
+          left: 10px;
+          span {
+            display: inline-block;
+            padding: 6px 15px;
+            background: #67c23a;
+            color: #fff;
+            border-radius: 5px;
+            font-size: 16px;
+            font-weight: bold;
+            line-height: 1;
+          }
+        }
+        .info {
+          margin: 15px 0 0 0;
+          .label {
+            margin: 0 0 10px 0;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,162 @@
+<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">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @exam="toExam" @edit="toEdit" @del="toDel"> </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="{}" @save="toSave" label-width="auto">
+            <template #status>
+              <el-option v-for="i in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </template>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+// 接口
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/dict/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const userAxios = UserStore();
+const dictAxios = DictDataStore();
+const { proxy } = getCurrentInstance() as any;
+// 路由
+const router = useRouter();
+// 加载中
+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: 'openid' },
+  { label: '用户账号', model: 'account', isSearch: true },
+  { label: '姓名', model: 'name', isSearch: true },
+  { label: '性别', model: 'gender', format: (i: any) => getDict(i, 'gender') },
+  { label: '年龄', model: 'age' },
+  { label: '手机号', model: 'phone', isSearch: true },
+  { label: '电子邮箱', model: 'email' },
+  { label: '工作单位', model: 'work' },
+  { label: '状态', model: 'status', format: (i: any) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '审核', method: 'exam', type: 'warning', display: (i) => i.status == '0' },
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+const statusList: Ref<any> = ref([]);
+// 弹框
+const dialog: Ref<any> = ref({ title: '审核管理', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFields: Ref<any> = ref([{ label: '状态', model: 'status', type: 'select' }]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, type: '1' };
+  const res: IQueryResult = await userAxios.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 = (e, model) => {
+  if (model == 'gender') {
+    let data: any = genderList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  } else if (model == 'status') {
+    let data: any = statusList.value.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 审核
+const toExam = async (data) => {
+  let res: IQueryResult = await userAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    dialog.value = { title: '审核管理', show: true, type: '1' };
+  }
+};
+// 提交保存
+const toSave = async (data) => {
+  let res: IQueryResult = await userAxios.update(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: '信息审核成功', type: 'success' });
+    toClose();
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+// 修改
+const toEdit = (data) => {
+  router.push({ path: '/users/team/detail', query: { id: data._id } });
+};
+// 删除
+const toDel = async (data: any) => {
+  let res: IQueryResult = await userAxios.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value = { show: false };
+  search({ skip, limit });
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 性别
+  res = await dictAxios.query({ type: 'gender' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类别
+  res = await dictAxios.query({ type: 'type' });
+  if (res.errcode == '0') typeList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'status' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 20 - 0
tsconfig.app.json

@@ -0,0 +1,20 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": [
+    "env.d.ts",
+    "src/**/*",
+    "src/**/*.vue"
+  ],
+  "exclude": [
+    "src/**/__tests__/*"
+  ],
+  "compilerOptions": {
+    "composite": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": [
+        "./src/*"
+      ]
+    }
+  }
+}

+ 44 - 0
tsconfig.json

@@ -0,0 +1,44 @@
+{
+  // "extends": "@vue/tsconfig/tsconfig.web.json",
+  // "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "../common/src/stores/user", "../common/src/stores/user"],
+  "include": [
+    "src/**/*.ts",
+    "src/**/*.d.ts",
+    "src/**/*.tsx",
+    "src/**/*.vue",
+    "src/untils/baiduMap.js",
+    "src/untils/debuonce.js",
+    "**/*.ts",
+    "**/*.tsx"
+  ],
+  "compilerOptions": {
+    "ignoreDeprecations": "5.0",
+    "target": "esnext",
+    "module": "esnext",
+    "strict": false,
+    "jsx": "preserve",
+    "moduleResolution": "node",
+    "baseUrl": "./",
+    "paths": {
+      "@/*": [
+        "./src/*"
+      ],
+    },
+    "isolatedModules": false,
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "lib": [
+      "es5",
+      "es6",
+      "dom",
+      "dom.iterable"
+    ],
+    "allowSyntheticDefaultImports": true
+  },
+  "references": [
+    {
+      "path": "./tsconfig.node.json"
+    }
+  ]
+}

+ 16 - 0
tsconfig.node.json

@@ -0,0 +1,16 @@
+{
+  // "extends": "@vue/tsconfig/tsconfig.node.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "ignoreDeprecations": "5.0",
+    "composite": true,
+    "types": [
+      "node"
+    ]
+  }
+}

+ 34 - 0
vite.config.ts

@@ -0,0 +1,34 @@
+import { fileURLToPath, URL } from 'node:url';
+import { defineConfig, loadEnv } from 'vite';
+import vue from '@vitejs/plugin-vue';
+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', //http://169.254.16.148:13005  http://192.168.1.113:13005
+          changeOrigin: true,
+          ws: false
+        }
+      },
+      fs: { strict: false }
+    },
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url))
+      }
+    }
+  };
+});