浏览代码

数据中台

xiaochan 1 年之前
父节点
当前提交
499e9f8779

+ 1 - 1
index.html

@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="zh-cmn-Hans">
 <head>
     <meta charset="UTF-8"/>
     <link rel="icon" href="/favicon.ico"/>

+ 2 - 2
package.json

@@ -1,8 +1,8 @@
 {
   "name": "init",
   "version": "0.0.0",
-  "scripts": {
-    "dev": "vite",
+  "scripts": {    "dev": "vite",
+    "dev:dev": "vite --mode=dev --port=8998",
     "start": "vite",
     "update:element": "npm install element-plus deps/element-plus.2.0.4.1.tar.gz",
     "build": "vite build --mode=production",

+ 4 - 1
src/App.vue

@@ -166,15 +166,18 @@ function getWindowSize() {
   const h = window.innerHeight - 96
   return {w, h}
 }
+
 </script>
 
 <style lang="scss">
 ::-webkit-scrollbar {
-  width: 6px;
+  width: 10px;
+  height: 10px;
 
   &:hover {
     cursor: pointer;
   }
+
 }
 
 ::-webkit-scrollbar-thumb {

+ 33 - 0
src/api/data-base/data-base.ts

@@ -0,0 +1,33 @@
+import RequestV2 from "../../utils/request-v2";
+import {TDataBaseConfig} from "../../ts-type/data-base-type";
+
+export function getDataBase() {
+    return RequestV2<TDataBaseConfig[]>({
+        url: '/dataBase/getDataBase',
+        method: 'post'
+    })
+}
+
+export function testConnectApi(data) {
+    return RequestV2({
+        url: '/dataBase/testConnect',
+        method: 'post',
+        data
+    })
+}
+
+export function addOrUpdateDataBase(data) {
+    return RequestV2({
+        url: '/dataBase/addOrUpdateDataBase',
+        method: 'post',
+        data
+    })
+}
+
+export function delById(id) {
+    return RequestV2({
+        url: '/dataBase/delById',
+        method: 'get',
+        params: {id}
+    })
+}

+ 61 - 0
src/api/data-base/sql-api.ts

@@ -0,0 +1,61 @@
+import requestV2 from "../../utils/request-v2";
+import XEUtils from "xe-utils";
+
+export async function getDataBaseSqlApiList() {
+    const res = await requestV2({
+        url: '/dataBase/api/getDataBaseSqlApiList',
+        method: 'post'
+    })
+    return XEUtils.toArrayTree(res, {
+        key: 'id',
+        parentKey: 'parentId'
+    })
+}
+
+export function testSqlApi(data) {
+    return requestV2({
+        url: '/dataBase/api/testSqlApi',
+        method: 'post',
+        data
+    })
+}
+
+export function updateSqlApi(data) {
+    return requestV2({
+        url: '/dataBase/api/updateSqlApi',
+        method: 'post',
+        data
+    })
+}
+
+export function addSqlApi(data) {
+    return requestV2({
+        url: '/dataBase/api/addSqlApi',
+        method: 'post',
+        data
+    })
+}
+
+export function updateNameSqlApi(data) {
+    return requestV2({
+        url: '/dataBase/api/updateNameSqlApi',
+        method: 'post',
+        data
+    })
+}
+
+export function delSqlApi(id: string) {
+    return requestV2({
+        url: '/dataBase/api/delSqlApi',
+        method: 'get',
+        params: {id}
+    })
+}
+
+export function publishApi(id: string, type: 0 | 1 | number) {
+    return requestV2({
+        url: '/dataBase/api/publishApi',
+        method: 'get',
+        params: {id, type}
+    })
+}

二进制
src/assets/data-base/sqlserver.png


+ 1 - 0
src/components/cy/message-box/src/index.vue

@@ -289,6 +289,7 @@ onMounted(async () => {
                     <el-input v-model="inputValue"
                               size="small"
                               v-else
+                              @keydown.prevent.enter="handelClose('confirm')"
                               :type="props.inputRows ? 'textarea' : 'text'"
                               :rows="props.inputRows"
                               :id="inputId"

+ 1 - 1
src/components/query-components/convert-sql.ts

@@ -47,7 +47,7 @@ const convertSql = (sqlStr: string, data: any): string => {
             // 获取 test =" 里面的内容
             let testReg = item.match(/test="(.+?)"/)[1]
             // 把 and 换成 && 把 or 换成 || 这样 js 才能判断
-            testReg = testReg.replace(/and/g, ' && ').replace(/or/g, ' || ')
+            testReg = testReg.replace(/\band\b/g, ' && ').replace(/\bor\b/g, ' || ')
             // 获取到 <if test="id != null and id != \'\'"> id = ${id} </if>
             // 被 if 包裹的元素
             if (dynamicJudgment(testReg, data, dataParam)) {

+ 31 - 3
src/icons/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 2473230 */
-  src: url('iconfont.woff2?t=1681957496590') format('woff2'),
-       url('iconfont.woff?t=1681957496590') format('woff'),
-       url('iconfont.ttf?t=1681957496590') format('truetype');
+  src: url('iconfont.woff2?t=1700549846502') format('woff2'),
+       url('iconfont.woff?t=1700549846502') format('woff'),
+       url('iconfont.ttf?t=1700549846502') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,34 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-quxiaofabu:before {
+  content: "\e663";
+}
+
+.icon-ceshi:before {
+  content: "\e6db";
+}
+
+.icon-baocun:before {
+  content: "\ec09";
+}
+
+.icon-fabu:before {
+  content: "\ec0a";
+}
+
+.icon-moduanshouqi_o:before {
+  content: "\eb98";
+}
+
+.icon-shangxiazhankai_o:before {
+  content: "\eb9a";
+}
+
+.icon-kantanshujuzhongtai:before {
+  content: "\e6a1";
+}
+
 .icon-zhantie:before {
   content: "\e636";
 }

二进制
src/icons/iconfont.ttf


二进制
src/icons/iconfont.woff


二进制
src/icons/iconfont.woff2


+ 2 - 1
src/layout/PageLayer.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-container>
+  <el-container style="box-sizing: border-box">
     <el-header v-if="useSlots().header" ref="headerRef" class="header">
       <slot name="header"></slot>
     </el-header>
@@ -79,5 +79,6 @@ onMounted(async () => {
   border-radius: 4px;
   padding: 4px;
   background-color: white;
+  box-sizing: border-box;
 }
 </style>

+ 19 - 2
src/router/modules/dashboard.js

@@ -1013,17 +1013,34 @@ const route = [
                 path: 'drugManage/DsyInfo',
                 component: createNameComponent(() => import('@/views/medical-advice/drug-manage/DsyInfo.vue')),
                 meta: {title: '大输液统计'},
-            },{
+            }, {
                 path: 'executeItem/yzChange',
                 component: createNameComponent(() => import('@/views/medical-advice/execute-item/YzChange.vue')),
                 meta: {title: '医嘱变更单'},
-            },{
+            }, {
                 path: 'executeItem/yzExecuteSignature',
                 component: createNameComponent(() => import('@/views/medical-advice/execute-item/YzExecuteSignature.vue')),
                 meta: {title: '医嘱执行签名'},
             },
         ],
     },
+    {
+        path: '/dataBase',
+        component: Layout,
+        meta: {title: '数据中台'},
+        children: [
+            {
+                path: 'dataBaseManage',
+                meta: {title: '数据源管理'},
+                component: createNameComponent(() => import('@/views/data-base/DataSourceManagement.vue'))
+            },
+            {
+                path: 'dataBaseApi',
+                meta: {title: "api管理"},
+                component: createNameComponent(() => import('@/views/data-base/data-base-api/src/DataBaseApi.vue'))
+            }
+        ]
+    },
     {
         path: '/targetManagement',
         component: Layout,

+ 45 - 0
src/ts-type/data-base-type.ts

@@ -0,0 +1,45 @@
+export interface TDataBaseConfig {
+    id: number;
+    name: string;
+    url: string;
+    dataName: string;
+    dataPwd: string;
+    config: string;
+    driverClassName: string;
+    alias: string;
+    configJson: {
+        minIdle: number;
+        testOnBorrow: boolean;
+        testWhileIdle: boolean;
+        testOnReturn: boolean;
+        initialSize: number;
+        maxActive: number;
+    };
+}
+
+export interface TDataBaseSqlApi {
+    id: string;
+    name: string
+    config: string;
+    dataBaseId: number;
+    sql: string;
+    requestMapping: string;
+    enable: number
+    parentId: string | null
+    type: number
+    configJson: {
+        params: {
+            key: string,
+            defaultValue: any
+            required: boolean
+            describe: string
+        }[]
+        pageInfo: {
+            page: boolean
+            pageSize: number
+        },
+        removeSpaces: boolean
+    }
+    children?: TDataBaseSqlApi[]
+    params?: any
+}

+ 321 - 0
src/views/data-base/DataSourceManagement.vue

@@ -0,0 +1,321 @@
+<script setup lang="ts">
+import sqlserver from '@/assets/data-base/sqlserver.png'
+import {nextTick, onMounted, ref} from "vue";
+import PageLayer from "@/layout/PageLayer.vue";
+import {addOrUpdateDataBase, delById, getDataBase, testConnectApi} from "@/api/data-base/data-base";
+import {TDataBaseConfig} from "@/ts-type/data-base-type";
+import XEUtils from "xe-utils";
+import * as monaco from "monaco-editor";
+import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
+import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
+import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
+import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
+import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
+import {FormInstance, FormRules} from "element-plus";
+import {CyMessageBox} from "@/components/cy/message-box";
+
+// 代码提示
+self.MonacoEnvironment = {
+  getWorker(_: string, label: string) {
+    if (label === 'json') {
+      return JsonWorker();
+    }
+    if (label === 'css' || label === 'scss' || label === 'less') {
+      return CssWorker();
+    }
+    if (label === 'html' || label === 'handlebars' || label === 'razor') {
+      return HtmlWorker();
+    }
+    if (['typescript', 'javascript'].includes(label)) {
+      return TsWorker();
+    }
+    return EditorWorker();
+  },
+}
+
+interface TDataBaseConfig2 extends TDataBaseConfig {
+  show?: boolean
+}
+
+const dataSource = ref<TDataBaseConfig2[]>([])
+
+const dialog = ref(false)
+const currentData = ref<TDataBaseConfig2>()
+
+function editClick(row: TDataBaseConfig2) {
+  dialog.value = true
+  currentData.value = XEUtils.clone(row, true)
+  currentData.value.dataPwd = ""
+}
+
+const jsEdit = ref<HTMLDivElement>()
+let monacoEditor = null
+
+
+async function opened() {
+  await nextTick()
+  monacoEditor = monaco.editor.create(jsEdit.value, {
+    value: currentData.value.config,
+    language: 'json',
+    automaticLayout: true,
+    theme: 'vs-dark',
+    foldingStrategy: 'indentation',
+    renderLineHighlight: 'all',
+    selectOnLineNumbers: true,
+    minimap: {
+      enabled: false,
+    },
+    readOnly: false,
+    fontSize: 16,
+    scrollBeyondLastLine: false,
+    overviewRulerBorder: true,
+  })
+  monacoEditor.onDidChangeModelContent((val) => {
+    currentData.value.config = monacoEditor.getValue();
+  })
+}
+
+const formRef = ref<FormInstance>()
+const rules = ref<FormRules<typeof currentData>>({
+  name: [{
+    required: true,
+    message: '此项必填',
+    trigger: 'change',
+  }],
+  alias: [{
+    required: true,
+    message: '此项必填',
+    trigger: 'change',
+  }],
+  url: [{
+    required: true,
+    message: '此项必填',
+    trigger: 'change',
+  }],
+  dataName: [{
+    required: true,
+    message: '此项必填',
+    trigger: 'change',
+  }]
+})
+
+function closed() {
+  monacoEditor.dispose()
+}
+
+async function testConnect(data: TDataBaseConfig2) {
+  const res = await testConnectApi(data)
+  console.log('测试连接', res)
+  return res
+}
+
+async function saveClick() {
+  await formRef.value.validate()
+  if (await testConnect(currentData.value)) {
+    await addOrUpdateDataBase(currentData.value)
+    dialog.value = false
+    if (currentData.value.id == null) {
+      query()
+    }
+  }
+}
+
+function addDataBaseClick() {
+  currentData.value = {
+    alias: "",
+    config: "{\n" +
+        "    \"initialSize\": 10,\n" +
+        "    \"maxActive\": 10,\n" +
+        "    \"minIdle\": 10,\n" +
+        "    \"testOnBorrow\": false,\n" +
+        "    \"testOnReturn\": false,\n" +
+        "    \"testWhileIdle\": false\n" +
+        "}",
+    configJson: {
+      initialSize: 10,
+      maxActive: 10,
+      minIdle: 10,
+      testOnBorrow: false,
+      testOnReturn: false,
+      testWhileIdle: false
+    },
+    dataName: "",
+    dataPwd: "",
+    driverClassName: "com.microsoft.sqlserver.jdbc.SQLServerDriver",
+    id: null,
+    url: "",
+    name: '',
+    show: false
+  }
+  dialog.value = true;
+}
+
+async function delByIdClick(id) {
+  await CyMessageBox.alert({
+    type: 'delete',
+    message: '确认删除'
+  })
+  await delById(id)
+  query()
+}
+
+function query() {
+  getDataBase().then(res => {
+    dataSource.value = res
+  })
+}
+
+onMounted(() => {
+  query()
+})
+</script>
+
+<template>
+  <PageLayer>
+    <template #header>
+            <span class="span_title">
+        数据源管理
+      </span>
+      <div class="add-data_source">
+        <el-button type="primary" icon="Plus" @click="addDataBaseClick">新增</el-button>
+      </div>
+    </template>
+    <template #main>
+      <div style="width: calc(100% - 10px); height: 100%; padding: 5px;box-sizing: border-box">
+        <el-row :gutter="20" class="data_source-row">
+          <el-col :span="6" v-for="item in dataSource">
+            <div class="data_source-col">
+              <div class="item_img"
+                   @mouseover.prevent.stop="item.show = true"
+                   @mouseleave.prevent.stop="item.show = false">
+                <div class="mark_info" v-show="item.show">
+                  <el-button type="primary" @click="editClick(item)">编辑</el-button>
+                  <el-button type="danger" @click="delByIdClick(item.id)">删除</el-button>
+                </div>
+                <img :src="sqlserver" alt="数据库">
+              </div>
+              <br/>
+              <div style="padding: 0 10px ">
+                名称: {{ item.name }}
+                <el-divider direction="vertical"></el-divider>
+                别名: {{ item.alias }}
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+    </template>
+  </PageLayer>
+
+  <el-dialog v-model="dialog" title="配置详情" @opened="opened" @closed="closed">
+    <el-form label-position="right" label-width="100px" ref="formRef" :rules="rules" :model="currentData">
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="currentData.name"/>
+      </el-form-item>
+      <el-form-item label="别名" prop="alias">
+        <el-input v-model="currentData.alias"/>
+      </el-form-item>
+
+      <el-form-item label="地址连接" prop="url">
+        <el-input v-model="currentData.url"/>
+      </el-form-item>
+
+      <el-form-item label="数据库用户名" prop="dataName">
+        <el-input v-model="currentData.dataName"/>
+      </el-form-item>
+
+      <el-form-item label="数据库密码" prop="dataPwd">
+        <el-input v-model="currentData.dataPwd" show-password/>
+      </el-form-item>
+
+      <el-form-item label="额外配置">
+        <div ref="jsEdit" style='width: 100%; height: 200px'>
+        </div>
+      </el-form-item>
+
+    </el-form>
+    <template #footer>
+      <el-button type="primary" @click="saveClick">保存</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style scoped lang="scss">
+.span_title {
+  position: relative;
+  display: inline-block;
+  padding: 0 30px;
+  line-height: 34px;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+  font-size: 14px;
+
+  &::after {
+    content: "";
+    width: 5px;
+    height: 15px;
+    background-color: rgb(225, 33, 80);
+    position: absolute;
+    left: 20px;
+    top: 10px;
+    box-sizing: border-box;
+  }
+}
+
+.data_source-main {
+  border-radius: 5px;
+  overflow: auto;
+  box-shadow: var(--el-box-shadow);
+  background: white;
+
+}
+
+.add-data_source {
+  -webkit-box-flex: 1;
+  flex: 1;
+  padding: 0 20px;
+  text-align: right;
+  margin: 0;
+  height: 100%;
+}
+
+.data_source-row {
+  .data_source-col {
+    padding: 5px;
+    border-radius: 5px;
+    height: 170px;
+    background-color: rgb(255, 255, 255);
+    box-shadow: var(--el-box-shadow);
+
+    .item_img {
+      box-sizing: border-box;
+      margin: 0;
+      padding: 0;
+      position: relative;
+      height: 129px;
+      overflow: hidden;
+
+      .mark_info {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        z-index: 1000;
+        text-align: center;
+        background-color: var(--el-overlay-color-lighter);
+      }
+
+      img {
+        display: block;
+        margin: 0 auto;
+        padding: 20px 0;
+        border-style: none;
+      }
+    }
+  }
+}
+</style>

+ 491 - 0
src/views/data-base/data-base-api/components/ApiTabPane.vue

@@ -0,0 +1,491 @@
+<script setup lang="ts">
+import {useVModel} from "@vueuse/core";
+import {nextTick, onMounted, onUnmounted, ref, Ref, unref} from 'vue'
+import {TDataBaseSqlApi} from "@/ts-type/data-base-type";
+import * as monaco from "monaco-editor";
+import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
+import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
+import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
+import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
+import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
+import XcElOption from "@/components/xiao-chan/xc-el-option/XcElOption.vue";
+import XEUtils from "xe-utils";
+import {publishApi, testSqlApi, updateSqlApi} from "@/api/data-base/sql-api";
+import {FormInstance} from "element-plus";
+import {xcMessage} from "@/utils/xiaochan-element-plus";
+import {copyStrFunc, isDev} from "@/utils/public";
+import {windowSizeStore} from "@/utils/store-public";
+
+// 代码提示
+self.MonacoEnvironment = {
+  getWorker(_: string, label: string) {
+    if (label === 'json') {
+      return JsonWorker();
+    }
+    if (label === 'css' || label === 'scss' || label === 'less') {
+      return CssWorker();
+    }
+    if (label === 'html' || label === 'handlebars' || label === 'razor') {
+      return HtmlWorker();
+    }
+    if (['typescript', 'javascript'].includes(label)) {
+      return TsWorker();
+    }
+    return EditorWorker();
+  },
+}
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    default: null
+  },
+  dataBase: {
+    type: Array,
+    default: []
+  }
+})
+
+const urlPrefix = isDev ? `http://172.16.30.61:8706/dataBase/api/data` : `http://172.16.32.160:8077/dataBase/api/data`;
+
+const emits = defineEmits(['update:modelValue'])
+const modelValue: Ref<TDataBaseSqlApi> = useVModel(props, 'modelValue', emits) as Ref<TDataBaseSqlApi>
+const sqlEditor = ref<HTMLDivElement>()
+const drawerDialog = ref(false)
+const pageSizeList = [{code: 30, name: '30'}, {code: 50, name: '50'}, {code: 100, name: '100'}]
+
+const testParams = ref({})
+
+let monacoEditor = null
+
+function handleSave() {
+  const dataTemp = prepareData()
+  console.log(dataTemp)
+  updateSqlApi(dataTemp)
+}
+
+const testDialog = ref(false)
+const testFormRef = ref<FormInstance>()
+const testRules = ref({})
+const testResData = ref<{
+  code?: string,
+  message?: string
+  data?: any
+}>({})
+
+function handleDialogOpen() {
+  testRules.value = {}
+  testParams.value = {}
+  modelValue.value.configJson.params.forEach(item => {
+    testParams.value[item.key] = item.defaultValue || ''
+    if (item.required) {
+      testRules.value[item.key] = [{
+        required: true,
+        message: '此项必填',
+        trigger: 'change',
+      }]
+    }
+  })
+  if (modelValue.value.configJson.pageInfo.page) {
+    testParams.value['currentPage'] = 1
+
+    testRules.value['currentPage'] = [{
+      required: true,
+      message: '此项必填',
+      trigger: 'change',
+    }]
+
+  }
+  testDialog.value = true
+}
+
+function prepareData() {
+  const data = XEUtils.clone(modelValue.value, true)
+  data.config = XEUtils.toJSONString(data.configJson)
+  return data
+}
+
+async function handleTest() {
+  await testFormRef.value.validate()
+
+  const data = prepareData()
+  data.params = unref(testParams)
+
+  testSqlApi(data).then(res => {
+    console.log(res)
+    testResData.value.code = '200'
+    testResData.value.message = '成功'
+    testResData.value.data = res
+  }).catch((res) => {
+    testResData.value.code = '1002'
+    testResData.value.message = res
+    testResData.value.data = null
+  })
+}
+
+function handlePublishApi() {
+  modelValue.value.enable = modelValue.value.enable === 0 ? 1 : 0
+  publishApi(modelValue.value.id, modelValue.value.enable)
+}
+
+function handleAddParams() {
+  modelValue.value.configJson.params.push({
+    key: '',
+    required: false,
+    defaultValue: '',
+    describe: ''
+  })
+}
+
+function handleParamsClose(index) {
+  modelValue.value.configJson.params.splice(index, 1)
+}
+
+const paramsFormMap = new Map<string, FormInstance>()
+const paramsFormRules = {
+  key: [{
+    required: true,
+    message: '此项必填',
+    trigger: 'change',
+  }]
+}
+
+function setParamsFormMap(name: string, ref: FormInstance) {
+  paramsFormMap.set(name, ref)
+}
+
+async function drawerClose(done: (cancel?: boolean) => void) {
+  for (let key of paramsFormMap.keys()) {
+    const value = paramsFormMap.get(key)
+    if (value == null) break
+    try {
+      await value.validate();
+    } catch (e) {
+      xcMessage.error('请求参数中,有必填项不能为空。')
+      done(true)
+      return
+    }
+  }
+  done(false)
+}
+
+function copyUrl(url: string) {
+  copyStrFunc(url)
+}
+
+function fillingFunc() {
+  const str = modelValue.value.sql;
+  const regex = /\{(.*?)\}/g;
+  const matches = str.match(regex);
+  if (!matches) {
+    return
+  }
+
+  const keys = new Set()
+  const addKeys = new Set()
+
+  matches.forEach(item => {
+    addKeys.add(item.slice(1, -1).trim())
+  })
+
+  modelValue.value.configJson.params.forEach(item => {
+    keys.add(item.key)
+  })
+
+  const result1 = new Set([...addKeys].filter(x => !keys.has(x)));
+  const result2 = new Set([...keys].filter(x => !addKeys.has(x)));
+
+  XEUtils.remove(modelValue.value.configJson.params, (item) => {
+    return result2.has(item.key)
+  })
+
+  result1.forEach(item => {
+    modelValue.value.configJson.params.push({
+      key: item,
+      required: false,
+      describe: '',
+      defaultValue: null
+    })
+  })
+
+}
+
+function processingOpenProperties() {
+  fillingFunc()
+  drawerDialog.value = true
+}
+
+onUnmounted(() => {
+  monacoEditor.dispose()
+})
+
+onMounted(async () => {
+  await nextTick()
+  monacoEditor = monaco.editor.create(sqlEditor.value, {
+    value: modelValue.value.sql,
+    language: 'sql',
+    automaticLayout: true,
+    theme: 'vs-dark',
+    foldingStrategy: 'indentation',
+    formatOnType: true,
+    renderLineHighlight: 'all',
+    selectOnLineNumbers: true,
+    minimap: {
+      enabled: false,
+    },
+    readOnly: false,
+    fontSize: 16,
+    scrollBeyondLastLine: false,
+    overviewRulerBorder: true,
+  })
+
+  monacoEditor.onDidChangeModelContent((val) => {
+    modelValue.value.sql = monacoEditor.getValue();
+  })
+})
+</script>
+
+<template>
+  <div class="api_pane-container">
+    <div class="api_pane-header">
+      <span class="iconfont icon-baocun" @click="handleSave" title="保存"/>
+      <span class="iconfont icon-ceshi" title="测试" @click="handleDialogOpen"></span>
+      <el-button
+          @click="handlePublishApi"
+          :type="modelValue.enable ? 'warning' : 'success'">
+        {{ modelValue.enable ? '停用' : '启用' }}
+      </el-button>
+      <el-divider direction="vertical"/>
+      <span style="font-size: 12px;margin: 0">
+          数据库:
+      </span>
+
+      <!--   multiple   -->
+      <el-select v-model="modelValue.dataBaseId"
+                 placeholder="请选择"
+                 size="small"
+                 style="width: 120px">
+        <el-option v-for="item in props.dataBase" :label="item.name" :value="item.id"/>
+      </el-select>
+      <el-divider direction="vertical"/>
+      <el-button @click="processingOpenProperties" type="primary">属性</el-button>
+    </div>
+
+    <div class="api_pane-main">
+      <div ref="sqlEditor" class="editor"></div>
+    </div>
+  </div>
+
+  <el-drawer v-model="drawerDialog" title="配置" size="40%" class="api_pane-drawer" :before-close="drawerClose">
+    <div class="api_pane-drawer-main">
+      <div style="height: 70%; overflow: auto">
+        <el-divider style="margin: 12px 0">
+          请求参数
+          <el-button-group>
+            <el-button text icon="Plus" @click="handleAddParams"/>
+            <el-button @click="fillingFunc">填充</el-button>
+          </el-button-group>
+        </el-divider>
+        <div v-for="(item,index) in modelValue.configJson.params" style="padding: 0 40px 0 20px">
+          <el-divider style="margin: 10px 0" border-style="dashed" v-if="index !== 0"/>
+          <el-form label-width="50px"
+                   :model="item"
+                   :rules="paramsFormRules"
+                   label-position="right"
+                   :ref="(el) => setParamsFormMap(`form${index}`, el)">
+            <el-row>
+              <el-col :span="7">
+                <el-form-item label="名称" prop="key">
+                  <el-input v-model.trim="item.key"/>
+                </el-form-item>
+              </el-col>
+              <el-col :span="7">
+                <el-form-item label="必填">
+                  <el-switch v-model.trim="item.required"/>
+                </el-form-item>
+              </el-col>
+              <el-col :span="10">
+                <el-form-item label="默认值">
+                  <el-input v-model.trim="item.defaultValue"/>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-row>
+              <el-col :span="24">
+                <el-form-item label="描述">
+                  <div style="width: 100%;position: relative">
+                    <div class="params_close">
+                      <el-icon :size="18" @click="handleParamsClose(index)">
+                        <CircleClose/>
+                      </el-icon>
+                    </div>
+                    <el-input v-model.trim="item.describe"/>
+                  </div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </div>
+      </div>
+      <div style="flex: 1 ;overflow: auto">
+        <div style="padding: 0 20px">
+          <el-divider style="margin: 10px 0">其他</el-divider>
+          <el-form label-width="70px" label-position="right">
+            <el-row :gutter="10">
+
+              <el-col :span="24">
+                <el-form-item label="去除空格">
+                  <el-switch v-model="modelValue.configJson.removeSpaces"></el-switch>
+                </el-form-item>
+              </el-col>
+
+              <el-col :span="24">
+                <el-form-item label="请求方式">
+                  <el-select v-model="modelValue.requestMapping">
+                    <xc-el-option :data="[{code: 'GET', name: 'GET'},{code: 'POST', name: 'POST'}]"/>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+
+              <el-col :span="24">
+                <el-form-item label="分页">
+                  <el-switch v-model="modelValue.configJson.pageInfo.page"></el-switch>
+                </el-form-item>
+              </el-col>
+
+              <el-col :span="24" v-if="modelValue.configJson.pageInfo.page">
+                <el-form-item label="页大写">
+                  <el-select v-model="modelValue.configJson.pageInfo.pageSize">
+                    <xc-el-option :data="pageSizeList"/>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+
+            </el-row>
+
+          </el-form>
+        </div>
+      </div>
+    </div>
+  </el-drawer>
+
+  <el-dialog title="预览" v-model="testDialog" fullscreen>
+    <el-descriptions
+        class="margin-top"
+        border
+        :title="modelValue.name"
+        :column="3"
+        size="small">
+      <el-descriptions-item label="地址" :span="3">
+        {{ `${urlPrefix}/${modelValue.id}` }}
+        <el-button icon="CopyDocument" circle
+                   @click="copyUrl(`${urlPrefix}/${modelValue.id}`)"/>
+      </el-descriptions-item>
+      <el-descriptions-item label="请求方式">
+        {{ modelValue.requestMapping }}
+      </el-descriptions-item>
+      <el-descriptions-item label="分页">
+        {{ modelValue.configJson.pageInfo.page ? `分页,页大小${modelValue.configJson.pageInfo.pageSize}` : '不分页' }}
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <el-divider border-style="dashed">参数</el-divider>
+
+    <el-form label-width="100px"
+             label-position="right"
+             :rules="testRules"
+             :model="testParams"
+             ref="testFormRef">
+      <el-form-item v-for="item in modelValue.configJson.params"
+                    :label="item.key"
+                    :prop="item.key">
+        <el-input v-model="testParams[item.key]"/>
+      </el-form-item>
+
+      <el-form-item label="页码" prop="currentPage" v-if="modelValue.configJson.pageInfo.page">
+        <el-input-number v-model="testParams.currentPage"/>
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" @click="handleTest">测试</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-divider border-style="dashed">结果 {{ modelValue.configJson.pageInfo.page ? '' : '测试环境默认10条' }}
+    </el-divider>
+
+    <div :style="{height: windowSizeStore.h / 1.8 + 'px'}" style="overflow: auto">
+      <JsonViewer :value="testResData"
+                  style="height: 100%" copyable :expandDepth="3"/>
+    </div>
+
+
+  </el-dialog>
+
+</template>
+
+<style lang="scss">
+.api_pane-container {
+  display: flex;
+  height: 100%;
+  flex-flow: column nowrap;
+}
+
+.api_pane-header {
+  line-height: 20px;
+  font-size: 20px;
+  padding: 5px 10px;
+  box-sizing: border-box;
+  border-right: 1px solid #E4E7ED;
+  border-left: 1px solid #E4E7ED;
+
+  .iconfont {
+    cursor: pointer;
+    margin: 0 10px;
+  }
+}
+
+.api_pane-main {
+  flex: 1;
+  width: 100%;
+
+  .editor {
+    height: 100%;
+    width: 100%;
+  }
+}
+
+.api_pane-database {
+  padding: 0 10px;
+  height: 40px;
+}
+
+.api_pane-drawer {
+  .api_pane-drawer-main {
+    height: 100%;
+    width: 100%;
+    overflow: auto;
+    display: flex;
+    flex-flow: column nowrap;
+  }
+
+  .el-drawer__header {
+    margin-bottom: 10px;
+  }
+
+  .el-drawer__body {
+    padding: 5px;
+  }
+
+  .params_close {
+    position: absolute;
+    top: 0;
+    right: -30px;
+    display: flex;
+    align-items: center;
+    height: 100%;
+    cursor: pointer;
+  }
+}
+</style>

+ 254 - 0
src/views/data-base/data-base-api/components/ApiTree.vue

@@ -0,0 +1,254 @@
+<script setup lang="ts">
+import {onMounted, ref} from "vue";
+import {addSqlApi, delSqlApi, getDataBaseSqlApiList, updateNameSqlApi} from "@/api/data-base/sql-api";
+import {useCompRef} from "@/utils/useCompRef";
+import {ElTree} from "element-plus";
+import {CyMessageBox} from "@/components/cy/message-box";
+import RightClickMenu from "@/components/menu-item/RightClickMenu.vue";
+
+const emits = defineEmits(['node-click', 'del-api'])
+const expandBoolean = ref(false)
+const searchInput = ref('')
+const treeRef = useCompRef(ElTree)
+
+const position = ref()
+const opt = [
+  {
+    name: '新建文件',
+    click: (data) => newFile(data, 1)
+  },
+  {
+    name: '新建文件夹',
+    click: (data) => newFile(data, 2)
+  },
+  {
+    name: '重命名',
+    click: async (data) => {
+      const {value} = await CyMessageBox.prompt({
+        message: '请输入名称',
+        inputMaxLength: 50
+      })
+      await updateNameSqlApi({
+        id: data.id,
+        name: value
+      })
+      data.name = value
+    }
+  },
+  {
+    name: '删除',
+    click: async (data) => {
+      await CyMessageBox.confirm({
+        message: "是否删除",
+        type: 'delete',
+      })
+      await delSqlApi(data.id)
+      treeRef.value.remove(data)
+      emits('del-api', data.id)
+    }
+  },
+]
+
+const dataApi = ref([])
+const defaultProps = {
+  children: 'children',
+  label: 'name',
+  value: 'id',
+}
+
+function query() {
+  getDataBaseSqlApiList().then(res => {
+    dataApi.value = res
+    console.log(res)
+  })
+}
+
+function handleSearch() {
+  treeRef.value.filter(searchInput.value)
+}
+
+function handleFilterTree(value, data) {
+  if (!value) return true
+  return data.name.includes(value) || data.id === value
+}
+
+function handleLaunch() {
+  expandBoolean.value = !expandBoolean.value
+  const nodes = treeRef.value.store.nodesMap
+  for (let i in nodes) {
+    nodes[i].expanded = expandBoolean.value
+  }
+}
+
+function handleNodeClick(node, object, event) {
+  if (node.type === 1) {
+    emits('node-click', node)
+  }
+}
+
+function treeRightClick(event: Event, data) {
+  position.value = {
+    event,
+    data: data,
+    index: data['$treeNodeId']
+  }
+}
+
+function newAndInsertTree(id, data) {
+  treeRef.value.append(data, id)
+}
+
+function newFolder() {
+  CyMessageBox.prompt({
+    message: '此处按钮仅能创建根目录下的文件夹,如需在子节点创建请选择子文件夹后鼠标右键。',
+    inputMaxLength: 50
+  }).then(async ({value}) => {
+    const data = prepareData(2, value)
+    const res = await addSqlApi(data)
+    if (dataApi.value.length > 0) {
+      treeRef.value.insertAfter(res, dataApi.value[dataApi.value.length - 1].id)
+    } else {
+      query()
+    }
+  })
+}
+
+async function newFile(data, type) {
+  const {value} = await CyMessageBox.prompt({
+    message: '请输入名称',
+    inputMaxLength: 50
+  })
+  let tempData, parentId
+  if (data.type === 1) {
+    parentId = data.parentId
+    tempData = prepareData(type, value, data.parentId)
+  } else {
+    parentId = data.id
+    tempData = prepareData(type, value, data.id)
+  }
+  const res = await addSqlApi(tempData)
+  newAndInsertTree(parentId, res)
+}
+
+/**
+ *
+ * @param type 1 文件 2 文件夹
+ * @param name 名称
+ * @param parentId 父节点名称
+ */
+function prepareData(type: 1 | 2, name: string, parentId: string = null) {
+  const a = {
+    params: [
+      {key: 'currentPage', defaultValue: 1, required: true, describe: '当前页码'}
+    ],
+    pageInfo: {
+      page: true,
+      pageSize: 50
+    },
+    removeSpaces: true
+  }
+
+  return {
+    "id": null,
+    name,
+    "sql": "",
+    "dataBaseId": 0,
+    "requestMapping": "GET",
+    "config": JSON.stringify(a),
+    "enable": 0,
+    "parentId": parentId,
+    type,
+  }
+}
+
+function setCurrentKey(data) {
+  treeRef.value.setCurrentKey(data.id, true)
+}
+
+onMounted(() => {
+  query()
+})
+
+defineExpose({
+  setCurrentKey
+})
+
+</script>
+
+<template>
+  <RightClickMenu :mouse-position="position" :config="opt"/>
+  <div class="api_tree-main">
+    <div class="api_tree-function_buttons">
+      <el-button-group>
+
+        <el-button text @click="newFolder">
+          新建
+        </el-button>
+
+        <el-button text @click="handleLaunch">
+          {{ expandBoolean ? '收起' : '展开' }}
+        </el-button>
+
+        <el-button text @click="query">
+          刷新
+        </el-button>
+
+      </el-button-group>
+    </div>
+
+    <div class="api_tree-input">
+      <el-input v-model="searchInput" clearable @change="handleSearch"/>
+    </div>
+
+    <div class="api_tree-tree">
+      <el-tree :data="dataApi"
+               highlight-current
+               :props="defaultProps"
+               :filter-node-method="handleFilterTree"
+               ref="treeRef"
+               @node-contextmenu="treeRightClick"
+               @node-click="handleNodeClick"
+               node-key="id">
+        <template #default="{ node,data }">
+          <div style="display: flex;align-content: center">
+            <el-icon>
+              <FolderOpened v-if="data.type === 2 "/>
+              <Document v-else/>
+            </el-icon>
+            <span style="margin-left: 5px">{{ node.label }}</span>
+          </div>
+        </template>
+      </el-tree>
+    </div>
+
+  </div>
+</template>
+
+<style scoped lang="scss">
+.api_tree-main {
+  width: 100%;
+  height: 100%;
+  background: white;
+  border-radius: 5px;
+  box-shadow: var(--el-box-shadow);
+  box-sizing: border-box;
+  display: flex;
+  flex-flow: column nowrap;
+}
+
+.api_tree-function_buttons {
+  height: 30px;
+  display: flex;
+  align-items: center;
+}
+
+.api_tree-input {
+  box-sizing: border-box;
+  padding: 0 10px;
+}
+
+.api_tree-tree {
+  flex: 1;
+  overflow: auto
+}
+</style>

+ 104 - 0
src/views/data-base/data-base-api/src/DataBaseApi.vue

@@ -0,0 +1,104 @@
+<script setup lang="ts">
+import ApiTree from "@/views/data-base/data-base-api/components/ApiTree.vue";
+import {computed, onMounted, ref} from "vue";
+import {TDataBaseSqlApi} from "@/ts-type/data-base-type";
+import ApiTabPane from "@/views/data-base/data-base-api/components/ApiTabPane.vue";
+import {getDataBase} from "@/api/data-base/data-base";
+import XEUtils from "xe-utils";
+
+const activeName = ref('')
+const tabsList = ref<TDataBaseSqlApi[]>([])
+const dataSource = ref([])
+const apiTreeRef = ref()
+
+const currentData = computed(() => {
+  return XEUtils.filter(tabsList.value, (item) => {
+    return item.id === activeName.value
+  })[0]
+})
+
+function handleTabChange() {
+  apiTreeRef.value.setCurrentKey(currentData.value)
+}
+
+function removeTab(targetName) {
+  const index = XEUtils.findIndexOf(tabsList.value, (item) => {
+    return item.id === targetName
+  })
+  tabsList.value.splice(index, 1)
+}
+
+function handleNodeClick(data: TDataBaseSqlApi) {
+  const index = XEUtils.findIndexOf(tabsList.value, (item) => {
+    return item.id === data.id
+  })
+  if (index === -1) {
+    tabsList.value.push(data)
+  }
+  activeName.value = data.id
+}
+
+function delApi(id) {
+  const index = XEUtils.findIndexOf(tabsList.value, (item) => {
+    return item.id === id
+  })
+
+  if (index > -1) {
+    tabsList.value.splice(index, 1)
+  }
+
+}
+
+function query() {
+  getDataBase().then(res => {
+    dataSource.value = res
+  })
+}
+
+onMounted(() => {
+  query()
+})
+</script>
+
+<template>
+  <el-container>
+    <el-aside>
+      <ApiTree @nodeClick="handleNodeClick" @delApi="delApi" ref="apiTreeRef"/>
+    </el-aside>
+    <el-main>
+      <el-tabs type="card"
+               @tabChange="handleTabChange"
+               class="data_base-tabs"
+               v-if="tabsList.length > 0"
+               v-model="activeName"
+               style="height: calc(100% - 2px)"
+               closable
+               @tab-remove="removeTab">
+        <el-tab-pane :name="item.id" :key="item.id"
+                     :label="item.name"
+                     v-for="(item,index) in tabsList">
+          <ApiTabPane v-model="tabsList[index]" :data-base="dataSource"/>
+        </el-tab-pane>
+      </el-tabs>
+    </el-main>
+  </el-container>
+</template>
+
+<style lang="scss">
+.data_base-tabs {
+  background: white;
+
+  .el-tabs__content {
+    padding: 0;
+    height: calc(100% - 40px);
+  }
+
+  .el-tab-pane {
+    height: 100%;
+  }
+
+  .el-tabs__header {
+    margin: 0;
+  }
+}
+</style>

+ 7 - 3
src/views/hospitalization/zhu-yuan-yi-sheng/electronic-medical-record/emr-editor/EmrMain.vue

@@ -1224,19 +1224,23 @@ const courseSegmentLocking = async () => {
       jump: true,
       createName: value['编辑者']?.value[0].name,
       createDate: value['查房时间']?.value,
-      createId: value['编辑者']?.value[0].code,
+      createId: value['编辑者']?.value[0].code || fragment.createId,
       type: 'category',
       trueCreationTime: '',
     }
+    if (typeof pushData.createId === 'undefined' || !pushData.createId) {
+      console.log(value, fragment)
+    }
     if (fragment != null) {
       pushData.trueCreationTime = fragment?.creationTime
     }
     courseTitles.push(pushData);
     if (emrConfig.value.editor) {
       let editorCode = value['编辑者']?.value[0]?.code;
-      if (XEUtils.isEmpty(editorCode)) {
+      // 如果这个为空的话,就让她删了重新写,只能删除不能写
+      if (stringIsBlank(editorCode)) {
         node.view.setReadonly(true);
-        node.view.setDeletable(false);
+        node.view.setDeletable(true);
       } else {
         let isEdit = emrEditCreateLimit.isEdit(editorCode)
         if (isEdit) {