zs 1 jaar geleden
commit
6c968eec71
54 gewijzigde bestanden met toevoegingen van 13058 en 0 verwijderingen
  1. 7 0
      .env.development
  2. 7 0
      .env.production
  3. 28 0
      .eslintrc.cjs
  4. 28 0
      .gitignore
  5. 8 0
      .prettierrc.json
  6. 8 0
      .vscode/extensions.json
  7. 46 0
      README.md
  8. 5 0
      env.d.ts
  9. 16 0
      index.html
  10. 10521 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. 59 0
      src/assets/icon/iconfont.css
  17. 1 0
      src/assets/icon/iconfont.js
  18. 86 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. 84 0
      src/components/admin-frame/parts/Header.vue
  26. 114 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. 69 0
      src/components/frame/c-code.vue
  30. 29 0
      src/components/frame/c-dialog.vue
  31. 236 0
      src/components/frame/c-form.vue
  32. 53 0
      src/components/frame/c-pages.vue
  33. 172 0
      src/components/frame/c-search.vue
  34. 215 0
      src/components/frame/c-table.vue
  35. 108 0
      src/components/frame/c-upload.vue
  36. 69 0
      src/components/frame/wang-editor.vue
  37. 16 0
      src/components/index.ts
  38. 15 0
      src/layout/site.ts
  39. 43 0
      src/main.ts
  40. 58 0
      src/router/index.ts
  41. 62 0
      src/stores/basic/role.ts
  42. 11 0
      src/stores/counter.ts
  43. 6 0
      src/stores/user/mutations.ts
  44. 1 0
      src/stores/user/state.ts
  45. 63 0
      src/stores/users/admin.ts
  46. 150 0
      src/util/axios-wrapper.ts
  47. 29 0
      src/util/types.util.ts
  48. 41 0
      src/views/home/index.vue
  49. 169 0
      src/views/login/index.vue
  50. 41 0
      src/views/register/index.vue
  51. 12 0
      tsconfig.app.json
  52. 44 0
      tsconfig.json
  53. 16 0
      tsconfig.node.json
  54. 35 0
      vite.config.ts

+ 7 - 0
.env.development

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

+ 7 - 0
.env.production

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

+ 28 - 0
.gitignore

@@ -0,0 +1,28 @@
+# 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?

+ 8 - 0
.prettierrc.json

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

+ 8 - 0
.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "Vue.vscode-typescript-vue-plugin",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode"
+  ]
+}

+ 46 - 0
README.md

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

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

File diff suppressed because it is too large
+ 10521 - 0
package-lock.json


+ 48 - 0
package.json

@@ -0,0 +1,48 @@
+{
+  "name": "travel-admin",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "dev": "vite",
+    "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.5.0",
+    "echarts": "^5.4.3",
+    "element-plus": "^2.3.4",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.4",
+    "naf-core": "^0.1.2",
+    "pinia": "^2.1.6",
+    "vue": "^3.3.4",
+    "vue-router": "^4.2.4",
+    "vuex": "^4.1.0"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.3.2",
+    "@tsconfig/node18": "^18.2.0",
+    "@types/node": "^18.17.5",
+    "@vitejs/plugin-vue": "^4.3.1",
+    "@vue/eslint-config-prettier": "^8.0.0",
+    "@vue/eslint-config-typescript": "^11.0.3",
+    "@vue/tsconfig": "^0.4.0",
+    "eslint": "^8.46.0",
+    "eslint-plugin-vue": "^9.16.1",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^3.0.0",
+    "sass": "^1.66.1",
+    "sass-loader": "^13.3.2",
+    "typescript": "~5.1.6",
+    "vite": "^4.4.9",
+    "vue-tsc": "^1.8.8"
+  }
+}

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


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

@@ -0,0 +1,59 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 4182834 */
+  src: url('iconfont.woff2?t=1690504779226') format('woff2'),
+       url('iconfont.woff?t=1690504779226') format('woff'),
+       url('iconfont.ttf?t=1690504779226') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-yonghu:before {
+  content: "\e643";
+}
+
+.icon-shezhi:before {
+  content: "\10127";
+}
+
+.icon-shouye:before {
+  content: "\10128";
+}
+
+.icon-taocan:before {
+  content: "\10129";
+}
+
+.icon-pingjia:before {
+  content: "\1012a";
+}
+
+.icon-fuwu:before {
+  content: "\1012b";
+}
+
+.icon-bangzhuzhongxin:before {
+  content: "\1012c";
+}
+
+.icon-tukuguanli:before {
+  content: "\1012d";
+}
+
+.icon-gengduo:before {
+  content: "\1012e";
+}
+
+.icon-haoyoutuijian:before {
+  content: "\1012f";
+}
+
+.icon-zengjia:before {
+  content: "\e608";
+}
+

File diff suppressed because it is too large
+ 1 - 0
src/assets/icon/iconfont.js


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

@@ -0,0 +1,86 @@
+{
+  "id": "4182834",
+  "name": "耗材",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "16530775",
+      "name": "用户",
+      "font_class": "yonghu",
+      "unicode": "e643",
+      "unicode_decimal": 58947
+    },
+    {
+      "icon_id": "36409813",
+      "name": "设置",
+      "font_class": "shezhi",
+      "unicode": "10127",
+      "unicode_decimal": 65831
+    },
+    {
+      "icon_id": "36409815",
+      "name": "首页",
+      "font_class": "shouye",
+      "unicode": "10128",
+      "unicode_decimal": 65832
+    },
+    {
+      "icon_id": "36409818",
+      "name": "套餐",
+      "font_class": "taocan",
+      "unicode": "10129",
+      "unicode_decimal": 65833
+    },
+    {
+      "icon_id": "36409820",
+      "name": "评价",
+      "font_class": "pingjia",
+      "unicode": "1012a",
+      "unicode_decimal": 65834
+    },
+    {
+      "icon_id": "36409824",
+      "name": "服务",
+      "font_class": "fuwu",
+      "unicode": "1012b",
+      "unicode_decimal": 65835
+    },
+    {
+      "icon_id": "36409827",
+      "name": "帮助中心",
+      "font_class": "bangzhuzhongxin",
+      "unicode": "1012c",
+      "unicode_decimal": 65836
+    },
+    {
+      "icon_id": "36409831",
+      "name": "图库管理",
+      "font_class": "tukuguanli",
+      "unicode": "1012d",
+      "unicode_decimal": 65837
+    },
+    {
+      "icon_id": "36409832",
+      "name": "更多",
+      "font_class": "gengduo",
+      "unicode": "1012e",
+      "unicode_decimal": 65838
+    },
+    {
+      "icon_id": "36409833",
+      "name": "好友推荐",
+      "font_class": "haoyoutuijian",
+      "unicode": "1012f",
+      "unicode_decimal": 65839
+    },
+    {
+      "icon_id": "25739959",
+      "name": "增加",
+      "font_class": "zengjia",
+      "unicode": "e608",
+      "unicode_decimal": 58888
+    }
+  ]
+}

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>

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

@@ -0,0 +1,84 @@
+<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>
+                {{ 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';
+import { useRouter } from 'vue-router';
+const router = useRouter();
+let user: Ref<any> = ref(store.state.user);
+// 退出登录
+const logout = () => {
+  localStorage.removeItem('token');
+  // 跳转开发模式404.就是直接跳回去就完事了
+  router.push('login');
+};
+</script>
+<style scoped lang="scss">
+.main {
+  background-color: #242f42;
+
+  .one {
+    height: 60px;
+    border-bottom: 1px solid #f1f1f1;
+    padding: 0 10px;
+    display: flex;
+
+    .left {
+      display: flex;
+      align-items: center;
+      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>

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

@@ -0,0 +1,114 @@
+<!-- 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';
+
+// 接口
+import { RoleStore } from '@/stores/basic/role';
+import type { IQueryResult } from '@/util/types.util';
+const roleAxios = RoleStore();
+
+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 () => {
+  // let res: IQueryResult = await roleAxios.am();
+  // if (res.errcode == 0) items.value = res.data;
+};
+
+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>

+ 69 - 0
src/components/frame/c-code.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="ValidCode disabled-select" :style="`width:${width}; height:${height}`" @click="refreshCode">
+    <span v-for="(item, index) in codeList" :key="index" :style="getStyle(item)">
+      {{ item.code }}
+    </span>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Ref } from 'vue';
+import { onMounted, toRefs, ref } from 'vue';
+const emit = defineEmits(['input']);
+const codeList: Ref<any> = ref([]);
+const props = defineProps({
+  width: { type: String, default: () => '100px' },
+  height: { type: String, default: () => '38px' },
+  length: { type: Number, default: () => 4 }
+});
+const { width } = toRefs(props);
+const { height } = toRefs(props);
+const { length } = toRefs(props);
+// 请求
+onMounted(async () => {
+  createdCode();
+});
+const refreshCode = () => {
+  createdCode();
+}
+const createdCode = () => {
+  const len = length.value;
+  const code = [];
+  const chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789';
+  const charsLen = chars.length;
+  // 生成
+  for (let i = 0; i < len; i++) {
+    const rgb = [Math.round(Math.random() * 220), Math.round(Math.random() * 240), Math.round(Math.random() * 200)];
+    code.push({
+      code: chars.charAt(Math.floor(Math.random() * charsLen)),
+      color: `rgb(${rgb})`,
+      fontSize: `1${[Math.floor(Math.random() * 10)]}px`,
+      padding: `${[Math.floor(Math.random() * 10)]}px`,
+      transform: `rotate(${Math.floor(Math.random() * 90) - Math.floor(Math.random() * 90)}deg)`,
+    });
+  }
+  // 指向
+  codeList.value = code;
+  // 将当前数据派发出去
+  emit(`input`, codeList.value.map((item) => item.code).join(''));
+};
+const getStyle = (data: any) => {
+  return `color: ${data.color}; font-size: ${data.fontSize}; padding: ${data.padding}; transform: ${data.transform}`;
+}
+</script>
+
+<style lang="scss" scoped>
+.ValidCode {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  margin: 0 0 0 2px;
+
+  span {
+    display: inline-block;
+  }
+}
+</style>

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

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

@@ -0,0 +1,172 @@
+<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 { useRoute } from 'vue-router';
+import _ from 'lodash';
+// 路由
+const route = useRoute();
+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; data: { uri: any } }, 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.data, name: file.name, url: `${import.meta.env.VITE_APP_HOST}${response.data.uri}` });
+  } else {
+    arr.value = [{ ...ponse.data, name: file.name, url: `${import.meta.env.VITE_APP_HOST}${response.data.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>

+ 16 - 0
src/components/index.ts

@@ -0,0 +1,16 @@
+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';
+import cCode from './frame/c-code.vue';
+
+const components: {
+  [propName: string]: Component;
+} = { cButton, cDialog, cSearch, cForm, cTable, cUpload, cPages, cEditor, cCode };
+
+export default components;

+ 15 - 0
src/layout/site.ts

@@ -0,0 +1,15 @@
+// 网站基本设置
+export const siteInfo = {
+  display: false,
+  zhTitle: '神鹿峰管理平台'
+};
+// 菜单设置
+export const menuInfo = {
+  info: {
+    display: false,
+    mode: 'horizontal',
+    backColor: '#242f42',
+    textColor: '#ffffff'
+  },
+  menuList: [{ icon: 'icon-shouye', _id: 'admin_1', path: '/homeIndex', name: '系统首页' }]
+};

+ 43 - 0
src/main.ts

@@ -0,0 +1,43 @@
+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';
+
+// 引入 echarts
+import * as echarts from 'echarts';
+
+// 组件
+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;
+// 全局挂载 echarts
+app.config.globalProperties.$echarts = echarts;
+
+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');

+ 58 - 0
src/router/index.ts

@@ -0,0 +1,58 @@
+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: '/register',
+      name: 'register',
+      meta: { title: '账号注册' },
+      component: () => import('@/views/register/index.vue')
+    },
+    {
+      path: '/homeIndex',
+      meta: { title: '系统首页' },
+      component: () => import('@/components/admin-frame/home.vue'),
+      children: [
+        {
+          path: '/homeIndex',
+          meta: { title: '系统首页' },
+          component: () => import('@/views/home/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: '/travel/v1/api/tool/token',
+        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;

+ 62 - 0
src/stores/basic/role.ts

@@ -0,0 +1,62 @@
+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: `/travel/v1/api/role`
+};
+export const RoleStore = defineStore('role', () => {
+  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 um = async (): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/um`);
+    return res;
+  };
+  const am = async (): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/am`);
+    return res;
+  };
+  return {
+    count,
+    doubleCount,
+    increment,
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    um,
+    am
+  };
+});

+ 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

+ 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: `/travel/v1/api/admin`
+};
+export const AdminStore = defineStore('admin', () => {
+  const count = ref(0);
+  const doubleCount = computed(() => count.value * 2);
+  function increment() {
+    count.value++;
+  }
+  const query = async ({ skip = 0, limit = undefined, ...info }: IQueryParams = {}): Promise<IQueryResult> => {
+    let cond: IQueryType = {};
+    if (skip) cond.skip = skip;
+    if (limit) cond.limit = limit;
+    cond = { ...cond, ...info };
+    const res = await axios.$get(`${api.url}`, cond);
+    return res;
+  };
+  const fetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/${payload}`);
+    return res;
+  };
+  const create = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}`, payload);
+    return res;
+  };
+  const update = async (payload: any): Promise<IQueryResult> => {
+    const id = _.get(payload, 'id', _.get(payload, '_id'));
+    const res = await axios.$post(`${api.url}/${id}`, payload);
+    return res;
+  };
+  const del = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${api.url}/${payload}`);
+    return res;
+  };
+  const login = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}/login`, payload);
+    return res;
+  };
+  //password
+  const rp = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}/rp`, payload);
+    return res;
+  };
+  return {
+    count,
+    doubleCount,
+    increment,
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    login,
+    rp
+  };
+});

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

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

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

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

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

@@ -0,0 +1,41 @@
+<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';
+import { onMounted, ref } from 'vue';
+
+// 接口
+// import { ToolsStore } from '@/stores/tool';
+// import type { IQueryResult } from '@/util/types.util';
+// const toolsAxios = ToolsStore();
+
+// 加载中
+const loading: Ref<any> = ref(false);
+
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  search();
+  loading.value = false;
+});
+const search = async () => {
+  // let res: IQueryResult = await toolsAxios.dataCount();
+  // if (res.errcode == '0') {
+  //   info.value = res.data;
+  // }
+};
+</script>
+<style scoped lang="scss">
+.main {
+  padding: 2px;
+}
+</style>

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

@@ -0,0 +1,169 @@
+<template>
+  <div class="login">
+    <el-col :span="24" class="main animate__animated animate__backInRight">
+      <el-col :span="24" class="one w_1200">
+        <el-form ref="formRef" :model="form" :rules="rules" class="login-form">
+          <h3 class="title">{{ siteInfo.zhTitle }}</h3>
+          <el-form-item prop="account">
+            <el-input :prefix-icon="UserFilled" v-model="form.account" type="text" placeholder="账号"></el-input>
+          </el-form-item>
+          <el-form-item prop="password">
+            <el-input :prefix-icon="Lock" v-model="form.password" type="password" placeholder="密码"></el-input>
+          </el-form-item>
+          <el-form-item prop="code">
+            <el-input v-model="code" placeholder="验证码" style="width: 70%"> </el-input>
+            <cCode @input="createValidCode"></cCode>
+          </el-form-item>
+          <el-form-item style="width: 100%">
+            <el-button :loading="loading" size="default" type="primary" @click="toSave(formRef)" style="width: 100%">
+              <span v-if="!loading">登 录</span>
+              <span v-else>登 录 中...</span>
+            </el-button>
+          </el-form-item>
+          <div style="float: right">
+            <el-button type="primary" @click="register()" link>立即注册</el-button>
+          </div>
+        </el-form>
+        <!--  底部  -->
+        <div class="el-login-footer">
+          <span> Copyright © 2018-2022 free All Rights Reserved.</span>
+        </div>
+      </el-col>
+    </el-col>
+  </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 { Lock } from '@element-plus/icons-vue';
+import { UserFilled } from '@element-plus/icons-vue';
+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>();
+interface formData {
+  account?: string;
+  password?: string;
+  code?: string;
+}
+const form: Ref<formData> = ref({});
+const rules = reactive<FormRules>({
+  account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入账号密码', trigger: 'blur' }]
+});
+const validCode: Ref<String> = ref('');
+// 验证码
+const code: Ref<String> = ref('');
+// 加载中
+const loading: Ref<Boolean> = ref(false);
+// 请求
+onMounted(async () => {
+  window.addEventListener('keydown', (e) => {
+    if (e.code === 'Enter') toSave(formRef.value);
+  });
+});
+//接收验证码组件提交的4位验证码
+const createValidCode = (data) => {
+  validCode.value = data;
+};
+// 提交登录
+const toSave = async (formEl: FormInstance) => {
+  if (!formEl) return;
+  await formEl.validate((valid: any) => {
+    loading.value = false;
+    if (valid) {
+      if (!code.value) {
+        ElMessage({ message: `请填写验证码`, type: 'error' });
+        return;
+      }
+      if (code.value.toLowerCase() !== validCode.value.toLowerCase()) {
+        ElMessage({ message: `验证码错误`, type: 'error' });
+        return;
+      }
+      toLogin(form.value);
+    } else {
+      console.log('error submit!');
+    }
+  });
+};
+const toLogin = async (data: formData) => {
+  let res: IQueryResult = await adminAxios.login(data);
+  if (res.errcode == '0') {
+    loading.value = true;
+    ElMessage({ message: `登录成功`, type: 'success' });
+    localStorage.setItem('token', `${res.data}`);
+    router.push({ path: '/homeIndex' });
+  } else {
+    loading.value = false;
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+const register = () => {
+  router.push({ path: '/register' });
+};
+</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 {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+
+    .title {
+      margin: 0px auto 30px auto;
+      text-align: center;
+      color: #707070;
+    }
+
+    .login-form {
+      border-radius: 6px;
+      background: #ffffff;
+      width: 350px;
+      padding: 25px;
+
+      .el-input {
+        height: 38px;
+
+        input {
+          height: 38px;
+        }
+      }
+    }
+
+    .el-login-footer {
+      height: 40px;
+      line-height: 40px;
+      position: fixed;
+      bottom: 0;
+      width: 100%;
+      text-align: center;
+      color: #fff;
+      font-family: Arial;
+      font-size: 12px;
+      letter-spacing: 1px;
+    }
+  }
+}
+</style>

+ 41 - 0
src/views/register/index.vue

@@ -0,0 +1,41 @@
+<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';
+import { onMounted, ref } from 'vue';
+
+// 接口
+// import { ToolsStore } from '@/stores/tool';
+// import type { IQueryResult } from '@/util/types.util';
+// const toolsAxios = ToolsStore();
+
+// 加载中
+const loading: Ref<any> = ref(false);
+
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  search();
+  loading.value = false;
+});
+const search = async () => {
+  // let res: IQueryResult = await toolsAxios.dataCount();
+  // if (res.errcode == '0') {
+  //   info.value = res.data;
+  // }
+};
+</script>
+<style scoped lang="scss">
+.main {
+  padding: 2px;
+}
+</style>

+ 12 - 0
tsconfig.app.json

@@ -0,0 +1,12 @@
+{
+  "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": "es2022",
+    "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": "@tsconfig/node18/tsconfig.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "nightwatch.conf.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "composite": true,
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "types": ["node"]
+  }
+}

+ 35 - 0
vite.config.ts

@@ -0,0 +1,35 @@
+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: 10002,
+      proxy: {
+        '/files': {
+          target: 'https://broadcast.waityou24.cn',
+          changeOrigin: true
+        },
+        '/travel/v1/api': {
+          target: 'http://192.168.1.113:14001',
+          changeOrigin: true,
+          ws: false
+        }
+      },
+      fs: { strict: false }
+    },
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url))
+      }
+    }
+  };
+});