zs 1 年之前
当前提交
04ad2c9a36
共有 89 个文件被更改,包括 16143 次插入0 次删除
  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. 二进制
      public/favicon.ico
  13. 26 0
      src/App.vue
  14. 73 0
      src/assets/base.css
  15. 二进制
      src/assets/bglogin.jpg
  16. 51 0
      src/assets/icon/iconfont.css
  17. 1 0
      src/assets/icon/iconfont.js
  18. 72 0
      src/assets/icon/iconfont.json
  19. 二进制
      src/assets/icon/iconfont.ttf
  20. 二进制
      src/assets/icon/iconfont.woff
  21. 二进制
      src/assets/icon/iconfont.woff2
  22. 1 0
      src/assets/logo.svg
  23. 19 0
      src/assets/main.css
  24. 69 0
      src/components/admin-frame/home.vue
  25. 85 0
      src/components/admin-frame/parts/Header.vue
  26. 119 0
      src/components/admin-frame/parts/Sidebar.vue
  27. 28 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. 107 0
      src/components/frame/c-upload.vue
  36. 69 0
      src/components/frame/wang-editor.vue
  37. 16 0
      src/components/index.ts
  38. 14 0
      src/layout/site.ts
  39. 53 0
      src/main.ts
  40. 87 0
      src/router/index.ts
  41. 52 0
      src/stores/basic/config.ts
  42. 52 0
      src/stores/basic/dictData.ts
  43. 52 0
      src/stores/basic/dictType.ts
  44. 52 0
      src/stores/basic/map.ts
  45. 52 0
      src/stores/basic/menus.ts
  46. 52 0
      src/stores/basic/module.ts
  47. 52 0
      src/stores/basic/region.ts
  48. 62 0
      src/stores/basic/role.ts
  49. 52 0
      src/stores/business/coupon.ts
  50. 52 0
      src/stores/content/activity.ts
  51. 52 0
      src/stores/content/article.ts
  52. 52 0
      src/stores/content/news.ts
  53. 52 0
      src/stores/content/notice.ts
  54. 11 0
      src/stores/counter.ts
  55. 52 0
      src/stores/customer/problem.ts
  56. 57 0
      src/stores/info/comment.ts
  57. 52 0
      src/stores/info/hotel.ts
  58. 52 0
      src/stores/info/location.ts
  59. 52 0
      src/stores/info/media.ts
  60. 52 0
      src/stores/info/room.ts
  61. 52 0
      src/stores/info/ticket.ts
  62. 52 0
      src/stores/order/afterSale.ts
  63. 52 0
      src/stores/order/order.ts
  64. 52 0
      src/stores/order/rateOrder.ts
  65. 52 0
      src/stores/problem/opinion.ts
  66. 6 0
      src/stores/user/mutations.ts
  67. 1 0
      src/stores/user/state.ts
  68. 63 0
      src/stores/users/admin.ts
  69. 57 0
      src/stores/users/chat.ts
  70. 57 0
      src/stores/users/user.ts
  71. 150 0
      src/util/axios-wrapper.ts
  72. 29 0
      src/util/types.util.ts
  73. 100 0
      src/views/acccount/information/index.vue
  74. 69 0
      src/views/acccount/updatepd/index.vue
  75. 41 0
      src/views/home/index.vue
  76. 170 0
      src/views/login/index.vue
  77. 196 0
      src/views/system/dict/index.vue
  78. 194 0
      src/views/system/dictData/index.vue
  79. 253 0
      src/views/system/menus/index.vue
  80. 114 0
      src/views/system/role/detail.vue
  81. 140 0
      src/views/system/role/index.vue
  82. 104 0
      src/views/user/admin/detail.vue
  83. 161 0
      src/views/user/admin/index.vue
  84. 258 0
      src/views/user/user/chat.vue
  85. 187 0
      src/views/user/user/index.vue
  86. 12 0
      tsconfig.app.json
  87. 44 0
      tsconfig.json
  88. 16 0
      tsconfig.node.json
  89. 35 0
      vite.config.ts

+ 7 - 0
.env.development

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

+ 7 - 0
.env.production

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

文件差异内容过多而无法显示
+ 10521 - 0
package-lock.json


+ 48 - 0
package.json

@@ -0,0 +1,48 @@
+{
+  "name": "follow_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"
+  }
+}

二进制
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;
+}

二进制
src/assets/bglogin.jpg


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

@@ -0,0 +1,51 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 4293191 */
+  src: url('iconfont.woff2?t=1697681511893') format('woff2'),
+       url('iconfont.woff?t=1697681511893') format('woff'),
+       url('iconfont.ttf?t=1697681511893') format('truetype');
+}
+
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-qunzu:before {
+  content: "\e656";
+}
+
+.icon-yonghuxinxi:before {
+  content: "\e680";
+}
+
+.icon-Detailsoftheproblem:before {
+  content: "\e612";
+}
+
+.icon-yonghu:before {
+  content: "\e614";
+}
+
+.icon-shanchu:before {
+  content: "\e68a";
+}
+
+.icon-dayuhao:before {
+  content: "\e625";
+}
+
+.icon-gengduo:before {
+  content: "\1017a";
+}
+
+.icon-erweima:before {
+  content: "\1017b";
+}
+
+.icon-shezhi:before {
+  content: "\1017c";
+}
+

文件差异内容过多而无法显示
+ 1 - 0
src/assets/icon/iconfont.js


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

@@ -0,0 +1,72 @@
+{
+  "id": "4293191",
+  "name": "随访",
+  "font_family": "iconfont",
+  "css_prefix_text": "icon-",
+  "description": "",
+  "glyphs": [
+    {
+      "icon_id": "657588",
+      "name": "群组",
+      "font_class": "qunzu",
+      "unicode": "e656",
+      "unicode_decimal": 58966
+    },
+    {
+      "icon_id": "2471408",
+      "name": "用户信息",
+      "font_class": "yonghuxinxi",
+      "unicode": "e680",
+      "unicode_decimal": 59008
+    },
+    {
+      "icon_id": "8577233",
+      "name": "Details of the problem",
+      "font_class": "Detailsoftheproblem",
+      "unicode": "e612",
+      "unicode_decimal": 58898
+    },
+    {
+      "icon_id": "18444826",
+      "name": "用户",
+      "font_class": "yonghu",
+      "unicode": "e614",
+      "unicode_decimal": 58900
+    },
+    {
+      "icon_id": "591862",
+      "name": "删除",
+      "font_class": "shanchu",
+      "unicode": "e68a",
+      "unicode_decimal": 59018
+    },
+    {
+      "icon_id": "4598257",
+      "name": "大于号",
+      "font_class": "dayuhao",
+      "unicode": "e625",
+      "unicode_decimal": 58917
+    },
+    {
+      "icon_id": "37684055",
+      "name": "更多",
+      "font_class": "gengduo",
+      "unicode": "1017a",
+      "unicode_decimal": 65914
+    },
+    {
+      "icon_id": "37684056",
+      "name": "二维码",
+      "font_class": "erweima",
+      "unicode": "1017b",
+      "unicode_decimal": 65915
+    },
+    {
+      "icon_id": "37684063",
+      "name": "设置",
+      "font_class": "shezhi",
+      "unicode": "1017c",
+      "unicode_decimal": 65916
+    }
+  ]
+}

二进制
src/assets/icon/iconfont.ttf


二进制
src/assets/icon/iconfont.woff


二进制
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;
+}

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

@@ -0,0 +1,69 @@
+<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' }">
+        <el-scrollbar>
+          <component :is="cAside"></component>
+        </el-scrollbar>
+      </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 0 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>

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

@@ -0,0 +1,85 @@
+<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>
+                <el-icon><Expand /></el-icon>
+                {{ 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>

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

@@ -0,0 +1,119 @@
+<!-- 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([]);
+onMounted(async () => {
+  user.value = store.state.user;
+});
+
+const getMenu = async () => {
+  let res: IQueryResult = await roleAxios.um();
+  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: 200px;
+}
+
+.sidebar>ul {
+  height: 100%;
+}
+
+.main {
+  .one {
+    .iconfont {
+      font-size: 18px;
+      margin: 0 5px 0 0;
+    }
+  }
+}
+
+.el-menu {
+  border-right: 0;
+}
+</style>

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

@@ -0,0 +1,28 @@
+<template>
+  <div id="breadcrumb">
+    <el-row>
+      <el-col :span="24" class="crumbs">
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item>
+            <el-icon>
+              <Expand />
+            </el-icon>
+            {{ 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>

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

@@ -0,0 +1,107 @@
+<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 }) => {
+  if (response.errcode !== 0) {
+    ElMessage({ type: 'error', message: '删除成功' });
+    return;
+  }
+  let arr: Ref<ListItem[]> = _.cloneDeep(list);
+  if (_.isArray(list.value)) {
+    arr.value.push({ ...response, url: `${import.meta.env.VITE_APP_HOST}${response.uri}` });
+  } else {
+    arr.value = [{ ...response, 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>

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

+ 14 - 0
src/layout/site.ts

@@ -0,0 +1,14 @@
+// 网站基本设置
+export const siteInfo = {
+  display: false,
+  zhTitle: '随访管理平台'
+};
+// 菜单设置
+export const menuInfo = {
+  info: {
+    display: false,
+    mode: 'horizontal',
+    backColor: '#242f42',
+    textColor: '#ffffff'
+  }
+};

+ 53 - 0
src/main.ts

@@ -0,0 +1,53 @@
+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.directive('scrollBottom', {
+  updated(el) {
+    // 这里的el即是绑定指令处的dom元素
+    el.scrollTo({
+      top: el.scrollHeight - el.clientHeight,
+      behavior: 'smooth'
+    });
+  }
+});
+
+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');

+ 87 - 0
src/router/index.ts

@@ -0,0 +1,87 @@
+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: '/system/role',
+          meta: { title: '角色管理' },
+          component: () => import('@/views/system/role/index.vue')
+        },
+        {
+          path: '/system/role/detail',
+          meta: { title: '角色信息管理' },
+          component: () => import('@/views/system/role/detail.vue')
+        },
+        {
+          path: '/system/menus',
+          meta: { title: '菜单管理' },
+          component: () => import('@/views/system/menus/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: '/acccount/information',
+          meta: { title: '账号信息' },
+          component: () => import('@/views/acccount/information/index.vue')
+        },
+        {
+          path: '/acccount/updatepd',
+          meta: { title: '修改密码' },
+          component: () => import('@/views/acccount/updatepd/index.vue')
+        }
+      ]
+    }
+  ]
+});
+router.beforeEach(async (to, from, next) => {
+  document.title = `${to.meta.title} `;
+  const token = localStorage.getItem('token');
+  if (to.name != 'login' && to.name != 'register') {
+    if (token) {
+      const res = await axios.request({
+        method: 'get',
+        url: '/follow/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;

+ 52 - 0
src/stores/basic/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: `/travel/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
+  };
+});

+ 52 - 0
src/stores/basic/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: `/travel/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/basic/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: `/travel/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/basic/map.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: `/travel/v1/api/map`
+};
+export const MapStore = defineStore('map', () => {
+  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/basic/menus.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: `/travel/v1/api/menus`
+};
+export const MenusStore = defineStore('menus', () => {
+  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/basic/module.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: `/travel/v1/api/module`
+};
+export const ModuleStore = defineStore('module', () => {
+  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/basic/region.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: `/travel/v1/api/region`
+};
+export const RegionStore = defineStore('region', () => {
+  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
+  };
+});

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

+ 52 - 0
src/stores/business/coupon.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: `/travel/v1/api/coupon`
+};
+export const CouponStore = defineStore('coupon', () => {
+  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/content/activity.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: `/travel/v1/api/activity`
+};
+export const ActivityStore = defineStore('activity', () => {
+  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/content/article.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: `/travel/v1/api/article`
+};
+export const ArticleStore = defineStore('article', () => {
+  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/content/news.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: `/travel/v1/api/news`
+};
+export const NewsStore = defineStore('news', () => {
+  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/content/notice.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: `/travel/v1/api/notice`
+};
+export const NoticeStore = defineStore('notice', () => {
+  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/customer/problem.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: `/travel/v1/api/problem`
+};
+export const ProblemStore = defineStore('problem', () => {
+  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/info/comment.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: `/travel/v1/api/comment`
+};
+export const CommentStore = defineStore('comment', () => {
+  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 exam = async (): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/exam`);
+    return res;
+  };
+  return {
+    count,
+    doubleCount,
+    increment,
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    exam
+  };
+});

+ 52 - 0
src/stores/info/hotel.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: `/travel/v1/api/hotel`
+};
+export const HotelStore = defineStore('hotel', () => {
+  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/info/location.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: `/travel/v1/api/location`
+};
+export const LocationStore = defineStore('location', () => {
+  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/info/media.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: `/travel/v1/api/media`
+};
+export const MediaStore = defineStore('media', () => {
+  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/info/room.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: `/travel/v1/api/room`
+};
+export const RoomStore = defineStore('room', () => {
+  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/info/ticket.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: `/travel/v1/api/ticket`
+};
+export const TicketStore = defineStore('ticket', () => {
+  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/order/afterSale.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: `/travel/v1/api/afterSale`
+};
+export const AfterSaleStore = defineStore('afterSale', () => {
+  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/order/order.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: `/travel/v1/api/order`
+};
+export const OrderStore = defineStore('order', () => {
+  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/order/rateOrder.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: `/travel/v1/api/rateOrder`
+};
+export const RateOrderStore = defineStore('rateOrder', () => {
+  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/problem/opinion.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: `/travel/v1/api/opinion`
+};
+export const OpinionStore = defineStore('opinion', () => {
+  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: `/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
+  };
+});

+ 57 - 0
src/stores/users/chat.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: `/travel/v1/api/chat`
+};
+export const ChatStore = defineStore('chat', () => {
+  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 read = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/read`, payload);
+    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,
+    read,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 57 - 0
src/stores/users/user.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: `/travel/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 user = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${api.url}/user`, payload);
+    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,
+    user,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

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

+ 100 - 0
src/views/acccount/information/index.vue

@@ -0,0 +1,100 @@
+<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">
+          <cForm :span="12" :fields="fields" :form="form" :rules="rules" @save="toSave" label-width="auto">
+            <template #role>
+              <el-option v-for="i in roleList" :key="i._id" :label="i.name" :value="i._id"></el-option>
+            </template>
+            <template #region>
+              <el-option v-for="i in regionList" :key="i._id" :label="i.name" :value="i._id"></el-option>
+            </template>
+          </cForm>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import store from '@/stores/counter';
+import type { Ref } from 'vue';
+import { ref, reactive, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import type { FormRules } from 'element-plus';
+// 接口
+import { AdminStore } from '@/stores/users/admin';
+import { RoleStore } from '@/stores/basic/role'; // 角色
+import { RegionStore } from '@/stores/basic/region'; // 区域
+import { DictDataStore } from '@/stores/basic/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const adminAxios = AdminStore();
+const dictAxios = DictDataStore();
+const roleAxios = RoleStore();
+const regionAxios = RegionStore();
+let user: Ref<any> = ref(store.state.user);
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '账号', model: 'account', options: { disabled: true } },
+  { label: '姓名', model: 'name' },
+  { label: '手机号', model: 'phone' },
+  { label: '角色', model: 'role', type: 'select' },
+  { label: '区域', model: 'region', type: 'select' }
+]);
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+  role: [{ required: true, message: '请输入角色', trigger: 'blur' }]
+});
+// 字典表
+const roleList: Ref<any> = ref([]);
+const regionList: Ref<any> = ref([]);
+const statusList: Ref<any> = ref([]);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  let id = user.value._id;
+  if (id) {
+    let res: IQueryResult = await adminAxios.fetch(id);
+    if (res.errcode == '0') form.value = res.data as {};
+  }
+};
+// 保存
+const toSave = async (data: any) => {
+  let res: IQueryResult;
+  if (data._id) res = await adminAxios.update(data);
+  else res = await adminAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 角色
+  res = await roleAxios.query({ is_use: '0' });
+  if (res.errcode == '0') roleList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'exam_status', is_use: '0' });
+  if (res.errcode == '0') statusList.value = res.data;
+  // 区域
+  res = await regionAxios.query({ is_use: '0' });
+  if (res.errcode == '0') regionList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

+ 69 - 0
src/views/acccount/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: any = 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>

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

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

@@ -0,0 +1,170 @@
+<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 class="one_1">
+            <span @click="toRegister()">立即注册</span>
+          </div>
+        </el-form>
+        <!--  底部  -->
+        <div class="el-login-footer">
+          <span> Copyright © 2018-2023 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 () => {});
+//接收验证码组件提交的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 toRegister = () => {
+  console.log(1);
+  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;
+        }
+      }
+    }
+    .one_1 {
+      float: right;
+      color: #409eff;
+    }
+
+    .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>

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

@@ -0,0 +1,196 @@
+<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>
+            <template #is_use="{ row }">
+              <el-switch
+                v-model="row.is_use"
+                inline-prompt
+                active-text="是"
+                inactive-text="否"
+                active-value="0"
+                inactive-value="1"
+                @click="handleChange(row)"
+              ></el-switch>
+            </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, ElMessageBox } from 'element-plus';
+import { useRouter } from 'vue-router';
+
+// 接口
+import { DictDataStore } from '@/stores/basic/dictData'; //
+import { DictTypeStore } from '@/stores/basic/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', custom: true },
+  { 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 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', is_use: '0' });
+  if (res.errcode == 0) is_useList.value = res.data;
+};
+// 修改是否启用
+const handleChange = (row: any) => {
+  const text = row.is_use === '0' ? '启用' : '停用';
+  ElMessageBox.confirm('确认要"' + text + '""' + row.type + '"吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      let res: IQueryResult;
+      if (row._id) res = await dictType.update(row);
+      if (res.errcode == 0) {
+        ElMessage({ type: `success`, message: `修改成功` });
+      }
+    })
+    .catch(() => {
+      row.is_use = row.is_use === '0' ? '1' : '0';
+    });
+};
+</script>
+<style lang="scss" scoped>
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

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

@@ -0,0 +1,194 @@
+<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">
+            <template #is_use="{ row }">
+              <el-switch
+                v-model="row.is_use"
+                inline-prompt
+                active-text="是"
+                inactive-text="否"
+                active-value="0"
+                inactive-value="1"
+                @click="handleChange(row)"
+              ></el-switch>
+            </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="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, ElMessageBox } from 'element-plus';
+import { useRoute, useRouter } from 'vue-router';
+// 接口
+import { DictDataStore } from '@/stores/basic/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', custom: true }
+]);
+// 操作
+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 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', is_use: '0' });
+  if (res.errcode == '0') is_useList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  router.push({ path: '/system/dict' });
+};
+// 修改是否启用
+const handleChange = (row: any) => {
+  const text = row.is_use === '0' ? '启用' : '停用';
+  ElMessageBox.confirm('确认要"' + text + '""' + row.label + '"吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      let res: IQueryResult;
+      if (row._id) res = await dictData.update(row);
+      if (res.errcode == 0) {
+        ElMessage({ type: `success`, message: `修改成功` });
+      }
+    })
+    .catch(() => {
+      row.is_use = row.is_use === '0' ? '1' : '0';
+    });
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 253 - 0
src/views/system/menus/index.vue

@@ -0,0 +1,253 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="one">
+        <el-button type="primary" @click="toAdd">添加</el-button>
+      </el-col>
+      <el-col :span="24">
+        <el-table :data="list" row-key="_id" border>
+          <el-table-column align="center" label="菜单名称" prop="name"></el-table-column>
+          <el-table-column align="center" label="父级菜单" prop="parent_name"></el-table-column>
+          <el-table-column align="center" label="图标" width="80">
+            <template #default="scope"><span :class="['iconfont', scope.row.icon]"></span></template>
+          </el-table-column>
+          <el-table-column align="center" label="顺序" sortable prop="order_num" width="80"></el-table-column>
+          <el-table-column align="center" label="路由地址" prop="path"></el-table-column>
+          <el-table-column align="center" label="组件地址" prop="component"></el-table-column>
+          <el-table-column align="center" label="菜单类型" prop="type">
+            <template #default="scope">{{ getDict('type', scope.row) }} </template>
+          </el-table-column>
+          <el-table-column align="center" label="是否启用" prop="is_use">
+            <template #default="scope">
+              <el-switch
+                v-model="scope.row.is_use"
+                inline-prompt
+                active-text="是"
+                inactive-text="否"
+                active-value="0"
+                inactive-value="1"
+                @click="handleChange(scope.row)"
+              ></el-switch>
+            </template>
+          </el-table-column>
+          <el-table-column align="center" label="备注" prop="remark"> </el-table-column>
+          <el-table-column align="center" label="操作">
+            <template #default="scope">
+              <el-link :underline="false" type="primary" size="mini" @click="toUpdate(scope.row)" style="margin-right: 10px">修改</el-link>
+              <el-link :underline="false" type="primary" size="mini" @click="toAddNext(scope.row)" style="margin-right: 10px">添加下一级</el-link>
+              <el-link :underline="false" type="danger" size="mini" @click="toDelete(scope.row)">删除</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-col>
+    </el-row>
+    <el-dialog v-model="dialog" title="菜单信息" :destroy-on-close="false" @close="toClose">
+      <el-tabs v-model="tab" type="border-card">
+        <el-tab-pane label="基本信息" name="basic">
+          <el-form label-position="left" label-width="120px">
+            <el-form-item label="菜单名称">
+              <el-input v-model="form.name" placeholder="请填写菜单名称"></el-input>
+            </el-form-item>
+            <el-form-item label="菜单类型">
+              <el-select v-model="form.type" placeholder="请选择菜单类型">
+                <el-option v-for="(i, index) in typeList" :key="`t${index}`" :label="i.label" :value="i.value"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="父级菜单">
+              <el-select v-model="form.parent_id" placeholder="" :disabled="false">
+                <el-option v-for="(i, index) in getOneDimensionList()" :key="`m${index}`" :label="i.name" :value="i._id"></el-option>
+              </el-select>
+            </el-form-item>
+            <template v-if="form.type === '1' || form.type === '2'">
+              <el-form-item label="路由地址">
+                <el-input v-model="form.path" placeholder="请填写路由地址"></el-input>
+              </el-form-item>
+              <el-form-item label="组件地址">
+                <el-input v-model="form.component" placeholder="请填写组件地址"></el-input>
+              </el-form-item>
+            </template>
+            <el-form-item label="顺序">
+              <el-input-number v-model="form.order_num"></el-input-number>
+            </el-form-item>
+            <el-form-item label="图标">
+              <el-select v-model="form.icon" clearable filterable placeholder="请选择图标">
+                <el-option v-for="item in iconList" :key="item.label" :label="item.label" :value="item.label">
+                  <span style="float: left" :class="['iconfont', item.label]"></span>
+                  <span style="float: right; color: #8492a6; font-size: 13px">{{ item.label }}</span>
+                </el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="是否启用">
+              <el-select v-model="form.is_use" clearable filterable placeholder="请选择">
+                <el-option v-for="item in is_useList" :key="item.value" :label="item.label" :value="item.value">
+                  {{ item.label }}
+                </el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="备注">
+              <el-input v-model="form.remark" placeholder="请输入备注" type="textarea" :autosize="{ minRows: 5, maxRows: 5 }"></el-input>
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+      <el-row type="flex" justify="space-around" style="margin-top: 10px">
+        <el-col :span="6">
+          <el-button @click="toSave(form)" size="small" type="primary">保存</el-button>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import _ from 'lodash';
+import type { Ref } from 'vue';
+import { onMounted, ref } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+// 接口
+import { MenusStore } from '@/stores/basic/menus'; // 菜单
+import { DictDataStore } from '@/stores/basic/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const menusAxios = MenusStore();
+const dictAxios = DictDataStore();
+
+let list: Ref<any> = ref([]);
+// 表单
+let form: Ref<any> = ref({});
+const tab: Ref<any> = ref('basic');
+// 字典表
+const is_useList: Ref<any> = ref([]);
+const iconList: Ref<any> = ref([]);
+const typeList: Ref<any> = ref([]);
+// 加载中
+const loading: Ref<any> = ref(false);
+// 弹框
+const dialog: Ref<any> = ref(false);
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search();
+  loading.value = false;
+});
+const search = async () => {
+  const res: IQueryResult = await menusAxios.query();
+  if (res.errcode == '0') list.value = res.data;
+};
+const getDict = (model: string, row: any) => {
+  let word = '';
+  const r = typeList.value.find((f) => f.value === row.type);
+  if (r) word = r.label;
+  return word;
+};
+// 添加
+const toAdd = () => {
+  dialog.value = true;
+};
+// 修改
+const toUpdate = (row: any) => {
+  form.value = _.cloneDeep(row);
+  dialog.value = true;
+};
+// 添加下一级
+const toAddNext = (row: any) => {
+  form.value = { parent_id: row._id };
+  dialog.value = true;
+};
+// 保存
+const toSave = async (data: any) => {
+  let res: IQueryResult;
+  data = _.omit(data, ['children', 'parent_name']); // 过滤掉属性为children的项
+  if (data._id) res = await menusAxios.update(data);
+  else res = await menusAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toClose();
+  }
+};
+// 删除
+const toDelete = (data: any) => {
+  ElMessageBox.confirm('删除该菜单吗', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      let res: IQueryResult = await menusAxios.del(data._id);
+      if (res.errcode == 0) {
+        ElMessage({ type: `success`, message: `删除成功` });
+        search();
+      }
+    })
+    .catch(() => {});
+};
+// 获取一维(平面)的菜单数据
+const getOneDimensionList = () => {
+  let dup = _.cloneDeep(list.value);
+  let arr = getAllChild(dup);
+  return arr;
+};
+// 获取所有的子菜单
+const getAllChild = (children: any) => {
+  let arr = [];
+  for (const i of children) {
+    const { children, ...others } = i;
+    arr.push(others);
+    if (children) {
+      const marr = getAllChild(children);
+      arr.push(...marr);
+    }
+  }
+  return arr;
+};
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  tab.value = 'basic';
+  dialog.value = false;
+  search();
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 是否启用
+  res = await dictAxios.query({ type: 'is_use', is_use: '0' });
+  if (res.errcode == '0') is_useList.value = res.data;
+  // 图标
+  res = await dictAxios.query({ type: 'info_icon', is_use: '0' });
+  if (res.errcode == '0') iconList.value = res.data;
+  // 菜单类型
+  res = await dictAxios.query({ type: 'menus_type', is_use: '0' });
+  if (res.errcode == '0') typeList.value = res.data;
+};
+// 修改是否启用
+const handleChange = (row: any) => {
+  const text = row.is_use === '0' ? '启用' : '停用';
+  ElMessageBox.confirm('确认要"' + text + '""' + row.name + '"吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      let res: IQueryResult;
+      if (row._id) res = await menusAxios.update(row);
+      if (res.errcode == 0) {
+        ElMessage({ type: `success`, message: `修改成功` });
+      }
+    })
+    .catch(() => {
+      row.is_use = row.is_use === '0' ? '1' : '0';
+    });
+};
+</script>
+<style scoped lang="scss">
+.main {
+  padding: 2px;
+
+  .one {
+    padding: 20px 0;
+  }
+}
+</style>

+ 114 - 0
src/views/system/role/detail.vue

@@ -0,0 +1,114 @@
+<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 #is_use>
+              <el-option v-for="i in is_useList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+            <template #menu>
+              <el-cascader
+                v-model="form.menu"
+                :options="menuList"
+                :props="{ multiple: true, value: '_id', label: 'name', children: 'children' }"
+                clearable
+                collapse-tags
+                style="width: 100%"
+              ></el-cascader>
+            </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 { RoleStore } from '@/stores/basic/role'; // 角色
+import { DictDataStore } from '@/stores/basic/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const roleAxios = RoleStore();
+const dictAxios = DictDataStore();
+// 路由
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({ menu: [] });
+let fields: Ref<any[]> = ref([
+  { label: '角色名称', model: 'name' },
+  { label: '角色编码', model: 'code' },
+  { label: '简介', model: 'brief', type: 'textarea' },
+  { label: '是否启用', model: 'is_use', type: 'select' },
+  { label: '菜单', model: 'menu', custom: true }
+]);
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
+  code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
+  is_use: [{ required: true, message: '请选择是否启用', trigger: 'blur' }]
+});
+// 字典表
+const is_useList: Ref<any> = ref([]);
+const menuList: 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 roleAxios.fetch(id);
+    if (res.errcode == '0') {
+      let info: any = res.data as {};
+      form.value = info;
+    }
+  }
+};
+// 保存
+const toSave = async (data: any) => {
+  let res: IQueryResult;
+  if (data._id) res = await roleAxios.update(data);
+  else res = await roleAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 是否启用
+  res = await dictAxios.query({ type: 'is_use', is_use: '0' });
+  if (res.errcode == '0') is_useList.value = res.data;
+  // 查询菜单
+  res = await roleAxios.am();
+  if (res.errcode == '0') menuList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    .two_1 {
+      margin: 5px 0;
+    }
+  }
+}
+</style>

+ 140 - 0
src/views/system/role/index.vue

@@ -0,0 +1,140 @@
+<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 #is_use="{ row }">
+              <el-switch
+                v-model="row.is_use"
+                inline-prompt
+                active-text="是"
+                inactive-text="否"
+                active-value="0"
+                inactive-value="1"
+                @click="handleChange(row)"
+              ></el-switch>
+            </template>
+          </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, ElMessageBox } from 'element-plus';
+import { useRouter } from 'vue-router';
+// 接口
+import { RoleStore } from '@/stores/basic/role'; // 角色
+import { DictDataStore } from '@/stores/basic/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const roleAxios = RoleStore();
+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: 'code', isSearch: true },
+  { label: '是否启用', model: 'is_use', custom: true }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const 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 info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await roleAxios.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 toAdd = async () => {
+  router.push({ path: '/system/role/detail' });
+};
+// 修改
+const toEdit = async (data: any) => {
+  router.push({ path: '/system/role/detail', query: { id: data._id } });
+};
+// 删除
+const toDel = async (data: any) => {
+  let res: IQueryResult = await roleAxios.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: 'is_use', is_use: '0' });
+  if (res.errcode == '0') is_useList.value = res.data;
+};
+// 修改是否启用
+const handleChange = (row: any) => {
+  const text = row.is_use === '0' ? '启用' : '停用';
+  ElMessageBox.confirm('确认要"' + text + '""' + row.name + '"吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(async () => {
+      let res: IQueryResult;
+      if (row._id) res = await roleAxios.update(row);
+      if (res.errcode == 0) {
+        ElMessage({ type: `success`, message: `修改成功` });
+      }
+    })
+    .catch(() => {
+      row.is_use = row.is_use === '0' ? '1' : '0';
+    });
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+
+.dialog_one {
+  .one {
+    margin: 5px 0;
+  }
+}
+</style>

+ 104 - 0
src/views/user/admin/detail.vue

@@ -0,0 +1,104 @@
+<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 #role>
+              <el-option v-for="i in roleList" :key="i._id" :label="i.name" :value="i._id"></el-option>
+            </template>
+            <template #region>
+              <el-option v-for="i in regionList" :key="i._id" :label="i.name" :value="i._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 { ElMessage } from 'element-plus';
+import type { FormRules } from 'element-plus';
+import { useRoute } from 'vue-router';
+// 接口
+import { AdminStore } from '@/stores/users/admin';
+import { RoleStore } from '@/stores/basic/role'; // 角色
+import { RegionStore } from '@/stores/basic/region'; // 区域
+import { DictDataStore } from '@/stores/basic/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const adminAxios = AdminStore();
+const dictAxios = DictDataStore();
+const roleAxios = RoleStore();
+const regionAxios = RegionStore();
+// 路由
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+// 表单
+let form: Ref<any> = ref({});
+let fields: Ref<any[]> = ref([
+  { label: '账号', model: 'account', options: { disabled: true } },
+  { label: '姓名', model: 'name' },
+  { label: '手机号', model: 'phone' },
+  { label: '角色', model: 'role', type: 'select' },
+  { label: '区域', model: 'region', type: 'select' }
+]);
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+  phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+  role: [{ required: true, message: '请输入角色', trigger: 'blur' }]
+});
+// 字典表
+const roleList: Ref<any> = ref([]);
+const regionList: Ref<any> = ref([]);
+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 adminAxios.fetch(id);
+    if (res.errcode == '0') form.value = res.data as {};
+  }
+};
+// 保存
+const toSave = async (data: any) => {
+  let res: IQueryResult;
+  if (data._id) res = await adminAxios.update(data);
+  else res = await adminAxios.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toBack();
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let res: IQueryResult;
+  // 角色
+  res = await roleAxios.query({ is_use: '0' });
+  if (res.errcode == '0') roleList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'exam_status', is_use: '0' });
+  if (res.errcode == '0') statusList.value = res.data;
+  // 区域
+  res = await regionAxios.query({ is_use: '0' });
+  if (res.errcode == '0') regionList.value = res.data;
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss"></style>

+ 161 - 0
src/views/user/admin/index.vue

@@ -0,0 +1,161 @@
+<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 in statusList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </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 { AdminStore } from '@/stores/users/admin';
+import { RoleStore } from '@/stores/basic/role'; // 角色
+import { DictDataStore } from '@/stores/basic/dictData'; // 字典表
+import type { IQueryResult } from '@/util/types.util';
+const adminAxios = AdminStore();
+const dictAxios = DictDataStore();
+const roleAxios = RoleStore();
+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: 'account', isSearch: true },
+  { label: '昵称', model: 'name', isSearch: true },
+  { label: '手机号', model: 'phone', isSearch: true },
+  { label: '角色', model: 'role', format: (i: any) => getDict(i, roleList.value, 'role') },
+  { label: '状态', model: 'status', format: (i: any) => getDict(i, statusList.value, 'status'), type: 'select', isSearch: true }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '审核', method: 'exam', type: 'warning', display: (i: any) => i.status == '0' },
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const roleList: 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 };
+  const res: IQueryResult = await adminAxios.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 = (e: any, model: any, type: any) => {
+  if (type == 'role') {
+    let data: any = model.find((i: any) => i._id == e);
+    if (data) return data.name;
+    else return '暂无';
+  } else if (type == 'status') {
+    let data: any = model.find((i: any) => i.value == e);
+    if (data) return data.label;
+    else return '暂无';
+  }
+};
+// 审核
+const toExam = async (data: any) => {
+  let res: IQueryResult = await adminAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    dialog.value = { title: '审核管理', show: true, type: '1' };
+  }
+};
+// 提交保存
+const toSave = async (data: any) => {
+  let res: IQueryResult = await adminAxios.update(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: '信息审核成功', type: 'success' });
+    toClose();
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+// 修改
+const toEdit = (data: any) => {
+  router.push({ path: '/admin/detail', query: { id: data._id } });
+};
+// 删除
+const toDel = async (data: any) => {
+  let res: IQueryResult = await adminAxios.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 roleAxios.query({ is_use: '0' });
+  if (res.errcode == '0') roleList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'exam_status', is_use: '0' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 258 - 0
src/views/user/user/chat.vue

@@ -0,0 +1,258 @@
+<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="14" class="two">
+          <div style="overflow: auto" v-scrollBottom>
+            <ul v-infinite-scroll="load" class="infinite-list">
+              <el-col :span="24" class="list" v-for="item in list" :key="item._id">
+                <el-col :span="24" class="two_1" v-if="item.speaker == user._id">
+                  <el-col class="time" v-if="item.time != ''">{{ item.time }}</el-col>
+                  <el-col :span="24" class="content">
+                    <el-col :span="2" class="logo">
+                      <el-image
+                        v-if="user && user.logo.length != 0"
+                        :src="user.logo[0].url || '../../assets/bglogin.jpg'"
+                        style="width: 60px; height: 60px; border-radius: 60px"
+                        mode="widthFix"
+                      ></el-image>
+                    </el-col>
+                    <el-col :span="8" v-if="item.msg_type == '0'">
+                      <text class="text">{{ item.content }}</text>
+                    </el-col>
+                    <el-col :span="8" class="image" v-else>
+                      <el-image :src="item.content" style="width: 15vw" mode="widthFix"></el-image>
+                    </el-col>
+                  </el-col>
+                </el-col>
+                <el-col :span="24" class="two_2" v-else>
+                  <el-col class="time" v-if="item.time != ''">{{ item.time }}</el-col>
+                  <el-col :span="24" class="content">
+                    <el-col :span="8" v-if="item.msg_type == '0'">
+                      <text class="text">{{ item.content }}</text>
+                    </el-col>
+                    <el-col :span="8" class="image" v-else>
+                      <el-image :src="item.content" style="width: 15vw" mode="widthFix"></el-image>
+                    </el-col>
+                    <el-col :span="2" class="logo">
+                      <el-image
+                        v-if="config && config.file.length != 0"
+                        :src="config.file[0].url"
+                        style="width: 60px; height: 60px; border-radius: 60px"
+                        mode="widthFix"
+                      ></el-image>
+                    </el-col>
+                  </el-col>
+                </el-col>
+              </el-col>
+            </ul>
+          </div>
+        </el-col>
+        <el-col :span="14" class="bottom">
+          <el-col :span="20" class="input">
+            <el-input v-model="input" type="textarea" placeholder="请输入" />
+          </el-col>
+          <el-col :span="4" class="button">
+            <el-button type="primary" @click="toSend">发送</el-button>
+            <cUpload class="upload" :limit="1" accept="*" url="/files/travel/chat/upload" :list="file" @change="onUpload"> </cUpload>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script setup lang="ts">
+// 基础
+import moment from 'moment';
+import type { Ref } from 'vue';
+import { onMounted, ref, getCurrentInstance } from 'vue';
+import { useRoute } from 'vue-router';
+import store from '@/stores/counter';
+import { ElMessage } from 'element-plus';
+// 接口
+import { ChatStore } from '@/stores/users/chat';
+import { ConfigStore } from '@/stores/basic/config';
+import { UserStore } from '@/stores/users/user';
+import type { IQueryResult } from '@/util/types.util';
+const { proxy } = getCurrentInstance() as any;
+
+const chatAxios = ChatStore();
+const configAxios = ConfigStore();
+const userAxios = UserStore();
+let admin: Ref<any> = ref(store.state.user);
+// 路由
+const route = useRoute();
+// 加载中
+const loading: Ref<any> = ref(false);
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let page = 0;
+let limit: number = proxy.$limit;
+const user: Ref<any> = ref({ logo: [] });
+const config: Ref<any> = ref({ file: [] });
+const file: Ref<any> = ref([]);
+const input: 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 }) => {
+  if (user.value._id) {
+    const info = { skip: e.skip, limit: e.limit, user: user.value._id };
+    const res: any = await chatAxios.query(info);
+    if (res.errcode == '0') {
+      list.value = res.data.reverse();
+      total.value = res.total;
+    }
+  }
+};
+// 查询其他信息
+const searchOther = async () => {
+  let id = route.query.id;
+  let res: IQueryResult;
+  if (id) {
+    res = await userAxios.fetch(id);
+    if (res.errcode == '0') user.value = res.data;
+    // 全部已读
+    await chatAxios.read({ user: id });
+  }
+  res = await configAxios.query();
+  if (res.errcode == '0') {
+    config.value = res.data;
+  }
+};
+// 发送
+const toSend = async () => {
+  let res: IQueryResult;
+  let form: any = { user: user.value._id, speaker: admin.value._id, content: input.value, msg_type: '0', time: moment().format('YYYY-MM-DD HH:mm:ss') };
+  // 发送给服务器消息
+  res = await chatAxios.create(form);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `发送消息信息成功` });
+    input.value = '';
+    search({ skip, limit });
+  }
+};
+// 上传
+const onUpload = async (e: { value: Array<any> }) => {
+  const { value } = e;
+  let res: IQueryResult;
+  let form: any = { user: user.value._id, speaker: admin.value._id, content: value[0].url, msg_type: '1', time: moment().format('YYYY-MM-DD HH:mm:ss') };
+  // 发送给服务器消息
+  res = await chatAxios.create(form);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `发送消息信息成功` });
+    file.value = [];
+    search({ skip, limit });
+  }
+};
+// 下拉查看聊天记录
+const load = () => {
+  if (total.value != 0) {
+    if (total.value > limit) {
+      page = page + 1;
+      limit = limit * page;
+      search({ skip, limit });
+    } else ElMessage({ type: `warning`, message: `已经到底了!` });
+  }
+};
+// 返回上一页
+const toBack = () => {
+  window.history.go(-1);
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    padding: 20px;
+    border: 1px solid #d5d5da;
+    background: #f1f1f1;
+    border-radius: 5px;
+
+    .infinite-list {
+      height: 550px;
+      padding: 0;
+      margin: 0;
+      list-style: none;
+    }
+
+    .list {
+      display: flex;
+      flex-direction: column;
+
+      .two_1 {
+        .time {
+          text-align: center;
+          color: #858585;
+          font-size: 12px;
+        }
+
+        .content {
+          display: flex;
+          align-items: center;
+
+          .logo {
+            height: 60px;
+          }
+
+          .text {
+            padding: 12px;
+            background: #ffffff;
+            border-radius: 0px 12px 12px 12px;
+          }
+        }
+      }
+
+      .two_2 {
+        text-align: right;
+
+        .time {
+          text-align: center;
+          color: #858585;
+          font-size: 12px;
+        }
+
+        .content {
+          display: flex;
+          align-items: center;
+          justify-content: right;
+
+          .logo {
+            height: 60px;
+          }
+
+          .text {
+            padding: 12px;
+            border-radius: 12px 0px 12px 12px;
+            background: #16f80f;
+          }
+        }
+      }
+    }
+  }
+
+  .bottom {
+    display: flex;
+    align-items: center;
+    padding: 5px 0 0 0;
+
+    .button {
+      display: flex;
+      margin: 10px 0 0 5px;
+
+      .upload {
+        text-align: right;
+        padding: 0 0 0 5px;
+      }
+    }
+  }
+}
+</style>

+ 187 - 0
src/views/user/user/index.vue

@@ -0,0 +1,187 @@
+<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 #type>
+              <el-option v-for="i in typeList" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            </template>
+          </cSearch>
+        </el-col>
+        <el-col :span="24" class="two">
+          <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @chat="toChat" @edit="toEdit" @del="toDel">
+            <template #is_read="{ item, row }">
+              <template v-if="item.model === 'is_read'">
+                <span :class="[row.is_read == '未读' ? 'text' : '']">{{ row.is_read }}</span>
+              </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="{}" @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 #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/basic/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: 'name', isSearch: true },
+  { label: '昵称', model: 'nick_name', isSearch: true },
+  { label: '性别', model: 'gender', format: (i: any) => getDict(i, genderList.value) },
+  { label: '手机号', model: 'phone', isSearch: true },
+  { label: '生日', model: 'birthday' },
+  { label: '类型', model: 'type', format: (i: any) => getDict(i, typeList.value), isSearch: true },
+  { label: '所在城市', model: 'city', isSearch: true },
+  { label: '是否已读', model: 'is_read', custom: true }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' },
+  { label: '聊天记录', method: 'chat' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+// 字典表
+const genderList: Ref<any> = ref([]);
+const statusList: Ref<any> = ref([]);
+const typeList: 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: 'openid', options: { disabled: true } },
+  { label: '真实姓名', model: 'name' },
+  { label: '昵称', model: 'nick_name' },
+  { label: '性别', model: 'gender', type: 'select' },
+  { label: '手机号', model: 'phone' },
+  { label: '生日', model: 'birthday', type: 'date' },
+  { label: '类型', model: 'type', type: 'select' },
+  { label: '所在城市', model: 'city' },
+  { 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 };
+  const res: IQueryResult = await userAxios.user(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 = (e: any, model: any) => {
+  let data: any = model.find((i: any) => i.value == e);
+  if (data) return data.label;
+  else return '暂无';
+};
+// 提交保存
+const toSave = async (data: any) => {
+  delete data.is_read;
+  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 = async (data: any) => {
+  let res: IQueryResult = await userAxios.fetch(data._id);
+  if (res.errcode == '0') {
+    form.value = res.data;
+    dialog.value = { title: '信息管理', show: true, type: '1' };
+  }
+};
+// 聊天记录
+const toChat = async (data: any) => {
+  router.push({ path: '/user/chat', 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', is_use: '0' });
+  if (res.errcode == '0') genderList.value = res.data;
+  // 类型
+  res = await dictAxios.query({ type: 'user_type', is_use: '0' });
+  if (res.errcode == '0') typeList.value = res.data;
+  // 状态
+  res = await dictAxios.query({ type: 'exam_status', is_use: '0' });
+  if (res.errcode == '0') statusList.value = res.data;
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+    .text {
+      color: #f10000;
+    }
+  }
+}
+</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
+        },
+        '/follow/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))
+      }
+    }
+  };
+});