Compare commits

...

232 Commits

Author SHA1 Message Date
scarqin b02f47b8a4 default model 2025-02-12 10:22:41 +08:00
scarqin 9cb09905f9 feat: delete model 2025-02-12 00:33:16 +08:00
scarqin eeb2fbcad6 feat: api list 2025-02-11 19:15:11 +08:00
scarqin 400faf92c0 feat: online model settings 2025-02-11 19:01:15 +08:00
Dot.L 901bef1463 Merge pull request #189 from APIParkLab/feature/openapi
update workflows actions/download-artifact to v4
2025-02-08 16:15:32 +08:00
Liujian 8d44d796b4 update workflows actions/download-artifact to v4 2025-02-08 16:14:01 +08:00
Dot.L 5a10ad478e Merge pull request #188 from APIParkLab/feature/openapi
update workflows actions/upload-artifact to v4
2025-02-08 15:59:53 +08:00
Liujian fd6680d615 update workflows actions/upload-artifact to v4 2025-02-08 15:57:36 +08:00
Dot.L e03cdfc42b Merge pull request #187 from APIParkLab/feature/openapi
Feature/openapi
2025-02-08 15:02:06 +08:00
Liujian 945d53fcfd Merge remote-tracking branch 'github-pro/main' into feature/openapi 2025-02-08 15:01:27 +08:00
Liujian ac7045b724 Fix: AI provider's default key synchronization to Apinto failed issue 2025-02-08 14:58:36 +08:00
Liujian c907bdc4a5 Merge remote-tracking branch 'origin/main' into feature/openapi 2025-01-23 16:54:58 +08:00
ningyv 733ed9ac2f Merge pull request #183 from APIParkLab/feature/1.4
chroe: optimize AI model node graphics
2025-01-23 14:01:10 +08:00
ningyv 1d8e579a10 Merge remote-tracking branch 'origin/main' into feature/1.4 2025-01-23 13:57:01 +08:00
lichunxian 567cac9c95 Merge branch 'feature/1.4' into 'main'
chroe: optimize AI model node graphics

See merge request apipark/APIPark!161
2025-01-21 11:52:02 +08:00
ningyv 095c09c8c0 chroe: optimize AI model node graphics 2025-01-21 11:50:58 +08:00
刘健 e9c949822d Merge branch 'feature/openapi' into 'main'
Feature/openapi

See merge request apipark/APIPark!160
2025-01-20 17:36:53 +08:00
Dot.L 3482d5416c Merge pull request #181 from APIParkLab/feature/openapi
fix:ai init bug
2025-01-20 14:08:37 +08:00
Liujian d8cb4a0c94 fix:ai init bug 2025-01-20 14:03:03 +08:00
Dot.L 59acfa7a47 Merge pull request #180 from APIParkLab/feature/openapi
Feature/openapi
2025-01-20 13:55:59 +08:00
Liujian 2eb2e690d1 update ai bug 2025-01-20 13:54:58 +08:00
刘健 f7801261c3 Merge branch 'feature/openapi' into 'main'
fix: Nsq returns no error directly after parsing JSON exceptionNsq returns no...

See merge request apipark/APIPark!159
2025-01-17 16:03:51 +08:00
Liujian 7e7be7f040 add openapi 2025-01-17 16:03:09 +08:00
Dot.L 0187fd16b2 Merge pull request #174 from jeak01/patch-2
Update readme-zh-cn.md
2025-01-17 15:55:09 +08:00
Dot.L ba0bdb5e99 Merge pull request #175 from jeak01/patch-3
Update readme-zh-tw.md
2025-01-17 15:54:50 +08:00
Dot.L 9d3e4f07bf Merge pull request #176 from jeak01/patch-4
Update readme-jp.md
2025-01-17 15:54:37 +08:00
Dot.L bd81d7584d Merge pull request #177 from jeak01/patch-1
Update README.md
2025-01-17 15:54:20 +08:00
jeak 9577339e14 Update readme-jp.md 2025-01-17 14:59:10 +08:00
jeak 5c292ef1cb Update readme-zh-tw.md 2025-01-17 14:58:46 +08:00
jeak 4f3de85068 Update readme-zh-cn.md 2025-01-17 14:58:19 +08:00
jeak 07a25c9643 Update README.md 2025-01-17 14:57:31 +08:00
Dot.L 8f60426b4c Merge pull request #173 from APIParkLab/feature/ai-balance
fix: Nsq returns no error directly after parsing JSON exceptionNsq re…
2025-01-17 11:35:43 +08:00
Liujian 37f87615bd fix: Nsq returns no error directly after parsing JSON exceptionNsq returns no error directly after parsing JSON exception 2025-01-17 11:34:34 +08:00
Dot.L 3f96de660b Merge pull request #172 from APIParkLab/feature/ai-balance
fix: ai event handler read event error
2025-01-17 10:42:14 +08:00
Liujian e86999770f fix: ai event handler read event error 2025-01-17 10:38:35 +08:00
Dot.L a8bb0c24ec Merge pull request #170 from APIParkLab/feature/ai-balance
update init plugin config
2025-01-16 18:58:36 +08:00
Liujian 6ba2a08b62 update init plugin config 2025-01-16 18:53:58 +08:00
Dot.L d232269416 Merge pull request #167 from APIParkLab/feature/ai-balance
Feature/ai balance
2025-01-16 16:37:41 +08:00
Liujian 9d2208e14d update provider status default value 2025-01-16 16:36:25 +08:00
Liujian 8d69d45d1d update build script 2025-01-16 16:36:06 +08:00
刘健 b0c37918b5 Merge branch 'feature/merge' into 'main'
fix: login page redirect multiple times (#166)

See merge request apipark/APIPark!158
2025-01-16 14:57:39 +08:00
刘健 d5af1c8da3 Merge branch 'feature/ai-balance' into 'main'
Feature/1.4 (#154)

See merge request apipark/APIPark!157
2025-01-15 16:04:24 +08:00
ScarChin a6105cfc3c fix: 1.3-beta版本,超级管理员(admin)账户无法修改分类和添加子分类,页面显示无权限操作 (#164) 2025-01-14 17:52:07 +08:00
ScarChin 0aa5ffd2c2 fix: login page redirect multiple times (#166)
* fix: System Settings - General After changing the interface language, the internal pages do not automatically follow the language switch

* fix: login page language error
2025-01-13 18:33:55 +08:00
Dot.L 968f5b986f Merge pull request #165 from APIParkLab/feature/ai-balance
update docker run script
2025-01-13 11:25:50 +08:00
Liujian 014a7e0362 update docker run script 2025-01-13 11:24:29 +08:00
Dot.L a92baf09d9 Merge pull request #162 from APIParkLab/feature/ai-balance
Feature/ai balance
2025-01-08 11:26:18 +08:00
Liujian 46e2edbe13 Merge branch 'main-github-pro' into feature/ai-balance
# Conflicts:
#	frontend/packages/core/src/pages/aiApis/aiApisLayout.tsx
#	frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx
#	frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx
2025-01-08 11:25:55 +08:00
Liujian 5aba86965e Merge remote-tracking branch 'github-pro/main' into main-github-pro
# Conflicts:
#	frontend/packages/common/src/components/aoplatform/LanguageSetting.tsx
#	frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx
#	frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx
#	frontend/packages/common/src/components/aoplatform/prompt-editor/PromptEditorResizable.tsx
#	frontend/packages/common/src/const/permissions.ts
#	frontend/packages/common/src/contexts/GlobalStateContext.tsx
#	frontend/packages/common/src/hooks/pluginLoader.ts
#	frontend/packages/common/src/utils/systemRunning.ts
#	frontend/packages/core/src/components/AIProviderSelect/index.tsx
#	frontend/packages/core/src/const/const.tsx
#	frontend/packages/core/src/const/system/const.tsx
#	frontend/packages/core/src/pages/aiApis/index.tsx
#	frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx
#	frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx
#	frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx
#	frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx
#	frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx
#	frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx
#	frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx
#	frontend/packages/core/src/pages/aiSetting/constants.ts
#	frontend/packages/core/src/pages/aiSetting/styles.css
#	frontend/packages/core/src/pages/aiSetting/types.ts
#	frontend/packages/core/src/pages/keySettings/components/ApiKeyContent.tsx
#	frontend/packages/core/src/pages/keySettings/components/StatusFilter.tsx
#	frontend/packages/core/src/pages/keySettings/index.tsx
#	frontend/packages/dashboard/src/component/MonitorApiPage.tsx
2025-01-08 11:20:57 +08:00
Liujian 5924208aaa ignore config.yaml 2025-01-08 11:14:47 +08:00
Liujian 526390816b update package script 2025-01-08 11:11:41 +08:00
ScarChin d7e28c9704 Feature/1.4 (#154)
- Load balancing (can connect to multiple accounts, automatically switch accounts when there is no quota)
- AI call log
- Model rate configuration
2025-01-07 18:47:08 +08:00
秦圆圆 7c827804f4 Merge branch 'feature/1.4' into 'main'
fix: In the supplier load chart, the mouse should not show a hand shape except...

See merge request apipark/APIPark!156
2025-01-07 18:41:58 +08:00
scarqin b0dacbda0d fix: When the current supplier is abnormal, there should be a line on the model pointing to the next model, which means that the APIs on this link are associated with the next valid supplier. 2025-01-07 18:40:43 +08:00
scarqin d5abde2593 fix: The language option is wrong. The current language is Chinese, but the option is displayed as English. 2025-01-07 18:14:25 +08:00
scarqin bc3290de3b fix: jump link error 2025-01-07 17:56:16 +08:00
scarqin 7f438bf776 fix: When the current supplier is abnormal, there should be a line on the model pointing to the next model, which means that the APIs on this link are associated with the next valid supplier. 2025-01-07 17:54:53 +08:00
scarqin 13cfe24b2f fix: error line 2025-01-07 17:21:21 +08:00
Liujian 9cf1cd99c2 Merge remote-tracking branch 'github-pro/feature/1.4' into feature/ai-balance 2025-01-07 16:53:41 +08:00
刘健 f5cfd77550 Merge branch 'feature/ai-balance' into 'main'
fix: ai provider status error

See merge request apipark/APIPark!155
2025-01-07 16:50:27 +08:00
Liujian f27abbd454 fix: ai provider status error 2025-01-07 16:49:47 +08:00
scarqin 6a7a11a811 fix: In the supplier load chart, the mouse should not show a hand shape except for the cards that can be dragged and sorted. 2025-01-07 16:17:50 +08:00
刘健 4a8f5152b3 Merge branch 'feature/ai-balance' into 'main'
AI API token quantity docking completed

See merge request apipark/APIPark!154
2025-01-07 12:49:51 +08:00
Liujian 599ee6b9b8 AI API token quantity docking completed 2025-01-07 12:49:23 +08:00
秦圆圆 83ac747cb1 Merge branch 'feature/1.4' into 'main'
fix: After the release log configuration is successful, there is no success...

See merge request apipark/APIPark!153
2025-01-07 11:33:54 +08:00
scarqin 09b98c6c0d fix: Details has two slashes 2025-01-07 11:32:29 +08:00
scarqin 13eac21609 fix: After the release log configuration is successful, there is no success prompt and the list is not refreshed 2025-01-07 10:45:42 +08:00
scarqin d0d9e2a9a8 fix: system logs lack of edit permission 2025-01-07 10:32:03 +08:00
scarqin b047c93965 fix: i18n error 2025-01-07 10:04:18 +08:00
刘健 d5eedd1dd2 Merge branch 'feature/ai-balance' into 'main'
fix: ai key config error

See merge request apipark/APIPark!152
2025-01-07 09:23:34 +08:00
Liujian 9cc6696340 fix: ai key config error 2025-01-07 01:42:36 +08:00
秦圆圆 86758383c4 Merge branch 'feature/1.4' into 'main'
Feature/1.4

See merge request apipark/APIPark!151
2025-01-06 19:45:37 +08:00
scarqin 150a0264c5 fix: AI API should remove prefix matching drop-down selection 2025-01-06 19:29:15 +08:00
scarqin 558a2d8aad fix: There is no slide rail, so the bottom part cannot be displayed. 2025-01-06 19:14:09 +08:00
scarqin fa327114f7 fix: No refetching of lists after deleting keyword search 2025-01-06 17:53:30 +08:00
刘健 6ce3e0bfac Merge branch 'feature/ai-balance' into 'main'
update ai key status to gateway

See merge request apipark/APIPark!150
2025-01-06 10:57:54 +08:00
Liujian 4f7dee570a update ai key status to gateway 2025-01-06 10:57:23 +08:00
刘健 e4eadf863e Merge branch 'feature/ai-balance' into 'main'
fix: ai key sort

See merge request apipark/APIPark!149
2025-01-06 10:26:39 +08:00
Liujian 7a70a6ce01 fix: ai key sort 2025-01-06 10:22:01 +08:00
刘健 ca328e784c Merge branch 'feature/ai-balance' into 'main'
Feature/ai balance

See merge request apipark/APIPark!148
2025-01-06 10:02:19 +08:00
Liujian 9871e252bc ai balance finish 2025-01-06 09:47:23 +08:00
Liujian d40eb6c4e1 Merge remote-tracking branch 'origin/main' into feature/ai-balance 2025-01-05 23:25:08 +08:00
Liujian a7b0e6d0bf update ai key iml 2025-01-05 23:25:00 +08:00
lichunxian f4f546e654 Merge branch 'feature/1.4' into 'main'
feat: aiApi detail page

See merge request apipark/APIPark!147
2025-01-03 17:47:21 +08:00
ningyv 1fcbb3ecbc feat: aiApi detail page 2025-01-03 17:46:23 +08:00
ningyv 98e3cc973b Merge branch 'feature/1.4' of http://gitlab.eolink.com/apipark/APIPark into feature/1.4 2025-01-03 17:33:17 +08:00
ningyv 220ab53ef2 feat: aiApi detail page 2025-01-03 17:33:06 +08:00
秦圆圆 9ba70063d2 Merge branch 'feature/1.4' into 'main'
fix: The load diagram moves as a whole to the left so that the main content is...

See merge request apipark/APIPark!146
2025-01-03 17:14:05 +08:00
scarqin 076277d0a9 fix: KEY pool adjustment order will fail 2025-01-03 17:13:40 +08:00
scarqin bcd2ba1ec9 fix: The bottom margin of the banner should be sufficient 2025-01-03 16:37:36 +08:00
scarqin 5827afd09c fix: The load diagram moves as a whole to the left so that the main content is in the middle and the AI ​​Services icon is aligned to the left 2025-01-03 16:29:27 +08:00
秦圆圆 4a3e49f4e3 Merge branch 'feature/1.4' into 'main'
Feature/1.4

See merge request apipark/APIPark!144
2025-01-03 16:12:34 +08:00
Liujian 95d24aca41 Merge remote-tracking branch 'origin/feature/1.4' into feature/ai-balance 2025-01-03 11:25:59 +08:00
刘健 345e37bd81 Merge branch 'feature/ai-balance' into 'main'
update ai api list

See merge request apipark/APIPark!145
2025-01-03 10:47:10 +08:00
Liujian 91b2fabf10 update ai api list 2025-01-03 10:46:25 +08:00
scarqin bae1803157 i18n 2025-01-03 09:42:17 +08:00
刘健 911d16de31 Merge branch 'feature/ai-balance' into 'main'
update ai api list

See merge request apipark/APIPark!143
2025-01-03 09:33:46 +08:00
Liujian 1efe924221 update ai api list 2025-01-03 09:33:12 +08:00
scarqin 0230235427 feat: navigate 2025-01-03 09:24:32 +08:00
scarqin 58e737ee28 chore: apis 2025-01-03 09:09:40 +08:00
scarqin 78c98f121d fix: api key timestamp error 2025-01-03 09:06:36 +08:00
秦圆圆 2e3b86741b Merge branch 'feature/1.4' into 'main'
Feature/1.4

See merge request apipark/APIPark!138
2025-01-03 08:20:23 +08:00
scarqin fcd9869caa feat: api url 2025-01-02 10:07:54 +08:00
scarqin f60579b735 fix: padding error 2025-01-02 09:45:32 +08:00
scarqin 6574f36c73 fix: provider tips 2025-01-02 09:32:19 +08:00
scarqin 246ce245b9 fix: alert api list tips 2025-01-02 09:29:03 +08:00
scarqin fa0a211db9 feat: add banner 2025-01-02 08:21:33 +08:00
scarqin 1c536df3c8 feat: apilist 2024-12-31 17:21:56 +08:00
scarqin 82fa1b5b1c chore: change disable to disabled 2024-12-31 16:17:53 +08:00
刘健 dacce748a1 Merge branch 'feature/ai-balance' into 'main'
update api disable to disabled

See merge request apipark/APIPark!142
2024-12-31 15:24:55 +08:00
Liujian b59088c598 update api disable to disabled 2024-12-31 15:24:36 +08:00
scarqin 26dbce9dbf feat: ai column add disable 2024-12-31 15:17:36 +08:00
scarqin 9b6d07dc4c feat: apikey 2024-12-31 15:07:43 +08:00
刘健 00e21c8000 Merge branch 'feature/ai-balance' into 'main'
fix: provider status incorrect

See merge request apipark/APIPark!141
2024-12-31 14:50:41 +08:00
Liujian 8c5f5326d5 fix: provider status incorrect 2024-12-31 14:48:19 +08:00
scarqin 3ebafcbc03 feat: stopaable tips 2024-12-31 14:24:15 +08:00
scarqin 9777859f42 feat: add llm status manage 2024-12-31 12:01:42 +08:00
scarqin 54f76d6576 feat: add priority 2024-12-31 10:27:50 +08:00
刘健 3bb8293478 Merge branch 'feature/ai-balance' into 'main'
update go-common version

See merge request apipark/APIPark!140
2024-12-31 10:17:36 +08:00
Liujian 16e899cab7 update go-common version 2024-12-31 10:16:44 +08:00
scarqin b4b9469284 refacor: delete useless code 2024-12-30 17:03:36 +08:00
scarqin e0896864c2 feat: update list after edit modal 2024-12-30 17:00:09 +08:00
scarqin c7b16e0ea9 feat: ai model 2024-12-30 15:59:43 +08:00
scarqin f6d6920cfb feat: ai model detail 2024-12-30 14:59:22 +08:00
刘健 8b4059a249 Merge branch 'feature/ai-balance' into 'main'
update ai provider list key

See merge request apipark/APIPark!139
2024-12-30 13:50:18 +08:00
Liujian 94b19e7589 update ai provider list key 2024-12-30 13:49:59 +08:00
scarqin a090bb7caa feat: ai apis 2024-12-27 21:28:35 +08:00
刘健 101e97dfec Merge branch 'feature/ai-balance' into 'main'
Feature/ai balance

See merge request apipark/APIPark!137
2024-12-27 16:56:28 +08:00
Liujian 31054a2df7 update simple/providers/configured api 2024-12-27 16:55:54 +08:00
scarqin 58231eb19c feat: api tab 2024-12-27 09:56:15 +08:00
Liujian 8f8b5d2684 Merge branch 'main-github-pro' into feature/ai-balance 2024-12-26 18:22:30 +08:00
Liujian ca5e497dbb Merge remote-tracking branch 'origin/main' into main-github-pro 2024-12-26 18:21:30 +08:00
刘健 13aabcacd2 Merge branch 'feature/ai-balance' into 'main'
Add filtering options to the list

See merge request apipark/APIPark!136
2024-12-26 18:07:17 +08:00
Liujian 3afd0bb609 Add filtering options to the list 2024-12-26 18:06:54 +08:00
刘健 e250a8b57f Merge branch 'feature/ai-balance' into 'main'
Feature/ai balance

See merge request apipark/APIPark!135
2024-12-26 17:26:01 +08:00
Liujian 051fa7647d ai api list add filter condition 2024-12-26 17:25:36 +08:00
Liujian 9c4c794d0f Merge branch 'main-github-pro' into feature/ai-balance 2024-12-26 16:48:03 +08:00
Liujian 9211a28675 update service list api 2024-12-26 16:46:11 +08:00
scarqin 8d660ec7c0 feat: ai model config 2024-12-26 16:05:56 +08:00
秦圆圆 ed8109fc30 Merge branch 'feature/1.4' into 'main'
Feature/1.4

See merge request apipark/APIPark!134
2024-12-26 15:54:20 +08:00
scarqin 37fc63def8 feat: apikey 2024-12-26 15:52:43 +08:00
刘健 8c1a8f67d8 Merge branch 'feature/ai-balance' into 'main'
update simple configured providers

See merge request apipark/APIPark!133
2024-12-26 15:41:24 +08:00
Liujian 94ed7581bb update simple configured providers 2024-12-26 15:41:07 +08:00
scarqin 4a84a69fe7 feat: add api key 2024-12-26 15:39:52 +08:00
刘健 1ebf6c9319 Merge branch 'feature/ai-balance' into 'main'
Feature/ai balance

See merge request apipark/APIPark!132
2024-12-26 15:10:24 +08:00
Liujian d0fc353d0b finish ai provider api 2024-12-26 15:09:44 +08:00
scarqin 977919fdb1 feat: add key 2024-12-26 14:36:24 +08:00
scarqin ae2e37cedb feat: add modal 2024-12-26 14:20:06 +08:00
scarqin 02e5394924 feat: add api keys 2024-12-25 20:23:47 +08:00
scarqin cdc9bb73bb feat: support sort table 2024-12-25 17:53:15 +08:00
scarqin 3a2c0c744c feat: delete apikey 2024-12-25 17:36:26 +08:00
scarqin ffeb76f608 fix: provider change 2024-12-25 17:15:42 +08:00
scarqin 3748cb39b2 feat: sort api 2024-12-25 17:08:26 +08:00
scarqin 543ea52bb3 feat: pagination and search api 2024-12-25 16:51:41 +08:00
scarqin 2ad508ec60 feat: ai provider component 2024-12-25 15:41:20 +08:00
scarqin f9501d6f60 style: apikey layout 2024-12-25 15:05:19 +08:00
scarqin 6bbdf9600d feat: encapsulation select options 2024-12-25 14:43:29 +08:00
scarqin c554c010c5 style: remove keys card border 2024-12-25 14:12:43 +08:00
scarqin 075c976d19 fix: some type error 2024-12-25 14:08:22 +08:00
scarqin 6da49e78ee feat: remove attribute 2024-12-25 13:52:00 +08:00
scarqin 59ce2e0623 feat: add token 2024-12-25 12:01:19 +08:00
Dot.L 09b2a7f1a4 Merge pull request #159 from APIParkLab/feature/ai-balance
add get simple ai provider api
2024-12-25 12:01:03 +08:00
Liujian bd466ac420 add get simple ai provider api 2024-12-25 11:59:34 +08:00
scarqin 96183eb5df feat: apikey get 2024-12-25 10:38:28 +08:00
scarqin 21164859cf feat: template modal 2024-12-25 10:14:07 +08:00
Dot.L 9b50fe68c9 Merge pull request #158 from APIParkLab/feature/ai-balance
Feature/ai balance
2024-12-24 18:23:14 +08:00
刘健 d8576e4dc6 Merge branch 'feature/ai-balance' into 'main'
update go.mod

See merge request apipark/APIPark!131
2024-12-24 18:20:44 +08:00
Liujian 9fc23ad4be update go.mod 2024-12-24 18:20:09 +08:00
scarqin 6c997c0b51 feat: apikeys 2024-12-24 18:14:50 +08:00
刘健 9b10421882 Merge branch 'feature/ai-balance' into 'main'
finish ai apis

See merge request apipark/APIPark!130
2024-12-24 18:01:41 +08:00
Liujian e5bc98cac0 finish ai apis 2024-12-24 18:00:46 +08:00
scarqin e98f320f41 feat: add route apikey and ai api 2024-12-24 17:29:31 +08:00
scarqin d0ef35fb92 feat: flow chart 2024-12-24 16:36:23 +08:00
scarqin 8c512f3163 feat: use translateExtent to limit scroll area 2024-12-24 15:41:14 +08:00
scarqin 8cc0fc9987 disable dragger 2024-12-24 15:36:55 +08:00
scarqin 604200e1db feat: flow chart 2024-12-24 15:31:02 +08:00
scarqin 2a1581acdb feat: remove fit view 2024-12-24 15:14:52 +08:00
scarqin 39e3198821 feat: custom edge 2024-12-24 15:06:48 +08:00
scarqin b4c2b3614b feat: keycard change place 2024-12-24 14:37:45 +08:00
scarqin 5ea0cc7838 fet 2024-12-24 14:34:16 +08:00
scarqin a9b7fc525a feat: model 2024-12-24 14:28:10 +08:00
scarqin b05874fb0a feat: fetch data from api 2024-12-24 13:51:08 +08:00
scarqin 5fdd142a17 feat: change component place 2024-12-24 11:58:51 +08:00
scarqin 243e1da716 feat: dmo 2024-12-24 11:35:31 +08:00
scarqin 772217258c refactor: encapsulation constant 2024-12-24 10:37:10 +08:00
scarqin 57cc9c9db7 feat: draggable button 2024-12-23 18:36:12 +08:00
scarqin e6f6560f3a feat: draggable 2024-12-23 18:17:51 +08:00
scarqin cbce30b4d7 feat: draggable 2024-12-23 18:16:17 +08:00
scarqin 23c8a84b4c feat: add multiple data 2024-12-23 17:56:22 +08:00
scarqin a521bff1f1 chore: 文件拆分 2024-12-23 17:45:50 +08:00
scarqin 986784b128 feat: change type status 2024-12-23 17:42:55 +08:00
scarqin 238c2b8cd3 feat: change group title 2024-12-23 17:31:58 +08:00
scarqin c6a418e00c chore: card status 2024-12-23 17:14:23 +08:00
scarqin e59ea1f84d feat: add handler 2024-12-23 16:11:12 +08:00
Dot.L a1acde5df0 Merge pull request #156 from APIParkLab/feature/ai-balance
update go-common dependence in go.mod
2024-12-23 16:05:41 +08:00
刘健 3ec61165ca Merge branch 'feature/ai-balance' into 'main'
update go-common dependence in go.mod

See merge request apipark/APIPark!129
2024-12-23 15:58:43 +08:00
Liujian cc823a18d4 update go-common dependence in go.mod 2024-12-23 15:58:24 +08:00
刘健 b6aa865a67 Merge branch 'feature/ai-balance' into 'main'
Feature/ai balance

See merge request apipark/APIPark!128
2024-12-23 15:51:09 +08:00
Dot.L 86f83d995b Merge pull request #155 from APIParkLab/feature/ai-balance
finish ai key api
2024-12-23 15:49:27 +08:00
Liujian e314e09fdb finish ai key api 2024-12-23 15:48:19 +08:00
scarqin 4aa4238943 feat: block 2024-12-23 14:09:53 +08:00
scarqin 5d638b8bf4 feat: basic card component 2024-12-23 14:00:24 +08:00
scarqin 5027d817c5 chore: add global rule 2024-12-23 11:36:25 +08:00
scarqin 40b92330eb chore: add global_rules.md 2024-12-23 10:16:59 +08:00
scarqin b990447226 feat: chart 2024-12-20 20:26:18 +08:00
scarqin 482019f514 chore: refactor format eslint and prettier code 2024-12-19 16:17:50 +08:00
scarqin 2027af3a3f chore: add remove unused 2024-12-19 15:49:20 +08:00
scarqin c1a55385d3 change 2024-12-18 17:37:20 +08:00
scarqin 8be47c99dd test 2024-12-18 17:33:39 +08:00
scarqin 10d11f5b99 test 2024-12-18 17:33:08 +08:00
scarqin 3db2c66f12 test 2024-12-18 17:32:23 +08:00
scarqin e8614c0072 test 2024-12-18 17:31:03 +08:00
scarqin 11b4c43845 test 2024-12-18 17:30:16 +08:00
scarqin 36fec4ddbe test 2024-12-18 17:28:17 +08:00
scarqin bb64039a0d test 2024-12-18 17:27:49 +08:00
scarqin 7b43b0b300 test 2024-12-18 17:25:54 +08:00
scarqin fd270c80f5 test 2024-12-18 17:24:02 +08:00
scarqin 12b5801b1b test 2024-12-18 17:20:47 +08:00
scarqin 23a6b38a7d test 2024-12-18 17:19:10 +08:00
scarqin 296118470c test 2024-12-18 17:13:29 +08:00
scarqin 6d4dccc6b7 feat: push 2024-12-18 17:11:43 +08:00
scarqin e449f86c01 fix: login page refresh multiple times 2024-12-18 15:48:51 +08:00
Dot.L a1bdc048a7 Merge pull request #149 from APIParkLab/feature/aibug-fix
add default router when create new rest service
2024-12-16 14:43:26 +08:00
刘健 b5be78416f Update .gitlab-ci.yml file 2024-12-16 14:34:33 +08:00
刘健 89d1fe2c49 Merge branch 'feature/aibug-fix' into 'main'
add default router when create new rest service

See merge request apipark/APIPark!127
2024-12-16 14:33:14 +08:00
Liujian 0a1b08157d add default router when create new rest service 2024-12-16 14:31:52 +08:00
刘健 61503f4146 Merge branch 'feature/aibug-fix' into 'main'
chore: update index.tsx

See merge request apipark/APIPark!126
2024-12-13 20:06:43 +08:00
ningyv d36c66371f Merge pull request #147 from APIParkLab/fix/fixbug-cx
fix: consumer permission
2024-12-13 20:02:45 +08:00
lichunxian 34b259562d Merge branch 'fix/fixbug-cx' into 'main'
fix: consumer permission

See merge request apipark/APIPark!125
2024-12-13 20:01:59 +08:00
ningyv b7bb409e96 fix: consumer permission 2024-12-13 20:00:38 +08:00
Dot.L 517007c941 Merge pull request #146 from APIParkLab/feature/aibug-fix
fix aksk bug
2024-12-13 19:41:10 +08:00
Liujian 4c685a9ec6 fix aksk bug 2024-12-13 18:52:21 +08:00
383 changed files with 32291 additions and 23899 deletions
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
echo "Build frontend..." echo "Build frontend..."
cd ./frontend && pnpm run build cd ./frontend && pnpm run build
- name: upload frontend release - name: upload frontend release
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: frontend-package name: frontend-package
path: frontend/dist path: frontend/dist
@@ -41,7 +41,7 @@ jobs:
- name: Checkout #Checkout代码 - name: Checkout #Checkout代码
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: download frontend release - name: download frontend release
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: frontend-package name: frontend-package
path: frontend/dist path: frontend/dist
@@ -71,7 +71,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: download frontend release - name: download frontend release
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
name: frontend-package name: frontend-package
path: frontend/dist path: frontend/dist
+1 -1
View File
@@ -3,4 +3,4 @@
/config.yml /config.yml
/build/ /build/
/apipark /apipark
.gitlab-ci.yml .gitlab-ci.yml
+98
View File
@@ -0,0 +1,98 @@
variables:
PATH: /opt/go-1.21/go/bin/:/opt/node/node/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
GOROOT: /opt/go-1.21/go
GOPROXY: https://goproxy.cn
VERSION: $CI_COMMIT_SHORT_SHA
APP: apipark
APP_PRE: ${APP}_${VERSION}
BUILD_DIR: ${APP}-build
DEPLOY_DESC: "DEV 环境"
VIEW_ADDR: http://172.18.166.219:8288
SAVE_DIR: /opt/${APP}
NODE_OPTIONS: --max_old_space_size=8192
stages:
- notice
- prefix
- build
- deploy
- webhook
feishu-informer: # 飞书回调
stage: notice
variables:
DIFF_URL: "$CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID/diffs"
rules:
- if: $CI_PIPELINE_SOURCE=="merge_request_event" && $CI_COMMIT_BRANCH =~ "main"
script:
- echo "merge request"
- |
curl -X POST -H "Content-Type: application/json" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"项目:${CI_PROJECT_NAME}\\n提交人:${GITLAB_USER_NAME}\\n提交信息:${CI_MERGE_REQUEST_TITLE}\\n合并分支信息:${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} -> ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}\\n差异性地址:${DIFF_URL}\\n请及时review代码\"}}" \
https://open.feishu.cn/open-apis/bot/v2/hook/1c334752-2874-41a1-8f1b-3060f2d46b6c
prebuild:
stage: prefix
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- echo "prebuild"
- chmod +x ./scripts/prefix.sh
- ./scripts/prefix.sh
builder:
stage: build
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- set -e
- |
if [ ! -d "../artifacts" ]; then
mkdir -p ../artifacts
fi
if [ -d "../artifacts/dist" ]; then
cp -r ../artifacts/dist frontend/dist
fi
- |
if [ -n "$(git diff --name-status HEAD~1 HEAD -- frontend)" ]; then
./scripts/build.sh $BUILD_DIR ${VERSION} all ""
else
./scripts/build.sh $BUILD_DIR ${VERSION}
fi
if [ -d "frontend/dist" ]; then
echo "copy frontend/dist to artifacts/dist"
rm -fr ../artifacts/dist
cp -r frontend/dist ../artifacts/dist
fi
cp $BUILD_DIR/${APP_PRE}_linux_amd64.tar.gz ${SAVE_DIR}
deployer:
stage: deploy
rules:
- if: $CI_COMMIT_BRANCH == "main"
variables:
APIPARK_GUEST_MODE: allow
APIPARK_GUEST_ID: dklejrfbhjqwdh
script:
- cd ${SAVE_DIR};mkdir -p ${APP_PRE};tar -zxvf ${APP_PRE}_linux_amd64.tar.gz -C ${APP_PRE};cd ${APP_PRE};./install.sh ${SAVE_DIR};./run.sh restart;cd ${SAVE_DIR} && ./clean.sh ${APP_PRE}
when: on_success
success:
stage: webhook
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- |
curl -X POST -H "Content-Type: application/json" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"最近一次提交:${CI_COMMIT_TITLE}\\n提交人:${GITLAB_USER_NAME}\\n项目:${CI_PROJECT_NAME}\\n环境:${DEPLOY_DESC}\\n更新部署完成.\\n访问地址:${VIEW_ADDR}\\n工作流地址:${CI_PIPELINE_URL}\"}}" \
https://open.feishu.cn/open-apis/bot/v2/hook/c3672932-4dfa-4989-8023-0128bae59338
when: on_success
failure:
stage: webhook
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- |
curl -X POST -H "Content-Type: application/json" \
-d "{\"msg_type\":\"text\",\"content\":{\"text\":\"最近一次提交:${CI_COMMIT_TITLE}\\n提交人:${GITLAB_USER_NAME}\\n项目:${CI_PROJECT_NAME}\\n环境:${DEPLOY_DESC}\\n更新部署失败,请及时到gitlab上查看\\n工作流地址:${CI_PIPELINE_URL}\"}}" \
https://open.feishu.cn/open-apis/bot/v2/hook/c3672932-4dfa-4989-8023-0128bae59338
when: on_failure
-9
View File
@@ -1,9 +0,0 @@
{
"cSpell.words": [
"Antd",
"apinto",
"Apipark",
"logsettings",
"resourcesettings"
]
}
+1 -1
View File
@@ -210,7 +210,7 @@ APIPark uses the Apache 2.0 License. For more details, please refer to the LICEN
For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing. For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing.
- Website: https://apipark.com - Website: https://apipark.com
- Email: dev@apipark.com - Email: contact@apipark.com
<br> <br>
+7
View File
@@ -40,7 +40,14 @@ func (c *Config) Check(cfg string) error {
} }
func (c *Config) GenConfig(target string, origin string) (string, error) { func (c *Config) GenConfig(target string, origin string) (string, error) {
if target == "" {
target = "{}"
}
if origin == "" {
origin = "{}"
}
var targetData map[string]interface{} var targetData map[string]interface{}
err := json.Unmarshal([]byte(target), &targetData) err := json.Unmarshal([]byte(target), &targetData)
if err != nil { if err != nil {
return "", err return "", err
+1
View File
@@ -0,0 +1 @@
/config.yml
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/eolinker/go-common/autowire"
nsq "github.com/nsqio/go-nsq"
"github.com/eolinker/go-common/cftool"
_ "github.com/eolinker/go-common/store/store_mysql"
_ "github.com/go-sql-driver/mysql"
)
var (
version string
confPath string
)
func init() {
flag.StringVar(&confPath, "c", "config.yml", "`config` file path for server ")
}
type ServerConfig struct {
Port int `yaml:"port"`
}
func main() {
// 1. 连接 MySQL 数据库
cftool.Register[ServerConfig](fmt.Sprintf("root:%s", confPath))
cftool.ReadFile(confPath)
handler := &NSQHandler{}
autowire.Autowired(handler)
err := autowire.CheckComplete()
if err != nil {
log.Fatal("check autowired:", err)
return
}
// 2. 创建 NSQ 消费者
config := nsq.NewConfig()
hostname, err := os.Hostname()
if err != nil {
log.Fatalf("Failed to get hostname: %v", err)
return
}
nsqConfig := handler.nsqConfig
consumer, err := nsq.NewConsumer(fmt.Sprintf("%s_ai_event", nsqConfig.TopicPrefix), hostname, config)
if err != nil {
log.Fatalf("Failed to create NSQ consumer: %v", err)
}
consumer.AddHandler(handler)
// 4. 连接到 NSQ
//nsqAddress := "172.18.166.219:9150" // NSQ 地址
err = consumer.ConnectToNSQD(nsqConfig.Addr)
if err != nil {
log.Fatalf("Failed to connect to NSQ: %v", err)
}
log.Println("Connected to NSQ")
// 5. 捕获系统信号,优雅关闭
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
// 优雅停止消费者
consumer.Stop()
<-consumer.StopChan
log.Println("NSQ Consumer stopped")
}
+167
View File
@@ -0,0 +1,167 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"time"
"github.com/eolinker/go-common/cftool"
ai_dto "github.com/APIParkLab/APIPark/module/ai/dto"
"github.com/eolinker/go-common/store"
"github.com/APIParkLab/APIPark/service/ai"
ai_key "github.com/APIParkLab/APIPark/service/ai-key"
nsq "github.com/nsqio/go-nsq"
ai_api "github.com/APIParkLab/APIPark/service/ai-api"
)
func init() {
cftool.Register[NSQConfig]("nsq")
}
type NSQConfig struct {
Addr string `json:"addr" yaml:"addr"`
TopicPrefix string `json:"topic_prefix" yaml:"topic_prefix"`
}
// 定义 NSQ 消息结构
type AIProviderStatus struct {
Provider string `json:"provider"`
Model string `json:"model"`
Key string `json:"key"`
Status string `json:"status"`
}
type AIInfo struct {
Model string `json:"ai_model"`
Cost interface{} `json:"ai_model_cost"`
InputToken interface{} `json:"ai_model_input_token"`
OutputToken interface{} `json:"ai_model_output_token"`
TotalToken interface{} `json:"ai_model_total_token"`
Provider string `json:"ai_provider"`
ProviderStats []AIProviderStatus `json:"ai_provider_statuses"`
}
type NSQMessage struct {
AI AIInfo `json:"ai"`
API string `json:"api"`
Provider string `json:"provider"`
RequestID string `json:"request_id"`
TimeISO8601 string `json:"time_iso8601"`
}
// NSQHandler 处理 NSQ 消息并写入 MySQL
type NSQHandler struct {
apiUseService ai_api.IAPIUseService `autowired:""`
aiKeyService ai_key.IKeyService `autowired:""`
aiService ai.IProviderService `autowired:""`
transaction store.ITransaction `autowired:""`
nsqConfig *NSQConfig `autowired:""`
ctx context.Context
}
func convertInt(value interface{}) int {
switch v := value.(type) {
case int:
return v
case float64:
return int(v)
default:
return 0
}
}
func genAIKey(key string, provider string) string {
keys := strings.Split(key, "@")
return strings.TrimSuffix(keys[0], fmt.Sprintf("-%s", provider))
}
// HandleMessage 处理从 NSQ 读取的消息
func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
log.Printf("Received message: %s", string(message.Body))
// 解析消息为结构体
var data NSQMessage
err := json.Unmarshal(message.Body, &data)
if err != nil {
log.Printf("Failed to unmarshal message: %v", err)
return nil
}
// 将时间字符串转换为 time.Time
timestamp, err := time.Parse(time.RFC3339, data.TimeISO8601)
if err != nil {
log.Printf("Failed to parse timestamp: %v", err)
return nil
}
day := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, timestamp.Location())
hour := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), 0, 0, 0, timestamp.Location())
minute := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), timestamp.Minute(), 0, 0, timestamp.Location())
return h.transaction.Transaction(context.Background(), func(ctx context.Context) error {
finalStatus := &AIProviderStatus{}
for _, s := range data.AI.ProviderStats {
status := ToKeyStatus(s.Status).Int()
key := genAIKey(s.Key, s.Provider)
err = h.aiKeyService.Save(ctx, key, &ai_key.Edit{
Status: &status,
})
if err != nil {
log.Printf("Failed to save AI key: %v", err)
return nil
}
if s.Provider != data.AI.Provider {
pStatus := ai_dto.ProviderAbnormal.Int()
err = h.aiService.Save(ctx, s.Provider, &ai.SetProvider{
Status: &pStatus,
})
} else {
pStatus := ai_dto.ProviderEnabled.Int()
err = h.aiService.Save(ctx, s.Provider, &ai.SetProvider{
Status: &pStatus,
})
}
finalStatus = &s
}
if finalStatus != nil {
//keys := strings.Split(finalStatus.Key, "@")
key := genAIKey(finalStatus.Key, finalStatus.Provider)
err = h.aiKeyService.IncrUseToken(ctx, key, convertInt(data.AI.TotalToken))
if err != nil {
log.Printf("Failed to increment AI key token: %v", err)
return nil
}
}
// 调用 AI API 接口
err = h.apiUseService.Incr(context.Background(), &ai_api.IncrAPIUse{
API: data.API,
Service: data.Provider,
Provider: data.AI.Provider,
Model: data.AI.Model,
Day: day.Unix(),
Hour: hour.Unix(),
Minute: minute.Unix(),
InputToken: convertInt(data.AI.InputToken),
OutputToken: convertInt(data.AI.OutputToken),
TotalToken: convertInt(data.AI.TotalToken),
})
if err != nil {
log.Printf("Failed to call AI API: %v", err)
return nil
}
log.Printf("Message processed and saved to MySQL: %+v", data)
return nil
})
}
+34
View File
@@ -0,0 +1,34 @@
package main
import ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto"
var (
StatusNormal = "normal"
StatusInvalidRequest = "invalid request"
StatusQuotaExhausted = "quota exhausted"
StatusExpired = "expired"
StatusExceeded = "exceeded"
StatusInvalid = "invalid"
StatusTimeout = "timeout"
)
func ToKeyStatus(status string) ai_key_dto.KeyStatus {
switch status {
case StatusNormal:
return ai_key_dto.KeyNormal
case StatusInvalidRequest:
return ai_key_dto.KeyNormal
case StatusQuotaExhausted:
return ai_key_dto.KeyExceed
case StatusExpired:
return ai_key_dto.KeyExpired
case StatusExceeded:
return ai_key_dto.KeyNormal
case StatusInvalid:
return ai_key_dto.KeyError
case StatusTimeout:
return ai_key_dto.KeyError
default:
return ai_key_dto.KeyNormal
}
}
+7 -8
View File
@@ -2,7 +2,6 @@ package ai_api
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"github.com/APIParkLab/APIPark/model/plugin_model" "github.com/APIParkLab/APIPark/model/plugin_model"
@@ -52,7 +51,7 @@ func (i *imlAPIController) Create(ctx *gin.Context, serviceId string, input *ai_
plugins["ai_formatter"] = api.PluginSetting{ plugins["ai_formatter"] = api.PluginSetting{
Config: plugin_model.ConfigType{ Config: plugin_model.ConfigType{
"model": input.AiModel.Id, "model": input.AiModel.Id,
"provider": fmt.Sprintf("%s@ai-provider", input.AiModel.Provider), "provider": input.AiModel.Provider,
"config": input.AiModel.Config, "config": input.AiModel.Config,
}, },
} }
@@ -73,8 +72,8 @@ func (i *imlAPIController) Create(ctx *gin.Context, serviceId string, input *ai_
Retry: input.Retry, Retry: input.Retry,
Plugins: plugins, Plugins: plugins,
}, },
Upstream: input.AiModel.Provider, //Upstream: input.AiModel.Provider,
Disable: false, Disable: false,
}) })
return err return err
@@ -101,16 +100,16 @@ func (i *imlAPIController) Edit(ctx *gin.Context, serviceId string, apiId string
Retry: apiInfo.Proxy.Retry, Retry: apiInfo.Proxy.Retry,
Plugins: apiInfo.Proxy.Plugins, Plugins: apiInfo.Proxy.Plugins,
} }
var upstream *string //var upstream *string
if input.AiModel != nil { if input.AiModel != nil {
proxy.Plugins["ai_formatter"] = api.PluginSetting{ proxy.Plugins["ai_formatter"] = api.PluginSetting{
Config: plugin_model.ConfigType{ Config: plugin_model.ConfigType{
"model": input.AiModel.Id, "model": input.AiModel.Id,
"provider": fmt.Sprintf("%s@ai-provider", input.AiModel.Provider), "provider": input.AiModel.Provider,
"config": input.AiModel.Config, "config": input.AiModel.Config,
}, },
} }
upstream = &input.AiModel.Provider //upstream = &input.AiModel.Provider
} }
if input.AiPrompt != nil { if input.AiPrompt != nil {
@@ -128,7 +127,7 @@ func (i *imlAPIController) Edit(ctx *gin.Context, serviceId string, apiId string
Path: input.Path, Path: input.Path,
Disable: input.Disable, Disable: input.Disable,
Methods: &apiInfo.Methods, Methods: &apiInfo.Methods,
Upstream: upstream, //Upstream: upstream,
}) })
if err != nil { if err != nil {
return err return err
+26
View File
@@ -0,0 +1,26 @@
package ai_key
import (
"reflect"
ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto"
"github.com/eolinker/go-common/autowire"
"github.com/gin-gonic/gin"
)
type IKeyController interface {
Create(ctx *gin.Context, providerId string, input *ai_key_dto.Create) error
Edit(ctx *gin.Context, providerId string, id string, input *ai_key_dto.Edit) error
Delete(ctx *gin.Context, providerId string, id string) error
Get(ctx *gin.Context, providerId string, id string) (*ai_key_dto.Key, error)
List(ctx *gin.Context, providerId string, keyword string, page string, pageSize string, statuses string) ([]*ai_key_dto.Item, int64, error)
Enable(ctx *gin.Context, providerId string, id string) error
Disable(ctx *gin.Context, providerId string, id string) error
Sort(ctx *gin.Context, providerId string, input *ai_key_dto.Sort) error
}
func init() {
autowire.Auto[IKeyController](func() reflect.Value {
return reflect.ValueOf(new(imlAIKeyController))
})
}
+69
View File
@@ -0,0 +1,69 @@
package ai_key
import (
"encoding/json"
"strconv"
ai_key "github.com/APIParkLab/APIPark/module/ai-key"
ai_key_dto "github.com/APIParkLab/APIPark/module/ai-key/dto"
"github.com/gin-gonic/gin"
)
var _ IKeyController = &imlAIKeyController{}
type imlAIKeyController struct {
module ai_key.IKeyModule `autowired:""`
}
func (i *imlAIKeyController) Enable(ctx *gin.Context, providerId string, id string) error {
return i.module.UpdateKeyStatus(ctx, providerId, id, true)
}
func (i *imlAIKeyController) Disable(ctx *gin.Context, providerId string, id string) error {
return i.module.UpdateKeyStatus(ctx, providerId, id, false)
}
func (i *imlAIKeyController) Create(ctx *gin.Context, providerId string, input *ai_key_dto.Create) error {
return i.module.Create(ctx, providerId, input)
}
func (i *imlAIKeyController) Edit(ctx *gin.Context, providerId string, id string, input *ai_key_dto.Edit) error {
return i.module.Edit(ctx, providerId, id, input)
}
func (i *imlAIKeyController) Delete(ctx *gin.Context, providerId string, id string) error {
return i.module.Delete(ctx, providerId, id)
}
func (i *imlAIKeyController) Get(ctx *gin.Context, providerId string, id string) (*ai_key_dto.Key, error) {
return i.module.Get(ctx, providerId, id)
}
func (i *imlAIKeyController) List(ctx *gin.Context, providerId string, keyword string, page string, pageSize string, statuses string) ([]*ai_key_dto.Item, int64, error) {
p, err := strconv.Atoi(page)
if err != nil {
if page != "" {
return nil, 0, err
}
p = 1
}
ps, err := strconv.Atoi(pageSize)
if err != nil {
if pageSize != "" {
return nil, 0, err
}
ps = 20
}
ss := make([]string, 0)
if statuses != "" {
err = json.Unmarshal([]byte(statuses), &ss)
if err != nil {
return nil, 0, err
}
}
return i.module.List(ctx, providerId, keyword, p, ps, ss)
}
func (i *imlAIKeyController) Sort(ctx *gin.Context, providerId string, input *ai_key_dto.Sort) error {
return i.module.Sort(ctx, providerId, input)
}
+14 -2
View File
@@ -1,25 +1,37 @@
package ai package ai
import ( import (
"reflect"
ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto"
"github.com/eolinker/go-common/autowire" "github.com/eolinker/go-common/autowire"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"reflect"
) )
type IProviderController interface { type IProviderController interface {
Providers(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) ConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ConfiguredProviderItem, *ai_dto.BackupProvider, error)
UnConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ProviderItem, error)
SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error)
SimpleConfiguredProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, *ai_dto.BackupProvider, error)
Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error)
SimpleProvider(ctx *gin.Context, id string) (*ai_dto.SimpleProvider, error)
LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error)
Enable(ctx *gin.Context, id string) error Enable(ctx *gin.Context, id string) error
Disable(ctx *gin.Context, id string) error Disable(ctx *gin.Context, id string) error
UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error
UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error
Sort(ctx *gin.Context, input *ai_dto.Sort) error
}
type IStatisticController interface {
APIs(ctx *gin.Context, keyword string, providerId string, start string, end string, page string, pageSize string, sortCondition string, asc string, models string, services string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error)
} }
func init() { func init() {
autowire.Auto[IProviderController](func() reflect.Value { autowire.Auto[IProviderController](func() reflect.Value {
return reflect.ValueOf(&imlProviderController{}) return reflect.ValueOf(&imlProviderController{})
}) })
autowire.Auto[IStatisticController](func() reflect.Value {
return reflect.ValueOf(&imlStatisticController{})
})
} }
+72 -5
View File
@@ -1,6 +1,9 @@
package ai package ai
import ( import (
"encoding/json"
"strconv"
"github.com/APIParkLab/APIPark/module/ai" "github.com/APIParkLab/APIPark/module/ai"
ai_dto "github.com/APIParkLab/APIPark/module/ai/dto" ai_dto "github.com/APIParkLab/APIPark/module/ai/dto"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -14,28 +17,46 @@ type imlProviderController struct {
module ai.IProviderModule `autowired:""` module ai.IProviderModule `autowired:""`
} }
func (i *imlProviderController) Sort(ctx *gin.Context, input *ai_dto.Sort) error {
return i.module.Sort(ctx, input)
}
func (i *imlProviderController) ConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ConfiguredProviderItem, *ai_dto.BackupProvider, error) {
return i.module.ConfiguredProviders(ctx)
}
func (i *imlProviderController) UnConfiguredProviders(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) {
return i.module.UnConfiguredProviders(ctx)
}
func (i *imlProviderController) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) { func (i *imlProviderController) SimpleProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, error) {
return i.module.SimpleProviders(ctx) return i.module.SimpleProviders(ctx)
} }
func (i *imlProviderController) Providers(ctx *gin.Context) ([]*ai_dto.ProviderItem, error) { func (i *imlProviderController) SimpleConfiguredProviders(ctx *gin.Context) ([]*ai_dto.SimpleProviderItem, *ai_dto.BackupProvider, error) {
return i.module.Providers(ctx) return i.module.SimpleConfiguredProviders(ctx)
} }
func (i *imlProviderController) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) { func (i *imlProviderController) Provider(ctx *gin.Context, id string) (*ai_dto.Provider, error) {
return i.module.Provider(ctx, id) return i.module.Provider(ctx, id)
} }
func (i *imlProviderController) SimpleProvider(ctx *gin.Context, id string) (*ai_dto.SimpleProvider, error) {
return i.module.SimpleProvider(ctx, id)
}
func (i *imlProviderController) LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) { func (i *imlProviderController) LLMs(ctx *gin.Context, driver string) ([]*ai_dto.LLMItem, *ai_dto.ProviderItem, error) {
return i.module.LLMs(ctx, driver) return i.module.LLMs(ctx, driver)
} }
func (i *imlProviderController) Enable(ctx *gin.Context, id string) error { func (i *imlProviderController) Enable(ctx *gin.Context, id string) error {
return i.module.UpdateProviderStatus(ctx, id, true) //return i.module.UpdateProviderStatus(ctx, id, true)
return nil
} }
func (i *imlProviderController) Disable(ctx *gin.Context, id string) error { func (i *imlProviderController) Disable(ctx *gin.Context, id string) error {
return i.module.UpdateProviderStatus(ctx, id, false) //return i.module.UpdateProviderStatus(ctx, id, false)
return nil
} }
func (i *imlProviderController) UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error { func (i *imlProviderController) UpdateProviderConfig(ctx *gin.Context, id string, input *ai_dto.UpdateConfig) error {
@@ -43,5 +64,51 @@ func (i *imlProviderController) UpdateProviderConfig(ctx *gin.Context, id string
} }
func (i *imlProviderController) UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error { func (i *imlProviderController) UpdateProviderDefaultLLM(ctx *gin.Context, id string, input *ai_dto.UpdateLLM) error {
return i.module.UpdateProviderDefaultLLM(ctx, id, input) //return i.module.UpdateProviderDefaultLLM(ctx, id, input)
return nil
}
var _ IStatisticController = (*imlStatisticController)(nil)
type imlStatisticController struct {
module ai.IAIAPIModule `autowired:""`
}
func (i *imlStatisticController) APIs(ctx *gin.Context, keyword string, providerId string, start string, end string, page string, pageSize string, sortCondition string, asc string, models string, services string) ([]*ai_dto.APIItem, *ai_dto.Condition, int64, error) {
s, err := strconv.ParseInt(start, 10, 64)
if err != nil {
return nil, nil, 0, err
}
e, err := strconv.ParseInt(end, 10, 64)
if err != nil {
return nil, nil, 0, err
}
p, err := strconv.Atoi(page)
if err != nil {
if page != "" {
return nil, nil, 0, err
}
p = 1
}
ps, err := strconv.Atoi(pageSize)
if err != nil {
if pageSize != "" {
return nil, nil, 0, err
}
ps = 20
}
ms := make([]string, 0)
if models != "" {
json.Unmarshal([]byte(models), &ms)
ms = append(ms, models)
}
ss := make([]string, 0)
if services != "" {
json.Unmarshal([]byte(services), &ss)
ss = append(ss, services)
}
return i.module.APIs(ctx, keyword, providerId, s, e, p, ps, sortCondition, asc == "true", ms, ss)
} }
+29 -3
View File
@@ -318,15 +318,41 @@ func (i *imlServiceController) Get(ctx *gin.Context, id string) (*service_dto.Se
return i.module.Get(ctx, id) return i.module.Get(ctx, id)
} }
func (i *imlServiceController) Search(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) { func (i *imlServiceController) Search(ctx *gin.Context, teamIDs string, keyword string) ([]*service_dto.ServiceItem, error) {
return i.module.Search(ctx, teamID, keyword) return i.module.Search(ctx, teamIDs, keyword)
} }
func (i *imlServiceController) Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) { func (i *imlServiceController) Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) {
if input.Kind == "ai" { if input.Kind == "ai" {
return i.createAIService(ctx, teamID, input) return i.createAIService(ctx, teamID, input)
} }
return i.module.Create(ctx, teamID, input) var err error
var info *service_dto.Service
err = i.transaction.Transaction(ctx, func(txCtx context.Context) error {
info, err = i.module.Create(txCtx, teamID, input)
if err != nil {
return err
}
path := fmt.Sprintf("/%s/", strings.Trim(input.Prefix, "/"))
_, err = i.routerModule.Create(txCtx, info.Id, &router_dto.Create{
Id: uuid.New().String(),
Name: "",
Path: path + "*",
Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions},
Description: "auto create by create service",
Protocols: []string{"http", "https"},
MatchRules: nil,
Upstream: "",
Proxy: &router_dto.InputProxy{
Path: path,
Timeout: 30000,
Retry: 0,
},
Disable: false,
})
return err
})
return info, err
} }
func (i *imlServiceController) Edit(ctx *gin.Context, id string, input *service_dto.EditService) (*service_dto.Service, error) { func (i *imlServiceController) Edit(ctx *gin.Context, id string, input *service_dto.EditService) (*service_dto.Service, error) {
+1 -1
View File
@@ -15,7 +15,7 @@ type IServiceController interface {
Get(ctx *gin.Context, id string) (*service_dto.Service, error) Get(ctx *gin.Context, id string) (*service_dto.Service, error)
// SearchMyServices 搜索服务 // SearchMyServices 搜索服务
SearchMyServices(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) SearchMyServices(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error)
Search(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) Search(ctx *gin.Context, teamIDs string, keyword string) ([]*service_dto.ServiceItem, error)
// Create 创建 // Create 创建
Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error)
// Edit 编辑 // Edit 编辑
+25 -4
View File
@@ -265,7 +265,8 @@ func (i *imlInitController) OnInit() {
return fmt.Errorf("create default team error: %v", err) return fmt.Errorf("create default team error: %v", err)
} }
// 创建Rest服务 // 创建Rest服务
_, err = i.serviceModule.Create(ctx, info.Id, &service_dto.CreateService{ restPath := "/rest-demo"
serviceInfo, err := i.serviceModule.Create(ctx, info.Id, &service_dto.CreateService{
Name: "REST Demo Service", Name: "REST Demo Service",
Prefix: "/rest-demo", Prefix: "/rest-demo",
Description: "Auto created By APIPark", Description: "Auto created By APIPark",
@@ -277,6 +278,26 @@ func (i *imlInitController) OnInit() {
if err != nil { if err != nil {
return fmt.Errorf("create default service error: %v", err) return fmt.Errorf("create default service error: %v", err)
} }
path := fmt.Sprintf("/%s/", strings.Trim(restPath, "/"))
_, err = i.routerModule.Create(ctx, serviceInfo.Id, &router_dto.Create{
Id: uuid.NewString(),
Name: "",
Path: path + "*",
Methods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions},
Description: "auto create by create service",
Protocols: []string{"http", "https"},
MatchRules: nil,
Upstream: "",
Proxy: &router_dto.InputProxy{
Path: path,
Timeout: 30000,
Retry: 0,
},
Disable: false,
})
if err != nil {
return fmt.Errorf("create default router error: %v", err)
}
// 创建AI服务 // 创建AI服务
err = i.createAIService(ctx, info.Id, &service_dto.CreateService{ err = i.createAIService(ctx, info.Id, &service_dto.CreateService{
Name: "AI Demo Service", Name: "AI Demo Service",
@@ -416,7 +437,7 @@ func (i *imlInitController) createAIService(ctx context.Context, teamID string,
plugins["ai_formatter"] = api.PluginSetting{ plugins["ai_formatter"] = api.PluginSetting{
Config: plugin_model.ConfigType{ Config: plugin_model.ConfigType{
"model": aiModel.Id, "model": aiModel.Id,
"provider": fmt.Sprintf("%s@ai-provider", info.Provider.Id), "provider": info.Provider.Id,
"config": aiModel.Config, "config": aiModel.Config,
}, },
} }
@@ -436,8 +457,8 @@ func (i *imlInitController) createAIService(ctx context.Context, teamID string,
Retry: retry, Retry: retry,
Plugins: plugins, Plugins: plugins,
}, },
Disable: false, Disable: false,
Upstream: info.Provider.Id, //Upstream: info.Provider.Id,
}) })
if err != nil { if err != nil {
return err return err
+7
View File
@@ -0,0 +1,7 @@
node_modules
dist
build
coverage
.next
*.d.ts
*.js
+39
View File
@@ -0,0 +1,39 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["react", "@typescript-eslint", "prettier", "unused-imports"],
"rules": {
"react/react-in-jsx-scope": "off",
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"printWidth": 120,
"useTabs": false,
"tabWidth": 2,
"semi": false,
"bracketSpacing": true,
"arrowParens": "always",
"trailingComma": "none",
"singleQuote": true,
"bracketLine": true
}
+45
View File
@@ -0,0 +1,45 @@
Create detailed components with these requirements:
1. Use 'use client' directive for client-side components
2. Style with Tailwind CSS utility classes for responsive design
3. Use React Router for navigation
4. Use Ant Design for UI components
5. Use iconify React for icons (from @iconify/react package). Do NOT use other UI libraries unless requested
6. Use local photos from public folder where appropriate, only valid URLs you know exist
7. Create root layout.tsx page that wraps necessary navigation items to all pages
8. MUST implement the navigation elements items in their rightful place i.e. Left sidebar, Top header
9. Accurately implement necessary grid layouts
10. Follow proper import practices:
- Use @/ path aliases
- Keep component imports organized
- Update current packages/core/src/pages/Root.tsx with new comprehensive code
- Don't forget root route (page.tsx) handling
- You MUST complete the entire prompt before stopping
11. Table component should use `import PageList from "@common/components/aoplatform/PageList.tsx"`
12. PageList component MUST use addNewBtnTitle for add button, NOT toolBarRender. Example:
<PageList
id="global_team"
className="pl-btnbase"
ref={pageListRef}
columns = {[...columns]}
request = {()=>getTeamList()}
showPagination={false}
addNewBtnTitle={$t('添加团队')}
addNewBtnAccess = "system.organization.team.add"
searchPlaceholder={$t("输入名称、ID、负责人查找团队")}
onAddNewBtnClick={()=>{openModal('add')}}
onSearchWordChange={(e)=>{setSearchWord(e.target.value)}}
onRowClick={(row:TeamTableListItem)=>(navigate(`../inside/${row.id}/setting`))}
/>
13. use `const { fetchData } = useFetch()` to fetch http data,such as
```tsx
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setUserInfo(data.profile)
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
```
14. can't not import new package!
+14 -18
View File
@@ -6,7 +6,7 @@ const systemLanguage = {
en_US: 'en-US', en_US: 'en-US',
zh_CN: 'zh-CN', zh_CN: 'zh-CN',
ja_JP: 'ja-JP', ja_JP: 'ja-JP',
zh_TW: 'zh-TW' zh_TW: 'zh-TW',
}; };
const localesDir = 'packages/common/src/locales/scan'; const localesDir = 'packages/common/src/locales/scan';
const newJsonDir = 'packages/common/src/locales/scan/newJson'; const newJsonDir = 'packages/common/src/locales/scan/newJson';
@@ -19,7 +19,7 @@ fs.readdirSync(localesDir).forEach(file => {
const lang = path.basename(file, '.json'); const lang = path.basename(file, '.json');
const filePath = path.join(localesDir, file); const filePath = path.join(localesDir, file);
try { try {
console.log('Current working directory:', process.cwd(),filePath); console.log('Current working directory:', process.cwd(), filePath);
const existJsonData = fs.readFileSync(filePath); const existJsonData = fs.readFileSync(filePath);
existData[lang] = JSON.parse(existJsonData); existData[lang] = JSON.parse(existJsonData);
} catch (error) { } catch (error) {
@@ -36,20 +36,18 @@ fs.readdirSync(localesDir).forEach(file => {
const keyList = Object.keys(existData); const keyList = Object.keys(existData);
// 清空 newJson 目录下的所有语言文件 // 清空 newJson 目录下的所有语言文件
Object.values(systemLanguage).forEach(lng => { Object.values(systemLanguage).forEach(lng => {
const newJsonPath = path.join(newJsonDir, `${lng}.json`); const newJsonPath = path.join(newJsonDir, `${lng}.json`);
fs.writeFileSync(newJsonPath, JSON.stringify({})); // 清空文件 fs.writeFileSync(newJsonPath, JSON.stringify({})); // 清空文件
}); });
module.exports = { module.exports = {
input: [ input: [
'packages/*/src/**/*.{js,jsx,tsx,ts}', 'packages/*/src/**/*.{js,jsx,tsx,ts}',
// 不需要扫描的文件加! // 不需要扫描的文件加!
'!packages/*/src/locales/**', '!packages/*/src/locales/**',
'!**/node_modules/**' '!**/node_modules/**',
], ],
output: 'packages/common/src/locales/scan', // 输出目录 output: 'packages/common/src/locales/scan', // 输出目录
options: { options: {
@@ -62,15 +60,15 @@ module.exports = {
loadPath: './newJson/{{lng}}.json', // 输入路径 (手动新建目录) loadPath: './newJson/{{lng}}.json', // 输入路径 (手动新建目录)
savePath: './newJson/{{lng}}.json', // 输出路径 (输出会根据输入路径内容自增, 不会覆盖已有的key) savePath: './newJson/{{lng}}.json', // 输出路径 (输出会根据输入路径内容自增, 不会覆盖已有的key)
jsonIndent: 2, jsonIndent: 2,
lineEnding: '\n' lineEnding: '\n',
}, },
removeUnusedKeys: true, removeUnusedKeys: true,
nsSeparator: false, // namespace separator nsSeparator: false, // namespace separator
keySeparator: false, // key separator keySeparator: false, // key separator
interpolation: { interpolation: {
prefix: '{{', prefix: '{{',
suffix: '}}' suffix: '}}',
} },
}, },
// 这里我们要实现将中文转换成crc格式, 通过crc格式key作为索引, 最终实现语言包的切换. // 这里我们要实现将中文转换成crc格式, 通过crc格式key作为索引, 最终实现语言包的切换.
transform: function (file, enc, done) { transform: function (file, enc, done) {
@@ -80,11 +78,10 @@ module.exports = {
parser.parseFuncFromString(content, { list: ['t'] }, (key, options) => { parser.parseFuncFromString(content, { list: ['t'] }, (key, options) => {
options.defaultValue = key; options.defaultValue = key;
const hashKey = `K${crc32(key).toString(16)}`; // crc32转换格式 const hashKey = `K${crc32(key).toString(16)}`; // crc32转换格式
keyHashMap[key] = hashKey; keyHashMap[key] = hashKey;
// 遍历每种语言,逐个语言检查翻译是否存在 // 遍历每种语言,逐个语言检查翻译是否存在
keyList.forEach((lng) => { keyList.forEach(lng => {
const langData = existData[lng] || {}; const langData = existData[lng] || {};
// 如果某语言没有翻译该字段,则记录到该语言的 newJson 文件中 // 如果某语言没有翻译该字段,则记录到该语言的 newJson 文件中
@@ -116,13 +113,12 @@ module.exports = {
}); });
done(); done();
}, },
flush: function(done) { flush: function (done) {
// 将 keyHashMap 写入文件 // 将 keyHashMap 写入文件
fs.writeFileSync(keyHashFile, JSON.stringify(keyHashMap, null, 2)); fs.writeFileSync(keyHashFile, JSON.stringify(keyHashMap, null, 2));
// 遍历每种语言,处理旧字段 // 遍历每种语言,处理旧字段
keyList.forEach((lng) => { keyList.forEach(lng => {
const localeFilePath = path.join(localesDir, `${lng}.json`); const localeFilePath = path.join(localesDir, `${lng}.json`);
const oldJsonPath = path.join(oldJsonDir, `${lng}.json`); const oldJsonPath = path.join(oldJsonDir, `${lng}.json`);
const langData = existData[lng] || {}; const langData = existData[lng] || {};
@@ -132,7 +128,7 @@ module.exports = {
// 将不存在于 keyHashMap 中的键移动到 oldJson 文件中 // 将不存在于 keyHashMap 中的键移动到 oldJson 文件中
Object.keys(langData).forEach(hashKey => { Object.keys(langData).forEach(hashKey => {
if (!Object.values(keyHashMap).includes(hashKey)) { if (!Object.values(keyHashMap).includes(hashKey)) {
oldJsonData[hashKey] = langData[hashKey]; // 将旧的 key 移到 oldJson 中 oldJsonData[hashKey] = langData[hashKey]; // 将旧的 key 移到 oldJson 中
} }
}); });
@@ -142,5 +138,5 @@ module.exports = {
} }
}); });
done(); done();
} },
}; };
+8 -1
View File
@@ -13,7 +13,9 @@
"serve:remotes": "lerna run serve --scope=remote --parallel", "serve:remotes": "lerna run serve --scope=remote --parallel",
"dev": "lerna run dev --scope=core --stream", "dev": "lerna run dev --scope=core --stream",
"stop": "kill-port --port 5000", "stop": "kill-port --port 5000",
"scan": "i18next-scanner --config i18next-scanner.config.js" "scan": "i18next-scanner --config i18next-scanner.config.js",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix && prettier --write ."
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -65,8 +67,12 @@
"antd": "^5.19.4", "antd": "^5.19.4",
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"eslint": "^8.53.0", "eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-react": "7.37.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4", "eslint-plugin-react-refresh": "^0.4.4",
"eslint-plugin-unused-imports": "^4.1.4",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next-scanner": "^4.5.0", "i18next-scanner": "^4.5.0",
"jest": "^29.7.0", "jest": "^29.7.0",
@@ -78,6 +84,7 @@
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"postcss-nested": "^6.0.1", "postcss-nested": "^6.0.1",
"prettier": "^3.1.1",
"react-test-renderer": "^18.3.1", "react-test-renderer": "^18.3.1",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
@@ -1,39 +1,37 @@
import React from "react" import React from 'react'
import SwaggerUI from 'swagger-ui-react'; import SwaggerUI from 'swagger-ui-react'
import 'swagger-ui-react/swagger-ui.css'; import 'swagger-ui-react/swagger-ui.css'
export default function ApiDocument({spec}:{spec?:string|object}) { export default function ApiDocument({ spec }: { spec?: string | object }) {
class OperationsLayout extends React.Component {
class OperationsLayout extends React.Component { render() {
render() { const { getComponent } = this.props
const { const Operations = getComponent('operations', true)
getComponent
} = this.props return (
const Operations = getComponent("operations", true) <div className="swagger-ui">
<Operations />
return ( </div>
<div className="swagger-ui"> )
<Operations />
</div>
)
}
}
// Create the plugin that provides our layout component
const OperationsLayoutPlugin = () => {
return {
components: {
OperationsLayout: OperationsLayout
}
}
} }
}
return(
<SwaggerUI // Create the plugin that provides our layout component
spec={spec} const OperationsLayoutPlugin = () => {
supportedSubmitMethods={[]} return {
customComponents={{Header:()=>null}} components: {
layout="OperationsLayout" OperationsLayout: OperationsLayout
plugins={[OperationsLayoutPlugin ]} /> }
) }
} }
return (
<SwaggerUI
spec={spec}
supportedSubmitMethods={[]}
customComponents={{ Header: () => null }}
layout="OperationsLayout"
plugins={[OperationsLayoutPlugin]}
/>
)
}
@@ -1,29 +1,22 @@
import { import { ProConfigProvider, ProLayout } from '@ant-design/pro-components'
MenuProps,
App,
Button,
ConfigProvider,
Dropdown
} from 'antd';
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import Logo from '@common/assets/layout-logo.png';
import AvatarPic from '@common/assets/default-avatar.png' import AvatarPic from '@common/assets/default-avatar.png'
import { useCallback, useEffect, useMemo, useState} from "react"; import Logo from '@common/assets/layout-logo.png'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'; import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const.tsx'
import { PERMISSION_DEFINITION } from '@common/const/permissions.ts'; import { PERMISSION_DEFINITION } from '@common/const/permissions.ts'
import { BasicResponse, RESPONSE_TIPS, routerKeyMap, STATUS_CODE } from '@common/const/const.tsx'; import { UserInfoType } from '@common/const/type.ts'
import { UserInfoType } from '@common/const/type.ts'; import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import { useFetch } from '@common/hooks/http.ts'; import { usePluginSlotHub } from '@common/contexts/PluginSlotHubContext'
import { ProjectFilled } from '@ant-design/icons'; import { useFetch } from '@common/hooks/http.ts'
import { getNavItem, transformMenuData } from '@common/utils/navigation'; import { $t } from '@common/locales'
import { Icon } from '@iconify/react'; import { transformMenuData } from '@common/utils/navigation'
import { $t } from '@common/locales'; import { Icon } from '@iconify/react'
import { ProConfigProvider, ProLayout } from '@ant-design/pro-components'; import { App, Button, ConfigProvider, Dropdown, MenuProps } from 'antd'
import LanguageSetting from './LanguageSetting'; import { useEffect, useMemo, useState } from 'react'
import { usePluginSlotHub } from '@common/contexts/PluginSlotHubContext'; import { Outlet, useLocation, useNavigate } from 'react-router-dom'
import LanguageSetting from './LanguageSetting'
const APP_MODE = import.meta.env.VITE_APP_MODE; const APP_MODE = import.meta.env.VITE_APP_MODE
export type MenuItem = Required<MenuProps>['items'][number]; export type MenuItem = Required<MenuProps>['items'][number]
const themeToken = { const themeToken = {
bgLayout: '#17163E;', bgLayout: '#17163E;',
@@ -32,92 +25,99 @@ const themeToken = {
}, },
pageContainer: { pageContainer: {
paddingBlockPageContainerContent: 0, paddingBlockPageContainerContent: 0,
paddingInlinePageContainerContent: 0, paddingInlinePageContainerContent: 0
} }
} }
function BasicLayout({ project = 'core' }: { project: string }) {
const navigator = useNavigate()
const location = useLocation()
const currentUrl = location.pathname
const { state, accessData, checkPermission, accessInit, dispatch, resetAccess, getGlobalAccessData, menuList } =
useGlobalContext()
const [pathname, setPathname] = useState(currentUrl)
const mainPage = project === 'core' ? '/service/list' : '/serviceHub/list'
const [menuItems, setMenuItems] = useState<MenuProps['items']>()
const pluginSlotHub = usePluginSlotHub()
function BasicLayout({project = 'core'}:{project:string}){ useEffect(() => {
const navigator = useNavigate()
const location = useLocation()
const currentUrl = location.pathname
const { state,accessData,checkPermission,accessInit,dispatch,resetAccess,getGlobalAccessData, menuList} = useGlobalContext()
const [pathname, setPathname] = useState(currentUrl);
const mainPage = project === 'core' ?'/service/list':'/serviceHub/list'
const [menuItems, setMenuItems] = useState<MenuProps['items']>();
const pluginSlotHub = usePluginSlotHub()
useEffect(()=>{
const newMenu = transformMenuData(menuList) const newMenu = transformMenuData(menuList)
setMenuItems(newMenu); setMenuItems(newMenu)
},[menuList, state.language,accessInit]) }, [menuList, state.language, accessInit])
useEffect(() => { useEffect(() => {
if (currentUrl === '/') { if (currentUrl === '/') {
navigator(mainPage) navigator(mainPage)
} }
}, [currentUrl])
}, [currentUrl]);
const headerMenuData = useMemo(() => { const headerMenuData = useMemo(() => {
// 判断权限 // 判断权限
const hasAccess = (access: unknown) => checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]); const hasAccess = (access: unknown) => checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
// 过滤菜单项 // 过滤菜单项
const filterMenu = (menu: Array<{ [k: string]: unknown }>) => { const filterMenu = (menu: Array<{ [k: string]: unknown }>) => {
return [...menu] return [...menu]
.filter(x => x) // 过滤掉空数据 .filter((x) => x) // 过滤掉空数据
.map((item: any) => { .map((item: any) => {
if (item.routes && item.routes.length > 0) { if (item.routes && item.routes.length > 0) {
// 递归处理子菜单 // 递归处理子菜单
const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes); const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes)
if (filteredRoutes.length === 0) { if (filteredRoutes.length === 0) {
return false return false
} }
return { ...item,routes: filteredRoutes,name:$t(item.name) }; return { ...item, routes: filteredRoutes, name: $t(item.name) }
} }
// 处理没有 routes 的菜单项 // 处理没有 routes 的菜单项
if (item.access) { if (item.access) {
return (item.access === 'all' || hasAccess(item.access)) ? {...item,name:$t(item.name)} : null; return item.access === 'all' || hasAccess(item.access) ? { ...item, name: $t(item.name) } : null
} }
// 如果没有 access 和 routes,则保留 // 如果没有 access 和 routes,则保留
return {...item,name:$t(item.name) }; return { ...item, name: $t(item.name) }
}) })
.filter(x => x); // 过滤掉处理后为 null 的项 .filter((x) => x) // 过滤掉处理后为 null 的项
}; }
// 初始过滤操作 // 初始过滤操作
const res = [...(menuItems || [])]!.filter(x => x).map((x: any) => (x.routes ? { ...x,name:$t(x.name), routes: filterMenu(x.routes) } : {...x,name:$t(x.name)})); const res = [...(menuItems || [])]!
// 返回处理后的数据 .filter((x) => x)
return { path: '/', routes: res.map(x=> ({...x, routes: x.routes?.filter(x=> (x.access || x.routes?.length > 0))})).filter(x=> (x.access || x.routes?.length > 0)) }; .map((x: any) =>
}, [accessData, state.language,menuItems]); x.routes ? { ...x, name: $t(x.name), routes: filterMenu(x.routes) } : { ...x, name: $t(x.name) }
)
const { message } = App.useApp() // 返回处理后的数据
const [userInfo,setUserInfo] = useState<UserInfoType>() return {
const {fetchData} = useFetch() path: '/',
const navigate = useNavigate(); routes: res
.map((x) => ({ ...x, routes: x.routes?.filter((x) => x.access || x.routes?.length > 0) }))
.filter((x) => x.access || x.routes?.length > 0)
}
}, [accessData, state.language, menuItems])
const { message } = App.useApp()
const [userInfo, setUserInfo] = useState<UserInfoType>()
const { fetchData } = useFetch()
const navigate = useNavigate()
const getUserInfo = () => { const getUserInfo = () => {
fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }) fetchData<BasicResponse<{ profile: UserInfoType }>>('account/profile', { method: 'GET' }).then((response) => {
.then(response => { const { code, data, msg } = response
const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) {
if (code === STATUS_CODE.SUCCESS) { setUserInfo(data.profile)
setUserInfo(data.profile) dispatch({ type: 'UPDATE_USERDATA', userData: data.profile })
dispatch({ type: 'UPDATE_USERDATA', userData: data.profile }) } else {
} else { message.error(msg || $t(RESPONSE_TIPS.error))
message.error(msg || $t(RESPONSE_TIPS.error)) }
} })
})
} }
useEffect(() => { useEffect(() => {
getUserInfo() getUserInfo()
getGlobalAccessData() getGlobalAccessData()
}, []); }, [])
const logOut = () => { const logOut = () => {
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then(response => { fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then((response) => {
const { code, msg } = response const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) { if (code === STATUS_CODE.SUCCESS) {
dispatch({ type: 'LOGOUT' }) dispatch({ type: 'LOGOUT' })
@@ -130,133 +130,162 @@ const themeToken = {
}) })
} }
const items: MenuProps['items'] = useMemo(() => [ const items: MenuProps['items'] = useMemo(
userInfo?.type !== 'guest' && { () =>
key: '2', [
label: ( userInfo?.type !== 'guest' && {
<Button key="changePsw" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={() => navigator('/userProfile/changepsw')}> key: '2',
{$t('账号设置')} label: (
</Button>) <Button
}, key="changePsw"
{ type="text"
key: '3', className="flex items-center p-0 bg-transparent border-none"
label: ( onClick={() => navigator('/userProfile/changepsw')}
<Button key="logout" type="text" className="flex items-center p-0 bg-transparent border-none " onClick={logOut}> >
{$t('退出登录')} {$t('账号设置')}
</Button>) </Button>
}, )
].filter(Boolean), [userInfo]); },
{
key: '3',
label: (
<Button
key="logout"
type="text"
className="flex items-center p-0 bg-transparent border-none"
onClick={logOut}
>
{$t('退出登录')}
</Button>
)
}
].filter(Boolean),
[userInfo]
)
const actionRender = useMemo(() => {
return [
<LanguageSetting />,
<Button
className=" text-[#ffffffb3] hover:text-[#fff] border-none"
type="default"
ghost
onClick={() => {
window.open('https://docs.apipark.com', '_blank')
}}
>
<span className="flex items-center gap-[8px]">
{' '}
<Icon icon="ic:baseline-help" width="14" height="14" />
{$t('文档')}
</span>
</Button>,
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[]) || [])
]
}, [pluginSlotHub.getSlot('basicLayoutAfterBtns')])
const actionRender =useMemo( ()=>{ return (
return [ <div
<LanguageSetting />, id="test-pro-layout"
<Button className=" text-[#ffffffb3] hover:text-[#fff] border-none" type="default" ghost onClick={()=>{window.open('https://docs.apipark.com','_blank')}}> style={{
<span className='flex items-center gap-[8px]'> <Icon icon="ic:baseline-help" width="14" height="14"/>{$t('文档')}</span> height: '100vh',
</Button> , overflow: 'auto'
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[] )||[] ) }}
] >
},[pluginSlotHub.getSlot('basicLayoutAfterBtns') ]) <ProConfigProvider hashed={false}>
<ConfigProvider
getTargetContainer={() => {
return( return document.getElementById('test-pro-layout') || document.body
<div }}
id="test-pro-layout"
style={{
height: '100vh',
overflow: 'auto',
}}
> >
<ProConfigProvider hashed={false}> <ProLayout
<ConfigProvider prefixCls="apipark-layout"
getTargetContainer={() => { location={{
return document.getElementById('test-pro-layout') || document.body; pathname
}}
siderWidth={220}
breakpoint={'lg'}
route={headerMenuData}
token={themeToken}
siderMenuType="group"
menu={{
type: 'group',
collapsedShowGroupTitle: true
}}
disableMobile={true}
avatarProps={{
src: AvatarPic || userInfo?.avatar,
size: 'small',
title: userInfo?.username || 'unknown',
render: (props, dom) => {
return (
<Dropdown
menu={{
items
}} }}
> >
<ProLayout <div className="avatar-dom">{dom}</div>
prefixCls="apipark-layout" </Dropdown>
location={{ )
pathname, }
}} }}
siderWidth={220} actionsRender={(props) => {
breakpoint={'lg'} if (props.isMobile) return []
route={headerMenuData} if (typeof window === 'undefined') return []
token={themeToken} return actionRender
siderMenuType="group" }}
menu={{ headerTitleRender={() => (
type: 'group', <div className="w-[192px] flex items-center">
collapsedShowGroupTitle: true, <img className="h-[20px] cursor-pointer " src={Logo} onClick={() => navigator(mainPage)} />
}} </div>
disableMobile={true} )}
avatarProps={{ logo={Logo}
src: AvatarPic || userInfo?.avatar, pageTitleRender={() => $t('APIPark')}
size: 'small', menuFooterRender={(props) => {
title: userInfo?.username||'unknown', if (props?.collapsed) return undefined
render: (props, dom) => { }}
return ( menuItemRender={(item, dom) => (
<Dropdown <div
menu={{ onClick={() => {
items // 同级目录点击无效
}} if (
> item.key &&
<div className='avatar-dom'>{dom} routerKeyMap.get(item.key) &&
</div> routerKeyMap.get(item.key).length > 0 &&
</Dropdown> routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1
); ) {
}, return
}} }
actionsRender={(props) => { if (item.key === pathname.split('/')[1]) {
if (props.isMobile) return []; return
if (typeof window === 'undefined') return []; }
return actionRender;
}} if (item.path) {
headerTitleRender={() => ( navigator(item.path)
<div className="w-[192px] flex items-center"> }
<img setPathname(item.path || '')
className="h-[20px] cursor-pointer " }}
src={Logo} >
onClick={()=> navigator(mainPage)} {dom}
/> </div>
</div> )}
)} fixSiderbar={true}
logo={Logo} layout="mix"
pageTitleRender={()=>$t('APIPark')} splitMenus={true}
menuFooterRender={(props) => { collapsed={false}
if (props?.collapsed) return undefined; collapsedButtonRender={false}
}} >
menuItemRender={(item, dom) => ( <div
<div className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${
onClick={() => { currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
// 同级目录点击无效 }`}
if(item.key && routerKeyMap.get(item.key) && routerKeyMap.get(item.key).length > 0 && routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1){ >
return <Outlet />
} </div>
if(item.key === pathname.split('/')[1]){ </ProLayout>
return </ConfigProvider>
} </ProConfigProvider>
</div>
if(item.path){ )
navigator(item.path)
}
setPathname(item.path || '');
}}
>
{dom}
</div>
)}
fixSiderbar={true}
layout='mix'
splitMenus={true}
collapsed={false}
collapsedButtonRender={false}
>
<div className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden' }`}>
<Outlet />
</div>
</ProLayout>
</ConfigProvider>
</ProConfigProvider>
</div>
)
} }
export default BasicLayout export default BasicLayout
@@ -1,15 +1,11 @@
import { Breadcrumb } from "antd" import { Breadcrumb } from 'antd'
import { useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import {FC,useEffect} from "react"; import { FC, useEffect } from 'react'
const TopBreadcrumb: FC = () => { const TopBreadcrumb: FC = () => {
const { breadcrumb } = useBreadcrumb() const { breadcrumb } = useBreadcrumb()
useEffect(() => { useEffect(() => {}, [breadcrumb])
}, [breadcrumb]); return <Breadcrumb items={breadcrumb} />
return (
<Breadcrumb items={breadcrumb} />
)
} }
export default TopBreadcrumb export default TopBreadcrumb
@@ -1,34 +1,32 @@
import { FC } from 'react'; import { FC } from 'react'
import { Table } from 'antd'; import { Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table'
import { $t } from '@common/locales'; import { $t } from '@common/locales'
interface DataType { interface DataType {
httpStatusCode: string; httpStatusCode: string
systemStatusCode: string; systemStatusCode: string
description: string; description: string
} }
const columns: ColumnsType<DataType> = [ const columns: ColumnsType<DataType> = [
{ {
title:$t('HTTP 状态码'), title: $t('HTTP 状态码'),
dataIndex: 'httpStatusCode', dataIndex: 'httpStatusCode',
key: 'httpStatusCode', key: 'httpStatusCode'
}, },
{ {
title:$t('系统状态码'), title: $t('系统状态码'),
dataIndex: 'systemStatusCode', dataIndex: 'systemStatusCode',
key: 'systemStatusCode', key: 'systemStatusCode'
}, },
{ {
title: $t('描述'), title: $t('描述'),
dataIndex: 'description', dataIndex: 'description',
key: 'description', key: 'description',
ellipsis:true ellipsis: true
}, }
]
];
const data: DataType[] = [ const data: DataType[] = [
// { // {
@@ -44,12 +42,12 @@ const data: DataType[] = [
{ {
httpStatusCode: '413', httpStatusCode: '413',
systemStatusCode: '10003', systemStatusCode: '10003',
description: '请求频率过高', description: '请求频率过高'
}, },
{ {
httpStatusCode: '403', httpStatusCode: '403',
systemStatusCode: '10004', systemStatusCode: '10004',
description: '请求来源非法,不在白名单中', description: '请求来源非法,不在白名单中'
}, },
// { // {
// httpStatusCode: '416', // httpStatusCode: '416',
@@ -59,7 +57,7 @@ const data: DataType[] = [
{ {
httpStatusCode: '504', httpStatusCode: '504',
systemStatusCode: '10006', systemStatusCode: '10006',
description: '网关超时', description: '网关超时'
}, },
// { // {
// httpStatusCode: '504', // httpStatusCode: '504',
@@ -69,7 +67,7 @@ const data: DataType[] = [
{ {
httpStatusCode: '404', httpStatusCode: '404',
systemStatusCode: '10007', systemStatusCode: '10007',
description: '接口不存在', description: '接口不存在'
}, },
// { // {
// httpStatusCode: '416', // httpStatusCode: '416',
@@ -84,42 +82,43 @@ const data: DataType[] = [
{ {
httpStatusCode: '400', httpStatusCode: '400',
systemStatusCode: '10010', systemStatusCode: '10010',
description: '无法识别请求内容,请检查请求体是否正确', description: '无法识别请求内容,请检查请求体是否正确'
}, },
{ {
httpStatusCode: '400', httpStatusCode: '400',
systemStatusCode: '10011', systemStatusCode: '10011',
description: '请求头部缺少 Content-Type 字段', description: '请求头部缺少 Content-Type 字段'
}, },
{ {
httpStatusCode: '400', httpStatusCode: '400',
systemStatusCode: '10011', systemStatusCode: '10011',
description: '请求头部 Content-Type 字段错误', description: '请求头部 Content-Type 字段错误'
}, },
{ {
httpStatusCode: '400', httpStatusCode: '400',
systemStatusCode: '10014', systemStatusCode: '10014',
description: '批量参数超出单次批量数量的最大限制', description: '批量参数超出单次批量数量的最大限制'
}, },
{ {
httpStatusCode: '400', httpStatusCode: '400',
systemStatusCode: '10016', systemStatusCode: '10016',
description: '参数缺少内容', description: '参数缺少内容'
}, },
{ {
httpStatusCode: '500', httpStatusCode: '500',
systemStatusCode: '10017', systemStatusCode: '10017',
description: '参数类型错误', description: '参数类型错误'
}, }
]; ]
const CodePage: FC = () => const CodePage: FC = () => (
<Table <Table
size="small" size="small"
columns={columns} columns={columns}
className='table-border border-b-0 rounded' className="table-border border-b-0 rounded"
dataSource={data?.map((item, index) => ({...item, key: index})) || []} dataSource={data?.map((item, index) => ({ ...item, key: index })) || []}
pagination={false} pagination={false}
/>; />
)
export default CodePage; export default CodePage
@@ -1,33 +1,32 @@
import { useState, FC } from 'react'
import { useState,FC } from 'react'; import { Tooltip, Button } from 'antd'
import { Tooltip, Button } from 'antd'; import useCopyToClipboard from '@common/hooks/copy'
import useCopyToClipboard from '@common/hooks/copy'; import { Icon } from '@iconify/react/dist/iconify.js'
import { Icon } from '@iconify/react/dist/iconify.js';
type AddressItem = { type AddressItem = {
expand?: boolean; expand?: boolean
[key: string]: unknown; [key: string]: unknown
} }
type CopyAddrListProps = { type CopyAddrListProps = {
addrItem: AddressItem; addrItem: AddressItem
onAddrItemChange?: (addrItem: AddressItem) => void; onAddrItemChange?: (addrItem: AddressItem) => void
keyName: string; keyName: string
} }
const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyName }) => { const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyName }) => {
const [localAddrItem, setLocalAddrItem] = useState<AddressItem>(addrItem); const [localAddrItem, setLocalAddrItem] = useState<AddressItem>(addrItem)
const { copyToClipboard } = useCopyToClipboard(); const { copyToClipboard } = useCopyToClipboard()
const toggleExpand = () => { const toggleExpand = () => {
const updatedAddrItem = { ...localAddrItem, expand: !localAddrItem.expand }; const updatedAddrItem = { ...localAddrItem, expand: !localAddrItem.expand }
setLocalAddrItem(updatedAddrItem); setLocalAddrItem(updatedAddrItem)
onAddrItemChange?.(updatedAddrItem); onAddrItemChange?.(updatedAddrItem)
}; }
const renderTooltipTitle = () => { const renderTooltipTitle = () => {
// 假设keyName对应的值是一个字符串数组 // 假设keyName对应的值是一个字符串数组
const addresses:string[] = localAddrItem[keyName] as string[] const addresses: string[] = localAddrItem[keyName] as string[]
return ( return (
<div> <div>
{addresses?.map((addr, index) => ( {addresses?.map((addr, index) => (
@@ -36,29 +35,41 @@ const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyNa
</div> </div>
))} ))}
</div> </div>
); )
}; }
const renderAddresses = () => { const renderAddresses = () => {
if (!localAddrItem.expand) { if (!localAddrItem.expand) {
return ( return (
<span className="overflow-ellipsis w-full inline-block overflow-hidden align-middle"> <span className="overflow-ellipsis w-full inline-block overflow-hidden align-middle">
<Tooltip title={renderTooltipTitle}> <Tooltip title={renderTooltipTitle}>
<span className='flex items-center'> <span className="flex items-center">
<span className={`overflow-ellipsis inline-block overflow-hidden align-middle ${((localAddrItem[keyName] as string[]).length > 1) ? 'w-5/6' : 'w-full'}`}> <span
className={`overflow-ellipsis inline-block overflow-hidden align-middle ${(localAddrItem[keyName] as string[]).length > 1 ? 'w-5/6' : 'w-full'}`}
>
{(localAddrItem[keyName] as string[]).join(',')} {(localAddrItem[keyName] as string[]).join(',')}
</span> </span>
{(localAddrItem[keyName] as string[]).length === 1 && ( {(localAddrItem[keyName] as string[]).length === 1 && (
<Button type="primary" className="border-none ant-typography-copy text-theme hover:text-A_HOVER " ghost onClick={() => copyToClipboard((localAddrItem[keyName] as string))} icon={<Icon icon="ic:baseline-file-copy" width="14" height="14"/>} size="small" /> <Button
type="primary"
className="border-none ant-typography-copy text-theme hover:text-A_HOVER "
ghost
onClick={() => copyToClipboard(localAddrItem[keyName] as string)}
icon={<Icon icon="ic:baseline-file-copy" width="14" height="14" />}
size="small"
/>
)} )}
{(localAddrItem[keyName] as string[]).length !== 1 && ( {(localAddrItem[keyName] as string[]).length !== 1 && (
<Button className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]" icon={<iconpark-icon name="zhankai" style={{marginTop:'4px'}}></iconpark-icon>} onClick={toggleExpand} /> <Button
className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]"
icon={<iconpark-icon name="zhankai" style={{ marginTop: '4px' }}></iconpark-icon>}
onClick={toggleExpand}
/>
)} )}
</span> </span>
</Tooltip> </Tooltip>
</span> </span>
); )
} else { } else {
return ( return (
<div className="flex flex-nowrap items-center justify-between"> <div className="flex flex-nowrap items-center justify-between">
@@ -66,21 +77,28 @@ const CopyAddrList: FC<CopyAddrListProps> = ({ addrItem, onAddrItemChange, keyNa
{(localAddrItem[keyName] as string[])?.map((addr: string, index: number) => ( {(localAddrItem[keyName] as string[])?.map((addr: string, index: number) => (
<div key={index} className="block w-full"> <div key={index} className="block w-full">
<span className="leading-6">{addr}</span> <span className="leading-6">{addr}</span>
<Button type="primary" className="border-none bg-transparent w-[16px] h-[22px] p-[0px] ml-2 text-theme hover:text-A_HOVER" ghost onClick={() => copyToClipboard(addr)} icon={<Icon icon="ic:baseline-file-copy" width="14" height="14"/>} size="small" /> <Button
type="primary"
className="border-none bg-transparent w-[16px] h-[22px] p-[0px] ml-2 text-theme hover:text-A_HOVER"
ghost
onClick={() => copyToClipboard(addr)}
icon={<Icon icon="ic:baseline-file-copy" width="14" height="14" />}
size="small"
/>
</div> </div>
))} ))}
</div> </div>
<Button className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]" icon={<iconpark-icon name="shouqi-2"></iconpark-icon>} onClick={toggleExpand} /> <Button
className="border-none bg-transparent w-[16px] h-[22px] text-table_text p-[0px]"
icon={<iconpark-icon name="shouqi-2"></iconpark-icon>}
onClick={toggleExpand}
/>
</div> </div>
); )
} }
}; }
return ( return <div>{renderAddresses()}</div>
<div> }
{renderAddresses()}
</div>
);
};
export default CopyAddrList; export default CopyAddrList
@@ -1,59 +1,84 @@
import { Button, Drawer, DrawerProps, Space } from 'antd'
import { Button, Drawer, DrawerProps, Space } from "antd"; import WithPermission from './WithPermission'
import WithPermission from "./WithPermission"; import { useEffect, useState } from 'react'
import { useEffect, useState } from "react"; import { $t } from '@common/locales'
import { $t } from '@common/locales';
export type DrawerWithFooterProps = DrawerProps & { export type DrawerWithFooterProps = DrawerProps & {
onSubmit?: () => Promise<boolean|string>|undefined onSubmit?: () => Promise<boolean | string> | undefined
submitAccess?: string submitAccess?: string
submitDisabled?:boolean submitDisabled?: boolean
onClose?:()=>void onClose?: () => void
showLastStep?:boolean showLastStep?: boolean
onLastStep?:()=>void onLastStep?: () => void
notAutoClose?:boolean notAutoClose?: boolean
showOkBtn?:boolean showOkBtn?: boolean
extraBtn?:React.ReactNode extraBtn?: React.ReactNode
okBtnTitle?:string okBtnTitle?: string
cancelBtnTitle?:string cancelBtnTitle?: string
} }
export function DrawerWithFooter(props:DrawerWithFooterProps){ export function DrawerWithFooter(props: DrawerWithFooterProps) {
const {children,title,placement='right',onClose,onSubmit,submitDisabled = false,okBtnTitle= $t('提交'),cancelBtnTitle,open,submitAccess,showLastStep,onLastStep,notAutoClose,showOkBtn=true,extraBtn} = props const {
const [submitLoading, setSubmitLoading] = useState<boolean>(false) children,
const handlerSubmit = ()=>{ title,
setSubmitLoading(true) placement = 'right',
onSubmit?.()?.then(()=>{!notAutoClose && onClose?.()}).finally(()=>{setSubmitLoading(false)}) onClose,
} onSubmit,
submitDisabled = false,
okBtnTitle = $t('提交'),
cancelBtnTitle,
open,
submitAccess,
showLastStep,
onLastStep,
notAutoClose,
showOkBtn = true,
extraBtn
} = props
const [submitLoading, setSubmitLoading] = useState<boolean>(false)
const handlerSubmit = () => {
setSubmitLoading(true)
onSubmit?.()
?.then(() => {
!notAutoClose && onClose?.()
})
.finally(() => {
setSubmitLoading(false)
})
}
useEffect(()=>{!open && setSubmitLoading(false)},[open]) useEffect(() => {
return (<> !open && setSubmitLoading(false)
<Drawer }, [open])
{...props} return (
push={false} <>
title={title} <Drawer
placement={placement} {...props}
width="60%" push={false}
destroyOnClose={true} title={title}
maskClosable={false} placement={placement}
classNames={ width="60%"
{footer:'text-right'} destroyOnClose={true}
} maskClosable={false}
footer={ classNames={{ footer: 'text-right' }}
<Space className="flex flex-row-reverse" style={{}}> footer={
{showOkBtn && <WithPermission access={submitAccess}> <Space className="flex flex-row-reverse" style={{}}>
<Button onClick={handlerSubmit} type="primary" loading={submitLoading} disabled={submitDisabled}> {showOkBtn && (
{ okBtnTitle} <WithPermission access={submitAccess}>
</Button> <Button onClick={handlerSubmit} type="primary" loading={submitLoading} disabled={submitDisabled}>
</WithPermission>} {okBtnTitle}
{ showLastStep && <Button onClick={onLastStep ?? onClose}> { $t('上一步')}</Button>} </Button>
{ extraBtn } </WithPermission>
<Button onClick={onClose}>{cancelBtnTitle ?? (showOkBtn ? $t('取消'):$t('关闭'))}</Button> )}
</Space> {showLastStep && <Button onClick={onLastStep ?? onClose}> {$t('上一步')}</Button>}
} {extraBtn}
onClose={onClose} <Button onClick={onClose}>{cancelBtnTitle ?? (showOkBtn ? $t('取消') : $t('关闭'))}</Button>
open={open} </Space>
> }
{children} onClose={onClose}
</Drawer> open={open}
</>) >
} {children}
</Drawer>
</>
)
}
@@ -1,91 +1,93 @@
import { FC } from 'react'
import {FC } from 'react'; import { Input, Space } from 'antd'
import { Input, Space } from 'antd'; import { Icon } from '@iconify/react/dist/iconify.js'
import { Icon } from '@iconify/react/dist/iconify.js';
type KeyValueInput = { type KeyValueInput = {
key: string; key: string
value: string; value: string
}; }
type DynamicKeyValueInputProps = { type DynamicKeyValueInputProps = {
value?: KeyValueInput[]; value?: KeyValueInput[]
onChange?: (newValue: KeyValueInput[]) => void; onChange?: (newValue: KeyValueInput[]) => void
}; }
export function transferToList(rawData: unknown): Array<{ key: string; value: string }> {
export function transferToList (rawData:unknown):Array<{key:string, value:string}> { const res: Array<{ key: string; value: string }> = []
const res:Array<{key:string, value:string}> = [] if (!rawData) return res
if(!rawData) const keys: Array<string> = Object.keys(rawData)
return res
const keys:Array<string> = Object.keys(rawData)
if (keys?.length > 0) { if (keys?.length > 0) {
for (const key of keys) { for (const key of keys) {
res.push({ key: key, value: rawData[key] }) res.push({ key: key, value: rawData[key] })
}
return [...res, { key: '', value: '' }]
} }
return [...res, { key: '', value: '' }] return [{ key: '', value: '' }]
}
return [{ key: '', value: '' }]
} }
export function transferToMap (rawData:Array<{key:string, value:string}>):{[key:string]:string} { export function transferToMap(rawData: Array<{ key: string; value: string }>): { [key: string]: string } {
const res:{[key:string]:string} = {} const res: { [key: string]: string } = {}
for (const kv of rawData) { for (const kv of rawData) {
if (kv.key && kv.value) { res[kv.key] = kv.value } if (kv.key && kv.value) {
res[kv.key] = kv.value
}
} }
return res return res
} }
export const DynamicKeyValueInput: FC<DynamicKeyValueInputProps> = ({value = [{key:'',value:''}],onChange}) => { export const DynamicKeyValueInput: FC<DynamicKeyValueInputProps> = ({ value = [{ key: '', value: '' }], onChange }) => {
// const [keyValuePairs, setKeyValuePairs] = useState<KeyValueInput[]>([{ key: '', value: '' }]); // const [keyValuePairs, setKeyValuePairs] = useState<KeyValueInput[]>([{ key: '', value: '' }]);
// Define a handler for when the inputs change // Define a handler for when the inputs change
const handleInputChange = (index: number, type: 'key' | 'value', newValue: string) => { const handleInputChange = (index: number, type: 'key' | 'value', newValue: string) => {
// Create a new array with the updated value // Create a new array with the updated value
const newKeyValuePairs = value ? [...value] : []; const newKeyValuePairs = value ? [...value] : []
if (newKeyValuePairs[index]) { if (newKeyValuePairs[index]) {
newKeyValuePairs[index][type] = newValue; newKeyValuePairs[index][type] = newValue
// If we're changing the last input and it's not empty, add a new pair // If we're changing the last input and it's not empty, add a new pair
if (index === newKeyValuePairs.length - 1 && (newKeyValuePairs[index].key || newKeyValuePairs[index].value)) { if (index === newKeyValuePairs.length - 1 && (newKeyValuePairs[index].key || newKeyValuePairs[index].value)) {
newKeyValuePairs.push({ key: '', value: '' }); newKeyValuePairs.push({ key: '', value: '' })
} }
// Call the onChange handler if it exists // Call the onChange handler if it exists
onChange?.(newKeyValuePairs); onChange?.(newKeyValuePairs)
} }
}; }
const addNewPair = () => { const addNewPair = () => {
const newKeyValuePairs = value ? [...value, { key: '', value: '' }] : [{ key: '', value: '' }]; const newKeyValuePairs = value ? [...value, { key: '', value: '' }] : [{ key: '', value: '' }]
onChange?.(newKeyValuePairs); onChange?.(newKeyValuePairs)
}; }
const removePair = (index: number) => { const removePair = (index: number) => {
const newKeyValuePairs = value?.filter((_, idx) => idx !== index) || []; const newKeyValuePairs = value?.filter((_, idx) => idx !== index) || []
onChange?.(newKeyValuePairs); onChange?.(newKeyValuePairs)
}; }
return ( return (
<> <>
{value && value?.map((pair, index) => ( {value &&
<Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="baseline"> value?.map((pair, index) => (
<Input <Space key={index} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
placeholder="Key" <Input
value={pair.key} placeholder="Key"
onChange={(e) => handleInputChange(index, 'key', e.target.value)} value={pair.key}
style={{ width: 162 }} /> onChange={(e) => handleInputChange(index, 'key', e.target.value)}
<Input style={{ width: 162 }}
placeholder="Value" />
value={pair.value} <Input
onChange={(e) => handleInputChange(index, 'value', e.target.value)} placeholder="Value"
style={{ width: 162 }} /> value={pair.value}
{index !== value.length - 1 && ( onChange={(e) => handleInputChange(index, 'value', e.target.value)}
<> style={{ width: 162 }}
<Icon icon="ic:baseline-delete" onClick={() => removePair(index)} width="14" height="14"/> />
<Icon icon="ic:baseline-add" onClick={addNewPair} width="14" height="14"/> {index !== value.length - 1 && (
</> <>
)} <Icon icon="ic:baseline-delete" onClick={() => removePair(index)} width="14" height="14" />
</Space> <Icon icon="ic:baseline-add" onClick={addNewPair} width="14" height="14" />
</>
)}
</Space>
))} ))}
</> </>
); )
}; }
@@ -1,116 +1,130 @@
import { EditableProTable } from "@ant-design/pro-components"; import { EditableProTable } from '@ant-design/pro-components'
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect, useMemo } from 'react'
import { v4 as uuidv4} from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { PageProColumns } from "./PageList"; import { PageProColumns } from './PageList'
import TableBtnWithPermission from "./TableBtnWithPermission"; import TableBtnWithPermission from './TableBtnWithPermission'
import { $t } from "@common/locales"; import { $t } from '@common/locales'
import { useGlobalContext } from "@common/contexts/GlobalStateContext"; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
interface EditableTableProps<T> { interface EditableTableProps<T> {
configFields: PageProColumns<T>[]; configFields: PageProColumns<T>[]
value?: T[]; // 外部传入的值 value?: T[] // 外部传入的值
className?: string; className?: string
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数 onChange?: (newConfigItems: T[]) => void // 当配置项变化时,外部传入的回调函数
// tableProps?: TableProps<T>; // tableProps?: TableProps<T>;
disabled?:boolean disabled?: boolean
extendsId?:string[] // 自增一行时,需要和上一行数据一致的字段,比如集群id extendsId?: string[] // 自增一行时,需要和上一行数据一致的字段,比如集群id
} }
const EditableTable = <T extends { _id: string }>({ const EditableTable = <T extends { _id: string }>({
configFields, configFields,
value, // value 现在是外部传入的配置项数组 value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数 onChange, // onChange 现在是当配置项数组变化时的回调函数
// tableProps, // tableProps,
disabled, disabled,
className, className,
extendsId, extendsId
}: EditableTableProps<T>) => { }: EditableTableProps<T>) => {
const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]); const [configurations, setConfigurations] = useState<(T | { _id: string })[]>(value || [{ _id: '1234' }])
const {state} = useGlobalContext() const { state } = useGlobalContext()
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => value?.map((item) => item._id) || ['1234'])
value?.map((item) => item._id) || ['1234']
);
useEffect(() => { useEffect(() => {
setConfigurations(value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || [{_id:uuidv4()}]); setConfigurations(value?.map((x) => (x._id ? x : { ...x, _id: uuidv4() })) || [{ _id: uuidv4() }])
}, [value]); }, [value])
const getNotEmptyValue = (value:unknown)=>{ const getNotEmptyValue = (value: unknown) => {
return value return value
} }
const translatedColumns = useMemo(()=>configFields.map((x)=>({...x, title:$t(x.title as string)})),[state.language,configFields]) const translatedColumns = useMemo(
() => configFields.map((x) => ({ ...x, title: $t(x.title as string) })),
[state.language, configFields]
)
return ( return (
<EditableProTable<T> <EditableProTable<T>
className={className} className={className}
columns={translatedColumns} columns={translatedColumns}
rowKey="_id" rowKey="_id"
value={configurations as T[]} value={configurations as T[]}
size="small" size="small"
bordered={true} bordered={true}
recordCreatorProps={false} recordCreatorProps={false}
editable={ { editable={{
type: 'multiple', type: 'multiple',
editableKeys:disabled ? [] : configurations?.map(x=>x._id), editableKeys: disabled ? [] : configurations?.map((x) => x._id),
actionRender: (row, config) => { actionRender: (row, config) => {
return [ return [
<TableBtnWithPermission key="add" btnType="add" onClick={() => { <TableBtnWithPermission
const newId = uuidv4(); key="add"
setConfigurations((prev)=>{ btnType="add"
const tmpPreData = [...prev]; onClick={() => {
const newId = uuidv4() const newId = uuidv4()
const lastRecord:{[k:string]:unknown} = tmpPreData[tmpPreData.length - 1]; setConfigurations((prev) => {
const newRecord :{[k:string]:unknown, _id:string}= { _id: newId }; const tmpPreData = [...prev]
const newId = uuidv4()
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值 const lastRecord: { [k: string]: unknown } = tmpPreData[tmpPreData.length - 1]
if(extendsId && extendsId.length > 0) { const newRecord: { [k: string]: unknown; _id: string } = { _id: newId }
extendsId.forEach(field => {
newRecord[field] = lastRecord[field];
});
}
tmpPreData.splice(Number(config.index) + 1, 0,newRecord);
onChange?.(getNotEmptyValue(tmpPreData));
return tmpPreData});
setEditableRowKeys((prev)=>([...prev,newId]))
}}
btnTitle="增加"/>,
(config.index !== configurations.length - 1 )&& <TableBtnWithPermission key="remove" btnType="remove" btnTitle="删除" // 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
onClick={() => { if (extendsId && extendsId.length > 0) {
setConfigurations((prev)=>{ extendsId.forEach((field) => {
const tmpPreData = [...prev]; newRecord[field] = lastRecord[field]
tmpPreData.splice(Number(config.index), 1); })
onChange?.(tmpPreData); }
return tmpPreData}); tmpPreData.splice(Number(config.index) + 1, 0, newRecord)
setEditableRowKeys((prev)=>(prev.filter(x=>x !== config._id))) onChange?.(getNotEmptyValue(tmpPreData))
}}/>,, return tmpPreData
]; })
}, setEditableRowKeys((prev) => [...prev, newId])
onValuesChange: (record, recordList) => { }}
if(record._id === recordList[recordList.length - 1]._id){ btnTitle="增加"
const newId = uuidv4() />,
const lastRecord:{[k:string]:unknown} = recordList[recordList.length - 1];
const newRecord :{[k:string]:unknown, _id:string}= { _id: newId };
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
if(extendsId && extendsId.length > 0) {
extendsId.forEach(field => {
newRecord[field] = lastRecord[field];
});
}
recordList = ([...recordList, newRecord as T]);
setEditableRowKeys((prev)=>[...prev, newId])
}
setConfigurations(recordList);
onChange?.(recordList);
},
onChange: setEditableRowKeys,
}}
/>
)
}
export default EditableTable; config.index !== configurations.length - 1 && (
<TableBtnWithPermission
key="remove"
btnType="remove"
btnTitle="删除"
onClick={() => {
setConfigurations((prev) => {
const tmpPreData = [...prev]
tmpPreData.splice(Number(config.index), 1)
onChange?.(tmpPreData)
return tmpPreData
})
setEditableRowKeys((prev) => prev.filter((x) => x !== config._id))
}}
/>
),
,
]
},
onValuesChange: (record, recordList) => {
if (record._id === recordList[recordList.length - 1]._id) {
const newId = uuidv4()
const lastRecord: { [k: string]: unknown } = recordList[recordList.length - 1]
const newRecord: { [k: string]: unknown; _id: string } = { _id: newId }
// 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值
if (extendsId && extendsId.length > 0) {
extendsId.forEach((field) => {
newRecord[field] = lastRecord[field]
})
}
recordList = [...recordList, newRecord as T]
setEditableRowKeys((prev) => [...prev, newId])
}
setConfigurations(recordList)
onChange?.(recordList)
},
onChange: setEditableRowKeys
}}
/>
)
}
export default EditableTable
@@ -1,105 +1,119 @@
import { EditableFormInstance, EditableProTable } from "@ant-design/pro-components"; import { EditableFormInstance, EditableProTable } from '@ant-design/pro-components'
import { useState, useEffect, useMemo, useRef, MutableRefObject } from "react"; import { useState, useEffect, useMemo, useRef, MutableRefObject } from 'react'
import { v4 as uuidv4} from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { PageProColumns } from "./PageList"; import { PageProColumns } from './PageList'
import TableBtnWithPermission from "./TableBtnWithPermission"; import TableBtnWithPermission from './TableBtnWithPermission'
import { $t } from "@common/locales"; import { $t } from '@common/locales'
import { useGlobalContext } from "@common/contexts/GlobalStateContext"; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { Form } from "antd"; import { Form } from 'antd'
import { debounce } from "lodash-es"; import { debounce } from 'lodash-es'
interface EditableTableProps<T> { interface EditableTableProps<T> {
configFields: PageProColumns<T>[]; configFields: PageProColumns<T>[]
value?: T[]; // 外部传入的值 value?: T[] // 外部传入的值
className?: string; className?: string
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数 onChange?: (newConfigItems: T[]) => void // 当配置项变化时,外部传入的回调函数
// tableProps?: TableProps<T>; // tableProps?: TableProps<T>;
disabled?:boolean disabled?: boolean
getFromRef?:(form:MutableRefObject<EditableFormInstance<T> | undefined>)=>void getFromRef?: (form: MutableRefObject<EditableFormInstance<T> | undefined>) => void
} }
const EditableTableNotAutoGen = <T extends { _id: string }>({ const EditableTableNotAutoGen = <T extends { _id: string }>({
configFields, configFields,
value, // value 现在是外部传入的配置项数组 value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数 onChange, // onChange 现在是当配置项数组变化时的回调函数
// tableProps, // tableProps,
disabled, disabled,
className, className,
getFromRef getFromRef
}: EditableTableProps<T>) => { }: EditableTableProps<T>) => {
const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]); const [configurations, setConfigurations] = useState<(T | { _id: string })[]>(value || [{ _id: '1234' }])
const {state} = useGlobalContext() const { state } = useGlobalContext()
const form =useRef<EditableFormInstance<T>>(); const form = useRef<EditableFormInstance<T>>()
const [tableForm] = Form.useForm(); const [tableForm] = Form.useForm()
const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => value?.map((item) => item._id) || ['1234'])
value?.map((item) => item._id) || ['1234']
);
useEffect(()=>{ useEffect(() => {
getFromRef?.(form) getFromRef?.(form)
},[form]) }, [form])
useEffect(() => { useEffect(() => {
const newValue = value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || [{_id:uuidv4()}] const newValue = value?.map((x) => (x._id ? x : { ...x, _id: uuidv4() })) || [{ _id: uuidv4() }]
setConfigurations(newValue); setConfigurations(newValue)
setTimeout(()=>validateForm(),1000) setTimeout(() => validateForm(), 1000)
}, [value]); }, [value])
const validateForm = async ()=>{ const validateForm = async () => {
await tableForm.validateFields(); await tableForm.validateFields()
} }
const translatedColumns = useMemo(()=>configFields.map((x)=>( const translatedColumns = useMemo(
{...x, () =>
title:$t(x.title as string), configFields.map((x) => ({
formItemProps:{ ...x,
...(x. formItemProps || {}), title: $t(x.title as string),
rules:[...(x.formItemProps?.rules || []).map((r:Record<string, string>)=>{ formItemProps: {
if(r.message){ ...(x.formItemProps || {}),
r.message = $t(r.message) rules: [
} ...(x.formItemProps?.rules || []).map((r: Record<string, string>) => {
return r if (r.message) {
})], r.message = $t(r.message)
}})),[state.language,configFields]) }
return r
const debouncedOnChange = useMemo(() => debounce((value) => { })
onChange?.(value); ]
}, 500), [onChange]); }
})),
[state.language, configFields]
)
return ( const debouncedOnChange = useMemo(
<EditableProTable<T> () =>
className={className} debounce((value) => {
columns={translatedColumns} onChange?.(value)
onChange={debouncedOnChange} }, 500),
controlled={true} [onChange]
rowKey="_id" )
value={configurations as T[]}
size="small"
editableFormRef={form}
bordered={true}
recordCreatorProps={false}
editable={ {
type: 'multiple',
form: tableForm,
// errorType:'default',
editableKeys:disabled ? [] : configurations?.map(x=>x._id),
actionRender: (row, config) => {
return [
<TableBtnWithPermission key="delete" btnType="delete" btnTitle="删除"
onClick={() => {
setConfigurations((prev)=>{
const tmpPreData = [...prev];
tmpPreData.splice(Number(config.index), 1);
onChange?.(tmpPreData);
return tmpPreData});
setEditableRowKeys((prev)=>(prev.filter(x=>x !== config._id)))
}}/>,
];
},
onChange: setEditableRowKeys
}}
/>
)
}
export default EditableTableNotAutoGen; return (
<EditableProTable<T>
className={className}
columns={translatedColumns}
onChange={debouncedOnChange}
controlled={true}
rowKey="_id"
value={configurations as T[]}
size="small"
editableFormRef={form}
bordered={true}
recordCreatorProps={false}
editable={{
type: 'multiple',
form: tableForm,
// errorType:'default',
editableKeys: disabled ? [] : configurations?.map((x) => x._id),
actionRender: (row, config) => {
return [
<TableBtnWithPermission
key="delete"
btnType="delete"
btnTitle="删除"
onClick={() => {
setConfigurations((prev) => {
const tmpPreData = [...prev]
tmpPreData.splice(Number(config.index), 1)
onChange?.(tmpPreData)
return tmpPreData
})
setEditableRowKeys((prev) => prev.filter((x) => x !== config._id))
}}
/>
]
},
onChange: setEditableRowKeys
}}
/>
)
}
export default EditableTableNotAutoGen
@@ -1,165 +1,195 @@
import {useEffect, useMemo, useState} from 'react'; import { useEffect, useMemo, useState } from 'react'
import { Button, Modal, Form, Table, FormInstance, TableProps, Divider } from 'antd'; import { Button, Modal, Form, Table, FormInstance, TableProps, Divider } from 'antd'
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid'
import { ColumnsType } from 'antd/es/table'; import WithPermission from './WithPermission'
import WithPermission from './WithPermission'; import { $t } from '@common/locales'
import { $t } from '@common/locales'; import { COLUMNS_TITLE } from '@common/const/const'
import { COLUMNS_TITLE, VALIDATE_MESSAGE } from '@common/const/const'; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'; import TableBtnWithPermission from './TableBtnWithPermission'
import TableBtnWithPermission from './TableBtnWithPermission';
export interface ConfigField<T> { export interface ConfigField<T> {
title: string; title: string
key: keyof T; key: keyof T
component: React.ReactNode; component: React.ReactNode
renderText?: (value: unknown, record: T) => string; renderText?: (value: unknown, record: T) => string
required?: boolean; required?: boolean
ellipsis?:boolean ellipsis?: boolean
unRender?:(form:FormInstance)=>boolean unRender?: (form: FormInstance) => boolean
} }
interface EditableTableWithModalProps<T> { interface EditableTableWithModalProps<T> {
configFields: ConfigField<T>[]; configFields: ConfigField<T>[]
value?: T[]; // 外部传入的值 value?: T[] // 外部传入的值
className?: string; className?: string
onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数 onChange?: (newConfigItems: T[]) => void // 当配置项变化时,外部传入的回调函数
tableProps?: TableProps<T>; tableProps?: TableProps<T>
disabled?:boolean disabled?: boolean
} }
const EditableTableWithModal = <T extends { _id?: string }>({ const EditableTableWithModal = <T extends { _id?: string }>({
configFields, configFields,
value, // value 现在是外部传入的配置项数组 value, // value 现在是外部传入的配置项数组
onChange, // onChange 现在是当配置项数组变化时的回调函数 onChange, // onChange 现在是当配置项数组变化时的回调函数
tableProps, tableProps,
disabled, disabled,
className className
}: EditableTableWithModalProps<T>) => { }: EditableTableWithModalProps<T>) => {
const [form] = Form.useForm<FormInstance>(); const [form] = Form.useForm<FormInstance>()
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false)
const [configurations, setConfigurations] = useState<T[]>(value ||[]); const [configurations, setConfigurations] = useState<T[]>(value || [])
const [editingConfig, setEditingConfig] = useState<T | null>(null); const [editingConfig, setEditingConfig] = useState<T | null>(null)
const {state} = useGlobalContext() const { state } = useGlobalContext()
const [formsValue, setFormsValue] = useState<FormInstance<unknown>>() const [formsValue, setFormsValue] = useState<FormInstance<unknown>>()
const showModal = (config?: T) => { const showModal = (config?: T) => {
if (config) { if (config) {
form.setFieldsValue(config as Record<string, unknown>); form.setFieldsValue(config as Record<string, unknown>)
setEditingConfig(config); setEditingConfig(config)
} else {
form.resetFields()
setEditingConfig(null)
}
setIsModalVisible(true)
}
const handleCancel = () => {
setIsModalVisible(false)
}
const handleDelete = (_id: string) => {
const newConfigurations = configurations.filter((config) => config._id !== _id)
setConfigurations(newConfigurations)
onChange?.(newConfigurations)
}
const handleOk = () => {
form
.validateFields()
.then((values) => {
let newConfigurations = [...configurations]
if (editingConfig && editingConfig._id) {
newConfigurations = newConfigurations?.map((config) =>
config._id === editingConfig._id ? { ...config, ...values } : config
)
} else { } else {
form.resetFields(); const newConfig = { _id: uuidv4(), ...values } as Record<string, unknown>
setEditingConfig(null); newConfigurations.push(newConfig as T)
} }
setIsModalVisible(true); setConfigurations(newConfigurations)
}; onChange?.(newConfigurations)
setIsModalVisible(false)
})
.catch((info) => {
console.log('Validate Failed:', info)
})
}
const handleCancel = () => { useEffect(() => {
setIsModalVisible(false); setConfigurations(value?.map((x) => (x._id ? x : { ...x, _id: uuidv4() })) || [])
}; }, [value])
const handleDelete = (_id: string) => { const columns = useMemo(
const newConfigurations = configurations.filter(config => config._id !== _id); () => [
setConfigurations(newConfigurations); ...configFields.map(({ title, key, renderText }) => ({
onChange?.(newConfigurations); title: $t(title),
}; dataIndex: key as string,
key: key as string,
const handleOk = () => { render: renderText ? (value, record) => $t(renderText(value, record) || '') : undefined,
form.validateFields() ellipsis: true
.then(values => { })),
let newConfigurations = [...configurations]; ...(disabled
if (editingConfig && editingConfig._id) { ? []
newConfigurations = newConfigurations?.map(config => : [
config._id === editingConfig._id ? { ...config, ...values } : config {
); title: COLUMNS_TITLE.operate,
} else { key: 'action',
const newConfig = { _id: uuidv4(), ...values } as Record<string, unknown>; btnNums: 2,
newConfigurations.push(newConfig as T); render: (_: unknown, record: T) => (
}
setConfigurations(newConfigurations);
onChange?.(newConfigurations);
setIsModalVisible(false);
})
.catch(info => {
console.log('Validate Failed:', info);
});
};
useEffect(() => {
setConfigurations(value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || []);
}, [value]);
const columns = useMemo(()=>[
...configFields.map(({ title, key, renderText }) => ({
title:$t(title),
dataIndex: key as string,
key: key as string,
render: renderText ? (value, record) => $t(renderText(value, record) || '') : undefined,
ellipsis:true
})),
...(disabled ? []:[{
title: COLUMNS_TITLE.operate,
key: 'action',
btnNums:2,
render: (_: unknown, record: T) => (
<> <>
<div className="flex items-center"> <div className="flex items-center">
<TableBtnWithPermission key="add" disabled={disabled} btnType="edit" onClick={()=>{showModal(record)}} btnTitle='编辑'/> <TableBtnWithPermission
key="add"
disabled={disabled}
btnType="edit"
onClick={() => {
showModal(record)
}}
btnTitle="编辑"
/>
<Divider key="div1" type="vertical" /> <Divider key="div1" type="vertical" />
<TableBtnWithPermission key="delete" disabled={disabled} btnType="delete" onClick={()=>{handleDelete(record._id || '')}} btnTitle='删除'/> <TableBtnWithPermission
</div> key="delete"
disabled={disabled}
btnType="delete"
onClick={() => {
handleDelete(record._id || '')
}}
btnTitle="删除"
/>
</div>
</> </>
), )
}] ) }
],[state.language, disabled, configFields]) ])
],
[state.language, disabled, configFields]
)
const formItems = useMemo(() => {
const formItems = useMemo(()=>{ return configFields.map(({ title, key, component, required, unRender }) => {
return configFields.map(({ title,key, component, required,unRender }) => { return unRender && unRender(formsValue) ? null : (
return ( <Form.Item label={$t(title as string)} name={key as string} rules={[{ required }]}>
unRender && unRender(formsValue) ? null : {component}
<Form.Item </Form.Item>
label={$t(title as string)} )
name={key as string} })
rules={[{ required}]} }, [formsValue])
>
{component}
</Form.Item>
)
})
}
,[formsValue])
return (
<>
{!disabled && (
<Button className="" disabled={disabled} onClick={() => showModal()}>
{$t('添加配置')}
</Button>
)}
{configurations.length > 0 && (
<Table
className={`mt-btnybase border-solid border-[1px] border-BORDER border-b-0 rounded ${className}`}
{...tableProps}
dataSource={configurations}
size="small"
columns={columns}
rowKey="_id"
pagination={false}
/>
)}
<Modal
title={editingConfig ? $t('编辑配置') : $t('添加配置')}
open={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
maskClosable={false}
>
<WithPermission access="">
<Form
form={form}
name="editableTableWithModal"
layout="vertical"
scrollToFirstError
onFieldsChange={() => {
setFormsValue(form.getFieldsValue())
}}
// labelCol={{ span: 7 }}
// wrapperCol={{ span: 17}}
autoComplete="off"
>
{formItems}
</Form>
</WithPermission>
</Modal>
</>
)
}
export default EditableTableWithModal
return (
<>
{!disabled && <Button className="" disabled={disabled} onClick={() => showModal()}>{$t('添加配置')}</Button>}
{configurations.length > 0 &&
<Table
className={`mt-btnybase border-solid border-[1px] border-BORDER border-b-0 rounded ${className}`} {...tableProps} dataSource={configurations} size="small" columns={columns} rowKey="_id" pagination={false}/>}
<Modal
title={editingConfig ? $t('编辑配置') : $t('添加配置')}
open={isModalVisible}
onOk={handleOk}
onCancel={handleCancel}
width={600}
maskClosable={false}
>
<WithPermission access=""><Form form={form} name="editableTableWithModal"
layout="vertical"
scrollToFirstError
onFieldsChange={(()=>{
setFormsValue(form.getFieldsValue())
})}
// labelCol={{ span: 7 }}
// wrapperCol={{ span: 17}}
autoComplete="off">
{formItems}
</Form></WithPermission>
</Modal>
</>
);
};
export default EditableTableWithModal;
@@ -1,23 +1,23 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from 'react'
function ErrorBoundary({ children }) { function ErrorBoundary({ children }) {
const [error, setError] = useState(null); const [error, setError] = useState(null)
useEffect(() => { useEffect(() => {
window.addEventListener("error", (event) => { window.addEventListener('error', (event) => {
setError(event.error); setError(event.error)
}); })
}, []); }, [])
if (error) { if (error) {
return ( return (
<div> <div>
<h1>An error occurred</h1> <h1>An error occurred</h1>
<pre>{error.message}</pre> <pre>{error.message}</pre>
</div> </div>
); )
}
return children;
} }
export default ErrorBoundary return children
}
export default ErrorBoundary
@@ -1,66 +1,108 @@
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button, Tag } from "antd" import WithPermission from '@common/components/aoplatform/WithPermission'
import {useNavigate} from "react-router-dom"; import { $t } from '@common/locales'
import WithPermission from "@common/components/aoplatform/WithPermission"; import { Button, Tag } from 'antd'
import { FC, ReactNode } from "react"; import { FC, ReactNode } from 'react'
import { ArrowLeftOutlined, LeftOutlined } from "@ant-design/icons"; import { useNavigate } from 'react-router-dom'
import { $t } from "@common/locales";
class InsidePageProps { class InsidePageProps {
showBanner?:boolean = true showBanner?: boolean = true
pageTitle:string| React.ReactNode = '' pageTitle: string | React.ReactNode = ''
tagList?:Array<{label:string|ReactNode}> = [] tagList?: Array<{ label: string | ReactNode }> = []
children:React.ReactNode children: React.ReactNode
showBtn?:boolean = false showBtn?: boolean = false
btnTitle?:string = '' btnTitle?: string = ''
description?:string | React.ReactNode= '' description?: string | React.ReactNode = ''
onBtnClick?:()=>void onBtnClick?: () => void
backUrl?:string = '/' backUrl?: string = '/'
btnAccess?:string btnAccess?: string
showBorder?:boolean = true showBorder?: boolean = true
className?:string = '' className?: string = ''
contentClassName?:string='' contentClassName?: string = ''
headerClassName?:string='' headerClassName?: string = ''
/** 整个页面滚动 */ /** 整个页面滚动 */
scrollPage?:boolean = true scrollPage?: boolean = true
customBtn?:ReactNode customBtn?: ReactNode
} }
const InsidePage:FC<InsidePageProps> = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl,showBorder=true,className='',contentClassName='',headerClassName='',scrollPage=true,customBtn})=>{ const InsidePage: FC<InsidePageProps> = ({
const navigate = useNavigate(); showBanner = true,
pageTitle,
tagList,
showBtn,
btnTitle,
btnAccess,
description,
children,
onBtnClick,
backUrl,
showBorder = true,
className = '',
contentClassName = '',
headerClassName = '',
scrollPage = true,
customBtn
}) => {
const navigate = useNavigate()
const goBack = () => { const goBack = () => {
navigate(backUrl || '/'); navigate(backUrl || '/')
}; }
return ( return (
// <div className="h-full flex flex-col flex-1 overflow-hidden bg-[#f7f8fa]"> <div className={`flex overflow-hidden flex-col flex-1 h-full ${className}`}>
<div className={`h-full flex flex-col flex-1 overflow-hidden ${className}`}> {showBanner && (
{ showBanner && <div className={`border-[0px] mr-PAGE_INSIDE_X ${showBorder ? 'border-b-[1px] border-solid border-BORDER' : ''} ${headerClassName}`}> <div
{!pageTitle && !description && !backUrl &&!customBtn ? <></>: <div className="mb-[30px]"> className={`border-[0px] mr-PAGE_INSIDE_X ${showBorder ? 'border-solid border-b-[1px] border-BORDER' : ''} ${headerClassName}`}
{backUrl &&<div className="text-[18px] leading-[25px] mb-[12px]"> >
<Button type="text" onClick={goBack}><ArrowLeftOutlined className="max-h-[14px]" />{$t('返回')}</Button> {!pageTitle && !description && !backUrl && !customBtn ? (
</div>} <></>
<div className="flex justify-between mb-[20px] items-center "> ) : (
<div className="flex items-center gap-TAG_LEFT "> <div className="mb-[30px]">
<div className="text-theme text-[26px] ">{pageTitle}</div> {backUrl && (
{tagList && tagList?.length > 0 && tagList?.map((tag)=>{ <div className="text-[18px] leading-[25px] mb-[12px]">
return ( <Tag key={tag.label as string} bordered={false} >{tag.label}</Tag>) <Button type="text" onClick={goBack}>
})} <ArrowLeftOutlined className="max-h-[14px]" />
</div> {$t('返回')}
{showBtn && <WithPermission access={btnAccess}><Button type="primary" onClick={()=> { </Button>
onBtnClick&&onBtnClick() </div>
}}>{btnTitle}</Button></WithPermission>} )}
{customBtn} <div className="flex justify-between mb-[20px] items-center ">
</div> <div className="flex items-center gap-TAG_LEFT">
<div > <div className="text-theme text-[26px] ">{pageTitle}</div>
{description} {tagList &&
</div> tagList?.length > 0 &&
</div>} tagList?.map((tag) => {
</div>} return (
<div className={`h-full ${scrollPage ? 'overflow-hidden' : 'overflow-auto'} ${contentClassName || ''}`}>{children}</div> <Tag key={tag.label as string} bordered={false}>
{tag.label}
</Tag>
)
})}
</div>
{showBtn && (
<WithPermission access={btnAccess}>
<Button
type="primary"
onClick={() => {
onBtnClick && onBtnClick()
}}
>
{btnTitle}
</Button>
</WithPermission>
)}
{customBtn}
</div>
<div>{description}</div>
</div>
)}
</div> </div>
) )}
<div className={`h-full ${scrollPage ? 'overflow-hidden' : 'overflow-auto'} ${contentClassName || ''}`}>
{children}
</div>
</div>
)
} }
export default InsidePage export default InsidePage
@@ -1,72 +1,92 @@
import { Dropdown, Row, Col, Button } from 'antd'; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import i18n from '@common/locales'; import i18n from '@common/locales'
import { memo, useEffect, useMemo } from 'react'; import { Icon } from '@iconify/react/dist/iconify.js'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'; import { Button, Dropdown } from 'antd'
import { Icon } from '@iconify/react/dist/iconify.js'; import { memo, useEffect, useMemo } from 'react'
const LanguageSetting = ({mode = 'light'}:{mode?:'dark'|'light'}) => { const LanguageItems = [
const { dispatch,state} = useGlobalContext() {
const items = [ key: 'en-US',
{ label: (
key: 'en-US', <Button key="en" type="text" className="flex items-center p-0 bg-transparent border-none">
label:<Button key="en" type="text" className="border-none p-0 flex items-center bg-transparent "> English
English </Button>
</Button>, ),
title:'English' title: 'English'
}, },
{ {
key: 'ja-JP', key: 'ja-JP',
label: <Button key="jp" type="text" className="border-none p-0 flex items-center bg-transparent "> label: (
<Button key="jp" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>,
title: '日本語', </Button>
}, ),
{ title: '日本語'
key: 'zh-TW', },
label: <Button key="tw" type="text" className="border-none p-0 flex items-center bg-transparent "> {
key: 'zh-TW',
</Button>, label: (
title: '繁體中文', <Button key="tw" type="text" className="flex items-center p-0 bg-transparent border-none">
},
{ </Button>
key: 'zh-CN', ),
label: <Button key="cn" type="text" className="border-none p-0 flex items-center bg-transparent "> title: '繁體中文'
},
</Button>, {
title: '简体中文', key: 'zh-CN',
}, label: (
]; <Button key="cn" type="text" className="flex items-center p-0 bg-transparent border-none">
</Button>
),
title: '简体中文'
}
]
const LanguageSetting = ({ mode = 'light' }: { mode?: 'dark' | 'light' }) => {
const { dispatch, state } = useGlobalContext()
const langLabel = useMemo(()=>items.find((item) => item?.key === state.language)?.title,[state.language]) const langLabel = useMemo(() => LanguageItems.find((item) => item?.key === state.language)?.title, [state.language])
useEffect(()=>{ useEffect(() => {
const savedLang = sessionStorage.getItem('i18nextLng') const savedLang = i18n.language || sessionStorage.getItem('i18nextLng')
const browserLang = navigator.language || navigator.userLanguage if (savedLang && state.language !== savedLang) {
if(savedLang){ dispatch({ type: 'UPDATE_LANGUAGE', language: savedLang })
dispatch({ type: 'UPDATE_LANGUAGE', language: savedLang }); } else if (!savedLang) {
}else{ const browserLang = navigator.language
dispatch({ type: 'UPDATE_LANGUAGE', language: browserLang }); const supportedLang = LanguageItems.find((item) => item.key === browserLang) ? browserLang : 'zh-CN'
if (state.language === supportedLang) return
dispatch({ type: 'UPDATE_LANGUAGE', language: supportedLang })
i18n.changeLanguage(supportedLang)
} }
},[ }, [])
])
return ( return (
<Dropdown <Dropdown
trigger={['hover']} trigger={['hover']}
menu={{ menu={{
items, items: LanguageItems,
style:{minWidth:'80px'}, style: { minWidth: '80px' },
onClick: (e) => { onClick: (e) => {
const { key } = e; const { key } = e
dispatch({ type: 'UPDATE_LANGUAGE', language: key }); dispatch({ type: 'UPDATE_LANGUAGE', language: key })
i18n.changeLanguage(key); i18n.changeLanguage(key)
sessionStorage.setItem('i18nextLng', key)
} }
}} }}
> >
<Button className={`border-none ${mode==='dark' ? "text-[#333] hover:text-[#333333b3]" : "text-[#ffffffb3] hover:text-[#fff] "}`} type="default" ghost > <Button
<span className='flex items-center gap-[8px]'> <Icon icon="ic:baseline-language" width="14" height="14"/>{langLabel}</span> className={`border-none ${
</Button> mode === 'dark' ? 'text-[#333] hover:text-[#333333b3]' : 'text-[#ffffffb3] hover:text-[#fff] '
}`}
type="default"
ghost
>
<span className="flex items-center gap-[8px]">
{' '}
<Icon icon="ic:baseline-language" width="14" height="14" />
{langLabel}
</span>
</Button>
</Dropdown> </Dropdown>
); )
}; }
export default memo(LanguageSetting); export default memo(LanguageSetting)
@@ -1,171 +1,199 @@
import { TransferProps, TreeDataNode, Tree, Spin, Input, Empty } from 'antd'
import { TransferProps, TreeDataNode, Tree, Spin, Input, Empty } from "antd"; import { DataNode } from 'antd/es/tree'
import { DataNode } from "antd/es/tree"; import { Ref, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { Ref, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; import { ApartmentOutlined, LoadingOutlined, UserOutlined } from '@ant-design/icons'
import { ApartmentOutlined, LoadingOutlined, UserOutlined } from "@ant-design/icons"; import { ColumnsType } from 'antd/es/table'
import { ColumnsType } from "antd/es/table"; import { $t } from '@common/locales'
import { $t } from "@common/locales"; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
export type TransferTableProps<T> = { export type TransferTableProps<T> = {
request?:(k?:string)=>Promise<{data:T[],success:boolean}> request?: (k?: string) => Promise<{ data: T[]; success: boolean }>
columns: ColumnsType<T> columns: ColumnsType<T>
primaryKey:string primaryKey: string
onSelect:(selectedData:string[])=>void onSelect: (selectedData: string[]) => void
tableType?:'member'|'api' tableType?: 'member' | 'api'
disabledData:string[] disabledData: string[]
searchPlaceholder?:string searchPlaceholder?: string
} }
export type TransferTableHandle<T> = { export type TransferTableHandle<T> = {
selectedRowKeys: () => React.Key[]; selectedRowKeys: () => React.Key[]
} }
interface TreeTransferProps { interface TreeTransferProps {
dataSource: TreeDataNode[]; dataSource: TreeDataNode[]
targetKeys: TransferProps['targetKeys']; targetKeys: TransferProps['targetKeys']
onChange: TransferProps['onChange']; onChange: TransferProps['onChange']
} }
const generateTree = ( const generateTree = (
treeNodes: TreeDataNode[] = [], treeNodes: TreeDataNode[] = [],
checkedKeys: TreeTransferProps['targetKeys'] = [], checkedKeys: TreeTransferProps['targetKeys'] = [],
filterUnchecked: boolean = false, filterUnchecked: boolean = false,
disabledData:string[], disabledData: string[],
filteredItems?:Set<string> filteredItems?: Set<string>
): TreeDataNode[] => { ): TreeDataNode[] => {
const checkedKeysSet = new Set(checkedKeys); const checkedKeysSet = new Set(checkedKeys)
return treeNodes return treeNodes
.map(({ children, ...props }) => { .map(({ children, ...props }) => {
const childNodes = generateTree(children, checkedKeys, filterUnchecked, disabledData, filteredItems); const childNodes = generateTree(children, checkedKeys, filterUnchecked, disabledData, filteredItems)
const isDisabled = (!filterUnchecked && disabledData && disabledData.indexOf(props.id as string) !== -1) const isDisabled =
? true !filterUnchecked && disabledData && disabledData.indexOf(props.id as string) !== -1
: (filterUnchecked ? false : checkedKeysSet.has(props.id as string)); ? true
const hasEnabledChild = childNodes.some(node => !node.disabled); : filterUnchecked
? false
: checkedKeysSet.has(props.id as string)
const hasEnabledChild = childNodes.some((node) => !node.disabled)
return { return {
...props, ...props,
title: <span className="w-full truncate ml-[4px] block">{props.name}</span>, title: <span className="w-full truncate ml-[4px] block">{props.name}</span>,
key: props.id, key: props.id,
disabled: isDisabled && !hasEnabledChild, disabled: isDisabled && !hasEnabledChild,
children: childNodes, children: childNodes
};
})
.filter(node => {
let res:boolean= true
if(filterUnchecked){
res =(!disabledData || disabledData.indexOf(node.key as string) === -1) && (checkedKeysSet.has(node.key as string) || (node.children && node.children.length > 0) )
} }
})
if(filterUnchecked && filteredItems &&((filteredItems.size && !filteredItems.has(node.key as string))&& !(node.children && node.children.length > 0) )){ .filter((node) => {
let res: boolean = true
if (filterUnchecked) {
res =
(!disabledData || disabledData.indexOf(node.key as string) === -1) &&
(checkedKeysSet.has(node.key as string) || (node.children && node.children.length > 0))
}
if (
filterUnchecked &&
filteredItems &&
filteredItems.size &&
!filteredItems.has(node.key as string) &&
!(node.children && node.children.length > 0)
) {
return false return false
} }
return res return res
} })
) }
};
const MemberTransfer = forwardRef<
TransferTableHandle<{ [k: string]: unknown }>,
TransferTableProps<{ [k: string]: unknown }>
>(<T extends { [k: string]: unknown }>(props: TransferTableProps<T>, ref: Ref<TransferTableHandle<T>>) => {
const { request, columns, primaryKey, onSelect, tableType, disabledData = [], searchPlaceholder } = props
const [targetKeys, setTargetKeys] = useState<TreeTransferProps['targetKeys']>([])
const [dataSource, setDataSource] = useState<DataNode[]>([])
const parentRef = useRef<HTMLDivElement>(null)
const [loading, setLoading] = useState<boolean>(false)
const { state } = useGlobalContext()
const [expandedKeys, setExpandedKeys] = useState<string[]>([])
const [searchWord, setSearchWord] = useState<string>('')
useEffect(() => {
setTargetKeys(disabledData)
}, [disabledData])
const MemberTransfer= forwardRef<TransferTableHandle<{[k:string]:unknown}>, TransferTableProps<{[k:string]:unknown}>>( useImperativeHandle(ref, () => ({
<T extends {[k:string]:unknown}>(props: TransferTableProps<T>, ref:Ref<TransferTableHandle<T>>) => { selectedRowKeys: () => targetKeys
const {request,columns,primaryKey,onSelect,tableType,disabledData = [],searchPlaceholder} = props }))
const [targetKeys, setTargetKeys] = useState<TreeTransferProps['targetKeys']>([]);
const [dataSource, setDataSource] = useState<DataNode[] >([])
const parentRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState<boolean>(false)
const {state} = useGlobalContext()
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [searchWord, setSearchWord] = useState<string>('')
useEffect(()=>{
setTargetKeys(disabledData)
},[disabledData])
useImperativeHandle(ref, () =>({ const translatedDataSource = useMemo(() => {
selectedRowKeys: () => targetKeys,}))
const translatedDataSource = useMemo(()=>{
const loop = (data: DataNode[]): DataNode[] => const loop = (data: DataNode[]): DataNode[] =>
data?.map((item) => { data?.map((item) => {
const strTitle:string = item.name === '所有成员' ? $t(item.name) as string : item.name as string; const strTitle: string = item.name === '所有成员' ? ($t(item.name) as string) : (item.name as string)
const index = strTitle.indexOf(searchWord); const index = strTitle.indexOf(searchWord)
const beforeStr = strTitle.substring(0, index); const beforeStr = strTitle.substring(0, index)
const afterStr = strTitle.slice(index + searchWord.length); const afterStr = strTitle.slice(index + searchWord.length)
const title = const title =
index > -1 ? ( index > -1 ? (
<span className='w-[calc(100%-16px)] truncate' title={strTitle}> <span className="w-[calc(100%-16px)] truncate" title={strTitle}>
{beforeStr} {beforeStr}
<span className="text-theme">{searchWord}</span> <span className="text-theme">{searchWord}</span>
{afterStr} {afterStr}
</span> </span>
) : ( ) : (
<span className='w-[calc(100%-16px)] truncate' title={`${strTitle}`}>{strTitle}</span> <span className="w-[calc(100%-16px)] truncate" title={`${strTitle}`}>
) {strTitle}
if (item.children) { </span>
return { )
...item, if (item.children) {
title,
disableCheckbox:disabledData.indexOf(item.key as string) !== -1,
icon:<ApartmentOutlined />,
children: loop(item.children as T[]) };
}
return { return {
...item, ...item,
title, title,
icon:<UserOutlined />, disableCheckbox: disabledData.indexOf(item.key as string) !== -1,
isLeaf:true, icon: <ApartmentOutlined />,
disableCheckbox:disabledData.indexOf(item.key as string) !== -1 children: loop(item.children as T[])
}; }
}); }
return loop(dataSource);
},[dataSource, state.language, searchWord])
return {
...item,
title,
icon: <UserOutlined />,
isLeaf: true,
disableCheckbox: disabledData.indexOf(item.key as string) !== -1
}
})
return loop(dataSource)
}, [dataSource, state.language, searchWord])
const getInitExpandKeys = (data:T[], expandKeys:string[] = [])=>{ const getInitExpandKeys = (data: T[], expandKeys: string[] = []) => {
data.forEach((item)=>{ data.forEach((item) => {
if(item.children?.length){ if (item.children?.length) {
expandKeys.push(item.key as string) expandKeys.push(item.key as string)
getInitExpandKeys(item.children,expandKeys) getInitExpandKeys(item.children, expandKeys)
} }
}) })
return expandKeys return expandKeys
} }
const getDataSource = ()=>{
setLoading(true)
request && request().then((res)=>{
const {data,success} = res
setDataSource(success? data : [])
setExpandedKeys(getInitExpandKeys(success? data:[]))
}).finally(()=>{setLoading(false)})
}
const getDataSource = () => {
setLoading(true)
request &&
request()
.then((res) => {
const { data, success } = res
setDataSource(success ? data : [])
setExpandedKeys(getInitExpandKeys(success ? data : []))
})
.finally(() => {
setLoading(false)
})
}
useEffect(() => { useEffect(() => {
getDataSource() getDataSource()
}, []); }, [])
return ( return (
<div ref={parentRef}> <div ref={parentRef}>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className=''> <Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={loading} className="">
<Input className="mb-[10px]" placeholder={searchPlaceholder} onChange={(e)=>setSearchWord(e.target.value)} value={searchWord} /> <Input
<>{ translatedDataSource && translatedDataSource.length > 0 ? <Tree className="mb-[10px]"
checkable placeholder={searchPlaceholder}
expandedKeys={expandedKeys} onChange={(e) => setSearchWord(e.target.value)}
checkedKeys={targetKeys} value={searchWord}
selectable={false} />
onCheck={(e)=>{setTargetKeys(e); <>
onSelect(((e as string[])?.filter(x=>disabledData.indexOf(x as string) === -1))||[])}} {translatedDataSource && translatedDataSource.length > 0 ? (
onExpand={setExpandedKeys} <Tree
treeData={translatedDataSource} checkable
blockNode expandedKeys={expandedKeys}
showIcon checkedKeys={targetKeys}
/> selectable={false}
: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/> }</> onCheck={(e) => {
</Spin> setTargetKeys(e)
</div> onSelect((e as string[])?.filter((x) => disabledData.indexOf(x as string) === -1) || [])
); }}
onExpand={setExpandedKeys}
treeData={translatedDataSource}
blockNode
showIcon
/>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</>
</Spin>
</div>
)
}) })
export default MemberTransfer; export default MemberTransfer
@@ -1,22 +1,22 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react'
import { Result, Skeleton } from 'antd'; import { Result, Skeleton } from 'antd'
const NotFound: React.FC = () => { const NotFound: React.FC = () => {
const [showPage, setShowPage] = useState<boolean>(false) const [showPage, setShowPage] = useState<boolean>(false)
useEffect(()=>{ useEffect(() => {
setTimeout(()=>setShowPage(true), 1000) setTimeout(() => setShowPage(true), 1000)
},[]) }, [])
return ( return (
<div className={`h-full w-full flex flex-1 align-middle ${showPage ? 'items-center' : ''}`}> <div className={`h-full w-full flex flex-1 align-middle ${showPage ? 'items-center' : ''}`}>
{ showPage ? <Result {showPage ? (
className='w-full' <Result className="w-full" status="404" title="404" subTitle="Sorry, the page you visited does not exist." />
status="404" ) : (
title="404" <Skeleton active />
subTitle="Sorry, the page you visited does not exist." )}
/> : <Skeleton active /> } </div>
</div> )
)} }
export default NotFound; export default NotFound
@@ -1,243 +1,370 @@
import { SearchOutlined } from '@ant-design/icons'
import {Button, Dropdown, Input, MenuProps, TablePaginationConfig} from 'antd'; import type { ActionType, ParamsType, ProColumns, ProTableProps } from '@ant-design/pro-components'
import {ChangeEvent, RefAttributes, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import { DragSortTable, ProTable } from '@ant-design/pro-components'
import type {ActionType, ParamsType, ProColumns, ProTableProps} from '@ant-design/pro-components'; import WithPermission from '@common/components/aoplatform/WithPermission'
import { import { PERMISSION_DEFINITION } from '@common/const/permissions'
DragSortTable, import { withMinimumDelay } from '@common/utils/ux'
ProTable, import { Button, Dropdown, Input, MenuProps, TablePaginationConfig } from 'antd'
} from '@ant-design/pro-components'; import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface'
import './PageList.module.css'
import {SearchOutlined} from "@ant-design/icons";
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import WithPermission from '@common/components/aoplatform/WithPermission'; import {
import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface'; ChangeEvent,
import { useGlobalContext } from '../../contexts/GlobalStateContext'; RefAttributes,
import { PERMISSION_DEFINITION } from '@common/const/permissions'; forwardRef,
import { withMinimumDelay } from '@common/utils/ux'; useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react'
import { useGlobalContext } from '../../contexts/GlobalStateContext'
import './PageList.module.css'
export type PageProColumns<T = any, ValueType = 'text'> = ProColumns<T , ValueType> & {btnNums? : number} export type PageProColumns<T = any, ValueType = 'text'> = ProColumns<T, ValueType> & { btnNums?: number }
interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<ActionType> { interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<ActionType> {
id?:string id?: string
columns: PageProColumns<T,'text'>[] columns: PageProColumns<T, 'text'>[]
request?:(params: (ParamsType & {pageSize?: number | undefined, current?: number | undefined, keyword?: string | undefined}), sorter: unknown, filter: unknown)=>Promise<{data:T[], success:boolean}> request?: (
dropMenu?:MenuProps params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined },
searchPlaceholder?:string sorter: unknown,
showPagination?:boolean filter: unknown
primaryKey?:string ) => Promise<{ data: T[]; success: boolean }>
addNewBtnTitle?:string dropMenu?: MenuProps
addNewBtnAccess?:string searchPlaceholder?: string
tableClickAccess?:string showPagination?: boolean
onAddNewBtnClick?:()=>void primaryKey?: string
beforeSearchNode?:React.ReactNode[] addNewBtnTitle?: string
onSearchWordChange?:(e:ChangeEvent<HTMLInputElement>) => void addNewBtnAccess?: string
afterNewBtn?:React.ReactNode[] tableClickAccess?: string
dragSortKey?:string onAddNewBtnClick?: () => void
onDragSortEnd?:(beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise<void> beforeSearchNode?: React.ReactNode[]
tableTitle?:string onSearchWordChange?: (e: ChangeEvent<HTMLInputElement>) => void
dataSource?:T[] afterNewBtn?: React.ReactNode[]
onRowClick?:(record:T)=>void dragSortKey?: string
showColSetting?:boolean onDragSortEnd?: (beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise<void>
minVirtualHeight?:number tableTitle?: string
besidesTableHeight?:number dataSource?: T[]
noTop?:boolean onRowClick?: (record: T) => void
tableClass?:string showColSetting?: boolean
tableTitleClass?:string minVirtualHeight?: number
addNewBtnWrapperClass?:string besidesTableHeight?: number
delayLoading?:boolean noTop?: boolean
noScroll?:boolean tableClass?: string
tableTitleClass?: string
addNewBtnWrapperClass?: string
delayLoading?: boolean
noScroll?: boolean
/* 前端分页的表格,需要传入该字段以支持后端搜索 */ /* 前端分页的表格,需要传入该字段以支持后端搜索 */
manualReloadTable?:()=>void manualReloadTable?: () => void
} }
const PageList = <T extends Record<string, unknown>>(
props: React.PropsWithChildren<PageListProps<T>>,
const PageList = <T extends Record<string, unknown>>(props: React.PropsWithChildren<PageListProps<T>>,ref: React.Ref<ActionType>) => { ref: React.Ref<ActionType>
const {id,columns,request,dropMenu,searchPlaceholder,showPagination=true,primaryKey='id',addNewBtnTitle,addNewBtnAccess,tableClickAccess,tableClass,onAddNewBtnClick,beforeSearchNode,onSearchWordChange,manualReloadTable,afterNewBtn,dragSortKey,onDragSortEnd,tableTitle,rowSelection,onChange,dataSource,onRowClick,showColSetting=false,minVirtualHeight,noTop,addNewBtnWrapperClass = '',tableTitleClass,delayLoading = true,besidesTableHeight, noScroll} = props ) => {
const parentRef = useRef<HTMLDivElement>(null); const {
const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight); id,
const [tableWidth, setTableWidth] = useState<number|undefined>(undefined); columns,
const actionRef = useRef<ActionType>(); request,
const [allowTableClick,setAllowTableClick] = useState<boolean>(false) dropMenu,
const {accessData,checkPermission,accessInit,state} = useGlobalContext() searchPlaceholder,
showPagination = true,
primaryKey = 'id',
addNewBtnTitle,
addNewBtnAccess,
tableClickAccess,
tableClass,
onAddNewBtnClick,
beforeSearchNode,
onSearchWordChange,
manualReloadTable,
afterNewBtn,
dragSortKey,
onDragSortEnd,
tableTitle,
rowSelection,
onChange,
dataSource,
onRowClick,
showColSetting = false,
minVirtualHeight,
noTop,
addNewBtnWrapperClass = '',
tableTitleClass,
delayLoading = true,
besidesTableHeight,
noScroll
} = props
const parentRef = useRef<HTMLDivElement>(null)
const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight)
const [tableWidth, setTableWidth] = useState<number | undefined>(undefined)
const actionRef = useRef<ActionType>()
const [allowTableClick, setAllowTableClick] = useState<boolean>(false)
const { accessData, checkPermission, accessInit, state } = useGlobalContext()
const [minTableWidth, setMinTableWidth] = useState<number>(0) const [minTableWidth, setMinTableWidth] = useState<number>(0)
// 使用useImperativeHandle来自定义暴露给父组件的实例值 useImperativeHandle(ref, () => actionRef.current!)
useImperativeHandle(ref, () => actionRef.current!);
useEffect(()=>{ useEffect(() => {
actionRef?.current?.reload?.() actionRef?.current?.reload?.()
},[state.language]) }, [state.language])
const lastAccess = useMemo(()=>{
if(!tableClickAccess) return true
return checkPermission(tableClickAccess as keyof typeof PERMISSION_DEFINITION[0])
},[allowTableClick, accessData,accessInit])
useEffect(()=>{ const lastAccess = useMemo(() => {
tableClickAccess ? setAllowTableClick(lastAccess) : setAllowTableClick(true) if (!tableClickAccess) return true
},[accessData]) return checkPermission(tableClickAccess as keyof (typeof PERMISSION_DEFINITION)[0])
}, [allowTableClick, accessData, accessInit])
const resizeObserverRef = useRef<ResizeObserver |null >(null);
useEffect(() => {
tableClickAccess ? setAllowTableClick(lastAccess) : setAllowTableClick(true)
}, [accessData])
const resizeObserverRef = useRef<ResizeObserver | null>(null)
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
if (parentRef.current && !noScroll) { if (parentRef.current && !noScroll) {
const res = parentRef.current.getBoundingClientRect(); const res = parentRef.current.getBoundingClientRect()
const height = res.height - ((noTop ? 0 : 59) + 54 + (showPagination && !dragSortKey ? 52 : 0) +( besidesTableHeight ?? 0) + 1); // 减去顶部按钮、底部分页、表头高度 const height =
setTableWidth(minTableWidth - 5> res.width ? minTableWidth : undefined); res.height -
height && setTableHeight(minVirtualHeight === undefined ? height : (height > minVirtualHeight ? height : minVirtualHeight)); ((noTop ? 0 : 59) + 54 + (showPagination && !dragSortKey ? 52 : 0) + (besidesTableHeight ?? 0) + 1) // 减去顶部按钮、底部分页、表头高度
setTableWidth(minTableWidth - 5 > res.width ? minTableWidth : undefined)
height &&
setTableHeight(
minVirtualHeight === undefined ? height : height > minVirtualHeight ? height : minVirtualHeight
)
} }
}; }
const debouncedHandleResize = debounce(handleResize, 200); const debouncedHandleResize = debounce(handleResize, 200)
if (!resizeObserverRef.current && !noScroll) { if (!resizeObserverRef.current && !noScroll) {
// 创建一个 ResizeObserver 来监听高度变化,只创建一次 // 创建一个 ResizeObserver 来监听高度变化,只创建一次
resizeObserverRef.current = new ResizeObserver(debouncedHandleResize); resizeObserverRef.current = new ResizeObserver(debouncedHandleResize)
// 开始监听 // 开始监听
if (parentRef.current && !minVirtualHeight) { if (parentRef.current && !minVirtualHeight) {
resizeObserverRef.current.observe(parentRef.current); resizeObserverRef.current.observe(parentRef.current)
} }
} }
// 在 minTableWidth 变化时手动触发 handleResize // 在 minTableWidth 变化时手动触发 handleResize
handleResize(); handleResize()
// 清理函数 // 清理函数
return () => { return () => {
if (resizeObserverRef.current) { if (resizeObserverRef.current) {
resizeObserverRef.current.disconnect(); resizeObserverRef.current.disconnect()
resizeObserverRef.current = null; resizeObserverRef.current = null
} }
}; }
}, [minTableWidth, parentRef, noTop, showPagination, dragSortKey, minVirtualHeight]); // 将相关依赖项作为 useEffect 的依赖项 }, [minTableWidth, parentRef, noTop, showPagination, dragSortKey, minVirtualHeight]) // 将相关依赖项作为 useEffect 的依赖项
const newColumns = useMemo(() => {
let width: number = 0
const res = columns?.map((x, index) => {
const newColumns = useMemo(()=>{ const sorter = localStorage.getItem(`${id}_sorter`)
let width:number = 0 const filters = localStorage.getItem(`${id}_filters`)
const res = columns?.map( x.copyable = x.copyable ?? (index === 0 || x.dataIndex === 'id' || x.dataIndex === 'email')
(x, index)=>{ if (sorter && x.sorter) {
const sorter = localStorage.getItem(`${id}_sorter`) const sorterObj = JSON.parse(sorter)
const filters = localStorage.getItem(`${id}_filters`) const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(',') : x.dataIndex
x.copyable = x.copyable ?? (index === 0 || x.dataIndex === 'id' || x.dataIndex === 'email') x.defaultSortOrder = sorterObj?.columnKey === xName ? sorterObj?.order : undefined
if(sorter && x.sorter){ // x.showSorterTooltip = {target:'sorter-icon'}
const sorterObj = JSON.parse(sorter) }
const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(','):x.dataIndex if (filters && x.filters) {
x.defaultSortOrder = sorterObj?.columnKey === xName ? sorterObj?.order : undefined const filtersObj = JSON.parse(filters)
// x.showSorterTooltip = {target:'sorter-icon'} const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(',') : x.dataIndex
} x.defaultFilteredValue = filtersObj?.[xName as string]
if(filters && x.filters){ }
const filtersObj = JSON.parse(filters) if ((index === columns.length - 1 || x.key === 'option') && x.btnNums) {
const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(','):x.dataIndex const optionWidth = 24 + 18 * x.btnNums + (x.btnNums - 1) * 21
x.defaultFilteredValue = filtersObj?.[xName as string] x.width = Math.max(optionWidth, 54)
} }
if((index === columns.length -1 || x.key === 'option') && x.btnNums){ width += Number(x.width ?? (x.filters || x.sorter ? 120 : 100))
const optionWidth = 24 + 18 * x.btnNums + (x.btnNums - 1) * 21 return x
x.width = Math.max(optionWidth, 54) })
} setMinTableWidth(width)
width += Number(x.width ?? ((x.filters || x.sorter) ? 120 : 100))
return x})
setMinTableWidth(width)
return res return res
},[columns]) }, [columns])
const headerTitle = ()=>{ const headerTitle = () => {
return ( return (
<>{ <>
tableTitle ? <span className={`text-[30px] leading-[42px] my-mbase pl-[20px] ${tableTitleClass}`}>{tableTitle}</span> : ( {tableTitle ? (
addNewBtnTitle ? <WithPermission access={addNewBtnAccess} ><Button type="primary" className={`mr-btnrbase my-btnbase ${addNewBtnWrapperClass}`} onClick={onAddNewBtnClick}>{addNewBtnTitle}</Button></WithPermission> : undefined <span className={`text-[30px] leading-[42px] my-mbase pl-[20px] ${tableTitleClass}`}>{tableTitle}</span>
) ) : addNewBtnTitle ? (
<WithPermission access={addNewBtnAccess}>
} <Button
{afterNewBtn ? afterNewBtn as React.ReactNode[] :undefined} type="primary"
</> className={`mr-btnrbase my-btnbase ${addNewBtnWrapperClass}`}
) onClick={onAddNewBtnClick}
>
{addNewBtnTitle}
</Button>
</WithPermission>
) : undefined}
{afterNewBtn ? (afterNewBtn as React.ReactNode[]) : undefined}
</>
)
} }
const requestWithDelay = (params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined;}, sort: unknown, filter: unknown) => { const getTableActions = () => {
return withMinimumDelay(() => request!(params, sort, filter), delayLoading === false? 0 : undefined); return [
}; ...(beforeSearchNode ? [beforeSearchNode] : []),
...(searchPlaceholder
? [
<Input
key="search-input"
className="my-btnbase ml-btnbase"
onChange={onSearchWordChange ? (e) => debounce(onSearchWordChange, 100)(e) : undefined}
onPressEnter={() => {
if (manualReloadTable) {
manualReloadTable()
return
}
if (actionRef.current) {
actionRef.current.reset?.()
actionRef.current.reload?.()
}
}}
allowClear
placeholder={searchPlaceholder}
prefix={
<SearchOutlined
className="cursor-pointer"
onClick={() => {
if (actionRef.current) {
actionRef.current.reset?.()
actionRef.current.reload?.()
}
}}
/>
}
/>
]
: [])
]
}
const requestWithDelay = (
params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined },
sort: unknown,
filter: unknown
) => {
return withMinimumDelay(() => request!(params, sort, filter), delayLoading === false ? 0 : undefined)
}
return ( return (
<div ref={parentRef} className={`eo_page_list bg-MAIN_BG ${dragSortKey ? 'eo_page_drag':''} ${tableClass ?? ''}`}style={{ height: '100%' }}> <div
{dragSortKey? <DragSortTable<T> ref={parentRef}
className={`eo_page_list bg-MAIN_BG ${dragSortKey ? 'eo_page_drag' : ''} ${tableClass ?? ''}`}
style={{ height: '100%' }}
>
{dragSortKey ? (
<DragSortTable<T>
actionRef={actionRef} actionRef={actionRef}
columns={newColumns} columns={newColumns}
rowKey={primaryKey} rowKey={primaryKey}
search={false} search={false}
pagination={false} pagination={
showPagination
? {
showSizeChanger: true,
showQuickJumper: true,
size: 'default'
}
: false
}
request={request} request={request}
dragSortKey={dragSortKey} dragSortKey={dragSortKey}
onDragSortEnd={onDragSortEnd} onDragSortEnd={onDragSortEnd}
scroll={noScroll ? undefined :{ y: tableHeight }} scroll={noScroll ? undefined : { y: tableHeight }}
options={{ options={{
reload: false, reload: false,
density: false, density: false,
setting: false, setting: false
}} }}
headerTitle={ toolbar={{
headerTitle() actions: getTableActions()
} }}
/> : <ProTable<T> headerTitle={headerTitle()}
/>
) : (
<ProTable<T>
actionRef={actionRef} actionRef={actionRef}
columns={newColumns} columns={newColumns}
virtual virtual
scroll={noScroll ? undefined : {x:tableWidth,y: tableHeight }} scroll={noScroll ? undefined : { x: tableWidth, y: tableHeight }}
size="middle" size="middle"
rowSelection={rowSelection} rowSelection={rowSelection}
tableAlertRender={false} tableAlertRender={false}
tableAlertOptionRender={false} tableAlertOptionRender={false}
request={request ? requestWithDelay : undefined} request={request ? requestWithDelay : undefined}
toolBarRender={() => [ toolBarRender={() => [
dropMenu ? (<Dropdown dropMenu ? (
key="menu" <Dropdown key="menu" menu={dropMenu}>
menu={dropMenu} <Button></Button>
> </Dropdown>
<Button> ) : null
</Button>
</Dropdown>):null,
]} ]}
toolbar={{ toolbar={{
actions:[...[beforeSearchNode],...[searchPlaceholder?<Input className="my-btnbase ml-btnbase" onChange={ onSearchWordChange ? (e) => debounce(onSearchWordChange, 100)(e) : undefined } onPressEnter={()=>manualReloadTable ? manualReloadTable():actionRef.current?.reload?.()} allowClear placeholder={searchPlaceholder} prefix={<SearchOutlined className="cursor-pointer" onClick={()=>{actionRef.current?.reload?.()}}/>}/>:null]], actions: getTableActions()
}} }}
options={{ options={{
reload: false, reload: false,
density: false, density: false,
setting: showColSetting ? { setting: showColSetting
draggable:false, ? {
showListItemOption:false draggable: false,
} :false, showListItemOption: false
}
: false
}} }}
showSorterTooltip={false} showSorterTooltip={false}
columnsState={{persistenceType:'localStorage',persistenceKey:id}} columnsState={{ persistenceType: 'localStorage', persistenceKey: id }}
pagination={showPagination ? { pagination={
showSizeChanger: true, showPagination
showQuickJumper: true, ? {
size:'default' showSizeChanger: true,
}:false} showQuickJumper: true,
size: 'default'
}
: false
}
onChange={(
pagination: TablePaginationConfig,
filters: Record<string, FilterValue | null>,
sorter: SorterResult<T> | SorterResult<T>[],
extra: TableCurrentDataSource<T>
) => {
localStorage.setItem(`${id}_filters`, JSON.stringify(filters))
!Array.isArray(sorter) &&
localStorage.setItem(
`${id}_sorter`,
JSON.stringify({ columnKey: sorter?.columnKey, order: sorter?.order })
)
onChange?.(pagination, filters, sorter, extra)
}}
rowKey={primaryKey} rowKey={primaryKey}
onChange={(pagination: TablePaginationConfig, filters: Record<string, FilterValue | null>, sorter: SorterResult<T> | SorterResult<T>[],extra:TableCurrentDataSource<T>) =>{
localStorage.setItem(`${id}_filters`,JSON.stringify(filters))
!Array.isArray(sorter) && localStorage.setItem(`${id}_sorter`,JSON.stringify({columnKey:sorter?.columnKey, order: sorter?.order}))
onChange?.(pagination,filters,sorter,extra)}}
dataSource={dataSource} dataSource={dataSource}
search={false} search={false}
headerTitle={ headerTitle={headerTitle()}
headerTitle() onRow={
onRowClick && allowTableClick
? (record) => ({
onClick: () => {
onRowClick(record)
}
})
: undefined
} }
onRow={onRowClick && allowTableClick ? (record) => ({ rowClassName={() => (onRowClick && allowTableClick ? 'cursor-pointer' : '')}
onClick: () => { />
onRowClick(record); )}
}
}):undefined}
rowClassName={()=>onRowClick && allowTableClick ?"cursor-pointer":''}
/>}
</div> </div>
); )
}; }
export default forwardRef(PageList) as <T extends Record<string,unknown>>(props: React.PropsWithChildren<PageListProps<T>> & { ref?: React.Ref<ActionType> }) => ReturnType<typeof PageList>; export default forwardRef(PageList) as <T extends Record<string, unknown>>(
props: React.PropsWithChildren<PageListProps<T>> & { ref?: React.Ref<ActionType> }
) => ReturnType<typeof PageList>
@@ -1,95 +1,100 @@
import {App, Form, Input, Row, Table} from "antd"; import { App, Form, Input, Row, Table } from 'antd'
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react"; import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'
import {useFetch} from "@common/hooks/http.ts"; import { useFetch } from '@common/hooks/http.ts'
import {BasicResponse, PLACEHOLDER, PolicyPublishColumns, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; import { BasicResponse, PLACEHOLDER, PolicyPublishColumns, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { $t } from "@common/locales"; import { $t } from '@common/locales'
import { useGlobalContext } from "@common/contexts/GlobalStateContext"; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { PolicyPublishModalHandle, PolicyPublishModalProps } from "@common/const/type"; import { PolicyPublishModalHandle, PolicyPublishModalProps } from '@common/const/type'
export const PolicyPublishModalContent = forwardRef<PolicyPublishModalHandle, PolicyPublishModalProps>((props, ref) => {
const { message } = App.useApp()
const { data } = props
const [form] = Form.useForm()
const { fetchData } = useFetch()
const { state } = useGlobalContext()
export const PolicyPublishModalContent = forwardRef<PolicyPublishModalHandle,PolicyPublishModalProps>((props, ref) => { const publish: () => Promise<boolean | string | Record<string, unknown>> = () => {
const { message } = App.useApp() return new Promise((resolve, reject) => {
const { data} = props form
const [form] = Form.useForm(); .validateFields()
const {fetchData} = useFetch() .then((value) => {
const {state} = useGlobalContext() const body = { ...value, source: data.source }
fetchData<BasicResponse<null>>('strategy/global/data-masking/publish', {
const publish:()=>Promise<boolean | string | Record<string, unknown>> = ()=>{ method: 'POST',
return new Promise((resolve, reject)=>{ eoBody: body,
form.validateFields().then((value)=>{ eoTransformKeys: ['versionName']
const body = {...value, source:data.source} })
fetchData<BasicResponse<null>>('strategy/global/data-masking/publish',{method: 'POST',eoBody:body,eoTransformKeys:['versionName']}).then(response=>{ .then((response) => {
const {code,msg} = response const { code, msg } = response
if(code === STATUS_CODE.SUCCESS){ if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success)) message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response) resolve(response)
}else{ } else {
message.error(msg || $t(RESPONSE_TIPS.error)) message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error)) reject(msg || $t(RESPONSE_TIPS.error))
} }
}).catch((errorInfo)=> reject(errorInfo)) })
}).catch((errorInfo)=> reject(errorInfo)) .catch((errorInfo) => reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
publish,
}) })
) .catch((errorInfo) => reject(errorInfo))
})
}
useEffect(()=>{ useImperativeHandle(ref, () => ({
form.setFieldsValue(data) publish
},[data]) }))
const translatedPolicyColumns = useMemo(()=>PolicyPublishColumns.map((x)=>({ useEffect(() => {
...x, form.setFieldsValue(data)
title: typeof x.title === 'string' ? $t(x.title) : x.title, }, [data])
})),[state.language])
const translatedPolicyColumns = useMemo(
() =>
PolicyPublishColumns.map((x) => ({
...x,
title: typeof x.title === 'string' ? $t(x.title) : x.title
})),
[state.language]
)
return ( return (
<> <>
<WithPermission access=""><Form <WithPermission access="">
className=" mx-auto" <Form
form={form} className=" mx-auto"
labelAlign='left' form={form}
layout='vertical' labelAlign="left"
scrollToFirstError layout="vertical"
name="publishApprovalModalContent" scrollToFirstError
// labelCol={{span: 3}} name="publishApprovalModalContent"
// wrapperCol={{span: 21}} // labelCol={{span: 3}}
autoComplete="off" // wrapperCol={{span: 21}}
> autoComplete="off"
>
<Form.Item label={$t('发布名称')} name="versionName" rules={[{ required: true, whitespace: true }]}>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
<Form.Item <Form.Item label={$t('描述')} name="desc">
label={$t("发布名称")} <Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
name='versionName' </Form.Item>
rules={[{required: true,whitespace:true }]} <Row className="mt-mbase pb-[8px] h-[32px] font-bold">
> <span>{$t('策略列表')}</span>
<Input className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} /> </Row>
</Form.Item> <Row className="mb-mbase ">
<Table
<Form.Item columns={translatedPolicyColumns}
label={$t("描述")} bordered={true}
name="desc" rowKey="name"
> size="small"
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} /> dataSource={data.strategies || []}
</Form.Item> pagination={false}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('策略列表')}</span></Row> />
<Row className="mb-mbase "> {!data?.isPublish && data?.unpublishMsg && <p className="text-status_fail mt-[4px]">{data.unpublishMsg}</p>}
<Table </Row>
columns={translatedPolicyColumns} </Form>
bordered={true} </WithPermission>
rowKey="name" </>
size="small" )
dataSource={data.strategies || []} })
pagination={false}
/>
{!data?.isPublish&& data?.unpublishMsg&& <p className="text-status_fail mt-[4px]">{data.unpublishMsg}</p>}
</Row>
</Form>
</WithPermission>
</>)
})
@@ -1,266 +1,402 @@
import {App, Col, Form, Input, Row, Table, Tooltip} from "antd"; import { App, Col, Form, Input, Row, Table, Tooltip } from 'antd'
import {forwardRef, useEffect, useImperativeHandle, useMemo} from "react"; import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'
import {PublishApprovalInfoType, PublishApprovalModalHandle, PublishApprovalModalProps, PublishVersionTableListItem} from "@common/const/approval/type.tsx"; import {
import {useFetch} from "@common/hooks/http.ts"; PublishApprovalInfoType,
import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE, STATUS_COLOR} from "@common/const/const.tsx"; PublishApprovalModalHandle,
import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; PublishApprovalModalProps,
import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "@core/const/system/const.tsx"; PublishVersionTableListItem
import { $t } from "@common/locales"; } from '@common/const/approval/type.tsx'
import { ApprovalPolicyColumns, ApprovalRouteColumns, ApprovalStatusColorClass, ApprovalUpstreamColumns, ChangeTypeEnum } from "@common/const/approval/const"; import { useFetch } from '@common/hooks/http.ts'
import { useGlobalContext } from "@common/contexts/GlobalStateContext"; import {
import { LoadingOutlined } from "@ant-design/icons"; BasicResponse,
import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/SystemInsidePublishOnline"; FORM_ERROR_TIPS,
PLACEHOLDER,
RESPONSE_TIPS,
STATUS_CODE,
STATUS_COLOR
} from '@common/const/const.tsx'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from '@core/const/system/const.tsx'
import { $t } from '@common/locales'
import {
ApprovalPolicyColumns,
ApprovalRouteColumns,
ApprovalStatusColorClass,
ApprovalUpstreamColumns,
ChangeTypeEnum
} from '@common/const/approval/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { LoadingOutlined } from '@ant-design/icons'
import { SystemInsidePublishOnlineItems } from '@core/pages/system/publish/SystemInsidePublishOnline'
export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle, PublishApprovalModalProps>(
export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle,PublishApprovalModalProps>((props, ref) => { (props, ref) => {
const { message } = App.useApp() const { message } = App.useApp()
const { type,data,insidePage = false, serviceType = 'rest', serviceId, teamId} = props const { type, data, insidePage = false, serviceType = 'rest', serviceId, teamId } = props
const [form] = Form.useForm(); const [form] = Form.useForm()
const {fetchData} = useFetch() const { fetchData } = useFetch()
const {state} = useGlobalContext() const { state } = useGlobalContext()
const save:(operate:'pass'|'refuse')=>Promise<boolean | string> = (operate)=>{ const save: (operate: 'pass' | 'refuse') => Promise<boolean | string> = (operate) => {
if(type === 'view'){ if (type === 'view') {
return Promise.resolve(true)
}
return form
.validateFields()
.then((value) => {
if (operate === 'refuse' && form.getFieldValue('opinion') === '') {
form.setFields([
{
name: 'opinion',
errors: [$t(FORM_ERROR_TIPS.refuseOpinion)]
}
])
form.scrollToField('opinion')
return Promise.reject($t(RESPONSE_TIPS.refuseOpinion))
}
return fetchData<BasicResponse<null>>(`service/publish/${operate === 'pass' ? 'accept' : 'refuse'}`, {
method: 'PUT',
eoBody: { comments: value.opinion },
eoParams: { id: data!.id, project: serviceId },
eoTransformKeys: ['versionRemark']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true) return Promise.resolve(true)
} } else {
return form.validateFields().then((value)=>{ message.error(msg || $t(RESPONSE_TIPS.error))
if(operate === 'refuse' && form.getFieldValue('opinion') === '' ){ return Promise.reject(msg || $t(RESPONSE_TIPS.error))
form.setFields([{ }
name:'opinion',errors:[$t(FORM_ERROR_TIPS.refuseOpinion)] })
}]) .catch((errorInfo) => Promise.reject(errorInfo))
form.scrollToField('opinion')
return Promise.reject($t(RESPONSE_TIPS.refuseOpinion))
}
return fetchData<BasicResponse<null>>(`service/publish/${operate === 'pass' ? 'accept' : 'refuse'}`,{method: 'PUT',eoBody:({comments:value.opinion}), eoParams:{id:data!.id, project:serviceId},eoTransformKeys:['versionRemark']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> Promise.reject(errorInfo))
}).catch((err)=> {form.scrollToField(err.errorFields[0].name[0]); return Promise.reject(err)})
}
const publish:(notSave?:boolean)=>Promise<boolean | string | Record<string, unknown>> = (notSave)=>{
return new Promise((resolve, reject)=>{
form.validateFields().then((value)=>{
const body = {...value, ...(type === 'publish'&&{release:data.id})}
fetchData<BasicResponse<null>>(
notSave ? 'service/publish/apply' : 'service/publish/release/do',{method: 'POST',eoBody:body, eoParams:{service:serviceId, team:teamId},eoTransformKeys:['versionRemark']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
const online:()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
form.validateFields().then(()=>{
fetchData<BasicResponse<null>>('service/publish/execute',{method: 'PUT', eoParams:{project:serviceId,id:(data as PublishVersionTableListItem).flowId},eoTransformKeys:['versionRemark']}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo)=> reject(errorInfo))
})
}
useImperativeHandle(ref, ()=>({
save,
publish,
online
}) })
.catch((err) => {
form.scrollToField(err.errorFields[0].name[0])
return Promise.reject(err)
})
}
const publish: (notSave?: boolean) => Promise<boolean | string | Record<string, unknown>> = (notSave) => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then((value) => {
const body = { ...value, ...(type === 'publish' && { release: data.id }) }
fetchData<BasicResponse<null>>(notSave ? 'service/publish/apply' : 'service/publish/release/do', {
method: 'POST',
eoBody: body,
eoParams: { service: serviceId, team: teamId },
eoTransformKeys: ['versionRemark']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(response)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
}
const online: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then(() => {
fetchData<BasicResponse<null>>('service/publish/execute', {
method: 'PUT',
eoParams: { project: serviceId, id: (data as PublishVersionTableListItem).flowId },
eoTransformKeys: ['versionRemark']
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
}
useImperativeHandle(ref, () => ({
save,
publish,
online
}))
useEffect(() => {
form.setFieldsValue({ opinion: '', ...data })
}, [])
const translatedUpstreamColumns = useMemo(
() =>
ApprovalUpstreamColumns.map((x) => ({
...x,
...(x.dataIndex === 'type'
? {
valueEnum: {
static: {
text: $t('静态上游')
}
}
}
: {}),
...(x.dataIndex === 'change'
? {
render: (_, entity) => (
<Tooltip
placement="top"
title={
entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''
}
>
<span
className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}
>
{$t(ChangeTypeEnum[entity.change as keyof typeof ChangeTypeEnum] || '-')}
{entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''}
</span>
</Tooltip>
)
}
: {}),
title: typeof x.title === 'string' ? $t(x.title) : x.title
})),
[state.language]
) )
useEffect(()=>{ const translatedRouteColumns = useMemo(
form.setFieldsValue({ opinion:'',...data}) () =>
},[]) ApprovalRouteColumns.filter((x) =>
serviceType === 'rest' ? x.dataIndex !== 'name' : x.dataIndex !== 'methods'
const translatedUpstreamColumns = useMemo(()=>ApprovalUpstreamColumns.map((x)=>({ ).map((x) => ({
...x, ...x,
...(x.dataIndex === 'type' ? {valueEnum:{ ...(x.dataIndex === 'change'
'static':{ ? {
text:$t('静态上游') render: (_, entity) => (
} <Tooltip
}}:{}), placement="top"
...(x.dataIndex === 'change' ? { title={
render:(_,entity)=>( entity.change === 'error'
<Tooltip placement="top" title={entity.change === 'error' ? $t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}> ? $t('该 API 缺失(0)(1)(2)请先补充', [
<span className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}>{$t(ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-')} entity.proxyStatus == 1 && $t('转发信息,'),
{entity.change === 'error' ?$t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}</span> entity.docStatus == 1 && $t('文档信息,'),
</Tooltip>) entity.upstreamStatus == 1 && $t('上游信息,')
}:{}), ])
title: typeof x.title === 'string' ? $t(x.title) : x.title, : ''
})),[state.language])
const translatedRouteColumns = useMemo(()=>ApprovalRouteColumns.filter(x=> serviceType === 'rest' ? x.dataIndex !== 'name' : x.dataIndex !== 'methods').map((x)=>({
...x,
...(x.dataIndex === 'change' ? {
render:(_,entity)=>(
<Tooltip placement="top" title={entity.change === 'error' ?$t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}>
<span className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}>
{$t(ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-')}
{entity.change === 'error' ?$t('该 API 缺失(0)(1)(2)请先补充',[entity.proxyStatus == 1 && $t('转发信息,'),entity.docStatus == 1 && $t('文档信息,'),entity.upstreamStatus == 1 && $t('上游信息,')]):''}
</span>
</Tooltip>)
}:{}
),
title: typeof x.title === 'string' ? $t(x.title) : x.title,
})),[state.language, serviceType])
const translatedPublishColumns = useMemo(()=>SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x)=>{
if(x.dataIndex === 'status'){
return {...x,title:$t(x.title),
render:(_:unknown,entity:SystemInsidePublishOnlineItems)=>{
switch(entity.status){
case 'done':
return <span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{$t('成功')}</span>
case 'error':
return <Tooltip title={entity.error || $t('上线失败')}><span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>{$t('失败')} {entity.error}</span></Tooltip>
default:
return <LoadingOutlined className="text-theme" spin />
} }
}} >
} <span
}),[state.language]) className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}
>
{$t(ChangeTypeEnum[entity.change as keyof typeof ChangeTypeEnum] || '-')}
{entity.change === 'error'
? $t('该 API 缺失(0)(1)(2)请先补充', [
entity.proxyStatus == 1 && $t('转发信息,'),
entity.docStatus == 1 && $t('文档信息,'),
entity.upstreamStatus == 1 && $t('上游信息,')
])
: ''}
</span>
</Tooltip>
)
}
: {}),
title: typeof x.title === 'string' ? $t(x.title) : x.title
})),
[state.language, serviceType]
)
const translatedPublishColumns = useMemo(
const translatedPolicyColumns = useMemo(()=>ApprovalPolicyColumns.map((x)=>{ () =>
return { SYSTEM_PUBLISH_ONLINE_COLUMNS.map((x) => {
if (x.dataIndex === 'status') {
return {
...x,
title: $t(x.title),
render: (_: unknown, entity: SystemInsidePublishOnlineItems) => {
switch (entity.status) {
case 'done':
return (
<span className={STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]}>{$t('成功')}</span>
)
case 'error':
return (
<Tooltip title={entity.error || $t('上线失败')}>
<span className={`${STATUS_COLOR[entity.status as keyof typeof STATUS_COLOR]} truncate block`}>
{$t('失败')} {entity.error}
</span>
</Tooltip>
)
default:
return <LoadingOutlined className="text-theme" spin />
}
}
}
}
}),
[state.language]
)
const translatedPolicyColumns = useMemo(
() =>
ApprovalPolicyColumns.map((x) => {
return {
...x, ...x,
title: typeof x.title === 'string' ? $t(x.title) : x.title, title: typeof x.title === 'string' ? $t(x.title) : x.title,
...(x.dataIndex === 'status' ? { ...(x.dataIndex === 'status'
render:(_,entity)=> { ? {
switch(entity.change){ render: (_, entity) => (
case 'none': <span
return '-' className={`${ApprovalStatusColorClass[entity.change as keyof typeof ApprovalStatusColorClass]} truncate block`}
case 'new': >
return $t('新建') {$t(ChangeTypeEnum[entity.change as keyof typeof ChangeTypeEnum] || '-')}
case 'update': </span>
return $t('更新') )
case 'delete':
return $t('删除')
default:
return '-'
}
} }
}:{}) : {})
} }
}),[state.language]) }),
[state.language]
)
return ( return (
<> <>
{!insidePage && <> {!insidePage && (
<>
<Row className="my-mbase"> <Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('申请系统')}</span></Col> <Col className="text-left" span={4}>
<Col span={18}>{(data as PublishApprovalInfoType).project || '-'}</Col> <span>{$t('申请系统')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).project || '-'}</Col>
</Row> </Row>
<Row className="my-mbase"> <Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('所属团队')}</span></Col> <Col className="text-left" span={4}>
<Col span={18}>{(data as PublishApprovalInfoType).team || '-'}</Col> <span>{$t('所属团队')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).team || '-'}</Col>
</Row> </Row>
<Row className="my-mbase"> <Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('申请人')}</span></Col> <Col className="text-left" span={4}>
<Col span={18}>{(data as PublishApprovalInfoType).applier || '-'}</Col> <span>{$t('申请人')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).applier || '-'}</Col>
</Row> </Row>
<Row className="my-mbase"> <Row className="my-mbase">
<Col className="text-left" span={4}><span >{$t('申请时间')}</span></Col> <Col className="text-left" span={4}>
<Col span={18}>{(data as PublishApprovalInfoType).applyTime || '-'}</Col> <span>{$t('申请时间')}</span>
</Col>
<Col span={18}>{(data as PublishApprovalInfoType).applyTime || '-'}</Col>
</Row> </Row>
</> } </>
<WithPermission access=""><Form )}
className=" mx-auto" <WithPermission access="">
form={form} <Form
labelAlign='left' className=" mx-auto"
layout='vertical' form={form}
scrollToFirstError labelAlign="left"
name="publishApprovalModalContent" layout="vertical"
// labelCol={{span: 3}} scrollToFirstError
// wrapperCol={{span: 21}} name="publishApprovalModalContent"
autoComplete="off" // labelCol={{span: 3}}
disabled={type === 'view'} // wrapperCol={{span: 21}}
> autoComplete="off"
disabled={type === 'view'}
>
{insidePage && (
<>
<Form.Item label={$t('版本号')} name="version" rules={[{ required: true, whitespace: true }]}>
<Input className="w-INPUT_NORMAL" disabled={type !== 'add'} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
{ <Form.Item label={$t('版本说明')} name="versionRemark">
insidePage && <Input.TextArea
<> className="w-INPUT_NORMAL"
<Form.Item disabled={type !== 'add' && type !== 'publish'}
label={$t("版本号")} placeholder={$t(PLACEHOLDER.input)}
name="version" />
rules={[{required: true,whitespace:true }]} </Form.Item>
> </>
<Input className="w-INPUT_NORMAL" disabled={type !== 'add'} placeholder={$t(PLACEHOLDER.input)} /> )}
</Form.Item> <Row className="mt-mbase pb-[8px] h-[32px] font-bold">
<span>{$t('路由列表')}</span>
<Form.Item </Row>
label={$t("版本说明")} <Row className="mb-mbase ">
name="versionRemark" <Table
> columns={translatedRouteColumns}
<Input.TextArea className="w-INPUT_NORMAL" disabled={type !== 'add' && type !== 'publish'} placeholder={$t(PLACEHOLDER.input)} /> bordered={true}
</Form.Item> rowKey="id"
</> size="small"
} dataSource={data.diffs?.routers || []}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('路由列表')}</span></Row> pagination={false}
<Row className="mb-mbase "> />
<Table </Row>
columns={translatedRouteColumns} {serviceType === 'rest' && (
bordered={true} <>
rowKey="id" <Row className="mt-mbase pb-[8px] h-[32px] font-bold">
size="small" <span>{$t('上游列表')}</span>
dataSource={data.diffs?.routers || []} </Row>
pagination={false} <Row className="mb-mbase ">
/></Row> <Table
{ bordered={true}
serviceType === 'rest' && <> columns={translatedUpstreamColumns}
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('上游列表')}</span></Row> size="small"
<Row className="mb-mbase "> rowKey="id"
<Table dataSource={data.diffs?.upstreams || []}
bordered={true} pagination={false}
columns={translatedUpstreamColumns} />
size="small" </Row>
rowKey="id" </>
dataSource={data.diffs?.upstreams || []} )}
pagination={false} <Row className="mt-mbase pb-[8px] h-[32px] font-bold">
/></Row> <span>{$t('策略列表')}</span>
</> </Row>
} <Row className="mb-mbase ">
<Row className="mt-mbase pb-[8px] h-[32px] font-bold" ><span >{$t('策略列表')}</span></Row> <Table
<Row className="mb-mbase "> bordered={true}
<Table columns={translatedPolicyColumns}
bordered={true} size="small"
columns={translatedPolicyColumns} rowKey="id"
size="small" dataSource={data.diffs?.strategies || []}
rowKey="id" pagination={false}
dataSource={data.diffs?.strategies || []} />
pagination={false} </Row>
/></Row> {/* <Form.Item
{/* <Form.Item
label={$t("备注")} label={$t("备注")}
name="remark" name="remark"
> >
<Input.TextArea className="w-INPUT_NORMAL" disabled={type !== 'add' && type !== 'publish'} placeholder={$t(PLACEHOLDER.input)} /> <Input.TextArea className="w-INPUT_NORMAL" disabled={type !== 'add' && type !== 'publish'} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item> */} </Form.Item> */}
{/* {/*
{type !== 'add' && type !== 'publish' && <Form.Item {type !== 'add' && type !== 'publish' && <Form.Item
label={$t("审核意见" label={$t("审核意见"
name="opinion" name="opinion"
@@ -273,20 +409,29 @@ export const PublishApprovalModalContent = forwardRef<PublishApprovalModalHandle
}, },
]);}}/> ]);}}/>
</Form.Item>} */} </Form.Item>} */}
{['error','done'].indexOf(data.status) !== -1 && data.clusterPublishStatus &&data.clusterPublishStatus.length > 0 && <> {['error', 'done'].indexOf(data.status) !== -1 &&
<Row className="text-left h-[32px] mb-8px]" span={3}><span>{$t('上线情况')}</span></Row> data.clusterPublishStatus &&
<Row span={24} className="mb-mbase"> data.clusterPublishStatus.length > 0 && (
<Table <>
bordered={true} <Row className="text-left h-[32px] mb-8px]" span={3}>
columns={[...translatedPublishColumns]} <span>{$t('上线情况')}</span>
size="small" </Row>
rowKey="id" <Row span={24} className="mb-mbase">
dataSource={data.clusterPublishStatus || []} <Table
pagination={false} bordered={true}
/> columns={[...translatedPublishColumns]}
</Row></>} size="small"
</Form> rowKey="id"
</WithPermission> dataSource={data.clusterPublishStatus || []}
</>) pagination={false}
}) />
</Row>
</>
)}
</Form>
</WithPermission>
</>
)
}
)
@@ -1,64 +1,64 @@
import { FC, useRef, useEffect, Children, cloneElement, isValidElement } from 'react'
import {FC, useRef, useEffect, Children, cloneElement, isValidElement } from 'react';
interface ScrollableSectionProps { interface ScrollableSectionProps {
children: React.ReactNode; children: React.ReactNode
} }
const ScrollableSection: FC<ScrollableSectionProps> = ({ children }) => { const ScrollableSection: FC<ScrollableSectionProps> = ({ children }) => {
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null)
useEffect(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
if (scrollAreaRef.current) { if (scrollAreaRef.current) {
const scrollTop = scrollAreaRef.current.scrollTop; const scrollTop = scrollAreaRef.current.scrollTop
const scrollHeight = scrollAreaRef.current.scrollHeight; const scrollHeight = scrollAreaRef.current.scrollHeight
const clientHeight = scrollAreaRef.current.clientHeight; const clientHeight = scrollAreaRef.current.clientHeight
// 如果滚动到顶部,.content-before 应该显示阴影 // 如果滚动到顶部,.content-before 应该显示阴影
const showTopShadow = scrollTop > 0; const showTopShadow = scrollTop > 0
// 如果滚动到底部,.content-after 应该显示阴影 // 如果滚动到底部,.content-after 应该显示阴影
const showBottomShadow = scrollHeight - scrollTop < clientHeight; const showBottomShadow = scrollHeight - scrollTop < clientHeight
// 这里我们不直接更新状态,而是通过ref来设置样式 // 这里我们不直接更新状态,而是通过ref来设置样式
if (showTopShadow && !showBottomShadow) { if (showTopShadow && !showBottomShadow) {
setElementShadow('.content-before', true); setElementShadow('.content-before', true)
setElementShadow('.content-after', false); setElementShadow('.content-after', false)
} else if (!showTopShadow && showBottomShadow) { } else if (!showTopShadow && showBottomShadow) {
setElementShadow('.content-before', false); setElementShadow('.content-before', false)
setElementShadow('.content-after', true); setElementShadow('.content-after', true)
} else { } else {
setElementShadow('.content-before', false); setElementShadow('.content-before', false)
setElementShadow('.content-after', false); setElementShadow('.content-after', false)
} }
} }
}; }
scrollAreaRef.current?.addEventListener('scroll', handleScroll); scrollAreaRef.current?.addEventListener('scroll', handleScroll)
return () => { return () => {
scrollAreaRef.current?.removeEventListener('scroll', handleScroll); scrollAreaRef.current?.removeEventListener('scroll', handleScroll)
}; }
}, []); }, [])
const setElementShadow = (elementSelector: string, showShadow: boolean) => { const setElementShadow = (elementSelector: string, showShadow: boolean) => {
const element = document.querySelector(elementSelector); const element = document.querySelector(elementSelector)
if (element) { if (element) {
element.style.boxShadow = showShadow ? ( elementSelector === '.content-before' ? '0 2px 2px #0000000d':'0 -2px 2px -2px var(--border-color)') : 'none'; element.style.boxShadow = showShadow
? elementSelector === '.content-before'
? '0 2px 2px #0000000d'
: '0 -2px 2px -2px var(--border-color)'
: 'none'
} }
} }
const childrenWithRef = Children.toArray(children).map((child) => { const childrenWithRef = Children.toArray(children).map((child) => {
if (isValidElement(child) && child.props.className && child.props.className.includes('scroll-area')) { if (isValidElement(child) && child.props.className && child.props.className.includes('scroll-area')) {
// 将 ref 附加到具有 'scroll-area' 类名的子元素 // 将 ref 附加到具有 'scroll-area' 类名的子元素
return cloneElement(child, { ref: scrollAreaRef }); return cloneElement(child, { ref: scrollAreaRef })
} }
return child; return child
}); })
return ( return <> {childrenWithRef}</>
<> {childrenWithRef} }
</>
);
};
export default ScrollableSection; export default ScrollableSection
@@ -1,115 +1,129 @@
import {App, Col, Form, Input, Row} from "antd"; import { App, Col, Form, Input, Row } from 'antd'
import { forwardRef, useEffect, useImperativeHandle} from "react"; import { forwardRef, useEffect, useImperativeHandle } from 'react'
import {SubscribeApprovalInfoType} from "@common/const/approval/type.tsx"; import { SubscribeApprovalInfoType } from '@common/const/approval/type.tsx'
import {BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; import { BasicResponse, FORM_ERROR_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import {useFetch} from "@common/hooks/http.ts"; import { useFetch } from '@common/hooks/http.ts'
import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import { SubscribeApprovalList } from "@common/const/approval/const"; import { SubscribeApprovalList } from '@common/const/approval/const'
import { $t } from "@common/locales"; import { $t } from '@common/locales'
type SubscribeApprovalModalProps = { type SubscribeApprovalModalProps = {
type:'approval'|'view' type: 'approval' | 'view'
data?:SubscribeApprovalInfoType data?: SubscribeApprovalInfoType
inSystem?:boolean inSystem?: boolean
serviceId:string serviceId: string
teamId:string teamId: string
} }
export type SubscribeApprovalModalHandle = { export type SubscribeApprovalModalHandle = {
save:(operate:'pass'|'refuse') =>Promise<boolean|string> save: (operate: 'pass' | 'refuse') => Promise<boolean | string>
} }
type FieldType = { type FieldType = {
reason?:string; reason?: string
opinion?:string; opinion?: string
}; }
export const SubscribeApprovalModalContent = forwardRef<SubscribeApprovalModalHandle,SubscribeApprovalModalProps>((props, ref) => { export const SubscribeApprovalModalContent = forwardRef<SubscribeApprovalModalHandle, SubscribeApprovalModalProps>(
(props, ref) => {
const { message } = App.useApp() const { message } = App.useApp()
const {data, type,inSystem=false, teamId, serviceId} = props const { data, type, inSystem = false, teamId, serviceId } = props
const [form] = Form.useForm(); const [form] = Form.useForm()
const {fetchData} = useFetch() const { fetchData } = useFetch()
const save:(operate:'pass'|'refuse')=>Promise<boolean | string> = (operate)=>{ const save: (operate: 'pass' | 'refuse') => Promise<boolean | string> = (operate) => {
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject) => {
if(type === 'view'){ if (type === 'view') {
resolve(true) resolve(true)
return return
} }
form.validateFields().then((value)=>{ form
if(operate === 'refuse' && form.getFieldValue('opinion') === ''){ .validateFields()
form.setFields([{ .then((value) => {
name:'opinion',errors:[$t(FORM_ERROR_TIPS.refuseOpinion)] if (operate === 'refuse' && form.getFieldValue('opinion') === '') {
}]) form.setFields([
form.scrollToField('opinion') {
reject($t(RESPONSE_TIPS.refuseOpinion)) name: 'opinion',
return errors: [$t(FORM_ERROR_TIPS.refuseOpinion)]
} }
fetchData<BasicResponse<null>>(`${inSystem?'service/':''}approval/subscribe`,{method: 'POST',eoBody:({opinion:value.opinion,operate}), eoParams:(inSystem ? {apply:data!.id, team:teamId} : {id:data!.id,team:teamId})}).then(response=>{ ])
const {code,msg} = response form.scrollToField('opinion')
if(code === STATUS_CODE.SUCCESS){ reject($t(RESPONSE_TIPS.refuseOpinion))
message.success(msg || $t(RESPONSE_TIPS.success)) return
resolve(true) }
}else{ fetchData<BasicResponse<null>>(`${inSystem ? 'service/' : ''}approval/subscribe`, {
message.error(msg || $t(RESPONSE_TIPS.error)) method: 'POST',
reject(msg || $t(RESPONSE_TIPS.error)) eoBody: { opinion: value.opinion, operate },
} eoParams: inSystem ? { apply: data!.id, team: teamId } : { id: data!.id, team: teamId }
}).catch((errorInfo)=> reject(errorInfo)) })
}).catch((errorInfo)=> reject(errorInfo)) .then((response) => {
}) const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo) => reject(errorInfo))
})
} }
useImperativeHandle(ref, ()=>({ useImperativeHandle(ref, () => ({
save save
}) }))
)
useEffect(()=>{ useEffect(() => {
form.setFieldsValue({opinion:'',...data}) form.setFieldsValue({ opinion: '', ...data })
},[]) }, [])
return ( return (
<div className="my-btnybase">{ <div className="my-btnybase">
SubscribeApprovalList?.map((x)=>( {SubscribeApprovalList?.map((x) => (
<Row key={x.key} className="leading-[32px] mb-btnbase mx-auto"> <Row key={x.key} className="leading-[32px] mb-btnbase mx-auto">
<Col className="text-left" span={6}>{$t(x.title)}</Col> <Col className="text-left" span={6}>
<Col >{(data as {[k:string]:unknown})?.[x.key]?.name || (data as {[k:string]:unknown})?.[x.key] || '-'}</Col> {$t(x.title)}
</Row> </Col>
)) <Col>
} {(data as { [k: string]: unknown })?.[x.key]?.name || (data as { [k: string]: unknown })?.[x.key] || '-'}
</Col>
</Row>
))}
<WithPermission access=""> <WithPermission access="">
<Form <Form
labelAlign='left' labelAlign="left"
layout='vertical' layout="vertical"
form={form} form={form}
className="mx-auto " className="mx-auto "
name="subscribeApprovalModalContent" name="subscribeApprovalModalContent"
// labelCol={{ span: 6}} // labelCol={{ span: 6}}
// wrapperCol={{ span: 18}} // wrapperCol={{ span: 18}}
autoComplete="off" autoComplete="off"
disabled={type === 'view'} disabled={type === 'view'}
> >
<Form.Item<FieldType> label={$t('申请原因')} name="reason">
<Form.Item<FieldType> <Input.TextArea className="w-INPUT_NORMAL" disabled={true} placeholder=" " />
label={$t("申请原因")} </Form.Item>
name="reason" <Form.Item<FieldType> label={$t('审核意见')} name="opinion" extra={$t(FORM_ERROR_TIPS.refuseOpinion)}>
> <Input.TextArea
<Input.TextArea className="w-INPUT_NORMAL" disabled={true} placeholder=" " /> className="w-INPUT_NORMAL"
</Form.Item> placeholder={$t(PLACEHOLDER.input)}
<Form.Item<FieldType> onChange={() => {
label={$t("审核意见")} form.setFields([
name="opinion" {
extra={$t(FORM_ERROR_TIPS.refuseOpinion)} name: 'opinion',
> errors: [] // 设置为空数组来移除错误信息
<Input.TextArea className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} onChange={()=>{ form.setFields([ }
{ ])
name: 'opinion', }}
errors: [], // 设置为空数组来移除错误信息 />
}, </Form.Item>
])}} /> </Form>
</Form.Item> </WithPermission>
</Form> </div>
</WithPermission>
</div>
) )
}) }
)
@@ -1,41 +1,51 @@
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { Button, Tooltip } from "antd" import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useState, useMemo, useEffect, useCallback } from "react" import { $t } from '@common/locales'
import { useGlobalContext } from "@common/contexts/GlobalStateContext" import { Icon } from '@iconify/react/dist/iconify.js'
import { useNavigate } from "react-router-dom" import { Button, Tooltip } from 'antd'
import { PERMISSION_DEFINITION } from "@common/const/permissions" import { useCallback, useEffect, useMemo, useState } from 'react'
import { Icon } from "@iconify/react/dist/iconify.js" import { useNavigate } from 'react-router-dom'
import { $t } from "@common/locales"
type TableBtnWithPermissionProps = { type TableBtnWithPermissionProps = {
btnTitle: string btnTitle: string
access?: keyof typeof PERMISSION_DEFINITION[0], access?: keyof (typeof PERMISSION_DEFINITION)[0]
tooltip?: string, tooltip?: string
disabled?: boolean, disabled?: boolean
navigateTo?: string, navigateTo?: string
onClick?: (args?: unknown) => void onClick?: (args?: unknown) => void
className?: string className?: string
btnType: string btnType: string
} }
const TableIconName = { const TableIconName = {
'add': 'ic:baseline-add', add: 'ic:baseline-add',
'edit': 'ic:baseline-edit', edit: 'ic:baseline-edit',
'delete': 'ic:baseline-delete', delete: 'ic:baseline-delete',
'remove': 'ic:baseline-minus', remove: 'ic:baseline-minus',
'copy': 'ic:baseline-file-copy', copy: 'ic:baseline-file-copy',
'view': 'ic:baseline-remove-red-eye', view: 'ic:baseline-remove-red-eye',
'publish': 'ic:baseline-publish', publish: 'ic:baseline-publish',
'approval': 'ic:baseline-approval', offline: 'ic:baseline-file-download-off',
'stop': 'ic:baseline-stop-circle', approval: 'ic:baseline-approval',
'online': 'ic:baseline-check-circle', stop: 'ic:baseline-stop-circle',
'cancel': 'ic:baseline-cancel-schedule-send', online: 'ic:baseline-check-circle',
'refresh': 'ic:baseline-refresh', cancel: 'ic:baseline-cancel-schedule-send',
'logs': 'hugeicons:google-doc' refresh: 'ic:baseline-refresh',
logs: 'hugeicons:google-doc',
disable: 'ic:baseline-pause-circle',
enable: 'ic:baseline-play-circle'
} }
// 表格操作栏按钮,受权限控制 // 表格操作栏按钮,受权限控制
const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateTo, onClick, className, btnType }: TableBtnWithPermissionProps) => { const TableBtnWithPermission = ({
btnTitle,
access,
tooltip,
disabled,
navigateTo,
onClick,
className,
btnType
}: TableBtnWithPermissionProps) => {
const [btnAccess, setBtnAccess] = useState<boolean>(false) const [btnAccess, setBtnAccess] = useState<boolean>(false)
const [btnStatus, setBtnStatus] = useState<boolean>(false) const [btnStatus, setBtnStatus] = useState<boolean>(false)
const [closeToolTip, setCloseToolTip] = useState<boolean>(false) const [closeToolTip, setCloseToolTip] = useState<boolean>(false)
@@ -51,16 +61,18 @@ const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateT
access ? setBtnAccess(lastAccess) : setBtnAccess(true) access ? setBtnAccess(lastAccess) : setBtnAccess(true)
}, [access, lastAccess]) }, [access, lastAccess])
const handleClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
setTimeout(() => {
setBtnStatus(false)
setCloseToolTip(true)
})
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => { navigateTo ? navigate(navigateTo) : onClick?.()
e.stopPropagation() },
setTimeout(() => { [navigateTo, navigate, onClick]
setBtnStatus(false) )
setCloseToolTip(true)
})
navigateTo ? navigate(navigateTo) : onClick?.()
}, [navigateTo, navigate, onClick])
const changeTooltipStatus = (open: boolean) => { const changeTooltipStatus = (open: boolean) => {
setBtnStatus(open) setBtnStatus(open)
if (closeToolTip) { if (closeToolTip) {
@@ -68,18 +80,42 @@ const TableBtnWithPermission = ({ btnTitle, access, tooltip, disabled, navigateT
setCloseToolTip(false) setCloseToolTip(false)
} }
} }
return (<>{ return (
!btnAccess || (disabled && tooltip) ? <>
<Tooltip placement="top" title={tooltip ?? $t('暂无(0)权限,请联系管理员分配。', [$t(btnTitle).toLowerCase()])}> {!btnAccess || (disabled && tooltip) ? (
<Button type="text" disabled={true} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className}`} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />} >{ }</Button> <Tooltip placement="top" title={tooltip ?? $t('暂无(0)权限,请联系管理员分配。', [$t(btnTitle).toLowerCase()])}>
</Tooltip> <Button
: type="text"
<Tooltip placement="top" title={$t(btnTitle)} trigger='hover' open={btnStatus} onOpenChange={changeTooltipStatus}> disabled={true}
<Button type="text" disabled={disabled} className={`h-[22px] border-none p-0 flex items-center bg-transparent ${className} `} key={btnType} icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />} onClick={handleClick}>{ }</Button> className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
</Tooltip> key={btnType}
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
}</> >
); {}
</Button>
</Tooltip>
) : (
<Tooltip
placement="top"
title={$t(btnTitle)}
trigger="hover"
open={btnStatus}
onOpenChange={changeTooltipStatus}
>
<Button
type="text"
disabled={disabled}
className={`flex items-center p-0 bg-transparent border-none h-[22px] ${className}`}
key={btnType}
icon={<Icon icon={TableIconName[btnType as keyof typeof TableIconName]} width="18" height="18" />}
onClick={handleClick}
>
{}
</Button>
</Tooltip>
)}
</>
)
} }
export default TableBtnWithPermission export default TableBtnWithPermission
@@ -1,37 +1,38 @@
import { Tag, TagProps } from 'antd'
import { useState, useMemo, useEffect } from 'react'
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { Tag, TagProps } from "antd"; export interface TagWithPermission extends TagProps {
import { useState, useMemo, useEffect } from "react"; access?: string
import { PERMISSION_DEFINITION } from "@common/const/permissions";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
export interface TagWithPermission extends TagProps{
access?:string
} }
export default function TagWithPermission(props:TagWithPermission){ export default function TagWithPermission(props: TagWithPermission) {
const {access,onClose} = props const { access, onClose } = props
const [editAccess, setEditAccess] = useState<boolean>(access ? false:true) const [editAccess, setEditAccess] = useState<boolean>(access ? false : true)
const {accessData,checkPermission,accessInit} = useGlobalContext() const { accessData, checkPermission, accessInit } = useGlobalContext()
const lastAccess = useMemo(()=>{ const lastAccess = useMemo(() => {
if(!access) return true if (!access) return true
return checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]) return checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
},[access, accessData,checkPermission,accessInit]) }, [access, accessData, checkPermission, accessInit])
useEffect(()=>{ useEffect(() => {
access ? setEditAccess(lastAccess) : setEditAccess(true) access ? setEditAccess(lastAccess) : setEditAccess(true)
},[lastAccess]) }, [lastAccess])
const handleTagClose = (e: React.MouseEvent<HTMLElement>)=>{
e.preventDefault();
if(!editAccess) return
onClose?.(e)
}
return <Tag const handleTagClose = (e: React.MouseEvent<HTMLElement>) => {
closeIcon e.preventDefault()
{...props} if (!editAccess) return
className={` rounded-SEARCH_RADIUS h-[32px] text-[14px] leading-[22px] py-[5px] px-btnbase bg-transparent mb-[8px] ${props.className}`} onClose?.(e)
onClose={handleTagClose}> }
{props.children}
</Tag>
} return (
<Tag
closeIcon
{...props}
className={` rounded-SEARCH_RADIUS h-[32px] text-[14px] leading-[22px] py-[5px] px-btnbase bg-transparent mb-[8px] ${props.className}`}
onClose={handleTagClose}
>
{props.children}
</Tag>
)
}
@@ -1,34 +1,33 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react';
const ThemeSwitcher = () => { const ThemeSwitcher = () => {
const [darkMode, setDarkMode] = useState(true); const [darkMode, setDarkMode] = useState(true)
useEffect(() => { useEffect(() => {
let isDarkMode = localStorage.getItem('dark-mode'); const isDarkMode = localStorage.getItem('dark-mode')
if(isDarkMode !== undefined && isDarkMode !== null){ if (isDarkMode !== undefined && isDarkMode !== null) {
setDarkMode(isDarkMode === 'true') setDarkMode(isDarkMode === 'true')
}else{ } else {
localStorage.setItem('dark-mode', (darkMode).toString()); localStorage.setItem('dark-mode', darkMode.toString())
} }
}, []); }, [])
useEffect(()=>{ useEffect(() => {
document.documentElement.classList.toggle('dark', darkMode); document.documentElement.classList.toggle('dark', darkMode)
},[darkMode]) }, [darkMode])
const toggleDarkMode = () => { const toggleDarkMode = () => {
setDarkMode(!darkMode); setDarkMode(!darkMode)
localStorage.setItem('dark-mode', (!darkMode).toString()); localStorage.setItem('dark-mode', (!darkMode).toString())
document.documentElement.classList.toggle('dark', !darkMode); document.documentElement.classList.toggle('dark', !darkMode)
}; }
return ( return (
// <button onClick={toggleDarkMode}> // <button onClick={toggleDarkMode}>
// {darkMode ? '切换到白天模式' : '切换到黑夜模式'} // {darkMode ? '切换到白天模式' : '切换到黑夜模式'}
// </button> // </button>
<></> <></>
); )
}; }
export default ThemeSwitcher; export default ThemeSwitcher
@@ -1,25 +1,24 @@
import { useEffect, useState } from 'react'
import { Radio, DatePicker, GetProps, RadioChangeEvent } from 'antd'
import dayjs, { Dayjs } from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import '../../index.css'
import { $t } from '@common/locales'
import { useEffect, useState } from 'react'; type RangePickerProps = GetProps<typeof DatePicker.RangePicker>
import { Radio, DatePicker, GetProps, RadioChangeEvent } from 'antd'; export type RangeValue = [Dayjs | null, Dayjs | null] | null
import dayjs, { Dayjs } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import "../../index.css"
import { $t } from '@common/locales';
type RangePickerProps = GetProps<typeof DatePicker.RangePicker>; dayjs.extend(customParseFormat)
export type RangeValue = [Dayjs | null, Dayjs | null] | null;
dayjs.extend(customParseFormat);
export type TimeRange = { export type TimeRange = {
start: number | null start: number | null
end: number | null end: number | null
} }
export type TimeRangeButton = '' | 'hour' | 'day' | 'threeDays' | 'sevenDays'; export type TimeRangeButton = '' | 'hour' | 'day' | 'threeDays' | 'sevenDays'
type TimeRangeSelectorProps = { type TimeRangeSelectorProps = {
initialTimeButton?: TimeRangeButton, initialTimeButton?: TimeRangeButton
initialDatePickerValue?: RangeValue initialDatePickerValue?: RangeValue
onTimeRangeChange?: (timeRange: TimeRange) => void onTimeRangeChange?: (timeRange: TimeRange) => void
hideTitle?: boolean hideTitle?: boolean
@@ -30,17 +29,27 @@ type TimeRangeSelectorProps = {
defaultTimeButton?: TimeRangeButton defaultTimeButton?: TimeRangeButton
} }
const TimeRangeSelector = (props: TimeRangeSelectorProps) => { const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
const { initialTimeButton, initialDatePickerValue, onTimeRangeChange, hideTitle, onTimeButtonChange, labelSize = 'default', bindRef, hideBtns = [], defaultTimeButton = 'hour' } = props const {
const [timeButton, setTimeButton] = useState(initialTimeButton || ''); initialTimeButton,
const [datePickerValue, setDatePickerValue] = useState<RangeValue>(initialDatePickerValue || [null, null]); initialDatePickerValue,
onTimeRangeChange,
hideTitle,
onTimeButtonChange,
labelSize = 'default',
bindRef,
hideBtns = [],
defaultTimeButton = 'hour'
} = props
const [timeButton, setTimeButton] = useState(initialTimeButton || '')
const [datePickerValue, setDatePickerValue] = useState<RangeValue>(initialDatePickerValue || [null, null])
useEffect(() => { useEffect(() => {
if (bindRef) { if (bindRef) {
bindRef({ reset }); bindRef({ reset })
} }
}, [bindRef]) }, [bindRef])
// 根据选择的时间范围计算开始和结束时间 // 根据选择的时间范围计算开始和结束时间
const calculateTimeRange = (curBtn: TimeRangeButton) => { const calculateTimeRange = (curBtn: TimeRangeButton) => {
const currentSecond = Math.floor(Date.now() / 1000); // 当前秒级时间戳 const currentSecond = Math.floor(Date.now() / 1000) // 当前秒级时间戳
let startMin = currentSecond - 60 * 60 let startMin = currentSecond - 60 * 60
switch (curBtn) { switch (curBtn) {
case 'hour': { case 'hour': {
@@ -52,30 +61,26 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
break break
} }
case 'threeDays': { case 'threeDays': {
startMin = startMin = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) - 2 * 24 * 60 * 60
Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) -
2 * 24 * 60 * 60
break break
} }
case 'sevenDays': { case 'sevenDays': {
startMin = startMin = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) - 6 * 24 * 60 * 60
Math.floor(new Date().setHours(0, 0, 0, 0) / 1000) -
6 * 24 * 60 * 60
break break
} }
} }
if (onTimeRangeChange) { if (onTimeRangeChange) {
onTimeRangeChange({ start: startMin, end: currentSecond }); onTimeRangeChange({ start: startMin, end: currentSecond })
} }
}; }
// 处理单选按钮的变化 // 处理单选按钮的变化
const handleRadioChange = (e: RadioChangeEvent) => { const handleRadioChange = (e: RadioChangeEvent) => {
setTimeButton(e.target.value); setTimeButton(e.target.value)
onTimeButtonChange?.(e.target.value) onTimeButtonChange?.(e.target.value)
setDatePickerValue(null) setDatePickerValue(null)
calculateTimeRange(e.target.value); calculateTimeRange(e.target.value)
}; }
const reset = () => { const reset = () => {
setTimeButton(defaultTimeButton) setTimeButton(defaultTimeButton)
calculateTimeRange(defaultTimeButton) calculateTimeRange(defaultTimeButton)
@@ -86,35 +91,43 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
const handleDatePickerChange = (dates: RangeValue) => { const handleDatePickerChange = (dates: RangeValue) => {
setTimeButton(dates ? '' : defaultTimeButton) setTimeButton(dates ? '' : defaultTimeButton)
onTimeButtonChange?.(dates ? '' : defaultTimeButton) onTimeButtonChange?.(dates ? '' : defaultTimeButton)
setDatePickerValue(dates); setDatePickerValue(dates)
if (dates && Array.isArray(dates) && dates.length === 2) { if (dates && Array.isArray(dates) && dates.length === 2) {
const [startDate, endDate] = dates; const [startDate, endDate] = dates
const start = startDate!.startOf('day').unix(); // 开始日期的00:00:00 const start = startDate!.startOf('day').unix() // 开始日期的00:00:00
const end = endDate!.endOf('day').unix(); // 结束日期的23:59:59 const end = endDate!.endOf('day').unix() // 结束日期的23:59:59
if (onTimeRangeChange) { if (onTimeRangeChange) {
onTimeRangeChange({ start, end }); onTimeRangeChange({ start, end })
} }
} }
if (!dates) { if (!dates) {
calculateTimeRange(defaultTimeButton) calculateTimeRange(defaultTimeButton)
} }
}; }
const disabledDate: RangePickerProps['disabledDate'] = (current) => { const disabledDate: RangePickerProps['disabledDate'] = (current) => {
// Can not select days before today and today // Can not select days before today and today
return current && current.valueOf() > dayjs().startOf('day').valueOf(); return current && current.valueOf() > dayjs().startOf('day').valueOf()
}; }
return ( return (
<div className="flex flex-nowrap items-center pt-btnybase mr-btnybase"> <div className="flex flex-nowrap items-center pt-btnybase mr-btnybase">
{!hideTitle && <label className={`whitespace-nowrap `}>{$t('时间')}</label>} {!hideTitle && <label className={`whitespace-nowrap `}>{$t('时间')}</label>}
<Radio.Group className="whitespace-nowrap" value={timeButton} onChange={handleRadioChange} buttonStyle="solid"> <Radio.Group className="whitespace-nowrap" value={timeButton} onChange={handleRadioChange} buttonStyle="solid">
{hideBtns?.length && hideBtns.includes('hour') ? null : <Radio.Button value="hour">{$t('近1小时')}</Radio.Button>} {hideBtns?.length && hideBtns.includes('hour') ? null : (
{hideBtns?.length && hideBtns.includes('day') ? null : <Radio.Button value="day">{$t('近24小时')}</Radio.Button>} <Radio.Button value="hour">{$t('近1小时')}</Radio.Button>
{hideBtns?.length && hideBtns.includes('threeDays') ? null : <Radio.Button value="threeDays">{$t('近3天')}</Radio.Button>} )}
{hideBtns?.length && hideBtns.includes('sevenDays') ? null : <Radio.Button className="rounded-e-none" value="sevenDays">{$t('近7天')}</Radio.Button>} {hideBtns?.length && hideBtns.includes('day') ? null : (
<Radio.Button value="day">{$t('近24小时')}</Radio.Button>
)}
{hideBtns?.length && hideBtns.includes('threeDays') ? null : (
<Radio.Button value="threeDays">{$t('近3天')}</Radio.Button>
)}
{hideBtns?.length && hideBtns.includes('sevenDays') ? null : (
<Radio.Button className="rounded-e-none" value="sevenDays">
{$t('近7天')}
</Radio.Button>
)}
</Radio.Group> </Radio.Group>
<DatePicker.RangePicker <DatePicker.RangePicker
value={datePickerValue} value={datePickerValue}
@@ -129,7 +142,7 @@ const TimeRangeSelector = (props: TimeRangeSelectorProps) => {
}} }}
/> />
</div> </div>
); )
}; }
export default TimeRangeSelector; export default TimeRangeSelector
@@ -1,45 +1,94 @@
import { CheckOutlined, LoadingOutlined, MoreOutlined } from '@ant-design/icons'
import {CheckOutlined, LoadingOutlined, MoreOutlined} from "@ant-design/icons"; import { Dropdown, Input, InputRef, MenuProps } from 'antd'
import {Dropdown, Input, InputRef, MenuProps} from "antd"; import { ReactNode, useEffect, useRef, useState } from 'react'
import { ReactNode, useEffect, useRef, useState} from "react";
export type TreeWithMoreProp = { export type TreeWithMoreProp = {
children:ReactNode, children: ReactNode
dropdownMenu:MenuProps['items'] dropdownMenu: MenuProps['items']
editable?:boolean editable?: boolean
editingId?:string editingId?: string
afterEdit?:(val:string)=>Promise<string|boolean> afterEdit?: (val: string) => Promise<string | boolean>
editKey?:string editKey?: string
entity?:{id:string,[k:string]:unknown | string} entity?: { id: string; [k: string]: unknown | string }
onBlur?:()=>void onBlur?: () => void
stopClick?:boolean stopClick?: boolean
} }
const TreeWithMore = ({children,dropdownMenu,editable,editingId,entity,editKey='name',afterEdit,onBlur,stopClick=true}:TreeWithMoreProp)=>{ const TreeWithMore = ({
const [editValue, setEditValue] = useState<string>(entity?.[editKey] as string) children,
const [submitting, setSubmitting] = useState<boolean>(false) dropdownMenu,
const inputRef = useRef<InputRef>(null) editable,
editingId,
entity,
editKey = 'name',
afterEdit,
onBlur,
stopClick = true
}: TreeWithMoreProp) => {
const [editValue, setEditValue] = useState<string>(entity?.[editKey] as string)
const [submitting, setSubmitting] = useState<boolean>(false)
const inputRef = useRef<InputRef>(null)
const handleSubmit = (val:string)=>{ const handleSubmit = (val: string) => {
if(submitting) return if (submitting) return
setSubmitting(true) setSubmitting(true)
afterEdit && afterEdit(val).finally(()=>setSubmitting(false)) afterEdit && afterEdit(val).finally(() => setSubmitting(false))
} }
useEffect(()=>{inputRef.current?.focus()},[inputRef]) useEffect(() => {
inputRef.current?.focus()
}, [inputRef])
return (<> return (
{ <>
editable && editingId && entity?.id && editingId === entity.id ? <Input ref={inputRef} value={editValue} onChange={(e)=>{setEditValue(e.target.value)}} onBlur={()=>{onBlur?.()}} onClick={(e)=>stopClick&&e?.stopPropagation()} onPressEnter={()=>{handleSubmit(editValue)}} suffix={submitting ? <LoadingOutlined />:<CheckOutlined onClick={()=>{handleSubmit(editValue)}}/>} />: {editable && editingId && entity?.id && editingId === entity.id ? (
<Dropdown menu={{items:dropdownMenu}} trigger={['contextMenu']} > <Input
<div className='tree-title-hover' >{children} ref={inputRef}
<span onClick={(e)=>{ stopClick && e.stopPropagation();}}> value={editValue}
<Dropdown menu={{items:dropdownMenu}} trigger={['click']} > onChange={(e) => {
<MoreOutlined className="tree-title-more" onClick={(e)=>{ stopClick && e.stopPropagation(); }} /> setEditValue(e.target.value)
</Dropdown> }}
</span> onBlur={() => {
</div> onBlur?.()
}}
onClick={(e) => stopClick && e?.stopPropagation()}
onPressEnter={() => {
handleSubmit(editValue)
}}
suffix={
submitting ? (
<LoadingOutlined />
) : (
<CheckOutlined
onClick={() => {
handleSubmit(editValue)
}}
/>
)
}
/>
) : (
<Dropdown menu={{ items: dropdownMenu }} trigger={['contextMenu']}>
<div className="tree-title-hover">
{children}
<span
onClick={(e) => {
stopClick && e.stopPropagation()
}}
>
<Dropdown menu={{ items: dropdownMenu }} trigger={['click']}>
<MoreOutlined
className="tree-title-more"
onClick={(e) => {
stopClick && e.stopPropagation()
}}
/>
</Dropdown>
</span>
</div>
</Dropdown> </Dropdown>
}</>) )}
</>
)
} }
export default TreeWithMore export default TreeWithMore
@@ -1,190 +1,190 @@
import { $t } from "@common/locales" import { $t } from '@common/locales'
/* 本组件不在页面渲染,只是为了让i18next-scanner能找到从接口传递的、需要翻译的字段 , 此处的字段除非确认不在页面上渲染了,否则不应删除,容易导致翻译遗漏*/ /* 本组件不在页面渲染,只是为了让i18next-scanner能找到从接口传递的、需要翻译的字段 , 此处的字段除非确认不在页面上渲染了,否则不应删除,容易导致翻译遗漏*/
export const TranslateWord = ()=>{ export const TranslateWord = () => {
return ( return (
<> <>
{$t('文件日志')} {$t('文件日志')}
{$t('HTTP日志')} {$t('HTTP日志')}
{$t('Kafka日志')} {$t('Kafka日志')}
{$t('NSQ日志')} {$t('NSQ日志')}
{$t('Syslog日志')} {$t('Syslog日志')}
{$t('未分配')} {$t('未分配')}
{$t('超级管理员')} {$t('超级管理员')}
{$t('团队管理员')} {$t('团队管理员')}
{$t('运维管理员')} {$t('运维管理员')}
{$t('普通成员')} {$t('普通成员')}
{$t('只读成员')} {$t('只读成员')}
{$t('服务管理员')} {$t('服务管理员')}
{$t('服务开发者')} {$t('服务开发者')}
{$t('消费者开发者')} {$t('消费者开发者')}
{$t('消费者管理员')} {$t('消费者管理员')}
{$t('驱动名称')} {$t('驱动名称')}
{$t('请求失败数')} {$t('请求失败数')}
{$t('转发失败数')} {$t('转发失败数')}
{$t('作用范围')} {$t('作用范围')}
{$t('添加条目')} {$t('添加条目')}
{$t('添加地址')} {$t('添加地址')}
{$t('文件名称')} {$t('文件名称')}
{$t('存放目录')} {$t('存放目录')}
{$t('日志分割周期')} {$t('日志分割周期')}
{$t('过期时间')} {$t('过期时间')}
{$t('单位:天')} {$t('单位:天')}
{$t('输出格式')} {$t('输出格式')}
{$t('格式化配置')} {$t('格式化配置')}
{$t('服务器地址')} {$t('服务器地址')}
{$t('Access日志')} {$t('Access日志')}
{$t('NSQD地址列表')} {$t('NSQD地址列表')}
{$t('鉴权Secret')} {$t('鉴权Secret')}
{$t('网络协议')} {$t('网络协议')}
{$t('日志等级')} {$t('日志等级')}
{$t('单行')} {$t('单行')}
{$t('小时')} {$t('小时')}
{$t('天')} {$t('天')}
{$t('未发布')} {$t('未发布')}
{$t('待发布')} {$t('待发布')}
{$t('单位:s,最小值:1')} {$t('单位:s,最小值:1')}
{$t('上传文件')} {$t('上传文件')}
{$t('替换文件')} {$t('替换文件')}
{$t('是否放行')} {$t('是否放行')}
{$t('监控')} {$t('监控')}
{$t('必填')} {$t('必填')}
{$t('字符非法,仅支持英文')} {$t('字符非法,仅支持英文')}
{$t('上传 OpenAPI 文档 (.json/.yaml)')} {$t('上传 OpenAPI 文档 (.json/.yaml)')}
{$t('替换 OpenAPI 文档 (.json/.yaml)')} {$t('替换 OpenAPI 文档 (.json/.yaml)')}
{$t('打开 OpenAPI YAML 编辑器')} {$t('打开 OpenAPI YAML 编辑器')}
{$t('无需审核:允许任何消费者调用该服务')} {$t('无需审核:允许任何消费者调用该服务')}
{$t('人工审核:仅允许通过人工审核的消费者调用该服务')} {$t('人工审核:仅允许通过人工审核的消费者调用该服务')}
{$t('永久')} {$t('永久')}
{$t('否')} {$t('否')}
{$t('是')} {$t('是')}
{$t('无需审核')} {$t('无需审核')}
{$t('需要审核')} {$t('需要审核')}
{$t('创建时间')} {$t('创建时间')}
{$t('协议')} {$t('协议')}
{$t('方法')} {$t('方法')}
{$t('地址(IP 端口或域名)')} {$t('地址(IP 端口或域名)')}
{$t('权重(0-999')} {$t('权重(0-999')}
{$t('带权轮询')} {$t('带权轮询')}
{$t('发布版本')} {$t('发布版本')}
{$t('发布申请记录')} {$t('发布申请记录')}
{$t('创建版本时间')} {$t('创建版本时间')}
{$t('版本状态')} {$t('版本状态')}
{$t('创建人')} {$t('创建人')}
{$t('审核时间')} {$t('审核时间')}
{$t('申请方-消费者')} {$t('申请方-消费者')}
{$t('审核状态')} {$t('审核状态')}
{$t('申请人')} {$t('申请人')}
{$t('审核人')} {$t('审核人')}
{$t('审核时间')} {$t('审核时间')}
{$t('来源')} {$t('来源')}
{$t('订阅时间')} {$t('订阅时间')}
{$t('请输入')} {$t('请输入')}
{$t('请选择')} {$t('请选择')}
{$t('创建者')} {$t('创建者')}
{$t('服务数量')} {$t('服务数量')}
{$t('负责人')} {$t('负责人')}
{$t('姓名')} {$t('姓名')}
{$t('团队角色')} {$t('团队角色')}
{$t('添加日期')} {$t('添加日期')}
{$t('请求成功数')} {$t('请求成功数')}
{$t('转发成功数')} {$t('转发成功数')}
{$t('API 名称')} {$t('API 名称')}
{$t('失败状态码数')} {$t('失败状态码数')}
{$t('所属服务')} {$t('所属服务')}
{$t('平均响应时间(ms)')} {$t('平均响应时间(ms)')}
{$t('最大响应时间(ms)')} {$t('最大响应时间(ms)')}
{$t('最小响应时间(ms)')} {$t('最小响应时间(ms)')}
{$t('平均请求流量(KB)')} {$t('平均请求流量(KB)')}
{$t('最大请求流量(KB)')} {$t('最大请求流量(KB)')}
{$t('最小请求流量(KB)')} {$t('最小请求流量(KB)')}
{$t('所有成员')} {$t('所有成员')}
{$t('状态')} {$t('状态')}
{$t('角色名称')} {$t('角色名称')}
{$t('绑定域名')} {$t('绑定域名')}
{$t('过期日期')} {$t('过期日期')}
{$t('支持字母开头、英文数字中横线下划线组合')} {$t('支持字母开头、英文数字中横线下划线组合')}
{$t('英文数字下划线任意一种,首字母必须为英文')} {$t('英文数字下划线任意一种,首字母必须为英文')}
{$t('字符非法,仅支持英文')} {$t('字符非法,仅支持英文')}
{$t('无法连接集群,请检查集群地址是否正确或防火墙配置')} {$t('无法连接集群,请检查集群地址是否正确或防火墙配置')}
{$t('选择拒绝时,审核意见为必填')} {$t('选择拒绝时,审核意见为必填')}
{$t('操作成功')} {$t('操作成功')}
{$t('操作失败')} {$t('操作失败')}
{$t('正在操作')} {$t('正在操作')}
{$t('正在加载数据')} {$t('正在加载数据')}
{$t('获取数据失败')} {$t('获取数据失败')}
{$t('登录成功')} {$t('登录成功')}
{$t('退出成功,将跳转至登录页')} {$t('退出成功,将跳转至登录页')}
{$t('未填写审核意见')} {$t('未填写审核意见')}
{$t('复制成功')} {$t('复制成功')}
{$t('复制失败,请手动复制')} {$t('复制失败,请手动复制')}
{$t('服务所属团队')} {$t('服务所属团队')}
{$t('在线')} {$t('在线')}
{$t('已拒绝')} {$t('已拒绝')}
{$t('中止')} {$t('中止')}
{$t('发布异常')} {$t('发布异常')}
{$t('发布中')} {$t('发布中')}
{$t('申请方所属团队')} {$t('申请方所属团队')}
{$t('发布状态')} {$t('发布状态')}
{$t(' 次')} {$t(' 次')}
{$t('每分钟')} {$t('每分钟')}
{$t('每5分钟')} {$t('每5分钟')}
{$t('每小时')} {$t('每小时')}
{$t('每天')} {$t('每天')}
{$t('每周')} {$t('每周')}
{$t('上线结果')} {$t('上线结果')}
{$t('订阅服务数量')} {$t('订阅服务数量')}
{$t('鉴权数量')} {$t('鉴权数量')}
{$t('列表')} {$t('列表')}
{$t('块')} {$t('块')}
{$t('HTTP 请求头')} {$t('HTTP 请求头')}
{$t('全等匹配')} {$t('全等匹配')}
{$t('前缀匹配')} {$t('前缀匹配')}
{$t('后缀匹配')} {$t('后缀匹配')}
{$t('子串匹配')} {$t('子串匹配')}
{$t('非等匹配')} {$t('非等匹配')}
{$t('空值匹配')} {$t('空值匹配')}
{$t('存在匹配')} {$t('存在匹配')}
{$t('不存在匹配')} {$t('不存在匹配')}
{$t('区分大小写的正则匹配')} {$t('区分大小写的正则匹配')}
{$t('不区分大小写的正则匹配')} {$t('不区分大小写的正则匹配')}
{$t('任意匹配')} {$t('任意匹配')}
{$t('驳回')} {$t('驳回')}
{$t('已订阅')} {$t('已订阅')}
{$t('取消申请')} {$t('取消申请')}
{$t('透传客户端请求 Host')} {$t('透传客户端请求 Host')}
{$t('使用上游服务 Host')} {$t('使用上游服务 Host')}
{$t('重写 Host')} {$t('重写 Host')}
{$t('动态服务发现')} {$t('动态服务发现')}
{$t('地址')} {$t('地址')}
{$t('新增')} {$t('新增')}
{$t('申请方消费者')} {$t('申请方消费者')}
{$t('策略名称')} {$t('策略名称')}
{$t('优先级')} {$t('优先级')}
{$t('筛选条件')} {$t('筛选条件')}
{$t('处理数')} {$t('处理数')}
{$t('数据格式')} {$t('数据格式')}
{$t('关键字')} {$t('关键字')}
{$t('正则表达式')} {$t('正则表达式')}
{$t('手机号')} {$t('手机号')}
{$t('身份证号')} {$t('身份证号')}
{$t('银行卡号')} {$t('银行卡号')}
{$t('金额')} {$t('金额')}
{$t('日期')} {$t('日期')}
{$t('局部显示')} {$t('局部显示')}
{$t('局部遮蔽')} {$t('局部遮蔽')}
{$t('截取')} {$t('截取')}
{$t('替换')} {$t('替换')}
{$t('乱序')} {$t('乱序')}
{$t('随机字符串')} {$t('随机字符串')}
{$t('自定义字符串')} {$t('自定义字符串')}
{$t('请输入IP地址或CIDR范围,每条以换行分割')} {$t('请输入IP地址或CIDR范围,每条以换行分割')}
{$t('待更新')} {$t('待更新')}
{$t('待删除')} {$t('待删除')}
{$t('内容')} {$t('内容')}
{$t('调用地址')} {$t('调用地址')}
{$t('消费者 IP')} {$t('消费者 IP')}
{$t('鉴权名称')} {$t('鉴权名称')}
</> </>
) )
} }
@@ -1,49 +1,47 @@
import { PERMISSION_DEFINITION } from '@common/const/permissions'
import { Button, Tooltip, Upload } from "antd"; import { $t } from '@common/locales'
import { ReactElement, cloneElement, useEffect, useMemo, useState } from "react"; import { Button, Tooltip, Upload } from 'antd'
import { useGlobalContext } from "../../contexts/GlobalStateContext"; import { ReactElement, cloneElement, useEffect, useMemo, useState } from 'react'
import { PERMISSION_DEFINITION } from "@common/const/permissions"; import { useGlobalContext } from '../../contexts/GlobalStateContext'
import { $t } from "@common/locales";
import { last } from "lodash-es";
type WithPermissionProps = { type WithPermissionProps = {
access?:string | string[] access?: string | string[]
tooltip?:string tooltip?: string
children:ReactElement children: ReactElement
disabled?:boolean disabled?: boolean
showDisabled?:boolean showDisabled?: boolean
} }
// 权限控制的高阶组件 // 权限控制的高阶组件
const WithPermission = ({access, tooltip, children,disabled, showDisabled = true}:WithPermissionProps) => { const WithPermission = ({ access, tooltip, children, disabled, showDisabled = true }: WithPermissionProps) => {
const [editAccess, setEditAccess] = useState<boolean>(access ? false : true)
const [editAccess, setEditAccess] = useState<boolean>(access ? false:true) const { accessData, checkPermission, accessInit } = useGlobalContext()
const {accessData,checkPermission,accessInit} = useGlobalContext()
const lastAccess = useMemo(()=>{ const lastAccess = useMemo(() => {
if(!access) return true if (!access) return true
return checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]) return checkPermission(access as keyof (typeof PERMISSION_DEFINITION)[0])
},[access, accessData,checkPermission,accessInit]) }, [access, accessData, checkPermission, accessInit])
useEffect(()=>{ useEffect(() => {
// 先判断权限,无论权限是否为true,如果disabled为true时则必须为ture // 先判断权限,无论权限是否为true,如果disabled为true时则必须为ture
access && setEditAccess(lastAccess) access && setEditAccess(lastAccess)
},[lastAccess,disabled]) }, [lastAccess, disabled])
return (
<>
{}
{editAccess && !disabled && cloneElement(children)}
{editAccess && disabled && <Tooltip title={tooltip}>{cloneElement(children, { disabled: true })}</Tooltip>}
{!editAccess && children?.type !== Button && children?.type !== Upload && showDisabled && (
<Tooltip title={tooltip ?? $t('暂无操作权限,请联系管理员分配。')}>
{cloneElement(children, {
disabled: true,
onClick: (e) => e.preventDefault(),
okButtonProps: { disabled: true }
})}
</Tooltip>
)}
</>
)
}
export default WithPermission
return (
<>{
}
{editAccess && !disabled && cloneElement(children)}
{editAccess && disabled && <Tooltip title={tooltip}>
{ cloneElement(children, {disabled:true})}
</Tooltip>}
{!editAccess && (children?.type !== Button && children?.type !== Upload && showDisabled) && <Tooltip title={tooltip ?? $t("暂无操作权限,请联系管理员分配。")}>
{ cloneElement(children, {disabled:true, onClick:(e)=>e.preventDefault(),okButtonProps:{disabled:true}})}
</Tooltip>}
</>
);
}
export default WithPermission
@@ -1,79 +1,85 @@
import { set } from 'lodash-es'; import { ExoticComponent, JSXElementConstructor, useEffect, useState } from 'react'
import { ExoticComponent, JSXElementConstructor, ReactElement, useEffect, useState } from 'react'; import { useBlocker, useLocation } from 'react-router-dom'
import { useBlocker, useLocation, useNavigate } from 'react-router-dom'; import { JSX } from 'react/jsx-runtime'
import { JSX } from 'react/jsx-runtime';
const withRouteGuard = (WrappedComponent: ExoticComponent<any> | JSXElementConstructor<any>, { const withRouteGuard = (
canActivate, WrappedComponent: ExoticComponent<any> | JSXElementConstructor<any>,
canLoad , {
canDeactivate, canActivate,
deactivated, canLoad,
pathPrefix canDeactivate,
}: { pathPrefix?:string, canActivate?: () => Promise<boolean>; canLoad?: () => Promise<boolean>; canDeactivate?: () => Promise<boolean>; deactivated?: () => Promise<void>; } = {}) => { deactivated,
pathPrefix
}: {
pathPrefix?: string
canActivate?: () => Promise<boolean>
canLoad?: () => Promise<boolean>
canDeactivate?: () => Promise<boolean>
deactivated?: () => Promise<void>
} = {}
) => {
return function RouteGuard(props: JSX.IntrinsicAttributes) { return function RouteGuard(props: JSX.IntrinsicAttributes) {
const [isActivated, setIsActivated] = useState<boolean>(false); const [isActivated, setIsActivated] = useState<boolean>(false)
const location = useLocation(); const location = useLocation()
// check canActivate // check canActivate
const startLifecycle = async ()=>{ const startLifecycle = async () => {
if(canActivate){ if (canActivate) {
const activateRes = await canActivate(); const activateRes = await canActivate()
setIsActivated(activateRes); setIsActivated(activateRes)
}else{ } else {
setIsActivated(true); setIsActivated(true)
}
}
// check canDeactivate
const handleBeforeUnload = async (event: { preventDefault: () => void; returnValue: string }) => {
const deactivateRes = canDeactivate ? await canDeactivate() : true
if (!deactivateRes) {
event.preventDefault()
event.returnValue = ''
} }
} }
// check canDeactivate
const handleBeforeUnload =async (event: { preventDefault: () => void; returnValue: string; }) => {
const deactivateRes = canDeactivate? await canDeactivate():true;
if (!deactivateRes) {
event.preventDefault();
event.returnValue = '';
}
};
// 激活组件时的检查 // 激活组件时的检查
useEffect(() => { useEffect(() => {
startLifecycle(); startLifecycle()
window.addEventListener('beforeunload', handleBeforeUnload); window.addEventListener('beforeunload', handleBeforeUnload)
return () => { return () => {
window.removeEventListener('beforeunload', handleBeforeUnload); window.removeEventListener('beforeunload', handleBeforeUnload)
deactivated?.(); deactivated?.()
}; }
}, []); }, [])
const blocker = useBlocker((tx) => { const blocker = useBlocker((tx) => {
const currentPath = location.pathname; const currentPath = location.pathname
const targetPath = tx.nextLocation.pathname; const targetPath = tx.nextLocation.pathname
if (pathPrefix && currentPath.startsWith(pathPrefix) && !targetPath.startsWith(pathPrefix) && canDeactivate) { if (pathPrefix && currentPath.startsWith(pathPrefix) && !targetPath.startsWith(pathPrefix) && canDeactivate) {
canDeactivate().then((res) => { canDeactivate().then((res) => {
if(res){ if (res) {
return false; return false
}else{ } else {
return true; return true
} }
}) })
} else { } else {
return false return false
} }
}); })
const checkCanLoad = async () => {
const checkCanLoad = async()=>{ const loadRes = await canLoad!()
const loadRes = await canLoad!(); !loadRes && setIsActivated(false)
!loadRes && setIsActivated(false);
} }
useEffect(() => { useEffect(() => {
if (isActivated && canLoad) { if (isActivated && canLoad) {
checkCanLoad() checkCanLoad()
} }
}, [isActivated]); }, [isActivated])
return isActivated ? <WrappedComponent {...props}/> : null; return isActivated ? <WrappedComponent {...props} /> : null
}; }
} }
export default withRouteGuard; export default withRouteGuard
@@ -1,150 +1,143 @@
import { forwardRef, useImperativeHandle, useState } from 'react'
import {forwardRef, useImperativeHandle, useState} from 'react'
import { Input } from 'antd' import { Input } from 'antd'
import { Icon } from '@iconify/react/dist/iconify.js' import { Icon } from '@iconify/react/dist/iconify.js'
export const ArrayItemBlankComponent = forwardRef( export const ArrayItemBlankComponent = forwardRef((props: { [k: string]: any }, ref) => {
(props: { [k: string]: any }, ref) => { const { onChange, value, dataFormat } = props
const { onChange, value, dataFormat } = props
const getDefaultListItem = () => { const getDefaultListItem = () => {
const defaultData: { [k: string]: unknown } = {} const defaultData: { [k: string]: unknown } = {}
for (const data of dataFormat) { for (const data of dataFormat) {
defaultData[data.key] = '' defaultData[data.key] = ''
}
return [defaultData]
} }
const [resList, setResList] = useState( return [defaultData]
value && Object.keys(value).length > 0 }
? [
...value const [resList, setResList] = useState(
?.filter((v: string) => { value && Object.keys(value).length > 0
return v ? [
}) ...value
?.map((v: string) => { ?.filter((v: string) => {
const vTmp = v return v
const newValue: { [k: string]: unknown } = {} })
for (let index = 0; index < dataFormat.length; index++) { ?.map((v: string) => {
if (dataFormat[index]?.hideName) { const vTmp = v
newValue[dataFormat[index].key] = vTmp.split(' ')[index] const newValue: { [k: string]: unknown } = {}
} else { for (let index = 0; index < dataFormat.length; index++) {
const vTmp2: string | string[] | undefined = if (dataFormat[index]?.hideName) {
vTmp.indexOf(' ') === -1 newValue[dataFormat[index].key] = vTmp.split(' ')[index]
? vTmp } else {
: vTmp.split(' ')[index] const vTmp2: string | string[] | undefined =
vTmp.indexOf(' ') === -1
? vTmp
: vTmp.split(' ')[index]
? vTmp.split(' ')[index].indexOf('=') === -1 ? vTmp.split(' ')[index].indexOf('=') === -1
? '' ? ''
: vTmp.split(' ')[index].split('=') : vTmp.split(' ')[index].split('=')
: '' : ''
if (vTmp2 && vTmp2 instanceof Array && vTmp2.length > 0) { if (vTmp2 && vTmp2 instanceof Array && vTmp2.length > 0) {
vTmp2.shift() vTmp2.shift()
}
newValue[dataFormat[index].key] =
vTmp2 instanceof Array ? vTmp2?.join('=') : vTmp2
} }
newValue[dataFormat[index].key] = vTmp2 instanceof Array ? vTmp2?.join('=') : vTmp2
} }
return newValue }
}), return newValue
...getDefaultListItem() }),
] ...getDefaultListItem()
: [...getDefaultListItem()] ]
) : [...getDefaultListItem()]
)
useImperativeHandle(ref, () => ({})) useImperativeHandle(ref, () => ({}))
const emitNewArr = () => { const emitNewArr = () => {
const newArr: Array<string> = [] const newArr: Array<string> = []
for (const r of resList) { for (const r of resList) {
if (r[dataFormat[0].key]) { if (r[dataFormat[0].key]) {
newArr.push( newArr.push(
dataFormat?.map((format: { key: string; hideName: boolean }) => { dataFormat
return format?.hideName ?.map((format: { key: string; hideName: boolean }) => {
? r[format.key] return format?.hideName ? r[format.key] : `${format.key}=${r[format.key]}`
: `${format.key}=${r[format.key]}` })
}) .join(' ')
.join(' ') )
)
}
}
onChange(newArr)
}
const changeInputValue = (
newValue: string,
index: number,
keyName: string,
dataFormat: unknown
) => {
const newArr = [...resList]
newArr[index][keyName] = newValue
newArr[index].status =
(dataFormat.required && !newValue) ||
(dataFormat.pattern && !dataFormat.pattern.test(newValue))
? 'error'
: ''
setResList(newArr)
emitNewArr()
if (index === resList.length - 1) {
setResList([...newArr, ...getDefaultListItem()])
} }
} }
onChange(newArr)
const addLine = (index: number) => {
resList.splice(index + 1, 0, ...getDefaultListItem())
const newKvList = [...resList]
setResList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
resList.splice(index, 1)
const newKvList = [...resList]
setResList([...newKvList])
emitNewArr()
}
return (
<div>
{resList?.map((n: unknown, index: unknown) => {
return (
<div
key={n + index}
className="flex"
style={{ marginTop: index === 0 ? '0px' : '16px' }}
>
{dataFormat?.map((data: unknown, index2: unknown) => {
return (
<Input
key={data.key + index2}
className="mr-[8px]"
style={{ width: data.width }}
value={n[data.key]}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, data.key, data)
}}
placeholder={data.placeholder || `请输入${data.key}`}
status={n.status}
type={data.type || 'text'}
/>
)
})}
{index !== resList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n[dataFormat[0].key] && (
<Icon icon="ic:baseline-add" onClick={() => addLine(index as unknown as number)} width="14" height="14"/>
)}
<Icon icon="ic:baseline-minus" onClick={() => removeLine(index as unknown as number)} width="14" height="14"/>
</div>
)}
</div>
)
})}
</div>
)
} }
)
const changeInputValue = (newValue: string, index: number, keyName: string, dataFormat: unknown) => {
const newArr = [...resList]
newArr[index][keyName] = newValue
newArr[index].status =
(dataFormat.required && !newValue) || (dataFormat.pattern && !dataFormat.pattern.test(newValue)) ? 'error' : ''
setResList(newArr)
emitNewArr()
if (index === resList.length - 1) {
setResList([...newArr, ...getDefaultListItem()])
}
}
const addLine = (index: number) => {
resList.splice(index + 1, 0, ...getDefaultListItem())
const newKvList = [...resList]
setResList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
resList.splice(index, 1)
const newKvList = [...resList]
setResList([...newKvList])
emitNewArr()
}
return (
<div>
{resList?.map((n: unknown, index: unknown) => {
return (
<div key={n + index} className="flex" style={{ marginTop: index === 0 ? '0px' : '16px' }}>
{dataFormat?.map((data: unknown, index2: unknown) => {
return (
<Input
key={data.key + index2}
className="mr-[8px]"
style={{ width: data.width }}
value={n[data.key]}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, data.key, data)
}}
placeholder={data.placeholder || `请输入${data.key}`}
status={n.status}
type={data.type || 'text'}
/>
)
})}
{index !== resList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n[dataFormat[0].key] && (
<Icon
icon="ic:baseline-add"
onClick={() => addLine(index as unknown as number)}
width="14"
height="14"
/>
)}
<Icon
icon="ic:baseline-minus"
onClick={() => removeLine(index as unknown as number)}
width="14"
height="14"
/>
</div>
)}
</div>
)
})}
</div>
)
})
@@ -1,47 +1,35 @@
import { forwardRef, useImperativeHandle, useState } from 'react'
import {forwardRef, useImperativeHandle, useState} from 'react'
import { Codebox } from '@common/components/postcat/api/Codebox' import { Codebox } from '@common/components/postcat/api/Codebox'
export const CustomCodeboxComponent = forwardRef( export const CustomCodeboxComponent = forwardRef((props: { [k: string]: unknown }, ref) => {
(props: { [k: string]: unknown }, ref) => { const { mode = 'yaml', theme = 'xcode', fontSize, height, width = '100%', onChange, value } = props
const { const [code, setCode] = useState(mode === 'json' ? JSON.stringify(value) : value)
mode = 'yaml', useImperativeHandle(ref, () => ({}))
theme = 'xcode', const handleChange = (value: string) => {
fontSize, setCode(value)
height, let res = value
width = '100%', if (mode === 'json') {
onChange, try {
value res = JSON.parse(value)
} = props } catch {
const [code, setCode] = useState( console.warn(' 输入的json语句格式有误')
mode === 'json' ? JSON.stringify(value) : value
)
useImperativeHandle(ref, () => ({}))
const handleChange = (value: string) => {
setCode(value)
let res = value
if (mode === 'json') {
try {
res = JSON.parse(value)
} catch {
console.warn(' 输入的json语句格式有误')
}
} }
onChange(res)
} }
onChange(res)
return (
<div className=" mt-[4px] border-[1px] border-solid border-BORDER">
<Codebox
value={code}
language={mode}
enableToolbar={false}
theme={theme}
fontSize={fontSize}
height={height ?? 500}
width={width}
onChange={handleChange} />
</div>
)
} }
)
return (
<div className=" mt-[4px] border-[1px] border-solid border-BORDER">
<Codebox
value={code}
language={mode}
enableToolbar={false}
theme={theme}
fontSize={fontSize}
height={height ?? 500}
width={width}
onChange={handleChange}
/>
</div>
)
})
@@ -1,5 +1,4 @@
import { forwardRef, useImperativeHandle } from 'react'
import {forwardRef,useImperativeHandle} from 'react'
import { createSchemaField } from '@formily/react' import { createSchemaField } from '@formily/react'
import { import {
FormItem, FormItem,
@@ -82,57 +81,53 @@ const SchemaField = createSchemaField({
} }
}) })
export const CustomDialogComponent = forwardRef( export const CustomDialogComponent = forwardRef((props: { [k: string]: unknown }, ref) => {
(props: { [k: string]: unknown }, ref) => { const { onChange, title, value, render } = props
const { onChange, title, value, render } = props useImperativeHandle(ref, () => ({}))
useImperativeHandle(ref, () => ({})) let editPage: boolean = false
let editPage: boolean = false try {
try { editPage = Object.keys(JSON.parse(JSON.stringify(value))).length > 0
editPage = Object.keys(JSON.parse(JSON.stringify(value))).length > 0 } catch {}
} catch {}
return ( return (
<FormDialog.Portal> <FormDialog.Portal>
<span <span
className="ant-formily-array-base-config" className="ant-formily-array-base-config"
onClick={() => { onClick={() => {
const dialog = FormDialog( const dialog = FormDialog(editPage ? $t('编辑(0)', [title || '']) : $t('添加(0)', [title || '']), () => {
editPage ? $t('编辑(0)',[title||'']) : $t('添加(0)',[title||'']), return (
() => { <FormLayout
return ( // labelCol={6}
<FormLayout layout={'vertical'}
// labelCol={6} scrollToFirstError
layout={'vertical'} name="CustomDialogComponent"
scrollToFirstError // wrapperCol={10}
name="CustomDialogComponent" form={value}
// wrapperCol={10} >
form={value}> <SchemaField schema={JSON.parse(render)} />
<SchemaField schema={JSON.parse(render)} /> </FormLayout>
</FormLayout>
)
}
) )
dialog })
.forOpen((payload, next) => { dialog
next({ .forOpen((payload, next) => {
initialValues: value next({
}) initialValues: value
}) })
.forConfirm((payload, next) => { })
next(payload) .forConfirm((payload, next) => {
}) next(payload)
.forCancel((payload, next) => { })
next(payload) .forCancel((payload, next) => {
}) next(payload)
.open() })
.then(onChange) .open()
}} .then(onChange)
> }}
<svg style={{ width: '16px', height: '16px' }}> >
<use href="#tool"></use> <svg style={{ width: '16px', height: '16px' }}>
</svg> <use href="#tool"></use>
</span> </svg>
</FormDialog.Portal> </span>
) </FormDialog.Portal>
} )
) })
@@ -1,104 +1,99 @@
import {forwardRef, useImperativeHandle, useState} from 'react' import { forwardRef, useImperativeHandle, useState } from 'react'
import { Input } from '@formily/antd-v5' import { Input } from '@formily/antd-v5'
import { $t } from '@common/locales' import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js' import { Icon } from '@iconify/react/dist/iconify.js'
export const SimpleMapComponent = forwardRef( export const SimpleMapComponent = forwardRef((props: { [k: string]: unknown }, ref) => {
(props: { [k: string]: unknown }, ref) => { const { onChange, value, placeholderKey = $t('请输入Key'), placeholderValue = $t('请输入Value') } = props
const {
onChange,
value,
placeholderKey = $t('请输入Key'),
placeholderValue = $t('请输入Value')
} = props
const [kvList, setKvList] = useState( const [kvList, setKvList] = useState(
value && Object.keys(value).length > 0 value && Object.keys(value).length > 0
? [ ? [
...Object.keys(value)?.map((k: string) => { ...Object.keys(value)?.map((k: string) => {
return { key: k, value: value[k] } return { key: k, value: value[k] }
}), }),
{ key: '', value: '' } { key: '', value: '' }
] ]
: [{ key: '', value: '' }] : [{ key: '', value: '' }]
) )
useImperativeHandle(ref, () => ({})) useImperativeHandle(ref, () => ({}))
const emitNewArr = () => { const emitNewArr = () => {
const res: { [k: string]: unknown } = {} const res: { [k: string]: unknown } = {}
for (const kv of kvList) { for (const kv of kvList) {
res[kv.key] = kv.value res[kv.key] = kv.value
}
onChange(res)
} }
onChange(res)
const changeInputValue = (
newValue: string,
index: number,
type: 'key' | 'value'
) => {
const newArr = [...kvList]
newArr[index][type] = newValue
setKvList(newArr)
emitNewArr()
if (index === kvList.length - 1) {
setKvList([...newArr, { key: '', value: '' }])
}
}
const addLine = (index: number) => {
kvList.splice(index + 1, 0, { key: '', value: '' })
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
kvList.splice(index, 1)
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
return (
<div>
{kvList?.map((n: unknown, index: unknown) => {
return (
<div
key={n + index}
className="flex"
style={{ marginTop: index === 0 ? '0px' : '16px' }}
>
<Input
className="w-INPUT_NORMAL mr-[8px]"
style={{ width: '174px' }}
value={n.key}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'key')
}}
placeholder={placeholderKey}
/>
<Input
style={{ width: '164px', marginRight: '10px' }}
value={n.value}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'value')
}}
placeholder={placeholderValue}
/>
{index !== kvList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n.key && (
<Icon icon="ic:baseline-add" onClick={() => addLine(index as unknown as number)} width="14" height="14"/>
)}
<Icon icon="ic:baseline-minus" onClick={() => removeLine(index as unknown as number)} width="14" height="14"/>
</div>
)}
</div>
)
})}
</div>
)
} }
)
const changeInputValue = (newValue: string, index: number, type: 'key' | 'value') => {
const newArr = [...kvList]
newArr[index][type] = newValue
setKvList(newArr)
emitNewArr()
if (index === kvList.length - 1) {
setKvList([...newArr, { key: '', value: '' }])
}
}
const addLine = (index: number) => {
kvList.splice(index + 1, 0, { key: '', value: '' })
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
const removeLine = (index: number) => {
kvList.splice(index, 1)
const newKvList = [...kvList]
setKvList(newKvList)
emitNewArr()
}
return (
<div>
{kvList?.map((n: unknown, index: unknown) => {
return (
<div key={n + index} className="flex" style={{ marginTop: index === 0 ? '0px' : '16px' }}>
<Input
className="w-INPUT_NORMAL mr-[8px]"
style={{ width: '174px' }}
value={n.key}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'key')
}}
placeholder={placeholderKey}
/>
<Input
style={{ width: '164px', marginRight: '10px' }}
value={n.value}
onChange={(e: unknown) => {
changeInputValue(e.target.value, index, 'value')
}}
placeholder={placeholderValue}
/>
{index !== kvList.length - 1 && (
<div style={{ display: 'inline-block' }}>
{n.key && (
<Icon
icon="ic:baseline-add"
onClick={() => addLine(index as unknown as number)}
width="14"
height="14"
/>
)}
<Icon
icon="ic:baseline-minus"
onClick={() => removeLine(index as unknown as number)}
width="14"
height="14"
/>
</div>
)}
</div>
)
})}
</div>
)
})
@@ -1,327 +1,333 @@
import {forwardRef, useEffect, useImperativeHandle, useMemo, useState} from "react"; import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
import { action } from '@formily/reactive' import { action } from '@formily/reactive'
import { import {
FormItem, FormItem,
Space, Space,
ArrayItems, ArrayItems,
DatePicker, DatePicker,
Editable, Editable,
FormButtonGroup, FormButtonGroup,
Input, Input,
Radio, Radio,
Select, Select,
Submit, Submit,
Cascader, Cascader,
Form, Form,
FormGrid, FormGrid,
FormLayout, FormLayout,
Upload, Upload,
ArrayCollapse,
ArrayTable,
ArrayTabs,
Checkbox,
FormCollapse,
FormDialog,
FormDrawer,
FormStep,
FormTab,
NumberPicker,
Password,
PreviewText,
Reset,
SelectTable,
Switch,
TimePicker,
Transfer,
TreeSelect,
ArrayCards
} from '@formily/antd-v5'
import { createForm } from '@formily/core'
import { CustomCodeboxComponent } from '@common/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx'
import { SimpleMapComponent } from '@common/components/aoplatform/formily2-customize/SimpleMapComponent.tsx'
import { CustomDialogComponent } from '@common/components/aoplatform/formily2-customize/CustomDialogComponent.tsx'
import { ArrayItemBlankComponent } from '@common/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx'
import { DefaultOptionType } from 'antd/es/cascader'
import { createSchemaField, FormProvider, RecursionField, useField, useForm } from '@formily/react'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { useFetch } from '@common/hooks/http.ts'
import { App } from 'antd'
import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { setValidateLanguage } from '@formily/core'
export const DynamicRender = (props) => {
const { schema } = props
const field = useField()
const form = useForm()
const [renderSchema, setRenderSchema] = useState({})
const { state } = useGlobalContext()
useEffect(() => {
form.clearFormGraph(`${field.address}.*`)
try {
const parsedSchema = JSON.parse(schema)
setRenderSchema(parsedSchema[form?.values?.driver])
} catch (e) {
console.error('渲染出错', e?.message)
}
}, [form.values.driver])
const translateSchema = (render) => {
const res1 = {
...render,
...(render.title ? { title: $t(render.title) } : {}),
...(render.description ? { description: $t(render.description) } : {}),
...(render.label ? { label: $t(render.label) } : {}),
...(render.properties
? {
properties: Object.keys(render.properties).reduce((total, cur) => {
try {
total[cur] = translateSchema(render.properties[cur])
} catch (error) {
console.error(`Error translating schema for property ${cur}:`, error)
}
return total
}, {})
}
: {}),
...(render.items && Array.isArray(render.items) ? { items: render.items.map((x) => translateSchema(x)) } : {}),
...(render.items && !Array.isArray(render.items) ? { items: translateSchema(render.items) } : {}),
...(render.additionalProperties ? { additionalProperties: translateSchema(render.additionalProperties) } : {}),
...(render.enum ? { enum: render.enum.map((x) => ({ ...x, label: $t(x.label) })) } : {})
}
return res1
}
const translatedRenderSchema = useMemo(() => {
const res = renderSchema && translateSchema(renderSchema)
return res
}, [state.language, renderSchema])
return <RecursionField basePath={field.address} schema={translatedRenderSchema} onlyRenderProperties />
}
export type IntelligentPluginConfigProps = {
type: 'add' | 'edit'
renderSchema: unknown
tabData: DefaultOptionType[]
moduleId: string
driverSelectionOptions: DefaultOptionType[]
entityId?: string
initFormValue: { [k: string]: unknown }
}
export type IntelligentPluginConfigHandle = {
save: () => Promise<boolean | string>
}
const SchemaField = createSchemaField({
components: {
ArrayCards,
ArrayCollapse, ArrayCollapse,
ArrayItems,
ArrayTable, ArrayTable,
ArrayTabs, ArrayTabs,
Cascader,
Checkbox, Checkbox,
DatePicker,
Editable,
Form,
FormButtonGroup,
FormCollapse, FormCollapse,
// @ts-ignore
FormDialog, FormDialog,
// @ts-ignore
FormDrawer, FormDrawer,
FormGrid,
FormItem,
FormLayout,
FormStep, FormStep,
FormTab, FormTab,
Input,
NumberPicker, NumberPicker,
Password, Password,
PreviewText, PreviewText,
Radio,
Reset, Reset,
Select,
SelectTable, SelectTable,
Space,
Submit,
Switch, Switch,
TimePicker, TimePicker,
Transfer, Transfer,
TreeSelect, TreeSelect,
ArrayCards Upload,
} from '@formily/antd-v5' CustomCodeboxComponent,
import { createForm } from '@formily/core' SimpleMapComponent,
import {CustomCodeboxComponent} from "@common/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx"; CustomDialogComponent,
import {SimpleMapComponent} from "@common/components/aoplatform/formily2-customize/SimpleMapComponent.tsx"; ArrayItemBlankComponent,
import {CustomDialogComponent} from "@common/components/aoplatform/formily2-customize/CustomDialogComponent.tsx"; DynamicRender
import {ArrayItemBlankComponent} from "@common/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx"; }
import {DefaultOptionType} from "antd/es/cascader";
import {createSchemaField, FormProvider, RecursionField, useField, useForm} from "@formily/react";
import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx";
import {useFetch} from "@common/hooks/http.ts";
import {App, Descriptions} from "antd";
import { $t } from "@common/locales";
import { useGlobalContext } from "@common/contexts/GlobalStateContext";
import { setValidateLanguage } from '@formily/core'
export const DynamicRender = (props) => {
const {schema} = props
const field = useField()
const form = useForm()
const [renderSchema, setRenderSchema] = useState({})
const {state} = useGlobalContext()
useEffect(() => {
form.clearFormGraph(`${field.address}.*`)
try{
const parsedSchema = JSON.parse(schema)
setRenderSchema(parsedSchema[form?.values?.driver])
}catch(e){
console.error('渲染出错',e?.message)
}
}, [form.values.driver])
const translateSchema = (render) =>{
const res1 = {
...render,
...(render.title ? {title:$t(render.title)} : {}),
...(render.description) ? {description:$t(render.description)} : {},
...(render.label ? {label:$t(render.label)} : {}),
...(render.properties ? {properties: Object.keys(render.properties).reduce((total, cur) => {
try {
total[cur] = translateSchema(render.properties[cur]);
} catch (error) {
console.error(`Error translating schema for property ${cur}:`, error);
}
return total;
}, {})} : {}),
...(render.items && Array.isArray(render.items) ? {items:render.items.map(x=>translateSchema(x))} : {}),
...(render.items && !Array.isArray(render.items) ? {items:translateSchema(render.items)} : {}),
...(render.additionalProperties ? {additionalProperties: translateSchema(render.additionalProperties)} : {}),
...(render.enum ? {enum: render.enum.map(x=>({...x, label:$t(x.label)}))} : {}),
}
return res1
}
const translatedRenderSchema = useMemo(()=>{
const res = renderSchema && translateSchema(renderSchema)
return res
},[state.language,renderSchema])
return (
<RecursionField
basePath={field.address}
schema={translatedRenderSchema}
onlyRenderProperties
/>
)
}
export type IntelligentPluginConfigProps = {
type:'add'|'edit'
renderSchema:unknown
tabData:DefaultOptionType[]
moduleId:string
driverSelectionOptions:DefaultOptionType[]
entityId?:string
initFormValue:{[k:string]:unknown}
}
export type IntelligentPluginConfigHandle = {
save:()=>Promise<boolean | string>
}
const SchemaField = createSchemaField({
components: {
ArrayCards,
ArrayCollapse,
ArrayItems,
ArrayTable,
ArrayTabs,
Cascader,
Checkbox,
DatePicker,
Editable,
Form,
FormButtonGroup,
FormCollapse,
// @ts-ignore
FormDialog,
// @ts-ignore
FormDrawer,
FormGrid,
FormItem,
FormLayout,
FormStep,
FormTab,
Input,
NumberPicker,
Password,
PreviewText,
Radio,
Reset,
Select,
SelectTable,
Space,
Submit,
Switch,
TimePicker,
Transfer,
TreeSelect,
Upload,
CustomCodeboxComponent,
SimpleMapComponent,
CustomDialogComponent,
ArrayItemBlankComponent,
DynamicRender
}
}) })
export const IntelligentPluginConfig = forwardRef<IntelligentPluginConfigHandle,IntelligentPluginConfigProps>((props,ref)=>{ export const IntelligentPluginConfig = forwardRef<IntelligentPluginConfigHandle, IntelligentPluginConfigProps>(
const { type,renderSchema,moduleId,driverSelectionOptions,initFormValue} = props (props, ref) => {
const { type, renderSchema, moduleId, driverSelectionOptions, initFormValue } = props
const { message } = App.useApp() const { message } = App.useApp()
const {fetchData} = useFetch() const { fetchData } = useFetch()
const form = createForm({ validateFirst: type === 'edit' }) const form = createForm({ validateFirst: type === 'edit' })
form.setInitialValues(initFormValue || {}) form.setInitialValues(initFormValue || {})
const { state } = useGlobalContext() const { state } = useGlobalContext()
useEffect(()=>{ useEffect(() => {
setValidateLanguage(state.language) setValidateLanguage(state.language)
},[state.language]) }, [state.language])
const pluginEditSchema = { const pluginEditSchema = {
type: 'object', type: 'object',
properties: { properties: {
layout: { layout: {
type: 'void', type: 'void',
'x-component': 'FormLayout', 'x-component': 'FormLayout',
'x-component-props': { 'x-component-props': {
labelCol: 6, labelCol: 6,
wrapperCol: 10, wrapperCol: 10,
layout: 'vertical', layout: 'vertical'
},
properties: {
id: {
type: 'string',
title: $t('ID'),
required: true,
pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/,
'x-decorator': 'FormItem',
'x-decorator-props': {
labelCol: 4,
wrapperCol: 20,
labelAlign: 'left'
}, },
properties: { 'x-component': 'Input',
id: { 'x-component-props': {
type: 'string', placeholder: $t(PLACEHOLDER.specialStartWithAlphabet)
title: $t('ID'), },
required: true, 'x-disabled': type === 'edit'
pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/, },
'x-decorator': 'FormItem', title: {
'x-decorator-props': { type: 'string',
labelCol:4, title: $t('名称'),
wrapperCol: 20, required: true,
labelAlign:'left' 'x-decorator': 'FormItem',
}, 'x-decorator-props': {
'x-component': 'Input', labelCol: 4,
'x-component-props': { wrapperCol: 20,
placeholder: $t(PLACEHOLDER.specialStartWithAlphabet), labelAlign: 'left'
}, },
'x-disabled': type === 'edit' 'x-component': 'Input',
}, 'x-component-props': {
title: { placeholder: $t(PLACEHOLDER.input)
type: 'string', }
title: $t('名称'), },
required: true, driver: {
'x-decorator': 'FormItem', type: 'string',
'x-decorator-props': { title: $t('Driver'),
labelCol:4, required: true,
wrapperCol: 20, 'x-decorator': 'FormItem',
labelAlign:'left' 'x-decorator-props': {
}, labelCol: 4,
'x-component': 'Input', wrapperCol: 20,
'x-component-props': { labelAlign: 'left'
placeholder: $t(PLACEHOLDER.input), },
} 'x-component': 'Select',
}, 'x-component-props': {
driver: { disabled: type === 'edit'
type: 'string', },
title: $t('Driver'), 'x-display': driverSelectionOptions.length > 1 ? 'visible' : 'hidden',
required: true, enum: [...driverSelectionOptions]
'x-decorator': 'FormItem', },
'x-decorator-props': { description: {
labelCol:4, type: 'string',
wrapperCol: 20, title: $t('描述'),
labelAlign:'left' 'x-decorator': 'FormItem',
}, 'x-decorator-props': {
'x-component': 'Select', labelCol: 4,
'x-component-props': { wrapperCol: 20,
disabled: type === 'edit' labelAlign: 'left'
}, },
'x-display': driverSelectionOptions.length > 1 ? 'visible' : 'hidden', 'x-component': 'Input.TextArea',
enum: [...driverSelectionOptions] 'x-component-props': {
}, placeholder: $t(PLACEHOLDER.input)
description: { }
type: 'string', },
title: $t('描述'), config: {
'x-decorator': 'FormItem', type: 'object',
'x-decorator-props': { 'x-component': 'DynamicRender',
labelCol:4, 'x-component-props': {
wrapperCol: 20, schema: JSON.stringify(renderSchema)
labelAlign:'left' }
},
'x-component': 'Input.TextArea',
'x-component-props': {
placeholder: $t(PLACEHOLDER.input),
}
},
config: {
type: 'object',
'x-component': 'DynamicRender',
'x-component-props': {
schema: JSON.stringify(renderSchema),
}
}
} }
}
} }
} }
}
const save :()=>Promise<boolean | string> = ()=>{
return new Promise((resolve, reject)=>{
form.validate().then(()=>{
fetchData<BasicResponse<null>>(type === 'add'?`dynamic/${moduleId}`:`dynamic/${moduleId}/config`,{method:type === 'add'? 'POST' : 'PUT',eoBody:form.values, eoParams:{...(type !== 'add' && {id:initFormValue.id})}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> reject(errorInfo))
}).catch((errorInfo:unknown)=> reject(errorInfo))
})
} }
useImperativeHandle(ref, ()=>({ const save: () => Promise<boolean | string> = () => {
save return new Promise((resolve, reject) => {
}) form
) .validate()
.then(() => {
fetchData<BasicResponse<null>>(type === 'add' ? `dynamic/${moduleId}` : `dynamic/${moduleId}/config`, {
method: type === 'add' ? 'POST' : 'PUT',
eoBody: form.values,
eoParams: { ...(type !== 'add' && { id: initFormValue.id }) }
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => reject(errorInfo))
})
.catch((errorInfo: unknown) => reject(errorInfo))
})
}
useImperativeHandle(ref, () => ({
save
}))
const getSkillData = async (skill: string) => { const getSkillData = async (skill: string) => {
return new Promise((resolve,reject) => { return new Promise((resolve, reject) => {
fetchData<BasicResponse<{[k:string]:Array<{name:string,title:string}>}>>(`api/common/provider/${skill}`,{method:'GET'}).then(response=>{ fetchData<BasicResponse<{ [k: string]: Array<{ name: string; title: string }> }>>(
const {code,data,msg} = response `api/common/provider/${skill}`,
if(code === STATUS_CODE.SUCCESS){ { method: 'GET' }
resolve(data[skill]?.map((x:{name:string,title:string})=>{return{label:x.title, value:x.name}}) || []) ).then((response) => {
}else{ const { code, data, msg } = response
message.error(msg || $t(RESPONSE_TIPS.error)) if (code === STATUS_CODE.SUCCESS) {
reject(msg || $t(RESPONSE_TIPS.error)) resolve(
} data[skill]?.map((x: { name: string; title: string }) => {
}) return { label: x.title, value: x.name }
}) || []
)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}) })
})
} }
const useAsyncDataSource = const useAsyncDataSource = (service: unknown, skill: string) => (field: unknown) => {
(service: unknown, skill: string) => (field: unknown) => { field.loading = true
field.loading = true service(skill).then(
service(skill).then( action.bound &&
action.bound && action.bound((data: unknown) => {
action.bound((data: unknown) => { field.dataSource = data
field.dataSource = data field.loading = false
field.loading = false })
}) )
) }
}
return ( return (
<div className="pl-[12px]"> <div className="pl-[12px]">
<FormProvider form={form} > <FormProvider form={form}>
<SchemaField <SchemaField schema={pluginEditSchema} scope={{ useAsyncDataSource, getSkillData, form }} />
schema={pluginEditSchema} </FormProvider>
scope={{ useAsyncDataSource, getSkillData, form }} </div>
/> )
</FormProvider> }
</div>) )
})
@@ -1,349 +1,434 @@
import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx"; import { LoadingOutlined } from '@ant-design/icons'
import {App, Divider, Spin} from "antd"; import { ActionType, ParamsType } from '@ant-design/pro-components'
import {useEffect, useMemo, useRef, useState} from "react"; import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter.tsx'
import { useLocation, useOutletContext, useParams} from "react-router-dom"; import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx'
import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx'
import {ActionType, ParamsType} from "@ant-design/pro-components"; import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import {DefaultOptionType} from "antd/es/cascader"; import { EntityItem } from '@common/const/type.ts'
import {IntelligentPluginConfig, IntelligentPluginConfigHandle} from "./IntelligentPluginConfig.tsx"; import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
import {useFetch} from "@common/hooks/http.ts"; import { useFetch } from '@common/hooks/http.ts'
import {EntityItem} from "@common/const/type.ts"; import { $t } from '@common/locales/index.ts'
import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; import { App, Divider, Spin } from 'antd'
import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx"; import { DefaultOptionType } from 'antd/es/cascader'
import { LoadingOutlined } from "@ant-design/icons"; import { useEffect, useMemo, useRef, useState } from 'react'
import { $t } from "@common/locales/index.ts"; import { useLocation, useOutletContext, useParams } from 'react-router-dom'
import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; import { IntelligentPluginConfig, IntelligentPluginConfigHandle } from './IntelligentPluginConfig.tsx'
type DynamicTableField = { type DynamicTableField = {
name: string, name: string
title: string, title: string
attr: string, attr: string
enum: Array<string> enum: Array<string>
} }
type DynamicDriverData = { type DynamicDriverData = {
name:string, title:string name: string
title: string
} }
export type DynamicTableConfig = { export type DynamicTableConfig = {
basic:{ basic: {
id:string, id: string
name: string, name: string
title: string, title: string
drivers: Array<DynamicDriverData>, drivers: Array<DynamicDriverData>
fields: Array<DynamicTableField>, fields: Array<DynamicTableField>
} }
list: Array<DynamicTableItem>, list: Array<DynamicTableItem>
total:number total: number
} }
export type DynamicRender = { export type DynamicRender = {
render:unknown, render: unknown
basic:{ basic: {
id:string, id: string
name:string, name: string
title:string title: string
} }
} }
export type DynamicPublishCluster = { export type DynamicPublishCluster = {
name:string, name: string
title:string, title: string
status:string, status: string
updater:EntityItem, updater: EntityItem
update_time:string, update_time: string
checked?:boolean checked?: boolean
} }
export type DynamicPublishData = { export type DynamicPublishData = {
id:string, id: string
name:string, name: string
title:string, title: string
description:string description: string
clusters:DynamicPublishCluster[] clusters: DynamicPublishCluster[]
} }
export type DynamicTableItem = {[k:string]:unknown} export type DynamicTableItem = { [k: string]: unknown }
export const StatusColorClass = { export const StatusColorClass = {
"已发布":'text-[#03a9f4]', : 'text-[#03a9f4]',
"待发布":'text-[#46BE11]', : 'text-[#46BE11]',
"未发布":'text-[#03a9f4]' : 'text-[#03a9f4]'
} }
export type DynamicPublish = { export type DynamicPublish = {
code:number, code: number
msg:string, msg: string
data:{ data: {
success:Array<string>, success: Array<string>
fail:Array<string> fail: Array<string>
} }
} }
export default function IntelligentPluginList(){ export default function IntelligentPluginList() {
const { modal,message } = App.useApp() const { modal, message } = App.useApp()
const [searchWord, setSearchWord] = useState<string>('') const [searchWord, setSearchWord] = useState<string>('')
const { moduleId } = useParams<RouterParams>(); const { moduleId } = useParams<RouterParams>()
const [pluginName,setPluginName] = useState<string>('-') const [pluginName, setPluginName] = useState<string>('-')
const [partitionOptions] = useState<DefaultOptionType[]>([{label:'default', value:'default'}]) const [partitionOptions] = useState<DefaultOptionType[]>([{ label: 'default', value: 'default' }])
const { setBreadcrumb } = useBreadcrumb() const { setBreadcrumb } = useBreadcrumb()
const [renderSchema ,setRenderSchema] = useState<{[k:string]:unknown}>({}) const [renderSchema, setRenderSchema] = useState<{ [k: string]: unknown }>({})
const drawerFormRef = useRef<IntelligentPluginConfigHandle>(null); const drawerFormRef = useRef<IntelligentPluginConfigHandle>(null)
const [driverOptions, setDriverOptions] = useState<DefaultOptionType[]>([]) const [driverOptions, setDriverOptions] = useState<DefaultOptionType[]>([])
const [tableListDataSource, setTableListDataSource] = useState<DynamicTableItem[]>([]); const [tableListDataSource, setTableListDataSource] = useState<DynamicTableItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true); const [tableHttpReload, setTableHttpReload] = useState(true)
const [columns,setColumns] = useState<DynamicTableField[] >([]) const [columns, setColumns] = useState<DynamicTableField[]>([])
const {fetchData} = useFetch() const { fetchData } = useFetch()
const pageListRef = useRef<ActionType>(null); const pageListRef = useRef<ActionType>(null)
const [publishBtnLoading, setPublishBtnLoading] = useState<boolean>(false) const [publishBtnLoading, setPublishBtnLoading] = useState<boolean>(false)
const [curDetail,setCurDetail] = useState<{[k: string]: unknown;}|undefined>() const [curDetail, setCurDetail] = useState<{ [k: string]: unknown } | undefined>()
const [drawerType, setDrawerType] = useState<'add'|'edit'>('add') const [drawerType, setDrawerType] = useState<'add' | 'edit'>('add')
const [drawerOpen, setDrawerOpen] = useState<boolean>(false) const [drawerOpen, setDrawerOpen] = useState<boolean>(false)
const [drawerLoading, setDrawerLoading] = useState<boolean>(false) const [drawerLoading, setDrawerLoading] = useState<boolean>(false)
const location = useLocation().pathname const location = useLocation().pathname
const {accessPrefix} = useOutletContext<{accessPrefix:string}>() const { accessPrefix } = useOutletContext<{ accessPrefix: string }>()
const {state} = useGlobalContext() const { state } = useGlobalContext()
const getIntelligentPluginTableList = (
const getIntelligentPluginTableList=(params:ParamsType & { params: ParamsType & {
pageSize?: number | undefined; pageSize?: number | undefined
current?: number | undefined; current?: number | undefined
keyword?: string | undefined; keyword?: string | undefined
}): Promise<{ data: DynamicTableItem[], success: boolean }>=> { }
if(!tableHttpReload){ ): Promise<{ data: DynamicTableItem[]; success: boolean }> => {
setTableHttpReload(true) if (!tableHttpReload) {
return Promise.resolve({ setTableHttpReload(true)
data: tableListDataSource, return Promise.resolve({
success: true, data: tableListDataSource,
}); success: true
})
}
const query = {
page: params.current,
pageSize: params.pageSize,
keyword: searchWord
}
return fetchData<BasicResponse<DynamicTableConfig>>(`dynamic/${moduleId}/list`, {
method: 'GET',
eoParams: query,
eoTransformKeys: ['pageSize']
})
.then((res) => {
message.destroy()
if (res.code === STATUS_CODE.SUCCESS) {
getConfig(res.data)
setColumns(res.data.basic.fields)
setTableListDataSource(res.data.list)
return { data: res.data.list, success: true, total: res.data.total }
} else {
setTableListDataSource([])
return { data: [], success: false }
} }
const query = { })
page:params.current, .catch((e) => {
pageSize:params.pageSize, console.warn(e)
keyword:searchWord, return { data: [], success: false }
} })
return fetchData<BasicResponse<DynamicTableConfig>>( }
`dynamic/${moduleId}/list`,
{method:'GET',eoParams:query,eoTransformKeys:['pageSize']}).then((res)=>{
message.destroy();
if(res.code === STATUS_CODE.SUCCESS){
getConfig(res.data)
setColumns(res.data.basic.fields)
setTableListDataSource(res.data.list);
return ({ data: res.data.list, success: true,total:res.data.total });
}else{
setTableListDataSource([]);
return ({ data: [], success: false });
}
}).catch((e)=>{console.warn(e);
return ({ data: [], success: false });})
}
const translatedCol = useMemo(()=>columns.map((field:DynamicTableField, index:number)=>({ const translatedCol = useMemo(
title: typeof field.title === 'string' ? $t(field.title as string): field.title, () =>
dataIndex:field.name, columns.map((field: DynamicTableField, index: number) => ({
fixed:field.name === 'title' ? 'left' : undefined, title: typeof field.title === 'string' ? $t(field.title as string) : field.title,
ellipsis:true, dataIndex: field.name,
width:field.name === 'title' ? 150 : undefined, fixed: field.name === 'title' ? 'left' : undefined,
...(field.enum?.length > 0 ?{ ellipsis: true,
onFilter: (value: string, record: { [x: string]: string | string[]; }) => record[field.name].indexOf(value) === 0, width: field.name === 'title' ? 150 : undefined,
filters:field.enum?.map((x:string)=>{return {text:$t(x), value:x}}), ...(field.enum?.length > 0
render:(_: unknown, entity: { [x: string]: string; })=> { ? {
return <span className={StatusColorClass[entity[field.name] as keyof typeof StatusColorClass]}>{$t(entity[field.name] as string)}</span> onFilter: (value: string, record: { [x: string]: string | string[] }) =>
}, record[field.name].indexOf(value) === 0,
}:{}), filters: field.enum?.map((x: string) => {
})),[state.language,columns]) return { text: $t(x), value: x }
}),
render: (_: unknown, entity: { [x: string]: string }) => {
const getConfig = (data:DynamicTableConfig)=>{
const {basic,list } = data
const {title,drivers} = basic
setBreadcrumb([
{title:location.includes('resourcesettings') ? $t('资源'): $t('日志')},
{
title
}
])
setPluginName(title)
setDriverOptions(drivers?.map((driver:DynamicDriverData) => {
return { label: driver.title, value: driver.name }
}) || [])
}
const getRender = ()=>{
return fetchData<BasicResponse<DynamicRender>>(`dynamic/${moduleId}/render`,{method:'GET'}).then((resp) => {
if (resp.code === STATUS_CODE.SUCCESS) {
setRenderSchema(resp.data.render)
return Promise.resolve(resp.data.render)
}
return Promise.reject(resp.msg || $t(RESPONSE_TIPS.error))
})
}
const operation:PageProColumns<DynamicTableItem>[] =[
{
title: COLUMNS_TITLE.operate,
key: 'option',
fixed:'right',
valueType: 'option',
btnNums:3,
render: (_: React.ReactNode, entity: DynamicTableItem) => [
<TableBtnWithPermission access={`${accessPrefix}.publish`} key="publish" btnType="publish" onClick={()=>{openModal('publish',entity)}} btnTitle={entity.status === $t('已发布') ? $t('下线') : $t('上线')}/>,
<Divider type="vertical" className="mx-0" key="div1"/>,
<TableBtnWithPermission access={`${accessPrefix}.view`} key="edit" btnType="edit" onClick={()=>{openDrawer('edit',entity)}} btnTitle={$t("查看")}/>,
<Divider type="vertical" className="mx-0" key="div2"/>,
<TableBtnWithPermission access={`${accessPrefix}.delete`} key="delete" btnType="delete" onClick={()=>{openModal('delete',entity)}} btnTitle={$t("删除")}/>,
],
}
]
const handleClusterChange = (e:string[])=>{
setTableHttpReload(true)
pageListRef.current?.reload()
}
const manualReloadTable = () => {
setTableHttpReload(true); // 表格数据需要从后端接口获取
pageListRef.current?.reload()
};
const deleteInstance = (entity:DynamicTableItem)=>{
return new Promise((resolve, reject)=>{
fetchData<BasicResponse<null>>(`dynamic/${moduleId}/batch`,{method:'DELETE',eoParams:{ids:JSON.stringify([entity!.id])}}).then(response=>{
const {code,msg} = response
if(code === STATUS_CODE.SUCCESS){
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
})
}
const openDrawer = async (type:'add'|'edit', entity?:DynamicTableItem)=>{
switch (type){
case 'add':
setCurDetail({driver:driverOptions[0].value || '',config:{}})
break;
case 'edit':{
setDrawerLoading(true)
fetchData<BasicResponse<{info:DynamicTableItem}>>(
`dynamic/${moduleId}/info`,
{method:'GET',eoParams:{id:entity!.id}}).then((res)=>{
const {code, data, msg } = res
if(code === STATUS_CODE.SUCCESS){
if(data.info.config){
}
setCurDetail(data.info)
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).finally(()=>setDrawerLoading(false))
break;
}
}
setDrawerType(type)
setDrawerOpen(true)
}
const openModal = async (type:'publish'|'delete', entity?:DynamicTableItem)=>{
let title:string = ''
let content:string|React.ReactNode = ''
switch (type){
case 'publish':{
message.loading($t(RESPONSE_TIPS.operating))
await fetchData<BasicResponse<DynamicPublish>>(`dynamic/${moduleId}/${entity!.status === $t('已发布') ? 'offline':'online'}`, {
method: 'PUT',
eoParams:{id:entity!.id},
}).then(response => {
const {code, msg} = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
return Promise.resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> Promise.reject(errorInfo))
message.destroy()
return;}
case 'delete':
title='删除'
content=<span>{$t(DELETE_TIPS.default)}</span>
break;
}
modal.confirm({
title,
content,
onOk:()=>{
switch (type){ // case 'publish':
// return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()})
case 'delete':
return deleteInstance(entity!).then((res)=>{if(res === true) manualReloadTable()})
}
},
width: type === 'delete'? 600 : 900,
okText:$t('确认'),
okButtonProps:{
disabled:false
},
cancelText:$t('取消'),
closable:true,
icon:<></>,
footer:(_, { OkBtn, CancelBtn }) =>{
return ( return (
<> <span className={StatusColorClass[entity[field.name] as keyof typeof StatusColorClass]}>
<WithPermission access=""><CancelBtn/></WithPermission> {$t(entity[field.name] as string)}
<WithPermission access=""><OkBtn/></WithPermission> </span>
</> )
); }
}, }
: {})
})),
[state.language, columns]
)
const getConfig = (data: DynamicTableConfig) => {
const { basic, list } = data
const { title, drivers } = basic
setBreadcrumb([
{ title: location.includes('resourcesettings') ? $t('资源') : $t('日志') },
{
title
}
])
setPluginName(title)
setDriverOptions(
drivers?.map((driver: DynamicDriverData) => {
return { label: driver.title, value: driver.name }
}) || []
)
}
const getRender = () => {
return fetchData<BasicResponse<DynamicRender>>(`dynamic/${moduleId}/render`, { method: 'GET' }).then((resp) => {
if (resp.code === STATUS_CODE.SUCCESS) {
setRenderSchema(resp.data.render)
return Promise.resolve(resp.data.render)
}
return Promise.reject(resp.msg || $t(RESPONSE_TIPS.error))
})
}
const operation: PageProColumns<DynamicTableItem>[] = [
{
title: COLUMNS_TITLE.operate,
key: 'option',
fixed: 'right',
valueType: 'option',
btnNums: 3,
render: (_: React.ReactNode, entity: DynamicTableItem) => [
<TableBtnWithPermission
access={`${accessPrefix}.publish`}
key={entity.status === $t('已发布') ? 'offline' : 'publish'}
btnType={entity.status === $t('已发布') ? 'offline' : 'publish'}
onClick={() => {
openModal('publish', entity)
}}
btnTitle={entity.status === $t('已发布') ? $t('下线') : $t('上线')}
/>,
<Divider type="vertical" className="mx-0" key="div1" />,
<TableBtnWithPermission
access={`${accessPrefix}.view`}
key="edit"
btnType="edit"
onClick={() => {
openDrawer('edit', entity)
}}
btnTitle={$t('查看 ')}
/>,
<Divider type="vertical" className="mx-0" key="div2" />,
<TableBtnWithPermission
access={`${accessPrefix}.delete`}
key="delete"
btnType="delete"
onClick={() => {
openModal('delete', entity)
}}
btnTitle={$t('删除')}
/>
]
}
]
const handleClusterChange = (e: string[]) => {
setTableHttpReload(true)
pageListRef.current?.reload()
}
const manualReloadTable = () => {
setTableHttpReload(true) // 表格数据需要从后端接口获取
pageListRef.current?.reload()
}
const deleteInstance = (entity: DynamicTableItem) => {
return new Promise((resolve, reject) => {
fetchData<BasicResponse<null>>(`dynamic/${moduleId}/batch`, {
method: 'DELETE',
eoParams: { ids: JSON.stringify([entity!.id]) }
}).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
})
})
}
const openDrawer = async (type: 'add' | 'edit', entity?: DynamicTableItem) => {
switch (type) {
case 'add':
setCurDetail({ driver: driverOptions[0].value || '', config: {} })
break
case 'edit': {
setDrawerLoading(true)
fetchData<BasicResponse<{ info: DynamicTableItem }>>(`dynamic/${moduleId}/info`, {
method: 'GET',
eoParams: { id: entity!.id }
}) })
.then((res) => {
const { code, data, msg } = res
if (code === STATUS_CODE.SUCCESS) {
if (data.info.config) {
}
setCurDetail(data.info)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => setDrawerLoading(false))
break
}
}
setDrawerType(type)
setDrawerOpen(true)
}
const openModal = async (type: 'publish' | 'delete', entity?: DynamicTableItem) => {
let title: string = ''
let content: string | React.ReactNode = ''
switch (type) {
case 'publish': {
message.loading($t(RESPONSE_TIPS.operating))
await fetchData<BasicResponse<DynamicPublish>>(
`dynamic/${moduleId}/${entity!.status === $t('已发布') ? 'offline' : 'online'}`,
{
method: 'PUT',
eoParams: { id: entity!.id }
}
)
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
manualReloadTable()
return Promise.resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => Promise.reject(errorInfo))
return
}
case 'delete':
title = '删除'
content = <span>{$t(DELETE_TIPS.default)}</span>
break
} }
useEffect(() => { modal.confirm({
getRender() title,
pageListRef.current?.reload() content,
}, [moduleId]); onOk: () => {
switch (
type // case 'publish':
) {
// return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()})
case 'delete':
return deleteInstance(entity!).then((res) => {
if (res === true) manualReloadTable()
})
}
},
width: type === 'delete' ? 600 : 900,
okText: $t('确认'),
okButtonProps: {
disabled: false
},
cancelText: $t('取消'),
closable: true,
icon: <></>,
footer: (_, { OkBtn, CancelBtn }) => {
return (
<>
<WithPermission access="">
<CancelBtn />
</WithPermission>
<WithPermission access="">
<OkBtn />
</WithPermission>
</>
)
}
})
}
useEffect(() => {
getRender()
pageListRef.current?.reload()
}, [moduleId])
return (<> return (
<PageList <>
ref={pageListRef} <PageList
columns = {[...translatedCol,...operation]} ref={pageListRef}
request={(params)=>getIntelligentPluginTableList(params)} columns={[...translatedCol, ...operation]}
addNewBtnTitle={$t('添加(0)',[$t(pluginName)])} request={(params) => getIntelligentPluginTableList(params)}
searchPlaceholder={$t('搜索(0)名称',[$t(pluginName)])} addNewBtnTitle={$t('添加(0)', [$t(pluginName)])}
onChange={() => { searchPlaceholder={$t('搜索(0)名称', [$t(pluginName)])}
setTableHttpReload(false) onChange={() => {
}} setTableHttpReload(false)
addNewBtnAccess={`${accessPrefix}.add`} }}
onAddNewBtnClick={()=>{openDrawer('add')}} addNewBtnAccess={`${accessPrefix}.add`}
onSearchWordChange={(e)=>{setSearchWord(e.target.value);setTableHttpReload(true);setTableHttpReload(true)}} onAddNewBtnClick={() => {
/> openDrawer('add')
}}
<DrawerWithFooter title={`${drawerType === 'add' ? $t('添加(0)',[$t(pluginName)]) : $t('编辑(0)',[$t(pluginName)])}`} open={drawerOpen} onClose={()=>{setCurDetail(undefined);setDrawerOpen(false)}} onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} submitAccess=''> onSearchWordChange={(e) => {
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin/>} spinning={drawerLoading}> setSearchWord(e.target.value)
<IntelligentPluginConfig setTableHttpReload(true)
ref={drawerFormRef!} setTableHttpReload(true)
type={drawerType} }}
renderSchema={renderSchema} />
tabData={partitionOptions}
moduleId={moduleId!} <DrawerWithFooter
driverSelectionOptions={driverOptions} title={`${drawerType === 'add' ? $t('添加(0)', [$t(pluginName)]) : $t('编辑(0)', [$t(pluginName)])}`}
initFormValue={curDetail as { [k: string]: unknown; }} /> open={drawerOpen}
</Spin> onClose={() => {
</DrawerWithFooter> setCurDetail(undefined)
</>) setDrawerOpen(false)
} }}
onSubmit={() =>
drawerFormRef.current?.save()?.then((res) => {
res && manualReloadTable()
return res
})
}
submitAccess=""
>
<Spin indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} spinning={drawerLoading}>
<IntelligentPluginConfig
ref={drawerFormRef!}
type={drawerType}
renderSchema={renderSchema}
tabData={partitionOptions}
moduleId={moduleId!}
driverSelectionOptions={driverOptions}
initFormValue={curDetail as { [k: string]: unknown }}
/>
</Spin>
</DrawerWithFooter>
</>
)
}
@@ -1,14 +1,9 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useMemo } from 'react' import { useEffect } from 'react'
import type { import type { EditorState } from 'lexical'
EditorState, import { $getRoot, TextNode } from 'lexical'
} from 'lexical'
import {
$getRoot,
TextNode,
} from 'lexical'
import { CodeNode } from '@lexical/code' import { CodeNode } from '@lexical/code'
import { LexicalComposer } from '@lexical/react/LexicalComposer' import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
@@ -19,25 +14,13 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
// import TreeView from './plugins/tree-view' // import TreeView from './plugins/tree-view'
import Placeholder from './plugins/placeholder' import Placeholder from './plugins/placeholder'
import ComponentPickerBlock from './plugins/component-picker-block/index' import ComponentPickerBlock from './plugins/component-picker-block/index'
import { import { ContextBlock, ContextBlockNode, ContextBlockReplacementBlock } from './plugins/context-block/index'
ContextBlock, import { QueryBlock, QueryBlockNode, QueryBlockReplacementBlock } from './plugins/query-block/index'
ContextBlockNode, import { HistoryBlock, HistoryBlockNode, HistoryBlockReplacementBlock } from './plugins/history-block/index'
ContextBlockReplacementBlock,
} from './plugins/context-block/index'
import {
QueryBlock,
QueryBlockNode,
QueryBlockReplacementBlock,
} from './plugins/query-block/index'
import {
HistoryBlock,
HistoryBlockNode,
HistoryBlockReplacementBlock,
} from './plugins/history-block/index'
import { import {
WorkflowVariableBlock, WorkflowVariableBlock,
WorkflowVariableBlockNode, WorkflowVariableBlockNode,
WorkflowVariableBlockReplacementBlock, WorkflowVariableBlockReplacementBlock
} from './plugins/workflow-variable-block/index' } from './plugins/workflow-variable-block/index'
import VariableBlock from './plugins/variable-block/index' import VariableBlock from './plugins/variable-block/index'
import VariableValueBlock from './plugins/variable-value-block/index' import VariableValueBlock from './plugins/variable-value-block/index'
@@ -52,12 +35,9 @@ import type {
HistoryBlockType, HistoryBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType
} from './types' } from './types'
import { import { UPDATE_DATASETS_EVENT_EMITTER, UPDATE_HISTORY_EVENT_EMITTER } from './constants'
UPDATE_DATASETS_EVENT_EMITTER,
UPDATE_HISTORY_EVENT_EMITTER,
} from './constants'
import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext' import { useEventEmitterContextContext } from '@common/contexts/EventEmitterContext'
export type PromptEditorProps = { export type PromptEditorProps = {
@@ -97,7 +77,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
historyBlock, historyBlock,
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock
}) => { }) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const initialConfig = { const initialConfig = {
@@ -107,51 +87,58 @@ const PromptEditor: FC<PromptEditorProps> = ({
CustomTextNode, CustomTextNode,
{ {
replace: TextNode, replace: TextNode,
with: (node: TextNode) => new CustomTextNode(node.__text), with: (node: TextNode) => new CustomTextNode(node.__text)
}, },
ContextBlockNode, ContextBlockNode,
HistoryBlockNode, HistoryBlockNode,
QueryBlockNode, QueryBlockNode,
WorkflowVariableBlockNode, WorkflowVariableBlockNode,
VariableValueBlockNode, VariableValueBlockNode
], ],
editorState: textToEditorState(value || ''), editorState: textToEditorState(value || ''),
onError: (error: Error) => { onError: (error: Error) => {
throw error throw error
}, }
} }
const handleEditorChange = (editorState: EditorState) => { const handleEditorChange = (editorState: EditorState) => {
const text = editorState.read(() => { const text = editorState.read(() => {
return $getRoot().getChildren().map(p => p.getTextContent()).join('\n') return $getRoot()
.getChildren()
.map((p) => p.getTextContent())
.join('\n')
}) })
if (onChange) if (onChange) onChange(text)
onChange(text)
} }
useEffect(() => { useEffect(() => {
eventEmitter?.emit({ eventEmitter?.emit({
type: UPDATE_DATASETS_EVENT_EMITTER, type: UPDATE_DATASETS_EVENT_EMITTER,
payload: contextBlock?.datasets, payload: contextBlock?.datasets
} as any) } as any)
}, [eventEmitter, contextBlock?.datasets]) }, [eventEmitter, contextBlock?.datasets])
useEffect(() => { useEffect(() => {
eventEmitter?.emit({ eventEmitter?.emit({
type: UPDATE_HISTORY_EVENT_EMITTER, type: UPDATE_HISTORY_EVENT_EMITTER,
payload: historyBlock?.history, payload: historyBlock?.history
} as any) } as any)
}, [eventEmitter, historyBlock?.history]) }, [eventEmitter, historyBlock?.history])
return ( return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}> <LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className='relative min-h-5'> <div className="relative min-h-5">
<RichTextPlugin <RichTextPlugin
contentEditable={<ContentEditable className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`} style={style || {}} />} contentEditable={
<ContentEditable
className={`${className} outline-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'} text-gray-700`}
style={style || {}}
/>
}
placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />} placeholder={<Placeholder value={placeholder} className={placeholderClassName} compact={compact} />}
ErrorBoundary={LexicalErrorBoundary} ErrorBoundary={LexicalErrorBoundary}
/> />
<ComponentPickerBlock <ComponentPickerBlock
triggerString='/' triggerString="/"
contextBlock={contextBlock} contextBlock={contextBlock}
historyBlock={historyBlock} historyBlock={historyBlock}
queryBlock={queryBlock} queryBlock={queryBlock}
@@ -160,7 +147,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
workflowVariableBlock={workflowVariableBlock} workflowVariableBlock={workflowVariableBlock}
/> />
<ComponentPickerBlock <ComponentPickerBlock
triggerString='{' triggerString="{"
contextBlock={contextBlock} contextBlock={contextBlock}
historyBlock={historyBlock} historyBlock={historyBlock}
queryBlock={queryBlock} queryBlock={queryBlock}
@@ -168,46 +155,36 @@ const PromptEditor: FC<PromptEditorProps> = ({
externalToolBlock={externalToolBlock} externalToolBlock={externalToolBlock}
workflowVariableBlock={workflowVariableBlock} workflowVariableBlock={workflowVariableBlock}
/> />
{ {contextBlock?.show && (
contextBlock?.show && ( <>
<> <ContextBlock {...contextBlock} />
<ContextBlock {...contextBlock} /> <ContextBlockReplacementBlock {...contextBlock} />
<ContextBlockReplacementBlock {...contextBlock} /> </>
</> )}
) {queryBlock?.show && (
} <>
{ <QueryBlock {...queryBlock} />
queryBlock?.show && ( <QueryBlockReplacementBlock />
<> </>
<QueryBlock {...queryBlock} /> )}
<QueryBlockReplacementBlock /> {historyBlock?.show && (
</> <>
) <HistoryBlock {...historyBlock} />
} <HistoryBlockReplacementBlock {...historyBlock} />
{ </>
historyBlock?.show && ( )}
<> {(variableBlock?.show || externalToolBlock?.show) && (
<HistoryBlock {...historyBlock} /> <>
<HistoryBlockReplacementBlock {...historyBlock} /> <VariableBlock />
</> <VariableValueBlock />
) </>
} )}
{ {workflowVariableBlock?.show && (
(variableBlock?.show || externalToolBlock?.show) && ( <>
<> <WorkflowVariableBlock {...workflowVariableBlock} />
<VariableBlock /> <WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
<VariableValueBlock /> </>
</> )}
)
}
{
workflowVariableBlock?.show && (
<>
<WorkflowVariableBlock {...workflowVariableBlock} />
<WorkflowVariableBlockReplacementBlock {...workflowVariableBlock} />
</>
)
}
<OnChangePlugin onChange={handleEditorChange} /> <OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} /> <OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
<UpdateBlock instanceId={instanceId} /> <UpdateBlock instanceId={instanceId} />
@@ -1,82 +1,94 @@
import PromptEditor from '@common/components/aoplatform/prompt-editor/PromptEditor.tsx'
import PromptEditorHeightResizeWrap from '@common/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx'
import { useState } from 'react'
import { getVars } from './utils'
import { VariableItems } from '@core/const/ai-service/type'
import PromptEditor from '@common/components/aoplatform/prompt-editor/PromptEditor.tsx'; const PromptEditorResizable = (props: {
import PromptEditorHeightResizeWrap from '@common/components/aoplatform/prompt-editor/prompt-editor-height-resize-wrap.tsx'; value?: string
import { useEffect, useState } from 'react'; onChange?: (value: string) => void
import { getVars } from './utils'; variablesChange?: (keys: string[]) => void
import { VariableItems } from '@core/const/ai-service/type'; promptVariables: VariableItems[]
disabled?: boolean
}) => {
const { value, onChange, variablesChange, promptVariables, disabled } = props
const minHeight = 68
const [editorHeight, setEditorHeight] = useState(minHeight)
const [previousKeys, setPreviousKeys] = useState<string[]>([])
const handleChange = (newTemplates: string, keys: string[]) => {
onChange?.(newTemplates)
}
const PromptEditorResizable = (props:{value?:string, onChange?:(value:string)=>void, variablesChange?:(keys:string[])=>void,promptVariables:VariableItems[]}) =>{ return (
const {value , onChange,variablesChange,promptVariables} = props <PromptEditorHeightResizeWrap
const minHeight = 68 className="px-4 pt-2 min-h-[94px] bg-white rounded-t-xl text-sm text-gray-700"
const [editorHeight, setEditorHeight] = useState(minHeight) height={editorHeight}
const [previousKeys, setPreviousKeys] = useState<string[]>([]) minHeight={minHeight}
const handleChange = (newTemplates: string, keys: string[]) => { onHeightChange={setEditorHeight}
onChange?.(newTemplates) hideResize={false}
footer={
<div className="pl-4 pb-2 flex bg-white rounded-b-xl">
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">
{value?.length || 0}
</div>
</div>
} }
>
return ( <PromptEditorHeightResizeWrap <>
className='px-4 pt-2 min-h-[94px] bg-white rounded-t-xl text-sm text-gray-700' {value !== undefined && (
height={editorHeight} <PromptEditor
minHeight={minHeight} className="min-h-[68px]"
onHeightChange={setEditorHeight} compact
hideResize={false} value={value}
footer={( contextBlock={{
<div className='pl-4 pb-2 flex bg-white rounded-b-xl'> show: false,
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value?.length || 0}</div> selectable: true
</div> // datasets: dataSets.map(item => ({
// id: item.id,
// name: item.name,
// type: item.data_source_type,
// })),
// onAddContext: ()=>{console.log('?onAddContext')},
}}
variableBlock={{
show: true,
variables: promptVariables?.map((x) => ({ name: x.key, value: x.key })) || []
}}
externalToolBlock={{
show: false,
externalTools: []
// onAddExternalTool: handleOpenExternalDataToolModal,
}}
historyBlock={{
show: false,
selectable: false,
history: {
user: '',
assistant: ''
},
onEditRole: () => {}
}}
queryBlock={{
show: false,
selectable: true
}}
onChange={(value) => {
handleChange?.(value, [])
}}
onBlur={() => {
const keys = getVars(value)
handleChange(value, keys)
if (keys.filter((key) => !previousKeys.includes(key)).length > 0) {
variablesChange?.(keys)
setPreviousKeys(keys)
}
}}
editable={disabled ? false : true}
/>
)} )}
><> </>
{value !== undefined && <PromptEditor </PromptEditorHeightResizeWrap>
className='min-h-[68px]' )
compact
value={value}
contextBlock={{
show: false,
selectable: true,
// datasets: dataSets.map(item => ({
// id: item.id,
// name: item.name,
// type: item.data_source_type,
// })),
// onAddContext: ()=>{console.log('?onAddContext')},
}}
variableBlock={{
show: true,
variables:promptVariables?.map(x=>({name:x.key, value:x.key})) || [],
}}
externalToolBlock={{
show: false,
externalTools: [],
// onAddExternalTool: handleOpenExternalDataToolModal,
}}
historyBlock={{
show: false,
selectable: false,
history: {
user: '',
assistant: '',
},
onEditRole: () => { },
}}
queryBlock={{
show: false,
selectable: true,
}}
onChange={(value) => {
handleChange?.(value, [])
}}
onBlur={() => {
const keys = getVars(value)
handleChange(value, keys)
if(keys.filter(key => !previousKeys.includes(key)).length > 0){
variablesChange?.(keys)
setPreviousKeys(keys)
}
}}
editable={true}
/>
}</>
</PromptEditorHeightResizeWrap>)
} }
export default PromptEditorResizable export default PromptEditorResizable
@@ -9,40 +9,35 @@ export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-
export const MAX_VAR_KEY_LENGTH = 30 export const MAX_VAR_KEY_LENGTH = 30
export const checkHasContextBlock = (text: string) => { export const checkHasContextBlock = (text: string) => {
if (!text) if (!text) return false
return false
return text.includes(CONTEXT_PLACEHOLDER_TEXT) return text.includes(CONTEXT_PLACEHOLDER_TEXT)
} }
export const checkHasHistoryBlock = (text: string) => { export const checkHasHistoryBlock = (text: string) => {
if (!text) if (!text) return false
return false
return text.includes(HISTORY_PLACEHOLDER_TEXT) return text.includes(HISTORY_PLACEHOLDER_TEXT)
} }
export const checkHasQueryBlock = (text: string) => { export const checkHasQueryBlock = (text: string) => {
if (!text) if (!text) return false
return false
return text.includes(QUERY_PLACEHOLDER_TEXT) return text.includes(QUERY_PLACEHOLDER_TEXT)
} }
/* /*
* {{#1711617514996.name#}} => [1711617514996, name] * {{#1711617514996.name#}} => [1711617514996, name]
* {{#1711617514996.sys.query#}} => [sys, query] * {{#1711617514996.sys.query#}} => [sys, query]
*/ */
export const getInputVars = (text: string) => { export const getInputVars = (text: string) => {
if (!text) if (!text) return []
return []
const allVars = text.match(/{{#([^#]*)#}}/g) const allVars = text.match(/{{#([^#]*)#}}/g)
if (allVars && allVars?.length > 0) { if (allVars && allVars?.length > 0) {
// {{#context#}}, {{#query#}} is not input vars // {{#context#}}, {{#query#}} is not input vars
const inputVars = allVars const inputVars = allVars
.filter(item => item.includes('.')) .filter((item) => item.includes('.'))
.map((item) => { .map((item) => {
const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.') const valueSelector = item.replace('{{#', '').replace('#}}', '').split('.')
if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0])) if (valueSelector[1] === 'sys' && /^\d+$/.test(valueSelector[0])) return valueSelector.slice(1)
return valueSelector.slice(1)
return valueSelector return valueSelector
}) })
@@ -1,16 +1,6 @@
import { import { useCallback, useEffect, useRef, useState } from 'react'
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import type { Dispatch, RefObject, SetStateAction } from 'react' import type { Dispatch, RefObject, SetStateAction } from 'react'
import type { import type { Klass, LexicalCommand, LexicalEditor, TextNode } from 'lexical'
Klass,
LexicalCommand,
LexicalEditor,
TextNode,
} from 'lexical'
import { import {
$getNodeByKey, $getNodeByKey,
$getSelection, $getSelection,
@@ -18,12 +8,10 @@ import {
$isNodeSelection, $isNodeSelection,
COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_LOW,
KEY_BACKSPACE_COMMAND, KEY_BACKSPACE_COMMAND,
KEY_DELETE_COMMAND, KEY_DELETE_COMMAND
} from 'lexical' } from 'lexical'
import type { EntityMatch } from '@lexical/text' import type { EntityMatch } from '@lexical/text'
import { import { mergeRegister } from '@lexical/utils'
mergeRegister,
} from '@lexical/utils'
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection' import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { $isContextBlockNode } from './plugins/context-block/node' import { $isContextBlockNode } from './plugins/context-block/node'
@@ -35,7 +23,10 @@ import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
import type { CustomTextNode } from './plugins/custom-text/node' import type { CustomTextNode } from './plugins/custom-text/node'
import { registerLexicalTextEntity } from './utils' import { registerLexicalTextEntity } from './utils'
export type UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean] export type UseSelectOrDeleteHandler = (
nodeKey: string,
command?: LexicalCommand<undefined>
) => [RefObject<HTMLDivElement>, boolean]
export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => { export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, command?: LexicalCommand<undefined>) => {
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -46,13 +37,11 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
const selection = $getSelection() const selection = $getSelection()
const nodes = selection?.getNodes() const nodes = selection?.getNodes()
if ( if (
!isSelected !isSelected &&
&& nodes?.length === 1 nodes?.length === 1 &&
&& ( (($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND) ||
($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND) ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND) ||
|| ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND) ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND))
|| ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
)
) )
editor.dispatchCommand(command, undefined) editor.dispatchCommand(command, undefined)
@@ -60,8 +49,7 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
event.preventDefault() event.preventDefault()
const node = $getNodeByKey(nodeKey) const node = $getNodeByKey(nodeKey)
if ($isDecoratorNode(node)) { if ($isDecoratorNode(node)) {
if (command) if (command) editor.dispatchCommand(command, undefined)
editor.dispatchCommand(command, undefined)
node.remove() node.remove()
return true return true
@@ -70,38 +58,31 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com
return false return false
}, },
[isSelected, nodeKey, command, editor], [isSelected, nodeKey, command, editor]
) )
const handleSelect = useCallback((e: MouseEvent) => { const handleSelect = useCallback(
e.stopPropagation() (e: MouseEvent) => {
clearSelection() e.stopPropagation()
setSelected(true) clearSelection()
}, [setSelected, clearSelection]) setSelected(true)
},
[setSelected, clearSelection]
)
useEffect(() => { useEffect(() => {
const ele = ref.current const ele = ref.current
if (ele) if (ele) ele.addEventListener('click', handleSelect)
ele.addEventListener('click', handleSelect)
return () => { return () => {
if (ele) if (ele) ele.removeEventListener('click', handleSelect)
ele.removeEventListener('click', handleSelect)
} }
}, [handleSelect]) }, [handleSelect])
useEffect(() => { useEffect(() => {
return mergeRegister( return mergeRegister(
editor.registerCommand( editor.registerCommand(KEY_DELETE_COMMAND, handleDelete, COMMAND_PRIORITY_LOW),
KEY_DELETE_COMMAND, editor.registerCommand(KEY_BACKSPACE_COMMAND, handleDelete, COMMAND_PRIORITY_LOW)
handleDelete,
COMMAND_PRIORITY_LOW,
),
editor.registerCommand(
KEY_BACKSPACE_COMMAND,
handleDelete,
COMMAND_PRIORITY_LOW,
),
) )
}, [editor, clearSelection, handleDelete]) }, [editor, clearSelection, handleDelete])
@@ -114,17 +95,15 @@ export const useTrigger: UseTriggerHandler = () => {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const handleOpen = useCallback((e: MouseEvent) => { const handleOpen = useCallback((e: MouseEvent) => {
e.stopPropagation() e.stopPropagation()
setOpen(v => !v) setOpen((v) => !v)
}, []) }, [])
useEffect(() => { useEffect(() => {
const trigger = triggerRef.current const trigger = triggerRef.current
if (trigger) if (trigger) trigger.addEventListener('click', handleOpen)
trigger.addEventListener('click', handleOpen)
return () => { return () => {
if (trigger) if (trigger) trigger.removeEventListener('click', handleOpen)
trigger.removeEventListener('click', handleOpen)
} }
}, [handleOpen]) }, [handleOpen])
@@ -134,7 +113,7 @@ export const useTrigger: UseTriggerHandler = () => {
export function useLexicalTextEntity<T extends TextNode>( export function useLexicalTextEntity<T extends TextNode>(
getMatch: (text: string) => null | EntityMatch, getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>, targetNode: Klass<T>,
createNode: (textNode: CustomTextNode) => T, createNode: (textNode: CustomTextNode) => T
) { ) {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -148,24 +127,16 @@ export type MenuTextMatch = {
matchingString: string matchingString: string
replaceableString: string replaceableString: string
} }
export type TriggerFn = ( export type TriggerFn = (text: string, editor: LexicalEditor) => MenuTextMatch | null
text: string,
editor: LexicalEditor,
) => MenuTextMatch | null
export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;' export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
export function useBasicTypeaheadTriggerMatch( export function useBasicTypeaheadTriggerMatch(
trigger: string, trigger: string,
{ minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }, { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number }
): TriggerFn { ): TriggerFn {
return useCallback( return useCallback(
(text: string) => { (text: string) => {
const validChars = `[${PUNCTUATION}\\s]` const validChars = `[${PUNCTUATION}\\s]`
const TypeaheadTriggerRegex = new RegExp( const TypeaheadTriggerRegex = new RegExp('(.*)(' + `[${trigger}]` + `((?:${validChars}){0,${maxLength}})` + ')$')
'(.*)('
+ `[${trigger}]`
+ `((?:${validChars}){0,${maxLength}})`
+ ')$',
)
const match = TypeaheadTriggerRegex.exec(text) const match = TypeaheadTriggerRegex.exec(text)
if (match !== null) { if (match !== null) {
const maybeLeadingWhitespace = match[1] const maybeLeadingWhitespace = match[1]
@@ -174,12 +145,12 @@ export function useBasicTypeaheadTriggerMatch(
return { return {
leadOffset: match.index + maybeLeadingWhitespace.length, leadOffset: match.index + maybeLeadingWhitespace.length,
matchingString, matchingString,
replaceableString: match[2], replaceableString: match[2]
} }
} }
} }
return null return null
}, },
[maxLength, minLength, trigger], [maxLength, minLength, trigger]
) )
} }
@@ -8,7 +8,7 @@ import type {
HistoryBlockType, HistoryBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType
} from '../../types' } from '../../types'
import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block' import { INSERT_CONTEXT_BLOCK_COMMAND } from '../context-block'
import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block' import { INSERT_HISTORY_BLOCK_COMMAND } from '../history-block'
@@ -32,32 +32,35 @@ import { $t } from '@common/locales'
export const usePromptOptions = ( export const usePromptOptions = (
contextBlock?: ContextBlockType, contextBlock?: ContextBlockType,
queryBlock?: QueryBlockType, queryBlock?: QueryBlockType,
historyBlock?: HistoryBlockType, historyBlock?: HistoryBlockType
) => { ) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const promptOptions: PickerBlockMenuOption[] = [] const promptOptions: PickerBlockMenuOption[] = []
if (contextBlock?.show) { if (contextBlock?.show) {
promptOptions.push(new PickerBlockMenuOption({ promptOptions.push(
key: $t('上下文'), new PickerBlockMenuOption({
group: 'prompt context', key: $t('上下文'),
render: ({ isSelected, onSelect, onSetHighlight }) => { group: 'prompt context',
return <PromptMenuItem render: ({ isSelected, onSelect, onSetHighlight }) => {
title={$t('上下文')} return (
icon={<></>} <PromptMenuItem
// icon={<File05 className='w-4 h-4 text-[#6938EF]' />} title={$t('上下文')}
disabled={!contextBlock.selectable} icon={<></>}
isSelected={isSelected} // icon={<File05 className='w-4 h-4 text-[#6938EF]' />}
onClick={onSelect} disabled={!contextBlock.selectable}
onMouseEnter={onSetHighlight} isSelected={isSelected}
/> onClick={onSelect}
}, onMouseEnter={onSetHighlight}
onSelect: () => { />
if (!contextBlock?.selectable) )
return },
editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined) onSelect: () => {
}, if (!contextBlock?.selectable) return
})) editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
}
})
)
} }
if (queryBlock?.show) { if (queryBlock?.show) {
@@ -79,11 +82,10 @@ export const usePromptOptions = (
) )
}, },
onSelect: () => { onSelect: () => {
if (!queryBlock?.selectable) if (!queryBlock?.selectable) return
return
editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined) editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
}, }
}), })
) )
} }
@@ -98,8 +100,7 @@ export const usePromptOptions = (
title={$t('会话历史')} title={$t('会话历史')}
icon={<></>} icon={<></>}
// icon={<MessageClockCircle className='w-4 h-4 text-[#DD2590]' />} // icon={<MessageClockCircle className='w-4 h-4 text-[#DD2590]' />}
disabled={!historyBlock.selectable disabled={!historyBlock.selectable}
}
isSelected={isSelected} isSelected={isSelected}
onClick={onSelect} onClick={onSelect}
onMouseEnter={onSetHighlight} onMouseEnter={onSetHighlight}
@@ -107,11 +108,10 @@ export const usePromptOptions = (
) )
}, },
onSelect: () => { onSelect: () => {
if (!historyBlock?.selectable) if (!historyBlock?.selectable) return
return
editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined) editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
}, }
}), })
) )
} }
return promptOptions return promptOptions
@@ -119,16 +119,15 @@ export const usePromptOptions = (
export const useVariableOptions = ( export const useVariableOptions = (
variableBlock?: VariableBlockType, variableBlock?: VariableBlockType,
queryString?: string, queryString?: string
): PickerBlockMenuOption[] => { ): PickerBlockMenuOption[] => {
const { t } = useTranslation() const { t } = useTranslation()
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const options = useMemo(() => { const options = useMemo(() => {
if (!variableBlock?.variables) if (!variableBlock?.variables) return []
return []
const baseOptions = (variableBlock.variables).map((item) => { const baseOptions = variableBlock.variables.map((item) => {
return new PickerBlockMenuOption({ return new PickerBlockMenuOption({
key: item.value, key: item.value,
group: 'prompt variable', group: 'prompt variable',
@@ -147,15 +146,14 @@ export const useVariableOptions = (
}, },
onSelect: () => { onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`) editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
}, }
}) })
}) })
if (!queryString) if (!queryString) return baseOptions
return baseOptions
const regex = new RegExp(queryString, 'i') const regex = new RegExp(queryString, 'i')
return baseOptions.filter(option => regex.test(option.key)) return baseOptions.filter((option) => regex.test(option.key))
}, [editor, queryString, variableBlock]) }, [editor, queryString, variableBlock])
const addOption = useMemo(() => { const addOption = useMemo(() => {
@@ -182,7 +180,7 @@ export const useVariableOptions = (
$insertNodes([prefixNode, suffixNode]) $insertNodes([prefixNode, suffixNode])
prefixNode.select() prefixNode.select()
}) })
}, }
}) })
}, [editor, t]) }, [editor, t])
@@ -191,17 +189,13 @@ export const useVariableOptions = (
}, [options, addOption, variableBlock?.show]) }, [options, addOption, variableBlock?.show])
} }
export const useExternalToolOptions = ( export const useExternalToolOptions = (externalToolBlockType?: ExternalToolBlockType, queryString?: string) => {
externalToolBlockType?: ExternalToolBlockType,
queryString?: string,
) => {
const { t } = useTranslation() const { t } = useTranslation()
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const options = useMemo(() => { const options = useMemo(() => {
if (!externalToolBlockType?.externalTools) if (!externalToolBlockType?.externalTools) return []
return [] const baseToolOptions = externalToolBlockType.externalTools.map((item) => {
const baseToolOptions = (externalToolBlockType.externalTools).map((item) => {
return new PickerBlockMenuOption({ return new PickerBlockMenuOption({
key: item.name, key: item.name,
group: 'external tool', group: 'external tool',
@@ -217,7 +211,7 @@ export const useExternalToolOptions = (
// background={item.icon_background} // background={item.icon_background}
// /> // />
// } // }
extraElement={<div className='text-xs text-gray-400'>{item.variableName}</div>} extraElement={<div className="text-xs text-gray-400">{item.variableName}</div>}
queryString={queryString} queryString={queryString}
isSelected={isSelected} isSelected={isSelected}
onClick={onSelect} onClick={onSelect}
@@ -227,15 +221,14 @@ export const useExternalToolOptions = (
}, },
onSelect: () => { onSelect: () => {
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`) editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.variableName}}}`)
}, }
}) })
}) })
if (!queryString) if (!queryString) return baseToolOptions
return baseToolOptions
const regex = new RegExp(queryString, 'i') const regex = new RegExp(queryString, 'i')
return baseToolOptions.filter(option => regex.test(option.key)) return baseToolOptions.filter((option) => regex.test(option.key))
}, [editor, queryString, externalToolBlockType]) }, [editor, queryString, externalToolBlockType])
const addOption = useMemo(() => { const addOption = useMemo(() => {
@@ -257,7 +250,7 @@ export const useExternalToolOptions = (
}, },
onSelect: () => { onSelect: () => {
externalToolBlockType?.onAddExternalTool?.() externalToolBlockType?.onAddExternalTool?.()
}, }
}) })
}, [externalToolBlockType, t]) }, [externalToolBlockType, t])
@@ -273,14 +266,13 @@ export const useOptions = (
variableBlock?: VariableBlockType, variableBlock?: VariableBlockType,
externalToolBlockType?: ExternalToolBlockType, externalToolBlockType?: ExternalToolBlockType,
workflowVariableBlockType?: WorkflowVariableBlockType, workflowVariableBlockType?: WorkflowVariableBlockType,
queryString?: string, queryString?: string
) => { ) => {
const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock)
const variableOptions = useVariableOptions(variableBlock, queryString) const variableOptions = useVariableOptions(variableBlock, queryString)
const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString) const externalToolOptions = useExternalToolOptions(externalToolBlockType, queryString)
const workflowVariableOptions = useMemo(() => { const workflowVariableOptions = useMemo(() => {
if (!workflowVariableBlockType?.show) if (!workflowVariableBlockType?.show) return []
return []
return workflowVariableBlockType.variables || [] return workflowVariableBlockType.variables || []
}, [workflowVariableBlockType]) }, [workflowVariableBlockType])
@@ -288,7 +280,7 @@ export const useOptions = (
return useMemo(() => { return useMemo(() => {
return { return {
workflowVariableOptions, workflowVariableOptions,
allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions], allFlattenOptions: [...promptOptions, ...variableOptions, ...externalToolOptions]
} }
}, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions]) }, [promptOptions, variableOptions, externalToolOptions, workflowVariableOptions])
} }
@@ -1,16 +1,6 @@
import { import { Fragment, memo, useCallback, useState } from 'react'
Fragment,
memo,
useCallback,
useState,
} from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import { import { flip, offset, shift, useFloating } from '@floating-ui/react'
flip,
offset,
shift,
useFloating,
} from '@floating-ui/react'
import type { TextNode } from 'lexical' import type { TextNode } from 'lexical'
import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin' import type { MenuRenderFn } from '@lexical/react/LexicalTypeaheadMenuPlugin'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
@@ -21,7 +11,7 @@ import type {
HistoryBlockType, HistoryBlockType,
QueryBlockType, QueryBlockType,
VariableBlockType, VariableBlockType,
WorkflowVariableBlockType, WorkflowVariableBlockType
} from '../../types' } from '../../types'
import { useBasicTypeaheadTriggerMatch } from '../../hooks' import { useBasicTypeaheadTriggerMatch } from '../../hooks'
import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block' import { INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND } from '../workflow-variable-block'
@@ -48,7 +38,7 @@ const ComponentPicker = ({
historyBlock, historyBlock,
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock
}: ComponentPickerProps) => { }: ComponentPickerProps) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const { refs, floatingStyles, isPositioned } = useFloating({ const { refs, floatingStyles, isPositioned } = useFloating({
@@ -56,15 +46,15 @@ const ComponentPicker = ({
middleware: [ middleware: [
offset(0), // fix hide cursor offset(0), // fix hide cursor
shift({ shift({
padding: 8, padding: 8
}), }),
flip(), flip()
], ]
}) })
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, { const checkForTriggerMatch = useBasicTypeaheadTriggerMatch(triggerString, {
minLength: 0, minLength: 0,
maxLength: 0, maxLength: 0
}) })
const [queryString, setQueryString] = useState<string | null>(null) const [queryString, setQueryString] = useState<string | null>(null)
@@ -74,123 +64,114 @@ const ComponentPicker = ({
editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`) editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${v.payload}}}`)
}) })
const { const { allFlattenOptions, workflowVariableOptions } = useOptions(
allFlattenOptions,
workflowVariableOptions,
} = useOptions(
contextBlock, contextBlock,
queryBlock, queryBlock,
historyBlock, historyBlock,
variableBlock, variableBlock,
externalToolBlock, externalToolBlock,
workflowVariableBlock, workflowVariableBlock
) )
const onSelectOption = useCallback( const onSelectOption = useCallback(
( (selectedOption: PickerBlockMenuOption, nodeToRemove: TextNode | null, closeMenu: () => void) => {
selectedOption: PickerBlockMenuOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
) => {
editor.update(() => { editor.update(() => {
if (nodeToRemove && selectedOption?.key) if (nodeToRemove && selectedOption?.key) nodeToRemove.remove()
nodeToRemove.remove()
selectedOption.onSelectMenuOption() selectedOption.onSelectMenuOption()
closeMenu() closeMenu()
}) })
}, },
[editor], [editor]
) )
const handleSelectWorkflowVariable = useCallback((variables: string[]) => { const handleSelectWorkflowVariable = useCallback(
editor.update(() => { (variables: string[]) => {
const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!) editor.update(() => {
if (needRemove) const needRemove = $splitNodeContainingQuery(checkForTriggerMatch(triggerString, editor)!)
needRemove.remove() if (needRemove) needRemove.remove()
}) })
if (variables[1] === 'sys.query' || variables[1] === 'sys.files') if (variables[1] === 'sys.query' || variables[1] === 'sys.files')
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]]) editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]])
else else editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables)
editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, variables) },
}, [editor, checkForTriggerMatch, triggerString]) [editor, checkForTriggerMatch, triggerString]
)
const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>(( const renderMenu = useCallback<MenuRenderFn<PickerBlockMenuOption>>(
anchorElementRef, (anchorElementRef, { options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
{ options, selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }, if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show))) return null
) => { refs.setReference(anchorElementRef.current)
if (!(anchorElementRef.current && (allFlattenOptions.length || workflowVariableBlock?.show)))
return null
refs.setReference(anchorElementRef.current)
return ( return (
<> <>
{ {ReactDOM.createPortal(
ReactDOM.createPortal(
// The `LexicalMenu` will try to calculate the position of the floating menu based on the first child. // The `LexicalMenu` will try to calculate the position of the floating menu based on the first child.
// Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected. // Since we use floating ui, we need to wrap it with a div to prevent the position calculation being affected.
// See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493 // See https://github.com/facebook/lexical/blob/ac97dfa9e14a73ea2d6934ff566282d7f758e8bb/packages/lexical-react/src/shared/LexicalMenu.ts#L493
<div className='w-0 h-0'> <div className="w-0 h-0">
<div <div
className='p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto overflow-x-hidden' className="p-1 w-[260px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg overflow-y-auto overflow-x-hidden"
style={{ style={{
...floatingStyles, ...floatingStyles,
visibility: isPositioned ? 'visible' : 'hidden', visibility: isPositioned ? 'visible' : 'hidden',
maxHeight: 'calc(1 / 3 * 100vh)', maxHeight: 'calc(1 / 3 * 100vh)'
}} }}
ref={refs.setFloating} ref={refs.setFloating}
> >
{ {options.map((option, index) => (
options.map((option, index) => ( <Fragment key={option.key}>
<Fragment key={option.key}> {
{ // Divider
// Divider index !== 0 && options.at(index - 1)?.group !== option.group && (
index !== 0 && options.at(index - 1)?.group !== option.group && ( <div className="h-px bg-gray-100 my-1 w-screen -translate-x-1"></div>
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div> )
) }
{option.renderMenuOption({
queryString,
isSelected: selectedIndex === index,
onSelect: () => {
selectOptionAndCleanUp(option)
},
onSetHighlight: () => {
setHighlightedIndex(index)
} }
{option.renderMenuOption({ })}
queryString, </Fragment>
isSelected: selectedIndex === index, ))}
onSelect: () => { {workflowVariableBlock?.show && (
selectOptionAndCleanUp(option) <>
}, {!!options.length && <div className="h-px bg-gray-100 my-1 w-screen -translate-x-1"></div>}
onSetHighlight: () => { <div className="p-1">
setHighlightedIndex(index) {/* <VarReferenceVars
},
})}
</Fragment>
))
}
{
workflowVariableBlock?.show && (
<>
{
(!!options.length) && (
<div className='h-px bg-gray-100 my-1 w-screen -translate-x-1'></div>
)
}
<div className='p-1'>
{/* <VarReferenceVars
hideSearch hideSearch
vars={workflowVariableOptions} vars={workflowVariableOptions}
onChange={(variables: string[]) => { onChange={(variables: string[]) => {
handleSelectWorkflowVariable(variables) handleSelectWorkflowVariable(variables)
}} }}
/> */} /> */}
</div> </div>
</> </>
) )}
}
</div> </div>
</div>, </div>,
anchorElementRef.current, anchorElementRef.current
) )}
} </>
</> )
) },
}, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable]) [
allFlattenOptions.length,
workflowVariableBlock?.show,
refs,
isPositioned,
floatingStyles,
queryString,
workflowVariableOptions,
handleSelectWorkflowVariable
]
)
return ( return (
<LexicalTypeaheadMenuPlugin <LexicalTypeaheadMenuPlugin
@@ -202,7 +183,7 @@ const ComponentPicker = ({
// //
// We no need the position function of the `LexicalTypeaheadMenuPlugin`, // We no need the position function of the `LexicalTypeaheadMenuPlugin`,
// so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui. // so the reference anchor should be positioned based on the range of the trigger string, and the menu will be positioned by the floating ui.
anchorClassName='z-[999999] translate-y-[calc(-100%-3px)]' anchorClassName="z-[999999] translate-y-[calc(-100%-3px)]"
menuRenderFn={renderMenu} menuRenderFn={renderMenu}
triggerFn={checkForTriggerMatch} triggerFn={checkForTriggerMatch}
/> />
@@ -20,12 +20,14 @@ export class PickerBlockMenuOption extends MenuOption {
group?: string group?: string
onSelect?: () => void onSelect?: () => void
render: (menuRenderProps: MenuOptionRenderProps) => JSX.Element render: (menuRenderProps: MenuOptionRenderProps) => JSX.Element
}, }
) { ) {
super(data.key) super(data.key)
this.group = data.group this.group = data.group
} }
public onSelectMenuOption = () => this.data.onSelect?.() public onSelectMenuOption = () => this.data.onSelect?.()
public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => <Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment> public renderMenuOption = (menuRenderProps: MenuOptionRenderProps) => (
<Fragment key={this.data.key}>{this.data.render(menuRenderProps)}</Fragment>
)
} }
@@ -9,37 +9,30 @@ type PromptMenuItemMenuItemProps = {
onMouseEnter: () => void onMouseEnter: () => void
setRefElement?: (element: HTMLDivElement) => void setRefElement?: (element: HTMLDivElement) => void
} }
export const PromptMenuItem = memo(({ export const PromptMenuItem = memo(
icon, ({ icon, title, disabled, isSelected, onClick, onMouseEnter, setRefElement }: PromptMenuItemMenuItemProps) => {
title, return (
disabled, <div
isSelected, className={`
onClick,
onMouseEnter,
setRefElement,
}: PromptMenuItemMenuItemProps) => {
return (
<div
className={`
flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md flex items-center px-3 h-6 cursor-pointer hover:bg-gray-50 rounded-md
${isSelected && !disabled && '!bg-gray-50'} ${isSelected && !disabled && '!bg-gray-50'}
${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'} ${disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
`} `}
tabIndex={-1} tabIndex={-1}
ref={setRefElement} ref={setRefElement}
onMouseEnter={() => { onMouseEnter={() => {
if (disabled) if (disabled) return
return onMouseEnter()
onMouseEnter() }}
}} onClick={() => {
onClick={() => { if (disabled) return
if (disabled) onClick()
return }}
onClick() >
}}> {icon}
{icon} <div className="ml-1 text-[13px] text-gray-900">{title}</div>
<div className='ml-1 text-[13px] text-gray-900'>{title}</div> </div>
</div> )
) }
}) )
PromptMenuItem.displayName = 'PromptMenuItem' PromptMenuItem.displayName = 'PromptMenuItem'
@@ -10,51 +10,52 @@ type VariableMenuItemProps = {
onMouseEnter: () => void onMouseEnter: () => void
setRefElement?: (element: HTMLDivElement) => void setRefElement?: (element: HTMLDivElement) => void
} }
export const VariableMenuItem = memo(({ export const VariableMenuItem = memo(
title, ({
icon, title,
extraElement, icon,
isSelected, extraElement,
queryString, isSelected,
onClick, queryString,
onMouseEnter, onClick,
setRefElement, onMouseEnter,
}: VariableMenuItemProps) => { setRefElement
let before = title }: VariableMenuItemProps) => {
let middle = '' let before = title
let after = '' let middle = ''
let after = ''
if (queryString) { if (queryString) {
const regex = new RegExp(queryString, 'i') const regex = new RegExp(queryString, 'i')
const match = regex.exec(title) const match = regex.exec(title)
if (match) { if (match) {
before = title.substring(0, match.index) before = title.substring(0, match.index)
middle = match[0] middle = match[0]
after = title.substring(match.index + match[0].length) after = title.substring(match.index + match[0].length)
}
} }
}
return ( return (
<div <div
className={` className={`
flex items-center px-3 h-6 rounded-md hover:bg-[#EBEEF2] cursor-pointer flex items-center px-3 h-6 rounded-md hover:bg-[#EBEEF2] cursor-pointer
${isSelected && 'bg-[#EBEEF2]'} ${isSelected && 'bg-[#EBEEF2]'}
`} `}
tabIndex={-1} tabIndex={-1}
ref={setRefElement} ref={setRefElement}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onClick={onClick}> onClick={onClick}
<div className='mr-2'> >
{icon} <div className="mr-2">{icon}</div>
<div className="grow text-[13px] text-gray-900 truncate" title={title}>
{before}
<span className="text-[#2970FF]">{middle}</span>
{after}
</div>
{extraElement}
</div> </div>
<div className='grow text-[13px] text-gray-900 truncate' title={title}> )
{before} }
<span className='text-[#2970FF]'>{middle}</span> )
{after}
</div>
{extraElement}
</div>
)
})
VariableMenuItem.displayName = 'VariableMenuItem' VariableMenuItem.displayName = 'VariableMenuItem'
@@ -1,6 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// import { // import {
// RiAddLine, // RiAddLine,
// } from '@remixicon/react' // } from '@remixicon/react'
@@ -28,7 +27,7 @@ const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
nodeKey, nodeKey,
datasets = [], datasets = [],
onAddContext, onAddContext,
canNotAddContext, canNotAddContext
}) => { }) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND) const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND)
const [triggerRef, open, setOpen] = useTrigger() const [triggerRef, open, setOpen] = useTrigger()
@@ -36,19 +35,20 @@ const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
const [localDatasets, setLocalDatasets] = useState<Dataset[]>(datasets) const [localDatasets, setLocalDatasets] = useState<Dataset[]>(datasets)
eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_DATASETS_EVENT_EMITTER) if (v?.type === UPDATE_DATASETS_EVENT_EMITTER) setLocalDatasets(v.payload)
setLocalDatasets(v.payload)
}) })
return ( return (
<div className={` <div
className={`
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent bg-[#F4F3FF] text-[#6938EF] rounded-[5px] hover:bg-[#EBE9FE] group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent bg-[#F4F3FF] text-[#6938EF] rounded-[5px] hover:bg-[#EBE9FE]
${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'} ${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'}
${isSelected && '!border-[#9B8AFB]'} ${isSelected && '!border-[#9B8AFB]'}
`} ref={ref}> `}
ref={ref}
>
{/* <File05 className='mr-1 w-[14px] h-[14px]' /> */} {/* <File05 className='mr-1 w-[14px] h-[14px]' /> */}
<div className='mr-1 text-xs font-medium'>{$t('上下文')}</div> <div className="mr-1 text-xs font-medium">{$t('上下文')}</div>
</div> </div>
) )
} }
@@ -1,18 +1,11 @@
import { import { memo, useCallback, useEffect } from 'react'
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical' import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils' import { decoratorTransform } from '../../utils'
import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants' import { CONTEXT_PLACEHOLDER_TEXT } from '../../constants'
import type { ContextBlockType } from '../../types' import type { ContextBlockType } from '../../types'
import { import { $createContextBlockNode, ContextBlockNode } from './node'
$createContextBlockNode,
ContextBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node' import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT) const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
@@ -21,7 +14,7 @@ const ContextBlockReplacementBlock = ({
datasets = [], datasets = [],
onAddContext = () => {}, onAddContext = () => {},
onInsert, onInsert,
canNotAddContext, canNotAddContext
}: ContextBlockType) => { }: ContextBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -31,29 +24,29 @@ const ContextBlockReplacementBlock = ({
}, [editor]) }, [editor])
const createContextBlockNode = useCallback((): ContextBlockNode => { const createContextBlockNode = useCallback((): ContextBlockNode => {
if (onInsert) if (onInsert) onInsert()
onInsert()
return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext)) return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext, canNotAddContext))
}, [datasets, onAddContext, onInsert, canNotAddContext]) }, [datasets, onAddContext, onInsert, canNotAddContext])
const getMatch = useCallback((text: string) => { const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text) const matchArr = REGEX.exec(text)
if (matchArr === null) if (matchArr === null) return null
return null
const startOffset = matchArr.index const startOffset = matchArr.index
const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length
return { return {
end: endOffset, end: endOffset,
start: startOffset, start: startOffset
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
REGEX.lastIndex = 0 REGEX.lastIndex = 0
return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)), editor.registerNodeTransform(CustomTextNode, (textNode) =>
decoratorTransform(textNode, getMatch, createContextBlockNode)
)
) )
}, []) }, [])
@@ -1,19 +1,9 @@
import { import { memo, useEffect } from 'react'
memo, import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { ContextBlockType } from '../../types' import type { ContextBlockType } from '../../types'
import { import { $createContextBlockNode, ContextBlockNode } from './node'
$createContextBlockNode,
ContextBlockNode,
} from './node'
export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND') export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND')
export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND') export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND')
@@ -24,49 +14,43 @@ export type Dataset = {
type: string type: string
} }
const ContextBlock = memo(({ const ContextBlock = memo(
datasets = [], ({ datasets = [], onAddContext = () => {}, onInsert, onDelete, canNotAddContext }: ContextBlockType) => {
onAddContext = () => {}, const [editor] = useLexicalComposerContext()
onInsert,
onDelete,
canNotAddContext,
}: ContextBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
if (!editor.hasNodes([ContextBlockNode])) if (!editor.hasNodes([ContextBlockNode]))
throw new Error('ContextBlockPlugin: ContextBlock not registered on editor') throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
return mergeRegister( return mergeRegister(
editor.registerCommand( editor.registerCommand(
INSERT_CONTEXT_BLOCK_COMMAND, INSERT_CONTEXT_BLOCK_COMMAND,
() => { () => {
const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext) const contextBlockNode = $createContextBlockNode(datasets, onAddContext, canNotAddContext)
$insertNodes([contextBlockNode]) $insertNodes([contextBlockNode])
if (onInsert) if (onInsert) onInsert()
onInsert()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
DELETE_CONTEXT_BLOCK_COMMAND, DELETE_CONTEXT_BLOCK_COMMAND,
() => { () => {
if (onDelete) if (onDelete) onDelete()
onDelete()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext]) }, [editor, datasets, onAddContext, onInsert, onDelete, canNotAddContext])
return null return null
}) }
)
ContextBlock.displayName = 'ContextBlock' ContextBlock.displayName = 'ContextBlock'
export { ContextBlock } export { ContextBlock }
@@ -3,7 +3,11 @@ import { DecoratorNode } from 'lexical'
import ContextBlockComponent from './component' import ContextBlockComponent from './component'
import type { Dataset } from './index' import type { Dataset } from './index'
export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void; canNotAddContext: boolean } export type SerializedNode = SerializedLexicalNode & {
datasets: Dataset[]
onAddContext: () => void
canNotAddContext: boolean
}
export class ContextBlockNode extends DecoratorNode<JSX.Element> { export class ContextBlockNode extends DecoratorNode<JSX.Element> {
__datasets: Dataset[] __datasets: Dataset[]
@@ -70,7 +74,11 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
} }
static importJSON(serializedNode: SerializedNode): ContextBlockNode { static importJSON(serializedNode: SerializedNode): ContextBlockNode {
const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext, serializedNode.canNotAddContext) const node = $createContextBlockNode(
serializedNode.datasets,
serializedNode.onAddContext,
serializedNode.canNotAddContext
)
return node return node
} }
@@ -81,7 +89,7 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
version: 1, version: 1,
datasets: this.getDatasets(), datasets: this.getDatasets(),
onAddContext: this.getOnAddContext(), onAddContext: this.getOnAddContext(),
canNotAddContext: this.getCanNotAddContext(), canNotAddContext: this.getCanNotAddContext()
} }
} }
@@ -89,12 +97,14 @@ export class ContextBlockNode extends DecoratorNode<JSX.Element> {
return '{{#context#}}' return '{{#context#}}'
} }
} }
export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void, canNotAddContext?: boolean): ContextBlockNode { export function $createContextBlockNode(
datasets: Dataset[],
onAddContext: () => void,
canNotAddContext?: boolean
): ContextBlockNode {
return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext) return new ContextBlockNode(datasets, onAddContext, undefined, canNotAddContext)
} }
export function $isContextBlockNode( export function $isContextBlockNode(node: ContextBlockNode | LexicalNode | null | undefined): boolean {
node: ContextBlockNode | LexicalNode | null | undefined,
): boolean {
return node instanceof ContextBlockNode return node instanceof ContextBlockNode
} }
@@ -37,13 +37,12 @@ export class CustomTextNode extends TextNode {
style: this.getStyle(), style: this.getStyle(),
text: this.getTextContent(), text: this.getTextContent(),
type: 'custom-text', type: 'custom-text',
version: 1, version: 1
} }
} }
isSimpleText() { isSimpleText() {
return ( return (this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0
(this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0)
} }
} }
@@ -1,6 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next'
// import { // import {
// RiMoreFill, // RiMoreFill,
// } from '@remixicon/react' // } from '@remixicon/react'
@@ -26,7 +25,7 @@ type HistoryBlockComponentProps = {
const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({ const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
nodeKey, nodeKey,
roleName = { user: '', assistant: '' }, roleName = { user: '', assistant: '' },
onEditRole, onEditRole
}) => { }) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND) const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND)
const [triggerRef, open, setOpen] = useTrigger() const [triggerRef, open, setOpen] = useTrigger()
@@ -34,18 +33,20 @@ const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
const [localRoleName, setLocalRoleName] = useState<RoleName>(roleName) const [localRoleName, setLocalRoleName] = useState<RoleName>(roleName)
eventEmitter?.useSubscription((v: any) => { eventEmitter?.useSubscription((v: any) => {
if (v?.type === UPDATE_HISTORY_EVENT_EMITTER) if (v?.type === UPDATE_HISTORY_EVENT_EMITTER) setLocalRoleName(v.payload)
setLocalRoleName(v.payload)
}) })
return ( return (
<div className={` <div
className={`
group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent text-[#DD2590] rounded-[5px] hover:bg-[#FCE7F6] group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent text-[#DD2590] rounded-[5px] hover:bg-[#FCE7F6]
${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'} ${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'}
${isSelected && '!border-[#F670C7]'} ${isSelected && '!border-[#F670C7]'}
`} ref={ref}> `}
ref={ref}
>
{/* <MessageClockCircle className='mr-1 w-[14px] h-[14px]' /> */} {/* <MessageClockCircle className='mr-1 w-[14px] h-[14px]' /> */}
<div className='mr-1 text-xs font-medium'>{$t('会话历史')}</div> <div className="mr-1 text-xs font-medium">{$t('会话历史')}</div>
</div> </div>
) )
} }
@@ -1,17 +1,11 @@
import { import { useCallback, useEffect } from 'react'
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical' import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils' import { decoratorTransform } from '../../utils'
import { HISTORY_PLACEHOLDER_TEXT } from '../../constants' import { HISTORY_PLACEHOLDER_TEXT } from '../../constants'
import type { HistoryBlockType } from '../../types' import type { HistoryBlockType } from '../../types'
import { import { $createHistoryBlockNode, HistoryBlockNode } from './node'
$createHistoryBlockNode,
HistoryBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node' import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT) const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
@@ -19,7 +13,7 @@ const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
const HistoryBlockReplacementBlock = ({ const HistoryBlockReplacementBlock = ({
history = { user: '', assistant: '' }, history = { user: '', assistant: '' },
onEditRole = () => {}, onEditRole = () => {},
onInsert, onInsert
}: HistoryBlockType) => { }: HistoryBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -29,29 +23,29 @@ const HistoryBlockReplacementBlock = ({
}, [editor]) }, [editor])
const createHistoryBlockNode = useCallback((): HistoryBlockNode => { const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
if (onInsert) if (onInsert) onInsert()
onInsert()
return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole)) return $applyNodeReplacement($createHistoryBlockNode(history, onEditRole))
}, [history, onEditRole, onInsert]) }, [history, onEditRole, onInsert])
const getMatch = useCallback((text: string) => { const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text) const matchArr = REGEX.exec(text)
if (matchArr === null) if (matchArr === null) return null
return null
const startOffset = matchArr.index const startOffset = matchArr.index
const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length
return { return {
end: endOffset, end: endOffset,
start: startOffset, start: startOffset
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
REGEX.lastIndex = 0 REGEX.lastIndex = 0
return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)), editor.registerNodeTransform(CustomTextNode, (textNode) =>
decoratorTransform(textNode, getMatch, createHistoryBlockNode)
)
) )
}, []) }, [])
@@ -1,19 +1,9 @@
import { import { memo, useEffect } from 'react'
memo, import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { HistoryBlockType } from '../../types' import type { HistoryBlockType } from '../../types'
import { import { $createHistoryBlockNode, HistoryBlockNode } from './node'
$createHistoryBlockNode,
HistoryBlockNode,
} from './node'
export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND') export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND')
export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND') export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND')
@@ -30,48 +20,43 @@ export type HistoryBlockProps = {
onDelete?: () => void onDelete?: () => void
} }
const HistoryBlock = memo(({ const HistoryBlock = memo(
history = { user: '', assistant: '' }, ({ history = { user: '', assistant: '' }, onEditRole = () => {}, onInsert, onDelete }: HistoryBlockType) => {
onEditRole = () => {}, const [editor] = useLexicalComposerContext()
onInsert,
onDelete,
}: HistoryBlockType) => {
const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
if (!editor.hasNodes([HistoryBlockNode])) if (!editor.hasNodes([HistoryBlockNode]))
throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor') throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
return mergeRegister( return mergeRegister(
editor.registerCommand( editor.registerCommand(
INSERT_HISTORY_BLOCK_COMMAND, INSERT_HISTORY_BLOCK_COMMAND,
() => { () => {
const historyBlockNode = $createHistoryBlockNode(history, onEditRole) const historyBlockNode = $createHistoryBlockNode(history, onEditRole)
$insertNodes([historyBlockNode]) $insertNodes([historyBlockNode])
if (onInsert) if (onInsert) onInsert()
onInsert()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
DELETE_HISTORY_BLOCK_COMMAND, DELETE_HISTORY_BLOCK_COMMAND,
() => { () => {
if (onDelete) if (onDelete) onDelete()
onDelete()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor, history, onEditRole, onInsert, onDelete]) }, [editor, history, onEditRole, onInsert, onDelete])
return null return null
}) }
)
HistoryBlock.displayName = 'HistoryBlock' HistoryBlock.displayName = 'HistoryBlock'
export { HistoryBlock } export { HistoryBlock }
@@ -40,11 +40,7 @@ export class HistoryBlockNode extends DecoratorNode<JSX.Element> {
decorate(): JSX.Element { decorate(): JSX.Element {
return ( return (
<HistoryBlockComponent <HistoryBlockComponent nodeKey={this.getKey()} roleName={this.getRoleName()} onEditRole={this.getOnEditRole()} />
nodeKey={this.getKey()}
roleName={this.getRoleName()}
onEditRole={this.getOnEditRole()}
/>
) )
} }
@@ -71,7 +67,7 @@ export class HistoryBlockNode extends DecoratorNode<JSX.Element> {
type: 'history-block', type: 'history-block',
version: 1, version: 1,
roleName: this.getRoleName(), roleName: this.getRoleName(),
onEditRole: this.getOnEditRole, onEditRole: this.getOnEditRole
} }
} }
@@ -83,8 +79,6 @@ export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => vo
return new HistoryBlockNode(roleName, onEditRole) return new HistoryBlockNode(roleName, onEditRole)
} }
export function $isHistoryBlockNode( export function $isHistoryBlockNode(node: HistoryBlockNode | LexicalNode | null | undefined): node is HistoryBlockNode {
node: HistoryBlockNode | LexicalNode | null | undefined,
): node is HistoryBlockNode {
return node instanceof HistoryBlockNode return node instanceof HistoryBlockNode
} }
@@ -1,11 +1,6 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { import { BLUR_COMMAND, COMMAND_PRIORITY_EDITOR, FOCUS_COMMAND, KEY_ESCAPE_COMMAND } from 'lexical'
BLUR_COMMAND,
COMMAND_PRIORITY_EDITOR,
FOCUS_COMMAND,
KEY_ESCAPE_COMMAND,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block' import { CLEAR_HIDE_MENU_TIMEOUT } from './workflow-variable-block'
@@ -14,10 +9,7 @@ type OnBlurBlockProps = {
onBlur?: () => void onBlur?: () => void
onFocus?: () => void onFocus?: () => void
} }
const OnBlurBlock: FC<OnBlurBlockProps> = ({ const OnBlurBlock: FC<OnBlurBlockProps> = ({ onBlur, onFocus }) => {
onBlur,
onFocus,
}) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
const ref = useRef<any>(null) const ref = useRef<any>(null)
@@ -33,7 +25,7 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
} }
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
BLUR_COMMAND, BLUR_COMMAND,
@@ -42,22 +34,20 @@ const OnBlurBlock: FC<OnBlurBlockProps> = ({
editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' })) editor.dispatchCommand(KEY_ESCAPE_COMMAND, new KeyboardEvent('keydown', { key: 'Escape' }))
}, 200) }, 200)
if (onBlur) if (onBlur) onBlur()
onBlur()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
FOCUS_COMMAND, FOCUS_COMMAND,
() => { () => {
if (onFocus) if (onFocus) onFocus()
onFocus()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor, onBlur, onFocus]) }, [editor, onBlur, onFocus])
@@ -1,18 +1,10 @@
import { $t } from '@common/locales' import { $t } from '@common/locales'
import { memo } from 'react' import { memo } from 'react'
const Placeholder = ({ const Placeholder = ({ compact, value }: { compact?: boolean; value?: string; className?: string }) => {
compact,
value
}: {
compact?: boolean
value?: string
className?: string
}) => {
return ( return (
<div <div
className={`absolute top-0 left-0 h-full w-full text-sm text-[#BBB] select-none pointer-events-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'}`} className={`absolute top-0 left-0 h-full w-full text-sm text-[#BBB] select-none pointer-events-none ${compact ? 'leading-5 text-[13px]' : 'leading-6 text-sm'}`}
> >
{value || $t('AI 模型调用默认仅使用 Query 变量,可输入 “{” 增加新变量。')} {value || $t('AI 模型调用默认仅使用 Query 变量,可输入 “{” 增加新变量。')}
</div> </div>
@@ -1,5 +1,4 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelectOrDelete } from '../../hooks' import { useSelectOrDelete } from '../../hooks'
import { DELETE_QUERY_BLOCK_COMMAND } from './index' import { DELETE_QUERY_BLOCK_COMMAND } from './index'
import { $t } from '@common/locales' import { $t } from '@common/locales'
@@ -9,9 +8,7 @@ type QueryBlockComponentProps = {
nodeKey: string nodeKey: string
} }
const QueryBlockComponent: FC<QueryBlockComponentProps> = ({ const QueryBlockComponent: FC<QueryBlockComponentProps> = ({ nodeKey }) => {
nodeKey,
}) => {
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND) const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
return ( return (
@@ -23,9 +20,9 @@ const QueryBlockComponent: FC<QueryBlockComponentProps> = ({
ref={ref} ref={ref}
> >
{/* <UserEdit02 className='mr-1 w-[14px] h-[14px] text-[#FD853A]' /> */} {/* <UserEdit02 className='mr-1 w-[14px] h-[14px] text-[#FD853A]' /> */}
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'{{'}</div> <div className="text-xs font-medium text-[#EC4A0A] opacity-60">{'{{'}</div>
<div className='text-xs font-medium text-[#EC4A0A]'>{$t('查询内容')}</div> <div className="text-xs font-medium text-[#EC4A0A]">{$t('查询内容')}</div>
<div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'}}'}</div> <div className="text-xs font-medium text-[#EC4A0A] opacity-60">{'}}'}</div>
</div> </div>
) )
} }
@@ -1,19 +1,9 @@
import { import { memo, useEffect } from 'react'
memo, import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { QueryBlockType } from '../../types' import type { QueryBlockType } from '../../types'
import { import { $createQueryBlockNode, QueryBlockNode } from './node'
$createQueryBlockNode,
QueryBlockNode,
} from './node'
export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND') export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND')
export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND') export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND')
@@ -22,15 +12,11 @@ export type QueryBlockProps = {
onInsert?: () => void onInsert?: () => void
onDelete?: () => void onDelete?: () => void
} }
const QueryBlock = memo(({ const QueryBlock = memo(({ onInsert, onDelete }: QueryBlockType) => {
onInsert,
onDelete,
}: QueryBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
if (!editor.hasNodes([QueryBlockNode])) if (!editor.hasNodes([QueryBlockNode])) throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
return mergeRegister( return mergeRegister(
editor.registerCommand( editor.registerCommand(
@@ -39,23 +25,21 @@ const QueryBlock = memo(({
const contextBlockNode = $createQueryBlockNode() const contextBlockNode = $createQueryBlockNode()
$insertNodes([contextBlockNode]) $insertNodes([contextBlockNode])
if (onInsert) if (onInsert) onInsert()
onInsert()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
DELETE_QUERY_BLOCK_COMMAND, DELETE_QUERY_BLOCK_COMMAND,
() => { () => {
if (onDelete) if (onDelete) onDelete()
onDelete()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor, onInsert, onDelete]) }, [editor, onInsert, onDelete])
@@ -40,7 +40,7 @@ export class QueryBlockNode extends DecoratorNode<JSX.Element> {
exportJSON(): SerializedNode { exportJSON(): SerializedNode {
return { return {
type: 'query-block', type: 'query-block',
version: 1, version: 1
} }
} }
@@ -52,8 +52,6 @@ export function $createQueryBlockNode(): QueryBlockNode {
return new QueryBlockNode() return new QueryBlockNode()
} }
export function $isQueryBlockNode( export function $isQueryBlockNode(node: QueryBlockNode | LexicalNode | null | undefined): node is QueryBlockNode {
node: QueryBlockNode | LexicalNode | null | undefined,
): node is QueryBlockNode {
return node instanceof QueryBlockNode return node instanceof QueryBlockNode
} }
@@ -1,25 +1,16 @@
import { import { memo, useCallback, useEffect } from 'react'
memo,
useCallback,
useEffect,
} from 'react'
import { $applyNodeReplacement } from 'lexical' import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { decoratorTransform } from '../../utils' import { decoratorTransform } from '../../utils'
import { QUERY_PLACEHOLDER_TEXT } from '../../constants' import { QUERY_PLACEHOLDER_TEXT } from '../../constants'
import type { QueryBlockType } from '../../types' import type { QueryBlockType } from '../../types'
import { import { $createQueryBlockNode, QueryBlockNode } from './node'
$createQueryBlockNode,
QueryBlockNode,
} from './node'
import { CustomTextNode } from '../custom-text/node' import { CustomTextNode } from '../custom-text/node'
const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT) const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
const QueryBlockReplacementBlock = ({ const QueryBlockReplacementBlock = ({ onInsert }: QueryBlockType) => {
onInsert,
}: QueryBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
@@ -28,29 +19,29 @@ const QueryBlockReplacementBlock = ({
}, [editor]) }, [editor])
const createQueryBlockNode = useCallback((): QueryBlockNode => { const createQueryBlockNode = useCallback((): QueryBlockNode => {
if (onInsert) if (onInsert) onInsert()
onInsert()
return $applyNodeReplacement($createQueryBlockNode()) return $applyNodeReplacement($createQueryBlockNode())
}, [onInsert]) }, [onInsert])
const getMatch = useCallback((text: string) => { const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text) const matchArr = REGEX.exec(text)
if (matchArr === null) if (matchArr === null) return null
return null
const startOffset = matchArr.index const startOffset = matchArr.index
const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length
return { return {
end: endOffset, end: endOffset,
start: startOffset, start: startOffset
} }
}, []) }, [])
useEffect(() => { useEffect(() => {
REGEX.lastIndex = 0 REGEX.lastIndex = 0
return mergeRegister( return mergeRegister(
editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)), editor.registerNodeTransform(CustomTextNode, (textNode) =>
decoratorTransform(textNode, getMatch, createQueryBlockNode)
)
) )
}, []) }, [])
@@ -11,9 +11,7 @@ export const PROMPT_EDITOR_INSERT_QUICKLY = 'PROMPT_EDITOR_INSERT_QUICKLY'
type UpdateBlockProps = { type UpdateBlockProps = {
instanceId?: string instanceId?: string
} }
const UpdateBlock = ({ const UpdateBlock = ({ instanceId }: UpdateBlockProps) => {
instanceId,
}: UpdateBlockProps) => {
const { eventEmitter } = useEventEmitterContextContext() const { eventEmitter } = useEventEmitterContextContext()
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -1,9 +1,5 @@
import { useEffect } from 'react' import { useEffect } from 'react'
import { import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { CustomTextNode } from '../custom-text/node' import { CustomTextNode } from '../custom-text/node'
@@ -24,7 +20,7 @@ const VariableBlock = () => {
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
INSERT_VARIABLE_VALUE_BLOCK_COMMAND, INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
@@ -34,8 +30,8 @@ const VariableBlock = () => {
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor]) }, [editor])
@@ -1,14 +1,8 @@
import { import { useCallback, useEffect } from 'react'
useCallback,
useEffect,
} from 'react'
import type { TextNode } from 'lexical' import type { TextNode } from 'lexical'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useLexicalTextEntity } from '../../hooks' import { useLexicalTextEntity } from '../../hooks'
import { import { $createVariableValueBlockNode, VariableValueBlockNode } from './node'
$createVariableValueBlockNode,
VariableValueBlockNode,
} from './node'
import { getHashtagRegexString } from './utils' import { getHashtagRegexString } from './utils'
const REGEX = new RegExp(getHashtagRegexString(), 'i') const REGEX = new RegExp(getHashtagRegexString(), 'i')
@@ -28,22 +22,21 @@ const VariableValueBlock = () => {
const getVariableValueMatch = useCallback((text: string) => { const getVariableValueMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text) const matchArr = REGEX.exec(text)
if (matchArr === null) if (matchArr === null) return null
return null
const hashtagLength = matchArr[0].length const hashtagLength = matchArr[0].length
const startOffset = matchArr.index const startOffset = matchArr.index
const endOffset = startOffset + hashtagLength const endOffset = startOffset + hashtagLength
return { return {
end: endOffset, end: endOffset,
start: startOffset, start: startOffset
} }
}, []) }, [])
useLexicalTextEntity<VariableValueBlockNode>( useLexicalTextEntity<VariableValueBlockNode>(
getVariableValueMatch, getVariableValueMatch,
VariableValueBlockNode, VariableValueBlockNode,
createVariableValueBlockNode, createVariableValueBlockNode
) )
return null return null
@@ -1,13 +1,5 @@
import type { import type { EditorConfig, LexicalNode, NodeKey, SerializedTextNode } from 'lexical'
EditorConfig, import { $applyNodeReplacement, TextNode } from 'lexical'
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical'
import {
$applyNodeReplacement,
TextNode,
} from 'lexical'
export class VariableValueBlockNode extends TextNode { export class VariableValueBlockNode extends TextNode {
static getType(): string { static getType(): string {
@@ -24,7 +16,15 @@ export class VariableValueBlockNode extends TextNode {
createDOM(config: EditorConfig): HTMLElement { createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config) const element = super.createDOM(config)
element.classList.add('inline-flex', 'items-center', 'px-0.5', 'h-[22px]', 'text-[#155EEF]', 'rounded-[5px]', 'align-middle') element.classList.add(
'inline-flex',
'items-center',
'px-0.5',
'h-[22px]',
'text-[#155EEF]',
'rounded-[5px]',
'align-middle'
)
return element return element
} }
@@ -45,7 +45,7 @@ export class VariableValueBlockNode extends TextNode {
style: this.getStyle(), style: this.getStyle(),
text: this.getTextContent(), text: this.getTextContent(),
type: 'variable-value-block', type: 'variable-value-block',
version: 1, version: 1
} }
} }
@@ -58,8 +58,6 @@ export function $createVariableValueBlockNode(text = ''): VariableValueBlockNode
return $applyNodeReplacement(new VariableValueBlockNode(text)) return $applyNodeReplacement(new VariableValueBlockNode(text))
} }
export function $isVariableValueNodeBlock( export function $isVariableValueNodeBlock(node: LexicalNode | null | undefined): node is VariableValueBlockNode {
node: LexicalNode | null | undefined,
): node is VariableValueBlockNode {
return node instanceof VariableValueBlockNode return node instanceof VariableValueBlockNode
} }
@@ -1,12 +1,6 @@
import { import { memo, useEffect, useState } from 'react'
memo,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import { COMMAND_PRIORITY_EDITOR } from 'lexical'
COMMAND_PRIORITY_EDITOR,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
// import { // import {
@@ -15,10 +9,7 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
import { useSelectOrDelete } from '../../hooks' import { useSelectOrDelete } from '../../hooks'
import type { WorkflowNodesMap } from './node' import type { WorkflowNodesMap } from './node'
import { WorkflowVariableBlockNode } from './node' import { WorkflowVariableBlockNode } from './node'
import { import { DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, UPDATE_WORKFLOW_NODES_MAP } from './index'
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
UPDATE_WORKFLOW_NODES_MAP,
} from './index'
// import cn from '@/utils/classnames' // import cn from '@/utils/classnames'
// import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' // import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
// import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others' // import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
@@ -36,7 +27,7 @@ type WorkflowVariableBlockComponentProps = {
const WorkflowVariableBlockComponent = ({ const WorkflowVariableBlockComponent = ({
nodeKey, nodeKey,
variables, variables,
workflowNodesMap = {}, workflowNodesMap = {}
}: WorkflowVariableBlockComponentProps) => { }: WorkflowVariableBlockComponentProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
@@ -66,14 +57,14 @@ const WorkflowVariableBlockComponent = ({
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor]) }, [editor])
const Item = ( const Item = (
<div <div
className={`mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none ${isSelected ? 'border-[#84ADFF] bg-[#F5F8FF]' : 'border-black/5 bg-white'}` } className={`mx-0.5 relative group/wrap flex items-center h-[18px] pl-0.5 pr-[3px] rounded-[5px] border select-none ${isSelected ? 'border-[#84ADFF] bg-[#F5F8FF]' : 'border-black/5 bg-white'}`}
ref={ref} ref={ref}
> >
{/* {!isEnv && !isChatVar && ( {/* {!isEnv && !isChatVar && (
@@ -93,7 +84,7 @@ const WorkflowVariableBlockComponent = ({
<Line3 className='mr-0.5 text-gray-300'></Line3> <Line3 className='mr-0.5 text-gray-300'></Line3>
</div> </div>
)} */} )} */}
<div className='flex items-center text-primary-600'> <div className="flex items-center text-primary-600">
{/* {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />} {/* {!isEnv && !isChatVar && <Variable02 className='shrink-0 w-3.5 h-3.5' />}
{isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />} {isEnv && <Env className='shrink-0 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />} {isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
@@ -107,7 +98,6 @@ const WorkflowVariableBlockComponent = ({
</div> </div>
) )
return Item return Item
} }
@@ -1,19 +1,9 @@
import { import { memo, useEffect } from 'react'
memo, import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand } from 'lexical'
useEffect,
} from 'react'
import {
$insertNodes,
COMMAND_PRIORITY_EDITOR,
createCommand,
} from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import type { WorkflowVariableBlockType } from '../../types' import type { WorkflowVariableBlockType } from '../../types'
import { import { $createWorkflowVariableBlockNode, WorkflowVariableBlockNode } from './node'
$createWorkflowVariableBlockNode,
WorkflowVariableBlockNode,
} from './node'
// import type { Node } from '@/app/components/workflow/types' // import type { Node } from '@/app/components/workflow/types'
export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND') export const INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND')
@@ -26,11 +16,7 @@ export type WorkflowVariableBlockProps = {
onInsert?: () => void onInsert?: () => void
onDelete?: () => void onDelete?: () => void
} }
const WorkflowVariableBlock = memo(({ const WorkflowVariableBlock = memo(({ workflowNodesMap, onInsert, onDelete }: WorkflowVariableBlockType) => {
workflowNodesMap,
onInsert,
onDelete,
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
@@ -51,23 +37,21 @@ const WorkflowVariableBlock = memo(({
const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap) const workflowVariableBlockNode = $createWorkflowVariableBlockNode(variables, workflowNodesMap)
$insertNodes([workflowVariableBlockNode]) $insertNodes([workflowVariableBlockNode])
if (onInsert) if (onInsert) onInsert()
onInsert()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), ),
editor.registerCommand( editor.registerCommand(
DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND, DELETE_WORKFLOW_VARIABLE_BLOCK_COMMAND,
() => { () => {
if (onDelete) if (onDelete) onDelete()
onDelete()
return true return true
}, },
COMMAND_PRIORITY_EDITOR, COMMAND_PRIORITY_EDITOR
), )
) )
}, [editor, onInsert, onDelete, workflowNodesMap]) }, [editor, onInsert, onDelete, workflowNodesMap])
@@ -63,7 +63,7 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
type: 'workflow-variable-block', type: 'workflow-variable-block',
version: 1, version: 1,
variables: this.getVariables(), variables: this.getVariables(),
workflowNodesMap: this.getWorkflowNodesMap(), workflowNodesMap: this.getWorkflowNodesMap()
} }
} }
@@ -81,12 +81,15 @@ export class WorkflowVariableBlockNode extends DecoratorNode<JSX.Element> {
return `{{#${this.getVariables().join('.')}#}}` return `{{#${this.getVariables().join('.')}#}}`
} }
} }
export function $createWorkflowVariableBlockNode(variables: string[], workflowNodesMap: WorkflowNodesMap): WorkflowVariableBlockNode { export function $createWorkflowVariableBlockNode(
variables: string[],
workflowNodesMap: WorkflowNodesMap
): WorkflowVariableBlockNode {
return new WorkflowVariableBlockNode(variables, workflowNodesMap) return new WorkflowVariableBlockNode(variables, workflowNodesMap)
} }
export function $isWorkflowVariableBlockNode( export function $isWorkflowVariableBlockNode(
node: WorkflowVariableBlockNode | LexicalNode | null | undefined, node: WorkflowVariableBlockNode | LexicalNode | null | undefined
): node is WorkflowVariableBlockNode { ): node is WorkflowVariableBlockNode {
return node instanceof WorkflowVariableBlockNode return node instanceof WorkflowVariableBlockNode
} }
@@ -1,8 +1,4 @@
import { import { memo, useCallback, useEffect } from 'react'
memo,
useCallback,
useEffect,
} from 'react'
import type { TextNode } from 'lexical' import type { TextNode } from 'lexical'
import { $applyNodeReplacement } from 'lexical' import { $applyNodeReplacement } from 'lexical'
import { mergeRegister } from '@lexical/utils' import { mergeRegister } from '@lexical/utils'
@@ -16,10 +12,7 @@ import { WorkflowVariableBlockNode } from './index'
export const REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi export const REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi
const WorkflowVariableBlockReplacementBlock = ({ const WorkflowVariableBlockReplacementBlock = ({ workflowNodesMap, onInsert }: WorkflowVariableBlockType) => {
workflowNodesMap,
onInsert,
}: WorkflowVariableBlockType) => {
const [editor] = useLexicalComposerContext() const [editor] = useLexicalComposerContext()
useEffect(() => { useEffect(() => {
@@ -27,37 +20,39 @@ const WorkflowVariableBlockReplacementBlock = ({
throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor') throw new Error('WorkflowVariableBlockNodePlugin: WorkflowVariableBlockNode not registered on editor')
}, [editor]) }, [editor])
const createWorkflowVariableBlockNode = useCallback((textNode: TextNode): WorkflowVariableBlockNode => { const createWorkflowVariableBlockNode = useCallback(
if (onInsert) (textNode: TextNode): WorkflowVariableBlockNode => {
onInsert() if (onInsert) onInsert()
const nodePathString = textNode.getTextContent().slice(3, -3) const nodePathString = textNode.getTextContent().slice(3, -3)
return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap)) return $applyNodeReplacement($createWorkflowVariableBlockNode(nodePathString.split('.'), workflowNodesMap))
}, [onInsert, workflowNodesMap]) },
[onInsert, workflowNodesMap]
)
const getMatch = useCallback((text: string) => { const getMatch = useCallback((text: string) => {
const matchArr = REGEX.exec(text) const matchArr = REGEX.exec(text)
if (matchArr === null) if (matchArr === null) return null
return null
const startOffset = matchArr.index const startOffset = matchArr.index
const endOffset = startOffset + matchArr[0].length const endOffset = startOffset + matchArr[0].length
return { return {
end: endOffset, end: endOffset,
start: startOffset, start: startOffset
} }
}, []) }, [])
const transformListener = useCallback((textNode: any) => { const transformListener = useCallback(
return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode) (textNode: any) => {
}, [createWorkflowVariableBlockNode, getMatch]) return decoratorTransform(textNode, getMatch, createWorkflowVariableBlockNode)
},
[createWorkflowVariableBlockNode, getMatch]
)
useEffect(() => { useEffect(() => {
REGEX.lastIndex = 0 REGEX.lastIndex = 0
return mergeRegister( return mergeRegister(editor.registerNodeTransform(CustomTextNode, transformListener))
editor.registerNodeTransform(CustomTextNode, transformListener),
)
}, []) }, [])
return null return null
@@ -20,7 +20,7 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
onHeightChange, onHeightChange,
children, children,
footer, footer,
hideResize, hideResize
}) => { }) => {
const [clientY, setClientY] = useState(0) const [clientY, setClientY] = useState(0)
const [isResizing, setIsResizing] = useState(false) const [isResizing, setIsResizing] = useState(false)
@@ -38,19 +38,20 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
document.body.style.userSelect = prevUserSelectStyle document.body.style.userSelect = prevUserSelectStyle
}, [prevUserSelectStyle]) }, [prevUserSelectStyle])
const { run: didHandleResize } = useDebounceFn((e) => { const { run: didHandleResize } = useDebounceFn(
if (!isResizing) (e) => {
return if (!isResizing) return
const offset = e.clientY - clientY const offset = e.clientY - clientY
let newHeight = height + offset let newHeight = height + offset
setClientY(e.clientY) setClientY(e.clientY)
if (newHeight < minHeight) if (newHeight < minHeight) newHeight = minHeight
newHeight = minHeight onHeightChange(newHeight)
onHeightChange(newHeight) },
}, { {
wait: 0, wait: 0
}) }
)
const handleResize = useCallback(didHandleResize, [isResizing, height, minHeight, clientY]) const handleResize = useCallback(didHandleResize, [isResizing, height, minHeight, clientY])
@@ -69,12 +70,11 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
}, [handleStopResize]) }, [handleStopResize])
return ( return (
<div <div className="relative rounded ant-input-outlined">
className='relative rounded ant-input-outlined' <div
> className={`${className} overflow-y-auto`}
<div className={`${className} overflow-y-auto`}
style={{ style={{
height, height
}} }}
> >
{children} {children}
@@ -83,9 +83,10 @@ const PromptEditorHeightResizeWrap: FC<Props> = ({
{footer} {footer}
{!hideResize && ( {!hideResize && (
<div <div
className='absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize' className="absolute bottom-0 left-0 w-full flex justify-center h-2 cursor-row-resize"
onMouseDown={handleStartResize}> onMouseDown={handleStartResize}
<div className='w-5 h-[3px] rounded-sm bg-gray-300'></div> >
<div className="w-5 h-[3px] rounded-sm bg-gray-300"></div>
</div> </div>
)} )}
</div> </div>
@@ -4,7 +4,6 @@ import type { RoleName } from './plugins/history-block/index'
// Node, // Node,
// } from '@/app/components/workflow/types' // } from '@/app/components/workflow/types'
export type NodeOutPutVar = { export type NodeOutPutVar = {
nodeId: string nodeId: string
title: string title: string
@@ -12,7 +11,6 @@ export type NodeOutPutVar = {
isStartNode?: boolean isStartNode?: boolean
} }
export type Option = { export type Option = {
value: string value: string
name: string name: string
@@ -1,45 +1,34 @@
import { $isAtNodeEnd } from '@lexical/selection' import { $isAtNodeEnd } from '@lexical/selection'
import type { import type { ElementNode, Klass, LexicalEditor, LexicalNode, RangeSelection, TextNode } from 'lexical'
ElementNode, import { $createTextNode, $getSelection, $isRangeSelection, $isTextNode } from 'lexical'
Klass,
LexicalEditor,
LexicalNode,
RangeSelection,
TextNode,
} from 'lexical'
import {
$createTextNode,
$getSelection,
$isRangeSelection,
$isTextNode,
} from 'lexical'
import type { EntityMatch } from '@lexical/text' import type { EntityMatch } from '@lexical/text'
import { CustomTextNode } from './plugins/custom-text/node' import { CustomTextNode } from './plugins/custom-text/node'
import type { MenuTextMatch } from './types' import type { MenuTextMatch } from './types'
import { CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT, MAX_VAR_KEY_LENGTH } from './constants' import {
CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT,
QUERY_PLACEHOLDER_TEXT,
PRE_PROMPT_PLACEHOLDER_TEXT,
MAX_VAR_KEY_LENGTH
} from './constants'
export function getSelectedNode( export function getSelectedNode(selection: RangeSelection): TextNode | ElementNode {
selection: RangeSelection,
): TextNode | ElementNode {
const anchor = selection.anchor const anchor = selection.anchor
const focus = selection.focus const focus = selection.focus
const anchorNode = selection.anchor.getNode() const anchorNode = selection.anchor.getNode()
const focusNode = selection.focus.getNode() const focusNode = selection.focus.getNode()
if (anchorNode === focusNode) if (anchorNode === focusNode) return anchorNode
return anchorNode
const isBackward = selection.isBackward() const isBackward = selection.isBackward()
if (isBackward) if (isBackward) return $isAtNodeEnd(focus) ? anchorNode : focusNode
return $isAtNodeEnd(focus) ? anchorNode : focusNode else return $isAtNodeEnd(anchor) ? anchorNode : focusNode
else
return $isAtNodeEnd(anchor) ? anchorNode : focusNode
} }
export function registerLexicalTextEntity<T extends TextNode>( export function registerLexicalTextEntity<T extends TextNode>(
editor: LexicalEditor, editor: LexicalEditor,
getMatch: (text: string) => null | EntityMatch, getMatch: (text: string) => null | EntityMatch,
targetNode: Klass<T>, targetNode: Klass<T>,
createNode: (textNode: TextNode) => T, createNode: (textNode: TextNode) => T
) { ) {
const isTargetNode = (node: LexicalNode | null | undefined): node is T => { const isTargetNode = (node: LexicalNode | null | undefined): node is T => {
return node instanceof targetNode return node instanceof targetNode
@@ -56,8 +45,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
} }
const textNodeTransform = (node: TextNode) => { const textNodeTransform = (node: TextNode) => {
if (!node.isSimpleText()) if (!node.isSimpleText()) return
return
const prevSibling = node.getPreviousSibling() const prevSibling = node.getPreviousSibling()
let text = node.getTextContent() let text = node.getTextContent()
@@ -73,8 +61,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
if (prevMatch === null || getMode(prevSibling) !== 0) { if (prevMatch === null || getMode(prevSibling) !== 0) {
replaceWithSimpleText(prevSibling) replaceWithSimpleText(prevSibling)
return return
} } else {
else {
const diff = prevMatch.end - previousText.length const diff = prevMatch.end - previousText.length
if (diff > 0) { if (diff > 0) {
@@ -85,8 +72,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
if (diff === text.length) { if (diff === text.length) {
node.remove() node.remove()
} } else {
else {
const remainingText = text.slice(diff) const remainingText = text.slice(diff)
node.setTextContent(remainingText) node.setTextContent(remainingText)
} }
@@ -94,8 +80,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
return return
} }
} }
} } else if (prevMatch === null || prevMatch.start < previousText.length) {
else if (prevMatch === null || prevMatch.start < previousText.length) {
return return
} }
} }
@@ -113,44 +98,34 @@ export function registerLexicalTextEntity<T extends TextNode>(
const nextMatch = getMatch(nextText) const nextMatch = getMatch(nextText)
if (nextMatch === null) { if (nextMatch === null) {
if (isTargetNode(nextSibling)) if (isTargetNode(nextSibling)) replaceWithSimpleText(nextSibling)
replaceWithSimpleText(nextSibling) else nextSibling.markDirty()
else
nextSibling.markDirty()
return return
} } else if (nextMatch.start !== 0) {
else if (nextMatch.start !== 0) {
return return
} }
} }
} } else {
else {
const nextMatch = getMatch(nextText) const nextMatch = getMatch(nextText)
if (nextMatch !== null && nextMatch.start === 0) if (nextMatch !== null && nextMatch.start === 0) return
return
} }
if (match === null) if (match === null) return
return
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) continue
continue
let nodeToReplace let nodeToReplace
if (match.start === 0) if (match.start === 0) [nodeToReplace, currentNode] = currentNode.splitText(match.end)
[nodeToReplace, currentNode] = currentNode.splitText(match.end) else [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
else
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
const replacementNode = createNode(nodeToReplace) const replacementNode = createNode(nodeToReplace)
replacementNode.setFormat(nodeToReplace.getFormat()) replacementNode.setFormat(nodeToReplace.getFormat())
nodeToReplace.replace(replacementNode) nodeToReplace.replace(replacementNode)
if (currentNode == null) if (currentNode == null) return
return
} }
} }
@@ -181,8 +156,7 @@ export function registerLexicalTextEntity<T extends TextNode>(
if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) { if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {
replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block
if (isTargetNode(node)) if (isTargetNode(node)) replaceWithSimpleText(node)
replaceWithSimpleText(node)
} }
} }
@@ -194,10 +168,9 @@ export function registerLexicalTextEntity<T extends TextNode>(
export const decoratorTransform = ( export const decoratorTransform = (
node: CustomTextNode, node: CustomTextNode,
getMatch: (text: string) => null | EntityMatch, getMatch: (text: string) => null | EntityMatch,
createNode: (textNode: TextNode) => LexicalNode, createNode: (textNode: TextNode) => LexicalNode
) => { ) => {
if (!node.isSimpleText()) if (!node.isSimpleText()) return
return
const prevSibling = node.getPreviousSibling() const prevSibling = node.getPreviousSibling()
let text = node.getTextContent() let text = node.getTextContent()
@@ -219,79 +192,56 @@ export const decoratorTransform = (
if (nextMatch === null) { if (nextMatch === null) {
nextSibling.markDirty() nextSibling.markDirty()
return return
} } else if (nextMatch.start !== 0) {
else if (nextMatch.start !== 0) {
return return
} }
} }
} } else {
else {
const nextMatch = getMatch(nextText) const nextMatch = getMatch(nextText)
if (nextMatch !== null && nextMatch.start === 0) if (nextMatch !== null && nextMatch.start === 0) return
return
} }
if (match === null) if (match === null) return
return
if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity()) continue
continue
let nodeToReplace let nodeToReplace
if (match.start === 0) if (match.start === 0) [nodeToReplace, currentNode] = currentNode.splitText(match.end)
[nodeToReplace, currentNode] = currentNode.splitText(match.end) else [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
else
[, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)
const replacementNode = createNode(nodeToReplace) const replacementNode = createNode(nodeToReplace)
nodeToReplace.replace(replacementNode) nodeToReplace.replace(replacementNode)
if (currentNode == null) if (currentNode == null) return
return
} }
} }
function getFullMatchOffset( function getFullMatchOffset(documentText: string, entryText: string, offset: number): number {
documentText: string,
entryText: string,
offset: number,
): number {
let triggerOffset = offset let triggerOffset = offset
for (let i = triggerOffset; i <= entryText.length; i++) { for (let i = triggerOffset; i <= entryText.length; i++) {
if (documentText.substr(-i) === entryText.substr(0, i)) if (documentText.substr(-i) === entryText.substr(0, i)) triggerOffset = i
triggerOffset = i
} }
return triggerOffset return triggerOffset
} }
export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null { export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null {
const selection = $getSelection() const selection = $getSelection()
if (!$isRangeSelection(selection) || !selection.isCollapsed()) if (!$isRangeSelection(selection) || !selection.isCollapsed()) return null
return null
const anchor = selection.anchor const anchor = selection.anchor
if (anchor.type !== 'text') if (anchor.type !== 'text') return null
return null
const anchorNode = anchor.getNode() const anchorNode = anchor.getNode()
if (!anchorNode.isSimpleText()) if (!anchorNode.isSimpleText()) return null
return null
const selectionOffset = anchor.offset const selectionOffset = anchor.offset
const textContent = anchorNode.getTextContent().slice(0, selectionOffset) const textContent = anchorNode.getTextContent().slice(0, selectionOffset)
const characterOffset = match.replaceableString.length const characterOffset = match.replaceableString.length
const queryOffset = getFullMatchOffset( const queryOffset = getFullMatchOffset(textContent, match.matchingString, characterOffset)
textContent,
match.matchingString,
characterOffset,
)
const startOffset = selectionOffset - queryOffset const startOffset = selectionOffset - queryOffset
if (startOffset < 0) if (startOffset < 0) return null
return null
let newNode let newNode
if (startOffset === 0) if (startOffset === 0) [newNode] = anchorNode.splitText(selectionOffset)
[newNode] = anchorNode.splitText(selectionOffset) else [, newNode] = anchorNode.splitText(startOffset, selectionOffset)
else
[, newNode] = anchorNode.splitText(startOffset, selectionOffset)
return newNode return newNode
} }
@@ -303,49 +253,57 @@ export function textToEditorState(text: string) {
root: { root: {
children: paragraph.map((p) => { children: paragraph.map((p) => {
return { return {
children: [{ children: [
detail: 0, {
format: 0, detail: 0,
mode: 'normal', format: 0,
style: '', mode: 'normal',
text: p, style: '',
type: 'custom-text', text: p,
version: 1, type: 'custom-text',
}], version: 1
}
],
direction: 'ltr', direction: 'ltr',
format: '', format: '',
indent: 0, indent: 0,
type: 'paragraph', type: 'paragraph',
version: 1, version: 1
} }
}), }),
direction: 'ltr', direction: 'ltr',
format: '', format: '',
indent: 0, indent: 0,
type: 'root', type: 'root',
version: 1, version: 1
}, }
}) })
} }
const varRegex = /\{\{(.+?)\}\}/g const varRegex = /\{\{(.+?)\}\}/g
export const getVars = (value: string) => { export const getVars = (value: string) => {
if (!value) if (!value) return []
return []
const keys = value.match(varRegex)?.filter((item) => { const keys =
return ![CONTEXT_PLACEHOLDER_TEXT, HISTORY_PLACEHOLDER_TEXT, QUERY_PLACEHOLDER_TEXT, PRE_PROMPT_PLACEHOLDER_TEXT].includes(item) value
}).map((item) => { .match(varRegex)
return item.replace('{{', '').replace('}}', '') ?.filter((item) => {
}).filter(key => key.length <= MAX_VAR_KEY_LENGTH) || [] return ![
CONTEXT_PLACEHOLDER_TEXT,
HISTORY_PLACEHOLDER_TEXT,
QUERY_PLACEHOLDER_TEXT,
PRE_PROMPT_PLACEHOLDER_TEXT
].includes(item)
})
.map((item) => {
return item.replace('{{', '').replace('}}', '')
})
.filter((key) => key.length <= MAX_VAR_KEY_LENGTH) || []
const keyObj: Record<string, boolean> = {} const keyObj: Record<string, boolean> = {}
// remove duplicate keys // remove duplicate keys
const res: string[] = [] const res: string[] = []
keys.forEach((key) => { keys.forEach((key) => {
if (keyObj[key]) if (keyObj[key]) return
return
keyObj[key] = true keyObj[key] = true
res.push(key) res.push(key)
@@ -1,14 +1,6 @@
import { $t } from "@common/locales" import { $t } from '@common/locales'
export type PARAM_TYPE = export type PARAM_TYPE = 'string' | 'float' | 'integer' | 'boolean' | 'date' | 'time' | 'datatime' | string
| 'string'
| 'float'
| 'integer'
| 'boolean'
| 'date'
| 'time'
| 'datatime'
| string
export type PARAM_KEY_REF_TYPE = { export type PARAM_KEY_REF_TYPE = {
key: string key: string
type: string type: string
@@ -17,7 +9,7 @@ export type PARAM_KEY_REF_TYPE = {
attribute?: string attribute?: string
description?: string description?: string
filter?: string filter?: string
arrayItemKey?:string arrayItemKey?: string
} }
export type PARAM_TYPE_REF_TYPE = { export type PARAM_TYPE_REF_TYPE = {
[key: string | number]: PARAM_TYPE [key: string | number]: PARAM_TYPE
@@ -1,8 +1,17 @@
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { parseFormData, parseFileValue, parseFileType, parseHeaders, parseRequestBodyToString, parseUri, payloadStr, goCodeParseFormData } from './transform' import {
parseFormData,
parseFileValue,
parseFileType,
parseHeaders,
parseRequestBodyToString,
parseUri,
payloadStr,
goCodeParseFormData
} from './transform'
// import { getJson } from '../.@common/utils/'; // import { getJson } from '../.@common/utils/';
import { ApiBodyType } from '@common/const/api-detail'; import { ApiBodyType } from '@common/const/api-detail'
import { $t } from '@common/locales'; import { $t } from '@common/locales'
function sameNameToParams(params: unknown) { function sameNameToParams(params: unknown) {
params = cloneDeep(params) params = cloneDeep(params)
@@ -54,9 +63,9 @@ function enrichParams(params: unknown) {
export function generateCode( export function generateCode(
type: string, type: string,
multipart: boolean, multipart: boolean,
{ protocol, URL, headers, params, method, requestType, apiRequestParamJsonType, raw }: unknown, { protocol, URL, headers, params, method, requestType, apiRequestParamJsonType, raw }: unknown
) { ) {
requestType=ApiBodyType[requestType] requestType = ApiBodyType[requestType]
let code: string = '' let code: string = ''
const indent = ' ' const indent = ' '
let urlObj: unknown = {} let urlObj: unknown = {}
@@ -243,9 +252,7 @@ export function generateCode(
`${indent}"headers": {\r\n` + `${indent}"headers": {\r\n` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`${indent}},\r\n` + `${indent}},\r\n` +
`${ `${multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''}` +
multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''
}` +
`${indent}"data": data,\r\n` + `${indent}"data": data,\r\n` +
`${indent}"crossDomain": true\r\n` + `${indent}"crossDomain": true\r\n` +
'})\r\n' + '})\r\n' +
@@ -334,14 +341,10 @@ export function generateCode(
'var requestInfo={\r\n' + 'var requestInfo={\r\n' +
`${indent}"method": "${method}",\r\n` + `${indent}"method": "${method}",\r\n` +
`${urlObj.hostname ? `${indent}"hostname": "${urlObj.hostname}",\r\n` : ''}` + `${urlObj.hostname ? `${indent}"hostname": "${urlObj.hostname}",\r\n` : ''}` +
`${ `${urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''}` +
urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''
}` +
`${ `${
urlObj.pathname || urlObj.search urlObj.pathname || urlObj.search
? `${indent}"path": "${urlObj.pathname || ''}${ ? `${indent}"path": "${urlObj.pathname || ''}${urlObj.search || ''}",\r\n`
urlObj.search || ''
}",\r\n`
: '' : ''
}` + }` +
`${indent}"headers": {\r\n` + `${indent}"headers": {\r\n` +
@@ -366,19 +369,11 @@ export function generateCode(
`var http = require("${urlObj.protocol.replace(':', '')}");\r\n` + `var http = require("${urlObj.protocol.replace(':', '')}");\r\n` +
'var requestInfo={\r\n' + 'var requestInfo={\r\n' +
`${indent}"method": "${method}",\r\n` + `${indent}"method": "${method}",\r\n` +
`${ `${urlObj.hostname ? `${indent}"hostname": "${urlObj.hostname}",\r\n` : ''}` +
urlObj.hostname `${urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''}` +
? `${indent}"hostname": "${urlObj.hostname}",\r\n`
: ''
}` +
`${
urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : ''
}` +
`${ `${
urlObj.pathname || urlObj.search urlObj.pathname || urlObj.search
? `${indent}"path": "${urlObj.pathname || ''}${ ? `${indent}"path": "${urlObj.pathname || ''}${urlObj.search || ''}",\r\n`
urlObj.search || ''
}",\r\n`
: '' : ''
}` + }` +
`${indent}"headers": {\r\n` + `${indent}"headers": {\r\n` +
@@ -439,11 +434,7 @@ export function generateCode(
} }
} }
code = code =
`${ `${(requestType || 'FORMDATA') === 'FORMDATA' && multipart ? 'var fs = require("fs");\r\n' : ''}` +
(requestType || 'FORMDATA') === 'FORMDATA' && multipart
? 'var fs = require("fs");\r\n'
: ''
}` +
'var request = require("request");\r\n' + 'var request = require("request");\r\n' +
'var requestInfo={\r\n' + 'var requestInfo={\r\n' +
` method: "${method}",\r\n` + ` method: "${method}",\r\n` +
@@ -472,7 +463,7 @@ export function generateCode(
params = sameNameToParams(params) params = sameNameToParams(params)
let tmpOutput: unknown = '' let tmpOutput: unknown = ''
params.map((val: unknown, key: number) => { params.map((val: unknown, key: number) => {
if(val.data_type === 'file') { if (val.data_type === 'file') {
tmpOutput += tmpOutput +=
` "${val.name}" =>array(\r\n` + ` "${val.name}" =>array(\r\n` +
` "type" => "${parseFileType(val.value)}",\r\n` + ` "type" => "${parseFileType(val.value)}",\r\n` +
@@ -481,11 +472,9 @@ export function generateCode(
` )${key === params.length - 1 ? '' : ','}\r\n` ` )${key === params.length - 1 ? '' : ','}\r\n`
} else { } else {
tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r` tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r`
} }
}) })
langTmp.paramsStr = langTmp.paramsStr = 'addForm(' + `${tmpOutput.length ? `array(\r\n${tmpOutput})` : ''}` + '\r\n);'
'addForm(' + `${tmpOutput.length ? `array(\r\n${tmpOutput})` : ''}` + '\r\n);'
langTmp.fileValue = parseFileValue(params) langTmp.fileValue = parseFileValue(params)
} else { } else {
langTmp.paramsStr = `append(new http\\QueryString(array({\r\n${parseFormData(params, { langTmp.paramsStr = `append(new http\\QueryString(array({\r\n${parseFormData(params, {
@@ -516,9 +505,7 @@ export function generateCode(
const tmpOutput: unknown = [] const tmpOutput: unknown = []
urlObj.searchParams.forEach((val: unknown, key: unknown) => { urlObj.searchParams.forEach((val: unknown, key: unknown) => {
tmpOutput.push(` ${JSON.stringify(key)} => ${JSON.stringify(val)}`) tmpOutput.push(` ${JSON.stringify(key)} => ${JSON.stringify(val)}`)
langTmp.queryStr = `$request->setQuery(new http\\QueryString(array(\r\n${tmpOutput.join( langTmp.queryStr = `$request->setQuery(new http\\QueryString(array(\r\n${tmpOutput.join(',\r\n')}\r\n)));`
',\r\n'
)}\r\n)));`
}) })
if (multipart) { if (multipart) {
code = code =
@@ -534,7 +521,7 @@ export function generateCode(
'$request->setBody($body);\r\n\r\n' + '$request->setBody($body);\r\n\r\n' +
`$request->getBody()->${langTmp.paramsStr}\r\n\r\n` + `$request->getBody()->${langTmp.paramsStr}\r\n\r\n` +
'$request->setHeaders(array(\r\n' + '$request->setHeaders(array(\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
' "Content-Type":"multipart/form-data"\r\n' + ' "Content-Type":"multipart/form-data"\r\n' +
'));\r\n\r\n' + '));\r\n\r\n' +
'$client->enqueue($request)->send();\r\n' + '$client->enqueue($request)->send();\r\n' +
@@ -552,7 +539,7 @@ export function generateCode(
'$request->setBody($body);\r\n\r\n' + '$request->setBody($body);\r\n\r\n' +
`${langTmp.queryStr ? `${langTmp.queryStr}\r\n\r\n` : ''}` + `${langTmp.queryStr ? `${langTmp.queryStr}\r\n\r\n` : ''}` +
'$request->setHeaders(array(\r\n' + '$request->setHeaders(array(\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
'));\r\n\r\n' + '));\r\n\r\n' +
'$client->enqueue($request)->send();\r\n' + '$client->enqueue($request)->send();\r\n' +
'$response = $client->getResponse();\r\n\r\n' + '$response = $client->getResponse();\r\n\r\n' +
@@ -572,9 +559,7 @@ export function generateCode(
let tmpOutput = '' let tmpOutput = ''
params.map((val: unknown, key: number) => { params.map((val: unknown, key: number) => {
if (val.data_type === 'file') { if (val.data_type === 'file') {
tmpOutput += ` "${val.name}" => new CURLFile($file_path)${ tmpOutput += ` "${val.name}" => new CURLFile($file_path)${key === params.length - 1 ? '' : ','}\r\n`
key === params.length - 1 ? '' : ','
}\r\n`
} else { } else {
tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r` tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r`
} }
@@ -614,7 +599,7 @@ export function generateCode(
` CURLOPT_CUSTOMREQUEST => "${method}",\r\n` + ` CURLOPT_CUSTOMREQUEST => "${method}",\r\n` +
` CURLOPT_POSTFIELDS => ${langTmp.paramsStr},\r\n` + ` CURLOPT_POSTFIELDS => ${langTmp.paramsStr},\r\n` +
' CURLOPT_HTTPHEADER => array(\r\n' + ' CURLOPT_HTTPHEADER => array(\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr},\r\n` : ''}` + `${langTmp.headerStr ? `${langTmp.headerStr},\r\n` : ''}` +
' "Content-Type:multipart/form-data"' + ' "Content-Type:multipart/form-data"' +
'\r\n ),\r\n' + '\r\n ),\r\n' +
'));\r\n\r\n' + '));\r\n\r\n' +
@@ -769,36 +754,33 @@ export function generateCode(
tmpOutput.push(`${JSON.stringify(key)} : ${JSON.stringify(val)}`) tmpOutput.push(`${JSON.stringify(key)} : ${JSON.stringify(val)}`)
// langTmp.querystring = `querystring={${tmpOutput.join(',')}};` // langTmp.querystring = `querystring={${tmpOutput.join(',')}};`
langTmp.querystring = `{${tmpOutput.join(',')}}` langTmp.querystring = `{${tmpOutput.join(',')}}`
}) })
if(multipart) { if (multipart) {
code = code =
'import requests \r\n\r\n' + 'import requests \r\n\r\n' +
'headers = {\r\n' + 'headers = {\r\n' +
`${langTmp.headerStr}\r\n` + `${langTmp.headerStr}\r\n` +
'}\r\n' + '}\r\n' +
`url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n` + `url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n` +
'//获取文件,需填路径 \r\n' + '//获取文件,需填路径 \r\n' +
"file_path = '' \r\n" + "file_path = '' \r\n" +
`filename = "${langTmp.fileValue}" \r\n` + `filename = "${langTmp.fileValue}" \r\n` +
`filetype = "${langTmp.fileType}" \r\n` + `filetype = "${langTmp.fileType}" \r\n` +
'data = { \r\n' + 'data = { \r\n' +
`${langTmp.paramsStr}\r\n` + `${langTmp.paramsStr}\r\n` +
'} \r\n' + '} \r\n' +
'files = {"file": (filename, open(file_path, "rb"), filetype)} \r\n' + 'files = {"file": (filename, open(file_path, "rb"), filetype)} \r\n' +
`response=requests.${method.toLowerCase()}(url, files=files, headers=headers, data=data)\r\n` + `response=requests.${method.toLowerCase()}(url, files=files, headers=headers, data=data)\r\n` +
'print(response.text)\r\n' 'print(response.text)\r\n'
} else { } else {
code = code =
'import requests\r\n\r\n' + 'import requests\r\n\r\n' +
`url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n\r\n` + `url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n\r\n` +
`payload = ${ `payload = ${langTmp.querystring ? langTmp.querystring : langTmp.paramsStr}\r\n\r\n` +
langTmp.querystring ? langTmp.querystring : langTmp.paramsStr `headers = {\r\n${langTmp.headerStr}` +
}\r\n\r\n` + '\r\n}\r\n\r\n' +
`headers = {\r\n${langTmp.headerStr}` + `response=requests.request("${method}", url, ${payloadStr(method, headers)}, headers=headers)\r\n\r\n` +
'\r\n}\r\n\r\n' + 'print(response.text)\r\n'
`response=requests.request("${method}", url, ${payloadStr(method, headers)}, headers=headers)\r\n\r\n` +
'print(response.text)\r\n'
} }
break break
} }
@@ -815,9 +797,7 @@ export function generateCode(
let tmpOutput = '' let tmpOutput = ''
params.map((val: unknown, key: number) => { params.map((val: unknown, key: number) => {
if (val.data_type === 'file') { if (val.data_type === 'file') {
tmpOutput += ` "${val.name}" => new CURLFile($file_path)${ tmpOutput += ` "${val.name}" => new CURLFile($file_path)${key === params.length - 1 ? '' : ','}\r\n`
key === params.length - 1 ? '' : ','
}\r\n`
} else { } else {
tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r` tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r`
} }
@@ -847,7 +827,7 @@ export function generateCode(
`request = Net::HTTP::${method.toLowerCase().replace(/^\S/, (s: string) => { `request = Net::HTTP::${method.toLowerCase().replace(/^\S/, (s: string) => {
return s?.toUpperCase() return s?.toUpperCase()
})}.new(url)\r\n` + })}.new(url)\r\n` +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
`request.body = ${langTmp.paramsStr}\r\n\r\n` + `request.body = ${langTmp.paramsStr}\r\n\r\n` +
'response = http.request(request)\r\n' + 'response = http.request(request)\r\n' +
'puts response.read_body' 'puts response.read_body'
@@ -883,25 +863,19 @@ export function generateCode(
} }
} }
code = code =
`${ `${requestType === 'JSON' ? `printf '${langTmp.paramsStr}'|` : ''} http ${
requestType === 'JSON' ? `printf '${langTmp.paramsStr}'|` : '' (requestType || 'FORMDATA').toString() === 'FORMDATA' && !multipart
} http ${
(requestType || 'FORMDATA').toString() === 'FORMDATA' &&
!multipart
? '--form' ? '--form'
: (requestType || 'JSON').toString() === 'JSON' && : (requestType || 'JSON').toString() === 'JSON' && !multipart
!multipart ? '--follow'
? '--follow' : ''
: ''
} ${multipart ? '--ignore-stdin --form --follow' : ''} ${method} '${ } ${multipart ? '--ignore-stdin --form --follow' : ''} ${method} '${
urlObj.href urlObj.href
}' ${multipart ? '\\\r' : '\\'}` + }' ${multipart ? '\\\r' : '\\'}` +
`${multipart ? langTmp.paramsStr : ''}\r` + `${multipart ? langTmp.paramsStr : ''}\r` +
`${langTmp.headerStr}` + `${langTmp.headerStr}` +
`${ `${
(requestType || 'FORMDATA').toString() === 'FORMDATA' && (requestType || 'FORMDATA').toString() === 'FORMDATA' && !multipart && langTmp.paramsStr
!multipart &&
langTmp.paramsStr
? ` \\\r\n${langTmp.paramsStr}` ? ` \\\r\n${langTmp.paramsStr}`
: '' : ''
}` }`
@@ -923,7 +897,6 @@ export function generateCode(
} }
}) })
langTmp.paramsStr = tmpOutput langTmp.paramsStr = tmpOutput
} else { } else {
const tmpOutput = parseFormData(params, { const tmpOutput = parseFormData(params, {
format: '${name}=${value}', format: '${name}=${value}',
@@ -1024,7 +997,7 @@ export function generateCode(
' if err != nil {\r\n' + ' if err != nil {\r\n' +
' return nil, err\r\n' + ' return nil, err\r\n' +
' }\r\n' + ' }\r\n' +
`${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` +
' req.Header.Add("Content-Type", writer.FormDataContentType())\r\n\r\n' + ' req.Header.Add("Content-Type", writer.FormDataContentType())\r\n\r\n' +
' client := &http.Client{}\r\n' + ' client := &http.Client{}\r\n' +
' resp, err := client.Do(req)\r\n' + ' resp, err := client.Do(req)\r\n' +
@@ -1071,7 +1044,7 @@ export function generateCode(
} }
default: { default: {
langTmp.paramsStr = requestParam ? JSON.stringify(requestParam) : '' langTmp.paramsStr = requestParam ? JSON.stringify(requestParam) : ''
code = code =
'package main\r\n\r\n' + 'package main\r\n\r\n' +
'import (\r\n' + 'import (\r\n' +
' "bytes"\r\n' + ' "bytes"\r\n' +
@@ -1091,7 +1064,7 @@ export function generateCode(
'func request() ([]byte, error) {\r\n' + 'func request() ([]byte, error) {\r\n' +
` uri := "${urlObj.href}"\r\n\r\n` + ` uri := "${urlObj.href}"\r\n\r\n` +
` ${ ` ${
langTmp.paramsStr langTmp.paramsStr
? ` payload := map[string]interface{}${langTmp.paramsStr}` ? ` payload := map[string]interface{}${langTmp.paramsStr}`
: ' payload := strings.NewReader("")' : ' payload := strings.NewReader("")'
}\r\n\r\n` + }\r\n\r\n` +
@@ -1116,7 +1089,7 @@ export function generateCode(
map: stringifyHeaders map: stringifyHeaders
}) })
let mediaType = 'application/octet-stream' let mediaType = 'application/octet-stream'
switch ( (requestType || 'FORMDATA').toUpperCase()) { switch ((requestType || 'FORMDATA').toUpperCase()) {
case 'FORMDATA': { case 'FORMDATA': {
if (multipart) { if (multipart) {
mediaType = 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' mediaType = 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW'
@@ -1154,8 +1127,8 @@ export function generateCode(
break break
} }
} }
if(multipart) { if (multipart) {
code = code =
'OkHttpClient client = new OkHttpClient();\r\n\r\n' + 'OkHttpClient client = new OkHttpClient();\r\n\r\n' +
'//获取文件,需填路径 \r\n' + '//获取文件,需填路径 \r\n' +
'File file = new File(""); \r\n\r\n' + 'File file = new File(""); \r\n\r\n' +
@@ -1175,11 +1148,7 @@ export function generateCode(
code = code =
'OkHttpClient client = new OkHttpClient().newBuilder().build();\r\n' + 'OkHttpClient client = new OkHttpClient().newBuilder().build();\r\n' +
`MediaType mediaType = MediaType.parse("${mediaType}");\r\n` + `MediaType mediaType = MediaType.parse("${mediaType}");\r\n` +
`${ `${method === 'GET' ? '' : `RequestBody body = RequestBody.create(mediaType, ${langTmp.paramsStr});\r\n`}` +
method === 'GET'
? ''
: `RequestBody body = RequestBody.create(mediaType, ${langTmp.paramsStr});\r\n`
}` +
'Request request = new Request.Builder()\r\n' + 'Request request = new Request.Builder()\r\n' +
` .url("${urlObj.href}")\r\n` + ` .url("${urlObj.href}")\r\n` +
` .method("${method}",${method === 'GET' ? 'null' : 'body'})\r\n` + ` .method("${method}",${method === 'GET' ? 'null' : 'body'})\r\n` +
@@ -1188,7 +1157,7 @@ export function generateCode(
'Response response = client.newCall(request).execute();\r\n' + 'Response response = client.newCall(request).execute();\r\n' +
'System.out.println(response.body().string());\r\n' 'System.out.println(response.body().string());\r\n'
} }
break break
} }
@@ -1233,9 +1202,7 @@ export function generateCode(
`${indent}"header": {\r\n` + `${indent}"header": {\r\n` +
`${langTmp.headerStr}\r\n` + `${langTmp.headerStr}\r\n` +
`${indent}},\r\n` + `${indent}},\r\n` +
`${ `${multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''}` +
multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : ''
}` +
`${langTmp.paramsStr ? `${indent}"data": data,\r\n` : ''}` + `${langTmp.paramsStr ? `${indent}"data": data,\r\n` : ''}` +
`${indent}"success": (response)=> {\r\n` + `${indent}"success": (response)=> {\r\n` +
`${indent + indent}console.log(response.data)\r\n` + `${indent + indent}console.log(response.data)\r\n` +
@@ -1,218 +1,228 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react'
import { Cascader } from 'antd'; import { Cascader } from 'antd'
import CODE_LANG from '@common/const/code/const'; import CODE_LANG from '@common/const/code/const'
import type { DefaultOptionType } from 'antd/es/cascader'; import type { DefaultOptionType } from 'antd/es/cascader'
import { useState } from 'react'; import { useState } from 'react'
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es'
import { paramsJsonType } from './code-snippets.type'; import { paramsJsonType } from './code-snippets.type'
import { DOMAIN_SUFIX } from './code-example.type'; import { DOMAIN_SUFIX } from './code-example.type'
import { transfromUrlParam } from './transform'; import { transfromUrlParam } from './transform'
import { generateCode } from './generate-code'; import { generateCode } from './generate-code'
import {ApiDetail} from "@common/const/api-detail"; import { ApiDetail } from '@common/const/api-detail'
import {Codebox} from "@common/components/postcat//api/Codebox"; import { Codebox } from '@common/components/postcat//api/Codebox'
import {Collapse} from "@common/components/postcat/api/Collapse"; import { Collapse } from '@common/components/postcat/api/Collapse'
import {Box} from "@mui/material"; import { Box } from '@mui/material'
import { $t } from '@common/locales'; import { $t } from '@common/locales'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'; import { useGlobalContext } from '@common/contexts/GlobalStateContext'
type CodeSnippetCompoType = { type CodeSnippetCompoType = {
title:string title: string
api:ApiDetail, api: ApiDetail
extraTitle:unknown, extraTitle: unknown
extraContent:unknown, extraContent: unknown
minLines:number minLines: number
} }
const file: unknown[] = [] const file: unknown[] = []
const env: unknown = {} const env: unknown = {}
const loading: boolean = false const loading: boolean = false
const codeMode: string = 'rust' const codeMode: string = 'rust'
const codeMens: string[] = ['reset', 'copy', 'download', 'newTab', 'search'] const codeMens: string[] = ['reset', 'copy', 'download', 'newTab', 'search']
const DOMAIN_REGEX: RegExp = new RegExp( const DOMAIN_REGEX: RegExp = new RegExp(
`^(((http|ftp|https):\/\/)|)(([\\\w\\\-_]+([\\\w\\\-\\\.]*)?(\\\.(${DOMAIN_SUFIX.join( `^(((http|ftp|https):\/\/)|)(([\\\w\\\-_]+([\\\w\\\-\\\.]*)?(\\\.(${DOMAIN_SUFIX.join(
'|' '|'
)})))|((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(localhost))((\\\/)|(\\\?)|(:)|($))` )})))|((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(localhost))((\\\/)|(\\\?)|(:)|($))`
) )
let isMultipart: boolean = false let isMultipart: boolean = false
export default function CodeSnippetCompo({title,api, extraTitle, extraContent, minLines=15}: CodeSnippetCompoType) { export default function CodeSnippetCompo({
const {state } = useGlobalContext() title,
// const [tokenState ] = useTokenBasicInfo() api,
const pretreatmentRequestInfo = (apiDoc: ApiDetail) =>{ extraTitle,
isMultipart = false extraContent,
const result: ApiDetail = cloneDeep(apiDoc) minLines = 15
const files: unknown = file || [] }: CodeSnippetCompoType) {
let isMuti: boolean = false const { state } = useGlobalContext()
const headers: string[] = [] // const [tokenState ] = useTokenBasicInfo()
let alreadyHadContentType: boolean = false const pretreatmentRequestInfo = (apiDoc: ApiDetail) => {
result.headers = [] isMultipart = false
const originHeader = apiDoc.requestParams?.headerParams const result: ApiDetail = cloneDeep(apiDoc)
//处理请求头部 const files: unknown = file || []
originHeader?.forEach((header: unknown) => { let isMuti: boolean = false
// if ( const headers: string[] = []
// tokenState?.selected_X_apibee_token && let alreadyHadContentType: boolean = false
// originHeader.length && result.headers = []
// originHeader[0].name == 'X-APISpace-Token' const originHeader = apiDoc.requestParams?.headerParams
// ) { //处理请求头部
// originHeader[0].value = tokenState?.selected_X_apibee_token originHeader?.forEach((header: unknown) => {
// } // if (
const { checkbox, name } = header // tokenState?.selected_X_apibee_token &&
if ((checkbox || !header.hasOwnProperty('checkbox')) && name) { // originHeader.length &&
headers.push(name?.toLowerCase()) // originHeader[0].name == 'X-APISpace-Token'
result.headers.push(header) // ) {
if (/content-type/i.test(name)) { // originHeader[0].value = tokenState?.selected_X_apibee_token
alreadyHadContentType = true // }
} const { checkbox, name } = header
if ((checkbox || !header.hasOwnProperty('checkbox')) && name) {
headers.push(name?.toLowerCase())
result.headers.push(header)
if (/content-type/i.test(name)) {
alreadyHadContentType = true
} }
}) }
const query: unknown = {} })
const query: unknown = {}
apiDoc.requestParams?.queryParams?.forEach((query: unknown) => { apiDoc.requestParams?.queryParams?.forEach((query: unknown) => {
const { checkbox, name } = query const { checkbox, name } = query
if ((checkbox || !query.hasOwnProperty('checkbox')) && name) { if ((checkbox || !query.hasOwnProperty('checkbox')) && name) {
query[name] = query?.paramAttr.example || '' query[name] = query?.paramAttr.example || ''
}
})
result.URL = transfromUrlParam(result.uri, query)
//处理 restful 参数
apiDoc.requestParams?.restParams?.forEach((rest: unknown) => {
if ((rest.checkbox || !rest.hasOwnProperty('checkbox')) && rest.name && rest.paramAttr.example) {
if (eval(`/:${rest.name}/`).test(result.URL.trim())) {
result.URL = result.URL.replaceAll(`:${rest.name}`, rest.paramAttr.example)
} else if (
result.URL.trim().indexOf(`{{${rest.name}}}`) == -1 &&
result.URL.trim().indexOf(`{${rest.name}}`) > -1
) {
result.URL = result.URL.replaceAll(`{${rest.name}}`, rest.paramAttr.example)
} }
}) }
result.URL = transfromUrlParam(result.uri, query) })
//处理 restful 参数 result.params = []
apiDoc.requestParams?.restParams?.forEach((rest: unknown) => { //为请求参数 中的header、reset、body、query 添加 value 和 valueQuery 的值
if ((rest.checkbox || !rest.hasOwnProperty('checkbox')) && rest.name && rest.paramAttr.example) { switch (result.requestParams?.bodyParams?.[0]?.contentType) {
if (eval(`/:${rest.name}/`).test(result.URL.trim())) { case 0: {
result.URL = result.URL.replaceAll(`:${rest.name}`, rest.paramAttr.example ) result.requestParams?.bodyParams?.forEach((body: unknown, key: unknown) => {
} else if ( if ((body.checkbox || !body.hasOwnProperty('checkbox')) && body.name) {
result.URL.trim().indexOf(`{{${rest.name}}}`) == -1 && if (paramsJsonType[body.dataType] == 'string' && body.paramAttr.example) {
result.URL.trim().indexOf(`{${rest.name}}`) > -1 isMuti = true
) { body.files = files[key] || []
result.URL = result.URL.replaceAll(`{${rest.name}}`, rest.paramAttr.example)
}
}
})
result.params = []
//为请求参数 中的header、reset、body、query 添加 value 和 valueQuery 的值
switch (result.requestParams?.bodyParams?.[0]?.contentType) {
case 0: {
result.requestParams?.bodyParams?.forEach((body: unknown, key: unknown) => {
if ((body.checkbox || !body.hasOwnProperty('checkbox')) && body.name) {
if (paramsJsonType[body.dataType] == 'string' && body.paramAttr.example) {
isMuti = true
body.files = files[key] || []
}
result.params.push(body)
} }
result.params.push(body)
}
})
if (!alreadyHadContentType) {
if (isMuti) {
isMultipart = true
result.headers.push({
name: 'Content-Type',
value: 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
checkbox: true
})
} else {
result.headers.push({
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
checkbox: true
})
}
}
break
}
case 1: {
result.params = apiDoc.requestParams?.bodyParams
break
}
case 2: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/json',
checkbox: true
}) })
if (!alreadyHadContentType) {
if (isMuti) {
isMultipart = true
result.headers.push({
name: 'Content-Type',
value: 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW',
checkbox: true
})
} else {
result.headers.push({
name: 'Content-Type',
value: 'application/x-www-form-urlencoded',
checkbox: true
})
}
}
break
}
case 1: {
result.params = apiDoc.requestParams?.bodyParams
break
}
case 2: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/json',
checkbox: true
})
}
break
}
case 3: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/xml',
checkbox: true
})
}
break
} }
break
}
case 3: {
result.params = apiDoc.requestParams?.bodyParams
if (!alreadyHadContentType) {
result.headers.push({
name: 'Content-Type',
value: 'application/xml',
checkbox: true
})
}
break
} }
result.requestType = result.requestParams?.bodyParams?.[0]?.contentType || 0
return result
} }
const [code, setCode] = useState<string>('') result.requestType = result.requestParams?.bodyParams?.[0]?.contentType || 0
const [lang, setLang] = useState<number[]>([20]) return result
}
let tempCode = '' const [code, setCode] = useState<string>('')
const getCode = (language: number | string) => { const [lang, setLang] = useState<number[]>([20])
if (!['HTTPS', 'HTTP'].includes(api.protocol?.toUpperCase())) {
tempCode = $t('暂不支持生成非 HTTPS 或非 HTTP 协议的代码示例') let tempCode = ''
setCode(tempCode) const getCode = (language: number | string) => {
return if (!['HTTPS', 'HTTP'].includes(api.protocol?.toUpperCase())) {
} tempCode = $t('暂不支持生成非 HTTPS 或非 HTTP 协议的代码示例')
tempCode = generateCode(
language.toString(),
isMultipart,
pretreatmentRequestInfo(cloneDeep(api))
)
setCode(tempCode) setCode(tempCode)
return
} }
tempCode = generateCode(language.toString(), isMultipart, pretreatmentRequestInfo(cloneDeep(api)))
setCode(tempCode)
}
useEffect(() => { useEffect(() => {
if(!Object.keys(api).length) return if (!Object.keys(api).length) return
getCode(lang[lang.length -1 ]) getCode(lang[lang.length - 1])
}, [api]) }, [api])
const onChange = (value: number[], record: DefaultOptionType[]) => {
const num = value[value.length - 1]
setLang(value)
if (!Object.keys(api).length) return
getCode(num)
}
const onChange = (value: number[],record:DefaultOptionType[]) => { const filter = (inputValue: string, path: DefaultOptionType[]) =>
const num = value[value.length - 1] path.some((option) => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1)
setLang(value)
if(!Object.keys(api).length) return
getCode(num)
};
const filter = (inputValue: string, path: DefaultOptionType[]) => const [placeholderTxt, setPlaceholderTxt] = useState($t('搜索编程语言...'))
path.some( const [selectItemTxt, setSelectItemTxt] = useState('')
(option) => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1,
);
const [placeholderTxt, setPlaceholderTxt] = useState($t('搜索编程语言...')) const codeLangOptions = useMemo(
const [selectItemTxt, setSelectItemTxt ] = useState('') () => CODE_LANG.map((x) => ({ ...x, label: $t(x.label as string) })),
[state.language]
const codeLangOptions = useMemo(()=>CODE_LANG.map(x=>({...x, label:$t(x.label as string)})),[state.language]) )
return ( return (
<Collapse title={title}>
<Collapse title={title}> <Box width="100%">
<Box width="100%"> <>
<> <Codebox
<Codebox extraContent={<><span className="ml-[12px]">{$t('编程语言')}</span><Cascader extraContent={
<>
<span className="ml-[12px]">{$t('编程语言')}</span>
<Cascader
options={codeLangOptions} options={codeLangOptions}
onChange={(value,record) => onChange(value as unknown as number[],record)} onChange={(value, record) => onChange(value as unknown as number[], record)}
placeholder={placeholderTxt} placeholder={placeholderTxt}
value={lang} // 当前的值 value={lang} // 当前的值
showSearch={{ filter }} showSearch={{ filter }}
size="small" size="small"
allowClear={false} allowClear={false}
// onDropdownVisibleChange={value => openChange(value)} // onDropdownVisibleChange={value => openChange(value)}
/></>} />
language={'javascript'} value={code} readOnly={true} height={'250px'} width={'100%'}/> </>
</> }
</Box> language={'javascript'}
</Collapse> value={code}
readOnly={true}
) height={'250px'}
width={'100%'}
} />
</>
</Box>
</Collapse>
)
}
@@ -1,6 +1,6 @@
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
import { PARAM_KEY_REF_TYPE, PARAM_TYPE_REF_TYPE } from './code-snippets.type'; import { PARAM_KEY_REF_TYPE, PARAM_TYPE_REF_TYPE } from './code-snippets.type'
import { tranformJson, tranformXml } from './util'; import { tranformJson, tranformXml } from './util'
type LANG_TYPE = 'Java' | 'HTTP' | 'shellHttpie' | 'go' | 'NodeJSNative' type LANG_TYPE = 'Java' | 'HTTP' | 'shellHttpie' | 'go' | 'NodeJSNative'
type PARAM_HEADER_TYPE = { name: string; value: string } type PARAM_HEADER_TYPE = { name: string; value: string }
type PARSE_OPTS_TYPE = { type PARSE_OPTS_TYPE = {
@@ -30,201 +30,199 @@ export const paramsJsonType: unknown = {
NULL: 'null' NULL: 'null'
} }
/**
/** * @description query参数url
* @description query参数url * @param {string} url url
* @param {string} url url * @param {Object} query query对象
* @param {Object} query query对象 */
*/ export function transfromUrlParam(url: string, query: { [key: string]: string }) {
export function transfromUrlParam(url: string, query: { [key: string]: string }) { const querys: string[] = []
const querys: string[] = [] Object.entries(query).forEach((item: string[]) => {
Object.entries(query).forEach((item: string[]) => { querys.push(`${item[0]}=${item[1]}`)
querys.push(`${item[0]}=${item[1]}`) })
}) if (!querys.length) return url
if (!querys.length) return url return `${url}${url.includes('?') ? '&' : '?'}${querys.join('&')}`
return `${url}${url.includes('?') ? '&' : '?'}${querys.join('&')}` }
} export function parseUri(protocol: string, url: string) {
export function parseUri(protocol: string, url: string) { if (!/((http:\/\/)|(https:\/\/))/.test(url)) {
if (!/((http:\/\/)|(https:\/\/))/.test(url)) { url = (protocol == 'HTTPS' ? 'https://' : 'http://') + url
url = (protocol == 'HTTPS' ? 'https://' : 'http://') + url
}
return url
} }
return url
}
/** /**
* @description FormData格式请求参数 * @description FormData格式请求参数
* @param {Object} options {format:生成Formdata格式[option]separator:组合字符串的分割符[option]} * @param {Object} options {format:生成Formdata格式[option]separator:组合字符串的分割符[option]}
* @param {Array} params * @param {Array} params
*/ */
export function parseFormData(params: unknown, { map, init, format, separator, langType, hasFileParams }: PARSE_OPTS_TYPE = {}) { export function parseFormData(
if (map) params = cloneDeep(params) params: unknown,
if (init) params = init(params) { map, init, format, separator, langType, hasFileParams }: PARSE_OPTS_TYPE = {}
if (format) { ) {
//x-www if (map) params = cloneDeep(params)
const result: unknown = [] if (init) params = init(params)
params.map((val: unknown) => { if (format) {
if (map) val = map(val) //x-www
result.push(format.replace('${name}', val.name).replace('${value}', val.value)) const result: unknown = []
}) params.map((val: unknown) => {
return result.join(separator || '&')
}
//multipart
let result: string = ''
const boundary: string = 'WebKitFormBoundary7MA4YWxkTrZu0gW'
params.forEach((val: unknown) => {
if (map) val = map(val) if (map) val = map(val)
if (val.files) { result.push(format.replace('${name}', val.name).replace('${value}', val.value))
if (val.files.length) {
result += `------${boundary}\r\n`
val.files.map((childVal: unknown) => {
result += `content-disposition: form-data; name="${val.name}"; filename="${val.value}"\r\n`
if (typeof childVal === 'string') {
result += `Content-Type: ${((childVal.match(/data:(.*);/) || [])[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
} else {
result += `Content-Type: ${(childVal.dataUrl.match(/data:(.*);/)[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
}
result += '\r\n'
})
}
} else {
result += `------${boundary}\r\n`
result += `content-disposition: form-data; name="${val.name}"\r\n`
result += '\r\n'
result += `${val.value}\r\n`
}
}) })
result += `------${boundary}--` return result.join(separator || '&')
return result
} }
export function parseFileValue (params: unknown[]) { //multipart
const tmp = { let result: string = ''
output: '' const boundary: string = 'WebKitFormBoundary7MA4YWxkTrZu0gW'
} params.forEach((val: unknown) => {
if (params && params.length) { if (map) val = map(val)
for (let i = 0; i < params.length; i++) { if (val.files) {
if (params[i].data_type === 'file') { if (val.files.length) {
tmp.output = params[i].value || '' result += `------${boundary}\r\n`
break val.files.map((childVal: unknown) => {
} result += `content-disposition: form-data; name="${val.name}"; filename="${val.value}"\r\n`
if (typeof childVal === 'string') {
result += `Content-Type: ${((childVal.match(/data:(.*);/) || [])[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
} else {
result += `Content-Type: ${(childVal.dataUrl.match(/data:(.*);/)[0] || '')
.replace(/^data:/, '')
.replace(/;$/, '')}\r\n`
}
result += '\r\n'
})
} }
return tmp.output
}
}
export function payloadStr (method: string, headers: unknown[]) {
let tmpStr = ''
if (method === 'GET') {
tmpStr = 'params=payload'
} else { } else {
headers.forEach((item) => { result += `------${boundary}\r\n`
if (item.name === 'Content-Type') { result += `content-disposition: form-data; name="${val.name}"\r\n`
switch (item.description || item.value) { result += '\r\n'
case 'application/x-www-form-urlencoded': result += `${val.value}\r\n`
tmpStr = 'data=payload' }
break })
case 'application/json': result += `------${boundary}--`
tmpStr = 'data=json.dumps(payload)' return result
break }
default: { export function parseFileValue(params: unknown[]) {
tmpStr = 'params=payload' const tmp = {
} output: ''
}
if (params && params.length) {
for (let i = 0; i < params.length; i++) {
if (params[i].data_type === 'file') {
tmp.output = params[i].value || ''
break
}
}
return tmp.output
}
}
export function payloadStr(method: string, headers: unknown[]) {
let tmpStr = ''
if (method === 'GET') {
tmpStr = 'params=payload'
} else {
headers.forEach((item) => {
if (item.name === 'Content-Type') {
switch (item.description || item.value) {
case 'application/x-www-form-urlencoded':
tmpStr = 'data=payload'
break
case 'application/json':
tmpStr = 'data=json.dumps(payload)'
break
default: {
tmpStr = 'params=payload'
} }
} }
})
}
return tmpStr
}
export function goCodeParseFormData(params: unknown[]) {
if (!params.length) return
let output = ''
params.forEach((item) => {
output += `payload.Set("${item.name}", "${item.value}")\r\n `
})
return output
}
export function parseFileType (fileValue: string) {
const isPng = fileValue.endsWith('.png')
const isJpeg = fileValue.endsWith('.jpeg')
const isJpg = fileValue.endsWith('.jpg')
let result = 'image/jpeg'
if (isPng) {
result = 'image/png'
} else if (isJpg) {
result = 'image/jpeg'
} else if (isJpeg) {
result = 'image/jpeg'
}
return result
}
/**
* @description
* @param {Object} options {format:生成请求头格式[option]separator:组合字符串的分割符[option]}
* @param {Array} headers
*/
export function parseHeaders(
headers: PARAM_HEADER_TYPE[],
{ map, filter, format, separator }: PARSE_OPTS_TYPE = {}
) {
const result = []
if (map) {
headers = cloneDeep(headers)
}
for (const key in headers) {
let val = headers[key]
if (map) {
val = map(val)
} }
if (filter) { })
if (filter(val)) { }
result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value)) return tmpStr
} }
} else {
export function goCodeParseFormData(params: unknown[]) {
if (!params.length) return
let output = ''
params.forEach((item) => {
output += `payload.Set("${item.name}", "${item.value}")\r\n `
})
return output
}
export function parseFileType(fileValue: string) {
const isPng = fileValue.endsWith('.png')
const isJpeg = fileValue.endsWith('.jpeg')
const isJpg = fileValue.endsWith('.jpg')
let result = 'image/jpeg'
if (isPng) {
result = 'image/png'
} else if (isJpg) {
result = 'image/jpeg'
} else if (isJpeg) {
result = 'image/jpeg'
}
return result
}
/**
* @description
* @param {Object} options {format:生成请求头格式[option]separator:组合字符串的分割符[option]}
* @param {Array} headers
*/
export function parseHeaders(headers: PARAM_HEADER_TYPE[], { map, filter, format, separator }: PARSE_OPTS_TYPE = {}) {
const result = []
if (map) {
headers = cloneDeep(headers)
}
for (const key in headers) {
let val = headers[key]
if (map) {
val = map(val)
}
if (filter) {
if (filter(val)) {
result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value)) result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value))
} }
} else {
result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value))
} }
return result.join(separator || '')
}
const keyRefs: PARAM_KEY_REF_TYPE = {
key: 'name',
type: 'data_type',
value: 'value',
childKey: 'child_list',
arrayItemKey: 'isArrItem'
} }
return result.join(separator || '')
}
const keyRefs: PARAM_KEY_REF_TYPE = {
key: 'name',
type: 'data_type',
value: 'value',
childKey: 'child_list',
arrayItemKey: 'isArrItem'
}
const typeRefs: PARAM_TYPE_REF_TYPE = paramsJsonType const typeRefs: PARAM_TYPE_REF_TYPE = paramsJsonType
export const parseRequestBodyToString = ({ requestType, params, apiRequestParamJsonType, raw }: unknown) => { export const parseRequestBodyToString = ({ requestType, params, apiRequestParamJsonType, raw }: unknown) => {
let result: string = '' let result: string = ''
switch ((requestType || 'FORAMDATA').toString()) { switch ((requestType || 'FORAMDATA').toString()) {
case 'RAW': { case 'RAW': {
//raw //raw
// todo // todo
result = raw result = raw
break break
}
case 'JSON': {
//json
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformJson(params, keyRefs, typeRefs)
if (apiRequestParamJsonType === 'ARRAY') {
//array
result = `[${result}]`
}
break
}
case 'XML': {
//xml
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformXml(params, keyRefs, typeRefs)
break
}
} }
return result case 'JSON': {
} //json
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformJson(params, keyRefs, typeRefs)
if (apiRequestParamJsonType === 'ARRAY') {
//array
result = `[${result}]`
}
break
}
case 'XML': {
//xml
if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name'
result = tranformXml(params, keyRefs, typeRefs)
break
}
}
return result
}
@@ -1,19 +1,25 @@
import {Random} from 'mockjs' import { Random } from 'mockjs'
import { PARAM_KEY_REF_TYPE, PARAM_LIST_TYPE, PARAM_LIS_ITEM_TYPE, PARAM_TYPE, PARAM_TYPE_REF_TYPE } from "./code-snippets.type" import {
PARAM_KEY_REF_TYPE,
PARAM_LIST_TYPE,
PARAM_LIS_ITEM_TYPE,
PARAM_TYPE,
PARAM_TYPE_REF_TYPE
} from './code-snippets.type'
const DEFAULT_PARAM_KEY_REF: PARAM_KEY_REF_TYPE = { const DEFAULT_PARAM_KEY_REF: PARAM_KEY_REF_TYPE = {
key: 'key', key: 'key',
type: 'type', type: 'type',
value: 'value' value: 'value'
} }
/** /**
* xml * xml
* @param list * @param list
* @param keyRefs * @param keyRefs
* @param random * @param random
* @returns xml * @returns xml
*/ */
export function tranformXml( export function tranformXml(
list: PARAM_LIST_TYPE, list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {}, typeRefs: PARAM_TYPE_REF_TYPE = {},
@@ -74,40 +80,40 @@ export function tranformJson(
return `{${result.join(',')}}` return `{${result.join(',')}}`
} }
/** /**
* *
* @param list * @param list
* @param keyRefs * @param keyRefs
* @param random * @param random
* @returns key-value * @returns key-value
*/ */
export function tranformUrlParam( export function tranformUrlParam(
list: PARAM_LIST_TYPE, list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {}, typeRefs: PARAM_TYPE_REF_TYPE = {},
random: boolean = false random: boolean = false
) { ) {
const { key, value, filter, type } = keyRefs const { key, value, filter, type } = keyRefs
const result: string[] = [] const result: string[] = []
list.forEach((item: unknown) => { list.forEach((item: unknown) => {
if (filter && item[filter]) return if (filter && item[filter]) return
const tab: string = item[key] const tab: string = item[key]
if (!tab) return if (!tab) return
const itemType: PARAM_TYPE = typeRefs[item[type]] const itemType: PARAM_TYPE = typeRefs[item[type]]
const text: string = random === true ? getRandomDataByType(itemType) : item[value] const text: string = random === true ? getRandomDataByType(itemType) : item[value]
result.push(`${tab}=${text}`) result.push(`${tab}=${text}`)
}) })
return result.join('&') //分隔符为& return result.join('&') //分隔符为&
} }
/** /**
* key-value * key-value
* @param list * @param list
* @param keyRefs * @param keyRefs
* @param random * @param random
* @returns * @returns
*/ */
export function tranformKeyValue( export function tranformKeyValue(
list: PARAM_LIST_TYPE, list: PARAM_LIST_TYPE,
keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF,
typeRefs: PARAM_TYPE_REF_TYPE = {}, typeRefs: PARAM_TYPE_REF_TYPE = {},
@@ -1,112 +1,147 @@
import {useState, useEffect, useImperativeHandle} from 'react'; import { useState, useEffect, useImperativeHandle } from 'react'
import {AutoComplete, Empty, Tabs} from 'antd'; import { AutoComplete, Empty, Tabs } from 'antd'
import { ResultListType} from "@common/const/api-detail"; import { ResultListType } from '@common/const/api-detail'
import {Collapse} from "@common/components/postcat/api/Collapse"; import { Collapse } from '@common/components/postcat/api/Collapse'
import {Box} from "@mui/material"; import { Box } from '@mui/material'
import {Codebox} from "@common/components/postcat/api/Codebox"; import { Codebox } from '@common/components/postcat/api/Codebox'
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es'
import { $t } from '@common/locales'; import { $t } from '@common/locales'
export interface ResponseExampleCompoEditorApi { export interface ResponseExampleCompoEditorApi {
getData: () => ResultListType[] | [] getData: () => ResultListType[] | []
} }
const DEFAULT_RESULT_LIST = [ const DEFAULT_RESULT_LIST = [
{id:'success',name:$t('成功示例'),httpCode:'200',content:''}, { id: 'success', name: $t('成功示例'), httpCode: '200', content: '' },
{id:'failed',name:$t('失败示例'),httpCode:'200',content:''}, { id: 'failed', name: $t('失败示例'), httpCode: '200', content: '' }
] ]
export const HTTP_STATUS_CODE = ['200', '403', '404', '410', '422', '500', '502', '503', '504'] export const HTTP_STATUS_CODE = ['200', '403', '404', '410', '422', '500', '502', '503', '504']
export const CONTENT_TYPE_TYPE = [ export const CONTENT_TYPE_TYPE = [
'application/json', 'application/json',
'application/x-www-form-urlencoded', 'application/x-www-form-urlencoded',
'image/jpeg', 'image/jpeg',
'image/png', 'image/png',
'multipart/form-data', 'multipart/form-data',
'text/asp', 'text/asp',
'text/css', 'text/css',
'text/html', 'text/html',
'text/html; charset=UTF-8', 'text/html; charset=UTF-8',
'text/plain', 'text/plain',
'text/xml' 'text/xml'
] ]
export function ResponseExampleCompo ({ editorRef,title,detail,mode='view' }: {editorRef?: React.RefObject<ResponseExampleCompoEditorApi>,title:string, detail:resultList[]}) { export function ResponseExampleCompo({
const [resultDemos, setResultDemos] = useState<unknown>([]); editorRef,
title,
detail,
mode = 'view'
}: {
editorRef?: React.RefObject<ResponseExampleCompoEditorApi>
title: string
detail: resultList[]
}) {
const [resultDemos, setResultDemos] = useState<unknown>([])
useImperativeHandle(editorRef, () => ({ useImperativeHandle(editorRef, () => ({
getData: () => { getData: () => {
return resultDemos||[] return resultDemos || []
} }
})) }))
useEffect(() => { useEffect(() => {
if(mode === 'view'){ if (mode === 'view') {
setResultDemos(detail); setResultDemos(detail)
}else{ } else {
setResultDemos(detail?.length > 0 ? detail: cloneDeep(DEFAULT_RESULT_LIST)) setResultDemos(detail?.length > 0 ? detail : cloneDeep(DEFAULT_RESULT_LIST))
}
}, [detail]);
const updateResultList = (id:string, type:'httpCode' | 'httpContentType'|'content',value:string) => {
setResultDemos((prevList)=>{
for(let i = 0 ; i < prevList.length; i++){
if(prevList[i].id === id){
prevList[i][type] = value
return prevList
}
}
})
} }
}, [detail])
const updateResultList = (id: string, type: 'httpCode' | 'httpContentType' | 'content', value: string) => {
setResultDemos((prevList) => {
for (let i = 0; i < prevList.length; i++) {
if (prevList[i].id === id) {
prevList[i][type] = value
return prevList
}
}
})
}
return ( return (
<Collapse title={title}> <Collapse title={title}>
<Box width="100%"> <Box width="100%">
<Tabs className=" small-tabs" defaultActiveKey={resultDemos?.[0]?.id}> <Tabs className=" small-tabs" defaultActiveKey={resultDemos?.[0]?.id}>
{resultDemos && resultDemos?.map((item:ResultListType) => ( {resultDemos &&
<Tabs.TabPane key={item.id} tab={item.name}> resultDemos?.map((item: ResultListType) => (
<div > <Tabs.TabPane key={item.id} tab={item.name}>
<div className="ml-[8px] mb-[8px] flex"> <div>
{mode === 'view' ? <div className="ml-[8px] mb-[8px] flex">
item.content ? <span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT"> HTTP Status Code: {item.httpCode}</span>:'' {mode === 'view' ? (
: <AutoComplete item.content ? (
className="mr-btnbase rounded " <span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT">
options={HTTP_STATUS_CODE.map((code)=>({label:code, value:code}))} {' '}
style={{ width: 200 }} HTTP Status Code: {item.httpCode}
value={item.httpCode} </span>
status={item.httpCode ? '' : 'error'} ) : (
onSelect={(value)=>updateResultList(item.id,'httpCode',value)} ''
placeholder={$t("HTTP 状态码")} )
/> ) : (
} <AutoComplete
{mode === 'view' ? className="mr-btnbase rounded "
item.content ? <span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT">Content-Type: {item.httpContentType || 'text/html;charset=UTF-8'}</span>:'' options={HTTP_STATUS_CODE.map((code) => ({ label: code, value: code }))}
: <AutoComplete style={{ width: 200 }}
options={CONTENT_TYPE_TYPE.map((type)=>({label:type, value:type}))} value={item.httpCode}
style={{ width: 200 }} status={item.httpCode ? '' : 'error'}
value={item.httpContentType || 'text/html;charset=UTF-8'} onSelect={(value) => updateResultList(item.id, 'httpCode', value)}
onSelect={(value)=>updateResultList(item.id,'httpContentType',value)} placeholder={$t('HTTP 状态码')}
placeholder={$t("默认 text/html;charset=UTF-8")} />
/>} )}
{mode === 'view' ? (
item.content ? (
<span className="mr-btnbase py-[5px] px-btnbase text-DEFAULT bg-[#f7f8fa] rounded border-[1px] border-solid border-DEFAULT">
Content-Type: {item.httpContentType || 'text/html;charset=UTF-8'}
</span>
) : (
''
)
) : (
<AutoComplete
options={CONTENT_TYPE_TYPE.map((type) => ({ label: type, value: type }))}
style={{ width: 200 }}
value={item.httpContentType || 'text/html;charset=UTF-8'}
onSelect={(value) => updateResultList(item.id, 'httpContentType', value)}
placeholder={$t('默认 text/html;charset=UTF-8')}
/>
)}
</div>
{mode === 'view' ? (
<>
{item.content ? (
<pre className="border-[1px] border-solid border-BORDER p-[6px] rounded w-auto min-h-[130px] max-h-[500px] overflow-auto mt-[0px]">
{item.content}
</pre>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={$t('暂未填写示例')} />
)}
</>
) : (
<>
<Codebox
value={item.content}
language="json"
width="100%"
height={'250px'}
onChange={(value) => updateResultList(item.id, 'content', value)}
/>
</>
)}
</div> </div>
{mode === 'view' ? </Tabs.TabPane>
<> ))}
{ item.content ?
<pre className="border-[1px] border-solid border-BORDER p-[6px] rounded w-auto min-h-[130px] max-h-[500px] overflow-auto mt-[0px]">{item.content}</pre>
:
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={$t("暂未填写示例")}/>
}</>
: <>
<Codebox value={item.content} language='json' width="100%" height={'250px'} onChange={(value)=>updateResultList(item.id,'content',value)}/>
</>
}
</div>
</Tabs.TabPane>
))}
</Tabs> </Tabs>
</Box> </Box>
</Collapse> </Collapse>
); )
} }
@@ -1,259 +1,297 @@
import { Collapse } from './api/Collapse'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { Select, Input, Space } from 'antd'
import { Box, Stack, ThemeProvider, createTheme } from '@mui/material'
import { ApiResponseEditor, ApiResponseEditorApi } from './api/ApiManager/components/ApiResponseEditor'
import { ApiRequestEditor, ApiRequestEditorApi } from './api/ApiManager/components/ApiRequestEditor'
import { ResponseExampleCompo, ResponseExampleCompoEditorApi } from '@common/components/apispace/response-example'
import { ResultListType } from '@common/const/api-detail'
import { SystemApiDetail, SystemInsideApiProxyHandle } from '@core/const/system/type'
import SystemInsideApiProxy from '@core/pages/system/api/SystemInsideApiProxy'
import ApiMatch from './api/ApiPreview/components/ApiMatch'
import { v4 as uuidv4 } from 'uuid'
import { PLACEHOLDER } from '@common/const/const'
import { $t } from '@common/locales'
import {Collapse} from "./api/Collapse"; const PROTOCOL_LIST = ['HTTP', 'HTTPS']
import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react"; const HTTP_METHOD_LIST = ['POST', 'GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH']
import {Select,Input,Space} from "antd"; export interface ApiEditApi {
import { Box, Stack, ThemeProvider, createTheme } from "@mui/material" getData: () => Promise<{ apiInfo: Partial<SystemApiDetail> } | string | boolean> | undefined
import {ApiResponseEditor, ApiResponseEditorApi} from "./api/ApiManager/components/ApiResponseEditor";
import {ApiRequestEditor, ApiRequestEditorApi} from "./api/ApiManager/components/ApiRequestEditor";
import {ResponseExampleCompo, ResponseExampleCompoEditorApi} from "@common/components/apispace/response-example";
import {ResultListType} from "@common/const/api-detail";
import { SystemApiDetail, SystemInsideApiProxyHandle } from "@core/const/system/type";
import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy";
import ApiMatch from "./api/ApiPreview/components/ApiMatch";
import {v4 as uuidv4} from 'uuid'
import { PLACEHOLDER } from "@common/const/const";
import { $t } from "@common/locales";
const PROTOCOL_LIST = ['HTTP','HTTPS']
const HTTP_METHOD_LIST = ['POST','GET','PUT', 'DELETE','HEAD','OPTIONS','PATCH']
export interface ApiEditApi{
getData:()=>(Promise<{apiInfo:Partial<SystemApiDetail>}|string|boolean> | undefined)
} }
interface DescriptionHandle{ interface DescriptionHandle {
getData:()=>string getData: () => string
} }
interface ApiNameProps{ interface ApiNameProps {
apiInfo:SystemApiDetail apiInfo: SystemApiDetail
} }
interface ApiNameHandle{ interface ApiNameHandle {
getData:()=>string getData: () => string
} }
export const theme = createTheme({ export const theme = createTheme({
palette: { palette: {
primary: { primary: {
main: '#3D46F2', // 自定义主色调 main: '#3D46F2' // 自定义主色调
},
text: {
primary: '#333', // 主要文字颜色
secondary: '#333', // 次要文字颜色
},
// 添加其他颜色配置,如错误色、背景色等
error: {
main: '#d32f2f',
},
background: {
paper: '#fff',
default: '#f7f8fa',
},
}, },
transitions:{ text: {
create:()=>'none' primary: '#333', // 主要文字颜色
secondary: '#333' // 次要文字颜色
}, },
components:{ // 添加其他颜色配置,如错误色、背景色等
MuiInput: { error: {
styleOverrides: { main: '#d32f2f'
root: { },
'&::placeholder': { background: {
color: '#BBB', // 设置 placeholder 的颜色 paper: '#fff',
}, default: '#f7f8fa'
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': { }
borderColor: '#3D46F2', // 设置 hover 时的边框颜色 },
borderWidth: '1px', // 设置边框粗细 transitions: {
}, create: () => 'none'
'&.Mui-focused': { },
borderColor: '#3D46F2', // 设置选中时的边框颜色 components: {
borderWidth: '1px', // 设置边框粗细 MuiInput: {
}, styleOverrides: {
}, root: {
'&::placeholder': {
color: '#BBB' // 设置 placeholder 的颜色
}, },
}, '&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
MuiTextField: { borderColor: '#3D46F2', // 设置 hover 时的边框颜色
styleOverrides: { borderWidth: '1px' // 设置边框粗细
root: { },
'&::placeholder': { '&.Mui-focused': {
color: '#BBB', // 设置 placeholder 的颜色 borderColor: '#3D46F2', // 设置选中时的边框颜色
}, borderWidth: '1px' // 设置边框粗细
'&:hover .MuiOutlinedInput-notchedOutline':{ }
borderColor: '#3D46F2', // 设置选中时的边框颜色 }
borderWidth: '1px', // 设置边框粗细 }
} },
MuiTextField: {
styleOverrides: {
root: {
'&::placeholder': {
color: '#BBB' // 设置 placeholder 的颜色
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2', // 设置选中时的边框颜色
borderWidth: '1px' // 设置边框粗细
}
}
}
},
MuiCheckbox: {
styleOverrides: {
root: {
'&:hover': {
backgroundColor: 'transparent' // 设置 hover 时的背景色为透明
},
'&:hover:before': {
backgroundColor: 'transparent' // 确保不透明度也为透明
},
transition: 'none' // 取消过渡效果
}
}
},
MuiButton: {
styleOverrides: {
root: {
'&': {
marginLeft: '0px',
padding: '3px 12px',
borderRadius: '4px'
}
}
}
},
MuiSelect: {
styleOverrides: {
root: {
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
borderColor: '#3D46F2',
borderWidth: '1px'
},
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error) .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px'
},
'&.Mui-focused': {
borderColor: '#3D46F2',
borderWidth: '1px'
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px'
}
}
}
},
MuiMenu: {
styleOverrides: {
root: {
'.MuiMenuItem-root:hover': {
backgroundColor: '#EBEEF2'
},
'.MuiMenuItem-root.Mui-selected': {
backgroundColor: '#EBEEF2'
}
}
}
},
MuiInputLabel: {
styleOverrides: {
root: {
color: '#BBB' // 设置 label 的颜色为灰色
}
}
}
}
})
export default function ApiEdit({
apiInfo,
editorRef,
loaded,
serviceId,
teamId
}: {
apiInfo: SystemApiDetail
editorRef?: React.RefObject<ApiEditApi>
loaded: boolean
serviceId: string
teamId: string
}) {
const requestRef = useRef<ApiRequestEditorApi>(null)
const responseRef = useRef<ApiResponseEditorApi>(null)
const resultListRef = useRef<ResponseExampleCompoEditorApi>(null)
const protocolOptionList = PROTOCOL_LIST.map((x) => ({ label: x, value: x }))
const methodOptionList = HTTP_METHOD_LIST.map((x) => ({ label: x, value: x }))
const [apiName, setApiName] = useState<string>('')
const [resultList, setResultList] = useState<ResultListType[]>([])
const proxyRef = useRef<SystemInsideApiProxyHandle>(null)
const descriptionRef = useRef<DescriptionHandle>(null)
const apiNameRef = useRef<ApiNameHandle>(null)
useImperativeHandle(editorRef, () => ({
getData: () => {
return proxyRef.current
?.validate()
.then((res) => {
const name = apiNameRef.current?.getData()
if (!name) return Promise.reject($t('请填写接口名称'))
const newData: { apiInfo: Partial<SystemApiDetail> } = {
apiInfo: {
info: {
name,
description: descriptionRef.current?.getData()
}, },
}, proxy: res,
}, doc: {
MuiCheckbox: { ...apiInfo?.doc,
styleOverrides: { requestParams: requestRef.current!.getData()!,
root: { responseList: responseRef.current!.getData()!,
'&:hover': { resultList: resultListRef.current!.getData()!
backgroundColor: 'transparent', // 设置 hover 时的背景色为透明
},
'&:hover:before': {
backgroundColor: 'transparent', // 确保不透明度也为透明
},
transition: 'none', // 取消过渡效果
},
},
},
MuiButton:{
styleOverrides: {
root: {
'&':{
marginLeft:'0px',
padding:'3px 12px',
borderRadius:'4px'
}
} }
} }
}, }
MuiSelect: { return Promise.resolve(newData)
styleOverrides: { })
root: { .catch((errInfo) => Promise.reject(errInfo))
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
'&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error) .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
'&.Mui-focused': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
borderColor: '#3D46F2',
borderWidth: '1px',
},
},
},
},
MuiMenu:{
styleOverrides: {
root:{
'.MuiMenuItem-root:hover':{
backgroundColor: '#EBEEF2',
},
'.MuiMenuItem-root.Mui-selected':{
backgroundColor: '#EBEEF2',
}
}
}
},
MuiInputLabel: {
styleOverrides: {
root: {
color: '#BBB', // 设置 label 的颜色为灰色
},
},
},
} }
}); }))
export default function ApiEdit({apiInfo,editorRef,loaded,serviceId, teamId}:{apiInfo:SystemApiDetail,editorRef?:React.RefObject<ApiEditApi>,loaded:boolean,serviceId:string, teamId:string}){ useEffect(() => {
const requestRef = useRef<ApiRequestEditorApi>(null) if (!apiInfo || Object.keys(apiInfo).length === 0) return
const responseRef = useRef<ApiResponseEditorApi>(null) setApiName(apiInfo.name!)
const resultListRef = useRef<ResponseExampleCompoEditorApi>(null) setResultList(apiInfo?.doc?.resultList || [])
const protocolOptionList = PROTOCOL_LIST.map((x)=>({label:x,value:x})) }, [apiInfo])
const methodOptionList = HTTP_METHOD_LIST.map((x)=>({label:x,value:x}))
const [apiName,setApiName]=useState<string>('') const Description = forwardRef<DescriptionHandle, { initDescription: string | undefined }>((props, ref) => {
const [resultList,setResultList] = useState<ResultListType[]>([]) const { initDescription } = props
const proxyRef = useRef<SystemInsideApiProxyHandle>(null) const [description, setDescription] = useState<string>(initDescription || '')
const descriptionRef = useRef<DescriptionHandle>(null) useImperativeHandle(ref, () => ({
const apiNameRef = useRef<ApiNameHandle>(null) getData: () => description
useImperativeHandle(editorRef, () => ({
getData: () => {
return proxyRef.current?.validate().then((res)=>{
const name = apiNameRef.current?.getData()
if(!name) return Promise.reject($t('请填写接口名称'))
const newData :{apiInfo:Partial<SystemApiDetail>}= {
apiInfo:{
info:{
name,
description:descriptionRef.current?.getData(),
},
proxy:res,
doc:{
...apiInfo?.doc,
requestParams:requestRef.current!.getData()!,
responseList:responseRef.current!.getData()!,
resultList:resultListRef.current!.getData()!
}
}
}
return Promise.resolve(newData)
}).catch((errInfo)=>Promise.reject(errInfo))
}
})) }))
return (
useEffect(() => { <Input.TextArea
if(!apiInfo || Object.keys(apiInfo).length === 0) return className="w-full border-none"
setApiName(apiInfo.name!) value={description}
setResultList(apiInfo?.doc?.resultList || []) onChange={(e) => setDescription(e.target.value)}
}, [apiInfo]); placeholder={$t(PLACEHOLDER.input)}
/>
const Description = forwardRef<DescriptionHandle,{initDescription:string|undefined}>((props,ref)=>{
const { initDescription } = props
const [description, setDescription] = useState<string>(initDescription||'')
useImperativeHandle(ref, ()=>({
getData:()=>description
}))
return (
<Input.TextArea className="w-full border-none" value={description} onChange={(e)=>setDescription(e.target.value)} placeholder={$t(PLACEHOLDER.input)}/>
)
})
const ApiName = forwardRef<ApiNameHandle,ApiNameProps>((props,ref)=>{
const {apiInfo} = props
const [apiName, setApiName] = useState<string>(apiInfo?.name || '')
useImperativeHandle(ref, ()=>({
getData:()=>apiName
}))
return (
<>
<Space.Compact className="w-full mb-btnybase">
<Select className="w-[15%] min-w-[100px]" value={apiInfo?.protocol || 'HTTP'} disabled={true} options={protocolOptionList} />
<Select className="w-[15%] min-w-[100px]" value={apiInfo?.method} disabled={true} options={methodOptionList} />
<Input className="w-[70%]" value={apiInfo?.path} disabled={true} />
</Space.Compact>
<Input value={apiName} onChange={(e)=>setApiName(e.target.value) } status={apiName ? '' : 'error'}/>
</>
)
})
return(
<>
<ThemeProvider theme={theme}>
<Box>
<Box>
<Stack direction="column" spacing={3}>
<ApiName apiInfo={apiInfo} ref={apiNameRef}/>
<Collapse key="description" title={$t('详细说明')}>
<Description initDescription={apiInfo?.description} ref={descriptionRef}/>
</Collapse>
{
apiInfo?.match && apiInfo.match?.length > 0 &&
<ApiMatch title={$t('高级匹配')} rows={apiInfo?.match.map((x)=>{x.id = uuidv4();return x})} />
}
<Collapse title={$t('转发配置')} key="proxy" >
<SystemInsideApiProxy className="m-[12px] px-[12px]" initProxyValue={apiInfo?.proxy} serviceId={serviceId!} ref={proxyRef} />
</Collapse>
<Collapse title={$t('请求参数')} key="request" >
<ApiRequestEditor editorRef={requestRef} apiInfo={apiInfo?.doc} loaded={loaded} />
</Collapse>
<Collapse title={$t('返回值')} key="response">
<ApiResponseEditor editorRef={responseRef} apiInfo={apiInfo?.doc} loaded={loaded}/>
</Collapse>
<ResponseExampleCompo editorRef={resultListRef} mode='edit' title={$t('返回示例')} detail={resultList}/>
</Stack>
</Box>
</Box>
</ThemeProvider>
</>
) )
} })
const ApiName = forwardRef<ApiNameHandle, ApiNameProps>((props, ref) => {
const { apiInfo } = props
const [apiName, setApiName] = useState<string>(apiInfo?.name || '')
useImperativeHandle(ref, () => ({
getData: () => apiName
}))
return (
<>
<Space.Compact className="w-full mb-btnybase">
<Select
className="w-[15%] min-w-[100px]"
value={apiInfo?.protocol || 'HTTP'}
disabled={true}
options={protocolOptionList}
/>
<Select
className="w-[15%] min-w-[100px]"
value={apiInfo?.method}
disabled={true}
options={methodOptionList}
/>
<Input className="w-[70%]" value={apiInfo?.path} disabled={true} />
</Space.Compact>
<Input value={apiName} onChange={(e) => setApiName(e.target.value)} status={apiName ? '' : 'error'} />
</>
)
})
return (
<>
<ThemeProvider theme={theme}>
<Box>
<Box>
<Stack direction="column" spacing={3}>
<ApiName apiInfo={apiInfo} ref={apiNameRef} />
<Collapse key="description" title={$t('详细说明')}>
<Description initDescription={apiInfo?.description} ref={descriptionRef} />
</Collapse>
{apiInfo?.match && apiInfo.match?.length > 0 && (
<ApiMatch
title={$t('高级匹配')}
rows={apiInfo?.match.map((x) => {
x.id = uuidv4()
return x
})}
/>
)}
<Collapse title={$t('转发配置')} key="proxy">
<SystemInsideApiProxy
className="m-[12px] px-[12px]"
initProxyValue={apiInfo?.proxy}
serviceId={serviceId!}
ref={proxyRef}
/>
</Collapse>
<Collapse title={$t('请求参数')} key="request">
<ApiRequestEditor editorRef={requestRef} apiInfo={apiInfo?.doc} loaded={loaded} />
</Collapse>
<Collapse title={$t('返回值')} key="response">
<ApiResponseEditor editorRef={responseRef} apiInfo={apiInfo?.doc} loaded={loaded} />
</Collapse>
<ResponseExampleCompo editorRef={resultListRef} mode="edit" title={$t('返回示例')} detail={resultList} />
</Stack>
</Box>
</Box>
</ThemeProvider>
</>
)
}

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