lrf 1 jaar geleden
commit
a5e6ab65ab

+ 24 - 0
.gitignore

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

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+## hooks-vue-starter
+
+Use this template:
+
+```bash
+npx degit https://github.com/midwayjs/hooks/examples/vue ./hooks-app
+```
+
+Use `npm install` to install the dependencies
+
+## Commands
+
+- `npm run dev`: Starts the development server
+- `npm run build`: Builds the application for production
+- `npm run start`: Runs the application in production mode
+
+## File Structure
+
+- `src`: source code, include backend and frontend
+  - `api`: backend code
+  - `others`: frontend code
+- `public`: static files
+- `midway.config.ts`: project config
+- `index.html`: entry file

+ 10 - 0
auto-imports.d.ts

@@ -0,0 +1,10 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+export {}
+declare global {
+  const ElMessage: typeof import('element-plus/es')['ElMessage']
+  const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
+}

+ 37 - 0
components.d.ts

@@ -0,0 +1,37 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+declare module 'vue' {
+  export interface GlobalComponents {
+    ElAside: typeof import('element-plus/es')['ElAside']
+    ElButton: typeof import('element-plus/es')['ElButton']
+    ElCard: typeof import('element-plus/es')['ElCard']
+    ElCol: typeof import('element-plus/es')['ElCol']
+    ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDrawer: typeof import('element-plus/es')['ElDrawer']
+    ElForm: typeof import('element-plus/es')['ElForm']
+    ElFormItem: typeof import('element-plus/es')['ElFormItem']
+    ElHeader: typeof import('element-plus/es')['ElHeader']
+    ElIcon: typeof import('element-plus/es')['ElIcon']
+    ElInput: typeof import('element-plus/es')['ElInput']
+    ElMain: typeof import('element-plus/es')['ElMain']
+    ElOption: typeof import('element-plus/es')['ElOption']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElRow: typeof import('element-plus/es')['ElRow']
+    ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
+    ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ElText: typeof import('element-plus/es')['ElText']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+  }
+}

+ 14 - 0
index.html

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

+ 24 - 0
midway.config.ts

@@ -0,0 +1,24 @@
+import vue from '@vitejs/plugin-vue';
+import { defineConfig } from '@midwayjs/hooks-kit';
+import AutoImport from 'unplugin-auto-import/vite';
+import Components from 'unplugin-vue-components/vite';
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+export default defineConfig({
+  vite: {
+    plugins: [
+      vue(),
+      AutoImport({
+        resolvers: [ElementPlusResolver()],
+      }),
+      Components({
+        resolvers: [ElementPlusResolver()],
+      }),
+    ],
+    server: {
+      port: 27011,
+      hmr: {
+        overlay: false,
+      },
+    },
+  },
+});

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


+ 34 - 0
package.json

@@ -0,0 +1,34 @@
+{
+  "name": "midway-vue",
+  "private": true,
+  "version": "3.0.1",
+  "scripts": {
+    "dev": "hooks dev",
+    "build": "hooks build",
+    "start": "hooks start"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.1.0",
+    "@midwayjs/hooks": "^3.1.0",
+    "@midwayjs/hooks-kit": "^3.1.0",
+    "@midwayjs/koa": "^3.10.6",
+    "@midwayjs/rpc": "^3.1.0",
+    "@midwayjs/serve": "^3.1.0",
+    "element-plus": "^2.3.12",
+    "isomorphic-unfetch": "^3.1.0",
+    "lodash": "^4.17.21",
+    "mongodb": "^6.0.0",
+    "mongoose": "^7.5.0",
+    "vue": "^3.2.25",
+    "vue-router": "^4.2.4",
+    "vue3-ace-editor": "^2.2.3"
+  },
+  "devDependencies": {
+    "@types/lodash": "^4.14.198",
+    "@vitejs/plugin-vue": "^4.0.0",
+    "sass": "^1.67.0",
+    "typescript": "^4.9.4",
+    "unplugin-auto-import": "^0.16.6",
+    "unplugin-vue-components": "^0.25.2"
+  }
+}

BIN
public/favicon.ico


BIN
public/logo.png


+ 37 - 0
src/App.vue

@@ -0,0 +1,37 @@
+<script setup lang="ts">
+import { Sunny, Moon } from '@element-plus/icons-vue'
+import { ref } from 'vue';
+const mode = ref(true)
+const modeChange = (data) => {
+  const html = document.getElementsByTagName('html')[0]
+  if (data) {
+    html.className += ' dark'
+  } else {
+    html.className = html.className.replace(/(?:^|\s)dark(?!\S)/g, '')
+  }
+}
+modeChange(mode.value)
+document.oncontextmenu = (e) => {
+  e.preventDefault()
+}
+</script>
+
+<template>
+  <div class="app">
+    <el-row justify="end">
+      <el-col :span="4">
+        <el-switch v-model="mode" @change="modeChange" :active-icon="Moon" :inactive-icon="Sunny" />
+      </el-col>
+    </el-row>
+    <Suspense>
+      <router-view></router-view>
+    </Suspense>
+  </div>
+</template>
+
+<style>
+body {
+  margin: 0;
+
+}
+</style>

+ 5 - 0
src/api/config/config.default.ts

@@ -0,0 +1,5 @@
+import { MidwayConfig } from '@midwayjs/core';
+export default {
+  keys: '1666946133599_5073',
+  connectCacheKey: 'connectCacheKey',
+} as MidwayConfig;

+ 20 - 0
src/api/configuration.ts

@@ -0,0 +1,20 @@
+import { createConfiguration, hooks } from '@midwayjs/hooks';
+import * as Koa from '@midwayjs/koa';
+import configDefault from './config/config.default';
+import { connectCacheKey } from '../util/util';
+/**
+ * setup midway server
+ */
+export default createConfiguration({
+  imports: [Koa, hooks()],
+  importConfigs: [{ default: configDefault }],
+  onStop(container, app) {
+    // 停止所有连接
+    const connCacheObject: object = app.getAttr(connectCacheKey);
+    if (!connCacheObject) return;
+    for (const key in connCacheObject) {
+      const conn = connCacheObject[key];
+      conn.close();
+    }
+  },
+});

+ 51 - 0
src/api/curd.ts

@@ -0,0 +1,51 @@
+import { Api, Get, Post, Query } from '@midwayjs/hooks';
+import { Collection, Document, MongoClient, MongoClientOptions } from 'mongodb';
+import { getAppCache, getQuery, getDbConnectKey, getRandomString } from '../util/util';
+import { omit, get, pick } from 'lodash';
+import { ObjectId } from 'mongodb';
+export interface collectionOptions {
+  key: string;
+  db: string;
+  collection: string;
+}
+const getCollection = (options: collectionOptions) => {
+  const conn: MongoClient = getAppCache(options.key, getDbConnectKey());
+  if (!conn) return;
+  const db = conn.db(options.db);
+  if (!db) return false;
+  const collection = db.collection(options.collection);
+  return collection;
+};
+export const query = Api(Get(), Query<any>(), async () => {
+  const query = getQuery();
+  const options = pick(query, ['key', 'db', 'collection']);
+  const collection = getCollection(options);
+  if (!collection) return;
+  const filter = omit(query, ['key', 'db', 'collection', 'skip', 'limit']);
+  const skip = get(query, 'skip', 0);
+  const limit = get(query, 'limit', 0);
+  const data = await collection.find(filter).sort({ 'meta.createdAt': -1 }).skip(parseInt(skip)).limit(parseInt(limit)).allowDiskUse().toArray();
+  const total = await collection.countDocuments(filter);
+  return { data, total };
+});
+export const create = Api(Post(), async (data: object, options: collectionOptions) => {
+  const collection = getCollection(options);
+  if (!collection) return;
+  const result = await collection.insertOne(data);
+  return result;
+});
+export const updateOne = Api(Post(), async (data: Document, options: collectionOptions) => {
+  const collection = getCollection(options);
+  if (!collection) return;
+  const _id= new ObjectId(data._id);
+  delete data._id
+  const result = await collection.updateOne({ _id }, { $set: data });
+  return result;
+});
+export const deleteOne = Api(Post(), async (data: Document, options: collectionOptions) => {
+  const collection = getCollection(options);
+  if (!collection) return;
+  const _id= new ObjectId(data._id);
+  const result = await collection.deleteOne({ _id });
+  return result;
+});

+ 64 - 0
src/api/db.ts

@@ -0,0 +1,64 @@
+import { Api, Get, Post, Query } from '@midwayjs/hooks';
+import { MongoClient, MongoClientOptions } from 'mongodb';
+import { dbConnectInfo } from '../interface/interface';
+import { setAppCache, getAppCache, getQuery, getDbConnectKey, getRandomString } from '../util/util';
+/**
+ * mongodb连接
+ */
+export const DBConnect = Api(Post(), async (info: dbConnectInfo) => {
+  let url = `mongodb://`;
+  const options: MongoClientOptions = { monitorCommands: true };
+  if (info.authDb) options.authSource = info.authDb;
+  if (info.username && info.password) {
+    // 使用url作为连接缓存的key
+    url = `${url}${info.username}:${info.password}@`;
+    // options.auth = { username: info.username, password: info.password }; // 另一种连接方式
+  }
+  url = `${url}${info.ip}:${info.port}`;
+  try {
+    let conn: MongoClient = getAppCache(url) as MongoClient;
+    if (!conn) {
+      const connection = await MongoClient.connect(url, options);
+      const key = getRandomString();
+      setAppCache(key, connection, getDbConnectKey());
+      conn = connection;
+      return { url, key };
+    }
+  } catch (error) {
+    console.error(error);
+    return '数据库连接失败';
+  }
+});
+
+/**
+ * 数据库查询
+ */
+export const DataBaseList = Api(
+  Get(),
+  Query<{
+    key: string;
+  }>(),
+  async () => {
+    const query = getQuery();
+    const conn: MongoClient = getAppCache(query.key, getDbConnectKey()) as MongoClient;
+    if (!conn) return;
+    const db = conn.db('admin');
+    const dataBaseList = await db.command({ listDatabases: 1 });
+    return dataBaseList;
+  }
+);
+
+export const getCollectionList = Api(Get(), Query<{ key: string; dbName: string }>(), async () => {
+  const query = getQuery();
+  const conn: MongoClient = getAppCache(query.key, getDbConnectKey()) as MongoClient;
+  const db = conn.db(query.dbName);
+  const res = await db.collections();
+  const data = [];
+  for (const i of res) {
+    const obj = { name: i.collectionName, total: 0 };
+    const total = await i.countDocuments();
+    obj.total = total;
+    data.push(obj);
+  }
+  return data;
+});

+ 8 - 0
src/env.d.ts

@@ -0,0 +1,8 @@
+/// <reference types="vite/client" />
+
+declare module '*.vue' {
+  import type { DefineComponent } from 'vue';
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
+  const component: DefineComponent<{}, {}, any>;
+  export default component;
+}

+ 7 - 0
src/interface/interface.ts

@@ -0,0 +1,7 @@
+export class dbConnectInfo {
+  ip?: string = '127.0.0.1';
+  port?: string = '27017';
+  authDb?: string = 'admin';
+  username?: string;
+  password?: string;
+}

+ 12 - 0
src/main.ts

@@ -0,0 +1,12 @@
+import { createApp } from 'vue';
+import App from './App.vue';
+import router from './router';
+import * as ElementPlusIconsVue from '@element-plus/icons-vue';
+import 'element-plus/theme-chalk/dark/css-vars.css';
+import 'element-plus/theme-chalk/src/message-box.scss';
+const app = createApp(App);
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component);
+}
+app.use(router);
+app.mount('#app');

+ 17 - 0
src/router/index.ts

@@ -0,0 +1,17 @@
+import { createRouter, createWebHistory } from 'vue-router';
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/',
+      name: 'index',
+      component: () => import('../views/home.vue'),
+    },
+    {
+      path: '/dataBaseList',
+      name: 'dataBaseList',
+      component: () => import('../views/dataBaseList.vue'),
+    },
+  ],
+});
+export default router;

+ 95 - 0
src/util/util.ts

@@ -0,0 +1,95 @@
+import { useContext, useConfig } from '@midwayjs/hooks';
+import { Context } from '@midwayjs/koa';
+import { head, set, get } from 'lodash';
+import { randomBytes } from 'crypto';
+/**数据库连接缓存key */
+export const connectCacheKey = 'connectCacheKey';
+/**操作枚举 */
+enum operaEum {
+  SET,
+  GET,
+}
+/**获取路径数组 */
+const getPathArray = (path) => {
+  const arr = path.split('.');
+  return arr;
+};
+/**处理有路径的存/储操作
+ * @property object ctx/app
+ * @property key 最后的属性值
+ * @property path 到达key的路径
+ * @property target 值
+ * @property opera 操作
+ */
+const deal = (object: any, key: string, path: string, opera: operaEum, target?: any) => {
+  const arr = getPathArray(path);
+  const objectKey: string = head(arr);
+  let obj: object = object.getAttr(objectKey);
+  arr.shift();
+  arr.push(key);
+  const pathKey = arr.join('.');
+  if (opera === operaEum.SET) {
+    // 存
+    if (obj) {
+      obj = set(obj, pathKey, target);
+    } else {
+      obj = {};
+      obj = set(obj, pathKey, target);
+    }
+    object.setAttr(objectKey, obj);
+  } else if (opera === operaEum.GET) {
+    // 取
+    return get(obj, pathKey);
+  }
+};
+
+export const setAppCache = (key: string, target: any, path?: string) => {
+  const ctx = useContext<Context>();
+  const app = ctx.getApp();
+  if (path) {
+    deal(app, key, path, operaEum.SET, target);
+  } else {
+    app.setAttr(key, target);
+  }
+  return 'ok';
+};
+
+export const getAppCache = (key: string, path?: string) => {
+  const ctx = useContext<Context>();
+  const app = ctx.getApp();
+  if (path) {
+    return deal(app, key, path, operaEum.GET);
+  } else return app.getAttr(key);
+};
+
+export const setCtxCache = (key: string, target: any, path?: string) => {
+  const ctx = useContext<Context>();
+  if (path) {
+    deal(ctx, key, path, operaEum.SET, target);
+  } else {
+    ctx.setAttr(key, target);
+  }
+  return 'ok';
+};
+export const getCtxCache = (key: string, path?: string) => {
+  const ctx = useContext<Context>();
+  if (path) {
+    return deal(ctx, key, path, operaEum.GET);
+  } else return ctx.getAttr(key);
+};
+
+export const getConfig = (key: string) => {
+  return useConfig(key);
+};
+export const getDbConnectKey = () => {
+  return useConfig('connectCacheKey');
+};
+
+export const getQuery = () => {
+  const ctx = useContext();
+  return ctx.query;
+};
+
+export const getRandomString = () => {
+  return randomBytes(16).toString('hex').slice(0, 16);
+};

+ 80 - 0
src/views/dataBaseList.vue

@@ -0,0 +1,80 @@
+<template>
+  <div id="dataBaseList">
+    <el-container style="height:95vh;weight:100wh">
+      <el-header>
+        <el-row style="padding:20px" justify="space-around">
+          <el-col :span="6"><el-button @click="toHome" style="margin-right:5px">返回</el-button><el-tag>{{ url }}</el-tag></el-col>
+          <el-col :span="4">当前数据库: <el-tag>{{ dbName || '请选择数据库' }}</el-tag> </el-col>
+          <el-col :span="4">数据库总大小: <el-tag>{{ totalSize }} MB</el-tag></el-col>
+        </el-row>
+      </el-header>
+      <el-container>
+        <el-aside width="200px">
+          <el-table :data="dataBaseList" height="85vh" border stripe style="width: 100%" @row-click="toSearchCollections">
+            <el-table-column prop="name" align="center">
+              <template #header>
+                <el-text class="mx-1" type="primary" @click="searchDataBaseList">
+                  库
+                  <el-icon>
+                    <RefreshRight />
+                  </el-icon>
+                </el-text>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-aside>
+        <el-main>
+          <views :dbName="dbName" :list="collectionList"></views>
+        </el-main>
+      </el-container>
+    </el-container>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router'
+import { DataBaseList, getCollectionList } from '../api/db'
+import views from './main/views.vue'
+const route = useRoute();
+const router = useRouter();
+// #region 数据库操作部分
+/**数据库连接地址 */
+const url = computed(() => {
+  return route.query.url;
+})
+const key = computed(() => {
+  return route.query.key;
+})
+/**数据库列表 */
+const dataBaseList = ref([]);
+/**数据库占用磁盘大小(MB) */
+const totalSize = ref(0);
+/**查询数据库列表 */
+const searchDataBaseList = async () => {
+  const res = await DataBaseList({ query: { key: key.value as string } })
+  if (res) {
+    const { databases, totalSizeMb } = res;
+    dataBaseList.value = [...databases]
+    totalSize.value = totalSizeMb
+  }
+}
+const dbName = ref();
+const collectionList = ref([]);
+const toSearchCollections = async ({ name }) => {
+  const res = await getCollectionList({ query: { key: key.value as string, dbName: name } })
+  collectionList.value = res
+  dbName.value = name
+}
+// #endregion
+onMounted(() => {
+  searchDataBaseList()
+})
+
+const toHome = () => {
+  router.push('/')
+}
+
+</script>
+
+<style scoped></style>

+ 56 - 0
src/views/home.vue

@@ -0,0 +1,56 @@
+<template>
+  <div id="home">
+    <el-row justify="center" style="margin-top:30vh">
+      <el-col :span="8">
+        <el-form :model="form" label-width="120px" ref="formRef" :rules="rules">
+          <el-form-item label="ip" prop="ip">
+            <el-input v-model="form.ip" />
+          </el-form-item>
+          <el-form-item label="port" prop="port">
+            <el-input v-model="form.port" />
+          </el-form-item>
+          <el-form-item label="验证数据库" prop="authDb" required>
+            <el-input v-model="form.authDb" />
+          </el-form-item>
+          <el-form-item label="用户名" prop="username" required>
+            <el-input v-model="form.username" />
+          </el-form-item>
+          <el-form-item label="密码" prop="password" required>
+            <el-input v-model="form.password" type="password" show-password />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="toSubmit">连接</el-button>
+          </el-form-item>
+        </el-form>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { useRouter } from 'vue-router'
+import type { FormInstance, FormRules } from 'element-plus'
+import { dbConnectInfo } from '../interface/interface'
+import { DBConnect } from '../api/db'
+import { get } from 'lodash'
+const router = useRouter()
+const formRef = ref<FormInstance>();
+const form = ref<dbConnectInfo>(new dbConnectInfo());
+const rules = ref<FormRules<dbConnectInfo>>({
+  authDb: [{ required: true, trigger: 'blur', message: '请填写验证数据库' }],
+  username: [{ required: true, trigger: 'blur', message: '请填写用户名' }],
+  password: [{ required: true, trigger: 'blur', message: '请填写密码' }],
+})
+
+const toSubmit = async () => {
+  await formRef.value.validate(async (valid, fileds) => {
+    if (valid) {
+      const res = await DBConnect(form.value)
+      if (res) router.push({ path: '/dataBaseList', query: { key: get(res, 'key'), url: get(res, 'url') } })
+    }
+  })
+}
+</script>
+
+<style scoped></style>

+ 217 - 0
src/views/main/dataList.vue

@@ -0,0 +1,217 @@
+<template>
+  <div id="dataList" v-show="view === viewType.DATA">
+    <el-row style="margin:10px 5px" justify="space-between">
+      <el-col :span="4">
+        <search-bar :fields="getFields()" @query-search="querySearch"></search-bar>
+      </el-col>
+      <el-col :span="4">
+        <el-button type="success" plain @click="toAddPage">添加数据</el-button>
+      </el-col>
+      <el-col :span="4">
+        <el-button type="primary" plain @click="changeShowFields">显示字段</el-button>
+      </el-col>
+      <el-col :span="8">
+        <pages v-model:page="page" :limit="limit" :total="total" @page-search="pageSearch" />
+      </el-col>
+    </el-row>
+    <el-table :data="list" height="70vh" border stripe style="width: 100%" @row-contextmenu="rightClick"
+      @row-click="leftClick">
+      <el-table-column v-for="i in getFields()" :key="i.prop" :prop="i.prop" :label="i.label" align="center" />
+    </el-table>
+  </div>
+  <div v-if="view === viewType.EDIT">
+    <json-editor :value="rightClickTarget" :fields="getFields()" @to-back="viewBack" @to-save="editorSave" />
+  </div>
+  <div v-show="showRightMenu" ref="rightMenu" class="rightMenu">
+    <el-card class="box-card" body-style="padding:10px">
+      <el-row>
+        <el-col :span="24" v-for="i in opera" :key="i.label">
+          <el-button :type="i.type" text @click.prevent="rightMenuBtnClick(i.event)">{{ i.label }}</el-button>
+        </el-col>
+      </el-row>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { head } from 'lodash'
+import { ref, defineProps, computed } from 'vue';
+import { useRoute } from 'vue-router'
+import { query, create, updateOne, deleteOne, collectionOptions } from '../../api/curd'
+import searchBar from './searchBar.vue'
+import pages from './pages.vue'
+import jsonEditor from './jsonEditor.vue'
+import { get } from 'lodash'
+const route = useRoute();
+const props = defineProps<{
+  collection: string
+  dbName: string
+}>();
+const key = computed(() => {
+  return route.query.key;
+})
+
+// #region 分页,查询,查询条件
+/**当前页数 */
+const page = ref(1);
+/**分页大小 */
+const limit = ref(100);
+/**数据列表 */
+const list = ref([]);
+/**根据条件查询的数据总数 */
+const total = ref(0)
+// 获取列头
+const getFields = () => {
+  const obj = head(list.value);
+  interface IField {
+    prop: string
+    label: string
+  }
+  const fields: Array<IField> = [];
+  for (const key in obj) {
+    fields.push({ prop: key, label: key })
+  }
+  if (fields.length > 0) {
+    const i = fields.findIndex(f => f.prop === '_id')
+    if (i > -1) {
+      const r = fields.find(f => f.prop === '_id')
+      fields.splice(i, 1)
+      fields.unshift(r)
+    }
+  }
+  return fields
+}
+/**查询数据必备的数据 */
+const defaultQuery = computed(() => {
+  const obj: collectionOptions = { key: key.value as string, db: props.dbName, collection: props.collection }
+  return obj;
+})
+/**查询条件 */
+const queryObject = ref({});
+/**查询的执行函数 */
+const toSearch = async (s = 0, l = limit.value) => {
+  const qo = { skip: s, limit: l, ...defaultQuery.value, ...queryObject.value };
+  const res = await query({ query: qo })
+  list.value = get(res, 'data', []);
+  total.value = get(res, 'total', 0);
+}
+/**默认初始化查 */
+toSearch();
+/**因条件修改查询 */
+const querySearch = (q) => {
+  queryObject.value = q;
+  page.value = 1;
+  toSearch()
+}
+/**因分页修改查询 */
+const pageSearch = (skip: number) => {
+  toSearch(skip)
+}
+// #endregion
+
+const changeShowFields = () => {
+  ElMessage.error('没做呢!赶紧滴!')
+}
+
+/**视图类型 */
+enum viewType {
+  DATA,
+  EDIT
+};
+/**右键菜单 */
+const opera = [
+  {
+    label: '修改',
+    type: 'primary',
+    event: 'rightMenuToUpdate'
+  },
+  {
+    label: '删除',
+    type: 'danger',
+    event: 'rightMenuToDelete'
+  }
+]
+/**是否显示右键菜单 */
+const showRightMenu = ref(false);
+/**当前视图变量 */
+const view = ref(viewType.DATA)
+/**去添加数据,进入编辑页面 */
+const toAddPage = () => {
+  view.value = viewType.EDIT
+}
+/**json编辑器返回 */
+const viewBack = () => {
+  view.value = viewType.DATA
+}
+/**json编辑器保存 */
+const editorSave = async (data: object) => {
+  let result;
+  if ('_id' in data) {
+    // update
+    result = await updateOne(data, defaultQuery.value)
+  } else {
+    // create
+    result = await create(data, defaultQuery.value)
+  }
+  if (result) {
+    ElMessage.success('操作成功')
+    viewBack();
+    toSearch();
+  }
+}
+/**右键菜单的实例 */
+const rightMenu = ref()
+/**右键菜单的数据 */
+const rightClickTarget = ref()
+/**表格右键事件-召唤菜单框 */
+const rightClick = (row: object, column: object, e: any) => {
+  let menu = rightMenu.value as HTMLElement;
+  let x = e.clientX;
+  let y = e.clientY;
+  menu.style.top = `${y}px`
+  menu.style.left = `${x}px`;
+  showRightMenu.value = true;
+  rightClickTarget.value = row;
+}
+/**右键菜单统一事件 */
+const rightMenuBtnClick = (method) => {
+  const fobj = { rightMenuToUpdate, rightMenuToDelete }
+  fobj[method]()
+}
+/**表格右键事件-修改 */
+const rightMenuToUpdate = () => {
+  view.value = viewType.EDIT
+  leftClick();
+}
+/**表格右键事件-删除 */
+const rightMenuToDelete = () => {
+  leftClick();
+  ElMessageBox.confirm('确认删除数据?', '删除提示', {
+    confirmButtonText: '删除',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(async () => {
+    // 删除
+    const result = await deleteOne(rightClickTarget.value, defaultQuery.value)
+    if (result) {
+      ElMessage.success('操作成功');
+      toSearch()
+    }
+  })
+}
+/**表格左键-关闭右键菜单 */
+const leftClick = () => {
+  showRightMenu.value = false
+}
+</script>
+
+<style scoped>
+.box-card {
+  width: 80px;
+}
+
+.rightMenu {
+  position: fixed;
+  z-index: 9999
+}
+</style>

+ 102 - 0
src/views/main/jsonEditor.vue

@@ -0,0 +1,102 @@
+<template>
+  <div id="jsonEditor">
+    <el-row style="margin:10px">
+      <el-col :span="4">
+        <el-button @click="viewBack">返回</el-button>
+      </el-col>
+      <el-col :span="4">
+        <el-button @click="editorSave" type="success">保存</el-button>
+      </el-col>
+    </el-row>
+    <el-row :gutter="10">
+      <el-col :span="20">
+        <v-ace-editor v-model:value="ev" @init="initFail" lang="json" theme="chrome" :options="options"
+          class="ace-editor" />
+      </el-col>
+      <el-col :span="4">
+        <el-row>
+          <el-col>所有字段:</el-col>
+          <el-col v-for="i in fields" :key="i.prop">
+            <el-button text type="primary" @click="addProp(i.prop)">{{ i.label }}</el-button>
+          </el-col>
+        </el-row>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, defineEmits, defineProps, watch } from 'vue';
+import { isObject } from 'lodash';
+const ev = ref('{}')
+interface field {
+  label: string;
+  prop: string
+}
+const props = defineProps<{
+  value?: object
+  fields: Array<field>
+}>();
+watch(props.value, (val, oval) => {
+  if (isObject(val)) {
+    ev.value = JSON.stringify(val, null, 2);
+  }
+}, { immediate: true })
+import { VAceEditor } from 'vue3-ace-editor'
+import ace from 'ace-builds';
+import modeJsonUrl from 'ace-builds/src-noconflict/mode-json?url';
+ace.config.setModuleUrl('ace/mode/json', modeJsonUrl);
+import themeChromeUrl from 'ace-builds/src-noconflict/theme-chrome?url';
+ace.config.setModuleUrl('ace/theme/chrome', themeChromeUrl);
+const options = ref({
+  fontSize: 14,
+  tabSize: 2,
+  showPrintMargin: false,
+  highlightActiveLine: true,
+  showLineNumbers: true,
+  showGutter: true
+})
+//init
+const initFail = (editor: any) => {
+  // console.log(editor)
+}
+const $emit = defineEmits(['toBack', 'toSave'])
+const viewBack = () => {
+  $emit('toBack')
+}
+const editorSave = () => {
+  try {
+    const data = JSON.parse(ev.value)
+    $emit('toSave', data)
+  } catch (error) {
+    ElMessage.error('JSON 格式错误.请检查输入的内容!')
+  }
+}
+const addProp = (prop) => {
+  try {
+    let nowValue: any = ev.value;
+    nowValue = JSON.parse(nowValue);
+    if (prop in nowValue) {
+      console.log('line 80 in function:');
+      ElMessage({
+        message: `已存在 ${prop} 字段!`,
+        type: 'warning'
+      })
+      return;
+    }
+    nowValue[prop] = ''
+    ev.value = JSON.stringify(nowValue, null, 2);
+  } catch (error) {
+    ElMessage.error('JSON 格式错误.请检查输入的内容!')
+  }
+
+}
+
+</script>
+
+<style scoped>
+.ace-editor {
+  width: 100%;
+  height: 70vh;
+}
+</style>

+ 24 - 0
src/views/main/pages.vue

@@ -0,0 +1,24 @@
+<template>
+  <div id="pages">
+    <el-pagination :current-page="page" :page-size="limit" :background="true" layout="total, prev, pager, next"
+      :total="total" @current-change="cc" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, defineEmits, defineProps } from 'vue';
+const props = defineProps<{
+  page: number
+  limit: number
+  total: number
+}>();
+const $emit = defineEmits(['update:page', 'pageSearch'])
+const cc = (cp) => {
+  $emit('update:page', cp)
+  const skip = (cp - 1) * props.limit
+  $emit('pageSearch', skip)
+}
+
+</script>
+
+<style scoped></style>

+ 66 - 0
src/views/main/searchBar.vue

@@ -0,0 +1,66 @@
+<template>
+  <div id="searchBar">
+    <el-button @click="toShowDrawer">按条件查询</el-button>
+    <el-drawer v-model="showDrawer" title="查询" direction="rtl">
+      <el-button @click="addCondition" style="margin:10px 5px">添加查询条件</el-button>
+      <el-form label-width="150px" style="margin-bottom:5px">
+        <el-row>
+          <el-col v-for="(i, index) in conditionList">
+            <el-form-item :prop="i.prop">
+              <template #label>
+                <el-select v-model="i.prop" filterable allow-create placeholder="字段">
+                  <el-option v-for="item in fields" :key="item.prop" :label="item.label" :value="item.prop" />
+                </el-select>
+              </template>
+              <el-input v-model="i.value">
+                <template #append>
+                  <el-icon @click="removeCondition(index)" style="color:#F56C6C">
+                    <CircleClose />
+                  </el-icon>
+                </template>
+              </el-input>
+            </el-form-item>
+          </el-col>
+          <el-col>
+            <el-button @click="closeDrawer">取消</el-button>
+            <el-button type="primary" @click="submit">查询</el-button>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-drawer>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, defineEmits, defineProps } from 'vue';
+const props = defineProps<{
+  fields: Array<{ prop: string, label: string }>
+}>();
+const showDrawer = ref(false)
+const toShowDrawer = () => {
+  showDrawer.value = true
+}
+const closeDrawer = () => {
+  showDrawer.value = false;
+}
+const conditionList = ref([]);
+const addCondition = () => {
+  conditionList.value.push({})
+}
+const removeCondition = (i) => {
+  conditionList.value.splice(i, 1)
+}
+const $emit = defineEmits(['querySearch']);
+
+const submit = () => {
+  const query = {}
+  conditionList.value.map(i => {
+    if (i.prop) query[i.prop] = i.value
+  })
+  closeDrawer()
+  $emit('querySearch', query)
+}
+</script>
+
+<style scoped></style>
+

+ 63 - 0
src/views/main/views.vue

@@ -0,0 +1,63 @@
+<template>
+  <div id="views" style="padding:10px">
+    <el-tabs v-model="view" type="card" @edit="tabsEdit">
+      <el-tab-pane v-for="item in viewList" :key="item.name" :label="item.label" :name="item.name"
+        :closable="item.name === 'COLLECTION' ? false : true">
+        <template v-if="item.name === 'COLLECTION'">
+          <el-table :data="list" height="75vh" border stripe style="width: 100%" @row-click="toSearchCollectionData">
+            <el-table-column prop="name" label="表名" align="center" />
+            <el-table-column prop="total" label="数据量" align="center" />
+          </el-table>
+        </template>
+        <template v-else>
+          <data-list :collection="item.name" :dbName="dbName"></data-list>
+        </template>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup lang="ts">
+import DataList from './dataList.vue'
+import { ref, reactive, defineProps, computed } from 'vue';
+import { useRoute } from 'vue-router'
+const props = defineProps<{
+  dbName?: string,
+  list?: Array<object>
+}>();
+/**当前视图 */
+const view = ref('COLLECTION')
+const viewList = ref([{
+  label: '表',
+  name: 'COLLECTION'
+}])
+/**点击表列表事件:查询 */
+const toSearchCollectionData = async ({ name }) => {
+  const i = viewList.value.findIndex(f => f.name === name);
+  if (i > -1) return
+  viewList.value.push({ label: name, name })
+  view.value = name
+}
+
+const tabsEdit = (targetName, action) => {
+  // 只有删除,添加关了
+  const i = viewList.value.findIndex(f => f.name === targetName)
+  if (i > -1) {
+    viewList.value.splice(i, 1);
+    // 删除后,需要处理当前视图位置
+    const is_has = viewList.value[i];
+    if (is_has) view.value = is_has.name
+    else {
+      const last = viewList.value[viewList.value.length - 1]
+      if (last) view.value = last.name
+    }
+  }
+}
+
+</script>
+
+<style scoped>
+.el-col {
+  margin: 2.5px 0;
+}
+</style>

+ 20 - 0
tsconfig.json

@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "useDefineForClassFields": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "jsx": "preserve",
+    "sourceMap": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "lib": ["esnext", "dom"],
+    "skipLibCheck": true
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"],
+  "ts-node": {
+    "compilerOptions": {
+      "module": "commonjs"
+    }
+  }
+}