YY 2 years ago
commit
c152d1182e

+ 28 - 0
.eslintrc.cjs

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

+ 28 - 0
.gitignore

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

+ 6 - 0
.prettierrc.json

@@ -0,0 +1,6 @@
+{
+    "singleQuote": true,
+  "printWidth": 160,
+  "bracketSpacing": true,
+  "endOfLine": "lf"
+}

+ 3 - 0
.vscode/extensions.json

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

+ 46 - 0
README.md

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

+ 1 - 0
env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 13 - 0
index.html

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

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


+ 39 - 0
package.json

@@ -0,0 +1,39 @@
+{
+  "name": "web",
+  "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",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.0.10",
+    "axios": "^1.3.2",
+    "element-plus": "^2.2.28",
+    "lodash": "^4.17.21",
+    "moment": "^2.29.4",
+    "naf-core": "^0.1.2",
+    "pinia": "^2.0.28",
+    "vue": "^3.2.45",
+    "vue-router": "^4.1.6"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.1.4",
+    "@types/node": "^18.11.12",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "@vue/eslint-config-prettier": "^7.0.0",
+    "@vue/eslint-config-typescript": "^11.0.0",
+    "@vue/tsconfig": "^0.1.3",
+    "eslint": "^8.22.0",
+    "eslint-plugin-vue": "^9.3.0",
+    "npm-run-all": "^4.1.5",
+    "prettier": "^2.7.1",
+    "typescript": "~4.7.4",
+    "vite": "^4.0.0",
+    "vue-tsc": "^1.0.12"
+  }
+}

BIN
public/favicon.ico


+ 25 - 0
src/App.vue

@@ -0,0 +1,25 @@
+<template>
+  <div>
+    <Suspense>
+      <router-view />
+    </Suspense>
+  </div>
+</template>
+
+<script lang="ts" setup></script>
+
+<style scoped>
+p {
+  margin: 0;
+  padding: 0;
+}
+.textOver {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+header {
+  line-height: 1.5;
+  max-height: 100vh;
+}
+</style>

+ 75 - 0
src/assets/base.css

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

BIN
src/assets/image/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"  xmlns:v="https://vecta.io/nano"><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>

+ 34 - 0
src/assets/main.css

@@ -0,0 +1,34 @@
+@import './base.css';
+
+#app {
+ width: 100%;
+  /* margin: 0 auto;
+  padding: 2rem; */
+  font-weight: normal;
+}
+
+a,
+.green {
+  text-decoration: none;
+  color: hsla(160, 100%, 37%, 1);
+  transition: 0.4s;
+}
+
+@media (hover: hover) {
+  a:hover {
+    background-color: hsla(160, 100%, 37%, 0.2);
+  }
+}
+
+@media (min-width: 100%) {
+  body {
+    display: flex;
+    place-items: center;
+  }
+
+  #app {
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    padding: 0 2rem;
+  }
+}

+ 54 - 0
src/layout/Home.vue

@@ -0,0 +1,54 @@
+<template>
+  <div id="home">
+    <el-container>
+      <el-header>
+        <HomeHead></HomeHead>
+      </el-header>
+      <el-container>
+        <el-aside class="one">
+          <HomeLeft @onMenu="onMenu"></HomeLeft>
+        </el-aside>
+        <el-main>
+          <el-scrollbar class="two">
+            <router-view v-if="Obj.ifRouterAlive" :id="Obj.id"></router-view>
+          </el-scrollbar>
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+<script setup lang="ts">
+import { reactive, nextTick } from 'vue';
+import HomeLeft from '@/layout/homeParts/left-1.vue';
+import HomeHead from '@/layout/homeParts/header-1.vue';
+const Obj = reactive({ id: '', name: '', ifRouterAlive: true });
+const onMenu = async (value: { id: string; name: string }) => {
+  Obj.id = value.id;
+  Obj.name = value.name;
+  Obj.ifRouterAlive = false;
+  nextTick(() => {
+    Obj.ifRouterAlive = true;
+  });
+};
+</script>
+
+<style scoped>
+.el-header {
+  height: 8vh;
+  padding: 0;
+  background-color: #f5d59d;
+}
+.el-main {
+  padding: 0;
+}
+.one {
+  background: #55c596;
+  width: 255px;
+}
+.two {
+  height: 92vh;
+}
+router-view {
+  height: 92vh;
+}
+</style>

+ 55 - 0
src/layout/homeParts/form-1.vue

@@ -0,0 +1,55 @@
+<template>
+  <el-form ref="ruleFormRef" :model="ruleForm" :rules="rules" label-width="120px" class="demo-ruleForm" :size="formSize" status-icon>
+    <el-form-item label="项目名" prop="name">
+      <el-input v-model="ruleForm.name" />
+    </el-form-item>
+    <el-form-item label="描述" prop="desc">
+      <el-input v-model="ruleForm.desc" type="textarea" />
+    </el-form-item>
+    <el-form-item label="备注" prop="remarks">
+      <el-input v-model="ruleForm.remarks" type="textarea" />
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="submitForm(ruleFormRef)"> 提交 </el-button>
+      <el-button @click="resetForm(ruleFormRef)">重置</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref } from 'vue';
+import type { FormInstance, FormRules } from 'element-plus';
+
+const formSize = ref('default');
+const ruleFormRef = ref<FormInstance>();
+let ruleForm: Object = reactive({
+  name: '',
+  desc: '',
+  remarks: '',
+});
+
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+});
+console.log(ruleForm);
+
+const props = defineProps({ ruleForm: Object });
+ruleForm = props.ruleForm;
+// 提交保存
+const emit = defineEmits(['submitForm']);
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      emit('submitForm');
+    } else {
+      console.log('error submit!');
+    }
+  });
+};
+
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.resetFields();
+};
+</script>

+ 66 - 0
src/layout/homeParts/header-1.vue

@@ -0,0 +1,66 @@
+<template>
+  <el-col :span="24" class="main">
+    <el-row class="head">
+      <el-col :span="8" class="left">
+        <el-image :src="data.imgUrl" class="image"></el-image>
+        <span>{{ data.name || '建表项目' }}-管理中心</span>
+      </el-col>
+      <el-col :span="16" class="right">
+        <span>{{ data.user.name || '游客' }}</span>
+        <el-button type="danger" @click="logout()">退出登录</el-button>
+      </el-col>
+    </el-row>
+  </el-col>
+</template>
+
+<script setup lang="ts">
+import { reactive } from 'vue';
+const data = reactive({
+  name: '',
+  user: { name: '王泓璎' },
+  imgUrl: new URL('@/assets/image/logo.png', import.meta.url).href,
+});
+function logout() {}
+</script>
+
+<style scoped>
+.head {
+  padding: 5px 10px 0;
+}
+.left {
+  line-height: 60px;
+}
+.left span {
+  display: inline-block;
+  margin: 0 10px;
+  font-size: 24px;
+  color: #fff;
+  font-weight: bold;
+  font-family: cursive;
+}
+.left .image {
+  background: #fff;
+  height: 32px;
+  width: 32px;
+  border-radius: 90px;
+  top: 5px;
+}
+.right {
+  text-align: right;
+  line-height: 60px;
+  word-break: keep-all;
+  white-space: nowrap;
+}
+.right i {
+  position: relative;
+  top: 5px;
+  margin: 0px 15px;
+  font-size: 30px;
+  color: #fff;
+}
+.right span {
+  color: #fff;
+  font-size: 16px;
+  padding: 0 15px 0 0px;
+}
+</style>

+ 161 - 0
src/layout/homeParts/left-1.vue

@@ -0,0 +1,161 @@
+<template>
+  <div class="main">
+    <div class="one">
+      <el-col :span="24" class="add">
+        <el-button type="primary" size="large" @click="toAdd('add')">添加项目</el-button>
+      </el-col>
+      <el-col :span="24">
+        <el-col v-if="data.id">
+          <el-button class="button" type="primary" @click="toAdd('edit')">修改项目</el-button>
+          <el-button class="button" type="danger" @click="toDelete()">删除项目</el-button>
+        </el-col>
+        <el-col v-else>
+          <el-button class="button" type="primary" disabled>修改项目</el-button>
+          <el-button class="button" type="danger" disabled>删除项目</el-button>
+        </el-col>
+      </el-col>
+    </div>
+    <el-col :span="24" wrap>
+      <div class="two">
+        <el-menu class="sidebar-el-menu" :collapse="false" unique-opened>
+          <template v-for="item in items" :key="item">
+            <el-menu-item class="first" :index="item._id" @click="onMenu(item)">
+              <el-col :span="4" class="icon">
+                <el-icon :size="20">
+                  <Opportunity />
+                </el-icon>
+              </el-col>
+              <el-col :span="18" class="title">
+                <span>{{ item.name }}</span>
+              </el-col>
+            </el-menu-item>
+          </template>
+        </el-menu>
+      </div>
+    </el-col>
+  </div>
+  <el-dialog v-model="data.dialog" title="项目管理" :before-close="handleClose">
+    <Form @submitForm="submitForm" :ruleForm="ruleForm"></Form>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import Form from '@/layout/homeParts/form-1.vue';
+import { useCounterStore as useStore } from '@/stores/counter';
+import type { IQueryResult } from '@/util/types.util';
+const store = useStore();
+
+// #region 1
+const data = reactive({
+  id: '',
+  dialog: ref(false),
+});
+let items: any = reactive([]);
+let ruleForm: Object = reactive({
+  _id: '',
+  name: '',
+  desc: '',
+  remarks: '',
+});
+const res: IQueryResult = await store.projectQuery();
+items = res.data;
+
+const emit = defineEmits(['onMenu']);
+const onMenu = (item: { _id: string; name: string }) => {
+  data['id'] = item._id;
+  emit('onMenu', item);
+};
+const toAdd = async (type: string) => {
+  if (type == 'edit') {
+    const aee: IQueryResult = await store.projectFetch(data.id);
+    ruleForm = aee.data;
+  } else if (type == 'add') {
+    ruleForm = {};
+  }
+  data.dialog = true;
+};
+const submitForm = () => {
+  console.log(ruleForm);
+};
+const handleClose = () => {
+  console.log(ruleForm);
+
+  data.dialog = false;
+};
+const toDelete = () => {};
+// #endregion
+</script>
+
+<style scoped>
+.one {
+  height: 14vh;
+  background: #1b5644;
+  padding: 10px 30px 10px 30px;
+}
+.one .button {
+  margin: 0 4px;
+}
+.one .add {
+  margin: 0 0 10px 0;
+  text-align: center;
+}
+.two {
+  height: 78vh;
+  padding: 0 0 30px 0;
+  background: #73ad9b;
+  overflow-y: scroll;
+}
+.sidebar-el-menu {
+  background: #73ad9b;
+}
+.sidebar-el-menu .first span {
+  font-size: 16px;
+  font-weight: 500;
+}
+.el-menu {
+  border-right: none;
+}
+.el-menu-item {
+  color: #fff;
+}
+.el-menu-item.is-active {
+  color: #fdda13;
+  background-color: #78c1aa;
+}
+.two::-webkit-scrollbar {
+  width: 2px;
+}
+.two::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(108, 43, 43, 0.2);
+}
+.two::-webkit-scrollbar-track {
+  border-radius: 0;
+  background: rgba(255, 254, 254, 0.1);
+}
+.two .name {
+  height: 50px;
+  color: #fff;
+  margin: 10px 0;
+  padding: 0 0 0 10px;
+  display: flex;
+}
+.two .name:first-child {
+  height: 50px;
+  color: #fff;
+  margin: 0 0 10px 0;
+}
+.two .name:last-child {
+  height: 50px;
+  color: #fff;
+  margin: 10px 0 0 0;
+}
+.two .name .icon {
+  padding: 15px 0 0 8px;
+}
+.two .name .title {
+  line-height: 50px;
+  font-size: 18px;
+}
+</style>

+ 25 - 0
src/main.ts

@@ -0,0 +1,25 @@
+import { createApp } from 'vue';
+import { createPinia } from 'pinia';
+
+import App from './App.vue';
+import router from './router';
+
+import './assets/main.css';
+
+import ElementPlus from 'element-plus';
+import 'element-plus/theme-chalk/index.css';
+import locale from 'element-plus/lib/locale/lang/zh-cn';
+import moment from 'moment';
+import _ from 'lodash';
+import * as ElementPlusIconsVue from '@element-plus/icons-vue';
+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;
+app.mount('#app');

+ 21 - 0
src/router/index.ts

@@ -0,0 +1,21 @@
+import { createRouter, createWebHistory } from 'vue-router';
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      component: () => import('@/layout/Home.vue'),
+      children: [
+        {
+          path: '/',
+          name: 'index',
+          meta: { title: '首页' },
+          component: () => import('@/views/index.vue'),
+        },
+      ],
+    },
+  ],
+});
+
+export default router;

+ 60 - 0
src/stores/counter.ts

@@ -0,0 +1,60 @@
+import { ref, computed } from 'vue';
+import { defineStore } from 'pinia';
+import { AxiosWrapper } from '@/util/axios-wrapper';
+import _ from 'lodash';
+
+import type { IQueryType, IQueryResult } from '@/util/types.util';
+const axios = new AxiosWrapper();
+const projectApi = {
+  url: '/api/util/dbInit/project',
+};
+const tableApi = {
+  url: '/api/util/dbInit/table',
+};
+export const useCounterStore = defineStore('counter', () => {
+  const count = ref(0);
+  const doubleCount = computed(() => count.value * 2);
+  function increment() {
+    count.value++;
+  }
+
+  const projectQuery = async ({ skip = 0, limit = undefined, ...info } = {}): Promise<IQueryResult> => {
+    let cond: IQueryType = {};
+    if (skip) cond.skip = skip;
+    if (limit) cond.limit = limit;
+    cond = { ...cond, ...info };
+    const res = await axios.$get(`${projectApi.url}`, cond);
+    return res;
+  };
+  const projectFetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${projectApi.url}/${payload}`);
+    return res;
+  };
+  const projectCreate = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$post(`${projectApi.url}`, payload);
+    return res;
+  };
+  const projecUpdate = async (payload: any): Promise<IQueryResult> => {
+    const id = _.get(payload, 'id', _.get(payload, '_id'));
+    const res = await axios.$post(`${projectApi.url}/${id}`, payload);
+    return res;
+  };
+  const projecDelete = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$delete(`${projectApi.url}/${payload}`);
+    return res;
+  };
+  const tabelQuery = async ({ skip = 0, limit = undefined, ...info } = {}): Promise<IQueryResult> => {
+    let cond: IQueryType = {};
+    if (skip) cond.skip = skip;
+    if (limit) cond.limit = limit;
+    cond = { ...cond, ...info };
+    const res = await axios.$get(`${tableApi.url}`, cond);
+    return res;
+  };
+  const tableFetch = async (payload: any): Promise<IQueryResult> => {
+    const res = await axios.$get(`${tableApi.url}/${payload}`);
+    return res;
+  };
+
+  return { count, doubleCount, increment, projectQuery, projectFetch, projectCreate, projecUpdate, projecDelete, tabelQuery, tableFetch };
+});

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

@@ -0,0 +1,151 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import _ from 'lodash';
+import Axios from 'axios';
+import { Util, Error } from 'naf-core';
+// import { Indicator } from 'mint-ui';
+import type { IOptionsType, IQueryType, IRequestResult } from './types.util';
+
+const { trimData, isNullOrUndefined } = Util;
+const { ErrorCode } = Error;
+
+let currentRequests = 0;
+
+// // 参数类型设置
+// type valueType = string | number | object | boolean | Array<any>;
+// type queryType = string | number | boolean;
+
+// export interface IQueryType {
+//   [props: string]: queryType;
+// }
+// export interface IOptionsType {
+//   [props: string]: valueType;
+// }
+
+// export interface IRequestResult {
+//   errcode: string | number;
+//   errmsg: string | number;
+//   details?: string;
+//   [props: string]: any;
+// }
+
+export class AxiosWrapper {
+  constructor({ baseUrl = '', 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 = sessionStorage.getItem('user');
+      const apiToken = sessionStorage.getItem('apiToken');
+      if (token) axios.defaults.headers.common['admin-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) sessionStorage.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();
+      }
+    }
+  }
+}

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

@@ -0,0 +1,22 @@
+// 参数类型设置
+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;
+}

+ 126 - 0
src/views/index.vue

@@ -0,0 +1,126 @@
+<template>
+  <el-col :span="24" class="main">
+    <el-col :span="24" class="head">
+      当前项目为:
+      <span>{{ data.name }}</span>
+    </el-col>
+    <el-col class="list" v-if="data.show == 'list'">
+      <Table @toAdd="toAdd" :list="list" @toEdit="toAdd"></Table>
+    </el-col>
+    <el-col class="form" v-else-if="data.show == 'form'">
+      <Form @toBack="toAdd" :form="form"></Form>
+    </el-col>
+  </el-col>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue';
+import { useCounterStore as useStore } from '@/stores/counter';
+import type { IQueryResult } from '@/util/types.util';
+import Form from './parts/form-1.vue';
+import Table from './parts/table-1.vue';
+const store = useStore();
+const data = reactive({
+  tableData: [],
+  id: ref(''),
+  name: '项目名称',
+  show: 'list',
+});
+let list: any = reactive([]);
+let id = ref('');
+const props = defineProps({ id: String });
+if (props.id) {
+  if (data.id == props.id) {
+    console.log(data.id);
+  } else {
+    id.value = props.id;
+    // 项目
+    const aee: IQueryResult = await store.projectFetch(props.id);
+    data.name = aee.data.name;
+    // 列表
+    const res: IQueryResult = await store.tabelQuery({ project: props.id });
+    list = res.data;
+  }
+}
+
+// list = props.data.list;
+let form: Object = reactive({
+  _id: '',
+  name: '',
+  columns: [],
+  name_zh: '',
+  remark: '',
+});
+
+const toAdd = async (query: { id: string; type: string }) => {
+  console.log(query.id);
+  if (query.id) {
+    const res: IQueryResult = await store.tableFetch(query.id);
+    form = res.data;
+  } else {
+    form = {};
+  }
+
+  data.id = query.id;
+  data.show = query.type;
+};
+</script>
+
+<style scoped>
+.main {
+  background: #d8abab;
+  padding: 24px;
+  height: 92vh;
+}
+.head {
+  height: 10vh;
+  background: #fff;
+  color: #000;
+  text-align: center;
+  font-size: 22px;
+  padding: 25px 0;
+}
+.head span {
+  color: rgb(13, 193, 64);
+  font-weight: 700;
+}
+.btn {
+  margin: 10px 0;
+  text-align: right;
+}
+.table {
+  background: #ccc;
+  /* overflow-y: scroll; */
+  color: #000;
+}
+.table::-webkit-scrollbar {
+  width: 1px;
+}
+.table::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(108, 43, 43, 0.2);
+}
+.table::-webkit-scrollbar-track {
+  border-radius: 0;
+  background: rgba(0, 0, 0, 0.1);
+}
+.form {
+  background: #fff;
+  overflow-y: scroll;
+  max-height: 75vh;
+  color: #000;
+  margin: 20px 0 0 0;
+  padding: 10px;
+}
+.form::-webkit-scrollbar {
+  width: 1px;
+}
+.form::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(108, 43, 43, 0.2);
+}
+.form::-webkit-scrollbar-track {
+  border-radius: 0;
+  background: rgba(0, 0, 0, 0.1);
+}
+</style>

+ 195 - 0
src/views/parts/form-1.vue

@@ -0,0 +1,195 @@
+<template>
+  <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" class="demo-form" status-icon>
+    <el-col :span="24" class="btn">
+      <el-button class="button" @click="toBack()">返回</el-button>
+      <el-button class="button" type="primary" @click="submitForm(formRef)"> 提交 </el-button>
+      <el-button class="button" @click="resetForm(formRef)">重置</el-button>
+    </el-col>
+    <el-col :span="24" class="form">
+      <el-form-item label="表名中文" prop="name">
+        <el-input v-model="form.name_zh" />
+      </el-form-item>
+      <el-form-item label="表名" prop="name">
+        <el-input v-model="form.name" />
+      </el-form-item>
+      <el-form-item label="备注" prop="desc">
+        <el-input v-model="form.remark" type="textarea" />
+      </el-form-item>
+      <el-form-item label="字段列表" prop="columns">
+        <el-col :span="24" style="margin: 0 0 10px 0; text-align: right">
+          <el-button type="primary" @click="toAdd()">添加字段</el-button>
+        </el-col>
+        <el-table :data="form.columns" border>
+          <el-table-column label="字段名" prop="title" align="center">
+            <template #default="{ row }">
+              <el-input v-model="row.title"></el-input>
+            </template>
+          </el-table-column>
+          <el-table-column label="字段中文" prop="zh" align="center">
+            <template #default="{ row }">
+              <el-input v-model="row.zh" type="text"></el-input>
+            </template>
+          </el-table-column>
+          <el-table-column label="字段类型" prop="type" align="center">
+            <template #default="{ row }">
+              <el-select v-model="row.type">
+                <el-option v-for="i in data.typeList" :key="i.value" :label="i.label" :value="i.value"> </el-option>
+              </el-select>
+            </template>
+          </el-table-column>
+          <el-table-column label="默认值" prop="def" align="center">
+            <template #default="{ row }">
+              <el-input v-model="row.def" type="text"></el-input>
+            </template>
+          </el-table-column>
+          <el-table-column label="是否为索引" prop="index" align="center">
+            <template #default="{ row }">
+              <el-radio-group v-model="row.index">
+                <el-radio v-for="i in data.groupList" :key="i.value" :label="i.value">{{ i.label }}</el-radio>
+              </el-radio-group>
+            </template>
+          </el-table-column>
+          <el-table-column label="是否必填" prop="required" align="center">
+            <template #default="{ row }">
+              <el-radio-group v-model="row.required">
+                <el-radio v-for="i in data.groupList" :key="i.value" :label="i.value">{{ i.label }}</el-radio>
+              </el-radio-group>
+            </template>
+          </el-table-column>
+          <el-table-column label="关联表" prop="ref" align="center">
+            <template #default="{ row }">
+              <el-input v-model="row.ref" type="text"></el-input>
+            </template>
+          </el-table-column>
+          <el-table-column label="关联显示字段(使用分号(;)分隔)" prop="getProp" align="center">
+            <template #default="{ row }">
+              <el-input v-model="row.getProp" type="text"></el-input>
+            </template>
+          </el-table-column>
+          <el-table-column label="描述" prop="remark" align="center">
+            <template #default="{ row }">
+              <el-input v-model="row.remark" type="text"></el-input>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" align="center" width="175">
+            <template #default="{ row }">
+              <el-button text type="danger" @click="toDel(row)">删除</el-button>
+              <el-button text type="primary" @click="toDel(row)">向下</el-button>
+              <el-button text type="success" @click="toDel(row)">向上</el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form-item>
+    </el-col>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+import { reactive, ref } from 'vue';
+import moment from 'moment';
+
+import type { FormInstance, FormRules } from 'element-plus';
+const emit = defineEmits(['toBack']);
+
+const formRef = ref<FormInstance>();
+let form: Object = reactive({
+  _id: '',
+  name: '',
+  columns: [],
+  name_zh: '',
+  remark: '',
+});
+const data = reactive({
+  typeList: [
+    { label: '字符串', value: 'String' },
+    { label: '布尔值', value: 'Boolean' },
+    { label: '数字', value: 'Number' },
+    { label: '金额', value: 'Money' },
+    { label: '对象', value: 'Object' },
+    { label: '集合', value: 'Array' },
+    { label: 'ObjectId', value: 'ObjectId' },
+    { label: 'Secret', value: 'Secret' },
+  ],
+  groupList: [
+    { label: '是', value: true },
+    { label: '否', value: false },
+  ],
+});
+const props = defineProps({ form: Object });
+console.log(props.form);
+
+form = props.form;
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: '表名', trigger: 'blur' }],
+});
+interface Arr {
+  id: number;
+  name?: string;
+  // [prop:string]: string;
+}
+
+const toAdd = () => {
+  let list: Array<Arr> = form.columns;
+  list.push({ id: moment().valueOf() });
+};
+
+const toDel = (row: {}) => {
+  console.log(row);
+};
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      console.log('submit!');
+    } else {
+      console.log('error submit!', fields);
+    }
+  });
+};
+
+const resetForm = (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  formEl.resetFields();
+};
+const toBack = () => {
+  let query: Object = { type: 'list' };
+  emit('toBack', query);
+};
+</script>
+
+<style>
+.btn {
+  height: 40px;
+  margin: 10px 0;
+  background: #7b9d93;
+  padding: 5px 5px;
+  text-align: center;
+}
+.btn .button {
+  margin: 0 16%;
+}
+.form {
+  max-height: 65vh;
+  background: #fff;
+  overflow-y: scroll;
+  margin: 20px 0 0 0;
+  padding: 10px;
+}
+/* .form::-webkit-scrollbar {
+  width: 1px;
+}
+.form::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(108, 43, 43, 0.2);
+}
+.form::-webkit-scrollbar-track {
+  border-radius: 0;
+  background: rgba(0, 0, 0, 0.1);
+} */
+.el-radio {
+  margin-right: 4px;
+}
+.el-button + .el-button {
+  margin-left: 0;
+}
+</style>

+ 63 - 0
src/views/parts/table-1.vue

@@ -0,0 +1,63 @@
+<template>
+  <el-col class="list">
+    <el-col :span="24" class="btn">
+      <el-button type="primary" size="large" @click="toAdd()">添加表</el-button>
+    </el-col>
+    <div class="table">
+      <el-table :data="list" border size="large" max-height="70vh" :cell-style="{ textAlign: 'center' }" :header-cell-style="{ 'text-align': 'center' }">
+        <el-table-column prop="name_zh" label="表名中文"> </el-table-column>
+        <el-table-column prop="name" label="表名"> </el-table-column>
+        <el-table-column prop="remark" label="备注"> </el-table-column>
+        <el-table-column fixed="right" label="操作">
+          <template #default="scope">
+            <el-button link type="primary" @click.prevent="toEdit(scope.row)">修改</el-button>
+            <el-button link type="warning">导出</el-button>
+            <el-button link type="warning">导出ts</el-button>
+            <el-button link type="danger">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+  </el-col>
+</template>
+<script setup lang="ts">
+import { reactive } from 'vue';
+let list: any = reactive([]);
+const props = defineProps({ list: Array });
+// eslint-disable-next-line vue/no-setup-props-destructure
+list = props.list;
+const emit = defineEmits(['toAdd', 'toEdit']);
+const toAdd = () => {
+  let query: Object = { type: 'form' };
+  emit('toAdd', query);
+};
+const toEdit = (row: { _id: string }) => {
+  let query: Object = { type: 'form', id: row._id };
+  emit('toEdit', query);
+};
+</script>
+
+<style scoped>
+.btn {
+  height: 50px;
+  margin: 10px 0;
+  text-align: right;
+  background: #86b2a5;
+}
+.table {
+  background: #ccc;
+  overflow-y: scroll;
+  color: #000;
+}
+.table::-webkit-scrollbar {
+  width: 1px;
+}
+.table::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: rgba(108, 43, 43, 0.2);
+}
+.table::-webkit-scrollbar-track {
+  border-radius: 0;
+  background: rgba(0, 0, 0, 0.1);
+}
+</style>

+ 8 - 0
tsconfig.config.json

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

+ 17 - 0
tsconfig.json

@@ -0,0 +1,17 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.web.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "compilerOptions": {
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    },
+    "isolatedModules": false,
+  },
+
+  "references": [
+    {
+      "path": "./tsconfig.config.json"
+    }
+  ]
+}

+ 25 - 0
vite.config.ts

@@ -0,0 +1,25 @@
+import { fileURLToPath, URL } from 'node:url';
+
+import { defineConfig } from 'vite';
+import vue from '@vitejs/plugin-vue';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+  server: {
+    port: 20001,
+    proxy: {
+      '/api/live/v0': {
+        target: 'http://broadcast.waityou24.cn',
+      },
+      '/api/util/dbInit': {
+        target: 'http://broadcast.waityou24.cn',
+      },
+    },
+  },
+  resolve: {
+    alias: {
+      '@': fileURLToPath(new URL('./src', import.meta.url)),
+    },
+  },
+});