Browse Source

refactor(app): migrate to @uozi-admin/curd

Jacky 1 month ago
parent
commit
2a3ef530b6
82 changed files with 3315 additions and 6259 deletions
  1. 8 8
      app/package.json
  2. 130 133
      app/pnpm-lock.yaml
  3. 2 2
      app/src/api/acme_user.ts
  4. 2 2
      app/src/api/config.ts
  5. 0 94
      app/src/api/curd.ts
  6. 2 2
      app/src/api/env_group.ts
  7. 2 2
      app/src/api/environment.ts
  8. 2 2
      app/src/api/notification.ts
  9. 1 1
      app/src/api/passkey.ts
  10. 2 2
      app/src/api/site.ts
  11. 2 2
      app/src/api/stream.ts
  12. 2 2
      app/src/api/template.ts
  13. 7 7
      app/src/components/ConfigHistory/ConfigHistory.vue
  14. 1 1
      app/src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue
  15. 2 3
      app/src/components/Notification/Notification.vue
  16. 1 1
      app/src/components/Notification/detailRender.tsx
  17. 0 90
      app/src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue
  18. 0 108
      app/src/components/StdDesign/StdDataDisplay/StdBulkActions.vue
  19. 0 309
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  20. 0 36
      app/src/components/StdDesign/StdDataDisplay/StdCurdDetail.vue
  21. 0 65
      app/src/components/StdDesign/StdDataDisplay/StdPagination.vue
  22. 0 623
      app/src/components/StdDesign/StdDataDisplay/StdTable.vue
  23. 0 155
      app/src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx
  24. 0 9
      app/src/components/StdDesign/StdDataDisplay/components/CustomRender.tsx
  25. 0 5
      app/src/components/StdDesign/StdDataDisplay/index.ts
  26. 0 7
      app/src/components/StdDesign/StdDataDisplay/methods/columns.ts
  27. 0 67
      app/src/components/StdDesign/StdDataDisplay/methods/exportCsv.ts
  28. 0 127
      app/src/components/StdDesign/StdDataDisplay/methods/sortable.ts
  29. 0 50
      app/src/components/StdDesign/StdDataDisplay/types.d.ts
  30. 0 119
      app/src/components/StdDesign/StdDataEntry/StdDataEntry.vue
  31. 0 63
      app/src/components/StdDesign/StdDataEntry/StdFormItem.vue
  32. 0 65
      app/src/components/StdDesign/StdDataEntry/components/StdPassword.vue
  33. 0 74
      app/src/components/StdDesign/StdDataEntry/components/StdSelect.vue
  34. 0 259
      app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue
  35. 0 169
      app/src/components/StdDesign/StdDataEntry/index.tsx
  36. 0 7
      app/src/components/StdDesign/StdDataEntry/style.less
  37. 0 25
      app/src/components/StdDesign/StdDataEntry/types.d.ts
  38. 0 141
      app/src/components/StdDesign/StdDetail/StdDetail.vue
  39. 0 154
      app/src/components/StdDesign/types.d.ts
  40. 1 1
      app/src/components/TwoFA/Authorization.vue
  41. 144 193
      app/src/language/ar/app.po
  42. 7 0
      app/src/language/curd.ts
  43. 143 192
      app/src/language/de_DE/app.po
  44. 144 201
      app/src/language/en/app.po
  45. 196 194
      app/src/language/es/app.po
  46. 210 206
      app/src/language/fr_FR/app.po
  47. 232 206
      app/src/language/ja_JP/app.po
  48. 222 204
      app/src/language/ko_KR/app.po
  49. 144 200
      app/src/language/messages.pot
  50. 203 198
      app/src/language/pt_PT/app.po
  51. 205 201
      app/src/language/ru_RU/app.po
  52. 204 201
      app/src/language/tr_TR/app.po
  53. 215 210
      app/src/language/uk_UA/app.po
  54. 201 201
      app/src/language/vi_VN/app.po
  55. 208 199
      app/src/language/zh_CN/app.po
  56. 211 202
      app/src/language/zh_TW/app.po
  57. 1 1
      app/src/layouts/BaseLayout.vue
  58. 1 1
      app/src/views/certificate/CertificateEditor.vue
  59. 1 1
      app/src/views/certificate/components/RemoveCert.vue
  60. 1 1
      app/src/views/certificate/components/WildcardCertificate.vue
  61. 12 6
      app/src/views/config/ConfigEditor.vue
  62. 1 1
      app/src/views/config/components/Rename.vue
  63. 1 2
      app/src/views/dashboard/components/ModulesTable.vue
  64. 6 6
      app/src/views/environments/list/envColumns.tsx
  65. 2 1
      app/src/views/notification/Notification.vue
  66. 8 8
      app/src/views/preference/components/ExternalNotify/ExternalNotifyEditor.vue
  67. 36 19
      app/src/views/preference/components/ExternalNotify/columns.tsx
  68. 1 2
      app/src/views/preference/tabs/AuthSettings.vue
  69. 2 2
      app/src/views/preference/tabs/ExternalNotify.vue
  70. 1 1
      app/src/views/site/site_add/SiteAdd.vue
  71. 11 5
      app/src/views/site/site_edit/components/Cert/ChangeCert.vue
  72. 1 1
      app/src/views/site/site_edit/components/Cert/ObtainCert.vue
  73. 1 1
      app/src/views/site/site_edit/components/ConfigTemplate/ConfigTemplate.vue
  74. 5 5
      app/src/views/site/site_edit/components/RightPanel/Basic.vue
  75. 3 3
      app/src/views/site/site_edit/components/SiteEditor/store.ts
  76. 18 38
      app/src/views/site/site_list/SiteList.vue
  77. 4 3
      app/src/views/site/site_list/columns.tsx
  78. 63 142
      app/src/views/stream/StreamList.vue
  79. 70 0
      app/src/views/stream/columns.tsx
  80. 5 5
      app/src/views/stream/components/RightPanel/Basic.vue
  81. 1 1
      app/src/views/stream/components/RightPanel/Chat.vue
  82. 3 3
      app/src/views/stream/store.ts

+ 8 - 8
app/package.json

@@ -17,8 +17,8 @@
     "@ant-design/icons-vue": "^7.0.1",
     "@formkit/auto-animate": "^0.8.2",
     "@simplewebauthn/browser": "^13.1.0",
-    "@uozi-admin/curd": "^4.1.4",
-    "@uozi-admin/request": "^2.6.0",
+    "@uozi-admin/curd": "^4.3.0",
+    "@uozi-admin/request": "^2.7.1",
     "@vue/reactivity": "^3.5.14",
     "@vue/shared": "^3.5.14",
     "@vueuse/components": "^13.2.0",
@@ -34,14 +34,14 @@
     "highlight.js": "^11.11.1",
     "jsencrypt": "^3.3.2",
     "lodash": "^4.17.21",
-    "marked": "^15.0.11",
+    "marked": "^15.0.12",
     "marked-highlight": "^2.2.1",
     "nprogress": "^0.2.0",
     "pinia": "^3.0.2",
     "pinia-plugin-persistedstate": "^4.3.0",
     "reconnecting-websocket": "^4.4.0",
     "sortablejs": "^1.15.6",
-    "splitpanes": "^4.0.3",
+    "splitpanes": "^4.0.4",
     "sse.js": "^2.6.0",
     "universal-cookie": "^8.0.1",
     "unocss": "^66.1.2",
@@ -57,18 +57,18 @@
     "vuedraggable": "^4.1.0"
   },
   "devDependencies": {
-    "@antfu/eslint-config": "^4.13.1",
+    "@antfu/eslint-config": "^4.13.2",
     "@iconify-json/fa": "1.2.1",
     "@iconify-json/tabler": "^1.2.18",
     "@iconify/tools": "^4.1.2",
     "@iconify/types": "^2.0.0",
     "@iconify/utils": "^2.3.0",
     "@iconify/vue": "^5.0.0",
-    "@types/lodash": "^4.17.16",
+    "@types/lodash": "^4.17.17",
     "@types/nprogress": "^0.2.3",
     "@types/sortablejs": "^1.15.8",
     "@vitejs/plugin-vue": "^5.2.4",
-    "@vitejs/plugin-vue-jsx": "^4.1.2",
+    "@vitejs/plugin-vue-jsx": "^4.2.0",
     "@vue/compiler-sfc": "^3.5.14",
     "@vue/tsconfig": "^0.7.0",
     "ace-builds": "^1.41.0",
@@ -82,7 +82,7 @@
     "unplugin-vue-components": "^28.5.0",
     "unplugin-vue-define-options": "^1.5.5",
     "vite": "^6.3.5",
-    "vite-plugin-inspect": "^11.0.1",
+    "vite-plugin-inspect": "^11.1.0",
     "vite-svg-loader": "^5.1.0",
     "vue-tsc": "^2.2.10"
   }

+ 130 - 133
app/pnpm-lock.yaml

@@ -21,11 +21,11 @@ importers:
         specifier: ^13.1.0
         version: 13.1.0
       '@uozi-admin/curd':
-        specifier: ^4.1.4
-        version: 4.1.4(@ant-design/icons-vue@7.0.1(vue@3.5.14(typescript@5.8.3)))(ant-design-vue@4.2.6(vue@3.5.14(typescript@5.8.3)))(dayjs@1.11.13)(lodash-es@4.17.21)(vue-router@4.5.1(vue@3.5.14(typescript@5.8.3)))(vue@3.5.14(typescript@5.8.3))
+        specifier: ^4.3.0
+        version: 4.3.0(@ant-design/icons-vue@7.0.1(vue@3.5.14(typescript@5.8.3)))(ant-design-vue@4.2.6(vue@3.5.14(typescript@5.8.3)))(dayjs@1.11.13)(lodash-es@4.17.21)(vue-router@4.5.1(vue@3.5.14(typescript@5.8.3)))(vue@3.5.14(typescript@5.8.3))
       '@uozi-admin/request':
-        specifier: ^2.6.0
-        version: 2.6.0(lodash-es@4.17.21)
+        specifier: ^2.7.1
+        version: 2.7.1(lodash-es@4.17.21)
       '@vue/reactivity':
         specifier: ^3.5.14
         version: 3.5.14
@@ -72,11 +72,11 @@ importers:
         specifier: ^4.17.21
         version: 4.17.21
       marked:
-        specifier: ^15.0.11
-        version: 15.0.11
+        specifier: ^15.0.12
+        version: 15.0.12
       marked-highlight:
         specifier: ^2.2.1
-        version: 2.2.1(marked@15.0.11)
+        version: 2.2.1(marked@15.0.12)
       nprogress:
         specifier: ^0.2.0
         version: 0.2.0
@@ -93,8 +93,8 @@ importers:
         specifier: ^1.15.6
         version: 1.15.6
       splitpanes:
-        specifier: ^4.0.3
-        version: 4.0.3(vue@3.5.14(typescript@5.8.3))
+        specifier: ^4.0.4
+        version: 4.0.4(vue@3.5.14(typescript@5.8.3))
       sse.js:
         specifier: ^2.6.0
         version: 2.6.0
@@ -103,7 +103,7 @@ importers:
         version: 8.0.1
       unocss:
         specifier: ^66.1.2
-        version: 66.1.2(postcss@8.5.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
+        version: 66.1.2(postcss@8.5.3)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
       uuid:
         specifier: ^11.1.0
         version: 11.1.0
@@ -136,8 +136,8 @@ importers:
         version: 4.1.0(vue@3.5.14(typescript@5.8.3))
     devDependencies:
       '@antfu/eslint-config':
-        specifier: ^4.13.1
-        version: 4.13.1(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
+        specifier: ^4.13.2
+        version: 4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
       '@iconify-json/fa':
         specifier: 1.2.1
         version: 1.2.1
@@ -157,8 +157,8 @@ importers:
         specifier: ^5.0.0
         version: 5.0.0(vue@3.5.14(typescript@5.8.3))
       '@types/lodash':
-        specifier: ^4.17.16
-        version: 4.17.16
+        specifier: ^4.17.17
+        version: 4.17.17
       '@types/nprogress':
         specifier: ^0.2.3
         version: 0.2.3
@@ -167,10 +167,10 @@ importers:
         version: 1.15.8
       '@vitejs/plugin-vue':
         specifier: ^5.2.4
-        version: 5.2.4(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
+        version: 5.2.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
       '@vitejs/plugin-vue-jsx':
-        specifier: ^4.1.2
-        version: 4.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
+        specifier: ^4.2.0
+        version: 4.2.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
       '@vue/compiler-sfc':
         specifier: ^3.5.14
         version: 3.5.14
@@ -200,19 +200,19 @@ importers:
         version: 5.8.3
       unplugin-auto-import:
         specifier: ^19.2.0
-        version: 19.2.0(@nuxt/kit@3.17.3)(@vueuse/core@13.2.0(vue@3.5.14(typescript@5.8.3)))
+        version: 19.2.0(@nuxt/kit@3.17.4)(@vueuse/core@13.2.0(vue@3.5.14(typescript@5.8.3)))
       unplugin-vue-components:
         specifier: ^28.5.0
-        version: 28.5.0(@babel/parser@7.27.2)(@nuxt/kit@3.17.3)(vue@3.5.14(typescript@5.8.3))
+        version: 28.5.0(@babel/parser@7.27.2)(@nuxt/kit@3.17.4)(vue@3.5.14(typescript@5.8.3))
       unplugin-vue-define-options:
         specifier: ^1.5.5
         version: 1.5.5(vue@3.5.14(typescript@5.8.3))
       vite:
         specifier: ^6.3.5
-        version: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+        version: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
       vite-plugin-inspect:
-        specifier: ^11.0.1
-        version: 11.0.1(@nuxt/kit@3.17.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))
+        specifier: ^11.1.0
+        version: 11.1.0(@nuxt/kit@3.17.4)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))
       vite-svg-loader:
         specifier: ^5.1.0
         version: 5.1.0(vue@3.5.14(typescript@5.8.3))
@@ -240,8 +240,8 @@ packages:
     peerDependencies:
       vue: '>=3.0.3'
 
-  '@antfu/eslint-config@4.13.1':
-    resolution: {integrity: sha512-Ldv0gzQEDH/M+6NfhVBK/9NTDwsYJuHHJBPaFQN9X6LGd927sfEWzMHQdEbrA7f8Rr6abbinReifK7OjDipJ/g==}
+  '@antfu/eslint-config@4.13.2':
+    resolution: {integrity: sha512-F+IVIQUCfw6eW4H06c9a9USJ3UOnoBx4I0qsTL3kO6GcyJB6mwk+nawFf95DfHKT3fJKv58YPPz0XCmsY/w0XA==}
     hasBin: true
     peerDependencies:
       '@eslint-react/eslint-plugin': ^1.38.4
@@ -437,8 +437,8 @@ packages:
   '@emotion/unitless@0.8.1':
     resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==}
 
-  '@es-joy/jsdoccomment@0.50.1':
-    resolution: {integrity: sha512-fas3qe1hw38JJgU/0m5sDpcrbZGysBeZcMwW5Ws9brYxY64MJyWLXRZCj18keTycT1LFTrFXdSNMS+GRVaU6Hw==}
+  '@es-joy/jsdoccomment@0.50.2':
+    resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==}
     engines: {node: '>=18'}
 
   '@esbuild/aix-ppc64@0.23.1':
@@ -896,8 +896,8 @@ packages:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
     engines: {node: '>= 8'}
 
-  '@nuxt/kit@3.17.3':
-    resolution: {integrity: sha512-aw6u6mT3TnM/MmcCRDMv3i9Sbm5/ZMSJgDl+N+WsrWNDIQ2sWmsqdDkjb/HyXF20SNwc2891hRBkaQr3hG2mhA==}
+  '@nuxt/kit@3.17.4':
+    resolution: {integrity: sha512-l+hY8sy2XFfg3PigZj+PTu6+KIJzmbACTRimn1ew/gtCz+F38f6KTF4sMRTN5CUxiB8TRENgEonASmkAWfpO9Q==}
     engines: {node: '>=18.12.0'}
 
   '@pkgjs/parseargs@0.11.0':
@@ -915,6 +915,9 @@ packages:
     resolution: {integrity: sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==}
     engines: {node: '>=20.0.0'}
 
+  '@rolldown/pluginutils@1.0.0-beta.9':
+    resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==}
+
   '@rollup/rollup-android-arm-eabi@4.41.0':
     resolution: {integrity: sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==}
     cpu: [arm]
@@ -1062,9 +1065,6 @@ packages:
   '@types/debug@4.1.12':
     resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
 
-  '@types/eslint@9.6.1':
-    resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
-
   '@types/estree@1.0.7':
     resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
 
@@ -1074,8 +1074,8 @@ packages:
   '@types/json-schema@7.0.15':
     resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
 
-  '@types/lodash@4.17.16':
-    resolution: {integrity: sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==}
+  '@types/lodash@4.17.17':
+    resolution: {integrity: sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==}
 
   '@types/mdast@4.0.4':
     resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
@@ -1086,8 +1086,8 @@ packages:
   '@types/ms@2.1.0':
     resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
 
-  '@types/node@22.15.18':
-    resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
+  '@types/node@22.15.21':
+    resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==}
 
   '@types/nprogress@0.2.3':
     resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
@@ -1331,24 +1331,24 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@uozi-admin/curd@4.1.4':
-    resolution: {integrity: sha512-KykkRypcmGMu0aVsPkwarwYEhy8mDhCNe/6bk6lMr93K4yZRpoK+cmp9k2tXjpLcauhw5NlfUDh5bnVezd7e2Q==}
+  '@uozi-admin/curd@4.3.0':
+    resolution: {integrity: sha512-ZIayQ3hquD84cObVZctHTQDDfIxzeieUEoWQ90ygF3AllEdlOWRhGUaMcN3LaFeUFI2eMKolBBE7scYOkh5jcw==}
     hasBin: true
     peerDependencies:
       '@ant-design/icons-vue': '>=7.0.1'
       ant-design-vue: '>=4.2.6'
       dayjs: '>=1.11.13'
       lodash-es: '>=4.17.21'
-      vue: '>=3.5.13'
+      vue: '>=3.5.14'
       vue-router: '>=4.5.1'
 
-  '@uozi-admin/request@2.6.0':
-    resolution: {integrity: sha512-zNzp1KvTT6tsWC1zPwywmbggsogCd1pHIxCrDEkhPXKE2wryB1ax0hO0Q8E3rP8lFi7DvBViPNXcmi8DRqjj/g==}
+  '@uozi-admin/request@2.7.1':
+    resolution: {integrity: sha512-KWheeuKm0//Ksy/eoSrB20Oysb0g/1e3ehlek+xlfrdYfPFZwancDrfdNG6gK8M0ACP+/XOysOubySLtr7DO/g==}
     peerDependencies:
       lodash-es: '>=4.17.21'
 
-  '@vitejs/plugin-vue-jsx@4.1.2':
-    resolution: {integrity: sha512-4Rk0GdE0QCdsIkuMmWeg11gmM4x8UmTnZR/LWPm7QJ7+BsK4tq08udrN0isrrWqz5heFy9HLV/7bOLgFS8hUjA==}
+  '@vitejs/plugin-vue-jsx@4.2.0':
+    resolution: {integrity: sha512-DSTrmrdLp+0LDNF77fqrKfx7X0ErRbOcUAgJL/HbSesqQwoUvUQ4uYQqaex+rovqgGcoPqVk+AwUh3v9CuiYIw==}
     engines: {node: ^18.0.0 || >=20.0.0}
     peerDependencies:
       vite: ^5.0.0 || ^6.0.0
@@ -2009,8 +2009,8 @@ packages:
     resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
     engines: {node: '>= 4'}
 
-  dompurify@3.2.5:
-    resolution: {integrity: sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==}
+  dompurify@3.2.6:
+    resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==}
 
   domutils@3.2.2:
     resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -2029,8 +2029,8 @@ packages:
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
-  electron-to-chromium@1.5.155:
-    resolution: {integrity: sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==}
+  electron-to-chromium@1.5.157:
+    resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==}
 
   emoji-regex@8.0.0:
     resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2073,8 +2073,8 @@ packages:
   errx@0.1.0:
     resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==}
 
-  es-abstract@1.23.9:
-    resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==}
+  es-abstract@1.23.10:
+    resolution: {integrity: sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==}
     engines: {node: '>= 0.4'}
 
   es-define-property@1.0.1:
@@ -2144,8 +2144,8 @@ packages:
     peerDependencies:
       eslint: ^9.5.0
 
-  eslint-flat-config-utils@2.0.1:
-    resolution: {integrity: sha512-brf0eAgQ6JlKj3bKfOTuuI7VcCZvi8ZCD1MMTVoEvS/d38j8cByZViLFALH/36+eqB17ukmfmKq3bWzGvizejA==}
+  eslint-flat-config-utils@2.1.0:
+    resolution: {integrity: sha512-6fjOJ9tS0k28ketkUcQ+kKptB4dBZY2VijMZ9rGn8Cwnn1SH0cZBoPXT8AHBFHxmHcLFQK9zbELDinZ2Mr1rng==}
 
   eslint-import-resolver-node@0.3.9:
     resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
@@ -2462,8 +2462,8 @@ packages:
     resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
     engines: {node: '>= 0.4'}
 
-  get-tsconfig@4.10.0:
-    resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==}
+  get-tsconfig@4.10.1:
+    resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
 
   gettext-extractor@3.8.0:
     resolution: {integrity: sha512-i/3mDQufQoJd2/EKm/B+VlaYrt3yGjVfLZu8DQpESKH29klNiW6z2S89FVCIEB85bDNgtGCeM/3A/yR1njr/Lw==}
@@ -2749,8 +2749,8 @@ packages:
   isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
-  isomorphic-git@1.30.1:
-    resolution: {integrity: sha512-eWBlPIPDOctGY/bTUc/whs6EZ8YvnG1H2kOjTCJ/AkvBWUzODXcfulhpiA8Y4Px9e+bRYYkifE5fSE8FcRk8Ew==}
+  isomorphic-git@1.30.2:
+    resolution: {integrity: sha512-Io/AkS58RFp0Sm+PPHkPT2NHfsxkG1+F6iOuPYxWvF1K0ZgIzT950lYt1G5PkhZr2edzIUoDJcqWYbxPIL6mXw==}
     engines: {node: '>=14.17'}
     hasBin: true
 
@@ -2894,8 +2894,8 @@ packages:
     peerDependencies:
       marked: '>=4 <16'
 
-  marked@15.0.11:
-    resolution: {integrity: sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==}
+  marked@15.0.12:
+    resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
     engines: {node: '>= 18'}
     hasBin: true
 
@@ -3608,8 +3608,8 @@ packages:
     resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
     engines: {node: '>=0.10.0'}
 
-  splitpanes@4.0.3:
-    resolution: {integrity: sha512-S/f1CoH2JroOib7kzQtTQNtQCa7VzNQ2qKOO5HNj/5EVVcNkfz1eX/sH+X3XKdBdDLihEKDekVGwrLADd2oirA==}
+  splitpanes@4.0.4:
+    resolution: {integrity: sha512-RbysugZhjbCw5fgplvk3hOXr41stahQDtZhHVkhnnJI6H4wlGDhM2kIpbehy7v92duy9GnMa8zIhHigIV1TWtg==}
     peerDependencies:
       vue: ^3.2.0
 
@@ -3688,8 +3688,8 @@ packages:
     resolution: {integrity: sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==}
     engines: {node: ^14.18.0 || >=16.0.0}
 
-  tapable@2.2.1:
-    resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
+  tapable@2.2.2:
+    resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
     engines: {node: '>=6'}
 
   tar@6.2.1:
@@ -3897,8 +3897,8 @@ packages:
   vite-plugin-build-id@0.5.0:
     resolution: {integrity: sha512-dvf3PSSjzSZSCoWodOjDSDei7wRgQKTYHBKfAZAEoIDTuQtxIVFNzKPHuWETFDOE3pnOa76BUjbTOKxRjMKD9Q==}
 
-  vite-plugin-inspect@11.0.1:
-    resolution: {integrity: sha512-aABw7eGTr9Cmbn9RAs76e0BztVUFDl6a2R+/IJXpoUZxjx5YHB0P+Em3ZTWzpIPZzuRj28tAMblvcUyhgJc4aQ==}
+  vite-plugin-inspect@11.1.0:
+    resolution: {integrity: sha512-r3Nx8xGQ08bSoNu7gJGfP5H/wNOROHtv0z3tWspplyHZJlABwNoPOdFEmcVh+lVMDyk/Be4yt8oS596ZHoYhOg==}
     engines: {node: '>=14'}
     peerDependencies:
       '@nuxt/kit': '*'
@@ -4149,7 +4149,7 @@ snapshots:
       '@ant-design/icons-svg': 4.4.2
       vue: 3.5.14(typescript@5.8.3)
 
-  '@antfu/eslint-config@4.13.1(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
+  '@antfu/eslint-config@4.13.2(@vue/compiler-sfc@3.5.14)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
     dependencies:
       '@antfu/install-pkg': 1.1.0
       '@clack/prompts': 0.10.1
@@ -4163,7 +4163,7 @@ snapshots:
       cac: 6.7.14
       eslint: 9.27.0(jiti@2.4.2)
       eslint-config-flat-gitignore: 2.1.0(eslint@9.27.0(jiti@2.4.2))
-      eslint-flat-config-utils: 2.0.1
+      eslint-flat-config-utils: 2.1.0
       eslint-merge-processors: 2.0.0(eslint@9.27.0(jiti@2.4.2))
       eslint-plugin-antfu: 3.1.1(eslint@9.27.0(jiti@2.4.2))
       eslint-plugin-command: 3.2.0(eslint@9.27.0(jiti@2.4.2))
@@ -4402,9 +4402,8 @@ snapshots:
 
   '@emotion/unitless@0.8.1': {}
 
-  '@es-joy/jsdoccomment@0.50.1':
+  '@es-joy/jsdoccomment@0.50.2':
     dependencies:
-      '@types/eslint': 9.6.1
       '@types/estree': 1.0.7
       '@typescript-eslint/types': 8.32.1
       comment-parser: 1.4.1
@@ -4754,7 +4753,7 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.19.1
 
-  '@nuxt/kit@3.17.3':
+  '@nuxt/kit@3.17.4':
     dependencies:
       c12: 3.0.4
       consola: 3.4.2
@@ -4792,6 +4791,8 @@ snapshots:
     dependencies:
       quansync: 0.2.10
 
+  '@rolldown/pluginutils@1.0.0-beta.9': {}
+
   '@rollup/rollup-android-arm-eabi@4.41.0':
     optional: true
 
@@ -4901,21 +4902,16 @@ snapshots:
     dependencies:
       '@types/ms': 2.1.0
 
-  '@types/eslint@9.6.1':
-    dependencies:
-      '@types/estree': 1.0.7
-      '@types/json-schema': 7.0.15
-
   '@types/estree@1.0.7': {}
 
   '@types/glob@7.2.0':
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 22.15.18
+      '@types/node': 22.15.21
 
   '@types/json-schema@7.0.15': {}
 
-  '@types/lodash@4.17.16': {}
+  '@types/lodash@4.17.17': {}
 
   '@types/mdast@4.0.4':
     dependencies:
@@ -4925,7 +4921,7 @@ snapshots:
 
   '@types/ms@2.1.0': {}
 
-  '@types/node@22.15.18':
+  '@types/node@22.15.21':
     dependencies:
       undici-types: 6.21.0
 
@@ -4937,7 +4933,7 @@ snapshots:
 
   '@types/tar@6.1.13':
     dependencies:
-      '@types/node': 22.15.18
+      '@types/node': 22.15.21
       minipass: 4.2.8
 
   '@types/trusted-types@2.0.7':
@@ -4949,7 +4945,7 @@ snapshots:
 
   '@types/yauzl@2.10.3':
     dependencies:
-      '@types/node': 22.15.18
+      '@types/node': 22.15.21
     optional: true
 
   '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
@@ -5029,13 +5025,13 @@ snapshots:
       '@typescript-eslint/types': 8.32.1
       eslint-visitor-keys: 4.2.0
 
-  '@unocss/astro@66.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
+  '@unocss/astro@66.1.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
     dependencies:
       '@unocss/core': 66.1.2
       '@unocss/reset': 66.1.2
-      '@unocss/vite': 66.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
+      '@unocss/vite': 66.1.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
     optionalDependencies:
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
     transitivePeerDependencies:
       - vue
 
@@ -5166,7 +5162,7 @@ snapshots:
     dependencies:
       '@unocss/core': 66.1.2
 
-  '@unocss/vite@66.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
+  '@unocss/vite@66.1.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@unocss/config': 66.1.2
@@ -5177,7 +5173,7 @@ snapshots:
       pathe: 2.0.3
       tinyglobby: 0.2.13
       unplugin-utils: 0.2.4
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
     transitivePeerDependencies:
       - vue
 
@@ -5234,7 +5230,7 @@ snapshots:
   '@unrs/resolver-binding-win32-x64-msvc@1.7.2':
     optional: true
 
-  '@uozi-admin/curd@4.1.4(@ant-design/icons-vue@7.0.1(vue@3.5.14(typescript@5.8.3)))(ant-design-vue@4.2.6(vue@3.5.14(typescript@5.8.3)))(dayjs@1.11.13)(lodash-es@4.17.21)(vue-router@4.5.1(vue@3.5.14(typescript@5.8.3)))(vue@3.5.14(typescript@5.8.3))':
+  '@uozi-admin/curd@4.3.0(@ant-design/icons-vue@7.0.1(vue@3.5.14(typescript@5.8.3)))(ant-design-vue@4.2.6(vue@3.5.14(typescript@5.8.3)))(dayjs@1.11.13)(lodash-es@4.17.21)(vue-router@4.5.1(vue@3.5.14(typescript@5.8.3)))(vue@3.5.14(typescript@5.8.3))':
     dependencies:
       '@ant-design/icons-vue': 7.0.1(vue@3.5.14(typescript@5.8.3))
       '@vueuse/core': 13.2.0(vue@3.5.14(typescript@5.8.3))
@@ -5249,26 +5245,27 @@ snapshots:
       vue-types: 6.0.0(vue@3.5.14(typescript@5.8.3))
       xlsx: https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
 
-  '@uozi-admin/request@2.6.0(lodash-es@4.17.21)':
+  '@uozi-admin/request@2.7.1(lodash-es@4.17.21)':
     dependencies:
       axios: 1.9.0
       lodash-es: 4.17.21
     transitivePeerDependencies:
       - debug
 
-  '@vitejs/plugin-vue-jsx@4.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
+  '@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
     dependencies:
       '@babel/core': 7.27.1
       '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.1)
+      '@rolldown/pluginutils': 1.0.0-beta.9
       '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.27.1)
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
       vue: 3.5.14(typescript@5.8.3)
     transitivePeerDependencies:
       - supports-color
 
-  '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
+  '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))':
     dependencies:
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
       vue: 3.5.14(typescript@5.8.3)
 
   '@vitest/eslint-plugin@1.2.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
@@ -5558,7 +5555,7 @@ snapshots:
     dependencies:
       call-bind: 1.0.8
       define-properties: 1.2.1
-      es-abstract: 1.23.9
+      es-abstract: 1.23.10
       es-object-atoms: 1.1.1
       get-intrinsic: 1.3.0
       is-string: 1.1.1
@@ -5569,7 +5566,7 @@ snapshots:
     dependencies:
       call-bind: 1.0.8
       define-properties: 1.2.1
-      es-abstract: 1.23.9
+      es-abstract: 1.23.10
       es-shim-unscopables: 1.1.0
 
   arraybuffer.prototype.slice@1.0.4:
@@ -5577,7 +5574,7 @@ snapshots:
       array-buffer-byte-length: 1.0.2
       call-bind: 1.0.8
       define-properties: 1.2.1
-      es-abstract: 1.23.9
+      es-abstract: 1.23.10
       es-errors: 1.3.0
       get-intrinsic: 1.3.0
       is-array-buffer: 3.0.5
@@ -5646,7 +5643,7 @@ snapshots:
   browserslist@4.24.5:
     dependencies:
       caniuse-lite: 1.0.30001718
-      electron-to-chromium: 1.5.155
+      electron-to-chromium: 1.5.157
       node-releases: 2.0.19
       update-browserslist-db: 1.1.3(browserslist@4.24.5)
 
@@ -5960,7 +5957,7 @@ snapshots:
     dependencies:
       domelementtype: 2.3.0
 
-  dompurify@3.2.5:
+  dompurify@3.2.6:
     optionalDependencies:
       '@types/trusted-types': 2.0.7
 
@@ -5982,7 +5979,7 @@ snapshots:
 
   eastasianwidth@0.2.0: {}
 
-  electron-to-chromium@1.5.155: {}
+  electron-to-chromium@1.5.157: {}
 
   emoji-regex@8.0.0: {}
 
@@ -6000,7 +5997,7 @@ snapshots:
   enhanced-resolve@5.18.1:
     dependencies:
       graceful-fs: 4.2.11
-      tapable: 2.2.1
+      tapable: 2.2.2
 
   entities@4.5.0: {}
 
@@ -6021,7 +6018,7 @@ snapshots:
 
   errx@0.1.0: {}
 
-  es-abstract@1.23.9:
+  es-abstract@1.23.10:
     dependencies:
       array-buffer-byte-length: 1.0.2
       arraybuffer.prototype.slice: 1.0.4
@@ -6179,7 +6176,7 @@ snapshots:
       '@eslint/compat': 1.2.9(eslint@9.27.0(jiti@2.4.2))
       eslint: 9.27.0(jiti@2.4.2)
 
-  eslint-flat-config-utils@2.0.1:
+  eslint-flat-config-utils@2.1.0:
     dependencies:
       pathe: 2.0.3
 
@@ -6207,7 +6204,7 @@ snapshots:
 
   eslint-plugin-command@3.2.0(eslint@9.27.0(jiti@2.4.2)):
     dependencies:
-      '@es-joy/jsdoccomment': 0.50.1
+      '@es-joy/jsdoccomment': 0.50.2
       eslint: 9.27.0(jiti@2.4.2)
 
   eslint-plugin-es-x@7.8.0(eslint@9.27.0(jiti@2.4.2)):
@@ -6224,7 +6221,7 @@ snapshots:
       debug: 4.4.1
       eslint: 9.27.0(jiti@2.4.2)
       eslint-import-resolver-node: 0.3.9
-      get-tsconfig: 4.10.0
+      get-tsconfig: 4.10.1
       is-glob: 4.0.3
       minimatch: 10.0.1
       semver: 7.7.2
@@ -6237,7 +6234,7 @@ snapshots:
 
   eslint-plugin-jsdoc@50.6.17(eslint@9.27.0(jiti@2.4.2)):
     dependencies:
-      '@es-joy/jsdoccomment': 0.50.1
+      '@es-joy/jsdoccomment': 0.50.2
       are-docs-informative: 0.0.2
       comment-parser: 1.4.1
       debug: 4.4.1
@@ -6271,7 +6268,7 @@ snapshots:
       enhanced-resolve: 5.18.1
       eslint: 9.27.0(jiti@2.4.2)
       eslint-plugin-es-x: 7.8.0(eslint@9.27.0(jiti@2.4.2))
-      get-tsconfig: 4.10.0
+      get-tsconfig: 4.10.1
       globals: 15.15.0
       ignore: 5.3.2
       minimatch: 9.0.5
@@ -6612,7 +6609,7 @@ snapshots:
       es-errors: 1.3.0
       get-intrinsic: 1.3.0
 
-  get-tsconfig@4.10.0:
+  get-tsconfig@4.10.1:
     dependencies:
       resolve-pkg-maps: 1.0.0
 
@@ -6891,7 +6888,7 @@ snapshots:
 
   isexe@2.0.0: {}
 
-  isomorphic-git@1.30.1:
+  isomorphic-git@1.30.2:
     dependencies:
       async-lock: 1.4.1
       clean-git-ref: 2.0.1
@@ -7036,11 +7033,11 @@ snapshots:
 
   markdown-table@3.0.4: {}
 
-  marked-highlight@2.2.1(marked@15.0.11):
+  marked-highlight@2.2.1(marked@15.0.12):
     dependencies:
-      marked: 15.0.11
+      marked: 15.0.12
 
-  marked@15.0.11: {}
+  marked@15.0.12: {}
 
   math-intrinsics@1.1.0: {}
 
@@ -7612,7 +7609,7 @@ snapshots:
 
   pinia-plugin-persistedstate@4.3.0(pinia@3.0.2(typescript@5.8.3)(vue@3.5.14(typescript@5.8.3))):
     dependencies:
-      '@nuxt/kit': 3.17.3
+      '@nuxt/kit': 3.17.4
       deep-pick-omit: 1.2.1
       defu: 6.1.4
       destr: 2.0.5
@@ -7710,7 +7707,7 @@ snapshots:
     dependencies:
       call-bind: 1.0.8
       define-properties: 1.2.1
-      es-abstract: 1.23.9
+      es-abstract: 1.23.10
       es-errors: 1.3.0
       es-object-atoms: 1.1.1
       get-intrinsic: 1.3.0
@@ -7937,7 +7934,7 @@ snapshots:
 
   speakingurl@14.0.1: {}
 
-  splitpanes@4.0.3(vue@3.5.14(typescript@5.8.3)):
+  splitpanes@4.0.4(vue@3.5.14(typescript@5.8.3)):
     dependencies:
       vue: 3.5.14(typescript@5.8.3)
 
@@ -7965,7 +7962,7 @@ snapshots:
       call-bound: 1.0.4
       define-data-property: 1.1.4
       define-properties: 1.2.1
-      es-abstract: 1.23.9
+      es-abstract: 1.23.10
       es-object-atoms: 1.1.1
       has-property-descriptors: 1.0.2
 
@@ -8030,7 +8027,7 @@ snapshots:
     dependencies:
       '@pkgr/core': 0.2.4
 
-  tapable@2.2.1: {}
+  tapable@2.2.2: {}
 
   tar@6.2.1:
     dependencies:
@@ -8071,7 +8068,7 @@ snapshots:
   tsx@4.19.2:
     dependencies:
       esbuild: 0.23.1
-      get-tsconfig: 4.10.0
+      get-tsconfig: 4.10.1
     optionalDependencies:
       fsevents: 2.3.3
     optional: true
@@ -8201,9 +8198,9 @@ snapshots:
     dependencies:
       cookie: 1.0.2
 
-  unocss@66.1.2(postcss@8.5.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3)):
+  unocss@66.1.2(postcss@8.5.3)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3)):
     dependencies:
-      '@unocss/astro': 66.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
+      '@unocss/astro': 66.1.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
       '@unocss/cli': 66.1.2
       '@unocss/core': 66.1.2
       '@unocss/postcss': 66.1.2(postcss@8.5.3)
@@ -8221,15 +8218,15 @@ snapshots:
       '@unocss/transformer-compile-class': 66.1.2
       '@unocss/transformer-directives': 66.1.2
       '@unocss/transformer-variant-group': 66.1.2
-      '@unocss/vite': 66.1.2(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
+      '@unocss/vite': 66.1.2(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))(vue@3.5.14(typescript@5.8.3))
     optionalDependencies:
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
     transitivePeerDependencies:
       - postcss
       - supports-color
       - vue
 
-  unplugin-auto-import@19.2.0(@nuxt/kit@3.17.3)(@vueuse/core@13.2.0(vue@3.5.14(typescript@5.8.3))):
+  unplugin-auto-import@19.2.0(@nuxt/kit@3.17.4)(@vueuse/core@13.2.0(vue@3.5.14(typescript@5.8.3))):
     dependencies:
       local-pkg: 1.1.1
       magic-string: 0.30.17
@@ -8238,7 +8235,7 @@ snapshots:
       unplugin: 2.3.4
       unplugin-utils: 0.2.4
     optionalDependencies:
-      '@nuxt/kit': 3.17.3
+      '@nuxt/kit': 3.17.4
       '@vueuse/core': 13.2.0(vue@3.5.14(typescript@5.8.3))
 
   unplugin-utils@0.2.4:
@@ -8246,7 +8243,7 @@ snapshots:
       pathe: 2.0.3
       picomatch: 4.0.2
 
-  unplugin-vue-components@28.5.0(@babel/parser@7.27.2)(@nuxt/kit@3.17.3)(vue@3.5.14(typescript@5.8.3)):
+  unplugin-vue-components@28.5.0(@babel/parser@7.27.2)(@nuxt/kit@3.17.4)(vue@3.5.14(typescript@5.8.3)):
     dependencies:
       chokidar: 3.6.0
       debug: 4.4.1
@@ -8259,7 +8256,7 @@ snapshots:
       vue: 3.5.14(typescript@5.8.3)
     optionalDependencies:
       '@babel/parser': 7.27.2
-      '@nuxt/kit': 3.17.3
+      '@nuxt/kit': 3.17.4
     transitivePeerDependencies:
       - supports-color
 
@@ -8326,24 +8323,24 @@ snapshots:
 
   uuid@11.1.0: {}
 
-  vite-dev-rpc@1.0.7(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)):
+  vite-dev-rpc@1.0.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)):
     dependencies:
       birpc: 2.3.0
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
-      vite-hot-client: 2.0.4(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite-hot-client: 2.0.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))
 
-  vite-hot-client@2.0.4(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)):
+  vite-hot-client@2.0.4(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)):
     dependencies:
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
 
   vite-plugin-build-id@0.5.0:
     dependencies:
-      isomorphic-git: 1.30.1
+      isomorphic-git: 1.30.2
       node-object-hash: 3.1.1
       picocolors: 1.1.1
       typescript: 5.8.3
 
-  vite-plugin-inspect@11.0.1(@nuxt/kit@3.17.3)(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)):
+  vite-plugin-inspect@11.1.0(@nuxt/kit@3.17.4)(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)):
     dependencies:
       ansis: 3.17.0
       debug: 4.4.1
@@ -8353,10 +8350,10 @@ snapshots:
       perfect-debounce: 1.0.0
       sirv: 3.0.1
       unplugin-utils: 0.2.4
-      vite: 6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
-      vite-dev-rpc: 1.0.7(vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))
+      vite: 6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0)
+      vite-dev-rpc: 1.0.7(vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0))
     optionalDependencies:
-      '@nuxt/kit': 3.17.3
+      '@nuxt/kit': 3.17.4
     transitivePeerDependencies:
       - supports-color
 
@@ -8365,7 +8362,7 @@ snapshots:
       svgo: 3.3.2
       vue: 3.5.14(typescript@5.8.3)
 
-  vite@6.3.5(@types/node@22.15.18)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0):
+  vite@6.3.5(@types/node@22.15.21)(jiti@2.4.2)(less@4.3.0)(tsx@4.19.2)(yaml@2.8.0):
     dependencies:
       esbuild: 0.25.4
       fdir: 6.4.4(picomatch@4.0.2)
@@ -8374,7 +8371,7 @@ snapshots:
       rollup: 4.41.0
       tinyglobby: 0.2.13
     optionalDependencies:
-      '@types/node': 22.15.18
+      '@types/node': 22.15.21
       fsevents: 2.3.3
       jiti: 2.4.2
       less: 4.3.0
@@ -8385,7 +8382,7 @@ snapshots:
 
   vue-dompurify-html@5.3.0(vue@3.5.14(typescript@5.8.3)):
     dependencies:
-      dompurify: 3.2.5
+      dompurify: 3.2.6
       vue: 3.5.14(typescript@5.8.3)
 
   vue-eslint-parser@10.1.3(eslint@9.27.0(jiti@2.4.2)):

+ 2 - 2
app/src/api/acme_user.ts

@@ -1,5 +1,5 @@
 import type { ModelBase } from '@/api/curd'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export interface AcmeUser extends ModelBase {
   name: string
@@ -10,7 +10,7 @@ export interface AcmeUser extends ModelBase {
 
 const baseUrl = '/acme_users'
 
-const acme_user = useCurdApi<AcmeUser>(baseUrl, {
+const acme_user = extendCurdApi(useCurdApi<AcmeUser>(baseUrl), {
   register: (id: number) => http.post(`${baseUrl}/${id}/register`),
 })
 

+ 2 - 2
app/src/api/config.ts

@@ -1,6 +1,6 @@
 import type { GetListResponse } from '@/api/curd'
 import type { ChatComplicationMessage } from '@/api/openai'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export interface ModelBase {
   id: number
@@ -25,7 +25,7 @@ export interface ConfigBackup extends ModelBase {
   content: string
 }
 
-const config = useCurdApi<Config>('/configs', {
+const config = extendCurdApi(useCurdApi<Config>('/configs'), {
   get_base_path: () => http.get('/config_base_path'),
   mkdir: (basePath: string, name: string) => http.post('/config_mkdir', { base_path: basePath, folder_name: name }),
   rename: (basePath: string, origName: string, newName: string, syncNodeIds?: number[]) => http.post('/config_rename', {

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

@@ -1,5 +1,3 @@
-import { http } from '@uozi-admin/request'
-
 export interface ModelBase {
   id: number
   created_at: string
@@ -23,95 +21,3 @@ export interface UpdateOrderRequest {
   direction: number
   affected_ids: number[]
 }
-
-class Curd<T> {
-  protected readonly baseUrl: string
-
-  get_list = this._get_list.bind(this)
-  get = this._get.bind(this)
-  save = this._save.bind(this)
-  import = this._import.bind(this)
-  import_check = this._import_check.bind(this)
-  destroy = this._destroy.bind(this)
-  recover = this._recover.bind(this)
-  update_order = this._update_order.bind(this)
-  batch_save = this._batch_save.bind(this)
-  batch_destroy = this._batch_destroy.bind(this)
-  batch_recover = this._batch_recover.bind(this)
-
-  constructor(baseUrl: string) {
-    this.baseUrl = baseUrl
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _get_list(params: any = null): Promise<GetListResponse<T>> {
-    return http.get(this.baseUrl, { params })
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _get(id: any = null, params: any = {}): Promise<T> {
-    return http.get(this.baseUrl + (id ? `/${encodeURIComponent(id)}` : ''), { params })
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _save(id: any = null, data: any = {}, config: any = undefined): Promise<T> {
-    return http.post(this.baseUrl + (id ? `/${encodeURIComponent(id)}` : ''), data, config)
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _import_check(formData: FormData, config: any = {}): Promise<T> {
-    return http.post(`${this.baseUrl}/import_check`, formData, {
-      headers: {
-        'Content-Type': 'multipart/form-data;charset=UTF-8',
-      },
-      ...config,
-    })
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _import(data: any, config: any = {}): Promise<T> {
-    return http.post(`${this.baseUrl}/import`, data, config)
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _destroy(id: any = null, params: any = {}) {
-    return http.delete(`${this.baseUrl}/${encodeURIComponent(id)}`, { params })
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _recover(id: any = null) {
-    return http.patch(`${this.baseUrl}/${encodeURIComponent(id)}`)
-  }
-
-  _update_order(data: { target_id: number, direction: number, affected_ids: number[] }) {
-    return http.post(`${this.baseUrl}/order`, data)
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _batch_save(ids: any, data: any) {
-    return http.put(this.baseUrl, {
-      ids,
-      data,
-    })
-  }
-
-  // eslint-disable-next-line ts/no-explicit-any
-  _batch_destroy(ids?: (string | number)[], params: any = {}) {
-    return http.delete(this.baseUrl, {
-      params,
-      data: {
-        ids,
-      },
-    })
-  }
-
-  _batch_recover(ids?: (string | number)[]) {
-    return http.patch(this.baseUrl, {
-      data: {
-        ids,
-      },
-    })
-  }
-}
-
-export default Curd

+ 2 - 2
app/src/api/env_group.ts

@@ -1,5 +1,5 @@
 import type { ModelBase, UpdateOrderRequest } from '@/api/curd'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 // Post-sync action types
 export const PostSyncAction = {
   None: 'none',
@@ -14,7 +14,7 @@ export interface EnvGroup extends ModelBase {
 
 const baseUrl = '/env_groups'
 
-const env_group = useCurdApi<EnvGroup>(baseUrl, {
+const env_group = extendCurdApi(useCurdApi<EnvGroup>(baseUrl), {
   updateOrder(data: UpdateOrderRequest) {
     return http.post('/env_groups/order', data)
   },

+ 2 - 2
app/src/api/environment.ts

@@ -1,5 +1,5 @@
 import type { ModelBase } from '@/api/curd'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export interface Environment extends ModelBase {
   name: string
@@ -17,7 +17,7 @@ export interface Node {
 
 const baseUrl = '/environments'
 
-const environment = useCurdApi<Environment>(baseUrl, {
+const environment = extendCurdApi(useCurdApi<Environment>(baseUrl), {
   load_from_settings: () => http.post(`${baseUrl}/load_from_settings`),
 })
 

+ 2 - 2
app/src/api/notification.ts

@@ -1,5 +1,5 @@
 import type { ModelBase } from '@/api/curd'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export interface Notification extends ModelBase {
   type: string
@@ -9,7 +9,7 @@ export interface Notification extends ModelBase {
 
 const baseUrl = '/notifications'
 
-const notification = useCurdApi<Notification>(baseUrl, {
+const notification = extendCurdApi(useCurdApi<Notification>(baseUrl), {
   clear: () => http.delete(baseUrl),
 })
 

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

@@ -19,7 +19,7 @@ const passkey = {
       },
     })
   },
-  get_list() {
+  getList() {
     return http.get('/passkeys')
   },
   update(passkeyId: number, data: Passkey) {

+ 2 - 2
app/src/api/site.ts

@@ -4,7 +4,7 @@ import type { EnvGroup } from '@/api/env_group'
 import type { NgxConfig } from '@/api/ngx'
 import type { ChatComplicationMessage } from '@/api/openai'
 import type { ConfigStatus, PrivateKeyType } from '@/constants'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export type SiteStatus = ConfigStatus.Enabled | ConfigStatus.Disabled | ConfigStatus.Maintenance
 
@@ -35,7 +35,7 @@ export interface AutoCertRequest {
 
 const baseUrl = '/sites'
 
-const site = useCurdApi<Site>(baseUrl, {
+const site = extendCurdApi(useCurdApi<Site>(baseUrl), {
   enable: (name: string) => http.post(`${baseUrl}/${encodeURIComponent(name)}/enable`),
   disable: (name: string) => http.post(`${baseUrl}/${name}/disable`),
   rename: (oldName: string, newName: string) => http.post(`${baseUrl}/${encodeURIComponent(oldName)}/rename`, { new_name: newName }),

+ 2 - 2
app/src/api/stream.ts

@@ -1,7 +1,7 @@
 import type { EnvGroup } from './env_group'
 import type { NgxConfig } from '@/api/ngx'
 import type { ChatComplicationMessage } from '@/api/openai'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export interface Stream {
   modified_at: string
@@ -19,7 +19,7 @@ export interface Stream {
 
 const baseUrl = '/streams'
 
-const stream = useCurdApi<Stream>(baseUrl, {
+const stream = extendCurdApi(useCurdApi<Stream>(baseUrl), {
   enable: (name: string) => http.post(`${baseUrl}/${encodeURIComponent(name)}/enable`),
   disable: (name: string) => http.post(`${baseUrl}/${encodeURIComponent(name)}/disable`),
   duplicate: (name: string, data: { name: string }) => http.post(`${baseUrl}/${encodeURIComponent(name)}/duplicate`, data),

+ 2 - 2
app/src/api/template.ts

@@ -1,5 +1,5 @@
 import type { NgxDirective, NgxLocation, NgxServer } from '@/api/ngx'
-import { http, useCurdApi } from '@uozi-admin/request'
+import { extendCurdApi, http, useCurdApi } from '@uozi-admin/request'
 
 export interface Variable {
   type?: string
@@ -22,7 +22,7 @@ export interface Template extends NgxServer {
 
 const baseUrl = '/templates'
 
-const template = useCurdApi<Template>(baseUrl, {
+const template = extendCurdApi(useCurdApi<Template>(baseUrl), {
   get_config_list: () => http.get(`${baseUrl}/configs`),
   get_block_list: () => http.get(`${baseUrl}/blocks`),
   get_config: (name: string) => http.get(`${baseUrl}/config/${name}`),

+ 7 - 7
app/src/components/ConfigHistory/ConfigHistory.vue

@@ -1,12 +1,10 @@
-<script setup lang="ts">
+<script setup lang="tsx">
 import type { Key } from 'ant-design-vue/es/_util/type'
 import type { ConfigBackup } from '@/api/config'
 import type { GetListResponse } from '@/api/curd'
+import { datetimeRender, StdPagination } from '@uozi-admin/curd'
 import { message } from 'ant-design-vue'
-import { defineAsyncComponent } from 'vue'
 import config from '@/api/config'
-import StdPagination from '@/components/StdDesign/StdDataDisplay/StdPagination.vue'
-import { datetime } from '../StdDesign/StdDataDisplay/StdTableTransformer'
 
 // Define props for the component
 const props = defineProps<{
@@ -54,7 +52,9 @@ const columns = [
   {
     title: () => $gettext('Modified At'),
     dataIndex: 'created_at',
-    customRender: datetime,
+    customRender: args => {
+      return <span>{datetimeRender(args)}</span>
+    },
   },
 ]
 
@@ -152,8 +152,8 @@ const compareButtonText = computed(() => {
 
         <div class="history-footer">
           <StdPagination
-            :pagination="pagination"
-            :loading="loading"
+            :pagination
+            :loading
             @change="changePage"
           />
 

+ 1 - 1
app/src/components/NgxConfigEditor/directive/DirectiveEditorItem.vue

@@ -38,7 +38,7 @@ init()
 watch(props, init)
 
 function save() {
-  config.save(directive.value.params, { content: content.value }).then(r => {
+  config.updateItem(directive.value.params, { content: content.value }).then(r => {
     content.value = r.content
     message.success($gettext('Saved successfully'))
   }).catch(r => {

+ 2 - 3
app/src/components/Notification/Notification.vue

@@ -1,7 +1,6 @@
 <script setup lang="ts">
 import type { Ref } from 'vue'
 import type { Notification } from '@/api/notification'
-import type { CustomRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { BellOutlined, CheckCircleOutlined, CloseCircleOutlined, DeleteOutlined, InfoCircleOutlined, WarningOutlined } from '@ant-design/icons-vue'
 import { message, notification } from 'ant-design-vue'
 import dayjs from 'dayjs'
@@ -39,7 +38,7 @@ connect({
 
     notification[typeTrans[data.type]]({
       message: $gettext(data.title),
-      description: detailRender({ text: data.details, record: data } as CustomRender),
+      description: detailRender({ text: data.details, record: data }),
     })
   },
 })
@@ -75,7 +74,7 @@ function clear() {
 }
 
 function remove(id: number) {
-  notificationApi.destroy(id).then(() => {
+  notificationApi.deleteItem(id).then(() => {
     message.success($gettext('Removed successfully'))
     init()
   })

+ 1 - 1
app/src/components/Notification/detailRender.tsx

@@ -2,7 +2,7 @@ import type { CustomRenderArgs } from '@uozi-admin/curd'
 import { NotificationTypeT } from '@/constants'
 import notifications from './notifications'
 
-export function detailRender(args: CustomRenderArgs) {
+export function detailRender(args: Pick<CustomRenderArgs, 'record' | 'text'>) {
   try {
     return (
       <div>

+ 0 - 90
app/src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue

@@ -1,90 +0,0 @@
-<script setup lang="ts">
-import type Curd from '@/api/curd'
-import type { Column } from '@/components/StdDesign/types'
-import { message } from 'ant-design-vue'
-import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
-import StdDataEntry from '@/components/StdDesign/StdDataEntry'
-
-const props = defineProps<{
-  // eslint-disable-next-line ts/no-explicit-any
-  api: Curd<any>
-  beforeSave?: () => Promise<void>
-  columns: Column[]
-}>()
-
-const emit = defineEmits(['save'])
-
-const batchColumns = ref<Column[]>([])
-const selectedRowKeys = ref<(number | string)[]>([])
-// eslint-disable-next-line ts/no-explicit-any
-const selectedRows = ref<any[]>([])
-
-const visible = ref(false)
-const data = ref({})
-const error = ref({})
-const loading = ref(false)
-
-// eslint-disable-next-line ts/no-explicit-any
-function showModal(c: Column[], rowKeys: (number | string)[], rows: any[]) {
-  data.value = {}
-  visible.value = true
-  selectedRowKeys.value = rowKeys
-  batchColumns.value = c
-  selectedRows.value = rows
-}
-
-defineExpose({
-  showModal,
-})
-
-async function ok() {
-  loading.value = true
-
-  await props.beforeSave?.()
-
-  await props.api.batch_save(selectedRowKeys.value, data.value)
-    .then(async () => {
-      message.success($gettext('Save successfully'))
-      emit('save')
-      visible.value = false
-    })
-    .finally(() => {
-      loading.value = false
-    })
-}
-</script>
-
-<template>
-  <AModal
-    v-model:open="visible"
-    class="std-curd-edit-modal"
-    :mask="false"
-    :title="$gettext('Batch Modify')"
-    :cancel-text="$gettext('No')"
-    :ok-text="$gettext('Save')"
-    :confirm-loading="loading"
-    :width="600"
-    destroy-on-close
-    @ok="ok"
-  >
-    <p>{{ $gettext('Belows are selected items that you want to batch modify') }}</p>
-    <ATable
-      class="mb-4"
-      size="small"
-      :columns="getPithyColumns(columns)"
-      :data-source="selectedRows"
-      :pagination="{ showSizeChanger: false, pageSize: 5, size: 'small' }"
-    />
-
-    <p>{{ $gettext('Leave blank if do not want to modify') }}</p>
-    <StdDataEntry
-      :data-list="batchColumns"
-      :data-source="data"
-      :errors="error"
-    />
-
-    <slot name="extra" />
-  </AModal>
-</template>
-
-<style scoped></style>

+ 0 - 108
app/src/components/StdDesign/StdDataDisplay/StdBulkActions.vue

@@ -1,108 +0,0 @@
-<script setup lang="ts" generic="T=any">
-import type Curd from '@/api/curd'
-import type { BulkActionOptions, BulkActions } from '@/components/StdDesign/types'
-import { message } from 'ant-design-vue'
-
-const props = defineProps<{
-  api: Curd<T>
-  actions: BulkActions
-  selectedRowKeys: Array<number | string>
-  inTrash?: boolean
-}>()
-
-const emit = defineEmits(['onSuccess'])
-
-const computedActions = computed(() => {
-  if (!props.inTrash) {
-    const result = { ...props.actions }
-
-    if (result.delete) {
-      result.delete = {
-        text: () => $gettext('Delete'),
-        action: ids => {
-          return props.api.batch_destroy(ids)
-        },
-      }
-    }
-    if (result.recover)
-      delete result.recover
-    return result
-  }
-  else {
-    const result = {} as { [key: string]: BulkActionOptions }
-    if (props.actions.delete) {
-      result.delete = {
-        text: () => $gettext('Delete Permanently'),
-        action: ids => {
-          return props.api.batch_destroy(ids, { permanent: true })
-        },
-      }
-    }
-    if (props.actions.recover) {
-      result.recover = {
-        text: () => $gettext('Recover'),
-        action: ids => {
-          return props.api.batch_recover(ids)
-        },
-      }
-    }
-    return result
-  }
-}) as ComputedRef<Record<string, BulkActionOptions>>
-
-const actionValue = ref('')
-
-watch(() => props.inTrash, () => {
-  actionValue.value = ''
-})
-
-function onClickApply() {
-  return new Promise(resolve => {
-    if (actionValue.value === '')
-      return resolve(false)
-
-    // call action
-    return resolve(
-      computedActions.value[actionValue.value]?.action(props.selectedRowKeys).then(async () => {
-        message.success($gettext('Apply bulk action successfully'))
-        emit('onSuccess')
-      }),
-    )
-  })
-}
-</script>
-
-<template>
-  <AFormItem>
-    <ASpace>
-      <ASelect
-        v-model:value="actionValue"
-        style="min-width: 150px"
-      >
-        <ASelectOption value="">
-          {{ $gettext('Batch Actions') }}
-        </ASelectOption>
-        <ASelectOption
-          v-for="(action, key) in computedActions"
-          :key
-          :value="key"
-        >
-          {{ action.text() }}
-        </ASelectOption>
-      </ASelect>
-      <APopconfirm
-        :cancel-text="$gettext('No')"
-        :ok-text="$gettext('OK')"
-        :title="$gettext('Are you sure you want to apply to all selected?')"
-        @confirm="onClickApply"
-      >
-        <AButton
-          danger
-          :disabled="!actionValue || !selectedRowKeys?.length"
-        >
-          {{ $gettext('Apply') }}
-        </AButton>
-      </APopconfirm>
-    </ASpace>
-  </AFormItem>
-</template>

+ 0 - 309
app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -1,309 +0,0 @@
-<script setup lang="ts" generic="T=any">
-import type { ComputedRef } from 'vue'
-import type { StdCurdProps, StdTableProps } from '@/components/StdDesign/StdDataDisplay/types'
-import type { Column } from '@/components/StdDesign/types'
-import { message } from 'ant-design-vue'
-import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
-import StdCurdDetail from '@/components/StdDesign/StdDataDisplay/StdCurdDetail.vue'
-import StdDataEntry from '@/components/StdDesign/StdDataEntry'
-import StdTable from './StdTable.vue'
-
-const props = defineProps<StdTableProps<T> & StdCurdProps<T>>()
-
-const selectedRowKeys = defineModel<(number | string)[]>('selectedRowKeys', {
-  default: () => reactive([]),
-})
-
-const selectedRows = defineModel<T[]>('selectedRows', {
-  default: () => reactive([]),
-})
-
-const visible = ref(false)
-// eslint-disable-next-line ts/no-explicit-any
-const data: any = reactive({ id: null })
-const modifyMode = ref(true)
-const editMode = ref<string>()
-const shouldRefetchList = ref(false)
-
-provide('data', data)
-provide('editMode', editMode)
-provide('shouldRefetchList', shouldRefetchList)
-
-// eslint-disable-next-line ts/no-explicit-any
-const error: any = reactive({})
-const selected = ref([])
-
-// eslint-disable-next-line ts/no-explicit-any
-function onSelect(keys: any) {
-  selected.value = keys
-}
-
-const editableColumns = computed(() => {
-  return props.columns!.filter(c => {
-    return c.edit
-  })
-}) as ComputedRef<Column[]>
-
-// eslint-disable-next-line ts/no-explicit-any
-function add(preset: any = undefined) {
-  if (props.onClickAdd)
-    return
-  Object.keys(data).forEach(v => {
-    delete data[v]
-  })
-
-  if (preset)
-    Object.assign(data, preset)
-
-  clearError()
-  visible.value = true
-  editMode.value = 'create'
-  modifyMode.value = true
-}
-
-const table = useTemplateRef('table')
-const inTrash = ref(false)
-const getParams = reactive(props.getParams ?? {})
-
-function get_list() {
-  table.value?.getList()
-}
-
-defineExpose({
-  add,
-  get_list,
-  data,
-  inTrash,
-})
-
-function clearError() {
-  Object.keys(error).forEach(v => {
-    delete error[v]
-  })
-}
-
-// eslint-disable-next-line vue/require-typed-ref
-const stdEntryRef = ref()
-
-async function ok() {
-  const { formRef } = stdEntryRef.value
-
-  clearError()
-  try {
-    await formRef.validateFields()
-    props?.beforeSave?.(data)
-    props
-      .api!.save(data.id, { ...data, ...props.overwriteParams }, { params: { ...props.overwriteParams } }).then(r => {
-      message.success($gettext('Save successfully'))
-      Object.assign(data, r)
-      get_list()
-      visible.value = false
-    }).catch(e => {
-      Object.assign(error, e.errors)
-    })
-  }
-  catch {
-    message.error($gettext('Please fill in the required fields'))
-  }
-}
-
-function cancel() {
-  visible.value = false
-
-  clearError()
-
-  if (shouldRefetchList.value) {
-    get_list()
-    shouldRefetchList.value = false
-  }
-}
-
-function edit(id: number | string) {
-  if (props.onClickEdit)
-    return
-  get(id).then(() => {
-    visible.value = true
-    modifyMode.value = true
-    editMode.value = 'modify'
-  })
-}
-
-function view(id: number | string) {
-  get(id).then(() => {
-    visible.value = true
-    modifyMode.value = false
-  })
-}
-
-async function get(id: number | string) {
-  return props
-    .api!.getItem(id, { ...props.overwriteParams }).then(async r => {
-    Object.keys(data).forEach(k => {
-      delete data[k]
-    })
-    data.id = null
-    Object.assign(data, r)
-  })
-}
-
-const modalTitle = computed(() => {
-  // eslint-disable-next-line sonarjs/no-nested-conditional
-  return data.id ? modifyMode.value ? $gettext('Modify') : $gettext('View Details') : $gettext('Add')
-})
-
-const localOverwriteParams = reactive(props.overwriteParams ?? {})
-
-const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
-
-async function handleClickBatchEdit(batchColumns: Column[]) {
-  stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys.value, selectedRows.value)
-}
-
-function handleBatchUpdated() {
-  table.value?.getList()
-  table.value?.resetSelection()
-}
-</script>
-
-<template>
-  <div class="std-curd">
-    <ACard>
-      <template #title>
-        <div class="flex items-center">
-          {{ title || $gettext('List') }}
-          <slot name="title-slot" />
-        </div>
-      </template>
-      <template #extra>
-        <ASpace>
-          <slot name="beforeAdd" />
-          <AButton
-            v-if="!disableAdd && !inTrash"
-            type="link"
-            size="small"
-            @click="add()"
-          >
-            {{ $gettext('Add') }}
-          </AButton>
-          <slot name="extra" />
-          <template v-if="!disableDelete">
-            <AButton
-              v-if="!inTrash"
-              type="link"
-              size="small"
-              :loading="table?.loading"
-              @click="inTrash = true"
-            >
-              {{ $gettext('Trash') }}
-            </AButton>
-            <AButton
-              v-else
-              type="link"
-              size="small"
-              :loading="table?.loading"
-              @click="inTrash = false"
-            >
-              {{ $gettext('Back to list') }}
-            </AButton>
-          </template>
-        </ASpace>
-      </template>
-
-      <slot name="beforeTable" />
-      <StdTable
-        ref="table"
-        v-bind="{
-          ...props,
-          getParams,
-          overwriteParams: localOverwriteParams,
-        }"
-        v-model:selected-row-keys="selectedRowKeys"
-        v-model:selected-rows="selectedRows"
-        :in-trash="inTrash"
-        @click-edit="edit"
-        @click-view="view"
-        @selected="onSelect"
-        @click-batch-modify="handleClickBatchEdit"
-      >
-        <template
-          v-for="(_, key) in $slots"
-          :key="key"
-          #[key]="slotProps"
-        >
-          <slot
-            :name="key"
-            v-bind="slotProps"
-          />
-        </template>
-      </StdTable>
-    </ACard>
-
-    <AModal
-      class="std-curd-edit-modal"
-      :mask="modalMask"
-      :title="modalTitle"
-      :open="visible"
-      :cancel-text="$gettext('Cancel')"
-      :ok-text="$gettext('Ok')"
-      :width="modalMaxWidth"
-      :footer="modifyMode ? undefined : null"
-      destroy-on-close
-      @cancel="cancel"
-      @ok="ok"
-    >
-      <div
-        v-if="!disableModify && !disableView && editMode === 'modify'"
-        class="m-2 flex justify-end"
-      >
-        <ASwitch
-          v-model:checked="modifyMode"
-          class="mr-2"
-        />
-        {{ modifyMode ? $gettext('Modify Mode') : $gettext('View Mode') }}
-      </div>
-
-      <template v-if="modifyMode">
-        <div
-          v-if="$slots.beforeEdit"
-          class="before-edit"
-        >
-          <slot
-            name="beforeEdit"
-            :data="data"
-          />
-        </div>
-
-        <StdDataEntry
-          ref="stdEntryRef"
-          :data-list="editableColumns"
-          :data-source="data"
-          :errors="error"
-        />
-
-        <slot
-          name="edit"
-          :data="data"
-        />
-      </template>
-
-      <StdCurdDetail
-        v-else
-        :columns
-        :data
-      />
-    </AModal>
-
-    <StdBatchEdit
-      ref="stdBatchEditRef"
-      :api
-      :columns
-      @save="handleBatchUpdated"
-    />
-  </div>
-</template>
-
-<style lang="less" scoped>
-:deep(.before-edit:last-child) {
-  margin-bottom: 20px;
-}
-</style>

+ 0 - 36
app/src/components/StdDesign/StdDataDisplay/StdCurdDetail.vue

@@ -1,36 +0,0 @@
-<script setup lang="ts">
-import type { ComputedRef } from 'vue'
-import type { Column } from '@/components/StdDesign/types'
-import { get } from 'lodash'
-import { CustomRender } from '@/components/StdDesign/StdDataDisplay/components/CustomRender'
-import { labelRender } from '@/components/StdDesign/StdDataEntry'
-
-const props = defineProps<{
-  columns: Column[]
-  // eslint-disable-next-line ts/no-explicit-any
-  data: any
-}>()
-
-const displayColumns: ComputedRef<Column[]> = computed(() => {
-  return props.columns.filter(c => !c.hiddenInDetail && c.dataIndex !== 'action')
-})
-</script>
-
-<template>
-  <ADescriptions
-    :column="1"
-    bordered
-  >
-    <ADescriptionsItem
-      v-for="(c, index) in displayColumns"
-      :key="index"
-      :label="labelRender(c.title)"
-    >
-      <CustomRender v-bind="{ column: c, record: data, index, text: get(data, c.dataIndex!), isDetail: true }" />
-    </ADescriptionsItem>
-  </ADescriptions>
-</template>
-
-<style scoped lang="less">
-
-</style>

+ 0 - 65
app/src/components/StdDesign/StdDataDisplay/StdPagination.vue

@@ -1,65 +0,0 @@
-<script setup lang="ts">
-import type { Pagination } from '@/api/curd'
-
-const props = withDefaults(defineProps<{
-  pagination: Pagination
-  size?: 'default' | 'small'
-  loading?: boolean
-  showSizeChanger?: boolean
-}>(), {
-  showSizeChanger: true,
-})
-
-const emit = defineEmits(['change', 'changePageSize', 'update:pagination'])
-
-function change(num: number, pageSize: number) {
-  emit('change', num, pageSize)
-}
-
-const pageSize = computed({
-  get() {
-    return props.pagination.per_page
-  },
-  set(v) {
-    emit('changePageSize', v)
-    emit('update:pagination', { ...props.pagination, per_page: v })
-  },
-})
-</script>
-
-<template>
-  <div
-    v-if="pagination.total > pagination.per_page"
-    class="pagination-container"
-  >
-    <APagination
-      v-model:page-size="pageSize"
-      :disabled="loading"
-      :current="pagination.current_page"
-      :show-size-changer="showSizeChanger"
-      :show-total="(total:number) => $ngettext('Total %{total} item', 'Total %{total} items', total, { total: total.toString() })"
-      :size="size"
-      :total="pagination.total"
-      @change="change"
-    />
-  </div>
-</template>
-
-<style lang="less">
-.ant-pagination-total-text {
-  @media (max-width: 450px) {
-    display: block;
-  }
-}
-</style>
-
-<style lang="less" scoped>
-.pagination-container {
-  padding: 10px 0 0 0;
-  display: flex;
-  justify-content: right;
-  @media (max-width: 450px) {
-    justify-content: center;
-  }
-}
-</style>

+ 0 - 623
app/src/components/StdDesign/StdDataDisplay/StdTable.vue

@@ -1,623 +0,0 @@
-<script setup lang="ts" generic="T=any">
-import type { TableProps } from 'ant-design-vue'
-import type { Key } from 'ant-design-vue/es/_util/type'
-import type { FilterValue } from 'ant-design-vue/es/table/interface'
-import type { SorterResult, TablePaginationConfig } from 'ant-design-vue/lib/table/interface'
-import type { ComputedRef, Ref } from 'vue'
-import type { RouteParams } from 'vue-router'
-import type { GetListResponse, Pagination } from '@/api/curd'
-import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/types'
-import type { Column } from '@/components/StdDesign/types'
-import { HolderOutlined } from '@ant-design/icons-vue'
-import { message } from 'ant-design-vue'
-import { debounce } from 'lodash'
-import { getPithyColumns } from '@/components/StdDesign/StdDataDisplay/methods/columns'
-import useSortable from '@/components/StdDesign/StdDataDisplay/methods/sortable'
-import StdBulkActions from '@/components/StdDesign/StdDataDisplay/StdBulkActions.vue'
-import StdDataEntry, { labelRender } from '@/components/StdDesign/StdDataEntry'
-import StdPagination from './StdPagination.vue'
-
-const props = withDefaults(defineProps<StdTableProps<T>>(), {
-  rowKey: 'id',
-})
-
-const emit = defineEmits([
-  'clickEdit',
-  'clickView',
-  'clickBatchModify',
-])
-
-const selectedRowKeys = defineModel<(number | string)[]>('selectedRowKeys', {
-  default: () => reactive([]),
-})
-
-const selectedRows = defineModel<T[]>('selectedRows', {
-  default: () => reactive([]),
-})
-
-const route = useRoute()
-
-const dataSource: Ref<T[]> = ref([])
-const expandKeysList: Ref<Key[]> = ref([])
-
-watch(dataSource, () => {
-  if (!props.expandAll)
-    return
-
-  const res: Key[] = []
-
-  function buildKeysList(record) {
-    record.children?.forEach(v => {
-      buildKeysList(v)
-    })
-    res.push(record[props.rowKey])
-  }
-
-  dataSource.value.forEach(v => {
-    buildKeysList(v)
-  })
-
-  expandKeysList.value = res
-})
-
-// eslint-disable-next-line ts/no-explicit-any
-const rowsKeyIndexMap: Ref<Record<number, any>> = ref({})
-const loading = ref(true)
-// eslint-disable-next-line ts/no-explicit-any
-const selectedRecords: Ref<Record<any, any>> = ref({})
-
-// This can be useful if there are more than one StdTable in the same page.
-// eslint-disable-next-line sonarjs/pseudo-random
-const randomId = ref(Math.random().toString(36).substring(2, 8))
-const updateFilter = ref(0)
-const init = ref(false)
-
-const pagination: Pagination = reactive({
-  total: 1,
-  per_page: 10,
-  current_page: 1,
-  total_pages: 1,
-})
-
-const filterParams = ref({})
-
-const paginationParams = ref({
-  page: 1,
-  page_size: 20,
-})
-
-const sortParams = ref({
-  order: 'desc' as 'desc' | 'asc' | undefined,
-  sort_by: '' as Key | readonly Key[] | undefined,
-})
-
-const params = computed(() => {
-  return {
-    ...filterParams.value,
-    ...sortParams.value,
-    ...props.getParams,
-    ...props.overwriteParams,
-    trash: props.inTrash,
-  }
-})
-
-onMounted(() => {
-  selectedRows.value.forEach(v => {
-    selectedRecords.value[v[props.rowKey]] = v
-  })
-})
-
-const searchColumns = computed(() => {
-  const _searchColumns: Column[] = []
-
-  props.columns.forEach((column: Column) => {
-    if (column.search) {
-      if (typeof column.search === 'object') {
-        _searchColumns.push({
-          ...column,
-          edit: column.search,
-        })
-      }
-
-      else {
-        _searchColumns.push({ ...column })
-      }
-    }
-  })
-
-  return _searchColumns
-})
-
-const pithyColumns = computed<Column[]>(() => {
-  if (props.pithy)
-    return getPithyColumns(props.columns)
-
-  return props.columns?.filter(c => {
-    return !c.hiddenInTable
-  })
-})
-
-const batchColumns = computed(() => {
-  return props.columns?.filter(column => column.batch) || []
-})
-
-const radioColumns = computed(() => {
-  return props.columns?.filter(column => column.radio) || []
-})
-
-const get_list = debounce(_get_list, 100, {
-  leading: false,
-  trailing: true,
-})
-
-onMounted(async () => {
-  if (!props.disableQueryParams) {
-    filterParams.value = {
-      ...route.query,
-      ...props.getParams,
-    }
-    paginationParams.value.page = Number(route.query.page) || 1
-    paginationParams.value.page_size = Number(route.query.page_size) || 20
-  }
-
-  await nextTick()
-
-  get_list()
-
-  if (props.sortable)
-    initSortable()
-
-  init.value = true
-})
-
-defineExpose({
-  get_list,
-  pagination,
-  resetSelection,
-  loading,
-})
-
-function destroy(id: number | string) {
-  props.api!.destroy(id, { permanent: props.inTrash }).then(() => {
-    get_list()
-    message.success($gettext('Deleted successfully'))
-  })
-}
-
-function recover(id: number | string) {
-  props.api.recover(id).then(() => {
-    message.success($gettext('Recovered Successfully'))
-    get_list()
-  })
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-function buildIndexMap(data: any, level: number = 0, index: number = 0, total: number[] = []) {
-  if (data && data.length > 0) {
-  // eslint-disable-next-line ts/no-explicit-any
-    data.forEach((v: any) => {
-      v.level = level
-
-      const current_indexes = [...total, index++]
-
-      rowsKeyIndexMap.value[v.id] = current_indexes
-      if (v.children)
-        buildIndexMap(v.children, level + 1, 0, current_indexes)
-    })
-  }
-}
-
-async function _get_list() {
-  dataSource.value = []
-  loading.value = true
-
-  // eslint-disable-next-line ts/no-explicit-any
-  await props.api?.getList({ ...params.value, ...paginationParams.value }).then(async (r: GetListResponse<any>) => {
-    dataSource.value = r.data
-    rowsKeyIndexMap.value = {}
-    if (props.sortable)
-      buildIndexMap(r.data)
-
-    if (r.pagination)
-      Object.assign(pagination, r.pagination)
-  })
-
-  loading.value = false
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-function onTableChange(_pagination: TablePaginationConfig, filters: Record<string, FilterValue>, sorter: SorterResult | SorterResult<any>[]) {
-  if (sorter) {
-    sorter = sorter as SorterResult
-    selectedRowKeys.value = []
-    sortParams.value.sort_by = sorter.field
-    switch (sorter.order) {
-      case 'ascend':
-        sortParams.value.order = 'asc'
-        break
-      case 'descend':
-        sortParams.value.order = 'desc'
-        break
-      default:
-        sortParams.value.order = undefined
-        break
-    }
-  }
-  if (filters) {
-    Object.keys(filters).forEach((v: string) => {
-      params[v] = filters[v]
-    })
-  }
-
-  if (_pagination)
-    selectedRowKeys.value = []
-}
-
-function expandedTable(keys: Key[]) {
-  expandKeysList.value = keys
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-async function onSelect(record: any, selected: boolean, _selectedRows: any[]) {
-  // console.log('onSelect', record, selected, _selectedRows)
-  if (props.selectionType === 'checkbox' || props.exportExcel || batchColumns.value.length > 0 || props.bulkActions) {
-    if (selected) {
-      _selectedRows.forEach(v => {
-        if (v) {
-          if (selectedRecords.value[v[props.rowKey]] === undefined)
-            selectedRowKeys.value.push(v[props.rowKey])
-
-          selectedRecords.value[v[props.rowKey]] = v
-        }
-      })
-    }
-    else {
-      selectedRowKeys.value.splice(selectedRowKeys.value.indexOf(record[props.rowKey]), 1)
-      delete selectedRecords.value[record[props.rowKey]]
-    }
-    await nextTick()
-    selectedRows.value = [...selectedRowKeys.value.map(v => selectedRecords.value[v])]
-  }
-  else if (selected) {
-    selectedRowKeys.value = record[props.rowKey]
-    selectedRows.value = [record]
-  }
-  else {
-    selectedRowKeys.value = []
-    selectedRows.value = []
-  }
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-async function onSelectAll(selected: boolean, _selectedRows: any[], changeRows: any[]) {
-  // console.log('onSelectAll', selected, selectedRows, changeRows)
-  // eslint-disable-next-line ts/no-explicit-any
-  changeRows.forEach((v: any) => {
-    if (v) {
-      if (selected) {
-        selectedRowKeys.value.push(v[props.rowKey])
-        selectedRecords.value[v[props.rowKey]] = v
-      }
-      else {
-        delete selectedRecords.value[v[props.rowKey]]
-      }
-    }
-  })
-
-  if (!selected) {
-    selectedRowKeys.value.splice(0, selectedRowKeys.value.length, ...selectedRowKeys.value.filter(v => selectedRecords.value[v]))
-  }
-
-  // console.log(selectedRowKeysBuffer.value, selectedRecords.value)
-
-  await nextTick()
-  selectedRows.value.splice(0, selectedRows.value.length, ...selectedRowKeys.value.map(v => selectedRecords.value[v]))
-}
-
-function resetSelection() {
-  selectedRowKeys.value = reactive([])
-  selectedRows.value = reactive([])
-  selectedRecords.value = reactive({})
-}
-
-const router = useRouter()
-
-async function resetSearch() {
-  filterParams.value = {}
-  updateFilter.value++
-}
-
-watch(params, async v => {
-  if (!init.value)
-    return
-
-  paginationParams.value = {
-    page: 1,
-    page_size: paginationParams.value.page_size,
-  }
-
-  await nextTick()
-
-  if (!props.disableQueryParams)
-    await router.push({ query: { ...v as unknown as RouteParams, ...paginationParams.value } })
-  else
-    get_list()
-}, { deep: true })
-
-watch(() => route.query, () => {
-  if (init.value)
-    get_list()
-})
-
-const rowSelection = computed(() => {
-  if (batchColumns.value.length > 0 || props.selectionType || props.exportExcel || props.bulkActions) {
-    return {
-      selectedRowKeys: unref(selectedRowKeys),
-      onSelect,
-      onSelectAll,
-      getCheckboxProps: props?.getCheckboxProps,
-      type: (batchColumns.value.length > 0 || props.exportExcel || props.bulkActions) ? 'checkbox' : props.selectionType,
-    }
-  }
-  else {
-    return null
-  }
-}) as ComputedRef<TableProps['rowSelection']>
-
-const hasSelectedRow = computed(() => {
-  return batchColumns.value.length > 0 && selectedRowKeys.value.length > 0
-})
-
-function clickBatchEdit() {
-  emit('clickBatchModify', batchColumns.value, selectedRowKeys.value, selectedRows.value)
-}
-
-function initSortable() {
-  useSortable(props, randomId, dataSource, rowsKeyIndexMap, expandKeysList)
-}
-
-async function changePage(page: number, page_size: number) {
-  if (page) {
-    paginationParams.value = {
-      page,
-      page_size,
-    }
-  }
-  else {
-    paginationParams.value = {
-      page: 1,
-      page_size,
-    }
-  }
-
-  await nextTick()
-
-  if (!props.disableQueryParams)
-    await router.push({ query: { ...route.query, ...paginationParams.value } })
-
-  get_list()
-}
-
-const paginationSize = computed(() => {
-  if (props.size === 'small')
-    return 'small'
-  else
-    return 'default'
-})
-</script>
-
-<template>
-  <div class="std-table">
-    <div v-if="radioColumns.length">
-      <AFormItem
-        v-for="column in radioColumns"
-        :key="column.dataIndex as PropertyKey"
-        :label="labelRender(column.title)"
-      >
-        <ARadioGroup v-model:value="params[column.dataIndex as string]">
-          <ARadioButton :value="undefined">
-            {{ $gettext('All') }}
-          </ARadioButton>
-          <ARadioButton
-            v-for="(value, key) in column.mask"
-            :key
-            :value="key"
-          >
-            {{ labelRender(value) }}
-          </ARadioButton>
-        </ARadioGroup>
-      </AFormItem>
-    </div>
-    <StdDataEntry
-      v-if="!disableSearch && searchColumns.length"
-      :key="updateFilter"
-      :data-list="searchColumns"
-      :data-source="filterParams"
-      type="search"
-      layout="inline"
-    >
-      <template #action>
-        <ASpace class="action-btn">
-          <AButton @click="resetSearch">
-            {{ $gettext('Reset') }}
-          </AButton>
-          <AButton
-            v-if="hasSelectedRow"
-            @click="clickBatchEdit"
-          >
-            {{ $gettext('Batch Modify') }}
-          </AButton>
-          <slot name="append-search" />
-        </ASpace>
-      </template>
-    </StdDataEntry>
-    <StdBulkActions
-      v-if="bulkActions"
-      v-model:selected-row-keys="selectedRowKeys"
-      :api
-      :in-trash="inTrash"
-      :actions="bulkActions"
-      @on-success="() => { resetSelection(); get_list() }"
-    />
-    <ATable
-      :id="`std-table-${randomId}`"
-      :columns="pithyColumns"
-      :data-source="dataSource"
-      :loading="loading"
-      :pagination="false"
-      :row-key="rowKey"
-      :row-selection="rowSelection"
-      :scroll="{ x: scrollX ?? true }"
-      :size="size as any"
-      :expanded-row-keys="expandKeysList"
-      @change="onTableChange"
-      @expanded-rows-change="expandedTable"
-    >
-      <template #bodyCell="{ text, record, column }: {text: any, record: Record<string, any>, column: any}">
-        <template v-if="column.handle === true">
-          <span class="ant-table-drag-icon"><HolderOutlined /></span>
-          {{ text }}
-        </template>
-        <div v-if="column.dataIndex === 'action'" class="action">
-          <template v-if="!props.disableView && !inTrash">
-            <AButton
-              type="link"
-              size="small"
-              @click="$emit('clickView', record[props.rowKey], record)"
-            >
-              {{ $gettext('View') }}
-            </AButton>
-          </template>
-
-          <template v-if="!props.disableModify && !inTrash">
-            <AButton
-              type="link"
-              size="small"
-              @click="$emit('clickEdit', record[props.rowKey], record)"
-            >
-              {{ $gettext('Modify') }}
-            </AButton>
-          </template>
-
-          <slot
-            name="actions"
-            :record="record"
-          />
-
-          <template v-if="!props.disableDelete">
-            <APopconfirm
-              v-if="!inTrash"
-              :cancel-text="$gettext('No')"
-              :ok-text="$gettext('Ok')"
-              :title="$gettext('Are you sure you want to delete this item?')"
-              @confirm="destroy(record[rowKey])"
-            >
-              <AButton
-                type="link"
-                size="small"
-              >
-                {{ $gettext('Delete') }}
-              </AButton>
-            </APopconfirm>
-            <APopconfirm
-              v-else
-              :cancel-text="$gettext('No')"
-              :ok-text="$gettext('Ok')"
-              :title="$gettext('Are you sure you want to recover this item?')"
-              @confirm="recover(record[rowKey])"
-            >
-              <AButton
-                type="link"
-                size="small"
-              >
-                {{ $gettext('Recover') }}
-              </AButton>
-            </APopconfirm>
-            <APopconfirm
-              v-if="inTrash"
-              :cancel-text="$gettext('No')"
-              :ok-text="$gettext('Ok')"
-              :title="$gettext('Are you sure you want to delete this item permanently?')"
-              @confirm="destroy(record[rowKey])"
-            >
-              <AButton
-                type="link"
-                size="small"
-              >
-                {{ $gettext('Delete Permanently') }}
-              </AButton>
-            </APopconfirm>
-          </template>
-        </div>
-      </template>
-    </ATable>
-    <StdPagination
-      :size="paginationSize"
-      :loading="loading"
-      :pagination="pagination"
-      @change="changePage"
-      @change-page-size="onTableChange"
-    />
-  </div>
-</template>
-
-<style lang="less">
-.ant-table-scroll {
-  .ant-table-body {
-    overflow-x: auto !important;
-    overflow-y: hidden !important;
-  }
-}
-
-.std-table {
-  overflow-x: hidden !important;
-  overflow-y: hidden !important;
-}
-</style>
-
-<style lang="less" scoped>
-.ant-form {
-  margin: 10px 0 20px 0;
-}
-
-.ant-slider {
-  min-width: 90px;
-}
-
-.action-btn {
-  // min-height: 50px;
-  height: 100%;
-  display: flex;
-  align-items: flex-start;
-}
-
-:deep(.ant-form-inline .ant-form-item) {
-  margin-bottom: 10px;
-}
-
-.ant-divider {
-  &:last-child {
-    display: none;
-  }
-}
-
-.action {
-  @media (max-width: 768px) {
-    .ant-divider-vertical {
-      display: none;
-    }
-  }
-}
-</style>
-
-<style lang="less">
-.ant-table-drag-icon {
-  float: left;
-  margin-right: 16px;
-  cursor: grab;
-}
-
-.sortable-ghost *, .sortable-chosen * {
-  cursor: grabbing !important;
-}
-</style>

+ 0 - 155
app/src/components/StdDesign/StdDataDisplay/StdTableTransformer.tsx

@@ -1,155 +0,0 @@
-import type { VNode } from 'vue'
-import type { JSX } from 'vue/jsx-runtime'
-import { CopyOutlined } from '@ant-design/icons-vue'
-import { message, Tag } from 'ant-design-vue'
-// text, record, index, column
-import dayjs from 'dayjs'
-import { get } from 'lodash'
-
-// eslint-disable-next-line ts/no-explicit-any
-export interface CustomRender<T = any, R = any> {
-  text: T
-  record: R
-  // eslint-disable-next-line ts/no-explicit-any
-  index: any
-  // eslint-disable-next-line ts/no-explicit-any
-  column: any
-  isExport?: boolean
-  isDetail?: boolean
-}
-
-export function datetime(args: CustomRender) {
-  if (!args.text)
-    return '/'
-
-  return dayjs(args.text).format('YYYY-MM-DD HH:mm:ss')
-}
-
-export function date(args: CustomRender) {
-  return args.text ? dayjs(args.text).format('YYYY-MM-DD') : '-'
-}
-
-// Used in Export
-date.isDate = true
-datetime.isDatetime = true
-
-// eslint-disable-next-line ts/no-explicit-any
-export function mask(maskObj: any): (args: CustomRender) => JSX.Element {
-  return (args: CustomRender) => {
-    // eslint-disable-next-line ts/no-explicit-any
-    let v: any
-    if (typeof maskObj?.[args.text] === 'function')
-      v = maskObj[args.text]()
-    else if (typeof maskObj?.[args.text] === 'string')
-      v = maskObj[args.text]
-    else v = args.text
-
-    return v ?? '-'
-  }
-}
-
-export function arrayToTextRender(args: CustomRender) {
-  return args.text?.join(', ')
-}
-export function actualValueRender(actualDataIndex: string | string[]) {
-  return (args: CustomRender) => {
-    return get(args.record, actualDataIndex) || ''
-  }
-}
-
-export function longTextWithEllipsis(len: number): (args: CustomRender) => JSX.Element {
-  return (args: CustomRender) => {
-    if (args.isExport || args.isDetail)
-      return args.text
-
-    return args.text.length > len ? `${args.text.substring(0, len)}...` : args.text
-  }
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function maskRenderWithColor(maskObj: any, customColors?: Record<string | number, string> | string) {
-  return (args: CustomRender) => {
-    let label: string
-    if (typeof maskObj[args.text] === 'function')
-      label = maskObj[args.text]()
-    else if (typeof maskObj[args.text] === 'string')
-      label = maskObj[args.text]
-    else label = args.text
-
-    if (args.isExport)
-      return label
-
-    let colorMap: Record<string | number, string> = {
-      0: '',
-      1: 'blue',
-      2: 'green',
-      3: 'purple',
-      4: 'cyan',
-    }
-
-    if (typeof customColors === 'object')
-      colorMap = customColors
-
-    let color = colorMap[args.text]
-
-    if (typeof customColors === 'string')
-      color = customColors
-
-    return args.text ? h(Tag, { color }, () => label) : '/'
-  }
-}
-
-interface MultiFieldRenderProps {
-  key: string | number | string[] | number[]
-  label?: () => string
-  prefix?: string
-  suffix?: string
-  render?: ((args: CustomRender) => string | number | VNode) | (() => ((args: CustomRender) => string | VNode))
-  direction?: 'vertical' | 'horizontal'
-}
-
-export function multiFieldsRender(fields: MultiFieldRenderProps[]) {
-  return (args: CustomRender) => {
-    const list = fields.map(field => {
-      let label = field.label?.()
-      let value = get(args.record, field.key)
-
-      if (field.prefix)
-        value = field.prefix + value
-      if (field.suffix)
-        value += field.suffix
-
-      if (label)
-        label += ':'
-
-      const valueNode = field.render?.({ ...args, text: value }) ?? value
-      const direction = field.direction ?? 'vertical'
-
-      const labelNode = label
-        // eslint-disable-next-line sonarjs/no-nested-conditional
-        ? h(direction === 'vertical' ? 'div' : 'span', { class: 'text-gray-500 my-1 mr-1' }, label)
-        : null
-
-      return h('div', { class: 'my-4' }, [labelNode, valueNode])
-    })
-
-    return h('div', null, list)
-  }
-}
-
-export function copiableFieldRender(args: CustomRender) {
-  return h('div', null, [
-    h('span', null, args.text),
-    h(CopyOutlined, {
-      style: {
-        marginLeft: '10px',
-        cursor: 'pointer',
-      },
-      onClick: () => {
-        navigator.clipboard.writeText(args.text).then(() => {
-          message.success($gettext('Copied'))
-        })
-      },
-    }),
-  ])
-}

+ 0 - 9
app/src/components/StdDesign/StdDataDisplay/components/CustomRender.tsx

@@ -1,9 +0,0 @@
-import type { CustomRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
-import { get } from 'lodash'
-
-// eslint-disable-next-line ts/no-redeclare
-export function CustomRender(props: CustomRender) {
-  return props.column.customRender
-    ? props.column.customRender(props)
-    : get(props.record, props.column.dataIndex!)
-}

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

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

+ 0 - 7
app/src/components/StdDesign/StdDataDisplay/methods/columns.ts

@@ -1,7 +0,0 @@
-import type { Column } from '@/components/StdDesign/types'
-
-export function getPithyColumns(columns: Column[]) {
-  return columns.filter(c => {
-    return c.pithy === true && !c.hiddenInTable
-  })
-}

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

@@ -1,67 +0,0 @@
-import type { ComputedRef } from 'vue'
-import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/types'
-import type { Column, StdTableResponse } from '@/components/StdDesign/types'
-import dayjs from 'dayjs'
-import { get, set } from 'lodash'
-import { downloadCsv } from '@/lib/helper'
-
-async function exportCsv(props: StdTableProps, pithyColumns: ComputedRef<Column[]>) {
-  const header: { title?: string, key: Column['dataIndex'] }[] = []
-  // eslint-disable-next-line ts/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?.toString())
-    showColumnsMap[column?.dataIndex?.toString() as string] = column
-  })
-
-  // eslint-disable-next-line ts/no-explicit-any
-  const dataSource: any[] = []
-  let hasMore = true
-  let page = 1
-  while (hasMore) {
-    // prepare dataSource
-    await props
-      .api!.get_list({ page }).then((r: StdTableResponse) => {
-      if (r.data.length === 0) {
-        hasMore = false
-
-        return
-      }
-      dataSource.push(...r.data)
-    }).catch(() => {
-      hasMore = false
-    })
-    page += 1
-  }
-  // eslint-disable-next-line ts/no-explicit-any
-  const data: any[] = []
-
-  dataSource.forEach(row => {
-    // eslint-disable-next-line ts/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
-      set(obj, c.dataIndex as string, _data)
-    })
-    data.push(obj)
-  })
-
-  downloadCsv(header, data, `${$gettext('Export')}-${props.title}-${dayjs().format('YYYYMMDDHHmmss')}.csv`)
-}
-
-export default exportCsv

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

@@ -1,127 +0,0 @@
-import type { Key } from 'ant-design-vue/es/_util/type'
-import type { Ref } from 'vue'
-import type { StdTableProps } from '@/components/StdDesign/StdDataDisplay/types'
-import { message } from 'ant-design-vue'
-import sortable from 'sortablejs'
-
-// eslint-disable-next-line ts/no-explicit-any
-function getRowKey(item: any) {
-  return item.dataset.rowKey
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-function getTargetData(data: any, indexList: number[]): any {
-  // eslint-disable-next-line ts/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 ts/no-explicit-any
-function useSortable(props: StdTableProps, randomId: Ref<string>, dataSource: Ref<any[]>, rowsKeyIndexMap: Ref<Record<number, number[]>>, expandKeysList: Ref<Key[]>) {
-  // eslint-disable-next-line ts/no-explicit-any
-  const table: any = document.querySelector(`#std-table-${randomId.value} tbody`)
-
-  // eslint-disable-next-line no-new,new-cap,sonarjs/constructor-for-side-effects
-  new sortable(table, {
-    handle: '.ant-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: Key) => _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 ts/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 ts/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 ts/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
-      // eslint-disable-next-line sonarjs/no-equals-in-for-termination
-      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'))
-      })
-    },
-  })
-}
-
-export default useSortable

+ 0 - 50
app/src/components/StdDesign/StdDataDisplay/types.d.ts

@@ -1,50 +0,0 @@
-import type { ImportConfig } from '@/components/StdDesign/StdDataImport/types'
-
-export interface StdCurdProps<T> extends StdTableProps<T> {
-  cardTitleKey?: string
-  modalMaxWidth?: string | number
-  modalMask?: boolean
-  exportExcel?: boolean
-  importExcel?: boolean
-
-  disableAdd?: boolean
-  onClickAdd?: () => void
-
-  onClickEdit?: (id: number | string, record: T, index: number) => void
-  // eslint-disable-next-line ts/no-explicit-any
-  beforeSave?: (data: any) => Promise<void>
-  importConfig?: ImportConfig
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export interface StdTableProps<T = any> {
-  title?: string
-  mode?: string
-  rowKey?: string
-
-  api: Curd<T>
-  columns: Column[]
-  // eslint-disable-next-line ts/no-explicit-any
-  getParams?: Record<string, any>
-  size?: string
-  disableQueryParams?: boolean
-  disableSearch?: boolean
-  pithy?: boolean
-  exportExcel?: boolean
-  exportMaterial?: boolean
-  // eslint-disable-next-line ts/no-explicit-any
-  overwriteParams?: Record<string, any>
-  disableView?: boolean
-  disableModify?: boolean
-  selectionType?: string
-  sortable?: boolean
-  disableDelete?: boolean
-  disablePagination?: boolean
-  sortableMoveHook?: (oldRow: number[], newRow: number[]) => boolean
-  scrollX?: string | number
-  // eslint-disable-next-line ts/no-explicit-any
-  getCheckboxProps?: (record: any) => any
-  bulkActions?: BulkActions
-  inTrash?: boolean
-  expandAll?: boolean
-}

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

@@ -1,119 +0,0 @@
-<script setup lang="tsx">
-import type { FormInstance } from 'ant-design-vue'
-import type { Ref } from 'vue'
-import type { Column, JSXElements, StdDesignEdit } from '@/components/StdDesign/types'
-import { Form } from 'ant-design-vue'
-import { labelRender } from '@/components/StdDesign/StdDataEntry'
-import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
-
-const props = defineProps<{
-  dataList: Column[]
-  errors?: Record<string, string>
-  type?: 'search' | 'edit'
-  layout?: 'horizontal' | 'vertical' | 'inline'
-}>()
-
-defineSlots<{
-  // eslint-disable-next-line ts/no-explicit-any
-  action: () => any
-}>()
-
-// eslint-disable-next-line ts/no-explicit-any
-const dataSource = defineModel<Record<string, any>>('dataSource')
-
-const slots = useSlots()
-
-function extraRender(extra?: string | (() => string)) {
-  if (typeof extra === 'function')
-    return extra()
-
-  return extra
-}
-
-const formRef = ref<FormInstance>()
-
-defineExpose({
-  formRef,
-})
-
-function Render() {
-  const template: JSXElements = []
-  const isCreate = inject<Ref<string>>('editMode', ref(''))?.value === 'create'
-
-  props.dataList.forEach((v: Column) => {
-    const dataIndex = (v.edit?.actualDataIndex ?? v.dataIndex) as string
-
-    dataSource.value![dataIndex] = dataSource.value![dataIndex]
-    if (props.type === 'search') {
-      if (v.search) {
-        const type = (v.search as StdDesignEdit)?.type || v.edit?.type
-
-        template.push(
-          <StdFormItem
-            label={labelRender(v.title)}
-            extra={extraRender(v.extra)}
-            error={props.errors}
-          >
-            {type?.(v.edit!, dataSource.value, v.dataIndex)}
-          </StdFormItem>,
-        )
-      }
-
-      return
-    }
-
-    // console.log(isCreate && v.hiddenInCreate, !isCreate && v.hiddenInModify)
-    if ((isCreate && v.hiddenInCreate) || (!isCreate && v.hiddenInModify))
-      return
-
-    let show = true
-    if (v.edit?.show && typeof v.edit.show === 'function')
-      show = v.edit.show(dataSource.value)
-
-    if (v.edit?.type && show) {
-      template.push(
-        <StdFormItem
-          key={dataIndex}
-          dataIndex={dataIndex}
-          label={labelRender(v.title)}
-          extra={extraRender(v.extra)}
-          error={props.errors}
-          required={v.edit?.config?.required}
-          hint={v.edit?.hint}
-          noValidate={v.edit?.config?.noValidate}
-        >
-          {v.edit.type(v.edit, dataSource.value, dataIndex)}
-        </StdFormItem>,
-      )
-    }
-  })
-
-  if (slots.action)
-    template.push(<div class="std-data-entry-action">{slots.action()}</div>)
-
-  return (
-    <Form
-      class="my-10px!"
-      ref={formRef}
-      model={dataSource.value}
-      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>

+ 0 - 63
app/src/components/StdDesign/StdDataEntry/StdFormItem.vue

@@ -1,63 +0,0 @@
-<script setup lang="ts">
-import type { Rule } from 'ant-design-vue/es/form'
-import type { Column } from '@/components/StdDesign/types'
-import FormErrors from '@/constants/form_errors'
-
-const props = defineProps<Props>()
-
-export interface Props {
-  dataIndex?: Column['dataIndex']
-  label?: string
-  extra?: string
-  hint?: string | (() => string)
-  error?: {
-    [key: string]: string
-  }
-  required?: boolean
-  noValidate?: boolean
-}
-
-const tag = computed(() => {
-  return props.error?.[props.dataIndex!.toString()] ?? ''
-})
-
-const help = computed(() => {
-  const rules = tag.value.split(',')
-
-  for (const rule of rules) {
-    if (FormErrors[rule])
-      return FormErrors[rule]()
-  }
-
-  return props.hint
-})
-
-// eslint-disable-next-line ts/no-explicit-any
-async function validator(_: Rule, value: any): Promise<any> {
-  return new Promise((resolve, reject) => {
-    if (props.required && !props.noValidate && (!value && value !== 0)) {
-      reject(help.value ?? $gettext('This field should not be empty'))
-
-      return
-    }
-
-    resolve(true)
-  })
-}
-</script>
-
-<template>
-  <AFormItem
-    :name="dataIndex as string"
-    :label="label"
-    :help="help"
-    :rules="{ required, validator }"
-    :validate-status="tag ? 'error' : undefined"
-    :auto-link="false"
-  >
-    <slot />
-  </AFormItem>
-</template>
-
-<style scoped lang="less">
-</style>

+ 0 - 65
app/src/components/StdDesign/StdDataEntry/components/StdPassword.vue

@@ -1,65 +0,0 @@
-<script setup lang="ts">
-defineProps<{
-  generate?: boolean
-  placeholder?: string
-}>()
-
-const modelValue = defineModel<string>('value', {
-  default: () => {
-    return ''
-  },
-})
-
-const visibility = ref(false)
-
-function handleGenerate() {
-  visibility.value = true
-  modelValue.value = 'xxxx'
-
-  const chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-  const passwordLength = 12
-  let password = ''
-  for (let i = 0; i <= passwordLength; i++) {
-    // eslint-disable-next-line sonarjs/pseudo-random
-    const randomNumber = Math.floor(Math.random() * chars.length)
-
-    password += chars.substring(randomNumber, randomNumber + 1)
-  }
-
-  modelValue.value = password
-}
-</script>
-
-<template>
-  <div>
-    <AInputGroup compact>
-      <AInputPassword
-        v-if="!visibility"
-        v-model:value="modelValue"
-        :class="{ compact: generate }"
-        :placeholoder="placeholder"
-        :maxlength="20"
-      />
-      <AInput
-        v-else
-        v-model:value="modelValue"
-        :class="{ compact: generate }"
-        :placeholoder="placeholder"
-        :maxlength="20"
-      />
-      <AButton
-        v-if="generate"
-        type="primary"
-        @click="handleGenerate"
-      >
-        {{ $gettext('Generate') }}
-      </AButton>
-    </AInputGroup>
-  </div>
-</template>
-
-<style lang="less" scoped>
-:deep(.ant-input-group.ant-input-group-compact) {
-  display: flex;
-}
-</style>

+ 0 - 74
app/src/components/StdDesign/StdDataEntry/components/StdSelect.vue

@@ -1,74 +0,0 @@
-<script setup lang="ts">
-import type { SelectProps } from 'ant-design-vue'
-
-const props = defineProps<{
-  mask?: Record<string | number, string | (() => string)> | (() => Promise<Record<string | number, string>>)
-  placeholder?: string
-  multiple?: boolean
-  // eslint-disable-next-line ts/no-explicit-any
-  defaultValue?: any
-}>()
-
-const selectedValue = defineModel<string | number | string[] | number[]>('value')
-const options = ref<SelectProps['options']>([])
-
-async function loadOptions() {
-  options.value = []
-  let actualValue: number | string
-  if (typeof props.mask === 'function') {
-    const getOptions = props.mask as (() => Promise<Record<string | number, string>>)
-
-    const r = await getOptions()
-    for (const [value, label] of Object.entries(r)) {
-      actualValue = value
-      if (typeof selectedValue.value === 'number')
-        actualValue = Number(value)
-      options.value?.push({ label, value: actualValue })
-    }
-
-    return
-  }
-  for (const [value, label] of Object.entries(props.mask as Record<string | number, string | (() => string)>)) {
-    let actualLabel = label
-
-    if (typeof label === 'function')
-      actualLabel = label()
-
-    actualValue = value
-    if (typeof selectedValue.value === 'number')
-      actualValue = Number(value)
-
-    options.value?.push({ label: actualLabel, value: actualValue })
-    if (actualValue === selectedValue.value)
-      selectedValue.value = actualValue
-  }
-}
-
-function init() {
-  loadOptions()
-}
-
-watch(props, init)
-
-onMounted(() => {
-  if (!selectedValue.value && props.defaultValue)
-    selectedValue.value = props.defaultValue
-
-  init()
-})
-</script>
-
-<template>
-  <ASelect
-    v-model:value="selectedValue"
-    allow-clear
-    :options="options"
-    :placeholder="props.placeholder"
-    :default-active-first-option="false"
-    :mode="props.multiple ? 'multiple' : undefined"
-    class="min-w-180px w-auto!"
-    :get-popup-container="triggerNode => triggerNode.parentNode"
-  />
-</template>
-
-<style lang="less" scoped></style>

+ 0 - 259
app/src/components/StdDesign/StdDataEntry/components/StdSelector.vue

@@ -1,259 +0,0 @@
-<script setup lang="ts">
-import type Curd from '@/api/curd'
-import type { Column } from '@/components/StdDesign/types'
-import { CloseCircleFilled } from '@ant-design/icons-vue'
-import { watchOnce } from '@vueuse/core'
-import { clone } from 'lodash'
-import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
-
-const props = defineProps<{
-  placeholder?: string
-  label?: string
-  selectionType: 'radio' | 'checkbox'
-  recordValueIndex: string // to index the value of the record
-  // eslint-disable-next-line ts/no-explicit-any
-  api: Curd<any>
-  columns: Column[]
-  disableSearch?: boolean
-  // eslint-disable-next-line ts/no-explicit-any
-  getParams?: any
-  description?: string
-  errorMessages?: string
-  itemKey?: string // default: id
-  // eslint-disable-next-line ts/no-explicit-any
-  value?: any | any[]
-  disabled?: boolean
-  // eslint-disable-next-line ts/no-explicit-any
-  valueApi?: Curd<any>
-  // eslint-disable-next-line ts/no-explicit-any
-  getCheckboxProps?: (record: any) => any
-  hideInputContainer?: boolean
-  expandAll?: boolean
-}>()
-
-const selectedKey = defineModel<number | number[] | undefined | null | string | string[]>('selectedKey')
-
-onMounted(() => {
-  if (!selectedKey.value)
-    watchOnce(selectedKey, _init)
-  else
-    _init()
-})
-
-const visible = ref(false)
-// eslint-disable-next-line ts/no-explicit-any
-const M_values = ref([]) as Ref<any[]>
-
-const ComputedMValue = computed(() => {
-  return M_values.value.filter(v => v && Object.keys(v).length > 0)
-})
-
-// eslint-disable-next-line ts/no-explicit-any
-const records = defineModel<any[]>('selectedRecords', {
-  default: () => [],
-})
-
-watch(() => props.value, () => {
-  if (props.selectionType === 'radio')
-    M_values.value = [props.value]
-  else if (typeof selectedKey.value === 'object')
-    M_values.value = props.value || []
-})
-
-async function _init() {
-  // valueApi is used to fetch items that are using itemKey as index value
-  const api = props.valueApi || props.api
-
-  M_values.value = []
-
-  if (props.selectionType === 'radio') {
-    // M_values.value = [props.value]
-    // not init value, we need to fetch them from api
-    if (!props.value && selectedKey.value && selectedKey.value !== '0') {
-      api.getItem(selectedKey.value, props.getParams).then(r => {
-        M_values.value = [r]
-        records.value = [r]
-      })
-    }
-  }
-  else if (typeof selectedKey.value === 'object') {
-    // M_values.value = props.value || []
-    // not init value, we need to fetch them from api
-    if (!props.value && (selectedKey.value?.length || 0) > 0) {
-      api.getList({
-        ...props.getParams,
-        id: selectedKey.value,
-      }).then(r => {
-        M_values.value = r.data
-        records.value = r.data
-      })
-    }
-  }
-}
-
-function show() {
-  if (!props.disabled)
-    visible.value = true
-}
-
-// eslint-disable-next-line vue/require-typed-ref
-const selectedKeyBuffer = ref()
-// eslint-disable-next-line ts/no-explicit-any
-const selectedBuffer: Ref<any[]> = ref([])
-
-watch(selectedKey, () => {
-  selectedKeyBuffer.value = clone(selectedKey.value)
-})
-
-watch(records, v => {
-  selectedBuffer.value = [...v]
-  M_values.value = [...v]
-})
-
-onMounted(() => {
-  selectedKeyBuffer.value = clone(selectedKey.value)
-  selectedBuffer.value = clone(records.value)
-})
-
-const computedSelectedKeys = computed({
-  get() {
-    if (props.selectionType === 'radio')
-      return [selectedKeyBuffer.value]
-    else
-      return selectedKeyBuffer.value
-  },
-  set(v) {
-    selectedKeyBuffer.value = v
-  },
-})
-
-async function ok() {
-  visible.value = false
-  selectedKey.value = selectedKeyBuffer.value
-  records.value = selectedBuffer.value
-  await nextTick()
-  M_values.value = clone(records.value)
-}
-
-function clear() {
-  M_values.value = []
-  if (props.selectionType === 'radio')
-    selectedKey.value = null
-  else
-    selectedKey.value = []
-}
-
-defineExpose({ show })
-</script>
-
-<template>
-  <div>
-    <div
-      v-if="!hideInputContainer"
-      class="std-selector-container"
-    >
-      <div
-        class="std-selector"
-      >
-        <div class="chips-container w-full" @click="show">
-          <div v-if="props.recordValueIndex">
-            <ATag
-              v-for="(chipText, index) in ComputedMValue"
-              :key="index"
-              class="mr-1"
-              color="orange"
-              :bordered="false"
-              @click="show"
-            >
-              {{ chipText?.[recordValueIndex] }}
-            </ATag>
-          </div>
-          <div
-            v-else
-            class="text-gray-400"
-          >
-            {{ placeholder }}
-          </div>
-        </div>
-
-        <div class="close-btn flex text-trueGray-3" @click="clear">
-          <CloseCircleFilled />
-        </div>
-      </div>
-    </div>
-    <AModal
-      :mask="false"
-      :open="visible"
-      :cancel-text="$gettext('Cancel')"
-      :ok-text="$gettext('Ok')"
-      :title="$gettext('Selector')"
-      :width="800"
-      destroy-on-close
-      @cancel="visible = false"
-      @ok="ok"
-    >
-      {{ description }}
-      <StdTable
-        v-model:selected-row-keys="computedSelectedKeys"
-        v-model:selected-rows="selectedBuffer"
-        :api
-        :columns
-        :disable-search
-        :row-key="itemKey"
-        :expand-all
-        :get-params
-        :selection-type
-        :get-checkbox-props
-        pithy
-        disable-query-params
-      />
-    </AModal>
-  </div>
-</template>
-
-<style lang="less" scoped>
-.std-selector-container {
-  min-height: 39.9px;
-  display: flex;
-  align-items: self-start;
-
-  .std-selector {
-    display: flex;
-    justify-content: space-between;
-    overflow-y: auto;
-    box-sizing: border-box;
-    font-variant: tabular-nums;
-    list-style: none;
-    font-feature-settings: 'tnum';
-    min-height: 32px;
-    max-height: 100px;
-    padding: 4px 11px;
-    font-size: 14px;
-    line-height: 1.5;
-    background-image: none;
-    border: 1px solid #d9d9d9;
-    border-radius: 6px;
-    transition: all 0.3s;
-    //margin: 0 10px 0 0;
-    cursor: pointer;
-    min-width: 180px;
-
-    .close-btn {
-      opacity: 0;
-      transition: opacity 0.3s;
-    }
-    &:hover {
-      .close-btn {
-        opacity: 1;
-      }
-    }
-  }
-}
-
-.dark {
-  .std-selector {
-    border: 1px solid #424242;
-    background-color: #141414;
-  }
-}
-</style>

+ 0 - 169
app/src/components/StdDesign/StdDataEntry/index.tsx

@@ -1,169 +0,0 @@
-import type { Dayjs } from 'dayjs'
-import type { StdDesignEdit } from '@/components/StdDesign/types'
-import {
-  DatePicker,
-  Input,
-  InputNumber,
-  RangePicker,
-  Switch,
-} from 'ant-design-vue'
-import dayjs from 'dayjs'
-import { h } from 'vue'
-import { DATE_FORMAT } from '@/constants'
-import StdPassword from './components/StdPassword.vue'
-import StdSelect from './components/StdSelect.vue'
-import StdSelector from './components/StdSelector.vue'
-import StdDataEntry from './StdDataEntry.vue'
-
-// eslint-disable-next-line ts/no-explicit-any
-export function readonly(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  return h('p', dataSource?.[dataIndex] ?? edit?.config?.defaultValue)
-}
-
-export function labelRender(title?: string | (() => string)) {
-  if (typeof title === 'function')
-    return title()
-
-  return title
-}
-
-export function placeholderHelper(edit: StdDesignEdit) {
-  return typeof edit.config?.placeholder === 'function' ? edit.config?.placeholder() : edit.config?.placeholder
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function input(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  return h(Input, {
-    'autocomplete': 'off',
-    'placeholder': placeholderHelper(edit),
-    'value': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
-    'onUpdate:value': value => {
-      dataSource[dataIndex] = value
-    },
-  })
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function inputNumber(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  if (edit.config?.defaultValue !== undefined)
-    dataSource[dataIndex] = edit.config.defaultValue
-
-  return h(InputNumber, {
-    'placeholder': placeholderHelper(edit),
-    'min': edit.config?.min,
-    'max': edit.config?.max,
-    'value': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
-    'onUpdate:value': value => {
-      dataSource[dataIndex] = value
-    },
-    'addon-before': edit.config?.addonBefore,
-    'addon-after': edit.config?.addonAfter,
-    'prefix': edit.config?.prefix,
-    'suffix': edit.config?.suffix,
-  })
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function textarea(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  if (!dataSource[dataIndex])
-    dataSource[dataIndex] = edit.config?.defaultValue
-
-  return (
-    <Input
-      v-model:value={dataSource[dataIndex]}
-      placeholder={placeholderHelper(edit)}
-    />
-  )
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function password(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  return (
-    <StdPassword
-      v-model:value={dataSource[dataIndex]}
-      value={dataSource[dataIndex] ?? edit?.config?.defaultValue}
-      generate={edit.config?.generate}
-      placeholder={placeholderHelper(edit)}
-    />
-  )
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function select(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  const actualDataIndex = edit?.actualDataIndex ?? dataIndex
-
-  return (
-    <StdSelect
-      v-model:value={dataSource[actualDataIndex]}
-      mask={edit.mask}
-      placeholder={placeholderHelper(edit)}
-      multiple={edit.select?.multiple}
-      defaultValue={edit.config?.defaultValue}
-    />
-  )
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function selector(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  return (
-    <StdSelector
-      v-model:selectedKey={dataSource[dataIndex]}
-      selectedKey={dataSource[dataIndex] || edit?.config?.defaultValue}
-      recordValueIndex={edit.selector?.recordValueIndex}
-      selectionType={edit.selector?.selectionType ?? 'radio'}
-      api={edit.selector?.api}
-      columns={edit.selector?.columns}
-      disableSearch={edit.selector?.disableSearch}
-      getParams={edit.selector?.getParams}
-      description={edit.selector?.description}
-      getCheckboxProps={edit.selector?.getCheckboxProps}
-      expandAll={edit.selector?.expandAll}
-    />
-  )
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function switcher(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  return h(Switch, {
-    'checked': dataSource?.[dataIndex] ?? edit?.config?.defaultValue,
-    // eslint-disable-next-line ts/no-explicit-any
-    'onUpdate:checked': (value: any) => {
-      dataSource[dataIndex] = value
-    },
-  })
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function datePicker(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  const date: Dayjs | undefined = dataSource?.[dataIndex] ? dayjs.unix(dataSource?.[dataIndex]) : undefined
-
-  return (
-    <DatePicker
-      allowClear
-      format={edit?.datePicker?.format ?? DATE_FORMAT}
-      picker={edit?.datePicker?.picker}
-      value={date}
-      onChange={(_, dataString) => dataSource[dataIndex] = dayjs(dataString).unix() ?? undefined}
-    />
-  )
-}
-
-// eslint-disable-next-line ts/no-explicit-any
-export function dateRangePicker(edit: StdDesignEdit, dataSource: any, dataIndex: any) {
-  const dates: [Dayjs, Dayjs] = dataSource
-    ?.[dataIndex]
-    ?.filter((item: string) => !!item)
-    ?.map((item: string) => dayjs(item))
-
-  return (
-    <RangePicker
-      allowClear
-      format={edit?.datePicker?.format ?? DATE_FORMAT}
-      picker={edit?.datePicker?.picker}
-      value={dates}
-      onChange={(_, dateStrings: [string, string]) => dataSource[dataIndex] = dateStrings}
-    />
-  )
-}
-
-export default StdDataEntry

+ 0 - 7
app/src/components/StdDesign/StdDataEntry/style.less

@@ -1,7 +0,0 @@
-.std-data-entry-action {
-  @media (max-width: 375px) {
-    display: block;
-    width: 100%;
-    margin: 10px 0;
-  }
-}

+ 0 - 25
app/src/components/StdDesign/StdDataEntry/types.d.ts

@@ -1,25 +0,0 @@
-import type { DefaultOptionType } from 'ant-design-vue/es/vc-cascader'
-
-export interface Author {
-  id?: number
-  name: string
-  checked?: boolean
-  sort?: number
-  affiliated_unit?: string
-}
-
-export interface AuthorSelector {
-  input?: {
-    title?: () => string
-    placeholder?: () => string
-  }
-  checkbox?: {
-    title?: () => string
-    placeholder?: () => string
-  }
-  select?: {
-    title?: () => string
-    placeholder?: () => string
-    options?: DefaultOptionType[]
-  }
-}

+ 0 - 141
app/src/components/StdDesign/StdDetail/StdDetail.vue

@@ -1,141 +0,0 @@
-<script setup lang="ts" generic="T extends ModelBase">
-import type { ButtonProps, FormInstance } from 'ant-design-vue'
-import type { DataIndex } from 'ant-design-vue/es/vc-table/interface'
-import type { ModelBase } from '@/api/curd'
-import type Curd from '@/api/curd'
-import type { Column, StdDesignEdit } from '@/components/StdDesign/types'
-import { message } from 'ant-design-vue'
-import { cloneDeep, get } from 'lodash'
-
-import { labelRender } from '@/components/StdDesign/StdDataEntry'
-
-const props = defineProps<{
-  title?: string
-  dataSource?: T
-  api: Curd<T>
-  columns: Column[]
-  actionButtonProps?: ButtonProps
-  useOutsideData?: boolean
-}>()
-
-const detail = ref(props.dataSource) as Ref<T | undefined>
-const editModel = ref({}) as Ref<T | undefined>
-const editStatus = ref(false)
-const loading = ref(false)
-
-const formRef = ref<FormInstance>()
-
-watch(() => props.dataSource, val => detail.value = val)
-
-async function save() {
-  try {
-    await formRef.value?.validate()
-    loading.value = true
-    props.api.save(editModel.value?.id, editModel.value).then(res => {
-      detail.value = res
-      editStatus.value = false
-    }).catch(() => {
-      message.error('Save failed')
-    }).finally(() => loading.value = false)
-  }
-  catch {
-    message.error('Validation failed')
-  }
-}
-
-function FormController(p: { editConfig: StdDesignEdit, dataIndex?: DataIndex }) {
-  return p?.editConfig?.type?.(p.editConfig, editModel.value, p.dataIndex)
-}
-
-function CustomRender(p: { column?: Column, text: unknown, record?: T }) {
-  const { column, text, record } = p
-  return column?.customRender?.({ text, record }) ?? text ?? '/'
-}
-
-const route = useRoute()
-
-onMounted(() => {
-  if (props?.useOutsideData) {
-    editModel.value = cloneDeep(props.dataSource)
-    return
-  }
-
-  props.api.getItem(route.params.id).then(res => {
-    detail.value = res
-  })
-})
-
-function clickEdit() {
-  editModel.value = cloneDeep(detail.value)
-  editStatus.value = true
-}
-</script>
-
-<template>
-  <AForm
-    ref="formRef"
-    :model="editModel"
-  >
-    <ADescriptions
-      bordered
-      :title="props.title ?? $gettext('Info')"
-      :column="2"
-    >
-      <template #extra>
-        <ASpace v-if="editStatus">
-          <AButton
-            type="primary"
-            :disabled="loading"
-            :loading="loading"
-            v-bind="props.actionButtonProps"
-            @click="save"
-          >
-            {{ $gettext('Save') }}
-          </AButton>
-          <AButton
-            :disabled="loading"
-            :loading="loading"
-            v-bind="props.actionButtonProps"
-            @click="editStatus = false"
-          >
-            {{ $gettext('Cancel') }}
-          </AButton>
-        </ASpace>
-        <div v-else>
-          <AButton
-            type="primary"
-            v-bind="props.actionButtonProps"
-            @click="clickEdit"
-          >
-            {{ $gettext('Edit') }}
-          </AButton>
-          <slot name="extra" />
-        </div>
-      </template>
-      <ADescriptionsItem
-        v-for="c in props.columns.filter(c => c.dataIndex !== 'action')"
-        :key="c.dataIndex?.toString()"
-        :label="$gettext(labelRender(c.title) ?? '')"
-      >
-        <AFormItem
-          v-if="editStatus && c.edit"
-          class="mb-0"
-          :name="c.dataIndex?.toString()"
-          :required="c?.edit?.config?.required"
-        >
-          <FormController
-            :edit-config="c.edit"
-            :data-index="c.dataIndex"
-          />
-        </AFormItem>
-        <span v-else>
-          <CustomRender
-            :column="c"
-            :text="get(detail, c.dataIndex as any)"
-            :record="detail"
-          />
-        </span>
-      </ADescriptionsItem>
-    </ADescriptions>
-  </AForm>
-</template>

+ 0 - 154
app/src/components/StdDesign/types.d.ts

@@ -1,154 +0,0 @@
-/* eslint-disable ts/no-explicit-any */
-
-import type { TableColumnType } from 'ant-design-vue'
-import type { RuleObject } from 'ant-design-vue/es/form'
-
-import type { JSX } from 'vue/jsx'
-import type { Pagination } from '@/api/curd'
-import type Curd from '@/api/curd'
-
-export type JSXElements = JSX.Element[]
-
-// use for select-option
-export type StdDesignMask =
-  Record<string | number, string | (() => string)>
-  | (() => Promise<Record<string | number, string>>)
-
-export interface StdDesignEdit {
-
-  type?: (edit: StdDesignEdit, dataSource: any, dataIndex: any) => JSX.Element // component type
-
-  show?: (dataSource: any) => boolean // show component or not
-
-  batch?: boolean // batch edit
-
-  mask?: StdDesignMask
-
-  rules?: RuleObject[] // validator rules
-
-  hint?: string | (() => string) // hint form item
-
-  actualDataIndex?: string
-
-  datePicker?: {
-    picker?: 'date' | 'week' | 'month' | 'year' | 'quarter'
-    format?: string
-  }
-
-  cascader?: {
-    api: () => Promise<any>
-    fieldNames: Record<string, string>
-  }
-
-  select?: {
-    multiple?: boolean
-  }
-
-  selector?: {
-    getParams?: Record<string | number, any>
-    selectionType?: 'radio' | 'checkbox'
-    api: Curd
-    valueApi?: Curd
-    columns: any
-    disableSearch?: boolean
-    description?: string
-    bind?: any
-    itemKey?: any // default is id
-    dataSourceValueIndex?: any // relative to dataSource
-    recordValueIndex?: any // relative to dataSource
-    getCheckboxProps?: (record: any) => any
-    expandAll?: boolean
-  } // StdSelector Config
-
-  upload?: {
-    limit?: number // upload file limitation
-    action: string // upload url
-  }
-
-  config?: {
-    label?: string | (() => string) // label for form item
-    recordValueIndex?: any // relative to api return
-    placeholder?: string | (() => string) // placeholder for input
-    generate?: boolean // generate btn for StdPassword
-    selectionType?: any
-    api?: Curd
-    valueApi?: Curd
-    columns?: any
-    disableSearch?: boolean
-    description?: string
-    bind?: any
-    itemKey?: any // default is id
-    dataSourceValueIndex?: any // relative to dataSource
-    defaultValue?: any
-    required?: boolean
-    noValidate?: boolean
-    min?: number // min value for input number
-    max?: number // max value for input number
-    addonBefore?: string // for inputNumber
-    addonAfter?: string // for inputNumber
-    prefix?: string // for inputNumber
-    suffix?: string // for inputNumber
-    size?: string // class size of Std image upload
-    error_messages?: Ref
-  }
-
-  flex?: Flex
-}
-
-export interface Flex {
-  // eslint-disable-next-line sonarjs/use-type-alias
-  sm?: string | number | boolean
-  md?: string | number | boolean
-  lg?: string | number | boolean
-  xl?: string | number | boolean
-  xxl?: string | number | boolean
-}
-
-export interface Column extends TableColumnType {
-  title?: string | (() => string)
-  edit?: StdDesignEdit
-  extra?: string | (() => string)
-  pithy?: boolean
-  search?: boolean | StdDesignEdit
-  handle?: boolean
-  hiddenInTable?: boolean
-  hiddenInTrash?: boolean
-  hiddenInCreate?: boolean
-  hiddenInModify?: boolean
-  hiddenInDetail?: boolean
-  hiddenInExport?: boolean
-  import?: boolean
-  batch?: boolean
-  radio?: boolean
-  mask?: StdDesignMask
-  customRender?: function
-  selector?: {
-    getParams?: Record<string | number, any>
-    recordValueIndex: any // relative to api return
-    selectionType: any
-    api: Curd
-    valueApi?: Curd
-    columns: any
-    disableSearch?: boolean
-    description?: string
-    bind?: any
-    itemKey?: any // default is id
-    dataSourceValueIndex?: any // relative to dataSource
-    getCheckboxProps?: (record: any) => any
-  }
-}
-
-export interface StdTableResponse {
-  data: any[]
-  pagination: Pagination
-}
-
-export interface BulkActionOptions {
-  text: () => string
-  action: (rows: (number | string)[] | undefined) => Promise<boolean>
-}
-
-export type BulkActions = Record<string, BulkActionOptions | boolean> & {
-  delete?: boolean | BulkActionOptions
-  recover?: boolean | BulkActionOptions
-}

+ 1 - 1
app/src/components/TwoFA/Authorization.vue

@@ -3,7 +3,7 @@ import type { TwoFAStatus } from '@/api/2fa'
 import { KeyOutlined } from '@ant-design/icons-vue'
 import { startAuthentication } from '@simplewebauthn/browser'
 import twoFA from '@/api/2fa'
-import OTPInput from '@/components/OTPInput/OTPInput.vue'
+import OTPInput from '@/components/OTPInput'
 import { useUserStore } from '@/pinia'
 
 defineProps<{

File diff suppressed because it is too large
+ 144 - 193
app/src/language/ar/app.po


+ 7 - 0
app/src/language/curd.ts

@@ -29,6 +29,13 @@ export const translations = {
   deletedSuccessfully: $gettext('Deleted successfully'),
   restoredSuccessfully: $gettext('Restored successfully'),
   selectAll: $gettext('Select all'),
+  batchEdit: $gettext('Batch Edit'),
+  pleaseSelectAtLeastOneItem: $gettext('Please select at least one item'),
+  batchModify: $gettext('Batch Modify'),
+  saveSuccessfully: $gettext('Save successfully'),
+  belowsAreSelectedItems: $gettext('Belows are selected items that you want to batch modify'),
+  leaveBlankIfDoNotWantToModify: $gettext('Leave blank if do not want to modify'),
+  no: $gettext('No'),
   validate: {
     required: $gettext('This field should not be empty'),
     email: $gettext('This field should be a valid email address'),

File diff suppressed because it is too large
+ 143 - 192
app/src/language/de_DE/app.po


File diff suppressed because it is too large
+ 144 - 201
app/src/language/en/app.po


File diff suppressed because it is too large
+ 196 - 194
app/src/language/es/app.po


File diff suppressed because it is too large
+ 210 - 206
app/src/language/fr_FR/app.po


File diff suppressed because it is too large
+ 232 - 206
app/src/language/ja_JP/app.po


File diff suppressed because it is too large
+ 222 - 204
app/src/language/ko_KR/app.po


File diff suppressed because it is too large
+ 144 - 200
app/src/language/messages.pot


File diff suppressed because it is too large
+ 203 - 198
app/src/language/pt_PT/app.po


File diff suppressed because it is too large
+ 205 - 201
app/src/language/ru_RU/app.po


File diff suppressed because it is too large
+ 204 - 201
app/src/language/tr_TR/app.po


File diff suppressed because it is too large
+ 215 - 210
app/src/language/uk_UA/app.po


File diff suppressed because it is too large
+ 201 - 201
app/src/language/vi_VN/app.po


File diff suppressed because it is too large
+ 208 - 199
app/src/language/zh_CN/app.po


File diff suppressed because it is too large
+ 211 - 202
app/src/language/zh_TW/app.po


+ 1 - 1
app/src/layouts/BaseLayout.vue

@@ -2,7 +2,7 @@
 import { throttle } from 'lodash'
 import { storeToRefs } from 'pinia'
 import settings from '@/api/settings'
-import PageHeader from '@/components/PageHeader/PageHeader.vue'
+import PageHeader from '@/components/PageHeader'
 import { useSettingsStore, useUserStore } from '@/pinia'
 import FooterLayout from './FooterLayout.vue'
 import HeaderLayout from './HeaderLayout.vue'

+ 1 - 1
app/src/views/certificate/CertificateEditor.vue

@@ -43,7 +43,7 @@ const errors = ref({}) as Ref<Record<string, string>>
 
 async function save() {
   try {
-    const r = await cert.save(data.value.id, data.value)
+    const r = await cert.updateItem(data.value.id, data.value)
     data.value = r
     errors.value = {}
     message.success($gettext('Save successfully'))

+ 1 - 1
app/src/views/certificate/components/RemoveCert.vue

@@ -57,7 +57,7 @@ function handleConfirm() {
   }
   else {
     // Only remove certificate from database
-    cert.destroy(props.id).then(() => {
+    cert.deleteItem(props.id).then(() => {
       message.success($gettext('Certificate removed successfully'))
       modalVisible.value = false
       confirmLoading.value = false

+ 1 - 1
app/src/views/certificate/components/WildcardCertificate.vue

@@ -2,7 +2,7 @@
 import type { Ref } from 'vue'
 import type { AutoCertOptions } from '@/api/auto_cert'
 import { message } from 'ant-design-vue'
-import AutoCertForm from '@/components/AutoCertForm/AutoCertForm.vue'
+import AutoCertForm from '@/components/AutoCertForm'
 import ObtainCertLive from '@/views/site/site_edit/components/Cert/ObtainCertLive.vue'
 
 const emit = defineEmits<{

+ 12 - 6
app/src/views/config/ConfigEditor.vue

@@ -7,11 +7,11 @@ import { message } from 'ant-design-vue'
 import { trim, trimEnd } from 'lodash'
 import config from '@/api/config'
 import ngx from '@/api/ngx'
-import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
-import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+import ChatGPT from '@/components/ChatGPT'
+import CodeEditor from '@/components/CodeEditor'
 import { ConfigHistory } from '@/components/ConfigHistory'
-import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
-import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import FooterToolBar from '@/components/FooterToolbar'
+import NodeSelector from '@/components/NodeSelector'
 import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
 import { formatDateTime } from '@/lib/helper'
 import { useSettingsStore } from '@/pinia'
@@ -183,13 +183,19 @@ onMounted(async () => {
 
 function save() {
   refForm.value?.validate().then(() => {
-    config.save(addMode.value ? undefined : relativePath.value, {
+    const payload = {
       name: addMode.value ? data.value.name : undefined,
       base_dir: addMode.value ? basePath.value : undefined,
       content: data.value.content,
       sync_node_ids: data.value.sync_node_ids,
       sync_overwrite: data.value.sync_overwrite,
-    }).then(r => {
+    }
+
+    const api = addMode.value
+      ? config.createItem(payload)
+      : config.updateItem(relativePath.value, payload)
+
+    api.then(r => {
       data.value.content = r.content
       message.success($gettext('Saved successfully'))
 

+ 1 - 1
app/src/views/config/components/Rename.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { message } from 'ant-design-vue'
 import config from '@/api/config'
-import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import NodeSelector from '@/components/NodeSelector'
 import use2FAModal from '@/components/TwoFA/use2FAModal'
 
 const emit = defineEmits(['renamed'])

+ 1 - 2
app/src/views/dashboard/components/ModulesTable.vue

@@ -1,7 +1,6 @@
 <script setup lang="tsx">
 import type { TableColumnType } from 'ant-design-vue'
 import type { FilterResetProps } from 'ant-design-vue/es/table/interface'
-import type { CustomRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { SearchOutlined } from '@ant-design/icons-vue'
 import { Button as AButton, Input as AInput } from 'ant-design-vue'
 import { useGlobalStore } from '@/pinia'
@@ -71,7 +70,7 @@ const modulesColumns: TableColumnType[] = [
         }, 100)
       }
     },
-    customRender: (args: CustomRender) => {
+    customRender: args => {
       return (
         <div>
           <div>{args.record.name}</div>

+ 6 - 6
app/src/views/environments/list/envColumns.tsx

@@ -1,5 +1,5 @@
 import type { CustomRenderArgs, StdTableColumn } from '@uozi-admin/curd'
-import type { JSXElements } from '@/components/StdDesign/types'
+import type { JSX } from 'vue/jsx-runtime'
 import { datetimeRender } from '@uozi-admin/curd'
 import { Badge, Tag } from 'ant-design-vue'
 import { h } from 'vue'
@@ -43,21 +43,21 @@ const columns: StdTableColumn[] = [{
   title: () => $gettext('Status'),
   dataIndex: 'status',
   customRender: (args: CustomRenderArgs) => {
-    const template: JSXElements = []
+    const template: JSX.Element[] = []
     const { text } = args
     if (args.record.enabled) {
       if (text === true || text > 0) {
         template.push(<Badge status="success" />)
-        template.push($gettext('Online'))
+        template.push(<span>{$gettext('Online')}</span>)
       }
       else {
         template.push(<Badge status="error" />)
-        template.push($gettext('Offline'))
+        template.push(<span>{$gettext('Offline')}</span>)
       }
     }
     else {
       template.push(<Badge status="default" />)
-      template.push($gettext('Disabled'))
+      template.push(<span>{$gettext('Disabled')}</span>)
     }
 
     return h('div', template)
@@ -69,7 +69,7 @@ const columns: StdTableColumn[] = [{
   title: () => $gettext('Enabled'),
   dataIndex: 'enabled',
   customRender: (args: CustomRenderArgs) => {
-    const template: JSXElements = []
+    const template: JSX.Element[] = []
     const { text } = args
     if (text === true || text > 0)
       template.push(<Tag color="green">{$gettext('Enabled')}</Tag>)

+ 2 - 1
app/src/views/notification/Notification.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
+import { StdCurd } from '@uozi-admin/curd'
 import { message } from 'ant-design-vue'
 import notification from '@/api/notification'
-import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
 import { useUserStore } from '@/pinia'
 import notificationColumns from '@/views/notification/notificationColumns'
 
@@ -31,6 +31,7 @@ watch(unreadCount, () => {
     disable-modify
     disable-add
     disable-trash
+    disable-export
   >
     <template #extra>
       <APopconfirm

+ 8 - 8
app/src/views/preference/components/ExternalNotify/ExternalNotifyEditor.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
+import type { StdTableColumn } from '@uozi-admin/curd'
 import type { ExternalNotifyConfig } from './types'
-import type { Column } from '@/components/StdDesign/types'
-import StdDataEntry, { input } from '@/components/StdDesign/StdDataEntry'
+import { StdForm } from '@uozi-admin/curd'
 import configMap from './index'
 
 const props = defineProps<{
@@ -14,7 +14,7 @@ const currentConfig = computed<ExternalNotifyConfig | undefined>(() => {
   return configMap[props.type?.toLowerCase() ?? '']
 })
 
-const columns = computed<Column[]>(() => {
+const columns = computed<StdTableColumn[]>(() => {
   if (!currentConfig.value)
     return []
 
@@ -23,8 +23,8 @@ const columns = computed<Column[]>(() => {
     dataIndex: item.key,
     key: item.key,
     edit: {
-      type: input,
-      config: {
+      type: 'input',
+      formItem: {
         label: item.label,
       },
     },
@@ -33,10 +33,10 @@ const columns = computed<Column[]>(() => {
 </script>
 
 <template>
-  <StdDataEntry
+  <StdForm
     v-if="currentConfig"
-    v-model:data-source="modelValue"
-    :data-list="columns"
+    v-model:data="modelValue"
+    :columns
   />
 </template>
 

+ 36 - 19
app/src/views/preference/components/ExternalNotify/columns.tsx

@@ -1,6 +1,6 @@
-import type { Column } from '@/components/StdDesign/types'
-import { datetime, mask } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
-import { select } from '@/components/StdDesign/StdDataEntry'
+import type { StdTableColumn } from '@uozi-admin/curd'
+import type { ExternalNotify } from '@/api/external_notify'
+import { datetimeRender, maskRender } from '@uozi-admin/curd'
 import gettext from '@/gettext'
 import ExternalNotifyEditor from './ExternalNotifyEditor.vue'
 import configMap from './index'
@@ -12,15 +12,17 @@ const configTypeMask = Object.keys(configMap).reduce((acc, key) => {
   return acc
 }, {})
 
-const columns: Column[] = [
+const columns: StdTableColumn[] = [
   {
     dataIndex: 'type',
     title: () => $gettext('Type'),
-    customRender: mask(configTypeMask),
+    customRender: maskRender(configTypeMask),
     edit: {
-      type: select,
-      mask: configTypeMask,
-      config: {
+      type: 'select',
+      select: {
+        mask: configTypeMask,
+      },
+      formItem: {
         required: true,
       },
     },
@@ -28,23 +30,38 @@ const columns: Column[] = [
   {
     dataIndex: 'language',
     title: () => $gettext('Language'),
-    customRender: mask(languageAvailable),
+    customRender: maskRender(languageAvailable),
     edit: {
-      type: select,
-      mask: languageAvailable,
-      config: {
+      type: 'select',
+      select: {
+        mask: languageAvailable,
+      },
+      formItem: {
         required: true,
       },
     },
   },
   {
     dataIndex: 'config',
+    title: () => $gettext('Config'),
     edit: {
-      type: (_, record) => {
-        if (!record.config) {
-          record.config = {}
+      type: (formData: ExternalNotify) => {
+        if (!formData.type) {
+          return <div />
         }
-        return <ExternalNotifyEditor v-model={record.config} type={record.type} />
+
+        if (!formData.config) {
+          formData.config = {}
+        }
+        return (
+          <div>
+            <div>{$gettext('Config')}</div>
+            <ExternalNotifyEditor v-model={formData.config} type={formData.type} />
+          </div>
+        )
+      },
+      formItem: {
+        hiddenLabelInEdit: true,
       },
     },
     hiddenInTable: true,
@@ -52,11 +69,11 @@ const columns: Column[] = [
   {
     dataIndex: 'created_at',
     title: () => $gettext('Created at'),
-    customRender: datetime,
+    customRender: datetimeRender,
   },
   {
-    dataIndex: 'action',
-    title: () => $gettext('Action'),
+    dataIndex: 'actions',
+    title: () => $gettext('Actions'),
   },
 ]
 

+ 1 - 2
app/src/views/preference/tabs/AuthSettings.vue

@@ -3,7 +3,6 @@ import type { Ref } from 'vue'
 import type { TwoFAStatus } from '@/api/2fa'
 import type { RecoveryCode } from '@/api/recovery'
 import type { BannedIP } from '@/api/settings'
-import type { CustomRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { message } from 'ant-design-vue'
 import dayjs from 'dayjs'
 import twoFA from '@/api/2fa'
@@ -23,7 +22,7 @@ const bannedIPColumns = [{
 }, {
   title: $gettext('Banned Until'),
   dataIndex: 'expired_at',
-  customRender: (args: CustomRender) => {
+  customRender: args => {
     return dayjs.unix(args.text).format('YYYY-MM-DD HH:mm:ss')
   },
 }, {

+ 2 - 2
app/src/views/preference/tabs/ExternalNotify.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
+import { StdCurd } from '@uozi-admin/curd'
 import externalNotify from '@/api/external_notify'
-import { StdCurd } from '@/components/StdDesign/StdDataDisplay'
 import columns from '../components/ExternalNotify/columns'
 </script>
 
@@ -10,7 +10,7 @@ import columns from '../components/ExternalNotify/columns'
     :columns="columns"
     :api="externalNotify"
     disable-view
-    disable-query-params
+    disable-export
   />
 </template>
 

+ 1 - 1
app/src/views/site/site_add/SiteAdd.vue

@@ -25,7 +25,7 @@ function init() {
 
 async function save() {
   return ngx.build_config(ngxConfig.value).then(r => {
-    site.save(ngxConfig.value.name, { name: ngxConfig.value.name, content: r.content, overwrite: true }).then(() => {
+    site.updateItem(ngxConfig.value.name, { name: ngxConfig.value.name, content: r.content, overwrite: true }).then(() => {
       message.success($gettext('Saved successfully'))
 
       site.enable(ngxConfig.value.name).then(() => {

+ 11 - 5
app/src/views/site/site_edit/components/Cert/ChangeCert.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import type { Cert } from '@/api/cert'
+import { StdTable } from '@uozi-admin/curd'
 import cert from '@/api/cert'
-import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
 import certColumns from '@/views/certificate/CertificateList/certColumns'
 
 interface Props {
@@ -41,6 +41,8 @@ async function ok() {
   records.value = []
   selectedKeys.value = []
 }
+
+const columns = computed(() => certColumns.filter(item => item.pure))
 </script>
 
 <template>
@@ -58,10 +60,14 @@ async function ok() {
       <StdTable
         v-model:selected-rows="records"
         :selected-row-keys="selectedKeys"
-        :api="cert"
-        pithy
-        :columns="certColumns"
-        :selection-type="selectionType"
+        :get-list-api="cert.getList"
+        only-query
+        disable-router-query
+        :columns
+        :row-selection-type="selectionType"
+        :table-props="{
+          rowKey: 'name',
+        }"
         @update:selected-row-keys="handleSelectionChange"
       />
     </AModal>

+ 1 - 1
app/src/views/site/site_edit/components/Cert/ObtainCert.vue

@@ -5,7 +5,7 @@ import type { PrivateKeyType } from '@/constants'
 import { message, Modal } from 'ant-design-vue'
 import { AutoCertChallengeMethod } from '@/api/auto_cert'
 import site from '@/api/site'
-import AutoCertStepOne from '@/components/AutoCertForm/AutoCertForm.vue'
+import AutoCertStepOne from '@/components/AutoCertForm'
 import { PrivateKeyTypeEnum } from '@/constants'
 import { useSiteEditorStore } from '../SiteEditor/store'
 import ObtainCertLive from './ObtainCertLive.vue'

+ 1 - 1
app/src/views/site/site_edit/components/ConfigTemplate/ConfigTemplate.vue

@@ -2,7 +2,7 @@
 import type { Template } from '@/api/template'
 import { storeToRefs } from 'pinia'
 import template from '@/api/template'
-import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+import CodeEditor from '@/components/CodeEditor'
 import { DirectiveEditor, LocationEditor, useNgxConfigStore } from '@/components/NgxConfigEditor'
 import { useSettingsStore } from '@/pinia'
 import { useConfigTemplateStore } from './store'

+ 5 - 5
app/src/views/site/site_edit/components/RightPanel/Basic.vue

@@ -1,9 +1,9 @@
 <script setup lang="ts">
 import type { SiteStatus } from '@/api/site'
 import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { StdSelector } from '@uozi-admin/curd'
 import envGroup from '@/api/env_group'
-import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
-import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
+import NodeSelector from '@/components/NodeSelector'
 import { formatDateTime } from '@/lib/helper'
 import { useSettingsStore } from '@/pinia'
 import envGroupColumns from '@/views/environments/group/columns'
@@ -40,10 +40,10 @@ function handleStatusChanged(event: { status: SiteStatus }) {
         </AFormItem>
         <AFormItem :label="$gettext('Node Group')">
           <StdSelector
-            v-model:selected-key="data.env_group_id"
-            :api="envGroup"
+            v-model:value="data.env_group_id"
+            :get-list-api="envGroup.getList"
             :columns="envGroupColumns"
-            record-value-index="name"
+            display-key="name"
             selection-type="radio"
           />
         </AFormItem>

+ 3 - 3
app/src/views/site/site_edit/components/SiteEditor/store.ts

@@ -59,7 +59,7 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
         await buildConfig()
       }
 
-      const response = await site.save(name.value, {
+      const response = await site.updateItem(name.value, {
         content: configText.value,
         overwrite: true,
         env_group_id: data.value.env_group_id,
@@ -81,7 +81,7 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
     console.error(e)
     parseErrorStatus.value = true
     parseErrorMessage.value = e.message
-    config.get(`sites-available/${name.value}`).then(r => {
+    config.getItem(`sites-available/${name.value}`).then(r => {
       configText.value = r.content
     })
   }
@@ -118,7 +118,7 @@ export const useSiteEditorStore = defineStore('siteEditor', () => {
         await buildConfig()
       }
       else {
-        let r = await site.get(name.value)
+        let r = await site.getItem(name.value)
         await handleResponse(r)
         r = await ngx.tokenize_config(configText.value)
         Object.assign(ngxConfig, {

+ 18 - 38
app/src/views/site/site_list/SiteList.vue

@@ -1,13 +1,10 @@
 <script setup lang="tsx">
 import type { EnvGroup } from '@/api/env_group'
-import type { Site } from '@/api/site'
-import type { Column } from '@/components/StdDesign/types'
-import { StdTable } from '@uozi-admin/curd'
+import { StdCurd } from '@uozi-admin/curd'
 import { message } from 'ant-design-vue'
 import env_group from '@/api/env_group'
 import site from '@/api/site'
 import EnvGroupTabs from '@/components/EnvGroupTabs'
-import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
 import { ConfigStatus } from '@/constants'
 import InspectConfig from '@/views/config/InspectConfig.vue'
 import columns from '@/views/site/site_list/columns'
@@ -16,11 +13,11 @@ import SiteDuplicate from '@/views/site/site_list/SiteDuplicate.vue'
 const route = useRoute()
 const router = useRouter()
 
-const table = ref()
+const curd = ref()
 const inspectConfig = ref()
 
 const envGroupId = ref(Number.parseInt(route.query.env_group_id as string) || 0)
-const envGroups = ref([]) as Ref<EnvGroup[]>
+const envGroups = ref<EnvGroup[]>([])
 
 watch(route, () => {
   inspectConfig.value?.test()
@@ -30,7 +27,7 @@ onMounted(async () => {
   let page = 1
   while (true) {
     try {
-      const { data, pagination } = await env_group.get_list({ page })
+      const { data, pagination } = await env_group.getList({ page })
       if (!data || !pagination)
         return
       envGroups.value.push(...data)
@@ -46,8 +43,8 @@ onMounted(async () => {
 })
 
 function destroy(site_name: string) {
-  site.destroy(site_name).then(() => {
-    table.value.getList()
+  site.deleteItem(site_name).then(() => {
+    curd.value.refresh()
     message.success($gettext('Delete site: %{site_name}', { site_name }))
     inspectConfig.value?.test()
   })
@@ -61,28 +58,14 @@ function handle_click_duplicate(name: string) {
   show_duplicator.value = true
   target.value = name
 }
-
-const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
-
-async function handleClickBatchEdit(batchColumns: Column[], selectedRowKeys: string[], selectedRows: Site[]) {
-  stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys, selectedRows)
-}
-
-function handleBatchUpdated() {
-  table.value?.get_list()
-  table.value?.resetSelection()
-}
 </script>
 
 <template>
-  <ACard :title="$gettext('Manage Sites')">
-    <InspectConfig ref="inspectConfig" />
-
-    <EnvGroupTabs v-model:active-key="envGroupId" :env-groups="envGroups" />
-
-    <StdTable
-      ref="table"
-      :get-list-api="site.getList"
+  <div>
+    <StdCurd
+      ref="curd"
+      :title="$gettext('Manage Sites')"
+      :api="site"
       :columns="columns"
       :table-props="{
         rowKey: 'name',
@@ -97,8 +80,11 @@ function handleBatchUpdated() {
       @edit-item="record => router.push({
         path: `/sites/${encodeURIComponent(record.name)}`,
       })"
-      @click-batch-modify="handleClickBatchEdit"
     >
+      <template #beforeCardBody>
+        <InspectConfig ref="inspectConfig" />
+        <EnvGroupTabs v-model:active-key="envGroupId" :env-groups="envGroups" />
+      </template>
       <template #afterActions="{ record }">
         <AButton
           type="link"
@@ -123,19 +109,13 @@ function handleBatchUpdated() {
           </AButton>
         </APopconfirm>
       </template>
-    </StdTable>
-    <StdBatchEdit
-      ref="stdBatchEditRef"
-      :api="site"
-      :columns
-      @save="handleBatchUpdated"
-    />
+    </StdCurd>
     <SiteDuplicate
       v-model:visible="show_duplicator"
       :name="target"
-      @duplicated="() => table.get_list()"
+      @duplicated="() => curd.refresh()"
     />
-  </ACard>
+  </div>
 </template>
 
 <style scoped>

+ 4 - 3
app/src/views/site/site_list/columns.tsx

@@ -2,7 +2,7 @@ import type {
   CustomRenderArgs,
   StdTableColumn,
 } from '@uozi-admin/curd'
-import type { SiteStatus } from '@/api/site'
+import type { Site, SiteStatus } from '@/api/site'
 import type { JSXElements } from '@/types'
 import { actualFieldRender, datetimeRender } from '@uozi-admin/curd'
 import { Tag } from 'ant-design-vue'
@@ -73,6 +73,7 @@ const columns: StdTableColumn[] = [{
       selectionType: 'radio',
     },
   },
+  batchEdit: true,
   sorter: true,
   pure: true,
   width: 100,
@@ -86,7 +87,7 @@ const columns: StdTableColumn[] = [{
 }, {
   title: () => $gettext('Status'),
   dataIndex: 'status',
-  customRender: (args: CustomRenderArgs) => {
+  customRender: (args: CustomRenderArgs<Site>) => {
     const { text, record } = args
     return h(SiteStatusSegmented, {
       'modelValue': text,
@@ -94,7 +95,7 @@ const columns: StdTableColumn[] = [{
       'enabled': record.status !== ConfigStatus.Disabled,
       'onUpdate:modelValue': (val: string) => {
         // This will be handled by the component internal events
-        record.status = val
+        record.status = val as SiteStatus
       },
       'onStatusChanged': ({ status }: { status: SiteStatus }) => {
         record.status = status

+ 63 - 142
app/src/views/stream/StreamList.vue

@@ -1,88 +1,50 @@
 <script setup lang="tsx">
-import type { CustomRenderArgs, StdTableColumn } from '@uozi-admin/curd'
 import type { EnvGroup } from '@/api/env_group'
-import type { Stream } from '@/api/stream'
-import type { JSXElements } from '@/types'
-import { actualFieldRender, datetimeRender, StdTable } from '@uozi-admin/curd'
-import { Badge, message } from 'ant-design-vue'
+import { StdCurd } from '@uozi-admin/curd'
+import { message } from 'ant-design-vue'
 import env_group from '@/api/env_group'
 import stream from '@/api/stream'
 import EnvGroupTabs from '@/components/EnvGroupTabs'
-import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
-import { ConfigStatus } from '@/constants'
 import InspectConfig from '@/views/config/InspectConfig.vue'
-import envGroupColumns from '@/views/environments/group/columns'
+import columns from '@/views/stream/columns'
 import StreamDuplicate from '@/views/stream/components/StreamDuplicate.vue'
 
-const columns: StdTableColumn[] = [{
-  title: () => $gettext('Name'),
-  dataIndex: 'name',
-  sorter: true,
-  pure: true,
-  edit: {
-    type: 'input',
-  },
-  search: true,
-  width: 150,
-}, {
-  title: () => $gettext('Node Group'),
-  dataIndex: 'env_group_id',
-  customRender: actualFieldRender('env_group.name'),
-  edit: {
-    type: 'selector',
-    selector: {
-      getListApi: env_group.getList,
-      columns: envGroupColumns,
-      valueKey: 'id',
-      displayKey: 'name',
-      selectionType: 'radio',
-    },
-  },
-  sorter: true,
-  pure: true,
-  width: 150,
-}, {
-  title: () => $gettext('Status'),
-  dataIndex: 'status',
-  customRender: (args: CustomRenderArgs) => {
-    const template: JSXElements = []
-    const { text } = args
-    if (text === ConfigStatus.Enabled) {
-      template.push(<Badge status="success" />)
-      template.push(h('span', $gettext('Enabled')))
-    }
-    else if (text === ConfigStatus.Disabled) {
-      template.push(<Badge status="warning" />)
-      template.push(h('span', $gettext('Disabled')))
-    }
+const route = useRoute()
+const router = useRouter()
 
-    return h('div', template)
-  },
-  sorter: true,
-  pure: true,
-  width: 200,
-}, {
-  title: () => $gettext('Updated at'),
-  dataIndex: 'modified_at',
-  customRender: datetimeRender,
-  sorter: true,
-  pure: true,
-  width: 200,
-}, {
-  title: () => $gettext('Actions'),
-  dataIndex: 'actions',
-  width: 250,
-  fixed: 'right',
-}]
+const curd = ref()
+const inspect_config = ref()
 
-const table = ref()
+const envGroupId = ref(Number.parseInt(route.query.env_group_id as string) || 0)
+const envGroups = ref<EnvGroup[]>([])
 
-const inspect_config = ref()
+onMounted(async () => {
+  let page = 1
+  while (true) {
+    try {
+      const { data, pagination } = await env_group.getList({ page })
+      if (!data || !pagination)
+        return
+      envGroups.value.push(...data)
+      if (data.length < pagination?.per_page) {
+        return
+      }
+      page++
+    }
+    catch {
+      return
+    }
+  }
+})
+
+watch(route, () => {
+  inspect_config.value?.test()
+})
 
 function enable(name: string) {
   stream.enable(name).then(() => {
     message.success($gettext('Enabled successfully'))
-    table.value?.get_list()
+    curd.value?.refresh()
     inspect_config.value?.test()
   }).catch(r => {
     message.error($gettext('Failed to enable %{msg}', { msg: r.message ?? '' }), 10)
@@ -92,7 +54,7 @@ function enable(name: string) {
 function disable(name: string) {
   stream.disable(name).then(() => {
     message.success($gettext('Disabled successfully'))
-    table.value?.get_list()
+    curd.value?.refresh()
     inspect_config.value?.test()
   }).catch(r => {
     message.error($gettext('Failed to disable %{msg}', { msg: r.message ?? '' }))
@@ -100,15 +62,14 @@ function disable(name: string) {
 }
 
 function destroy(stream_name: string) {
-  stream.destroy(stream_name).then(() => {
-    table.value.get_list()
+  stream.deleteItem(stream_name).then(() => {
+    curd.value.refresh()
     message.success($gettext('Delete stream: %{stream_name}', { stream_name }))
     inspect_config.value?.test()
   })
 }
 
 const showDuplicator = ref(false)
-
 const target = ref('')
 
 function handle_click_duplicate(name: string) {
@@ -116,91 +77,55 @@ function handle_click_duplicate(name: string) {
   target.value = name
 }
 
-const route = useRoute()
-
-const envGroupId = ref(Number.parseInt(route.query.env_group_id as string) || 0)
-const envGroups = ref([]) as Ref<EnvGroup[]>
-
-onMounted(async () => {
-  let page = 1
-  while (true) {
-    try {
-      const { data, pagination } = await env_group.getList({ page })
-      if (!data || !pagination)
-        return
-      envGroups.value.push(...data)
-      if (data.length < pagination?.per_page) {
-        return
-      }
-      page++
-    }
-    catch {
-      return
-    }
-  }
-})
-
-watch(route, () => {
-  inspect_config.value?.test()
-})
-
 const showAddStream = ref(false)
 const name = ref('')
+
 function add() {
   showAddStream.value = true
   name.value = ''
 }
 
 function handleAddStream() {
-  stream.save(name.value, { name: name.value, content: 'server\t{\n\n}' }).then(() => {
+  stream.updateItem(name.value, { name: name.value, content: 'server\t{\n\n}' }).then(() => {
     showAddStream.value = false
-    table.value?.get_list()
+    curd.value?.refresh()
     message.success($gettext('Added successfully'))
   })
 }
-
-const stdBatchEditRef = useTemplateRef('stdBatchEditRef')
-
-async function handleClickBatchEdit(batchColumns: StdTableColumn[], selectedRowKeys: string[], selectedRows: Stream[]) {
-  stdBatchEditRef.value?.showModal(batchColumns, selectedRowKeys, selectedRows)
-}
-
-function handleBatchUpdated() {
-  table.value?.get_list()
-  table.value?.resetSelection()
-}
 </script>
 
 <template>
-  <ACard :title="$gettext('Manage Streams')">
-    <template #extra>
-      <div class="flex items-center cursor-default">
-        <a class="mr-4" @click="add">{{ $gettext('Add') }}</a>
-      </div>
-    </template>
-
-    <InspectConfig ref="inspect_config" />
-
-    <EnvGroupTabs v-model:active-key="envGroupId" :env-groups="envGroups" />
-
-    <StdTable
-      ref="table"
-      :get-list-api="stream.getList"
+  <div>
+    <StdCurd
+      ref="curd"
+      :title="$gettext('Manage Streams')"
+      :api="stream"
       :columns="columns"
       :table-props="{
         rowKey: 'name',
       }"
       disable-delete
       disable-view
-      :scroll-x="800"
-      :get-params="{
+      row-selection-type="checkbox"
+      :custom-query-params="{
         env_group_id: envGroupId,
       }"
-      @edit-item="record => $router.push({
+      :scroll-x="800"
+      @edit-item="record => router.push({
         path: `/streams/${encodeURIComponent(record.name)}`,
       })"
-      @click-batch-modify="handleClickBatchEdit"
     >
+      <template #extra>
+        <div class="flex items-center cursor-default">
+          <a class="mr-4" @click="add">{{ $gettext('Add') }}</a>
+        </div>
+      </template>
+
+      <template #beforeCardBody>
+        <InspectConfig ref="inspect_config" />
+        <EnvGroupTabs v-model:active-key="envGroupId" :env-groups="envGroups" />
+      </template>
+
       <template #afterActions="{ record }">
         <AButton
           v-if="record.enabled"
@@ -241,7 +166,8 @@ function handleBatchUpdated() {
           </AButton>
         </APopconfirm>
       </template>
-    </StdTable>
+    </StdCurd>
+
     <AModal
       v-model:open="showAddStream"
       :title="$gettext('Add Stream')"
@@ -254,18 +180,13 @@ function handleBatchUpdated() {
         </AFormItem>
       </AForm>
     </AModal>
+
     <StreamDuplicate
       v-model:visible="showDuplicator"
       :name="target"
-      @duplicated="() => table.get_list()"
-    />
-    <StdBatchEdit
-      ref="stdBatchEditRef"
-      :api="stream"
-      :columns="columns"
-      @save="handleBatchUpdated"
+      @duplicated="() => curd.refresh()"
     />
-  </ACard>
+  </div>
 </template>
 
 <style scoped>

+ 70 - 0
app/src/views/stream/columns.tsx

@@ -0,0 +1,70 @@
+import type { CustomRenderArgs, StdTableColumn } from '@uozi-admin/curd'
+import type { JSXElements } from '@/types'
+import { actualFieldRender, datetimeRender } from '@uozi-admin/curd'
+import { Badge } from 'ant-design-vue'
+import env_group from '@/api/env_group'
+import { ConfigStatus } from '@/constants'
+import envGroupColumns from '@/views/environments/group/columns'
+
+const columns: StdTableColumn[] = [{
+  title: () => $gettext('Name'),
+  dataIndex: 'name',
+  sorter: true,
+  pure: true,
+  edit: {
+    type: 'input',
+  },
+  search: true,
+  width: 150,
+}, {
+  title: () => $gettext('Node Group'),
+  dataIndex: 'env_group_id',
+  customRender: actualFieldRender('env_group.name'),
+  edit: {
+    type: 'selector',
+    selector: {
+      getListApi: env_group.getList,
+      columns: envGroupColumns,
+      valueKey: 'id',
+      displayKey: 'name',
+      selectionType: 'radio',
+    },
+  },
+  sorter: true,
+  pure: true,
+  width: 150,
+}, {
+  title: () => $gettext('Status'),
+  dataIndex: 'status',
+  customRender: (args: CustomRenderArgs) => {
+    const template: JSXElements = []
+    const { text } = args
+    if (text === ConfigStatus.Enabled) {
+      template.push(<Badge status="success" />)
+      template.push(h('span', $gettext('Enabled')))
+    }
+    else if (text === ConfigStatus.Disabled) {
+      template.push(<Badge status="warning" />)
+      template.push(h('span', $gettext('Disabled')))
+    }
+
+    return h('div', template)
+  },
+  sorter: true,
+  pure: true,
+  width: 200,
+}, {
+  title: () => $gettext('Updated at'),
+  dataIndex: 'modified_at',
+  customRender: datetimeRender,
+  sorter: true,
+  pure: true,
+  width: 200,
+}, {
+  title: () => $gettext('Actions'),
+  dataIndex: 'actions',
+  width: 250,
+  fixed: 'right',
+}]
+
+export default columns

+ 5 - 5
app/src/views/stream/components/RightPanel/Basic.vue

@@ -1,12 +1,12 @@
 <script setup lang="ts">
 import type { CheckedType } from '@/types'
 import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { StdSelector } from '@uozi-admin/curd'
 import { message, Modal } from 'ant-design-vue'
 import { storeToRefs } from 'pinia'
 import envGroup from '@/api/env_group'
 import stream from '@/api/stream'
-import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
-import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelector.vue'
+import NodeSelector from '@/components/NodeSelector'
 import { formatDateTime } from '@/lib/helper'
 import { useSettingsStore } from '@/pinia'
 import envGroupColumns from '@/views/environments/group/columns'
@@ -76,10 +76,10 @@ function onChangeEnabled(checked: CheckedType) {
 
     <AFormItem :label="$gettext('Node Group')">
       <StdSelector
-        v-model:selected-key="data.env_group_id"
-        :api="envGroup"
+        v-model:value="data.env_group_id"
+        :get-list-api="envGroup.getList"
         :columns="envGroupColumns"
-        record-value-index="name"
+        display-key="name"
         selection-type="radio"
       />
     </AFormItem>

+ 1 - 1
app/src/views/stream/components/RightPanel/Chat.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
+import ChatGPT from '@/components/ChatGPT'
 import { useStreamEditorStore } from '../../store'
 
 const store = useStreamEditorStore()

+ 3 - 3
app/src/views/stream/store.ts

@@ -59,7 +59,7 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
         await buildConfig()
       }
 
-      const response = await stream.save(name.value, {
+      const response = await stream.updateItem(name.value, {
         content: configText.value,
         overwrite: true,
         env_group_id: data.value.env_group_id,
@@ -81,7 +81,7 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
     console.error(e)
     parseErrorStatus.value = true
     parseErrorMessage.value = e.message
-    config.get(`streams-available/${name.value}`).then(r => {
+    config.getItem(`streams-available/${name.value}`).then(r => {
       configText.value = r.content
     })
   }
@@ -116,7 +116,7 @@ export const useStreamEditorStore = defineStore('streamEditor', () => {
         await buildConfig()
       }
       else {
-        let r = await stream.get(name.value)
+        let r = await stream.getItem(name.value)
         await handleResponse(r)
         r = await ngx.tokenize_config(configText.value)
         Object.assign(ngxConfig, {

Some files were not shown because too many files changed in this diff