lrf 1 year ago
commit
23ed671365
95 changed files with 17659 additions and 0 deletions
  1. 7 0
      .env.development
  2. 8 0
      .env.production
  3. 28 0
      .eslintrc.cjs
  4. 30 0
      .gitignore
  5. 8 0
      .prettierrc.json
  6. 3 0
      .vscode/extensions.json
  7. 1 0
      README.md
  8. 5 0
      env.d.ts
  9. 16 0
      index.html
  10. 8644 0
      package-lock.json
  11. 47 0
      package.json
  12. BIN
      public/favicon.ico
  13. 26 0
      src/App.vue
  14. 0 0
      src/assets/base.css
  15. BIN
      src/assets/bglogin.jpg
  16. 95 0
      src/assets/icon/iconfont.css
  17. 1 0
      src/assets/icon/iconfont.js
  18. 149 0
      src/assets/icon/iconfont.json
  19. BIN
      src/assets/icon/iconfont.ttf
  20. BIN
      src/assets/icon/iconfont.woff
  21. BIN
      src/assets/icon/iconfont.woff2
  22. BIN
      src/assets/logo.png
  23. 1 0
      src/assets/logo.svg
  24. 27 0
      src/assets/main.css
  25. 2 0
      src/assets/style/mixin.scss
  26. 61 0
      src/components/admin-frame/home.vue
  27. 75 0
      src/components/admin-frame/parts/Header.vue
  28. 113 0
      src/components/admin-frame/parts/Sidebar.vue
  29. 23 0
      src/components/admin-frame/parts/breadcrumb.vue
  30. 26 0
      src/components/frame/c-button.vue
  31. 39 0
      src/components/frame/c-dialog.vue
  32. 238 0
      src/components/frame/c-form.vue
  33. 53 0
      src/components/frame/c-pages.vue
  34. 174 0
      src/components/frame/c-search.vue
  35. 215 0
      src/components/frame/c-table.vue
  36. 108 0
      src/components/frame/c-upload.vue
  37. 69 0
      src/components/frame/wang-editor.vue
  38. 15 0
      src/components/index.ts
  39. 59 0
      src/layout/site.ts
  40. 42 0
      src/main.ts
  41. 109 0
      src/router/index.ts
  42. 11 0
      src/stores/counter.ts
  43. 26 0
      src/stores/login.ts
  44. 43 0
      src/stores/shop.ts
  45. 43 0
      src/stores/system/brand.ts
  46. 43 0
      src/stores/system/dictData.ts
  47. 43 0
      src/stores/system/dictType.ts
  48. 41 0
      src/stores/system/menus.ts
  49. 41 0
      src/stores/system/role.ts
  50. 6 0
      src/stores/user/mutations.ts
  51. 1 0
      src/stores/user/state.ts
  52. 49 0
      src/stores/users/admin.ts
  53. 49 0
      src/stores/users/user.ts
  54. 150 0
      src/util/axios-wrapper.ts
  55. 19 0
      src/util/checkResult.ts
  56. 15 0
      src/util/provideApp.ts
  57. 29 0
      src/util/types.util.ts
  58. 1 0
      src/util/usualFunction.ts
  59. 67 0
      src/views/acccount/updatepd/index.vue
  60. 310 0
      src/views/core/achieve/admin.vue
  61. 309 0
      src/views/core/achieve/index.vue
  62. 256 0
      src/views/core/demand/admin.vue
  63. 253 0
      src/views/core/demand/index.vue
  64. 285 0
      src/views/core/service/admin.vue
  65. 285 0
      src/views/core/service/index.vue
  66. 306 0
      src/views/core/trade/auction/admin.vue
  67. 82 0
      src/views/core/trade/auction/makeListing.vue
  68. 308 0
      src/views/core/trade/listing/admin.vue
  69. 205 0
      src/views/core/trade/listing/index.vue
  70. 333 0
      src/views/core/trade/negotiate/admin.vue
  71. 226 0
      src/views/core/trade/negotiate/index.vue
  72. 232 0
      src/views/home/index.vue
  73. 53 0
      src/views/login/index.ts
  74. 108 0
      src/views/login/index.vue
  75. 196 0
      src/views/service/linkItem/index.vue
  76. 195 0
      src/views/service/notice/index.vue
  77. 200 0
      src/views/service/policy/index.vue
  78. 200 0
      src/views/service/trends/index.vue
  79. 195 0
      src/views/service/video/admin.vue
  80. 0 0
      src/views/service/video/index.vue
  81. 202 0
      src/views/shop/index.vue
  82. 207 0
      src/views/system/brand/index.vue
  83. 165 0
      src/views/system/dict/index.vue
  84. 161 0
      src/views/system/dictData/index.vue
  85. 241 0
      src/views/system/menus/index.vue
  86. 182 0
      src/views/system/role/form-1.vue
  87. 125 0
      src/views/system/role/index.vue
  88. 7 0
      src/views/system/role/interface.ts
  89. 171 0
      src/views/user/admin/index.vue
  90. 187 0
      src/views/user/admin/options.vue
  91. 172 0
      src/views/user/user/index.vue
  92. 12 0
      tsconfig.app.json
  93. 44 0
      tsconfig.json
  94. 16 0
      tsconfig.node.json
  95. 36 0
      vite.config.ts

+ 7 - 0
.env.development

@@ -0,0 +1,7 @@
+VITE_BASE_URL = "/usedCarAdmin"
+VITE_OUT_DIR = "usedCarAdmin"
+VITE_REQUEST_BASE = '/usedCar/api'
+VITE_APP_HOST="http://localhost:8001"
+VITE_APP_PAGE_SIZE=15
+VITE_APP_ROUTER="admin"
+VITE_APP_ENV = 'development'

+ 8 - 0
.env.production

@@ -0,0 +1,8 @@
+VITE_BASE_URL = "/zdlyjszyAdmin"
+VITE_OUT_DIR = "zdlyjszyAdmin"
+VITE_REQUEST_BASE = '/zdlyjszy/api'
+VITE_APP_HOST="https://broadcast.waityou24.cn"
+VITE_APP_PAGE_SIZE=15
+VITE_APP_ROUTER="admin"
+VITE_APP_ENV = 'production'
+VITE_API_BASE='/zdlyjszy/api'

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

+ 30 - 0
.gitignore

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

+ 8 - 0
.prettierrc.json

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

+ 3 - 0
.vscode/extensions.json

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

+ 1 - 0
README.md

@@ -0,0 +1 @@
+# schfriend_admin

+ 5 - 0
env.d.ts

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

+ 16 - 0
index.html

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

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


+ 47 - 0
package.json

@@ -0,0 +1,47 @@
+{
+  "name": "schfriend_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.4.0",
+    "echarts": "^5.4.2",
+    "element-plus": "^2.3.4",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.4",
+    "naf-core": "^0.1.2",
+    "pinia": "^2.0.36",
+    "vue": "^3.3.2",
+    "vue-router": "^4.2.0",
+    "vuex": "^4.1.0"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.2.0",
+    "@types/node": "^18.14.2",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vue/eslint-config-prettier": "^7.1.0",
+    "@vue/eslint-config-typescript": "^11.0.2",
+    "@vue/tsconfig": "^0.1.3",
+    "eslint": "^8.34.0",
+    "eslint-plugin-vue": "^9.9.0",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.8.4",
+    "sass": "^1.62.1",
+    "sass-loader": "^13.2.2",
+    "typescript": "~4.8.4",
+    "vite": "^4.1.4",
+    "vue-tsc": "^1.2.0"
+  }
+}

BIN
public/favicon.ico


+ 26 - 0
src/App.vue

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

+ 0 - 0
src/assets/base.css


BIN
src/assets/bglogin.jpg


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

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

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


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

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

BIN
src/assets/icon/iconfont.ttf


BIN
src/assets/icon/iconfont.woff


BIN
src/assets/icon/iconfont.woff2


BIN
src/assets/logo.png


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

+ 27 - 0
src/assets/main.css

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

+ 2 - 0
src/assets/style/mixin.scss

@@ -0,0 +1,2 @@
+$red:#66363c;
+$white:#f2ebd9;

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

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

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

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

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

@@ -0,0 +1,113 @@
+<!-- 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">
+                    <!-- TODO:这里有问题需要改成自引用输出方式.实现无线嵌套.目前只是最多三级 -->
+                    <template v-if="subItem.type === '0'">
+                      <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>
+                    </template>
+                    <el-menu-item v-else-if="subItem.type === '1'" :index="subItem.path" :key="subItem.path">
+                      <i :class="['iconfont', subItem.icon]"></i>
+                      <span>{{ subItem.name }}</span>
+                    </el-menu-item>
+                  </template>
+                </el-sub-menu>
+              </template>
+              <template v-else>
+                <el-menu-item :index="item.path" :key="item.path">
+                  <i :class="['iconfont', item.icon]"></i>
+                  <span>{{ item.name }}</span>
+                </el-menu-item>
+              </template>
+            </template>
+          </el-menu>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { menuInfo } from '@/layout/site';
+import { ElMessage } from 'element-plus';
+import store from '@/stores/counter';
+import type { Ref } from 'vue';
+import { ref, onMounted, watch } from 'vue';
+import { useRoute } from 'vue-router';
+
+const route = useRoute();
+
+let onRoutes = route.path;
+let user: Ref<any> = ref({});
+const styleInfo: Ref<any> = ref(menuInfo.info);
+let items: Ref<any> = ref(menuInfo.menuList);
+onMounted(async () => {
+  user.value = store.state.user;
+});
+
+const getMenu = async () => {
+  const menus = user.value.menus;
+  const newMenus = [...menus];
+  items.value = newMenus;
+};
+
+watch(
+  user,
+  (newVal) => {
+    if (newVal && newVal._id) {
+      if (newVal && newVal._id) getMenu();
+      else ElMessage({ message: `暂无用户信息,无法获取菜单信息`, type: 'error' });
+    }
+  },
+  {
+    deep: true
+  }
+);
+</script>
+<style scoped lang="scss">
+.sidebar::-webkit-scrollbar {
+  width: 0;
+}
+.sidebar-el-menu:not(.el-menu--collapse) {
+  width: 201px;
+}
+.sidebar > ul {
+  height: 100%;
+}
+.main {
+  .one {
+    .iconfont {
+      font-size: 18px;
+      margin: 0 5px 0 0;
+    }
+  }
+}
+</style>

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

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

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

@@ -0,0 +1,26 @@
+<template>
+  <div id="c-button" style="text-align: right">
+    <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></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>

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

@@ -0,0 +1,39 @@
+<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"
+      destroy-on-close
+    >
+      <el-col :span="24" class="dialogInfo" :style="{ 'max-height': height }">
+        <slot></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: '600px' }
+});
+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>

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

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

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

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

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

@@ -0,0 +1,215 @@
+<template>
+  <el-table :data="list" border stripe @selection-change="toSelect" height="60vh">
+    <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="200" 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 _, { cloneDeep } 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, cloneDeep(data), index);
+      })
+      .catch(() => {});
+  } else emit(method, cloneDeep(data), index);
+};
+// 分页
+const changePage = (page: number = currentPage.value) => {
+  emit('query', { skip: (page - 1) * limit, limit: limit });
+};
+const sizeChange = (limits: number) => {
+  limit = limits;
+  currentPage.value = 1;
+  emit('query', { skip: 0, limit: limit });
+};
+</script>
+<style scoped lang="scss">
+.link {
+  padding: 0 5px 0 0;
+}
+.page {
+  margin: 10px 0 0 0;
+}
+</style>

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

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

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

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

+ 15 - 0
src/components/index.ts

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

+ 59 - 0
src/layout/site.ts

@@ -0,0 +1,59 @@
+// 网站基本设置
+export const siteInfo = {
+  display: false,
+  zhTitle: '吉林省重点领域技术转移平台',
+  logo_url: '/src/assets/logo.png'
+};
+// 菜单设置
+export const menuInfo = {
+  info: {
+    display: false,
+    mode: 'horizontal',
+    backColor: '#242f42',
+    textColor: '#ffffff'
+  },
+  menuList: [
+    { icon: 'iconshouye', _id: 'admin_1', path: '/homeIndex', name: '系统首页' },
+    // {
+    //   icon: 'iconshouye',
+    //   _id: 'admin_2',
+    //   name: '校友信息管理',
+    //   index: '2',
+    //   type: '0',
+    //   children: [
+    //     { icon: 'iconshouye', _id: 'admin_2_1', path: '/users/info', name: '登记信息管理' },
+    //     { icon: 'iconshouye', _id: 'admin_2_2', path: '/users/images', name: '照片上传管理' }
+    //   ]
+    // },
+    // {
+    //   icon: 'iconshouye',
+    //   _id: 'admin_3',
+    //   name: '基本设置',
+    //   index: '3',
+    //   type: '0',
+    //   children: [
+    //     { icon: 'iconshouye', _id: 'admin_3_1', path: '/basic/agreement', name: '协议修改' },
+    //     { icon: 'iconshouye', _id: 'admin_3_2', path: '/basic/notice', name: '通知修改' }
+    //   ]
+    // },
+    // {
+    //   icon: 'iconshouye',
+    //   _id: 'admin_4',
+    //   name: '账号管理',
+    //   index: '4',
+    //   type: '0',
+    //   children: [{ icon: 'iconshouye', _id: 'admin_4_1', path: '/acccount/updatepd', name: '修改密码' }]
+    // },
+    // {
+    //   icon: 'iconshouye',
+    //   _id: 'admin_5',
+    //   name: '系统设置',
+    //   index: '5',
+    //   type: '0',
+    //   children: [
+    //     { icon: 'iconshouye', _id: 'admin_5_2', path: '/system/config', name: '基础设置' },
+    //     { icon: 'iconshouye', _id: 'admin_5_1', path: '/system/dict', name: '字典管理' }
+    //   ]
+    // }
+  ]
+};

+ 42 - 0
src/main.ts

@@ -0,0 +1,42 @@
+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';
+
+import { ProvideOnApp } from './util/provideApp';
+// lodash
+// import _ from 'lodash';
+
+// 组件
+import frameComponents from '@/components/index';
+const app = createApp(App);
+app.use(createPinia());
+app.use(router);
+app.use(ElementPlus, { locale });
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component);
+}
+app.config.globalProperties.$moment = moment;
+
+for (const componentItme in frameComponents) {
+  app.component(componentItme, frameComponents[componentItme]);
+}
+app.config.globalProperties.$limit = parseInt(import.meta.env.VITE_APP_PAGE_SIZE) || 10;
+// 执行挂载
+ProvideOnApp(app);
+app.mount('#app');

+ 109 - 0
src/router/index.ts

@@ -0,0 +1,109 @@
+import { createRouter, createWebHistory } from 'vue-router';
+import store from '@/stores/counter';
+import axios from 'axios';
+
+const system = [
+  {
+    path: '/system/menus',
+    name: 'menus',
+    meta: { title: '二手车交易平台-菜单管理' },
+    component: () => import('@/views/system/menus/index.vue')
+  },
+  {
+    path: '/system/role',
+    name: 'role',
+    meta: { title: '二手车交易平台-角色管理' },
+    component: () => import('@/views/system/role/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/updatepd',
+    meta: { title: '修改密码' },
+    component: () => import('@/views/acccount/updatepd/index.vue')
+  },
+  {
+    path: '/system/brand',
+    meta: { title: '品牌管理' },
+    component: () => import('@/views/system/brand/index.vue')
+  }
+];
+const user = [
+  {
+    path: '/user/admin',
+    meta: { title: '管理员管理' },
+    component: () => import('@/views/user/admin/index.vue')
+  },
+  {
+    path: '/user/user',
+    meta: { title: '用户管理' },
+    component: () => import('@/views/user/user/index.vue')
+  }
+];
+const core = [
+  {
+    path: '/shop',
+    meta: { title: '管理员-店铺管理' },
+    component: () => import('@/views/shop/index.vue')
+  }
+];
+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: [
+        ...system,
+        ...user,
+        ...core,
+        {
+          path: '/homeIndex',
+          meta: { title: '二手车交易平台-系统首页' },
+          component: () => import('@/views/home/index.vue')
+        }
+      ]
+    }
+  ]
+});
+router.beforeEach(async (to, from, next) => {
+  document.title = `${to.meta.title} `;
+  const token = localStorage.getItem('token');
+  if (to.name != 'login') {
+    if (token) {
+      const res = await axios.request({
+        method: 'get',
+        url: `${import.meta.env.VITE_REQUEST_BASE}/token/tokenView`,
+        responseType: 'json',
+        headers: {
+          token: token
+        }
+      });
+      if (res.data.errcode == '0') {
+        store.commit('setUser', res.data.data, { root: true });
+      }
+      next();
+    } else next('/login');
+  } else next();
+});
+
+export default router;

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

+ 26 - 0
src/stores/login.ts

@@ -0,0 +1,26 @@
+import { defineStore } from 'pinia';
+import type { IQueryResult } from '@/util/types.util';
+import { get, omit } from 'lodash';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+const axios = new AxiosWrapper();
+export const LoginStore = defineStore('login', () => {
+  const login = async (payload: any): Promise<IQueryResult> => {
+    const type = get(payload, 'type');
+    const np = omit(payload, 'type');
+    const res = await axios.$post(`/login/${type}`, np);
+    return res;
+  };
+  const rp = async (payload: any): Promise<IQueryResult> => {
+    const type = get(payload, 'type');
+    const np = omit(payload, 'type');
+    const res = await axios.$post(`/login/updatePwd/${type}`, np);
+    return res;
+  };
+  const rpNoNewPassword = async (payload: any): Promise<IQueryResult> => {
+    const type = get(payload, 'type');
+    const np = omit(payload, 'type');
+    const res = await axios.$post(`/login/resetPwd/${type}`, np);
+    return res;
+  };
+  return { login, rp, rpNoNewPassword };
+});

+ 43 - 0
src/stores/shop.ts

@@ -0,0 +1,43 @@
+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: `/shop`
+};
+export const ShopStore = defineStore('shop', () => {
+  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 {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 43 - 0
src/stores/system/brand.ts

@@ -0,0 +1,43 @@
+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: `/brand`
+};
+export const BrandStore = defineStore('brand', () => {
+  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 {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 43 - 0
src/stores/system/dictData.ts

@@ -0,0 +1,43 @@
+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: `/dictData`
+};
+export const DictDataStore = defineStore('dictData', () => {
+  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 {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 43 - 0
src/stores/system/dictType.ts

@@ -0,0 +1,43 @@
+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: `/dictType`
+};
+export const DictTypeStore = defineStore('dictType', () => {
+  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 {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 41 - 0
src/stores/system/menus.ts

@@ -0,0 +1,41 @@
+import { defineStore } from 'pinia';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+import type { IQueryType, IQueryResult, IQueryParams } from '@/util/types.util';
+import { get } from 'lodash';
+const url = '/menus';
+const axios = new AxiosWrapper();
+
+export const MenusStore = defineStore('menus', () => {
+  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(`${url}`, cond);
+    return res;
+  };
+  const fetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${url}/${payload}`);
+    return res;
+  };
+  const create = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${url}`, payload);
+    return res;
+  };
+  const update = async (payload: any): Promise<IQueryResult> => {
+    const id = get(payload, 'id', get(payload, '_id'));
+    const res = await axios.$post(`${url}/${id}`, payload);
+    return res;
+  };
+  const del = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${url}/${payload}`);
+    return res;
+  };
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  };
+});

+ 41 - 0
src/stores/system/role.ts

@@ -0,0 +1,41 @@
+import { defineStore } from 'pinia';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+import type { IQueryType, IQueryResult, IQueryParams } from '@/util/types.util';
+import { get } from 'lodash';
+const url = '/role';
+const axios = new AxiosWrapper();
+
+export const RoleStore = defineStore('role', () => {
+  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(`${url}`, cond);
+    return res;
+  };
+  const fetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${url}/${payload}`);
+    return res;
+  };
+  const create = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${url}`, payload);
+    return res;
+  };
+  const update = async (payload: any): Promise<IQueryResult> => {
+    const id = get(payload, 'id', get(payload, '_id'));
+    const res = await axios.$post(`${url}/${id}`, payload);
+    return res;
+  };
+  const del = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${url}/${payload}`);
+    return res;
+  };
+  return {
+    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 = {}

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

@@ -0,0 +1,49 @@
+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: `/admin`
+};
+export const AdminStore = defineStore('admin', () => {
+  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;
+  };
+  //password
+  const rp = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}/rp`, payload);
+    return res;
+  };
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    rp
+  };
+});

+ 49 - 0
src/stores/users/user.ts

@@ -0,0 +1,49 @@
+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: `/user`
+};
+export const UserStore = defineStore('user', () => {
+  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;
+  };
+  //password
+  const rp = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${api.url}/rp`, payload);
+    return res;
+  };
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    rp
+  };
+});

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

@@ -0,0 +1,150 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _, { isArray } 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();
+      }
+    }
+  }
+}

+ 19 - 0
src/util/checkResult.ts

@@ -0,0 +1,19 @@
+import { isFunction, isString } from 'lodash';
+import { ElMessage } from 'element-plus';
+export const checkResult = (res, okText, errText) => {
+  const { errcode = 0, errmsg } = res || {};
+  if (errcode === 0) {
+    if (isFunction(okText)) {
+      return okText();
+    }
+    if (isString(okText)) ElMessage.success(okText);
+    else if (okText) ElMessage.success('操作成功');
+    return true;
+  }
+  if (isFunction(errText)) {
+    return errText();
+  }
+  ElMessage.error(errText || errmsg);
+  // Message({ message: _errText || errmsg, duration: 60000 });
+  return false;
+};

+ 15 - 0
src/util/provideApp.ts

@@ -0,0 +1,15 @@
+import { App } from 'vue';
+// 请求检测函数
+import { checkResult } from './checkResult';
+// limit固定值
+const limitValue = import.meta.env.VITE_APP_PAGE_SIZE || 10;
+// is_use默认值,不在数据库中规定
+const isUseList = [
+  { label: '使用', value: '0' },
+  { label: '禁用', value: '1' }
+];
+export const ProvideOnApp = (app: App<Element>) => {
+  app.provide('$checkRes', checkResult);
+  app.provide('$limit', limitValue);
+  app.provide('$isUseList', isUseList);
+};

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

+ 1 - 0
src/util/usualFunction.ts

@@ -0,0 +1 @@
+export const search = async (store) => {};

+ 67 - 0
src/views/acccount/updatepd/index.vue

@@ -0,0 +1,67 @@
+<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, inject } from 'vue';
+import { useRouter } from 'vue-router';
+// 接口
+import type { IQueryResult } from '@/util/types.util';
+import { LoginStore } from '@/stores/login';
+
+const loginAxios = LoginStore();
+const $checkRes = inject('$checkRes') as Function;
+// 加载中
+const loading: Ref<any> = ref(false);
+// 路由
+const router = useRouter();
+// 表单
+const form: Ref<any> = ref({});
+const fields: Ref<any> = ref([
+  { label: '新密码', model: 'password', type: 'password' },
+  { label: '确认新密码', model: 'ispassword', type: 'password' }
+]);
+const rules: Ref<any> = ref({
+  password: [{ required: true, message: '请输入新密码' }],
+  ispassword: [
+    { required: true, message: '请输入确认新密码' },
+    {
+      trigger: 'blur',
+      validator: (rule, value, callback) => {
+        if (form.value.password !== value) {
+          callback(new Error('两次输入的密码不一致'));
+        } else {
+          callback();
+        }
+      }
+    }
+  ]
+});
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  loading.value = false;
+});
+// 提交保存
+const toSave = async (data) => {
+  let user = store.state.user;
+  let res: IQueryResult = await loginAxios.rp({ _id: user._id, password: data.password, type: user.role });
+  if ($checkRes(res, true)) {
+    // 退出登录
+    localStorage.removeItem('token');
+    router.push('/login');
+  }
+};
+</script>
+<style scoped lang="scss"></style>

+ 310 - 0
src/views/core/achieve/admin.vue

@@ -0,0 +1,310 @@
+<template>
+  <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 #industry>
+            <el-option v-for="i in industryList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+          <template #attr>
+            <el-option v-for="i in attrList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton :isAdd="false"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #industry>
+          <el-option v-for="i in industryList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #attr>
+          <el-option v-for="i in attrList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #maturity>
+          <el-option v-for="i in maturityList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #tech>
+          <el-option v-for="i in techList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #area>
+          <el-option v-for="i in areaList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #public_info>
+          <el-checkbox-group v-model="form.publicInfo">
+            <el-checkbox v-for="i in publicInfoList" :key="i.model" :label="i.model">{{ i.label }}</el-checkbox>
+          </el-checkbox-group>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { AchieveStore } from '@/stores/core/achieve';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = AchieveStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const industryList: Ref<any> = ref([]);
+const attrList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+const maturityList: Ref<any> = ref([]);
+const techList: Ref<any> = ref([]);
+const areaList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'achieveStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const imsResult: IQueryResult = await dictDataStore.query({ code: 'ims' });
+  if ($checkRes(imsResult)) {
+    industryList.value = imsResult.data;
+  }
+  const achievePropsResult: IQueryResult = await dictDataStore.query({ code: 'achieveProps' });
+  if ($checkRes(achievePropsResult)) {
+    attrList.value = achievePropsResult.data;
+  }
+  const outWayResult: IQueryResult = await dictDataStore.query({ code: 'outWay' });
+  if ($checkRes(outWayResult)) {
+    wayList.value = outWayResult.data;
+  }
+  const maturityResult: IQueryResult = await dictDataStore.query({ code: 'maturity' });
+  if ($checkRes(maturityResult)) {
+    maturityList.value = maturityResult.data;
+  }
+  const techResult: IQueryResult = await dictDataStore.query({ code: 'tech' });
+  if ($checkRes(techResult)) {
+    techList.value = techResult.data;
+  }
+  const achieveAreaResult: IQueryResult = await dictDataStore.query({ code: 'achieveArea' });
+  if ($checkRes(achieveAreaResult)) {
+    areaList.value = achieveAreaResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  const udata = { _id: data._id, status: data.status === '0' ? '1' : '0' };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '成果名称', model: 'name', isSearch: true },
+  { label: '行业分类', model: 'industry', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'industry') },
+  { label: '成果属性', model: 'attr', isSearch: true, type: 'select', format: (i) => getDict(i, 'attr') },
+  { label: '出让方式', model: 'way', format: (i) => getDict(i, 'way') },
+  { label: '成熟度', model: 'maturity', format: (i) => getDict(i, 'maturity') },
+  { label: '发布时间', model: 'time' },
+  { label: '交易金额', model: 'money' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '非公开', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定要非公开该成果?', display: (i) => i.status === '0' },
+  { label: '公开', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定要公开该成果?', display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'industry':
+      list = industryList.value;
+      break;
+    case 'attr':
+      list = attrList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    case 'maturity':
+      list = maturityList.value;
+      break;
+    case 'tech':
+      list = techList.value;
+      break;
+    case 'area':
+      list = areaList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  // 需要将data中public_info(object) 转换成 publicInfo(array,Object.keys())
+  formFields.value = formFieldsForUpdate;
+  const res = await store.getOwner({ owner: data.owner });
+  let owner_name;
+  if ($checkRes(res)) {
+    owner_name = get(res, 'data');
+  }
+  form.value = { ...data, publicInfo: Object.keys(data.public_info || {}), owner_name };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  // 需要将publicInfo(array) 转换成public_info(object), 并删除publicInfo
+  const public_info = {};
+  for (const i of data.publicInfo) {
+    public_info[i] = true;
+  }
+  data.public_info = public_info;
+  delete data.publicInfo;
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '1', files: [], public_info: {}, publicInfo: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '成果名称', model: 'name' },
+  { label: '行业分类', model: 'industry', type: 'select' },
+  { label: '成果属性', model: 'attr', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '交易金额', model: 'money' },
+  { label: '成熟度', model: 'maturity', type: 'select' },
+  { label: '技术分类', model: 'tech', type: 'select' },
+  { label: '成果地区', model: 'area', type: 'select' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '附件', model: 'files', custom: true },
+  { label: '成果描述', model: 'desc', type: 'textarea' },
+  { label: '状态', model: 'status', type: 'radio' },
+  { label: '公开信息', model: 'public_info', custom: true }
+];
+const formFieldsForUpdate = [
+  { label: '所属人', model: 'owner_name', options: { disabled: true } },
+  { label: '成果名称', model: 'name' },
+  { label: '行业分类', model: 'industry', type: 'select' },
+  { label: '成果属性', model: 'attr', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '交易金额', model: 'money' },
+  { label: '成熟度', model: 'maturity', type: 'select' },
+  { label: '技术分类', model: 'tech', type: 'select' },
+  { label: '成果地区', model: 'area', type: 'select' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '附件', model: 'files', custom: true },
+  { label: '成果描述', model: 'desc', type: 'textarea' },
+  { label: '状态', model: 'status', type: 'radio' },
+  { label: '公开信息', model: 'public_info', custom: true }
+];
+const publicInfoList = [
+  { label: '所属人', model: 'owner' },
+  { label: '成果名称', model: 'name' },
+  { label: '行业分类', model: 'industry' },
+  { label: '成果属性', model: 'attr' },
+  { label: '出让方式', model: 'way' },
+  { label: '交易金额', model: 'money' },
+  { label: '成熟度', model: 'maturity' },
+  { label: '技术分类', model: 'tech' },
+  { label: '成果地区', model: 'area' },
+  { label: '发布时间', model: 'time' },
+  { label: '附件', model: 'files' },
+  { label: '成果描述', model: 'desc' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 309 - 0
src/views/core/achieve/index.vue

@@ -0,0 +1,309 @@
+<template>
+  <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 #industry>
+            <el-option v-for="i in industryList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+          <template #attr>
+            <el-option v-for="i in attrList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #industry>
+          <el-option v-for="i in industryList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #attr>
+          <el-option v-for="i in attrList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #maturity>
+          <el-option v-for="i in maturityList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #tech>
+          <el-option v-for="i in techList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #area>
+          <el-option v-for="i in areaList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #public_info>
+          <el-checkbox-group v-model="form.publicInfo">
+            <el-checkbox v-for="i in publicInfoList" :key="i.model" :label="i.model">{{ i.label }}</el-checkbox>
+          </el-checkbox-group>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { AchieveStore } from '@/stores/core/achieve';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = AchieveStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const industryList: Ref<any> = ref([]);
+const attrList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+const maturityList: Ref<any> = ref([]);
+const techList: Ref<any> = ref([]);
+const areaList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  // const list = [
+  //   {code:'',}
+  // ]
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'achieveStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const imsResult: IQueryResult = await dictDataStore.query({ code: 'ims' });
+  if ($checkRes(imsResult)) {
+    industryList.value = imsResult.data;
+  }
+  const achievePropsResult: IQueryResult = await dictDataStore.query({ code: 'achieveProps' });
+  if ($checkRes(achievePropsResult)) {
+    attrList.value = achievePropsResult.data;
+  }
+  const outWayResult: IQueryResult = await dictDataStore.query({ code: 'outWay' });
+  if ($checkRes(outWayResult)) {
+    wayList.value = outWayResult.data;
+  }
+  const maturityResult: IQueryResult = await dictDataStore.query({ code: 'maturity' });
+  if ($checkRes(maturityResult)) {
+    maturityList.value = maturityResult.data;
+  }
+  const techResult: IQueryResult = await dictDataStore.query({ code: 'tech' });
+  if ($checkRes(techResult)) {
+    techList.value = techResult.data;
+  }
+  const achieveAreaResult: IQueryResult = await dictDataStore.query({ code: 'achieveArea' });
+  if ($checkRes(achieveAreaResult)) {
+    areaList.value = achieveAreaResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, owner: user.value._id };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  const udata = { _id: data._id, status: data.status === '0' ? '1' : '0' };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '成果名称', model: 'name', isSearch: true },
+  { label: '行业分类', model: 'industry', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'industry') },
+  { label: '成果属性', model: 'attr', isSearch: true, type: 'select', format: (i) => getDict(i, 'attr') },
+  { label: '出让方式', model: 'way', format: (i) => getDict(i, 'way') },
+  { label: '成熟度', model: 'maturity', format: (i) => getDict(i, 'maturity') },
+  { label: '发布时间', model: 'time' },
+  { label: '交易金额', model: 'money' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '非公开', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定要非公开该成果?', display: (i) => i.status === '0' },
+  { label: '公开', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定要公开该成果?', display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'industry':
+      list = industryList.value;
+      break;
+    case 'attr':
+      list = attrList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    case 'maturity':
+      list = maturityList.value;
+      break;
+    case 'tech':
+      list = techList.value;
+      break;
+    case 'area':
+      list = areaList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  // 默认公开信息全都选择
+  const publicInfo = publicInfoList.map((i) => i.model);
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id, publicInfo };
+  dialog.value.show = true;
+};
+const toEdit = (data) => {
+  // 需要将data中public_info(object) 转换成 publicInfo(array,Object.keys())
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data, publicInfo: Object.keys(data.public_info || {}) };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  // 需要将publicInfo(array) 转换成public_info(object), 并删除publicInfo
+  const public_info = {};
+  for (const i of data.publicInfo) {
+    public_info[i] = true;
+  }
+  data.public_info = public_info;
+  delete data.publicInfo;
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '1', files: [], public_info: {}, publicInfo: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '成果名称', model: 'name' },
+  { label: '行业分类', model: 'industry', type: 'select' },
+  { label: '成果属性', model: 'attr', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '交易金额', model: 'money' },
+  { label: '成熟度', model: 'maturity', type: 'select' },
+  { label: '技术分类', model: 'tech', type: 'select' },
+  { label: '成果地区', model: 'area', type: 'select' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '附件', model: 'files', custom: true },
+  { label: '成果描述', model: 'desc', type: 'textarea' },
+  { label: '状态', model: 'status', type: 'radio' },
+  { label: '公开信息', model: 'public_info', custom: true }
+];
+const formFieldsForUpdate = [
+  { label: '成果名称', model: 'name' },
+  { label: '行业分类', model: 'industry', type: 'select' },
+  { label: '成果属性', model: 'attr', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '交易金额', model: 'money' },
+  { label: '成熟度', model: 'maturity', type: 'select' },
+  { label: '技术分类', model: 'tech', type: 'select' },
+  { label: '成果地区', model: 'area', type: 'select' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '附件', model: 'files', custom: true },
+  { label: '成果描述', model: 'desc', type: 'textarea' },
+  { label: '状态', model: 'status', type: 'radio' },
+  { label: '公开信息', model: 'public_info', custom: true }
+];
+const publicInfoList = [
+  { label: '所属人', model: 'owner' },
+  { label: '成果名称', model: 'name' },
+  { label: '行业分类', model: 'industry' },
+  { label: '成果属性', model: 'attr' },
+  { label: '出让方式', model: 'way' },
+  { label: '交易金额', model: 'money' },
+  { label: '成熟度', model: 'maturity' },
+  { label: '技术分类', model: 'tech' },
+  { label: '成果地区', model: 'area' },
+  { label: '发布时间', model: 'time' },
+  { label: '附件', model: 'files' },
+  { label: '成果描述', model: 'desc' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 256 - 0
src/views/core/demand/admin.vue

@@ -0,0 +1,256 @@
+<template>
+  <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 #industry_sector>
+            <el-option v-for="i in industrySectorList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+          <template #urgency>
+            <el-option v-for="i in urgencyList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton :isAdd="false"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #industry_sector>
+          <el-option v-for="i in industrySectorList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #urgency>
+          <el-option v-for="i in urgencyList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #area>
+          <el-option v-for="i in areaList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { DemandStore } from '@/stores/core/demand';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = DemandStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const industrySectorList: Ref<any> = ref([]);
+const urgencyList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+const areaList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'demandStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const urgencyResult: IQueryResult = await dictDataStore.query({ code: 'urgency' });
+  if ($checkRes(urgencyResult)) {
+    urgencyList.value = urgencyResult.data;
+  }
+  const cooperateResult: IQueryResult = await dictDataStore.query({ code: 'cooperate' });
+  if ($checkRes(cooperateResult)) {
+    wayList.value = cooperateResult.data;
+  }
+  const techResult: IQueryResult = await dictDataStore.query({ code: 'tech' });
+  if ($checkRes(techResult)) {
+    industrySectorList.value = techResult.data;
+  }
+  const achieveAreaResult: IQueryResult = await dictDataStore.query({ code: 'achieveArea' });
+  if ($checkRes(achieveAreaResult)) {
+    areaList.value = achieveAreaResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '2';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '需求标题', model: 'title', isSearch: true },
+  { label: '需求领域', model: 'industry_sector', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'industry_sector') },
+  { label: '需求紧急度', model: 'urgency', isSearch: true, type: 'select', format: (i) => getDict(i, 'urgency') },
+  { label: '合作方式', model: 'way', format: (i) => getDict(i, 'way') },
+  { label: '地区', model: 'area', format: (i) => getDict(i, 'area') },
+  { label: '需求开始时间', model: 'start_time' },
+  { label: '需求结束时间', model: 'end_time' },
+  { label: '需求状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '进入洽谈', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定需求进入洽谈阶段?', display: (i) => i.status === '0' },
+  { label: '结束', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定结束该需求?', display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'industry_sector':
+      list = industrySectorList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    case 'area':
+      list = areaList.value;
+      break;
+    case 'urgency':
+      list = urgencyList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  const res = await store.getOwner({ owner: data.owner });
+  let owner_name;
+  if ($checkRes(res)) {
+    owner_name = get(res, 'data');
+  }
+  form.value = { ...data, owner_name };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '1' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '需求标题', model: 'title' },
+  { label: '需求领域', model: 'industry_sector', type: 'select' },
+  { label: '需求紧急度', model: 'urgency', type: 'select' },
+  { label: '合作方式', model: 'way', type: 'select' },
+  { label: '需求地区', model: 'area', type: 'select' },
+  { label: '需求开始时间', model: 'start_time', type: 'datetime' },
+  { label: '需求结束时间', model: 'end_time', type: 'datetime' },
+  { label: '需求详情', model: 'desc', type: 'textarea' }
+];
+const formFieldsForUpdate = [
+  { label: '所属人', model: 'owner_name', options: { disabled: true } },
+  { label: '需求标题', model: 'title' },
+  { label: '需求领域', model: 'industry_sector', type: 'select' },
+  { label: '需求紧急度', model: 'urgency', type: 'select' },
+  { label: '合作方式', model: 'way', type: 'select' },
+  { label: '需求地区', model: 'area', type: 'select' },
+  { label: '需求开始时间', model: 'start_time', type: 'datetime' },
+  { label: '需求结束时间', model: 'end_time', type: 'datetime' },
+  { label: '需求详情', model: 'desc', type: 'textarea' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 253 - 0
src/views/core/demand/index.vue

@@ -0,0 +1,253 @@
+<template>
+  <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 #industry_sector>
+            <el-option v-for="i in industrySectorList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+          <template #urgency>
+            <el-option v-for="i in urgencyList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #industry_sector>
+          <el-option v-for="i in industrySectorList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #urgency>
+          <el-option v-for="i in urgencyList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #area>
+          <el-option v-for="i in areaList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { DemandStore } from '@/stores/core/demand';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = DemandStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const industrySectorList: Ref<any> = ref([]);
+const urgencyList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+const areaList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  // const list = [
+  //   {code:'',}
+  // ]
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'demandStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const urgencyResult: IQueryResult = await dictDataStore.query({ code: 'urgency' });
+  if ($checkRes(urgencyResult)) {
+    urgencyList.value = urgencyResult.data;
+  }
+  const cooperateResult: IQueryResult = await dictDataStore.query({ code: 'cooperate' });
+  if ($checkRes(cooperateResult)) {
+    wayList.value = cooperateResult.data;
+  }
+  const techResult: IQueryResult = await dictDataStore.query({ code: 'tech' });
+  if ($checkRes(techResult)) {
+    industrySectorList.value = techResult.data;
+  }
+  const achieveAreaResult: IQueryResult = await dictDataStore.query({ code: 'achieveArea' });
+  if ($checkRes(achieveAreaResult)) {
+    areaList.value = achieveAreaResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, owner: user.value._id };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '2';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '需求标题', model: 'title', isSearch: true },
+  { label: '需求领域', model: 'industry_sector', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'industry_sector') },
+  { label: '需求紧急度', model: 'urgency', isSearch: true, type: 'select', format: (i) => getDict(i, 'urgency') },
+  { label: '合作方式', model: 'way', format: (i) => getDict(i, 'way') },
+  { label: '地区', model: 'area', format: (i) => getDict(i, 'area') },
+  { label: '需求开始时间', model: 'start_time' },
+  { label: '需求结束时间', model: 'end_time' },
+  { label: '需求状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit', display: (i) => i.status === '0' },
+  { label: '进入洽谈', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定需求进入洽谈阶段?', display: (i) => i.status === '0' },
+  { label: '结束', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定结束该需求?', display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status === '0' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'industry_sector':
+      list = industrySectorList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    case 'area':
+      list = areaList.value;
+      break;
+    case 'urgency':
+      list = urgencyList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '1' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '需求标题', model: 'title' },
+  { label: '需求领域', model: 'industry_sector', type: 'select' },
+  { label: '需求紧急度', model: 'urgency', type: 'select' },
+  { label: '合作方式', model: 'way', type: 'select' },
+  { label: '需求地区', model: 'area', type: 'select' },
+  { label: '需求开始时间', model: 'start_time', type: 'datetime' },
+  { label: '需求结束时间', model: 'end_time', type: 'datetime' },
+  { label: '需求详情', model: 'desc', type: 'textarea' }
+];
+const formFieldsForUpdate = [
+  { label: '需求标题', model: 'title' },
+  { label: '需求领域', model: 'industry_sector', type: 'select' },
+  { label: '需求紧急度', model: 'urgency', type: 'select' },
+  { label: '合作方式', model: 'way', type: 'select' },
+  { label: '需求地区', model: 'area', type: 'select' },
+  { label: '需求开始时间', model: 'start_time', type: 'datetime' },
+  { label: '需求结束时间', model: 'end_time', type: 'datetime' },
+  { label: '需求详情', model: 'desc', type: 'textarea' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 285 - 0
src/views/core/service/admin.vue

@@ -0,0 +1,285 @@
+<template>
+  <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 serviceTypeList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+          <template #way>
+            <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #type>
+          <el-option v-for="i in serviceTypeList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #area>
+          <el-option v-for="i in areaList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #tags>
+          <el-row>
+            <el-col :span="24">
+              <el-button size="small" @click="addTags" type="primary">添加标签</el-button>
+            </el-col>
+            <el-col :span="24">
+              <el-table border stripe :data="form.tags" height="20vh">
+                <el-table-column align="center" label="标签文案">
+                  <template v-slot="{ row }">
+                    <el-input v-model="row.label" />
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="标签级别">
+                  <template v-slot="{ row }">
+                    <el-select v-model="row.type">
+                      <el-option label="低" value="primary">
+                        <el-link type="primary">低</el-link>
+                      </el-option>
+                      <el-option label="中" value="warning">
+                        <el-link type="warning">中</el-link>
+                      </el-option>
+                      <el-option label="高" value="danger">
+                        <el-link type="danger">高</el-link>
+                      </el-option>
+                    </el-select>
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="删除">
+                  <template v-slot="{ $index }">
+                    <el-link type="danger" @click="toDelTag($index)">删除</el-link>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-col>
+          </el-row>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { ServiceStore } from '@/stores/core/service';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get, isArray, isString } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = ServiceStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const serviceTypeList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+const areaList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'serviceStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const serviceTypeResult: IQueryResult = await dictDataStore.query({ code: 'serviceType' });
+  if ($checkRes(serviceTypeResult)) {
+    serviceTypeList.value = serviceTypeResult.data;
+  }
+  const serviceWayResult: IQueryResult = await dictDataStore.query({ code: 'serviceWay' });
+  if ($checkRes(serviceWayResult)) {
+    wayList.value = serviceWayResult.data;
+  }
+  const areaResult: IQueryResult = await dictDataStore.query({ code: 'serviceArea' });
+  if ($checkRes(areaResult)) {
+    areaList.value = areaResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '服务标题', model: 'title', isSearch: true },
+  { label: '服务类型', model: 'type', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'type') },
+  { label: '服务方式', model: 'way', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'way') },
+  { label: '服务地区', model: 'area', format: (i) => getDict(i, 'area') },
+  { label: '价格', model: 'money' },
+  { label: '服务状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定禁用该服务?', display: (i) => i.status === '0' },
+  { label: '启用', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定启用该服务?', display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'type':
+      list = serviceTypeList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    case 'area':
+      list = areaList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  if (isString(data)) {
+    const res = list.find((f) => f.value == data);
+    return get(res, 'label');
+  } else if (isArray(data)) {
+    const arr = [];
+    for (const i of data) {
+      const res = list.find((f) => f.value == data);
+      if (res) arr.push(get(res, 'label'));
+    }
+  }
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0', tags: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '服务标题', model: 'title' },
+  { label: '服务类型', model: 'type', type: 'select' },
+  { label: '标签', model: 'tags', custom: true },
+  { label: '服务方式', model: 'way', type: 'select' },
+  { label: '服务地区', model: 'area', type: 'select' },
+  { label: '价格', model: 'money' },
+  { label: '服务描述', model: 'desc', type: 'textarea' }
+];
+const formFieldsForUpdate = [
+  { label: '服务标题', model: 'title' },
+  { label: '服务类型', model: 'type', type: 'select' },
+  { label: '标签', model: 'tags', custom: true },
+  { label: '服务方式', model: 'way', type: 'select' },
+  { label: '服务地区', model: 'area', type: 'select' },
+  { label: '价格', model: 'money' },
+  { label: '服务描述', model: 'desc', type: 'textarea' }
+];
+const addTags = () => {
+  form.value.tags.push({ type: 'primary' });
+};
+const toDelTag = (index) => {
+  form.value.tags.splice(index, 1);
+};
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 285 - 0
src/views/core/service/index.vue

@@ -0,0 +1,285 @@
+<template>
+  <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 serviceTypeList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+          <template #way>
+            <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #type>
+          <el-option v-for="i in serviceTypeList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #area>
+          <el-option v-for="i in areaList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #tags>
+          <el-row>
+            <el-col :span="24">
+              <el-button size="small" @click="addTags" type="primary">添加标签</el-button>
+            </el-col>
+            <el-col :span="24">
+              <el-table border stripe :data="form.tags" height="20vh">
+                <el-table-column align="center" label="标签文案">
+                  <template v-slot="{ row }">
+                    <el-input v-model="row.label" />
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="标签级别">
+                  <template v-slot="{ row }">
+                    <el-select v-model="row.type">
+                      <el-option label="低" value="primary">
+                        <el-link type="primary">低</el-link>
+                      </el-option>
+                      <el-option label="中" value="warning">
+                        <el-link type="warning">中</el-link>
+                      </el-option>
+                      <el-option label="高" value="danger">
+                        <el-link type="danger">高</el-link>
+                      </el-option>
+                    </el-select>
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="删除">
+                  <template v-slot="{ $index }">
+                    <el-link type="danger" @click="toDelTag($index)">删除</el-link>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-col>
+          </el-row>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { ServiceStore } from '@/stores/core/service';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get, isArray, isString } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = ServiceStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const serviceTypeList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+const areaList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'serviceStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const serviceTypeResult: IQueryResult = await dictDataStore.query({ code: 'serviceType' });
+  if ($checkRes(serviceTypeResult)) {
+    serviceTypeList.value = serviceTypeResult.data;
+  }
+  const serviceWayResult: IQueryResult = await dictDataStore.query({ code: 'serviceWay' });
+  if ($checkRes(serviceWayResult)) {
+    wayList.value = serviceWayResult.data;
+  }
+  const areaResult: IQueryResult = await dictDataStore.query({ code: 'serviceArea' });
+  if ($checkRes(areaResult)) {
+    areaList.value = areaResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, owner: user.value._id };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '服务标题', model: 'title', isSearch: true },
+  { label: '服务类型', model: 'type', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'type') },
+  { label: '服务方式', model: 'way', isSearch: true, type: 'selectMult', format: (i) => getDict(i, 'way') },
+  { label: '服务地区', model: 'area', format: (i) => getDict(i, 'area') },
+  { label: '价格', model: 'money' },
+  { label: '服务状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定禁用该服务?', display: (i) => i.status === '0' },
+  { label: '启用', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定启用该服务?', display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'type':
+      list = serviceTypeList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    case 'area':
+      list = areaList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  if (isString(data)) {
+    const res = list.find((f) => f.value == data);
+    return get(res, 'label');
+  } else if (isArray(data)) {
+    const arr = [];
+    for (const i of data) {
+      const res = list.find((f) => f.value == data);
+      if (res) arr.push(get(res, 'label'));
+    }
+  }
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0', tags: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '服务标题', model: 'title' },
+  { label: '服务类型', model: 'type', type: 'select' },
+  { label: '标签', model: 'tags', custom: true },
+  { label: '服务方式', model: 'way', type: 'select' },
+  { label: '服务地区', model: 'area', type: 'select' },
+  { label: '价格', model: 'money' },
+  { label: '服务描述', model: 'desc', type: 'textarea' }
+];
+const formFieldsForUpdate = [
+  { label: '服务标题', model: 'title' },
+  { label: '服务类型', model: 'type', type: 'select' },
+  { label: '标签', model: 'tags', custom: true },
+  { label: '服务方式', model: 'way', type: 'select' },
+  { label: '服务地区', model: 'area', type: 'select' },
+  { label: '价格', model: 'money' },
+  { label: '服务描述', model: 'desc', type: 'textarea' }
+];
+const addTags = () => {
+  form.value.tags.push({ type: 'primary' });
+};
+const toDelTag = (index) => {
+  form.value.tags.splice(index, 1);
+};
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 306 - 0
src/views/core/trade/auction/admin.vue

@@ -0,0 +1,306 @@
+<template>
+  <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" @status="toStatus" @listing="toListing">
+        </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+    <el-col :span="24" class="dialog_one" v-if="dialog.type == '2'">
+      <el-row justify="center">
+        <el-col :span="24">
+          <el-steps :space="200" finish-status="success">
+            <el-step title="审核中" :status="getStepStatus(0)" />
+            <el-step title="报名中" :status="getStepStatus(1)" />
+            <el-step title="公开交易中" :status="getStepStatus(2)" />
+            <el-step title="已结束" :status="getStepStatus(3)" />
+          </el-steps>
+        </el-col>
+      </el-row>
+
+      <cForm :span="24" :fields="formFields" :form="form" :rules="{}" @save="toSave" label-width="auto" :disabled="true" :isSave="false">
+        <template #status>
+          <el-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+      <el-row justify="center">
+        <el-col :span="4">
+          <el-button :disabled="false" type="warning" @click="toBack" v-if="form.status !== '0'">上一步</el-button>
+        </el-col>
+        <el-col :span="4">
+          <el-button :disabled="false" type="success" @click="toNext" v-if="form.status !== '3'">下一步</el-button>
+        </el-col>
+      </el-row>
+    </el-col>
+    <el-col :span="24" class="dialog_one" v-if="dialog.type == '3'">
+      <MakeListing v-model:list="form.listing" @toSave="listingEditSave"></MakeListing>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { AuctionStore } from '@/stores/core/trade/auction';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import MakeListing from './makeListing.vue';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = AuctionStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'auctionStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '公告标题', model: 'title', isSearch: true },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time' },
+  { label: '公告结束时间', model: 'end_time' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit', display: (i) => i.status === '0' },
+  { label: '流程管理', method: 'status' },
+  { label: '拍卖项目', method: 'listing' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status === '0' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+const toStatus = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+  dialog.value.type = '2';
+};
+const toNext = async () => {
+  const data = cloneDeep(form.value);
+  // 进行下一步
+  let status = data.status;
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '2';
+      break;
+    case '2':
+      status = '3';
+      break;
+    default:
+      break;
+  }
+  const res = await store.update({ _id: data._id, status });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toBack = async () => {
+  const data = cloneDeep(form.value);
+  // 返回上一步
+  let status = data.status;
+  switch (data.status) {
+    case '3':
+      status = '2';
+      break;
+    case '2':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const res = await store.update({ _id: data._id, status });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toListing = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // TODO: 需要将挂牌的id转换成数据再放到原字段上
+  const res = await store.getListingList({ ids: data.listing });
+  let listing = [];
+  if ($checkRes(res)) {
+    listing = res.data as [];
+  }
+  form.value = { ...data, listing };
+  dialog.value.show = true;
+  dialog.value.type = '3';
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+const listingEditSave = async () => {
+  // 修改接口,把挂牌的id提取出来,放到原字段上
+  const data = cloneDeep(form.value);
+  const ids = data.listing.map((i) => i._id);
+  data.listing = ids;
+  let res: IQueryResult = await store.update(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '拍卖标题', model: 'title' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' },
+  { label: '公告备注', model: 'remark', type: 'textarea' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+const formFieldsForUpdate = [
+  { label: '拍卖标题', model: 'title' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' },
+  { label: '公告备注', model: 'remark', type: 'textarea' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+  dialog.value.type = '1';
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+
+const getStepStatus = (step) => {
+  const nowStatus = form.value.status;
+  const numStatus = parseInt(nowStatus);
+
+  if (step > numStatus) return 'wait';
+  else if (step < numStatus) return 'success';
+  else {
+    if (step === 3) return 'success';
+    else return 'process';
+  }
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 82 - 0
src/views/core/trade/auction/makeListing.vue

@@ -0,0 +1,82 @@
+<template>
+  <div id="makeListing">
+    <el-row>
+      <el-col :span="24">
+        查询挂牌交易:
+        <el-autocomplete v-model="searchData" :fetch-suggestions="getListing" placeholder="请输入挂牌交易标题" @select="handleSelect">
+          <template #default="{ item }">
+            {{ item.title }}
+          </template>
+        </el-autocomplete>
+      </el-col>
+    </el-row>
+    <el-row>
+      <el-col :span="24">
+        <el-table border stripe :data="list" height="40vh">
+          <el-table-column align="center" prop="title" label="挂牌标题"></el-table-column>
+          <el-table-column align="center" prop="achieve.name" label="成果"></el-table-column>
+          <el-table-column align="center" prop="money" label="挂牌价格"></el-table-column>
+          <el-table-column align="center" label="删除">
+            <template v-slot="{ row }">
+              <el-link type="danger" @click="toDel(row)">删除</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-col>
+    </el-row>
+
+    <el-row>
+      <el-col :span="24" style="text-align: center">
+        <el-button type="primary" @click="toSave">保存</el-button>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, inject, toRefs } from 'vue';
+import { ListingStore } from '@/stores/core/trade/listing';
+import { cloneDeep } from 'lodash';
+const emits = defineEmits(['update:list', 'toBack', 'toSave']);
+const props = defineProps({
+  list: { type: Array<any>, default: () => [] }
+});
+const listingStore = ListingStore();
+const $checkRes = inject('$checkRes') as Function;
+
+const getListing = async (data) => {
+  if (!data || data === '' || data === ' ') return [];
+  const res = await listingStore.query({ name: data, status: ['0', '1', '2'] });
+  if ($checkRes(res)) {
+    return res.data;
+  }
+  return [];
+};
+const searchData = ref();
+const handleSelect = (data) => {
+  const l = cloneDeep(props.list);
+  l.push(data);
+  emits('update:list', l);
+};
+
+const toDel = (row) => {
+  const l = cloneDeep(props.list);
+  const index = l.findIndex((f) => f._id === row._id);
+  if (index >= 0) l.splice(index, 1);
+  emits('update:list', l);
+
+};
+const toSave = () => {
+  emits('toSave');
+};
+const toBack = () => {
+  // 返回, emit
+  emits('toBack');
+};
+</script>
+
+<style scoped>
+.el-row {
+  padding-top: 10px;
+}
+</style>

+ 308 - 0
src/views/core/trade/listing/admin.vue

@@ -0,0 +1,308 @@
+<template>
+  <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 :isAdd="false"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @status="toStatus"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #achieve>
+          <el-select v-model="form.achieve" filterable remote reserve-keyword placeholder="请输入成果名称" :remote-method="getAchieve" :loading="loading">
+            <el-option v-for="item in achieveList" :key="item._id" :label="item.name" :value="item._id" />
+          </el-select>
+        </template>
+      </cForm>
+    </el-col>
+    <el-col :span="24" class="dialog_one" v-if="dialog.type == '2'">
+      <el-row justify="center">
+        <el-col :span="24">
+          <el-steps :space="200" finish-status="success">
+            <el-step title="审核中" :status="getStepStatus(0)" />
+            <el-step title="报名中" :status="getStepStatus(1)" />
+            <el-step title="公开交易中" :status="getStepStatus(2)" />
+            <el-step title="已结束" :status="getStepStatus(3)" />
+          </el-steps>
+        </el-col>
+      </el-row>
+
+      <cForm :span="24" :fields="formFields" :form="form" :rules="{}" @save="toSave" label-width="auto" :disabled="true" :isSave="false">
+        <template #status>
+          <el-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #achieve>
+          <el-select v-model="form.achieve" filterable remote reserve-keyword placeholder="请输入成果名称" :remote-method="getAchieve" :loading="loading">
+            <el-option v-for="item in achieveList" :key="item._id" :label="item.name" :value="item._id" />
+          </el-select>
+        </template>
+      </cForm>
+      <el-row justify="center">
+        <el-col :span="4">
+          <el-button :disabled="false" type="warning" @click="toBack" v-if="form.status !== '0'">上一步</el-button>
+        </el-col>
+        <el-col :span="4">
+          <el-button :disabled="false" type="success" @click="toNext" v-if="form.status !== '3'">下一步</el-button>
+        </el-col>
+      </el-row>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { ListingStore } from '@/stores/core/trade/listing';
+import { AchieveStore } from '@/stores/core/achieve';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get, isObject } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = ListingStore();
+const dictDataStore = DictDataStore();
+const achieveStore = AchieveStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'negotiateStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const achieveList: Ref<any> = ref([]);
+const getAchieve = async (data) => {
+  const res = await achieveStore.query({ name: data });
+  if ($checkRes(res)) {
+    achieveList.value = res.data;
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '公告标题', model: 'title', isSearch: true },
+  { label: '成果', model: 'achieve.name' },
+  { label: '挂牌价格', model: 'money' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time' },
+  { label: '公告结束时间', model: 'end_time' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit', display: (i) => i.status === '0' },
+  { label: '流程管理', method: 'status' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status === '0' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // 需要将成果放到achieveList中,如果这个数据中的成果是id,就得去库里查询
+  const achieve = cloneDeep(data.achieve);
+  if (isObject(achieve)) achieveList.value = [cloneDeep(achieve)];
+  form.value = { ...data, achieve: achieve._id };
+  dialog.value.show = true;
+};
+const toStatus = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // 需要将成果放到achieveList中,如果这个数据中的成果是id,就得去库里查询
+  const achieve = cloneDeep(data.achieve);
+  if (isObject(achieve)) achieveList.value = [achieve];
+  form.value = { ...data };
+  dialog.value.show = true;
+  dialog.value.type = '2';
+};
+const toNext = async () => {
+  const data = cloneDeep(form.value);
+  // 进行下一步
+  let status = data.status;
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '2';
+      break;
+    case '2':
+      status = '3';
+      break;
+    default:
+      break;
+  }
+  const res = await store.update({ _id: data._id, status });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toBack = async () => {
+  const data = cloneDeep(form.value);
+  // 返回上一步
+  let status = data.status;
+  switch (data.status) {
+    case '3':
+      status = '2';
+      break;
+    case '2':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const res = await store.update({ _id: data._id, status });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '公告标题', model: 'title' },
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '挂牌价格', model: 'money' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' },
+  { label: '公告备注', model: 'remark', type: 'textarea' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+const formFieldsForUpdate = [
+  { label: '公告标题', model: 'title' },
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '挂牌价格', model: 'money' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' },
+  { label: '公告备注', model: 'remark', type: 'textarea' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  achieveList.value = [];
+  dialog.value.show = false;
+  dialog.value.type = '1';
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+
+const getStepStatus = (step) => {
+  const nowStatus = form.value.status;
+  const numStatus = parseInt(nowStatus);
+
+  if (step > numStatus) return 'wait';
+  else if (step < numStatus) return 'success';
+  else {
+    if (step === 3) return 'success';
+    else return 'process';
+  }
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 205 - 0
src/views/core/trade/listing/index.vue

@@ -0,0 +1,205 @@
+<template>
+  <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"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #achieve>
+          <el-select v-model="form.achieve" filterable remote reserve-keyword placeholder="请输入成果名称" :remote-method="getAchieve" :loading="loading">
+            <el-option v-for="item in achieveList" :key="item._id" :label="item.name" :value="item._id" />
+          </el-select>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { ListingStore } from '@/stores/core/trade/listing';
+import { AchieveStore } from '@/stores/core/achieve';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get, isObject } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = ListingStore();
+const dictDataStore = DictDataStore();
+const achieveStore = AchieveStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'listingStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, owner: user.value._id };
+  const res: IQueryResult = await store.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const achieveList: Ref<any> = ref([]);
+const getAchieve = async (data) => {
+  const res = await achieveStore.query({ name: data });
+  if ($checkRes(res)) {
+    achieveList.value = res.data;
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '公告标题', model: 'title', isSearch: true },
+  { label: '成果', model: 'achieve.name' },
+  { label: '挂牌价格', model: 'money' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time' },
+  { label: '公告结束时间', model: 'end_time' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit', display: (i) => i.status === '0' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status === '0' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // 需要将成果放到achieveList中,如果这个数据中的成果是id,就得去库里查询
+  const achieve = cloneDeep(data.achieve);
+  if (isObject(achieve)) achieveList.value = [achieve];
+  form.value = { ...data, achieve: achieve._id };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '公告标题', model: 'title' },
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '挂牌价格', model: 'money' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' },
+  { label: '公告备注', model: 'remark', type: 'textarea' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+const formFieldsForUpdate = [
+  { label: '公告标题', model: 'title' },
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '挂牌价格', model: 'money' },
+  { label: '服务机构', model: 'org' },
+  { label: '项目编号', model: 'code' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' },
+  { label: '公告备注', model: 'remark', type: 'textarea' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  achieveList.value = [];
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 333 - 0
src/views/core/trade/negotiate/admin.vue

@@ -0,0 +1,333 @@
+<template>
+  <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 :isAdd="false"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @status="toStatus"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #has_connect>
+          <el-option v-for="i in hasConnectList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #achieve>
+          <el-select v-model="form.achieve" filterable remote reserve-keyword placeholder="请输入成果名称" :remote-method="getAchieve" :loading="loading">
+            <el-option v-for="item in achieveList" :key="item._id" :label="item.name" :value="item._id" />
+          </el-select>
+        </template>
+      </cForm>
+    </el-col>
+    <el-col :span="24" class="dialog_one" v-if="dialog.type == '2'">
+      <el-row justify="center">
+        <el-col :span="24">
+          <el-steps :space="200" finish-status="success">
+            <el-step title="审核中" :status="getStepStatus(0)" />
+            <el-step title="报名中" :status="getStepStatus(1)" />
+            <el-step title="公开交易中" :status="getStepStatus(2)" />
+            <el-step title="已结束" :status="getStepStatus(3)" />
+          </el-steps>
+        </el-col>
+      </el-row>
+      <cForm :span="24" :fields="formFields" :form="form" :rules="{}" @save="toSave" label-width="auto" :disabled="true" :isSave="false">
+        <template #status>
+          <el-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #has_connect>
+          <el-option v-for="i in hasConnectList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #achieve>
+          <el-select v-model="form.achieve" filterable remote reserve-keyword placeholder="请输入成果名称" :remote-method="getAchieve" :loading="loading">
+            <el-option v-for="item in achieveList" :key="item._id" :label="item.name" :value="item._id" />
+          </el-select>
+        </template>
+      </cForm>
+      <el-row justify="center">
+        <el-col :span="4">
+          <el-button :disabled="false" type="warning" @click="toBack" v-if="form.status !== '0'">上一步</el-button>
+        </el-col>
+        <el-col :span="4">
+          <el-button :disabled="false" type="success" @click="toNext" v-if="form.status !== '3'">下一步</el-button>
+        </el-col>
+      </el-row>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { NegotiateStore } from '@/stores/core/trade/negotiate';
+import { AchieveStore } from '@/stores/core/achieve';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get, isObject } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = NegotiateStore();
+const dictDataStore = DictDataStore();
+const achieveStore = AchieveStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const hasConnectList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'negotiateStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const hasConnectResult: IQueryResult = await dictDataStore.query({ code: 'hasConnect' });
+  if ($checkRes(hasConnectResult)) {
+    hasConnectList.value = hasConnectResult.data;
+  }
+  const outWayResult: IQueryResult = await dictDataStore.query({ code: 'outWay' });
+  if ($checkRes(outWayResult)) {
+    wayList.value = outWayResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const achieveList: Ref<any> = ref([]);
+const getAchieve = async (data) => {
+  const res = await achieveStore.query({ name: data });
+  if ($checkRes(res)) {
+    achieveList.value = res.data;
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '成果', model: 'achieve.name' },
+  { label: '专利号', model: 'no', isSearch: true },
+  { label: '联系邮箱', model: 'email' },
+  { label: '出让方式', model: 'way', format: (i) => getDict(i, 'way') },
+  { label: '拟定价格', model: 'money' },
+  { label: '公告开始时间', model: 'start_time' },
+  { label: '公告结束时间', model: 'end_time' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit', display: (i) => i.status === '0' },
+  { label: '流程管理', method: 'status' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status === '0' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // 需要将成果放到achieveList中,如果这个数据中的成果是id,就得去库里查询
+  const achieve = cloneDeep(data.achieve);
+  if (isObject(achieve)) achieveList.value = [achieve];
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+const toStatus = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // 需要将成果放到achieveList中,如果这个数据中的成果是id,就得去库里查询
+  const achieve = cloneDeep(data.achieve);
+  if (isObject(achieve)) achieveList.value = [achieve];
+  form.value = { ...data };
+  dialog.value.show = true;
+  dialog.value.type = '2';
+};
+const toNext = async () => {
+  const data = cloneDeep(form.value);
+  // 进行下一步
+  let status = data.status;
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '2';
+      break;
+    case '2':
+      status = '3';
+      break;
+    default:
+      break;
+  }
+  const res = await store.update({ _id: data._id, status });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toBack = async () => {
+  const data = cloneDeep(form.value);
+  // 返回上一步
+  let status = data.status;
+  switch (data.status) {
+    case '3':
+      status = '2';
+      break;
+    case '2':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const res = await store.update({ _id: data._id, status });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '专利号', model: 'no' },
+  { label: '联系邮箱', model: 'email' },
+  { label: '受让方是否是成果完成人或者其利害关系人', model: 'has_connect', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '公示备注', model: 'public_remark', type: 'textarea' },
+  { label: '拟定价格', model: 'money' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+const formFieldsForUpdate = [
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '专利号', model: 'no' },
+  { label: '联系邮箱', model: 'email' },
+  { label: '受让方是否是成果完成人或者其利害关系人', model: 'has_connect', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '公示备注', model: 'public_remark', type: 'textarea' },
+  { label: '拟定价格', model: 'money' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  achieveList.value = [];
+  dialog.value.show = false;
+  dialog.value.type = '1';
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+const getStepStatus = (step) => {
+  const nowStatus = form.value.status;
+  const numStatus = parseInt(nowStatus);
+
+  if (step > numStatus) return 'wait';
+  else if (step < numStatus) return 'success';
+  else {
+    if (step === 3) return 'success';
+    else return 'process';
+  }
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 226 - 0
src/views/core/trade/negotiate/index.vue

@@ -0,0 +1,226 @@
+<template>
+  <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"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #has_connect>
+          <el-option v-for="i in hasConnectList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #way>
+          <el-option v-for="i in wayList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+        </template>
+        <template #files>
+          <cUpload model="files" :limit="5" url="/files/zdlyjszy/achieve/upload" :list="form.files" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+        <template #achieve>
+          <el-select v-model="form.achieve" filterable remote reserve-keyword placeholder="请输入成果名称" :remote-method="getAchieve" :loading="loading">
+            <el-option v-for="item in achieveList" :key="item._id" :label="item.name" :value="item._id" />
+          </el-select>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { NegotiateStore } from '@/stores/core/trade/negotiate';
+import { AchieveStore } from '@/stores/core/achieve';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get, isObject } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = NegotiateStore();
+const dictDataStore = DictDataStore();
+const achieveStore = AchieveStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const hasConnectList: Ref<any> = ref([]);
+const wayList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'negotiateStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+  const hasConnectResult: IQueryResult = await dictDataStore.query({ code: 'hasConnect' });
+  if ($checkRes(hasConnectResult)) {
+    hasConnectList.value = hasConnectResult.data;
+  }
+  const outWayResult: IQueryResult = await dictDataStore.query({ code: 'outWay' });
+  if ($checkRes(outWayResult)) {
+    wayList.value = outWayResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, owner: user.value._id };
+  const res: IQueryResult = await store.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const achieveList: Ref<any> = ref([]);
+const getAchieve = async (data) => {
+  const res = await achieveStore.query({ name: data });
+  if ($checkRes(res)) {
+    achieveList.value = res.data;
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '成果', model: 'achieve.name' },
+  { label: '专利号', model: 'no', isSearch: true },
+  { label: '联系邮箱', model: 'email' },
+  { label: '出让方式', model: 'way', format: (i) => getDict(i, 'way') },
+  { label: '拟定价格', model: 'money' },
+  { label: '公告开始时间', model: 'start_time' },
+  { label: '公告结束时间', model: 'end_time' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status') }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit', display: (i) => i.status === '0' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.status === '0' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    case 'way':
+      list = wayList.value;
+      break;
+    default:
+      break;
+  }
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  // 需要将成果放到achieveList中,如果这个数据中的成果是id,就得去库里查询
+  const achieve = cloneDeep(data.achieve);
+  if (isObject(achieve)) achieveList.value = [achieve];
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '专利号', model: 'no' },
+  { label: '联系邮箱', model: 'email' },
+  { label: '受让方是否是成果完成人或者其利害关系人', model: 'has_connect', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '公示备注', model: 'public_remark', type: 'textarea' },
+  { label: '拟定价格', model: 'money' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+const formFieldsForUpdate = [
+  { label: '交易成果', model: 'achieve', custom: true },
+  { label: '专利号', model: 'no' },
+  { label: '联系邮箱', model: 'email' },
+  { label: '受让方是否是成果完成人或者其利害关系人', model: 'has_connect', type: 'select' },
+  { label: '出让方式', model: 'way', type: 'select' },
+  { label: '公示备注', model: 'public_remark', type: 'textarea' },
+  { label: '拟定价格', model: 'money' },
+  { label: '公告开始时间', model: 'start_time', type: 'datetime' },
+  { label: '公告结束时间', model: 'end_time', type: 'datetime' }
+  // { label: '状态', model: 'status', type: 'radio' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  achieveList.value = [];
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,232 @@
+<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-row class="one_1">
+            <el-col :span="24" class="title">
+              <span>校友登记统计</span>
+            </el-col>
+            <el-col :span="24" class="info">
+              <el-row class="info_1">
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">校友登记数</el-col>
+                      <el-col :span="24" class="num">{{ info.num_1 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-dengji"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">审核中</el-col>
+                      <el-col :span="24" class="num">{{ info.num_2 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-shenhezhong"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">审核通过</el-col>
+                      <el-col :span="24" class="num">{{ info.num_3 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-shenhetongguo"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">审核未通过</el-col>
+                      <el-col :span="24" class="num">{{ info.num_4 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-dkw_shenheweitongguo"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+              </el-row>
+            </el-col>
+          </el-row>
+          <el-row class="one_1">
+            <el-col :span="24" class="title">
+              <span>照片上传统计</span>
+            </el-col>
+            <el-col :span="24" class="info">
+              <el-row class="info_1">
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">照片上传数</el-col>
+                      <el-col :span="24" class="num">{{ info.num_5 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-dengji"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">审核中</el-col>
+                      <el-col :span="24" class="num">{{ info.num_6 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-shenhezhong"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">审核通过</el-col>
+                      <el-col :span="24" class="num">{{ info.num_7 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-shenhetongguo"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+                <el-col :span="6" class="list">
+                  <el-row>
+                    <el-col :span="20" class="l">
+                      <el-col :span="24" class="name">审核未通过</el-col>
+                      <el-col :span="24" class="num">{{ info.num_8 }}<span class="dw">人</span></el-col>
+                    </el-col>
+                    <el-col :span="4" class="r">
+                      <span class="iconfont icon-dkw_shenheweitongguo"></span>
+                    </el-col>
+                  </el-row>
+                </el-col>
+              </el-row>
+            </el-col>
+          </el-row>
+        </el-col>
+        <el-col :span="24" class="two" style="display: none">
+          <el-row class="two_1">
+            <el-col :span="12" class="l">1</el-col>
+            <el-col :span="12" class="l">1</el-col>
+          </el-row>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import type { Ref } from 'vue';
+import { onMounted, ref } from 'vue';
+
+// 接口
+// import type { IQueryResult } from '@/util/types.util';
+
+// 加载中
+const loading: Ref<any> = ref(false);
+const info: Ref<any> = ref({});
+
+// 请求
+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 {
+  .one {
+    margin: 0 0 20px 0;
+    .one_1 {
+      margin: 0 0 20px 0;
+      .title {
+        margin: 0 0 20px 0;
+        span {
+          background-color: #439eff;
+          color: #ffffff;
+          padding: 5px 10px;
+          border-radius: 5px;
+          font-weight: bold;
+        }
+      }
+      .info {
+        .info_1 {
+          display: flex;
+          justify-content: center;
+          .list {
+            max-width: 23%;
+            margin: 0 10px;
+            border-radius: 5px;
+            padding: 15px 10px;
+            background: linear-gradient(to bottom, #439eff 90%, #f1f1f1);
+            .l {
+              .name {
+                font-size: 18px;
+                font-weight: bold;
+                color: #ffffff;
+                margin: 0 0 10px 0;
+              }
+              .num {
+                color: #ffffff;
+                font-size: 22px;
+                font-weight: bold;
+                .dw {
+                  padding: 0 0 0 5px;
+                  font-size: 12px;
+                  color: #f1f1f1;
+                }
+              }
+            }
+            .r {
+              text-align: center;
+              padding: 5px 0;
+              .iconfont {
+                font-size: 40px;
+                color: #ffffff;
+              }
+            }
+          }
+          .list:nth-child(2) {
+            background: linear-gradient(to bottom, #ffa500 90%, #f1f1f1);
+          }
+          .list:nth-child(3) {
+            background: linear-gradient(to bottom, #00ff7f 90%, #f1f1f1);
+          }
+          .list:nth-child(4) {
+            background: linear-gradient(to bottom, #ff0000 90%, #f1f1f1);
+          }
+          .list:hover {
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+  .two {
+    .two_1 {
+      display: flex;
+      justify-content: center;
+      .l {
+        max-width: 48%;
+        margin: 0 10px;
+        border: 1px solid #ff0000;
+        padding: 20px;
+        border-radius: 10px;
+      }
+    }
+  }
+}
+</style>

+ 53 - 0
src/views/login/index.ts

@@ -0,0 +1,53 @@
+import type { Ref } from 'vue';
+import { ref, reactive } from 'vue';
+import type { FormInstance, FormRules } from 'element-plus';
+import { ElMessage } from 'element-plus';
+import router from '@/router';
+// 接口
+import { LoginStore } from '@/stores/login';
+import type { IQueryResult } from '@/util/types.util';
+const loginStore = LoginStore();
+
+
+// 表单
+/**
+ * 表单对象
+ */
+const formRef = ref<FormInstance>();
+/**
+ * 表单model
+ */
+const form: Ref<any> = ref({ type: 'Admin' });
+/**
+ * 输入规则
+ */
+const rules = reactive<FormRules>({
+  account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入账号密码', trigger: 'blur' }]
+});
+
+/**
+ * 提交登录
+ */
+const toSave = async (formEl: any) => {
+  if (!formEl) return;
+  await formEl.validate((valid: any) => {
+    if (valid) {
+      toLogin(form.value);
+    } else {
+      console.log('error submit!');
+    }
+  });
+};
+const toLogin = async (data: any) => {
+  const res: IQueryResult = await loginStore.login(data);
+  if (res.errcode == '0') {
+    ElMessage({ message: `登录成功`, type: 'success' });
+    localStorage.setItem('token', `${res.data}`);
+    // 路由
+    router.push({ path: '/homeIndex' });
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' });
+  }
+};
+export { toSave, formRef, form, rules };

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

@@ -0,0 +1,108 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight">
+        <el-col :span="24" class="one w_1200">
+          <el-col :span="24" class="one_1">吉林省重点领域技术转移平台</el-col>
+          <el-col :span="24" class="one_2">
+            <div class="login">
+              <el-col :span="24" class="login_1"> 管理登录 </el-col>
+              <el-col :span="24" class="login_2">
+                <el-col :span="24" class="form">
+                  <el-form ref="formRef" :model="form" :rules="rules" label-width="auto">
+                    <!-- <el-form-item>
+                      <el-radio-group v-model="form.type" style="padding-left: 15px">
+                        <el-radio label="SelfUser">个人用户</el-radio>
+                        <el-radio label="Unit">企业用户</el-radio>
+                        <el-radio label="Research">科研单位</el-radio>
+                        <el-radio label="Admin">管理员</el-radio>
+                      </el-radio-group>
+                    </el-form-item> -->
+                    <el-form-item label="" prop="account">
+                      <el-input v-model="form.account" placeholder="请输入登录账号">
+                        <template #prefix>
+                          <el-icon>
+                            <User />
+                          </el-icon>
+                        </template>
+                      </el-input>
+                    </el-form-item>
+                    <el-form-item label="" prop="password">
+                      <el-input v-model="form.password" type="password" show-password placeholder="请输入登录密码">
+                        <template #prefix>
+                          <el-icon>
+                            <Unlock />
+                          </el-icon>
+                        </template>
+                      </el-input>
+                    </el-form-item>
+                    <el-col :span="24" class="btn">
+                      <el-button type="primary" @click="toSave(formRef)">提交登录</el-button>
+                    </el-col>
+                  </el-form>
+                </el-col>
+              </el-col>
+            </div>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { toSave, formRef, form, rules } from './index';
+</script>
+<style scoped lang="scss">
+.main {
+  display: flex;
+  flex-direction: column;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  background: url('@/assets/bglogin.jpg');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+
+  .one {
+    .one_1 {
+      text-align: center;
+      color: #ffffff;
+      font-size: 30px;
+      margin: 5% 0 40px 0;
+    }
+
+    .one_2 {
+      text-align: center;
+      display: flex;
+      justify-content: center;
+
+      .login {
+        width: 500px;
+        height: 500px;
+        background-color: #ffffff;
+        border-radius: 5px;
+        padding: 10px;
+
+        .login_1 {
+          text-align: center;
+          margin: 30px 0;
+          font-size: 25px;
+          color: #409eff;
+          font-weight: 700;
+        }
+
+        .login_2 {
+          .type {
+            margin: 0 0 10px 0;
+          }
+        }
+      }
+    }
+  }
+}
+
+:deep(.el-input-group__append, .el-input-group__prepend) {
+  padding: 0;
+}
+</style>

+ 196 - 0
src/views/service/linkItem/index.vue

@@ -0,0 +1,196 @@
+<template>
+  <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._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #icon>
+          <cUpload model="icon" :list="form.icon" :limit="1" url="/files/zdlyjszy/linkItem/upload" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { LinkItemStore } from '@/stores/service/linkItem';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = LinkItemStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '事项名称', model: 'title', isSearch: true },
+  { label: '跳转至', model: 'to' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.status === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0', icon: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '事项名称', model: 'title' },
+  { label: '跳转至', model: 'to' },
+  { label: '图标', model: 'icon', custom: true }
+];
+const formFieldsForUpdate = [
+  { label: '事项名称', model: 'title' },
+  { label: '跳转至', model: 'to' },
+  { label: '图标', model: 'icon', custom: true }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  console.log(e);
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 195 - 0
src/views/service/notice/index.vue

@@ -0,0 +1,195 @@
+<template>
+  <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._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { NoticeStore } from '@/stores/service/notice';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = NoticeStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '标题', model: 'title', isSearch: true },
+  { label: '发布时间', model: 'time' },
+  { label: '来源', model: 'source' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.status === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '来源', model: 'source' },
+  { label: '内容', model: 'content', type: 'editor' }
+];
+const formFieldsForUpdate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '来源', model: 'source' },
+  { label: '内容', model: 'content', type: 'editor' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 200 - 0
src/views/service/policy/index.vue

@@ -0,0 +1,200 @@
+<template>
+  <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._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :list="form.files" :limit="1" url="/files/zdlyjszy/policy/upload" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { PolicyStore } from '@/stores/service/policy';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = PolicyStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '标题', model: 'title', isSearch: true },
+  { label: '发布时间', model: 'time' },
+  { label: '来源', model: 'source' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.status === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '来源', model: 'source' },
+  { label: '标题图片', model: 'files', custom: true },
+  { label: '内容', model: 'content', type: 'editor' }
+];
+const formFieldsForUpdate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '来源', model: 'source' },
+  { label: '标题图片', model: 'files', custom: true },
+  { label: '内容', model: 'content', type: 'editor' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 200 - 0
src/views/service/trends/index.vue

@@ -0,0 +1,200 @@
+<template>
+  <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._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :list="form.files" :limit="1" url="/files/zdlyjszy/policy/upload" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { TrendsStore } from '@/stores/service/trends';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = TrendsStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '标题', model: 'title', isSearch: true },
+  { label: '发布时间', model: 'time' },
+  { label: '来源', model: 'source' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.status === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '来源', model: 'source' },
+  { label: '标题图片', model: 'files', custom: true },
+  { label: '内容', model: 'content', type: 'editor' }
+];
+const formFieldsForUpdate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '来源', model: 'source' },
+  { label: '标题图片', model: 'files', custom: true },
+  { label: '内容', model: 'content', type: 'editor' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 195 - 0
src/views/service/video/admin.vue

@@ -0,0 +1,195 @@
+<template>
+  <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._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #files>
+          <cUpload model="files" :list="form.files" :limit="1" url="/files/zdlyjszy/video/upload" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { VideoStore } from '@/stores/service/video';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = VideoStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.status) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '标题', model: 'title', isSearch: true },
+  { label: '发布时间', model: 'time' },
+  { label: '状态', model: 'status', format: (i) => getDict(i, 'status'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.status === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.status === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'status':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm), owner: user.value._id };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '0', files: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '内容', model: 'files', custom: true }
+];
+const formFieldsForUpdate = [
+  { label: '标题', model: 'title' },
+  { label: '发布时间', model: 'time', type: 'datetime' },
+  { label: '内容', model: 'files', custom: true }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 0 - 0
src/views/service/video/index.vue


+ 202 - 0
src/views/shop/index.vue

@@ -0,0 +1,202 @@
+<template>
+  <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 #is_use>
+            <el-option v-for="i in statusList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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 #is_use>
+          <el-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #logo>
+          <cUpload model="logo" :list="form.logo" :limit="1" url="/files/usedCar/shop/upload" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { ShopStore } from '@/stores/shop';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = ShopStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.is_use) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, is_use: status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '店铺名称', model: 'name', isSearch: true },
+  { label: '店铺地址', model: 'address' },
+  { label: '联系电话', model: 'tel' },
+  { label: '联系人', model: 'contacts' },
+  { label: '状态', model: 'is_use', format: (i) => getDict(i, 'is_use'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.is_use === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.is_use === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  switch (model) {
+    case 'is_use':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { is_use: '0', icon: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '店铺名称', model: 'name' },
+  { label: '店铺地址', model: 'address' },
+  { label: '联系电话', model: 'tel' },
+  { label: '联系人', model: 'contacts' },
+  { label: 'logo', model: 'logo', custom: true }
+];
+const formFieldsForUpdate = [
+  { label: '店铺名称', model: 'name' },
+  { label: '店铺地址', model: 'address' },
+  { label: '联系电话', model: 'tel' },
+  { label: '联系人', model: 'contacts' },
+  { label: 'logo', model: 'logo', custom: true }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  console.log(e);
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 207 - 0
src/views/system/brand/index.vue

@@ -0,0 +1,207 @@
+<template>
+  <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 #is_use>
+            <el-option v-for="i in statusList" :key="i._id" :label="i.label" :value="i.value"></el-option>
+          </template>
+        </cSearch>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse">
+          <template #logo="{ row }">
+            <el-image :src="getLogo(row.logo)" style="width: 64px; height: 64px"></el-image>
+          </template>
+        </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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 #is_use>
+          <el-radio v-for="i in statusList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+        <template #logo>
+          <cUpload model="logo" :list="form.logo" :limit="1" url="/files/usedCar/brand/upload" listType="picture-card" @change="onUpload"></cUpload>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+import { head } from 'lodash';
+// NeedChange
+import { BrandStore as mainStroe } from '@/stores/system/brand';
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import baseStore from '@/stores/counter';
+const user = ref(baseStore.state.user);
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = mainStroe();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toChangeUse = async (data) => {
+  let status = '0';
+  switch (data.is_use) {
+    case '0':
+      status = '1';
+      break;
+    case '1':
+      status = '0';
+      break;
+    default:
+      break;
+  }
+  const udata = { _id: data._id, is_use: status };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: '品牌名', model: 'name', isSearch: true },
+  { label: 'logo', model: 'logo', custom: true },
+  { label: '首字母', model: 'letter' },
+  { label: '状态', model: 'is_use', format: (i) => getDict(i, 'is_use'), isSearch: true, type: 'select' }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, display: (i) => i.is_use === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, display: (i) => i.is_use === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getDict = (data, model) => {
+  let list;
+  console.log(data, model);
+  switch (model) {
+    case 'is_use':
+      list = statusList.value;
+      break;
+    default:
+      break;
+  }
+  if (!list) return;
+  const res = list.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  // 所属人是自己,需要把自己的id放进去
+  form.value = { ...cloneDeep(defaultForm) };
+  dialog.value.show = true;
+};
+const toEdit = async (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = { ...data };
+  dialog.value.show = true;
+};
+const getLogo = (list = []) => {
+  const pic = head(list);
+  if (pic) return pic.uri;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { is_use: '0', logo: [] };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '品牌名', model: 'name' },
+  { label: 'logo', model: 'logo', custom: true },
+  { label: '首字母', model: 'letter' }
+];
+const formFieldsForUpdate = [
+  { label: '品牌名', model: 'name' },
+  { label: 'logo', model: 'logo', custom: true },
+  { label: '首字母', model: 'letter' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+const onUpload = (e: { model: string; value: Array<[]> }) => {
+  console.log(e);
+  const { model, value } = e;
+  form.value[model] = value;
+};
+// #endregion
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,165 @@
+<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 #code="{ item, row }">
+              <el-link size="small" type="primary" @click="toType(row)">{{ getProps(row, item.model) }}</el-link>
+            </template>
+          </cTable>
+        </el-col>
+      </el-col>
+    </el-row>
+    <cDialog :dialog="dialog" @toClose="toClose">
+      <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>
+    </cDialog>
+  </div>
+</template>
+<script lang="ts" setup>
+// 基础
+import _ from 'lodash';
+import type { FormRules } from 'element-plus';
+import type { Ref } from 'vue';
+import { ref, onMounted, reactive, inject } from 'vue';
+import { ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+
+// 接口
+import { DictTypeStore } from '@/stores/system/dictType'; //
+import type { IQueryResult } from '@/util/types.util';
+const dictType = DictTypeStore();
+
+// 路由
+const router = useRouter();
+
+// 加载中
+const loading = ref(false);
+
+// 列表数据
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit: number = inject('$limit');
+let fields: Ref<any[]> = ref([
+  { label: '字典名称', model: 'title', isSearch: true },
+  { label: '编码', model: 'code', custom: true },
+  { label: '是否使用', model: 'is_use', format: (i: any) => getDict(i) },
+  { 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: 'code' },
+  { 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 = inject('$isUseList') as Array<any>;
+
+onMounted(async () => {
+  loading.value = true;
+  await search({ skip, limit });
+  loading.value = false;
+});
+// 查询
+const search = async (e: { skip: number; limit: number }) => {
+  const { skip, limit } = e;
+  const condition = _.cloneDeep(searchForm.value);
+  let info = { limit: limit, skip: skip, ...condition };
+  let res: IQueryResult = await dictType.query(info);
+  if (res.errcode == 0) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const getDict = (value) => {
+  let data = is_useList.find((i) => i.value == value);
+  if (data) return data.label;
+  else return '暂无';
+};
+const getProps = (data, prop) => {
+  return _.get(data, prop);
+};
+// 新增
+const toAdd = () => {
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 修改
+const toEdit = async (data) => {
+  let res: IQueryResult = await dictType.fetch(data._id);
+  if (res.errcode == 0) {
+    form.value = res.data as {};
+    dialog.value = { title: '信息管理', show: true, type: '1' };
+  }
+};
+// 提交
+const onSubmit = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await dictType.update(data);
+  else res = await dictType.create(data);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `维护信息成功` });
+    toClose();
+  }
+};
+// 删除
+const toDel = async (data) => {
+  let res: IQueryResult = await dictType.del(data._id);
+  if (res.errcode == 0) {
+    ElMessage({ type: `success`, message: `刪除信息成功` });
+    search({ skip, limit });
+  }
+};
+// 弹框关闭
+const toClose = () => {
+  form.value = {};
+  searchForm.value = {};
+  dialog.value = { title: '信息管理', show: false, type: '1' };
+  search({ skip, limit });
+};
+// 字典类型跳转
+const toType = (data) => {
+  router.push({ path: '/system/dictData', query: { id: data._id, code: data.code } });
+};
+</script>
+<style lang="scss" scoped>
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

+ 161 - 0
src/views/system/dictData/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">
+          <el-button type="primary" @click="toBack">返回</el-button>
+        </el-col>
+        <el-col :span="24" class="one">
+          <cSearch :is_title="false" :is_search="true" :fields="fields" @search="toSearch"> </cSearch>
+        </el-col>
+      </el-col>
+      <el-col :span="24" class="two">
+        <cButton @toAdd="toAdd()"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel"> </cTable>
+      </el-col>
+    </el-row>
+    <cDialog :dialog="dialog" @toClose="toClose">
+      <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>
+    </cDialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+// 基础
+import _ from 'lodash';
+import type { FormRules } from 'element-plus';
+import type { Ref } from 'vue';
+import { ref, onMounted, inject, reactive } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+// 接口
+import { DictDataStore } from '@/stores/system/dictData';
+import type { IQueryResult } from '@/util/types.util';
+const dictData = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+// 路由
+const route = useRoute();
+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 = inject('$limit');
+let fields: Ref<any[]> = ref([
+  { label: '编码', model: 'code' },
+  { label: '标签', model: 'label', isSearch: true },
+  { label: '键值', model: 'value', isSearch: true },
+  { label: '排序', model: 'sort', type: 'number' },
+  { label: '是否使用', model: 'is_use', format: (i: any) => getDict(i) }
+]);
+// 操作
+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: 'code', 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 = inject('$isUseList') as Array<any>;
+
+// 请求
+onMounted(async () => {
+  loading.value = true;
+  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 code = route.query.code;
+  let info = { limit: limit, skip: skip, ...condition, code };
+  let res: IQueryResult = await dictData.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const getDict = (value) => {
+  let data = is_useList.find((i) => i.value == value);
+  if (data) return data.label;
+  else return '暂无';
+};
+// 新增
+const toAdd = () => {
+  let code = route.query.code;
+  form.value = { code };
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 修改
+const toEdit = async (data) => {
+  form.value = data;
+  dialog.value = { title: '信息管理', show: true, type: '1' };
+};
+// 提交保存
+const toSave = async (data) => {
+  let res: IQueryResult;
+  if (data._id) res = await dictData.update(data);
+  else res = await dictData.create(data);
+  if ($checkRes(res, true)) {
+    toClose();
+  }
+};
+// 删除
+const toDel = async (data) => {
+  let res: IQueryResult = await dictData.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip, limit });
+  }
+};
+// 弹框关闭
+const toClose = () => {
+  form.value = {};
+  dialog.value = { title: '信息管理', show: false, type: '1' };
+  search({ skip, limit });
+};
+// 返回上一页
+const toBack = () => {
+  router.push({ path: '/system/dict' });
+};
+</script>
+<style scoped lang="scss">
+.main {
+  .two {
+    margin: 0 0 10px 0;
+  }
+}
+</style>

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

@@ -0,0 +1,241 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" style="text-align: right; padding: 10px">
+        <el-button type="primary" size="small" @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="{ row }"><span :class="['iconfont', 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="{ row }">{{ getType(row) }} </template>
+          </el-table-column>
+          <el-table-column align="center" label="状态" prop="is_use" width="80">
+            <template #default="{ row }">{{ getStatus(row) }} </template>
+          </el-table-column>
+          <el-table-column align="center" label="备注" prop="remark"> </el-table-column>
+          <el-table-column align="center" label="操作">
+            <template #default="{ row }">
+              <el-link :underline="false" type="primary" size="mini" @click="toUpdate(row)" style="margin-right: 10px">修改</el-link>
+              <el-link :underline="false" type="primary" size="mini" @click="toAddNext(row)" style="margin-right: 10px">添加下一级</el-link>
+              <el-link :underline="false" type="danger" size="mini" @click="toDelete(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="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="true">
+                <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.dict_label" :label="item.dict_label" :value="item.dict_label">
+                  <span style="float: left" :class="['iconfont', item.dict_label]"></span>
+                  <span style="float: right; color: #8492a6; font-size: 13px">{{ item.dict_label }}</span>
+                </el-option>
+              </el-select>
+            </el-form-item> -->
+            <el-form-item label="状态">
+              <el-radio-group v-model="form.is_use">
+                <el-radio label="0">使用</el-radio>
+                <el-radio label="1">禁用</el-radio>
+              </el-radio-group>
+            </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-tab-pane label="设置" name="config" v-if="form.type && form.type !== '0'">
+          <el-row>
+            <el-col :span="24" style="text-align: right; margin: 10px 0">
+              <el-button size="mini" type="primary" @click="toAddConfig()">添加功能</el-button>
+            </el-col>
+            <el-col :span="24">
+              <el-table :data="form.config">
+                <el-table-column align="center" label="中文">
+                  <template #default="{ row }">
+                    <el-input v-model="row.zh"></el-input>
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="code">
+                  <template #default="{ row }">
+                    <el-input v-model="row.code"></el-input>
+                  </template>
+                </el-table-column>
+                <el-table-column align="center" label="操作">
+                  <template #default="{ $index }">
+                    <el-button size="mini" type="danger" @click="deleteConfig($index)">删除</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-col>
+          </el-row>
+        </el-tab-pane> -->
+      </el-tabs>
+      <el-row type="flex" justify="space-around" style="margin-top: 10px">
+        <el-col :span="6">
+          <el-button @click="toSave" size="small" type="primary">保存</el-button>
+        </el-col>
+      </el-row>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { cloneDeep, get, omit } from 'lodash';
+import { ref, onMounted, inject } from 'vue';
+import { MenusStore } from '@/stores/system/menus';
+import { ElMessageBox } from 'element-plus';
+const store = MenusStore();
+const $checkRes: Function = inject('$checkRes');
+const dialog = ref(false);
+const list = ref([]);
+const form = ref({});
+const typeList = [
+  { label: '目录', value: '0' },
+  { label: '菜单', value: '1' },
+  { label: '子页面', value: '2' }
+];
+const tab = ref('basic');
+onMounted(() => {
+  search();
+});
+
+const search = async () => {
+  const res = await store.query();
+  if ($checkRes(res)) {
+    list.value = res.data as [];
+  }
+};
+
+const toSave = async () => {
+  const data = cloneDeep(omit(form.value, ['children', 'parent_name']));
+  let res;
+  if (get(data, '_id')) {
+    res = await store.update(data);
+  } else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search();
+    toClose();
+  }
+};
+const toDelete = async (row) => {
+  ElMessageBox.confirm('您确定删除该数据?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    const res = await store.del(row._id);
+    if ($checkRes(res, true)) {
+      search();
+    }
+  });
+};
+// #region 整理数组
+const getOneDimensionList = () => {
+  let dup = cloneDeep(list.value);
+  let arr = getAllChild(dup);
+  return arr;
+};
+
+const getAllChild = (children) => {
+  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;
+};
+// #endregion
+
+// #region 工具函数
+const toAddNext = (row) => {
+  const obj = { parent_id: row._id, is_use: '0' };
+  form.value = obj;
+  dialog.value = true;
+};
+const toUpdate = (row) => {
+  // if (!row.config) row.config = [];
+  form.value = cloneDeep(row);
+  dialog.value = true;
+};
+// const toAddConfig = () => {
+//   const config = get(form.value, 'config', []);
+//   config.push({});
+// };
+// const deleteConfig = (index) => {
+//   const config = get(form.value, 'config', []);
+//   if (config) config.splice(index, 1);
+// };
+const getType = (row) => {
+  let word = '';
+  const r = typeList.find((f) => f.value === row.type);
+  if (r) word = r.label;
+  return word;
+};
+const getStatus = (row) => {
+  let word = '';
+  switch (row.is_use) {
+    case '0':
+      word = '启用';
+      break;
+    case '1':
+      word = '禁用';
+      break;
+
+    default:
+      break;
+  }
+  return word;
+};
+
+const toAdd = () => {
+  dialog.value = true;
+  form.value = { is_use: '0' }; //config: [],
+};
+const toClose = () => {
+  form.value = {};
+  tab.value = 'basic';
+  dialog.value = false;
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 182 - 0
src/views/system/role/form-1.vue

@@ -0,0 +1,182 @@
+<template>
+  <div id="form-1">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+          <el-form-item label="名称" prop="name">
+            <el-input v-model="form.name" placeholder="请输入名称"></el-input>
+          </el-form-item>
+          <el-form-item label="简介" prop="brief">
+            <el-input v-model="form.brief" placeholder="请输入简介"></el-input>
+          </el-form-item>
+          <el-form-item label="角色编码" prop="code">
+            <el-input v-model="form.code" placeholder="请输入角色编码"></el-input>
+          </el-form-item>
+          <el-form-item label="菜单" prop="menu">
+            <el-row>
+              <el-col :span="24">
+                <el-checkbox v-model="allMenu" @change="treeDisabled">所有权限</el-checkbox>
+              </el-col>
+              <el-col :span="24">
+                <el-tree
+                  :data="menuList"
+                  show-checkbox
+                  default-expand-all
+                  node-key="path"
+                  ref="tree"
+                  highlight-current
+                  :props="defaultProps"
+                  :expand-on-click-node="false"
+                  @check="nodeSelect"
+                >
+                  <template #default="{ node, data }">
+                    <span class="custom-tree-node">
+                      <span>{{ data.name }}</span>
+                      <span v-if="data.remark && data.remark !== ' '">({{ data.remark }})</span>
+                      <!-- <span>
+                    <el-button type="text" size="mini" @click="toUpdateRole(node, data)"> 修改权限 </el-button>
+                  </span> -->
+                    </span>
+                  </template>
+                </el-tree>
+              </el-col>
+            </el-row>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" size="mini" @click="onSubmit()">确定</el-button>
+            <el-button type="danger" size="mini" @click="toReset()">取消</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+    </el-row>
+    <!-- <el-dialog append-to-body v-model:visible="show" title="权限分配">
+      <el-form>
+        <el-form-item label="权限控制">
+          <el-radio-group v-model="operaData.mode" @change="operaModelSelect">
+            <el-radio label="allow">全部允许</el-radio>
+            <el-radio label="refuse">全部禁止</el-radio>
+            <el-radio :label="undefined">自定义</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="权限选择">
+          <el-checkbox-group v-model="operaData.opera" @change="operaSelect">
+            <el-checkbox v-for="i in opera" :key="i.code" :label="i.code">{{ i.zh }}</el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="saveOpera">保存权限</el-button>
+        </el-form-item>
+      </el-form>
+    </el-dialog> -->
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ElForm, ElTree } from 'element-plus';
+import { get } from 'lodash';
+import { ref, onMounted } from 'vue';
+// import { formModel } from './interface';
+interface FormModel {
+  name?: string;
+  code?: string;
+  menu?: Array<any>;
+  brief?: string;
+  is_use?: string;
+}
+const props = defineProps<{
+  form: FormModel;
+  rules: any;
+  menuList: Array<any>;
+}>();
+onMounted(() => {
+  const menu = get(props.form, 'menu');
+  reShowMenuSelect(menu);
+});
+const emits = defineEmits(['update:form', 'onSubmit', 'toReset']);
+const allMenu = ref(false);
+const defaultProps = {
+  children: 'children',
+  label: 'name'
+};
+const formRef = ref<InstanceType<typeof ElForm>>();
+const tree = ref<InstanceType<typeof ElTree>>();
+const treeDisabled = (data) => {
+  let keys = tree.value!.getCheckedKeys();
+  for (const key of keys) {
+    tree.value!.setChecked(key, false, true);
+  }
+  if (data) {
+    emits('update:form', { ...props.form, menu: { mode: 'all' } });
+  }
+};
+const nodeSelect = (data, { checkedKeys }) => {
+  checkedKeys = checkedKeys.filter((f) => f !== undefined);
+  if (checkedKeys.length <= 0) {
+    emits('update:form', { ...props.form, menu: {} });
+    return;
+  }
+  allMenu.value = false;
+  const menu = get(props.form, 'menu', {});
+  const newMenu = { mode: 'select' };
+  // 原有设置的菜单id
+  let keys = Object.keys(menu);
+  // 过滤掉mode key,这东西不用管
+  keys = keys.filter((f) => f !== 'mode');
+  for (const key of keys) {
+    // 找下 原key是否还在现在的已选择列表中
+    const r = checkedKeys.find((f) => f === key);
+    // 不在:说明被取消了,应该删除
+    if (!r) continue;
+    // 还在,那就直接把它拿出来放到新的里面
+    newMenu[key] = menu[key];
+  }
+  // 在过滤一次,查看新加的
+  for (const id of checkedKeys) {
+    if (id && newMenu[id]) continue;
+    newMenu[id] = { opera: [] };
+  }
+  emits('update:form', { ...props.form, menu: newMenu });
+};
+
+const onSubmit = () => {
+  formRef.value!.validate((valid) => {
+    if (valid) {
+      emits('onSubmit');
+    } else {
+      console.log('error submit!!');
+      return false;
+    }
+  });
+};
+const toReset = () => {
+  formRef.value!.resetFields();
+  emits('toReset');
+};
+
+const reShowMenuSelect = (menu) => {
+  if (!menu) return;
+  const { mode } = menu;
+  if (mode === 'all') {
+    allMenu.value = true;
+    return;
+  } else {
+    allMenu.value = false;
+    let keys = Object.keys(menu);
+    keys = keys.filter((f) => f !== 'mode');
+    for (const key of keys) {
+      tree.value!.setChecked(key, true, true);
+    }
+  }
+};
+</script>
+
+<style scoped>
+.custom-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 8px;
+}
+</style>

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

@@ -0,0 +1,125 @@
+<template>
+  <el-row>
+    <el-col :span="24" class="main animate__animated animate__backInRight">
+      <el-col :span="24" class="two">
+        <c-button @toAdd="toAdd"></c-button>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <c-table :fields="fields" :opera="opera" :vOpera="false" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="changeUse">
+        </c-table>
+      </el-col>
+    </el-col>
+  </el-row>
+  <c-dialog :dialog="dialog" @toClose="toClose">
+    <From1 v-model:form="form" :rules="rules" :menuList="menuList" @onSubmit="onSubmit" @toReset="toClose"></From1>
+  </c-dialog>
+</template>
+
+<script setup lang="ts">
+import { IQueryResult } from '@/util/types.util';
+import { ref, Ref, inject, onMounted } from 'vue';
+import { RoleStore } from '@/stores/system/role';
+import { MenusStore } from '@/stores/system/menus';
+import { ElMessageBox } from 'element-plus';
+import From1 from './form-1.vue';
+import { cloneDeep, get } from 'lodash';
+// #region 变量
+const store = RoleStore();
+const menuStore = MenusStore();
+const list: Ref<any> = ref([]);
+const form: Ref<{}> = ref({});
+const total = ref(0);
+const limit = inject('$limit') as number;
+const menuList: Ref<any> = ref([]);
+const $checkRes = inject('$checkRes') as Function;
+const dialog = ref({ type: '1', show: false, title: '角色信息' });
+const fields = ref([
+  { label: '角色名称', model: 'name' },
+  { label: '角色代码', model: 'code' },
+  {
+    label: '是否启用',
+    model: 'is_use',
+    format: (i) => (i === '0' ? '使用' : '停用')
+  }
+]);
+const opera = ref([
+  { label: '修改', method: 'edit' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定禁用该角色及角色相关的用户?', display: (i) => i.is_use === '0' },
+  { label: '启用', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定启用该角色及角色相关的用户?', display: (i) => i.is_use === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+// #endregion
+
+onMounted(() => {
+  searchMenus();
+  search({ skip: 0, limit });
+});
+const search = async (e: { skip: number; limit: number }) => {
+  const { skip, limit } = e;
+  let info = { limit: limit, skip: skip };
+  let res: IQueryResult = await store.query(info);
+  if ($checkRes(res)) {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const searchMenus = async () => {
+  const res: IQueryResult = await menuStore.query();
+  if ($checkRes(res)) {
+    menuList.value = res.data;
+  }
+};
+
+// #region 数据变动
+const onSubmit = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const changeUse = async (data) => {
+  let is_use = '1';
+  if (data.is_use === '1') is_use = '0';
+  const res = await store.update({ _id: data._id, is_use });
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+const toDel = async (data) => {
+  ElMessageBox.confirm('您确定删除该数据?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    const res = await store.del(data._id);
+    if ($checkRes(res, true)) {
+      search({ skip: 0, limit });
+    }
+  });
+};
+// #endregion
+// #region 操作
+const toAdd = () => {
+  dialog.value.show = true;
+  form.value = { menu: {} };
+};
+const toEdit = async (data) => {
+  let res: IQueryResult = await store.fetch(data._id);
+  if ($checkRes(res)) {
+    form.value = res.data as {};
+    dialog.value = { title: '角色信息', show: true, type: '1' };
+  }
+};
+const toClose = () => {
+  dialog.value.show = false;
+  form.value = { menu: {} };
+};
+// #endregion
+</script>
+
+<style scoped></style>

+ 7 - 0
src/views/system/role/interface.ts

@@ -0,0 +1,7 @@
+export interface formModel {
+  name: string;
+  code: string;
+  menu: Array<any>;
+  brief: string;
+  is_use: string;
+}

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

@@ -0,0 +1,171 @@
+<template>
+  <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"
+          @rp="toResetPwd"
+          @changeUse="toChangeUse"
+        >
+        </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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 #is_use>
+          <el-radio v-for="i in isUseList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+import { AdminStore } from '@/stores/users/admin';
+import { DictDataStore } from '@/stores/system/dictData';
+import { LoginStore } from '@/stores/login';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import { ElMessageBox } from 'element-plus';
+
+const loading: Ref<any> = ref(false);
+const store = AdminStore();
+const dictDataStore = DictDataStore();
+const loginStore = LoginStore();
+const $checkRes = inject('$checkRes') as Function;
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let fields: Ref<any[]> = ref([
+  { label: '账号', model: 'account', isSearch: true },
+  { label: '名称', model: 'nick_name' },
+  { label: '是否是超级管理员', model: 'is_super', format: (i) => (i === '0' ? '是' : '否') },
+  { label: '是否启用', model: 'is_use', format: (i) => getDict(i) }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  { label: '修改', method: 'edit' },
+  { label: '重置密码', method: 'rp', type: 'warning', confirm: true, confirmWord: '您确定要重置密码?' },
+  { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定要禁用该用户?', display: (i) => i.is_use === '0' },
+  { label: '使用', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定要启用该用户?', display: (i) => i.is_use === '1' },
+  { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.is_super !== '0' }
+]);
+// 查询数据
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, is_del: '0' };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '账号', model: 'account' },
+  { label: '名称', model: 'nick_name' },
+  { label: '密码', model: 'password', type: 'password' },
+  { label: '是否启用', model: 'is_use', type: 'radio' }
+];
+const formFieldsForUpdate = [
+  { label: '账号', model: 'account' },
+  { label: '名称', model: 'nick_name' },
+  { label: '是否启用', model: 'is_use', type: 'radio' }
+];
+const defaultForm = { is_use: '0' };
+const formFields: Ref<any> = ref();
+
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+const userType = 'Admin';
+const toResetPwd = async (data) => {
+  const res = await loginStore.rpNoNewPassword({ type: userType, _id: data._id });
+  if ($checkRes(res, true)) {
+    ElMessageBox.confirm(`新密码为:${res.data}`, '请确认', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    });
+  }
+};
+const toChangeUse = async (data) => {
+  const udata = { _id: data._id, is_use: data.is_use === '0' ? '1' : '0' };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  form.value = cloneDeep(defaultForm);
+  dialog.value.show = true;
+};
+const toEdit = (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = data;
+  dialog.value.show = true;
+};
+
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+const isUseList: Ref<any> = ref([]);
+const searchOther = async () => {
+  const result: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+  if ($checkRes(result)) {
+    isUseList.value = result.data;
+  }
+};
+const getDict = (data) => {
+  const res = isUseList.value.find((f) => f.value == data);
+  return get(res, 'label');
+};
+</script>
+
+<style scoped></style>

+ 187 - 0
src/views/user/admin/options.vue

@@ -0,0 +1,187 @@
+<template>
+  <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"
+          @rp="toResetPwd"
+          @changeUse="toChangeUse"
+        >
+        </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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 #is_use>
+          <el-radio v-for="i in isUseList" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script lang="ts">
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import { ElMessageBox } from 'element-plus';
+import { AdminStore } from '@/stores/users/admin';
+import { DictDataStore } from '@/stores/system/dictData';
+import { LoginStore } from '@/stores/login';
+// const store = AdminStore();
+const dictDataStore = DictDataStore();
+const loginStore = LoginStore();
+export default {
+  name: 'index',
+  inject: {
+    $checkRes: { from: '$checkRes' },
+    limit: { from: '$limit' }
+  },
+  data() {
+    return {
+      store: AdminStore(),
+      loading: false,
+      list: [],
+      total: 0,
+      fields: [
+        { label: '账号', model: 'account', isSearch: true },
+        { label: '名称', model: 'nick_name' },
+        { label: '是否是超级管理员', model: 'is_super', format: (i) => (i === '0' ? '是' : '否') },
+        { label: '是否启用', model: 'is_use', format: (i) => this.getDict(i) }
+      ],
+      opera: [
+        { label: '修改', method: 'edit' },
+        { label: '重置密码', method: 'rp', type: 'warning', confirm: true, confirmWord: '您确定要重置密码?' },
+        {
+          label: '禁用',
+          method: 'changeUse',
+          type: 'warning',
+          confirm: true,
+          confirmWord: '您确定要禁用该用户?',
+          display: (i) => i.is_use === '0' && i.is_super !== '0'
+        },
+        {
+          label: '使用',
+          method: 'changeUse',
+          type: 'success',
+          confirm: true,
+          confirmWord: '您确定要启用该用户?',
+          display: (i) => i.is_use === '1' && i.is_super !== '0'
+        },
+        { label: '删除', method: 'del', confirm: true, type: 'danger', display: (i) => i.is_super !== '0' }
+      ],
+      searchForm: {},
+      dialog: { title: '数据信息', show: false, type: '1' },
+      form: {},
+      formFieldsForCreate: [
+        { label: '账号', model: 'account' },
+        { label: '名称', model: 'nick_name' },
+        { label: '密码', model: 'password', type: 'password' },
+        { label: '是否启用', model: 'is_use', type: 'radio' }
+      ],
+      formFieldsForUpdate: [
+        { label: '账号', model: 'account' },
+        { label: '名称', model: 'nick_name' },
+        { label: '是否启用', model: 'is_use', type: 'radio' }
+      ],
+      defaultForm: { is_use: '0' },
+      formFields: [],
+      userType: 'Admin',
+      isUseList: []
+    };
+  },
+  async mounted() {
+    this.loading = true;
+    await this.searchOther();
+    await this.search({ skip: 0, limit: this.limit });
+    this.loading = false;
+  },
+  methods: {
+    async search(e: { skip: number; limit: number }) {
+      const info = { skip: e.skip, limit: e.limit, ...this.searchForm, is_del: '0' };
+      const res: IQueryResult = await this.store.query(info);
+      if (res.errcode == '0') {
+        this.list = res.data;
+        this.total = res.total;
+      }
+    },
+    async toSave() {
+      const data = cloneDeep(this.form);
+      let res: IQueryResult;
+      if (get(data, '_id')) res = await this.store.update(data);
+      else res = await this.store.create(data);
+      if (this.$checkRes(res, true)) {
+        this.search({ skip: 0, limit: this.limit });
+        this.toClose();
+      }
+    },
+    async toDel(data) {
+      const res = await this.store.del(data._id);
+      if (this.$checkRes(res, true)) {
+        this.search({ skip: 0, limit: this.limit });
+      }
+    },
+
+    async toResetPwd(data) {
+      const res = await loginStore.rpNoNewPassword({ type: this.userType, _id: data._id });
+      if (this.$checkRes(res, true)) {
+        ElMessageBox.confirm(`新密码为:${res.data}`, '请确认', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning'
+        });
+      }
+    },
+    async toChangeUse(data) {
+      const udata = { _id: data._id, is_use: data.is_use === '0' ? '1' : '0' };
+      const res = await this.store.update(udata);
+      if (this.$checkRes(res, true)) {
+        this.search({ skip: 0, limit: this.limit });
+      }
+    },
+    toSearch(query) {
+      this.searchForm = query;
+      this.search({ skip: 0, limit: this.limit });
+    },
+    toAdd() {
+      this.formFields = this.formFieldsForCreate;
+      this.form = this.defaultForm;
+      this.dialog.show = true;
+    },
+    toEdit(data) {
+      this.formFields = this.formFieldsForUpdate;
+      this.form = data;
+      this.dialog.show = true;
+    },
+    toClose() {
+      this.form = {};
+      this.dialog.show = false;
+    },
+    async searchOther() {
+      const result: IQueryResult = await dictDataStore.query({ code: 'isUse' });
+      if (this.$checkRes(result)) {
+        this.isUseList = result.data;
+      }
+    },
+    getDict(data) {
+      const res = this.isUseList.find((f) => f.value == data);
+      return get(res, 'label');
+    }
+  }
+};
+</script>
+
+<style scoped></style>

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

@@ -0,0 +1,172 @@
+<template>
+  <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 :isAdd="false"> </cButton>
+      </el-col>
+      <el-col :span="24" class="thr">
+        <cTable :fields="fields" :opera="opera" :list="list" @query="search" :total="total" @edit="toEdit" @del="toDel" @changeUse="toChangeUse"> </cTable>
+      </el-col>
+    </el-col>
+  </el-row>
+  <cDialog :dialog="dialog" @toClose="toClose">
+    <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-radio v-for="i in getEditStatusList()" :key="i._id" :label="i.value">{{ i.label }}</el-radio>
+        </template>
+      </cForm>
+    </el-col>
+  </cDialog>
+</template>
+
+<script setup lang="ts">
+import { ref, Ref, onMounted, inject } from 'vue';
+// NeedChange
+import { UserStore } from '@/stores/users/user';
+import { DictDataStore } from '@/stores/system/dictData';
+import { LoginStore } from '@/stores/login';
+import type { IQueryResult } from '@/util/types.util';
+import { cloneDeep, get } from 'lodash';
+import { ElMessageBox } from 'element-plus';
+
+onMounted(async () => {
+  loading.value = true;
+  await searchOther();
+  await search({ skip, limit });
+  loading.value = false;
+});
+
+const loading: Ref<any> = ref(false);
+// NeedChange
+const store = UserStore();
+const dictDataStore = DictDataStore();
+const $checkRes = inject('$checkRes') as Function;
+
+// #region 字典
+// NeedChange
+const statusList: Ref<any> = ref([]);
+const searchOther = async () => {
+  const statusResult: IQueryResult = await dictDataStore.query({ code: 'userStatus' });
+  if ($checkRes(statusResult)) {
+    statusList.value = statusResult.data;
+  }
+};
+// #endregion
+
+// #region 查询相关
+let list: Ref<any> = ref([]);
+let total: Ref<number> = ref(0);
+let skip = 0;
+let limit = inject('$limit') as number;
+let searchForm: Ref<any> = ref({});
+
+const search = async (e: { skip: number; limit: number }) => {
+  const info = { skip: e.skip, limit: e.limit, ...searchForm.value, is_del: '0' };
+  const res: IQueryResult = await store.query(info);
+  if (res.errcode == '0') {
+    list.value = res.data;
+    total.value = res.total;
+  }
+};
+const toSearch = (query) => {
+  searchForm.value = query;
+  search({ skip, limit });
+};
+// #endregion
+
+// #region 表格及操作
+// NeedChange
+let fields: Ref<any[]> = ref([
+  { label: 'openid', model: 'openid', isSearch: true },
+  { label: '昵称', model: 'nick_name', isSearch: true }
+]);
+// 操作
+let opera: Ref<any[]> = ref([
+  // { label: '修改', method: 'edit' },
+  // { label: '重置密码', method: 'rp', type: 'warning', confirm: true, confirmWord: '您确定要重置密码?' },
+  // { label: '禁用', method: 'changeUse', type: 'warning', confirm: true, confirmWord: '您确定要禁用该用户?', display: (i) => i.status === '1' },
+  // { label: '使用', method: 'changeUse', type: 'success', confirm: true, confirmWord: '您确定要启用该用户?', display: (i) => i.status === '-1' },
+  // { label: '删除', method: 'del', confirm: true, type: 'danger' }
+]);
+
+const getEditStatusList = () => {
+  return statusList.value.filter((f) => f.value === '1' || f.value === '2');
+};
+const getDict = (data) => {
+  const res = statusList.value.find((f) => f.value == data);
+  return get(res, 'label');
+};
+const toAdd = () => {
+  formFields.value = formFieldsForCreate;
+  form.value = cloneDeep(defaultForm);
+  dialog.value.show = true;
+};
+const toEdit = (data) => {
+  formFields.value = formFieldsForUpdate;
+  form.value = data;
+  dialog.value.show = true;
+};
+// #endregion
+
+// #region 常规接口
+const toSave = async () => {
+  const data = cloneDeep(form.value);
+  let res: IQueryResult;
+  if (get(data, '_id')) res = await store.update(data);
+  else res = await store.create(data);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+    toClose();
+  }
+};
+const toDel = async (data) => {
+  const res = await store.del(data._id);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+const toChangeUse = async (data) => {
+  const udata = { _id: data._id, status: data.status === '1' ? '-1' : '1' };
+  const res = await store.update(udata);
+  if ($checkRes(res, true)) {
+    search({ skip: 0, limit });
+  }
+};
+// #endregion
+
+// #region 表单及操作
+// NeedChange
+const defaultForm = { status: '1' };
+const formFields: Ref<any> = ref();
+const dialog: Ref<any> = ref({ title: '数据信息', show: false, type: '1' });
+const form: Ref<any> = ref({ file: [] });
+const formFieldsForCreate = [
+  { label: '账号', model: 'account' },
+  { label: '密码', model: 'password', type: 'password' },
+  { label: '用户名', model: 'nick_name' },
+  { label: '工作单位', model: 'unit' },
+  { label: '联系电话', model: 'tel' },
+  { label: '身份证号', model: 'id_card' },
+  { label: '联系邮箱', model: 'email' }
+];
+const formFieldsForUpdate = [
+  { label: '账号', model: 'account' },
+  { label: '用户名', model: 'nick_name' },
+  { label: '工作单位', model: 'unit' },
+  { label: '联系电话', model: 'tel' },
+  { label: '身份证号', model: 'id_card' },
+  { label: '联系邮箱', model: 'email' }
+];
+// 关闭弹框
+const toClose = () => {
+  form.value = {};
+  dialog.value.show = false;
+};
+// #endregion
+</script>
+
+<style scoped></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": "@vue/tsconfig/tsconfig.node.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "ignoreDeprecations": "5.0",
+    "composite": true,
+    "types": [
+      "node"
+    ]
+  }
+}

+ 36 - 0
vite.config.ts

@@ -0,0 +1,36 @@
+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: {
+      host: '0.0.0.0',
+      port: 8001,
+      proxy: {
+        '/files': {
+          target: 'http://120.48.146.1', // https://broadcast.waityou24.cn
+          changeOrigin: true
+        },
+        '/usedCar/api': {
+          target: 'http://127.0.0.1:9800', //https://www.ccwit.net
+          changeOrigin: true,
+          ws: false
+        }
+      },
+      fs: { strict: false }
+    },
+    resolve: {
+      alias: {
+        '@': fileURLToPath(new URL('./src', import.meta.url))
+      }
+    }
+  };
+});