Explorar el Código

fix: resolved eslint errors

0xJacky hace 1 año
padre
commit
d325dd7493
Se han modificado 33 ficheros con 644 adiciones y 585 borrados
  1. 1 8
      app/components.d.ts
  2. 5 0
      app/env.d.ts
  3. 7 7
      app/package.json
  4. 32 25
      app/pnpm-lock.yaml
  5. 9 0
      app/src/api/curd.ts
  6. 8 1
      app/src/api/environment.ts
  7. 1 1
      app/src/api/openai.ts
  8. 7 1
      app/src/api/user.ts
  9. 1 1
      app/src/components/Breadcrumb/Breadcrumb.vue
  10. 3 3
      app/src/components/Chart/AreaChart.vue
  11. 2 2
      app/src/components/ChatGPT/ChatGPT.vue
  12. 0 1
      app/src/components/NodeSelector/NodeSelector.vue
  13. 33 79
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  14. 1 1
      app/src/components/StdDesign/StdDataDisplay/StdPagination.vue
  15. 138 341
      app/src/components/StdDesign/StdDataDisplay/StdTable.vue
  16. 9 0
      app/src/components/StdDesign/StdDataDisplay/index.ts
  17. 71 0
      app/src/components/StdDesign/StdDataDisplay/methods/exportCsv.ts
  18. 132 0
      app/src/components/StdDesign/StdDataDisplay/methods/sortable.ts
  19. 0 54
      app/src/components/StdDesign/StdDataEntry/StdDataEntry.tsx
  20. 83 0
      app/src/components/StdDesign/StdDataEntry/StdDataEntry.vue
  21. 2 1
      app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue
  22. 1 1
      app/src/components/StdDesign/StdDataEntry/index.tsx
  23. 46 4
      app/src/components/StdDesign/types.d.ts
  24. 1 4
      app/src/layouts/BaseRouterView.vue
  25. 4 10
      app/src/layouts/Loading.vue
  26. 1 1
      app/src/version.json
  27. 1 1
      app/src/views/dashboard/ServerAnalytic.vue
  28. 19 1
      app/src/views/dashboard/components/NodeAnalyticItem.vue
  29. 2 8
      app/src/views/domain/DomainList.vue
  30. 8 19
      app/src/views/system/About.vue
  31. 11 8
      app/src/views/user/User.vue
  32. 4 1
      app/tsconfig.json
  33. 1 1
      app/version.json

+ 1 - 8
app/components.d.ts

@@ -77,14 +77,6 @@ declare module 'vue' {
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SetLanguageSetLanguage: typeof import('./src/components/SetLanguage/SetLanguage.vue')['default']
-    StdDataDisplayStdBatchEdit: typeof import('./src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue')['default']
-    StdDataDisplayStdCurd: typeof import('./src/components/StdDesign/StdDataDisplay/StdCurd.vue')['default']
-    StdDataDisplayStdPagination: typeof import('./src/components/StdDesign/StdDataDisplay/StdPagination.vue')['default']
-    StdDataDisplayStdTable: typeof import('./src/components/StdDesign/StdDataDisplay/StdTable.vue')['default']
-    StdDataEntryComponentsStdPassword: typeof import('./src/components/StdDesign/StdDataEntry/components/StdPassword.vue')['default']
-    StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelect.vue')['default']
-    StdDataEntryComponentsStdSelector: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelector.vue')['default']
-    StdDataEntryStdFormItem: typeof import('./src/components/StdDesign/StdDataEntry/StdFormItem.vue')['default']
     StdDesignStdDataDisplayStdBatchEdit: typeof import('./src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue')['default']
     StdDesignStdDataDisplayStdCurd: typeof import('./src/components/StdDesign/StdDataDisplay/StdCurd.vue')['default']
     StdDesignStdDataDisplayStdPagination: typeof import('./src/components/StdDesign/StdDataDisplay/StdPagination.vue')['default']
@@ -92,6 +84,7 @@ declare module 'vue' {
     StdDesignStdDataEntryComponentsStdPassword: typeof import('./src/components/StdDesign/StdDataEntry/components/StdPassword.vue')['default']
     StdDesignStdDataEntryComponentsStdSelect: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelect.vue')['default']
     StdDesignStdDataEntryComponentsStdSelector: typeof import('./src/components/StdDesign/StdDataEntry/components/StdSelector.vue')['default']
+    StdDesignStdDataEntryStdDataEntry: typeof import('./src/components/StdDesign/StdDataEntry/StdDataEntry.vue')['default']
     StdDesignStdDataEntryStdFormItem: typeof import('./src/components/StdDesign/StdDataEntry/StdFormItem.vue')['default']
     SwitchAppearanceIconsVPIconMoon: typeof import('./src/components/SwitchAppearance/icons/VPIconMoon.vue')['default']
     SwitchAppearanceIconsVPIconSun: typeof import('./src/components/SwitchAppearance/icons/VPIconSun.vue')['default']

+ 5 - 0
app/env.d.ts

@@ -0,0 +1,5 @@
+declare module '*.svg' {
+  import React from 'react'
+  const content: React.FC<React.SVGProps<SVGElement>>
+  export default content
+}

+ 7 - 7
app/package.json

@@ -1,11 +1,10 @@
 {
   "name": "nginx-ui-app-next",
-  "private": true,
   "version": "2.0.0-beta.4",
-  "type": "commonjs",
   "scripts": {
     "dev": "vite",
-    "lint": "eslint . -c .eslintrc.js --fix --ext .ts,.vue,.tsx",
+    "typecheck": "vue-tsc --noEmit",
+    "lint": "eslint . -c .eslintrc.js --fix --ext .ts,.vue,.tsx,.d.ts",
     "build": "vite build",
     "preview": "vite preview",
     "gettext:extract": "vue-gettext-extract",
@@ -14,9 +13,6 @@
   "dependencies": {
     "@ant-design/icons-vue": "^7.0.1",
     "@formkit/auto-animate": "^0.8.0",
-    "@types/lodash": "^4.14.202",
-    "@types/nprogress": "^0.2.0",
-    "@types/sortablejs": "^1.15.0",
     "@vue/reactivity": "^3.3.9",
     "@vue/shared": "^3.3.9",
     "@vueuse/core": "^10.6.1",
@@ -45,6 +41,10 @@
     "xterm-addon-fit": "^0.8.0"
   },
   "devDependencies": {
+    "@types/lodash": "^4.14.202",
+    "@types/nprogress": "^0.2.0",
+    "@types/sortablejs": "^1.15.0",
+    "@vue/tsconfig": "^0.4.0",
     "@antfu/eslint-config-vue": "^0.43.1",
     "@typescript-eslint/eslint-plugin": "^6.13.0",
     "@typescript-eslint/parser": "^6.13.0",
@@ -67,7 +67,7 @@
     "unplugin-auto-import": "^0.17.1",
     "unplugin-vue-components": "^0.25.2",
     "unplugin-vue-define-options": "^1.4.0",
-    "vite": "^5.0.2",
+    "vite": "^5.0.3",
     "vite-plugin-html": "^3.2.0",
     "vite-svg-loader": "^5.1.0",
     "vue-tsc": "^1.8.22"

+ 32 - 25
app/pnpm-lock.yaml

@@ -11,15 +11,6 @@ dependencies:
   '@formkit/auto-animate':
     specifier: ^0.8.0
     version: 0.8.1
-  '@types/lodash':
-    specifier: ^4.14.202
-    version: 4.14.202
-  '@types/nprogress':
-    specifier: ^0.2.0
-    version: 0.2.3
-  '@types/sortablejs':
-    specifier: ^1.15.0
-    version: 1.15.7
   '@vue/reactivity':
     specifier: ^3.3.9
     version: 3.3.9
@@ -103,6 +94,15 @@ devDependencies:
   '@antfu/eslint-config-vue':
     specifier: ^0.43.1
     version: 0.43.1(@typescript-eslint/eslint-plugin@6.13.0)(@typescript-eslint/parser@6.13.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.54.0)(typescript@5.3.2)
+  '@types/lodash':
+    specifier: ^4.14.202
+    version: 4.14.202
+  '@types/nprogress':
+    specifier: ^0.2.0
+    version: 0.2.3
+  '@types/sortablejs':
+    specifier: ^1.15.0
+    version: 1.15.7
   '@typescript-eslint/eslint-plugin':
     specifier: ^6.13.0
     version: 6.13.0(@typescript-eslint/parser@6.13.0)(eslint@8.54.0)(typescript@5.3.2)
@@ -111,13 +111,16 @@ devDependencies:
     version: 6.13.0(eslint@8.54.0)(typescript@5.3.2)
   '@vitejs/plugin-vue':
     specifier: ^4.5.0
-    version: 4.5.0(vite@5.0.2)(vue@3.3.9)
+    version: 4.5.0(vite@5.0.3)(vue@3.3.9)
   '@vitejs/plugin-vue-jsx':
     specifier: ^3.1.0
-    version: 3.1.0(vite@5.0.2)(vue@3.3.9)
+    version: 3.1.0(vite@5.0.3)(vue@3.3.9)
   '@vue/compiler-sfc':
     specifier: ^3.3.9
     version: 3.3.9
+  '@vue/tsconfig':
+    specifier: ^0.4.0
+    version: 0.4.0
   ace-builds:
     specifier: ^1.31.2
     version: 1.31.2
@@ -167,11 +170,11 @@ devDependencies:
     specifier: ^1.4.0
     version: 1.4.0(vue@3.3.9)
   vite:
-    specifier: ^5.0.2
-    version: 5.0.2(less@4.2.0)
+    specifier: ^5.0.3
+    version: 5.0.3(less@4.2.0)
   vite-plugin-html:
     specifier: ^3.2.0
-    version: 3.2.0(vite@5.0.2)
+    version: 3.2.0(vite@5.0.3)
   vite-svg-loader:
     specifier: ^5.1.0
     version: 5.1.0(vue@3.3.9)
@@ -1297,7 +1300,7 @@ packages:
 
   /@types/lodash@4.14.202:
     resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
-    dev: false
+    dev: true
 
   /@types/mdast@3.0.15:
     resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==}
@@ -1327,7 +1330,7 @@ packages:
 
   /@types/nprogress@0.2.3:
     resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
-    dev: false
+    dev: true
 
   /@types/parse-json@4.0.2:
     resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1343,7 +1346,7 @@ packages:
 
   /@types/sortablejs@1.15.7:
     resolution: {integrity: sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==}
-    dev: false
+    dev: true
 
   /@types/unist@2.0.10:
     resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
@@ -1549,7 +1552,7 @@ packages:
     resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
     dev: true
 
-  /@vitejs/plugin-vue-jsx@3.1.0(vite@5.0.2)(vue@3.3.9):
+  /@vitejs/plugin-vue-jsx@3.1.0(vite@5.0.3)(vue@3.3.9):
     resolution: {integrity: sha512-w9M6F3LSEU5kszVb9An2/MmXNxocAnUb3WhRr8bHlimhDrXNt6n6D2nJQR3UXpGlZHh/EsgouOHCsM8V3Ln+WA==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
@@ -1559,20 +1562,20 @@ packages:
       '@babel/core': 7.23.3
       '@babel/plugin-transform-typescript': 7.23.4(@babel/core@7.23.3)
       '@vue/babel-plugin-jsx': 1.1.5(@babel/core@7.23.3)
-      vite: 5.0.2(less@4.2.0)
+      vite: 5.0.3(less@4.2.0)
       vue: 3.3.9(typescript@5.3.2)
     transitivePeerDependencies:
       - supports-color
     dev: true
 
-  /@vitejs/plugin-vue@4.5.0(vite@5.0.2)(vue@3.3.9):
+  /@vitejs/plugin-vue@4.5.0(vite@5.0.3)(vue@3.3.9):
     resolution: {integrity: sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ==}
     engines: {node: ^14.18.0 || >=16.0.0}
     peerDependencies:
       vite: ^4.0.0 || ^5.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 5.0.2(less@4.2.0)
+      vite: 5.0.3(less@4.2.0)
       vue: 3.3.9(typescript@5.3.2)
     dev: true
 
@@ -1734,6 +1737,10 @@ packages:
   /@vue/shared@3.3.9:
     resolution: {integrity: sha512-ZE0VTIR0LmYgeyhurPTpy4KzKsuDyQbMSdM49eKkMnT5X4VfFBLysMzjIZhLEFQYjjOVVfbvUDHckwjDFiO2eA==}
 
+  /@vue/tsconfig@0.4.0:
+    resolution: {integrity: sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==}
+    dev: true
+
   /@vueuse/core@10.6.1(vue@3.3.9):
     resolution: {integrity: sha512-Pc26IJbqgC9VG1u6VY/xrXXfxD33hnvxBnKrLlA2LJlyHII+BSrRoTPJgGYq7qZOu61itITFUnm6QbacwZ4H8Q==}
     dependencies:
@@ -5360,7 +5367,7 @@ packages:
       - terser
     dev: false
 
-  /vite-plugin-html@3.2.0(vite@5.0.2):
+  /vite-plugin-html@3.2.0(vite@5.0.3):
     resolution: {integrity: sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==}
     peerDependencies:
       vite: '>=2.0.0'
@@ -5377,7 +5384,7 @@ packages:
       html-minifier-terser: 6.1.0
       node-html-parser: 5.4.2
       pathe: 0.2.0
-      vite: 5.0.2(less@4.2.0)
+      vite: 5.0.3(less@4.2.0)
     dev: true
 
   /vite-svg-loader@5.1.0(vue@3.3.9):
@@ -5426,8 +5433,8 @@ packages:
       fsevents: 2.3.3
     dev: false
 
-  /vite@5.0.2(less@4.2.0):
-    resolution: {integrity: sha512-6CCq1CAJCNM1ya2ZZA7+jS2KgnhbzvxakmlIjN24cF/PXhRMzpM/z8QgsVJA/Dm5fWUWnVEsmtBoMhmerPxT0g==}
+  /vite@5.0.3(less@4.2.0):
+    resolution: {integrity: sha512-WgEq8WEKpZ8c0DL4M1+E+kBZEJyjBmGVrul6z8Ljfhv+PPbNF4aGq014DwNYxGz2FGq6NKL0N8usdiESWd2l2w==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
     peerDependencies:

+ 9 - 0
app/src/api/curd.ts

@@ -26,6 +26,7 @@ class Curd<T> {
   get = this._get.bind(this)
   save = this._save.bind(this)
   destroy = this._destroy.bind(this)
+  update_order = this._update_order.bind(this)
 
   constructor(baseUrl: string, plural: string | null = null) {
     this.baseUrl = baseUrl
@@ -51,6 +52,14 @@ class Curd<T> {
   _destroy(id: any = null) {
     return http.delete(`${this.baseUrl}/${id}`)
   }
+
+  _update_order(data: {
+    target_id: number
+    direction: number
+    affected_ids: number[]
+  }) {
+    return http.post(`${this.plural}/order`, data)
+  }
 }
 
 export default Curd

+ 8 - 1
app/src/api/environment.ts

@@ -1,5 +1,12 @@
+import type { ModelBase } from '@/api/curd'
 import Curd from '@/api/curd'
 
-const environment = new Curd('/environment')
+export interface Environment extends ModelBase {
+  name: string
+  url: string
+  token: string
+}
+
+const environment: Curd<Environment> = new Curd('/environment')
 
 export default environment

+ 1 - 1
app/src/api/openai.ts

@@ -7,7 +7,7 @@ export interface ChatComplicationMessage {
 }
 
 const openai = {
-  store_record(data: { file_name: string; messages: ChatComplicationMessage[] }) {
+  store_record(data: { file_name?: string; messages: ChatComplicationMessage[] }) {
     return http.post('/chat_gpt_record', data)
   },
 }

+ 7 - 1
app/src/api/user.ts

@@ -1,5 +1,11 @@
+import type { ModelBase } from '@/api/curd'
 import Curd from '@/api/curd'
 
-const user: Curd = new Curd('user')
+export interface User extends ModelBase {
+  name: string
+  password: string
+}
+
+const user: Curd<User> = new Curd('user')
 
 export default user

+ 1 - 1
app/src/components/Breadcrumb/Breadcrumb.vue

@@ -17,7 +17,7 @@ const breadList = computed(() => {
   route.matched.forEach(item => {
     // item.name !== 'index' && this.breadList.push(item)
     _breadList.push({
-      name: item.name as () => string,
+      name: item.name as never as () => string,
       path: item.path,
     })
   })

+ 3 - 3
app/src/components/Chart/AreaChart.vue

@@ -5,7 +5,7 @@ import type { Ref } from 'vue'
 import { useSettingsStore } from '@/pinia'
 import type { Series } from '@/components/Chart/types'
 
-const { series, max, y_formatter } = defineProps<{
+const { series, max, yFormatter } = defineProps<{
   series: Series[]
   max?: number
   yFormatter?: (value: number) => string
@@ -69,7 +69,7 @@ let chartOptions = {
       style: {
         colors: fontColor(),
       },
-      formatter: y_formatter,
+      formatter: yFormatter,
     },
   },
   legend: {
@@ -106,7 +106,7 @@ const callback = () => {
           style: {
             colors: fontColor(),
           },
-          formatter: y_formatter,
+          formatter: yFormatter,
         },
       },
       legend: {

+ 2 - 2
app/src/components/ChatGPT/ChatGPT.vue

@@ -295,7 +295,7 @@ const show = computed(() => !messages.value || messages.value?.length === 0)
         v-model:value="ask_buffer"
         auto-size
       />
-      <div class="sned-btn">
+      <div class="send-btn">
         <AButton
           size="small"
           type="text"
@@ -356,7 +356,7 @@ const show = computed(() => !messages.value || messages.value?.length === 0)
       justify-content: center;
     }
 
-    .sned-btn {
+    .send-btn {
       position: absolute;
       right: 0;
       bottom: 3px;

+ 0 - 1
app/src/components/NodeSelector/NodeSelector.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import { computed, ref } from 'vue'
 import { useGettext } from 'vue3-gettext'
 import environment from '@/api/environment'
 

+ 33 - 79
app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -1,82 +1,45 @@
 <script setup lang="ts">
-import { provide, reactive, ref } from 'vue'
 import { message } from 'ant-design-vue'
+import type { ComputedRef } from 'vue'
+import type { StdTableProps } from './StdTable.vue'
 import StdTable from './StdTable.vue'
 import gettext from '@/gettext'
-
 import StdDataEntry from '@/components/StdDesign/StdDataEntry'
+import type { Column } from '@/components/StdDesign/types'
+
+export interface StdCurdProps {
+  cardTitleKey?: string
+  modalMaxWidth?: string | number
+  disableAdd?: boolean
+  onClickAdd?: () => void
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  onClickEdit?: (id: number | string, record: any, index: number) => void
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  beforeSave?: (data: any) => void
+}
 
-const props = defineProps({
-  api: Object,
-  columns: Array,
-  title: String,
-  data_key: {
-    type: String,
-    default: 'data',
-  },
-  disable_search: {
-    type: Boolean,
-    default: false,
-  },
-  disable_add: {
-    type: Boolean,
-    default: false,
-  },
-  soft_delete: {
-    type: Boolean,
-    default: false,
-  },
-  edit_text: String,
-  deletable: {
-    type: Boolean,
-    default: true,
-  },
-  get_params: {
-    type: Object,
-    default() {
-      return {}
-    },
-  },
-  editable: {
-    type: Boolean,
-    default: true,
-  },
-  beforeSave: {
-    type: Function,
-    default: () => {
-    },
-  },
-  exportCsv: {
-    type: Boolean,
-    default: false,
-  },
-  modalWidth: {
-    type: Number,
-    default: 600,
-  },
-  useSortable: Boolean,
-})
+const props = defineProps<StdTableProps & StdCurdProps>()
 
 const { $gettext } = gettext
 
 const visible = ref(false)
 const update = ref(0)
-const data: any = reactive({ id: null })
+const data = reactive({ id: null })
 
 provide('data', data)
 
-const error: any = reactive({})
+const error = reactive({})
 const selected = ref([])
 
-function onSelect(keys: any) {
+function onSelect(keys) {
   selected.value = keys
 }
 
-function editableColumns() {
-  return props.columns!.filter((c: any) => {
+const editableColumns = computed(() => {
+  return props.columns!.filter(c => {
     return c.edit
   })
-}
+}) as ComputedRef<Column[]>
 
 function add() {
   Object.keys(data).forEach(v => {
@@ -86,11 +49,9 @@ function add() {
   clear_error()
   visible.value = true
 }
-
+const table = ref()
 function get_list() {
-  const t: Table = table.value!
-
-  t!.get_list()
+  table.value?.get_list()
 }
 
 defineExpose({
@@ -99,12 +60,6 @@ defineExpose({
   data,
 })
 
-const table = ref(null)
-
-interface Table {
-  get_list(): void
-}
-
 function clear_error() {
   Object.keys(error).forEach(v => {
     delete error[v]
@@ -114,12 +69,12 @@ function clear_error() {
 const ok = async () => {
   clear_error()
   await props?.beforeSave?.(data)
-  props.api!.save(data.id, data).then((r: any) => {
+  props.api!.save(data.id, data).then(r => {
     message.success($gettext('Save Successfully'))
     Object.assign(data, r)
     get_list()
     visible.value = false
-  }).catch((e: any) => {
+  }).catch(e => {
     message.error($gettext(e?.message ?? 'Server error'), 5)
     Object.assign(error, e.errors)
   })
@@ -131,15 +86,15 @@ function cancel() {
   clear_error()
 }
 
-function edit(id: any) {
-  props.api!.get(id).then(async (r: any) => {
+function edit(id) {
+  props.api!.get(id).then(async r => {
     Object.keys(data).forEach(k => {
       delete data[k]
     })
     data.id = null
     Object.assign(data, r)
     visible.value = true
-  }).catch((e: any) => {
+  }).catch(e => {
     message.error($gettext(e?.message ?? 'Server error'), 5)
   })
 }
@@ -151,7 +106,7 @@ const selectedRowKeys = ref([])
   <div class="std-curd">
     <ACard :title="title || $gettext('Table')">
       <template
-        v-if="!disable_add"
+        v-if="!disableAdd"
         #extra
       >
         <a @click="add">{{ $gettext('Add') }}</a>
@@ -162,7 +117,7 @@ const selectedRowKeys = ref([])
         v-bind="props"
         :key="update"
         v-model:selected-row-keys="selectedRowKeys"
-        @clickEdit="edit"
+        @click-edit="edit"
         @selected="onSelect"
       >
         <template #actions="slotProps">
@@ -177,11 +132,11 @@ const selectedRowKeys = ref([])
     <AModal
       class="std-curd-edit-modal"
       :mask="false"
-      :title="edit_text ? edit_text : (data.id ? $gettext('Modify') : $gettext('Add'))"
+      :title="data.id ? $gettext('Modify') : $gettext('Add')"
       :open="visible"
       :cancel-text="$gettext('Cancel')"
       :ok-text="$gettext('OK')"
-      :width="modalWidth"
+      :width="modalMaxWidth"
       destroy-on-close
       @cancel="cancel"
       @ok="ok"
@@ -197,8 +152,7 @@ const selectedRowKeys = ref([])
       </div>
 
       <StdDataEntry
-        ref="std_data_entry"
-        :data-list="editableColumns()"
+        :data-list="editableColumns"
         :data-source="data"
         :error="error"
       />

+ 1 - 1
app/src/components/StdDesign/StdDataDisplay/StdPagination.vue

@@ -4,7 +4,7 @@ import type { Pagination } from '@/api/curd'
 
 const props = defineProps<{
   pagination: Pagination
-  size?: 'small' | 'default'
+  size?: string
 }>()
 
 const emit = defineEmits(['change', 'changePageSize', 'update:pagination'])

+ 138 - 341
app/src/components/StdDesign/StdDataDisplay/StdTable.vue

@@ -1,103 +1,71 @@
 <script setup lang="ts">
-import { computed, onMounted, reactive, ref, toRaw, watch } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
 import { message } from 'ant-design-vue'
-import dayjs from 'dayjs'
-import Sortable from 'sortablejs'
 import { HolderOutlined } from '@ant-design/icons-vue'
-import _ from 'lodash'
+import { useGettext } from 'vue3-gettext'
+import type { ComputedRef } from 'vue'
+import type { SorterResult } from 'ant-design-vue/lib/table/interface'
 import StdPagination from './StdPagination.vue'
-import { downloadCsv } from '@/lib/helper'
 import StdDataEntry from '@/components/StdDesign/StdDataEntry'
-import gettext from '@/gettext'
-
-const props = defineProps({
-  api: Object,
-  columns: Array,
-  data_key: {
-    type: String,
-    default: 'data',
-  },
-  disable_search: {
-    type: Boolean,
-    default: false,
-  },
-  disable_query_params: {
-    type: Boolean,
-    default: false,
-  },
-  disable_add: {
-    type: Boolean,
-    default: false,
-  },
-  edit_text: String,
-  deletable: {
-    type: Boolean,
-    default: true,
-  },
-  get_params: {
-    type: Object,
-    default() {
-      return {}
-    },
-  },
-  editable: {
-    type: Boolean,
-    default: true,
-  },
-  selectionType: {
-    type: String,
-    validator(value: string) {
-      return ['checkbox', 'radio'].includes(value)
-    },
-  },
-  pithy: {
-    type: Boolean,
-    default: false,
-  },
-  scrollX: {
-    type: [Number, Boolean],
-    default: true,
-  },
-  rowKey: {
-    type: String,
-    default: 'id',
-  },
-  exportCsv: {
-    type: Boolean,
-    default: false,
-  },
-  size: String,
-  selectedRowKeys: {
-    type: Array,
-  },
-  useSortable: Boolean,
+import type { Pagination } from '@/api/curd'
+import type { Column } from '@/components/StdDesign/types'
+import exportCsvHandler from '@/components/StdDesign/StdDataDisplay/methods/exportCsv'
+import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
+import type Curd from '@/api/curd'
+
+export interface StdTableProps {
+  title?: string
+  mode?: string
+  rowKey?: string
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  api: Curd<any>
+  columns: Column[]
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  getParams?: Record<string, any>
+  size?: string
+  disableQueryParams?: boolean
+  disableSearch?: boolean
+  pithy?: boolean
+  exportCsv?: boolean
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  overwriteParams?: Record<string, any>
+  disabledModify?: boolean
+  selectionType?: string
+  sortable?: boolean
+  disableDelete?: boolean
+  disablePagination?: boolean
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  selectedRowKeys?: any | any[]
+  sortableMoveHook?: (oldRow: number[], newRow: number[]) => boolean
+  scrollX?: string | number
+}
+
+const props = withDefaults(defineProps<StdTableProps>(), {
+  rowKey: 'id',
 })
 
 const emit = defineEmits(['onSelected', 'onSelectedRecord', 'clickEdit', 'update:selectedRowKeys', 'clickBatchModify'])
-
-const { $gettext, interpolate } = gettext
-
-const data_source: any = ref([])
-const expand_keys_list: any = ref([])
-const rows_key_index_map: any = ref({})
-
+const { $gettext } = useGettext()
+const route = useRoute()
+const dataSource = ref([])
+const expandKeysList = ref([])
+const rowsKeyIndexMap = ref({})
 const loading = ref(true)
 
-const pagination = reactive({
+// This can be useful if there are more than one StdTable in the same page.
+const randomId = ref(Math.random().toString(36).substring(2, 8))
+
+const pagination: Pagination = reactive({
   total: 1,
   per_page: 10,
   current_page: 1,
   total_pages: 1,
 })
 
-const route = useRoute()
-
 const params = reactive({
-  ...props.get_params,
+  ...props.getParams,
 })
 
-const selectedKeysLocalBuffer: any = ref([])
+const selectedKeysLocalBuffer = ref([])
 
 const selectedRowKeysBuffer = computed({
   get() {
@@ -109,17 +77,47 @@ const selectedRowKeysBuffer = computed({
   },
 })
 
-const searchColumns = getSearchColumns()
-const pithyColumns = getPithyColumns()
-const batchColumns = getBatchEditColumns()
+const searchColumns = computed(() => {
+  const _searchColumns = []
+
+  props.columns?.forEach(column => {
+    if (column.search)
+      _searchColumns.push(column)
+  })
+
+  return _searchColumns
+})
+
+const pithyColumns = computed(() => {
+  if (props.pithy) {
+    return props.columns?.filter(c => {
+      return c.pithy === true && !c.hidden
+    })
+  }
+
+  return props.columns?.filter(c => {
+    return !c.hidden
+  })
+}) as ComputedRef<Column[]>
+
+const batchColumns = computed(() => {
+  const batch = []
+
+  props.columns?.forEach(column => {
+    if (column.batch)
+      batch.push(column)
+  })
+
+  return batch
+})
 
 onMounted(() => {
-  if (!props.disable_query_params)
+  if (!props.disableQueryParams)
     Object.assign(params, route.query)
 
   get_list()
 
-  if (props.useSortable)
+  if (props.sortable)
     initSortable()
 })
 
@@ -127,11 +125,11 @@ defineExpose({
   get_list,
 })
 
-function destroy(id: any) {
+function destroy(id) {
   props.api!.destroy(id).then(() => {
     get_list()
-    message.success(interpolate($gettext('Delete ID: %{id}'), { id }))
-  }).catch((e: any) => {
+    message.success($gettext('Deleted successfully'))
+  }).catch(e => {
     message.error($gettext(e?.message ?? 'Server error'))
   })
 }
@@ -142,37 +140,35 @@ function get_list(page_num = null, page_size = 20) {
     params.page = page_num
     params.page_size = page_size
   }
-  props.api!.get_list(params).then(async (r: any) => {
-    data_source.value = r.data
-    rows_key_index_map.value = {}
-    if (props.useSortable) {
-      function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
-        if (data && data.length > 0) {
-          data.forEach((v: any) => {
-            v.level = level
-
-            const current_index = [...total, index++]
-
-            rows_key_index_map.value[v.id] = current_index
-            if (v.children)
-              buildIndexMap(v.children, level + 1, 0, current_index)
-          })
-        }
-      }
+  props.api?.get_list(params).then(async r => {
+    dataSource.value = r.data
+    rowsKeyIndexMap.value = {}
+    if (props.sortable)
 
       buildIndexMap(r.data)
-    }
 
-    if (r.pagination !== undefined)
+    if (r.pagination)
       Object.assign(pagination, r.pagination)
 
     loading.value = false
-  }).catch((e: any) => {
+  }).catch(e => {
     message.error(e?.message ?? $gettext('Server error'))
   })
 }
+function buildIndexMap(data, level: number = 0, index: number = 0, total: number[] = []) {
+  if (data && data.length > 0) {
+    data.forEach(v => {
+      v.level = level
+
+      const current_indexes = [...total, index++]
 
-function stdChange(pagination: any, filters: any, sorter: any) {
+      rowsKeyIndexMap.value[v.id] = current_indexes
+      if (v.children)
+        buildIndexMap(v.children, level + 1, 0, current_indexes)
+    })
+  }
+}
+function orderPaginationChange(_pagination: Pagination, filters, sorter: SorterResult) {
   if (sorter) {
     selectedRowKeysBuffer.value = []
     params.order_by = sorter.field
@@ -189,67 +185,29 @@ function stdChange(pagination: any, filters: any, sorter: any) {
         break
     }
   }
-  if (pagination)
+  if (_pagination)
     selectedRowKeysBuffer.value = []
 }
 
-function expandedTable(keys: any) {
-  expand_keys_list.value = keys
-}
-
-function getSearchColumns() {
-  const searchColumns: any = []
-
-  props.columns!.forEach((column: any) => {
-    if (column.search)
-      searchColumns.push(column)
-  })
-
-  return searchColumns
-}
-
-function getBatchEditColumns() {
-  const batch: any = []
-
-  props.columns!.forEach((column: any) => {
-    if (column.batch)
-      batch.push(column)
-  })
-
-  return batch
+function expandedTable(keys) {
+  expandKeysList.value = keys
 }
 
-function getPithyColumns() {
-  if (props.pithy) {
-    return props.columns!.filter((c: any, index: any, columns: any) => {
-      return c.pithy === true && c.display !== false
-    })
-  }
-
-  return props.columns!.filter((c: any, index: any, columns: any) => {
-    return c.display !== false
-  })
-}
-
-function checked(c: any) {
-  params[c.target.value] = c.target.checked
-}
+const crossPageSelect = {}
 
-const crossPageSelect: any = {}
-
-async function onSelectChange(_selectedRowKeys: any) {
+async function onSelectChange(_selectedRowKeys) {
   const page = params.page || 1
 
   crossPageSelect[page] = await _selectedRowKeys
 
-  let t: any = []
+  let t = []
   Object.keys(crossPageSelect).forEach(v => {
     t.push(...crossPageSelect[v])
   })
 
-  const n: any = [..._selectedRowKeys]
+  const n = [..._selectedRowKeys]
 
-  t = await t.concat(n)
+  t = t.concat(n)
 
   // console.log(crossPageSelect)
   const set = new Set(t)
@@ -258,7 +216,7 @@ async function onSelectChange(_selectedRowKeys: any) {
   emit('onSelected', selectedRowKeysBuffer.value)
 }
 
-function onSelect(record: any) {
+function onSelect(record) {
   emit('onSelectedRecord', record)
 }
 
@@ -270,7 +228,7 @@ const reset_search = async () => {
   })
 
   Object.assign(params, {
-    ...props.get_params,
+    ...props.getParams,
   })
 
   router.push({ query: {} }).catch(() => {
@@ -278,19 +236,19 @@ const reset_search = async () => {
 }
 
 watch(params, () => {
-  if (!props.disable_query_params)
+  if (!props.disableQueryParams)
     router.push({ query: params })
 
   get_list()
 })
 
 const rowSelection = computed(() => {
-  if (batchColumns.length > 0 || props.selectionType) {
+  if (batchColumns.value.length > 0 || props.selectionType) {
     return {
       selectedRowKeys: selectedRowKeysBuffer.value,
       onChange: onSelectChange,
       onSelect,
-      type: batchColumns.length > 0 ? 'checkbox' : props.selectionType,
+      type: batchColumns.value.length > 0 ? 'checkbox' : props.selectionType,
     }
   }
   else {
@@ -298,188 +256,27 @@ const rowSelection = computed(() => {
   }
 })
 
-const fn = _.get
-
-async function export_csv() {
-  const header = []
-  const headerKeys: any[] = []
-  const showColumnsMap: any = {}
-
-  for (const showColumnsKey in pithyColumns) {
-    if (pithyColumns[showColumnsKey].dataIndex === 'action')
-      continue
-
-    let t = pithyColumns[showColumnsKey].title
-
-    if (typeof t === 'function')
-      t = t()
-
-    header.push({
-      title: t,
-
-      key: pithyColumns[showColumnsKey].dataIndex,
-    })
-
-    headerKeys.push(pithyColumns[showColumnsKey].dataIndex)
-
-    showColumnsMap[pithyColumns[showColumnsKey].dataIndex] = pithyColumns[showColumnsKey]
-  }
-
-  let dataSource: any = []
-  let hasMore = true
-  let page = 1
-  while (hasMore) {
-    // 准备 DataSource
-    await props.api!.get_list({ page }).then((response: any) => {
-      if (response.data.length === 0) {
-        hasMore = false
-
-        return
-      }
-      if (response[props.data_key] === undefined)
-        dataSource = dataSource.concat(...response.data)
-      else
-        dataSource = dataSource.concat(...response[props.data_key])
-    }).catch((e: any) => {
-      message.error(e.message ?? $gettext('Server error'))
-      hasMore = false
-    })
-    page += 1
-  }
-  const data: any[] = []
-
-  dataSource.forEach((row: Object) => {
-    const obj: any = {}
-
-    headerKeys.forEach(key => {
-      let data = fn(row, key)
-      const c = showColumnsMap[key]
-
-      data = c?.customRender?.({ text: data }) ?? data
-      obj[c.dataIndex] = data
-    })
-    data.push(obj)
-  })
-
-  downloadCsv(header, data,
-    `${$gettext('Export')}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
-}
-
 const hasSelectedRow = computed(() => {
-  return batchColumns.length > 0 && selectedRowKeysBuffer.value.length > 0
+  return batchColumns.value.length > 0 && selectedRowKeysBuffer.value.length > 0
 })
 
-function click_batch_edit() {
-  emit('clickBatchModify', batchColumns, selectedRowKeysBuffer.value)
-}
-
-function getLeastIndex(index: number) {
-  return index >= 1 ? index : 1
-}
-
-function getTargetData(data: any, indexList: number[]): any {
-  let target: any = { children: data }
-  indexList.forEach((index: number) => {
-    target.children[index].parent = target
-    target = target.children[index]
-  })
-
-  return target
+function clickBatchEdit() {
+  emit('clickBatchModify', batchColumns.value, selectedRowKeysBuffer.value)
 }
 
 function initSortable() {
-  const table: any = document.querySelector('#std-table tbody')
-
-  new Sortable(table, {
-    handle: '.ant-table-drag-icon',
-    animation: 150,
-    sort: true,
-    forceFallback: true,
-    setData(dataTransfer) {
-      dataTransfer.setData('Text', '')
-    },
-    onStart({ item }) {
-      const targetRowKey = Number(item.dataset.rowKey)
-      if (targetRowKey)
-        expand_keys_list.value = expand_keys_list.value.filter((item: number) => item !== targetRowKey)
-    },
-    onMove({ dragged, related }) {
-      const oldRow: number[] = rows_key_index_map.value?.[Number(dragged.dataset.rowKey)]
-      const newRow: number[] = rows_key_index_map.value?.[Number(related.dataset.rowKey)]
-      if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] != newRow[newRow.length - 2])
-        return false
-    },
-    async onEnd({ item, newIndex, oldIndex }) {
-      if (newIndex === oldIndex)
-        return
-
-      const indexDelta: number = Number(oldIndex) - Number(newIndex)
-      const direction: number = indexDelta > 0 ? +1 : -1
-
-      const rowIndex: number[] = rows_key_index_map.value?.[Number(item.dataset.rowKey)]
-      const newRow = getTargetData(data_source.value, rowIndex)
-      const newRowParent = newRow.parent
-      const level: number = newRow.level
-
-      const currentRowIndex: number[] = [...rows_key_index_map.value?.
-        [Number(table.children[Number(newIndex) + direction].dataset.rowKey)]]
-
-      const currentRow: any = getTargetData(data_source.value, currentRowIndex)
-
-      // Reset parent
-      currentRow.parent = newRow.parent = null
-      newRowParent.children.splice(rowIndex[level], 1)
-      newRowParent.children.splice(currentRowIndex[level], 0, toRaw(newRow))
-
-      const changeIds: number[] = []
-
-      function processChanges(row: any, children: boolean = false, newIndex: number | undefined = undefined) {
-        // Build changes ID list expect new row
-        if (children || newIndex === undefined)
-          changeIds.push(row.id)
-
-        if (newIndex !== undefined)
-          rows_key_index_map.value[row.id][level] = newIndex
-        else if (children)
-          rows_key_index_map.value[row.id][level] += direction
-
-        row.parent = null
-        if (row.children)
-          row.children.forEach((v: any) => processChanges(v, true, newIndex))
-      }
-
-      // Replace row index for new row
-      processChanges(newRow, false, currentRowIndex[level])
-
-      // Rebuild row index maps for changes row
-      for (let i = Number(oldIndex); i != newIndex; i -= direction) {
-        const rowIndex: number[] = rows_key_index_map.value?.[table.children[i].dataset.rowKey]
-
-        rowIndex[level] += direction
-        processChanges(getTargetData(data_source.value, rowIndex))
-      }
-      console.log('Change row id', newRow.id, 'order', newRow.id, '=>', currentRow.id, ', direction: ', direction,
-        ', changes IDs:', changeIds)
-
-      props.api!.update_order({
-        target_id: newRow.id,
-        direction,
-        affected_ids: changeIds,
-      }).then(() => {
-        message.success($gettext('Updated successfully'))
-      }).catch((e: any) => {
-        message.error(e?.message ?? $gettext('Server error'))
-      })
-    },
-  })
+  useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
 }
 
+function export_csv() {
+  exportCsvHandler(props, pithyColumns)
+}
 </script>
 
 <template>
   <div class="std-table">
     <StdDataEntry
-      v-if="!disable_search && searchColumns.length"
+      v-if="!disableSearch && searchColumns.length"
       :data-list="searchColumns"
       :data-source="params"
       layout="inline"
@@ -487,7 +284,7 @@ function initSortable() {
       <template #action>
         <ASpace class="action-btn">
           <AButton
-            v-if="exportCsv"
+            v-if="props.exportCsv"
             type="primary"
             ghost
             @click="export_csv"
@@ -499,7 +296,7 @@ function initSortable() {
           </AButton>
           <AButton
             v-if="hasSelectedRow"
-            @click="click_batch_edit"
+            @click="clickBatchEdit"
           >
             {{ $gettext('Batch Modify') }}
           </AButton>
@@ -509,36 +306,36 @@ function initSortable() {
     <ATable
       id="std-table"
       :columns="pithyColumns"
-      :data-source="data_source"
+      :data-source="dataSource"
       :loading="loading"
       :pagination="false"
       :row-key="rowKey"
       :row-selection="rowSelection"
       :scroll="{ x: scrollX }"
       :size="size"
-      :expanded-row-keys="expand_keys_list"
-      @change="stdChange"
-      @expandedRowsChange="expandedTable"
+      :expanded-row-keys="expandKeysList"
+      @change="orderPaginationChange"
+      @expanded-rows-change="expandedTable"
     >
-      <template #bodyCell="{ text, record, index, column }">
+      <template #bodyCell="{ text, record, column }">
         <template v-if="column.handle === true">
           <span class="ant-table-drag-icon"><HolderOutlined /></span>
           {{ text }}
         </template>
         <template v-if="column.dataIndex === 'action'">
           <AButton
-            v-if="props.editable"
+            v-if="!props.disabledModify"
             type="link"
             size="small"
             @click="$emit('clickEdit', record[props.rowKey], record)"
           >
-            {{ props.edit_text || $gettext('Modify') }}
+            {{ $gettext('Modify') }}
           </AButton>
           <slot
             name="actions"
             :record="record"
           />
-          <template v-if="props.deletable">
+          <template v-if="!props.disableDelete">
             <ADivider type="vertical" />
             <APopconfirm
               :cancel-text="$gettext('No')"
@@ -561,7 +358,7 @@ function initSortable() {
       :size="size"
       :pagination="pagination"
       @change="get_list"
-      @changePageSize="stdChange"
+      @change-page-size="orderPaginationChange"
     />
   </div>
 </template>

+ 9 - 0
app/src/components/StdDesign/StdDataDisplay/index.ts

@@ -0,0 +1,9 @@
+import StdTable from './StdTable.vue'
+import StdCurd from './StdCurd.vue'
+import StdBatchEdit from './StdBatchEdit.vue'
+
+export {
+  StdTable,
+  StdCurd,
+  StdBatchEdit,
+}

+ 71 - 0
app/src/components/StdDesign/StdDataDisplay/methods/exportCsv.ts

@@ -0,0 +1,71 @@
+import { message } from 'ant-design-vue'
+import dayjs from 'dayjs'
+import type { ComputedRef } from 'vue'
+import _ from 'lodash'
+import { downloadCsv } from '@/lib/helper'
+import type { Column, StdTableResponse } from '@/components/StdDesign/types'
+import gettext from '@/gettext'
+import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
+
+const { $gettext } = gettext
+async function exportCsv(props: StdTableProps, pithyColumns: ComputedRef<Column[]>) {
+  const header: { title?: string; key: string }[] = []
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const headerKeys: any[] = []
+  const showColumnsMap: Record<string, Column> = {}
+
+  pithyColumns.value.forEach((column: Column) => {
+    if (column.dataIndex === 'action')
+      return
+    let t = column.title
+    if (typeof t === 'function')
+      t = t()
+    header.push({
+      title: t,
+      key: column.dataIndex,
+    })
+    headerKeys.push(column.dataIndex)
+    showColumnsMap[column.dataIndex] = column
+  })
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const dataSource: any[] = []
+  let hasMore = true
+  let page = 1
+  while (hasMore) {
+    // 准备 DataSource
+    await props.api!.get_list({ page }).then((r: StdTableResponse) => {
+      if (r.data.length === 0) {
+        hasMore = false
+
+        return
+      }
+      dataSource.push(...r.data)
+    }).catch((e: { message?: string }) => {
+      message.error(e.message ?? $gettext('Server error'))
+      hasMore = false
+    })
+    page += 1
+  }
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const data: any[] = []
+
+  dataSource.forEach(row => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const obj: Record<string, any> = {}
+
+    headerKeys.forEach(key => {
+      let _data = _.get(row, key)
+      const c = showColumnsMap[key]
+
+      _data = c?.customRender?.({ text: _data }) ?? _data
+      obj[c.dataIndex] = _data
+    })
+    data.push(obj)
+  })
+
+  downloadCsv(header, data,
+    `${$gettext('Export')}-${props.title}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
+}
+
+export default exportCsv

+ 132 - 0
app/src/components/StdDesign/StdDataDisplay/methods/sortable.ts

@@ -0,0 +1,132 @@
+import { message } from 'ant-design-vue'
+import SortableJs from 'sortablejs'
+import type { Ref } from 'vue'
+import gettext from '@/gettext'
+import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
+
+const { $gettext } = gettext
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function getRowKey(item: any) {
+  return item.children[0].children[0].dataset.rowKey
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function getTargetData(data: any, indexList: number[]): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  let target: any = { children: data }
+  indexList.forEach((index: number) => {
+    target.children[index].parent = target
+    target = target.children[index]
+  })
+
+  return target
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function useSortable(props: StdTableProps, randomId: Ref<string>, dataSource: Ref<any[]>,
+  rowsKeyIndexMap: Ref<Record<number, number[]>>, expandKeysList: Ref<number[]>) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const table: any = document.querySelector(`#std-table-${randomId.value} tbody`)
+
+  // eslint-disable-next-line no-new
+  new SortableJs(table, {
+    handle: '.table-drag-icon',
+    animation: 150,
+    sort: true,
+    forceFallback: true,
+    setData(dataTransfer) {
+      dataTransfer.setData('Text', '')
+    },
+    onStart({ item }) {
+      const targetRowKey = Number(getRowKey(item))
+      if (targetRowKey)
+        expandKeysList.value = expandKeysList.value.filter((_item: number) => _item !== targetRowKey)
+    },
+    onMove({
+      dragged,
+             related,
+    }) {
+      const oldRow: number[] = rowsKeyIndexMap.value?.[Number(getRowKey(dragged))]
+      const newRow: number[] = rowsKeyIndexMap.value?.[Number(getRowKey(related))]
+
+      if (oldRow.length !== newRow.length || oldRow[oldRow.length - 2] !== newRow[newRow.length - 2])
+        return false
+
+      if (props.sortableMoveHook)
+        return props.sortableMoveHook(oldRow, newRow)
+    },
+    async onEnd({
+      item,
+                  newIndex,
+                  oldIndex,
+    }) {
+      if (newIndex === oldIndex)
+        return
+
+      const indexDelta: number = Number(oldIndex) - Number(newIndex)
+      const direction: number = indexDelta > 0 ? +1 : -1
+
+      const rowIndex: number[] = rowsKeyIndexMap.value?.[Number(getRowKey(item))]
+      const newRow = getTargetData(dataSource.value, rowIndex)
+      const newRowParent = newRow.parent
+      const level: number = newRow.level
+
+      const currentRowIndex: number[] = [...rowsKeyIndexMap.value?.
+        [Number(getRowKey(table.children[Number(newIndex) + direction]))]]
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const currentRow: any = getTargetData(dataSource.value, currentRowIndex)
+
+      // Reset parent
+      currentRow.parent = newRow.parent = null
+      newRowParent.children.splice(rowIndex[level], 1)
+      newRowParent.children.splice(currentRowIndex[level], 0, toRaw(newRow))
+
+      const changeIds: number[] = []
+
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      function processChanges(row: any, children = false, _newIndex: number | undefined = undefined) {
+        // Build changes ID list expect new row
+        if (children || _newIndex === undefined)
+          changeIds.push(row.id)
+
+        if (_newIndex !== undefined)
+          rowsKeyIndexMap.value[row.id][level] = _newIndex
+        else if (children)
+          rowsKeyIndexMap.value[row.id][level] += direction
+
+        row.parent = null
+        if (row.children) {
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          row.children.forEach((v: any) => processChanges(v, true, _newIndex))
+        }
+      }
+
+      // Replace row index for new row
+      processChanges(newRow, false, currentRowIndex[level])
+
+      // Rebuild row index maps for changes row
+      for (let i = Number(oldIndex); i !== newIndex; i -= direction) {
+        const _rowIndex: number[] = rowsKeyIndexMap.value?.[getRowKey(table.children[i])]
+
+        _rowIndex[level] += direction
+        processChanges(getTargetData(dataSource.value, _rowIndex))
+      }
+      console.log('Change row id', newRow.id, 'order', newRow.id, '=>', currentRow.id, ', direction: ', direction,
+        ', changes IDs:', changeIds)
+
+      props.api.update_order({
+        target_id: newRow.id,
+        direction,
+        affected_ids: changeIds,
+      }).then(() => {
+        message.success($gettext('Updated successfully'))
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      }).catch((e: any) => {
+        message.error(e?.message ?? $gettext('Server error'))
+      })
+    },
+  })
+}
+
+export default useSortable

+ 0 - 54
app/src/components/StdDesign/StdDataEntry/StdDataEntry.tsx

@@ -1,54 +0,0 @@
-import { defineComponent } from 'vue'
-import { Form } from 'ant-design-vue'
-import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
-import './style.less'
-
-export default defineComponent({
-  props: {
-    dataList: {
-      type: Array,
-      required: true,
-    },
-    dataSource: {
-      type: Object,
-      required: true,
-    },
-    error: {
-      type: Object,
-      required: false,
-    },
-    layout: {
-      type: String,
-      required: false,
-    },
-  },
-  emits: ['update:dataSource'],
-  setup(props, { slots }) {
-    return () => {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      const template: any = []
-
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      props.dataList.forEach((v: any) => {
-        let show = true
-        if (v.edit.show) {
-          if (typeof v.edit.show === 'boolean')
-            show = v.edit.show
-          else if (typeof v.edit.show === 'function')
-            show = v.edit.show(props.dataSource)
-        }
-        if (v.edit.type && show) {
-          template.push(<StdFormItem dataIndex={v.dataIndex} label={v.title()} extra={v.extra} error={props.error}>
-            {v.edit.type(v.edit, props.dataSource, v.dataIndex)}
-          </StdFormItem>,
-          )
-        }
-      })
-
-      if (slots.action)
-        template.push(<div class={'std-data-entry-action'}>{slots.action()}</div>)
-
-      return <Form layout={props.layout || 'vertical'}>{template}</Form>
-    }
-  },
-})

+ 83 - 0
app/src/components/StdDesign/StdDataEntry/StdDataEntry.vue

@@ -0,0 +1,83 @@
+<script setup lang="tsx">
+import { Form } from 'ant-design-vue'
+import type { Column } from '@/components/StdDesign/types'
+import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
+
+const props = defineProps<{
+  dataList: Column[]
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  dataSource: Record<string, any>
+  errors?: Record<string, string>
+  layout?: 'horizontal' | 'vertical'
+}>()
+
+const emit = defineEmits<{
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  'update:dataSource': (v: any[]) => void
+}>()
+
+const dataSource = computed({
+  get() {
+    return props.dataSource
+  },
+  set(v) {
+    emit('update:dataSource', v)
+  },
+})
+
+const slots = useSlots()
+
+function labelRender(title?: string | (() => string)) {
+  if (typeof title === 'function')
+    return title()
+
+  return title
+}
+
+function extraRender(extra?: string | (() => string)) {
+  if (typeof extra === 'function')
+    return extra()
+
+  return extra
+}
+
+function Render() {
+  const template = []
+
+  props.dataList.forEach((v: Column) => {
+    let show = true
+    if (v.edit?.show && typeof v.edit.show === 'function')
+      show = v.edit.show(props.dataSource)
+
+    if (v.edit?.type && show) {
+      template.push(<StdFormItem
+        dataIndex={v.dataIndex}
+      label={labelRender(v.title)}
+      extra={extraRender(v.extra)}
+      error={props.errors}>
+        {v.edit.type(v.edit, dataSource.value, v.dataIndex)}
+        </StdFormItem>,
+      )
+    }
+  })
+
+  if (slots.action)
+    template.push(<div class={'std-data-entry-action'}>{slots.action()}</div>)
+
+  return <Form layout={props.layout || 'vertical'}>{template}</Form>
+}
+</script>
+
+<template>
+  <Render />
+</template>
+
+<style scoped lang="less">
+.std-data-entry-action {
+  @media (max-width: 375px) {
+    display: block;
+    width: 100%;
+    margin: 10px 0;
+  }
+}
+</style>

+ 2 - 1
app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue

@@ -2,6 +2,7 @@
 import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import gettext from '@/gettext'
 import type Curd from '@/api/curd'
+import type { Column } from '@/components/StdDesign/types'
 
 const props = defineProps<{
   selectedKey: string | number
@@ -10,7 +11,7 @@ const props = defineProps<{
   selectionType: 'radio' | 'checkbox'
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   api: Curd<any>
-  columns: any[]
+  columns: Column[]
   dataKey: string
   disableSearch: boolean
   // eslint-disable-next-line @typescript-eslint/no-explicit-any

+ 1 - 1
app/src/components/StdDesign/StdDataEntry/index.tsx

@@ -1,7 +1,7 @@
 import { h } from 'vue'
 import { Input, InputNumber, Switch, Textarea } from 'ant-design-vue'
 import _ from 'lodash'
-import StdDataEntry from './StdDataEntry'
+import StdDataEntry from './StdDataEntry.vue'
 import StdSelector from './components/StdSelector.vue'
 import StdSelect from './components/StdSelect.vue'
 import StdPassword from './components/StdPassword.vue'

+ 46 - 4
app/src/components/StdDesign/types.d.ts

@@ -1,10 +1,13 @@
-import Curd from '@/api/curd'
-import {IKeyEvt} from '@/components/StdDesign/StdDataDisplay/types'
-import {Ref} from 'vue'
+import Curd, {Pagination} from '@/api/curd'
+import { Ref } from 'vue'
 
 export interface StdDesignEdit {
   type?: function // component type
 
+  show?: function // show component
+
+  batch?: boolean // batch edit
+
   mask?: {
     [key: string]: () => string
   } // use for select-option
@@ -39,7 +42,6 @@ export interface StdDesignEdit {
   flex?: Flex
 }
 
-
 export interface Flex {
   sm?: string | number | boolean
   md?: string | number | boolean
@@ -47,3 +49,43 @@ export interface Flex {
   xl?: string | number | boolean
   xxl?: string | number | boolean
 }
+
+export interface Column {
+  title?: string | (() => string);
+  dataIndex: string;
+  edit?: StdDesignEdit;
+  customRender?: function;
+  extra?: string | (() => string);
+  pithy?: boolean;
+  search?: boolean | StdDesignEdit;
+  sortable?: boolean;
+  hidden?: boolean;
+  width?: string | number;
+  handle?: boolean;
+  hiddenInTrash?: boolean;
+  hiddenInCreate?: boolean;
+  hiddenInModify?: boolean;
+  batch?: boolean;
+}
+
+
+export interface StdTableProvideData {
+  displayColumns: Column[];
+  pithyColumns: Column[];
+  columnsMap: { [key: string]: Column };
+  displayKeys: string[];
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  editItem: (id: number, data: any, index: string | number) => void;
+  deleteItem: (id: number, index: string | number) => void;
+  recoverItem: (id: number) => {};
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  params: any;
+  dataSource: any;
+  get_list: () => void;
+  loading: Ref<boolean>;
+}
+
+export interface StdTableResponse {
+  data: any[]
+  pagination: Pagination
+}

+ 1 - 4
app/src/layouts/BaseRouterView.vue

@@ -1,7 +1,4 @@
-<script>
-export default {
-  name: 'BaseRouterView',
-}
+<script setup lang="ts">
 </script>
 
 <template>

+ 4 - 10
app/src/layouts/Loading.vue

@@ -1,13 +1,7 @@
-<script>
-export default {
-  name: 'Loading',
-  props: {
-    loading: {
-      type: [Boolean, String],
-      default: false,
-    },
-  },
-}
+<script setup lang="ts">
+defineProps<{
+  loading: boolean | string
+}>()
 </script>
 
 <template>

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.4","build_id":53,"total_build":257}
+{"version":"2.0.0-beta.4","build_id":56,"total_build":260}

+ 1 - 1
app/src/views/dashboard/ServerAnalytic.vue

@@ -97,7 +97,7 @@ function handle_uptime(t: number) {
   uptime.value = `${uptime_days}d ${uptime_hours}h ${Math.floor(_uptime / 60)}m`
 }
 
-function wsOnMessage(m: { data: any }) {
+function wsOnMessage(m) {
   const r = JSON.parse(m.data)
 
   const cpu_usage = r.cpu.system + r.cpu.user

+ 19 - 1
app/src/views/dashboard/components/NodeAnalyticItem.vue

@@ -5,7 +5,25 @@ import memory from '@/assets/svg/memory.svg'
 import { bytesToSize } from '@/lib/helper'
 import UsageProgressLine from '@/components/Chart/UsageProgressLine.vue'
 
-defineProps(['item'])
+defineProps<{
+  item: {
+    avg_load: {
+      load1: number
+      load5: number
+      load15: number
+    }
+    network: {
+      bytesSent: number
+      bytesRecv: number
+    }
+    cpu_percent: number
+    cpu_num: number
+    memory_percent: number
+    memory_total: string
+    disk_percent: number
+    disk_total: string
+  }
+}>()
 </script>
 
 <template>

+ 2 - 8
app/src/views/domain/DomainList.vue

@@ -52,10 +52,6 @@ const columns = [{
 
 const table = ref()
 
-interface Table {
-  get_list(): void
-}
-
 function enable(name) {
   domain.enable(name).then(() => {
     message.success($gettext('Enabled successfully'))
@@ -76,9 +72,7 @@ function disable(name) {
 
 function destroy(site_name) {
   domain.destroy(site_name).then(() => {
-    const t: Table | null = table.value
-
-    t!.get_list()
+    table.value.get_list()
     message.success($gettext('Delete site: %{site_name}', { site_name }))
   }).catch(e => {
     message.error(e?.message ?? $gettext('Server error'))
@@ -163,7 +157,7 @@ watch(route, () => {
     <SiteDuplicate
       v-model:visible="show_duplicator"
       :name="target"
-      @duplicated="table.get_list()"
+      @duplicated="() => table.get_list()"
     />
   </ACard>
 </template>

+ 8 - 19
app/src/views/system/About.vue

@@ -11,7 +11,7 @@ const this_year = new Date().getFullYear()
 
 <template>
   <ACard
-    style="text-align: center"
+    class="text-center"
     :bordered="false"
   >
     <div class="logo">
@@ -34,26 +34,23 @@ const this_year = new Date().getFullYear()
         Star
       </GithubButton>
     </div>
-    <h3 v-translate>
-      Project Team
+    <h3>
+      {{ $gettext('Project Team') }}
     </h3>
     <p><a href="https://jackyu.cn/">@0xJacky</a> <a href="https://blog.kugeek.com/">@Hintay</a></p>
-    <h3 v-translate>
-      Build with
+    <h3>
+      {{ $gettext('Build with') }}
     </h3>
     <p>❤️</p>
     <p>Go</p>
     <p>Gin</p>
     <p>Vue3 + Vite + TypeScript</p>
     <p>Websocket</p>
-    <h3
-      v-translate
-      translate-context="Project"
-    >
-      License
+    <h3>
+      {{ $gettext('License') }}
     </h3>
     <p>GNU General Public License v3.0</p>
-    <p>Copyright © 2020 - {{ this_year }} Nginx UI </p>
+    <p>Copyright © 2020 - {{ this_year }} Nginx UI Team</p>
   </ACard>
 </template>
 
@@ -64,14 +61,6 @@ const this_year = new Date().getFullYear()
   }
 }
 
-.egg {
-  padding: 10px 0;
-}
-
-.ant-btn {
-  margin: 10px 10px 0 0;
-}
-
 .star-on-github {
   margin-bottom: 10px;
 }

+ 11 - 8
app/src/views/user/User.vue

@@ -4,13 +4,14 @@ import gettext from '@/gettext'
 import user from '@/api/user'
 import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { input, password } from '@/components/StdDesign/StdDataEntry'
+import type { Column } from '@/components/StdDesign/types'
 
 const { $gettext } = gettext
 
-const columns = [{
+const columns: Column[] = [{
   title: () => $gettext('Username'),
   dataIndex: 'name',
-  sorter: true,
+  sortable: true,
   pithy: true,
   edit: {
     type: input,
@@ -19,25 +20,27 @@ const columns = [{
 }, {
   title: () => $gettext('Password'),
   dataIndex: 'password',
-  sorter: true,
+  sortable: true,
   pithy: true,
   edit: {
     type: password,
-    placeholder: () => $gettext('Leave blank for no change'),
-    generate: true,
+    config: {
+      placeholder: () => $gettext('Leave blank for no change'),
+      generate: true,
+    },
   },
-  display: false,
+  hidden: true,
 }, {
   title: () => $gettext('Created at'),
   dataIndex: 'created_at',
   customRender: datetime,
-  sorter: true,
+  sortable: true,
   pithy: true,
 }, {
   title: () => $gettext('Updated at'),
   dataIndex: 'updated_at',
   customRender: datetime,
-  sorter: true,
+  sortable: true,
   pithy: true,
 }, {
   title: () => $gettext('Action'),

+ 4 - 1
app/tsconfig.json

@@ -1,4 +1,5 @@
 {
+  "extends": "@vue/tsconfig/tsconfig.json",
   "compilerOptions": {
     "target": "ESNext",
     "useDefineForClassFields": true,
@@ -20,9 +21,11 @@
       "@/*": [
         "./src/*"
       ]
-    }
+    },
+    "types": ["vite/client"]
   },
   "include": [
+    "env.d.ts",
     "src/**/*.ts",
     "src/**/*.d.ts",
     "src/**/*.tsx",

+ 1 - 1
app/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.4","build_id":53,"total_build":257}
+{"version":"2.0.0-beta.4","build_id":56,"total_build":260}