diff --git a/docs/eslint-fix.md b/docs/eslint-fix.md new file mode 100644 index 0000000..f8da425 --- /dev/null +++ b/docs/eslint-fix.md @@ -0,0 +1,4 @@ +1.任务:根据以下的eslint检查提示,修复any类问题。解决方案,将any替换为unknown。修复define but not used问题 方案:删除掉未使用的变量。 + +有问题的内容: +@typescript-eslint/no-unused-vars 36:5 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\auto\components\BatchEvaluationPanel.tsx:36:5 34 | 35 | useEffect(() => { > 36 | setBatchManager(new BatchAnalysisManager(dispatch)); | ^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 37 | }, [dispatch]); 38 | 39 | const totalWeight = Math.round( react-hooks/set-state-in-effect 67:13 warning 'blockIds' is assigned a value but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\auto\components\EvaluationModel.tsx 11:3 warning 'Eye' is defined but never used @typescript-eslint/no-unused-vars 12:3 warning 'EyeOff' is defined but never used @typescript-eslint/no-unused-vars 16:3 warning 'Droplet' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\auto\components\batchAnalysisService.tsx 3:10 warning 'SpatialAnalysisState' is defined but never used @typescript-eslint/no-unused-vars 6:11 warning 'FactorData' is defined but never used @typescript-eslint/no-unused-vars 305:99 warning 'weights' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\auto\page.tsx 10:3 warning 'Play' is defined but never used @typescript-eslint/no-unused-vars 71:48 warning 'fieldId' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\multiFactor\components\MultiFactorEvaluation.tsx 8:20 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars 14:79 warning 'DialogTrigger' is defined but never used @typescript-eslint/no-unused-vars 17:10 warning 'Textarea' is defined but never used @typescript-eslint/no-unused-vars 19:3 warning 'Leaf' is defined but never used @typescript-eslint/no-unused-vars 26:3 warning 'Download' is defined but never used @typescript-eslint/no-unused-vars 27:3 warning 'Eye' is defined but never used @typescript-eslint/no-unused-vars 28:3 warning 'Calculator' is defined but never used @typescript-eslint/no-unused-vars 29:3 warning 'Database' is defined but never used @typescript-eslint/no-unused-vars 30:3 warning 'RefreshCw' is defined but never used @typescript-eslint/no-unused-vars 32:3 warning 'Target' is defined but never used @typescript-eslint/no-unused-vars 33:3 warning 'Droplet' is defined but never used @typescript-eslint/no-unused-vars 35:3 warning 'Sun' is defined but never used @typescript-eslint/no-unused-vars 36:3 warning 'ThermometerSun' is defined but never used @typescript-eslint/no-unused-vars 39:3 warning 'Info' is defined but never used @typescript-eslint/no-unused-vars 40:3 warning 'BarChart3' is defined but never used @typescript-eslint/no-unused-vars 41:3 warning 'Filter' is defined but never used @typescript-eslint/no-unused-vars 46:3 warning 'EvaluationFactor' is defined but never used @typescript-eslint/no-unused-vars 52:3 warning 'getSuitabilityLevelColor' is defined but never used @typescript-eslint/no-unused-vars 54:3 warning 'MOCK_FIELDS' is defined but never used @typescript-eslint/no-unused-vars 60:3 warning 'matchCropsForField' is defined but never used @typescript-eslint/no-unused-vars 68:10 warning 'batchProgress' is assigned a value but never used @typescript-eslint/no-unused-vars 76:10 warning 'isBatchRunning' is assigned a value but never used @typescript-eslint/no-unused-vars 102:9 warning 'handleRunBatchAnalysis' is assigned a value but never used @typescript-eslint/no-unused-vars 133:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars 172:14 warning 'error' is defined but never used @typescript-eslint/no-unused-vars 177:9 warning 'exportResults' is assigned a value but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\multiFactor\components\cropKnowledgeBase.ts 6:63 warning 'RiskFactor' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\recommend\components\CropRecommendations.tsx 145:39 warning 'state' is defined but never used @typescript-eslint/no-unused-vars 147:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 151:27 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 154:87 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\recommend\components\cropRecommendReducer.tsx 3:10 warning 'useReducer' is defined but never used @typescript-eslint/no-unused-vars 4:10 warning 'toast' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\land-information\suitability\recommend\page.tsx 11:3 warning 'SuitabilityResult' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(auth)\login\components\LoginForm.tsx 30:41 warning 'AuthState' is defined but never used @typescript-eslint/no-unused-vars 30:52 warning 'AuthAction' is defined but never used @typescript-eslint/no-unused-vars 31:10 warning 'getCaptchaApiV1AuthCaptchaGet' is defined but never used @typescript-eslint/no-unused-vars 43:10 warning 'phoneCaptchaData' is assigned a value but never used @typescript-eslint/no-unused-vars 43:28 warning 'setPhoneCaptchaData' is assigned a value but never used @typescript-eslint/no-unused-vars 75:14 warning 'err' is defined but never used @typescript-eslint/no-unused-vars 157:19 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 207:14 warning 'err' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(auth)\register\page.tsx 175:14 warning 'err' is defined but never used @typescript-eslint/no-unused-vars 249:14 warning 'err' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\layout.tsx 9:143 warning 'MapPin' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\app\not-found.tsx 8:61 warning 'ExternalLink' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\components\app-sidebar.tsx 5:10 warning 'SearchForm' is defined but never used @typescript-eslint/no-unused-vars 6:10 warning 'VersionSwitcher' is defined but never used @typescript-eslint/no-unused-vars 18:3 warning 'SidebarHeader' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\AuthContext.tsx 152:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 255:21 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 313:6 warning React Hook React.useEffect has a missing dependency: 'validateUser'. Either include it or remove the dependency array react-hooks/exhaustive-deps D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\CaptchaInput.tsx 127:6 warning React Hook useEffect has a missing dependency: 'fetchCaptcha'. Either include it or remove the dependency array react-hooks/exhaustive-deps 164:15 warning Using could result in slower LCP and higher bandwidth. Consider using from next/image or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\LoadingScreen.tsx 14:3 warning 'message' is defined but never used @typescript-eslint/no-unused-vars 15:3 warning 'subMessage' is defined but never used @typescript-eslint/no-unused-vars 16:3 warning 'variant' is assigned a value but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\SmartFieldBackground.tsx 24:5 warning Compilation Skipped: Inline class declarations are not supported Move class declarations outside of components/hooks. D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\SmartFieldBackground.tsx:24:5 22 | 23 | // 田间传感器节点 > 24 | class SensorNode { | ^^^^^^^^^^^^^^^^^^ > 25 | x: number; | ^^^^^^^^^^^^^^^^ > 26 | y: number; … | ^^^^^^^^^^^^^^^^ > 95 | } | ^^^^^^^^^^^^^^^^ > 96 | } | ^^^^^^ Inline class declarations are not supported 97 | 98 | // 无人机 99 | class Drone { react-hooks/unsupported-syntax 99:5 warning Compilation Skipped: Inline class declarations are not supported Move class declarations outside of components/hooks. D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\SmartFieldBackground.tsx:99:5 97 | 98 | // 无人机 > 99 | class Drone { | ^^^^^^^^^^^^^ > 100 | x: number; | ^^^^^^^^^^^^^^^^ > 101 | y: number; … | ^^^^^^^^^^^^^^^^ > 216 | } | ^^^^^^^^^^^^^^^^ > 217 | } | ^^^^^^ Inline class declarations are not supported 218 | 219 | // 田地波浪效果 220 | class FieldWave { react-hooks/unsupported-syntax 220:5 warning Compilation Skipped: Inline class declarations are not supported Move class declarations outside of components/hooks. D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\SmartFieldBackground.tsx:220:5 218 | 219 | // 田地波浪效果 > 220 | class FieldWave { | ^^^^^^^^^^^^^^^^^ > 221 | offset: number; | ^^^^^^^^^^^^^^^^^^^^^ > 222 | speed: number; … | ^^^^^^^^^^^^^^^^^^^^^ > 251 | } | ^^^^^^^^^^^^^^^^^^^^^ > 252 | } | ^^^^^^ Inline class declarations are not supported 253 | 254 | // 数据流粒子 255 | class DataParticle { react-hooks/unsupported-syntax 255:5 warning Compilation Skipped: Inline class declarations are not supported Move class declarations outside of components/hooks. D:\code\repotest\smart-crop-ui\crop-x-new\src\components\auth\SmartFieldBackground.tsx:255:5 253 | 254 | // 数据流粒子 > 255 | class DataParticle { | ^^^^^^^^^^^^^^^^^^^^ > 256 | x: number; | ^^^^^^^^^^^^^^^^ > 257 | y: number; … | ^^^^^^^^^^^^^^^^ > 301 | } | ^^^^^^^^^^^^^^^^ > 302 | } | ^^^^^^ Inline class declarations are not supported 303 | 304 | // 初始化传感器网络 305 | const sensors: SensorNode[] = []; react-hooks/unsupported-syntax D:\code\repotest\smart-crop-ui\crop-x-new\src\components\common\searchFormPagination\components\SearchFormComponent.tsx 40:3 warning 'loading' is assigned a value but never used @typescript-eslint/no-unused-vars 49:3 error Error: Cannot access refs during render React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the current property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\common\searchFormPagination\components\SearchFormComponent.tsx:49:3 47 | // 使用ref保持最新的onFiltersChange引用,避免useEffect重复触发 48 | const onFiltersChangeRef = useRef(onFiltersChange); > 49 | onFiltersChangeRef.current = onFiltersChange; | ^^^^^^^^^^^^^^^^^^^^^^^^^^ Cannot update ref during render 50 | 51 | // 同步外部filters到本地state 52 | useEffect(() => { react-hooks/refs 70:15 warning '_lastChangedFieldType' is assigned a value but never used @typescript-eslint/no-unused-vars 70:38 warning '_lastChangedFieldKey' is assigned a value but never used @typescript-eslint/no-unused-vars 98:19 warning '_lastChangedFieldType' is assigned a value but never used @typescript-eslint/no-unused-vars 98:42 warning '_lastChangedFieldKey' is assigned a value but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\components\common\searchFormPagination\components\searchFormPaginationReducer.tsx 12:9 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 36:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 49:57 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 193:55 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\components\common\searchFormPagination\page.tsx 11:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars 12:10 warning 'Input' is defined but never used @typescript-eslint/no-unused-vars 13:10 warning 'Label' is defined but never used @typescript-eslint/no-unused-vars 15:10 warning 'Select' is defined but never used @typescript-eslint/no-unused-vars 15:18 warning 'SelectContent' is defined but never used @typescript-eslint/no-unused-vars 15:33 warning 'SelectItem' is defined but never used @typescript-eslint/no-unused-vars 15:45 warning 'SelectTrigger' is defined but never used @typescript-eslint/no-unused-vars 15:60 warning 'SelectValue' is defined but never used @typescript-eslint/no-unused-vars 16:23 warning 'ChevronLeft' is defined but never used @typescript-eslint/no-unused-vars 16:36 warning 'ChevronRight' is defined but never used @typescript-eslint/no-unused-vars 17:10 warning 'toast' is defined but never used @typescript-eslint/no-unused-vars 37:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 37:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 72:48 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 126:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 351:7 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\common\searchFormPagination\page.tsx:351:7 349 | 350 | // 设置内部分页状态 > 351 | setInternalPagination(urlPagination); | ^^^^^^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 352 | 353 | // URL中有参数时,自动同步到内部状态并通知父组件 354 | if (hasUrlFilters || hasSearchParams) { react-hooks/set-state-in-effect 380:6 warning React Hook useEffect has a missing dependency: 'onUrlStateChange'. Either include it or remove the dependency array. If 'onUrlStateChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps 389:9 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\common\searchFormPagination\page.tsx:389:9 387 | // 如果父组件的分页状态与内部状态不一致,同步内部状态 388 | if (pagination.page !== internalPagination.page || pagination.size !== internalPagination.size) { > 389 | setInternalPagination({ | ^^^^^^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 390 | page: pagination.page, 391 | size: pagination.size 392 | }); react-hooks/set-state-in-effect 580:75 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\components\field\MapPointPicker.tsx 31:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 32:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 33:42 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 68:6 warning React Hook useEffect has missing dependencies: 'map' and 'points'. Either include them or remove the dependency array react-hooks/exhaustive-deps 103:5 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\field\MapPointPicker.tsx:103:5 101 | }); 102 | > 103 | setMarkers(newMarkers); | ^^^^^^^^^^ Avoid calling setState() directly within an effect 104 | 105 | // 绘制多边形(仅多边形模式) 106 | if (mode === 'polygon' && points.length >= 3) { react-hooks/set-state-in-effect 124:6 warning React Hook useEffect has missing dependencies: 'markers', 'mode', 'onPointsChange', and 'polygon'. Either include them or remove the dependency array. If 'onPointsChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps 130:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 148:6 warning React Hook useEffect has a missing dependency: 'onPointsChange'. Either include it or remove the dependency array. If 'onPointsChange' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\Navbar.tsx 3:10 warning 'Book' is defined but never used @typescript-eslint/no-unused-vars 3:22 warning 'Sunset' is defined but never used @typescript-eslint/no-unused-vars 3:30 warning 'Trees' is defined but never used @typescript-eslint/no-unused-vars 3:37 warning 'Zap' is defined but never used @typescript-eslint/no-unused-vars 4:18 warning 'Map' is defined but never used @typescript-eslint/no-unused-vars 4:23 warning 'Clipboard' is defined but never used @typescript-eslint/no-unused-vars 4:34 warning 'Package' is defined but never used @typescript-eslint/no-unused-vars 4:43 warning 'Brain' is defined but never used @typescript-eslint/no-unused-vars 4:50 warning 'Droplets' is defined but never used @typescript-eslint/no-unused-vars 4:60 warning 'Settings' is defined but never used @typescript-eslint/no-unused-vars 11:10 warning 'useRef' is defined but never used @typescript-eslint/no-unused-vars 11:18 warning 'useEffect' is defined but never used @typescript-eslint/no-unused-vars 11:29 warning 'useState' is defined but never used @typescript-eslint/no-unused-vars 23:3 warning 'NavigationMenuContent' is defined but never used @typescript-eslint/no-unused-vars 27:3 warning 'NavigationMenuTrigger' is defined but never used @typescript-eslint/no-unused-vars 69:11 warning 'auth' is assigned a value but never used @typescript-eslint/no-unused-vars 86:23 warning 'updateHeight' is assigned a value but never used @typescript-eslint/no-unused-vars 170:15 warning Using could result in slower LCP and higher bandwidth. Consider using from next/image or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element 186:23 warning Using could result in slower LCP and higher bandwidth. Consider using from next/image or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element 221:89 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 282:95 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\SideBar\SideBarOld.tsx 205:7 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\SideBar\SideBarOld.tsx:205:7 203 | useEffect(() => { 204 | if (pathname) { > 205 | setCurrentPath(pathname); | ^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 206 | } 207 | }, [pathname]); 208 | react-hooks/set-state-in-effect 212:7 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\SideBar\SideBarOld.tsx:212:7 210 | useEffect(() => { 211 | if (isMobile) { > 212 | setIsCollapsed(false); | ^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 213 | } 214 | }, [isMobile]); 215 | react-hooks/set-state-in-effect D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\SideBar\components\LeftSidebar.tsx 4:10 warning 'ChevronDown' is defined but never used @typescript-eslint/no-unused-vars 4:50 warning 'Menu' is defined but never used @typescript-eslint/no-unused-vars 7:10 warning 'Badge' is defined but never used @typescript-eslint/no-unused-vars 66:5 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\SideBar\components\LeftSidebar.tsx:66:5 64 | // 当侧边栏状态改变时,折叠所有菜单 65 | useEffect(() => { > 66 | setExpandedMenus(new Set()); | ^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect 67 | }, [isCollapsed]); 68 | 69 | return ( react-hooks/set-state-in-effect D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\SideBar\components\MainContent.tsx 4:10 warning 'ChevronLeft' is defined but never used @typescript-eslint/no-unused-vars 4:37 warning 'X' is defined but never used @typescript-eslint/no-unused-vars 5:10 warning 'cn' is defined but never used @typescript-eslint/no-unused-vars 7:31 warning 'SheetTrigger' is defined but never used @typescript-eslint/no-unused-vars 19:3 warning 'sidebarOpen' is assigned a value but never used @typescript-eslint/no-unused-vars 24:9 warning 'handleToggleSidebar' is assigned a value but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\ThemeToggle.tsx 14:5 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\ThemeToggle.tsx:14:5 12 | // 避免服务端渲染和客户端渲染不匹配 13 | useEffect(() => { > 14 | setMounted(true); | ^^^^^^^^^^ Avoid calling setState() directly within an effect 15 | }, []); 16 | 17 | const toggleTheme = () => { react-hooks/set-state-in-effect D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\components\MessageBell.tsx 4:29 warning 'X' is defined but never used @typescript-eslint/no-unused-vars 5:10 warning 'toast' is defined but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\components\auth\AuthContext.tsx 63:9 error Error: Cannot access variable before it is declared autoLoginDefaultUser is accessed before it is declared, which prevents the earlier access from updating when this value changes over time. D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\components\auth\AuthContext.tsx:63:9 61 | localStorage.removeItem('user_info'); 62 | // 如果数据无效,自动登录默认用户 > 63 | autoLoginDefaultUser(); | ^^^^^^^^^^^^^^^^^^^^ autoLoginDefaultUser accessed before it is declared 64 | } 65 | } else { 66 | // 如果没有本地存储数据,自动登录默认用户 D:\code\repotest\smart-crop-ui\crop-x-new\src\components\layouts\components\auth\AuthContext.tsx:72:3 70 | 71 | // 自动登录默认用户 > 72 | const autoLoginDefaultUser = () => { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 73 | const defaultUser: User = { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 74 | id: '1', … | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 95 | }); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 96 | }; | ^^^^^ autoLoginDefaultUser is declared here 97 | 98 | const login = async (username: string, password: string): Promise => { 99 | try { react-hooks/immutability D:\code\repotest\smart-crop-ui\crop-x-new\src\components\shared\BaseMap.tsx 73:23 warning 'setCoordinates' is assigned a value but never used @typescript-eslint/no-unused-vars 135:6 warning React Hook useEffect has missing dependencies: 'initialCenter', 'initialLayer', 'initialZoom', 'onMapReady', and 'provider'. Either include them or remove the dependency array. If 'onMapReady' changes too often, find the parent component that defines it and wrap that definition in useCallback react-hooks/exhaustive-deps D:\code\repotest\smart-crop-ui\crop-x-new\src\components\ui\sidebar.tsx 611:26 error Error: Cannot call impure function during render Math.random is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent). D:\code\repotest\smart-crop-ui\crop-x-new\src\components\ui\sidebar.tsx:611:26 609 | // Random width between 50 to 90%. 610 | const width = React.useMemo(() => { > 611 | return ${Math.floor(Math.random() * 40) + 50}% | ^^^^^^^^^^^^^ Cannot call impure function 612 | }, []) 613 | 614 | return ( react-hooks/purity D:\code\repotest\smart-crop-ui\crop-x-new\src\hooks\useElementHeight.ts 17:5 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\hooks\useElementHeight.ts:17:5 15 | // 确保在客户端执行 16 | useEffect(() => { > 17 | setIsClient(true); | ^^^^^^^^^^^ Avoid calling setState() directly within an effect 18 | }, []); 19 | 20 | // 使用 useCallback 来避免无限循环 react-hooks/set-state-in-effect 21:39 error Compilation Skipped: Existing memoization could not be preserved React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was options, but the source dependencies were [isClient, setNavigatorHeight, options.onUpdate]. Inferred less specific property than source. D:\code\repotest\smart-crop-ui\crop-x-new\src\hooks\useElementHeight.ts:21:39 19 | 20 | // 使用 useCallback 来避免无限循环 > 21 | const calculateHeight = useCallback(() => { | ^^^^^^^ > 22 | if (!elementRef.current || !isClient) return 0; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 23 | … | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 37 | return height; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 38 | }, [isClient, setNavigatorHeight, options.onUpdate]); | ^^^^ Could not preserve existing manual memoization 39 | 40 | // 手动更新高度的函数 41 | const updateHeight = useCallback(() => { react-hooks/preserve-manual-memoization 38:6 warning React Hook useCallback has a missing dependency: 'options'. Either include it or remove the dependency array react-hooks/exhaustive-deps D:\code\repotest\smart-crop-ui\crop-x-new\src\hooks\useViewHeight.ts 5:26 warning 'calculateMainBodyHeight' is assigned a value but never used @typescript-eslint/no-unused-vars 10:5 error Error: Calling setState synchronously within an effect can trigger cascading renders Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: * Update external systems with the latest state from React. * Subscribe for updates from some external system, calling setState in a callback function when external state changes. Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). D:\code\repotest\smart-crop-ui\crop-x-new\src\hooks\useViewHeight.ts:10:5 8 | // 确保在客户端执行 9 | useEffect(() => { > 10 | setIsClient(true); | ^^^^^^^^^^^ Avoid calling setState() directly within an effect 11 | }, []); 12 | 13 | const getViewHeight = () => { react-hooks/set-state-in-effect 51:6 warning React Hook useEffect has a missing dependency: 'getViewHeight'. Either include it or remove the dependency array react-hooks/exhaustive-deps D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\api\client\client.gen.ts 23:10 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 79:5 error Include a description after the "@ts-expect-error" directive to explain why the @ts-expect-error is necessary. The description must be 3 characters or longer @typescript-eslint/ban-ts-comment 121:24 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 147:17 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\api\core\bodySerializer.gen.ts 11:37 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 11:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 54:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 54:73 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 82:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 82:73 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\api\core\serverSentEvents.gen.ts 230:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\gisMapEngine.ts 68:16 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 69:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 70:33 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 589:11 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 590:8 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 591:26 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\leafletLoader.ts 96:8 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\mapLoader.ts 26:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 113:11 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\spatialDataService.ts 24:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 27:41 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 38:7 warning 'WGS84_B' is assigned a value but never used @typescript-eslint/no-unused-vars 39:7 warning 'WGS84_F' is assigned a value but never used @typescript-eslint/no-unused-vars 889:38 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 914:17 warning '_' is assigned a value but never used @typescript-eslint/no-unused-vars D:\code\repotest\smart-crop-ui\crop-x-new\src\stores\modules\ai-model.ts 66:71 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 66:87 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\stores\modules\auth.ts 28:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\types\index.ts 4:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 37:28 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 41:32 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 49:20 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 59:43 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 346:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 393:13 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 405:29 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\types\message.ts 40:30 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\src\types\system-params.ts 60:31 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any D:\code\repotest\smart-crop-ui\crop-x-new\tailwind.config.js 4:1 warning Assign object to a variable before exporting as module default import/no-anonymous-default-export \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f1d3c3c..5b49d96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "openapi-fetch": "^0.15.0", "react": "19.2.0", "react-dom": "19.2.0", + "recharts": "^3.4.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", @@ -3027,6 +3028,32 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.1", + "resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.10.1.tgz", + "integrity": "sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3034,6 +3061,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3325,6 +3364,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", @@ -3375,6 +3477,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.4", "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", @@ -4634,6 +4742,127 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -4713,6 +4942,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", @@ -5073,6 +5308,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.41.0", + "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.41.0.tgz", + "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", @@ -5520,6 +5765,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/exsolve": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", @@ -6044,6 +6295,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6086,6 +6347,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -12290,9 +12560,31 @@ "version": "16.13.1", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-remove-scroll": { "version": "2.7.1", "resolved": "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", @@ -12375,6 +12667,51 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/recharts": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.4.1.tgz", + "integrity": "sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -12419,6 +12756,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", @@ -13048,6 +13391,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", @@ -13455,6 +13804,28 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 29668c2..4b3fe96 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "openapi-fetch": "^0.15.0", "react": "19.2.0", "react-dom": "19.2.0", + "recharts": "^3.4.1", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss-animate": "^1.0.7", diff --git a/src/app/(app)/central-config/tenant/user-management/components/AddUserModal.tsx b/src/app/(app)/central-config/tenant/user-management/components/AddUserModal.tsx index 32a52f9..5656e55 100644 --- a/src/app/(app)/central-config/tenant/user-management/components/AddUserModal.tsx +++ b/src/app/(app)/central-config/tenant/user-management/components/AddUserModal.tsx @@ -23,6 +23,7 @@ import { toast } from 'sonner'; import { fetchEnterprisesForDropdown, transformEnterprisesToOptions, type EnterpriseOption } from './enterpriseApi'; import { PasswordInput } from './PasswordInput'; import { USER_TYPE_OPTIONS, USER_TYPES } from '../constants/userTypes'; +import { createUser, type CreateUserRequest } from './userManagementApi'; interface AddUserModalProps { open: boolean; @@ -88,6 +89,23 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp } }, [open]); + // 当弹窗关闭时重置表单状态 + useEffect(() => { + if (!open) { + setFormData({ + username: '', + password: '', + fullName: '', + phone: '', + email: '', + userType: 'tenant', + enterpriseId: '', + }); + setErrors({}); + setIsSubmitting(false); + } + }, [open]); + const validateForm = () => { const newErrors: Record = {}; @@ -131,40 +149,30 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp setIsSubmitting(true); try { - // TODO: 调用API创建用户 - // 暂时模拟API调用 - await new Promise(resolve => setTimeout(resolve, 1000)); - - const submitData = { + // 构建API请求数据 + const userData: CreateUserRequest = { + email: formData.email, username: formData.username, - password: formData.password, full_name: formData.fullName, phone: formData.phone, - email: formData.email || undefined, - user_type: formData.userType, - ...(formData.userType === 'tenant' && formData.enterpriseId ? { enterprise_id: formData.enterpriseId } : {}), + password: formData.password, + is_superuser: true, // 所有用户都是超管 + ...(formData.userType === 'tenant' && formData.enterpriseId ? { tenant_id: formData.enterpriseId } : {}), }; - console.log('新增用户数据:', submitData); + console.log('新增用户数据:', userData); + + // 调用API创建用户 + await createUser(userData); toast.success('用户创建成功'); onOpenChange(false); onSuccess?.(); - // 重置表单 - setFormData({ - username: '', - password: '', - fullName: '', - phone: '', - email: '', - userType: 'tenant', - enterpriseId: '', - }); - setErrors({}); } catch (error) { console.error('创建用户失败:', error); - toast.error('创建用户失败,请重试'); + const errorMessage = error instanceof Error ? error.message : '创建用户失败,请重试'; + toast.error(errorMessage); } finally { setIsSubmitting(false); } @@ -188,17 +196,18 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp -
+
{/* 第一行:用户名、密码 */}
- + handleInputChange('username', e.target.value)} placeholder="请输入用户名" className={errors.username ? 'border-red-500' : ''} + autoComplete="new-username" /> {errors.username && (

{errors.username}

@@ -219,13 +228,14 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp {/* 第二行:姓名、电话 */}
- + handleInputChange('fullName', e.target.value)} placeholder="请输入姓名" className={errors.fullName ? 'border-red-500' : ''} + autoComplete="name" /> {errors.fullName && (

{errors.fullName}

@@ -233,13 +243,14 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
- + handleInputChange('phone', e.target.value)} placeholder="请输入手机号码" className={errors.phone ? 'border-red-500' : ''} + autoComplete="tel" /> {errors.phone && (

{errors.phone}

@@ -258,6 +269,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp onChange={(e) => handleInputChange('email', e.target.value)} placeholder="请输入邮箱(可选)" className={errors.email ? 'border-red-500' : ''} + autoComplete="email" /> {errors.email && (

{errors.email}

@@ -268,13 +280,13 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp {/* 第四行:用户类型、所属企业 */}
- + handleInputChange('enterpriseId', value)} @@ -330,7 +342,7 @@ export function AddUserModal({ open, onOpenChange, onSuccess }: AddUserModalProp
)}
-
+ - - - - - ); -} \ No newline at end of file diff --git a/src/app/(app)/central-config/tenant/user-management/components/UserList.tsx b/src/app/(app)/central-config/tenant/user-management/components/UserList.tsx deleted file mode 100644 index 26c43ff..0000000 --- a/src/app/(app)/central-config/tenant/user-management/components/UserList.tsx +++ /dev/null @@ -1,245 +0,0 @@ -/** - * filekorolheader: 用户列表组件 - 用户数据表格展示界面 - * 功能:用户数据表格展示、状态徽章、操作按钮、分页功能 - * 路径:/central-config/tenant/user-management/components/UserList - * 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全 - */ - -import { User, PaginationState } from '../types'; -import { Card } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { Eye, Edit, Trash2, Lock, UserX, UserCheck } from 'lucide-react'; - -interface UserListProps { - users: User[]; - loading: boolean; - pagination: PaginationState; - onPageChange: (page: number) => void; - onViewDetail: (user: User) => void; - onEdit?: (user: User) => void; - onDelete?: (user: User) => void; - onToggleStatus?: (user: User) => void; - onResetPassword?: (user: User) => void; -} - -export function UserList({ - users, - loading, - pagination, - onPageChange, - onViewDetail, - onEdit, - onDelete, - onToggleStatus, - onResetPassword -}: UserListProps) { - const getStatusBadge = (user: User) => { - // 根据isSuperuser和isActive判断状态 - if (user.isSuperuser) { - return user.isActive ? ( - 正常 - ) : ( - 已冻结 - ); - } - - if (user.isActive) { - return 正常; - } else { - return 停用; - } - }; - - const getUserTypeBadge = (user: User) => { - if (user.isSuperuser) { - return 超级管理员; - } - // 根据scope或其他字段判断用户类型 - if (user.scope === 'enterprise' || user.companyName) { - return 企业管理员; - } - return 员工; - }; - - const getRoleBadge = (user: User) => { - if (user.isSuperuser) { - return 超级管理员; - } - return 普通用户; - }; - - const getVerifiedBadge = (isVerified: boolean) => { - return isVerified ? ( - 已验证 - ) : ( - 未验证 - ); - }; - - - if (loading) { - return ( - -
-
加载中...
-
-
- ); - } - - return ( -
- - - - - 用户名 - 姓名 - 电话 - 所属企业 - 用户类型 - 角色 - 状态 - 操作 - - - - {users.length === 0 ? ( - - - 暂无用户数据 - - - ) : ( - users.map((user) => ( - - -
- {user.avatarUrl && ( - {user.username} - )} - {user.username} -
-
- - {user.fullName || user.displayName || '-'} - - {user.phone || '-'} - {user.companyName || '-'} - {getUserTypeBadge(user)} - {getRoleBadge(user)} - {getStatusBadge(user)} - -
- - {onEdit && ( - - )} - {onResetPassword && ( - - )} - {onToggleStatus && ( - - )} - {onDelete && !user.isSuperuser && ( - - )} -
-
-
- )) - )} -
-
-
- - {/* 分页 */} - {pagination.total > 0 && ( -
-
- 显示第 {(pagination.page - 1) * pagination.size + 1} - {Math.min(pagination.page * pagination.size, pagination.total)} 条,共 {pagination.total} 条记录 -
-
- - -
- -
- { - const newPage = parseInt(e.target.value); - if (!isNaN(newPage)) { - onPageChange(newPage); - } - }} - className="w-16 h-8 text-center border rounded-md" - /> -
- / {pagination.totalPages} 页 -
- - -
-
- )} -
- ); -} \ No newline at end of file diff --git a/src/app/(app)/central-config/tenant/user-management/components/UserManagementFilters.tsx b/src/app/(app)/central-config/tenant/user-management/components/UserManagementFilters.tsx deleted file mode 100644 index f527b64..0000000 --- a/src/app/(app)/central-config/tenant/user-management/components/UserManagementFilters.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client'; - -import React from 'react'; -import { Card } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Search } from 'lucide-react'; -import { UserFilters } from '../types'; - -interface UserManagementFiltersProps { - filters: UserFilters; - onSearchChange: (value: string) => void; - onStatusFilterChange: (value: string) => void; - onTypeFilterChange: (value: string) => void; -} - -export function UserManagementFilters({ - filters, - onSearchChange, - onStatusFilterChange, - onTypeFilterChange -}: UserManagementFiltersProps) { - - return ( - -
-
-
- - onSearchChange(e.target.value)} - className="pl-10" - /> -
-
- - -
-
- ); -} \ No newline at end of file diff --git a/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts b/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts index e6fa071..6a88600 100644 --- a/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts +++ b/src/app/(app)/central-config/tenant/user-management/components/userManagementApi.ts @@ -8,6 +8,11 @@ import { getAuthToken } from "@/utils/token"; import { listSystemUsersApiV1UsersSystemUsersGet, + createSystemUserApiV1UsersSystemUsersPost, + getSystemUserApiV1UsersSystemUsersUserIdGet, + activateSystemUserApiV1UsersSystemUsersUserIdActivatePost, + deactivateSystemUserApiV1UsersSystemUsersUserIdDeactivatePost, + deleteSystemUserApiV1UsersSystemUsersUserIdDelete, } from "@/lib/api/sdk.gen"; // API返回的用户数据类型 @@ -191,4 +196,359 @@ export interface PaginationState { totalPages: number; hasNext: boolean; hasPrev: boolean; +} + +// 创建用户请求参数接口 +export interface CreateUserRequest { + email: string; + username: string; + full_name: string; + phone: string; + password: string; + is_superuser: boolean; + tenant_id?: string; // 系统管理员不传,企业管理员必传 +} + +// 创建用户响应数据类型 +export interface CreateUserResponse { + id: string; + email: string; + username: string; + full_name: string; + phone: string; + is_superuser: boolean; + tenant_id?: string; + is_active: boolean; + created_at: string; + updated_at: string; +} + +/** + * 创建系统用户(系统管理员或企业管理员) + * + * @param userData 用户创建数据 + * @returns 创建成功的用户数据 + */ +export async function createUser(userData: CreateUserRequest): Promise { + try { + console.log(`[API] createUser 创建用户:`, userData); + + // 获取认证token + const token = getAuthToken(); + + // 构建请求参数 + const requestData: any = { + email: userData.email, + username: userData.username, + full_name: userData.full_name, + phone: userData.phone, + password: userData.password, + is_superuser: userData.is_superuser, + }; + + // 只有企业管理员才传tenant_id + if (userData.tenant_id) { + requestData.tenant_id = userData.tenant_id; + } + + // 调用SDK API创建系统用户 + const response = await createSystemUserApiV1UsersSystemUsersPost({ + body: requestData, + headers: token ? { + 'Authorization': `Bearer ${token}`, + } : undefined, + }); + + if (response.error) { + // 处理API错误,提取错误信息 + const errorMessage = response.error.message || '创建用户失败'; + console.error('[API] createUser 创建用户失败:', response.error); + throw new Error(errorMessage); + } + + const data = response.data as any; + console.log('[API] createUser 创建用户成功:', data); + + // 返回创建成功的用户数据 + return { + id: data.id, + email: data.email, + username: data.username, + full_name: data.full_name, + phone: data.phone, + is_superuser: data.is_superuser, + tenant_id: data.tenant_id, + is_active: data.is_active, + created_at: data.created_at, + updated_at: data.updated_at, + }; + + } catch (error) { + console.error('[API] createUser 创建用户异常:', error); + + // 如果是已知错误,直接抛出 + if (error instanceof Error) { + throw error; + } + + // 未知错误包装成标准错误格式 + throw new Error('创建用户失败,请稍后重试'); + } +} + +/** + * 验证用户创建数据 + * + * @param userData 用户数据 + * @returns 验证结果 + */ +export function validateUserData(userData: CreateUserRequest): { isValid: boolean; errors: string[] } { + const errors: string[] = []; + + // 邮箱验证 + if (!userData.email) { + errors.push('邮箱不能为空'); + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) { + errors.push('邮箱格式不正确'); + } + + // 用户名验证 + if (!userData.username) { + errors.push('用户名不能为空'); + } else if (userData.username.length < 3) { + errors.push('用户名长度至少3位'); + } + + // 姓名验证 + if (!userData.full_name) { + errors.push('姓名不能为空'); + } + + // 电话验证 + if (!userData.phone) { + errors.push('电话不能为空'); + } else if (!/^1[3-9]\d{9}$/.test(userData.phone)) { + errors.push('请输入正确的手机号码'); + } + + // 密码验证 + if (!userData.password) { + errors.push('密码不能为空'); + } else if (userData.password.length < 6) { + errors.push('密码长度至少6位'); + } + + // 企业管理员需要tenant_id + if (!userData.is_superuser && !userData.tenant_id) { + errors.push('企业管理员必须选择所属企业'); + } + + return { + isValid: errors.length === 0, + errors + }; +} + +/** + * 获取用户详情信息 + * + * @param userId 用户ID + * @returns 用户详情数据 + */ +export async function fetchUserDetails(userId: string): Promise { + try { + console.log(`[API] fetchUserDetails 获取用户详情: ${userId}`); + + // 获取认证token + const token = getAuthToken(); + + // 调用SDK API获取用户详情 + const response = await getSystemUserApiV1UsersSystemUsersUserIdGet({ + path: { + user_id: userId, + }, + headers: token ? { + 'Authorization': `Bearer ${token}`, + } : undefined, + }); + + if (response.error) { + const errorMessage = response.error.message || '获取用户详情失败'; + console.error('[API] fetchUserDetails 获取用户详情失败:', response.error); + throw new Error(errorMessage); + } + + const data = response.data as any; + console.log('[API] fetchUserDetails 获取用户详情成功:', data); + + // 返回用户详情数据 + return { + id: data.id, + tenant_id: data.tenant_id, + email: data.email, + username: data.username, + full_name: data.full_name, + phone: data.phone, + is_active: data.is_active, + status: data.is_active ? 'active' : 'inactive', + is_superuser: data.is_superuser, + is_verified: data.is_verified, + created_at: data.created_at, + updated_at: data.updated_at, + last_login_at: data.last_login_at, + avatar_url: data.avatar_url, + bio: data.bio, + display_name: data.display_name, + department_id: data.department_id, + department_name: data.department_name, + scope: data.scope || 'system', + company_name: data.company_name, + }; + + } catch (error) { + console.error('[API] fetchUserDetails 获取用户详情异常:', error); + + // 如果是已知错误,直接抛出 + if (error instanceof Error) { + throw error; + } + + // 未知错误包装成标准错误格式 + throw new Error('获取用户详情失败,请稍后重试'); + } +} + +/** + * 激活系统用户 + * + * @param userId 用户ID + * @returns 操作结果 + */ +export async function activateUser(userId: string): Promise { + try { + console.log(`[API] activateUser 激活用户: ${userId}`); + + // 获取认证token + const token = getAuthToken(); + + // 调用SDK API激活用户 + const response = await activateSystemUserApiV1UsersSystemUsersUserIdActivatePost({ + path: { + user_id: userId, + }, + headers: token ? { + 'Authorization': `Bearer ${token}`, + } : undefined, + }); + + if (response.error) { + // 处理API错误,提取错误信息 + const errorMessage = response.error.message || '激活用户失败'; + console.error('[API] activateUser 激活用户失败:', response.error); + throw new Error(errorMessage); + } + + console.log('[API] activateUser 激活用户成功:', userId); + + } catch (error) { + console.error('[API] activateUser 激活用户异常:', error); + + // 如果是已知错误,直接抛出 + if (error instanceof Error) { + throw error; + } + + // 未知错误包装成标准错误格式 + throw new Error('激活用户失败,请稍后重试'); + } +} + +/** + * 停用系统用户 + * + * @param userId 用户ID + * @returns 操作结果 + */ +export async function deactivateUser(userId: string): Promise { + try { + console.log(`[API] deactivateUser 停用用户: ${userId}`); + + // 获取认证token + const token = getAuthToken(); + + // 调用SDK API停用用户 + const response = await deactivateSystemUserApiV1UsersSystemUsersUserIdDeactivatePost({ + path: { + user_id: userId, + }, + headers: token ? { + 'Authorization': `Bearer ${token}`, + } : undefined, + }); + + if (response.error) { + // 处理API错误,提取错误信息 + const errorMessage = response.error.message || '停用用户失败'; + console.error('[API] deactivateUser 停用用户失败:', response.error); + throw new Error(errorMessage); + } + + console.log('[API] deactivateUser 停用用户成功:', userId); + + } catch (error) { + console.error('[API] deactivateUser 停用用户异常:', error); + + // 如果是已知错误,直接抛出 + if (error instanceof Error) { + throw error; + } + + // 未知错误包装成标准错误格式 + throw new Error('停用用户失败,请稍后重试'); + } +} + +/** + * 删除系统用户 + * + * @param userId 用户ID + * @returns 操作结果 + */ +export async function deleteUser(userId: string): Promise { + try { + console.log(`[API] deleteUser 删除用户: ${userId}`); + + // 获取认证token + const token = getAuthToken(); + + // 调用SDK API删除用户 + const response = await deleteSystemUserApiV1UsersSystemUsersUserIdDelete({ + path: { + user_id: userId, + }, + headers: token ? { + 'Authorization': `Bearer ${token}`, + } : undefined, + }); + + if (response.error) { + // 处理API错误,提取错误信息 + const errorMessage = response.error.message || '删除用户失败'; + console.error('[API] deleteUser 删除用户失败:', response.error); + throw new Error(errorMessage); + } + + console.log('[API] deleteUser 删除用户成功:', userId); + + } catch (error) { + console.error('[API] deleteUser 删除用户异常:', error); + + // 如果是已知错误,直接抛出 + if (error instanceof Error) { + throw error; + } + + // 未知错误包装成标准错误格式 + throw new Error('删除用户失败,请稍后重试'); + } } \ No newline at end of file diff --git a/src/app/(app)/central-config/tenant/user-management/page.tsx b/src/app/(app)/central-config/tenant/user-management/page.tsx index 3e0dd1c..2f50462 100644 --- a/src/app/(app)/central-config/tenant/user-management/page.tsx +++ b/src/app/(app)/central-config/tenant/user-management/page.tsx @@ -9,13 +9,23 @@ import { useReducer, useEffect, useState, useCallback, useMemo,useRef } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; -import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react'; +import { Eye, Edit, Trash2, UserX, UserCheck } from 'lucide-react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { UserDetailDialog } from './components/UserDetailDialog'; import { AddUserModal } from './components/AddUserModal'; import { EditUserModal } from './components/EditUserModal'; import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination'; -import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi'; +import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState, activateUser, deactivateUser, deleteUser } from './components/userManagementApi'; import { UserManagementHeader } from './components/UserManagementHeader'; import { UserManagementStatsCards } from './components/UserManagementStatsCards'; import { UserFilters } from './types'; @@ -113,6 +123,12 @@ const initialState: UserManagementState = { export default function TenantUserManagementPage() { const [state, dispatch] = useReducer(userManagementReducer, initialState); + // 弹窗状态管理 + const [statusDialogOpen, setStatusDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [actionUser, setActionUser] = useState(null); + const [actionType, setActionType] = useState<'activate' | 'deactivate'>('activate'); + // 搜索字段配置 const searchFields: SearchFieldConfig[] = useMemo(() => [ { @@ -274,10 +290,11 @@ export default function TenantUserManagementPage() {
), @@ -487,18 +504,63 @@ export default function TenantUserManagementPage() { loadUsers({}); }, [loadUsers]); - // 切换用户状态 + // 切换用户状态 - 打开确认弹窗 const handleToggleStatus = (user: User) => { const newStatus = !user.isActive; - const statusText = newStatus ? '激活' : '停用'; - if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return; - toast.info(`${statusText}功能开发中...`); + setActionUser(user); + setActionType(newStatus ? 'activate' : 'deactivate'); + setStatusDialogOpen(true); }; - // 重置密码 - const handleResetPassword = (user: User) => { - if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return; - toast.info('重置密码功能开发中...'); + // 执行状态切换 + const handleStatusConfirm = async () => { + if (!actionUser) return; + + try { + if (actionType === 'activate') { + await activateUser(actionUser.id); + toast.success(`用户 ${actionUser.fullName || actionUser.username} 已激活`); + } else { + await deactivateUser(actionUser.id); + toast.success(`用户 ${actionUser.fullName || actionUser.username} 已停用`); + } + + // 刷新数据 + refreshData(); + } catch (error) { + console.error(`${actionType === 'activate' ? '激活' : '停用'}用户失败:`, error); + const errorMessage = error instanceof Error ? error.message : `${actionType === 'activate' ? '激活' : '停用'}用户失败,请重试`; + toast.error(errorMessage); + } finally { + setStatusDialogOpen(false); + setActionUser(null); + } + }; + + // 删除用户 - 打开确认弹窗 + const handleDeleteUser = (user: User) => { + setActionUser(user); + setDeleteDialogOpen(true); + }; + + // 执行删除用户 + const handleDeleteConfirm = async () => { + if (!actionUser) return; + + try { + await deleteUser(actionUser.id); + toast.success(`用户 ${actionUser.fullName || actionUser.username} 已删除`); + + // 刷新数据 + refreshData(); + } catch (error) { + console.error('删除用户失败:', error); + const errorMessage = error instanceof Error ? error.message : '删除用户失败,请重试'; + toast.error(errorMessage); + } finally { + setDeleteDialogOpen(false); + setActionUser(null); + } }; // 统计数据计算 @@ -589,6 +651,58 @@ export default function TenantUserManagementPage() { user={state.selectedUser} onSuccess={refreshData} /> + + {/* 状态切换确认对话框 */} + + + + + {actionType === 'activate' ? '激活用户' : '停用用户'} + + + 确定要{actionType === 'activate' ? '激活' : '停用'}用户 + + {actionUser?.fullName || actionUser?.username} + + 吗?此操作将影响该用户的登录权限。 + + + + 取消 + + 确认{actionType === 'activate' ? '激活' : '停用'} + + + + + + {/* 删除用户确认对话框 */} + + + + 删除用户 + + 确定要删除用户 + + {actionUser?.fullName || actionUser?.username} + + 吗?此操作不可恢复,该用户的所有数据将被永久删除。 + + + + 取消 + + 确认删除 + + + +
); } \ No newline at end of file diff --git a/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx b/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx index 279a213..6c13c2a 100644 --- a/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/BatchEvaluationPanel.tsx @@ -21,7 +21,7 @@ import { Square } from 'lucide-react'; import { SpatialAnalysisState, SpatialAnalysisAction, AnalysisResult } from './spatialAnalysisReducer'; -import { useState, useEffect } from 'react'; +import { useMemo } from 'react'; import BatchAnalysisManager from './batchAnalysisService'; interface BatchEvaluationPanelProps { @@ -30,11 +30,7 @@ interface BatchEvaluationPanelProps { } export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluationPanelProps) { - const [batchManager, setBatchManager] = useState(null); - - useEffect(() => { - setBatchManager(new BatchAnalysisManager(dispatch)); - }, [dispatch]); + const batchManager = useMemo(() => new BatchAnalysisManager(dispatch), [dispatch]); const totalWeight = Math.round( (state.weightConfig.soil + state.weightConfig.climate + @@ -53,19 +49,10 @@ export default function BatchEvaluationPanel({ state, dispatch }: BatchEvaluatio return; } - if (!batchManager) { - toast.error('批量分析服务未初始化'); - return; - } - try { // 创建批量分析任务 const taskName = `地块适宜性批量分析_${new Date().toLocaleString('zh-CN')}`; - // 固定分析68个地块 - const totalFields = 68; - const blockIds = Array.from({ length: totalFields }, (_, i) => `field-${i + 1}`); - // 开始批量分析 await batchManager.startBatchAnalysis(taskName, enabledFactors, state.weightConfig); diff --git a/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx b/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx index fb38abf..ce2502d 100644 --- a/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/EvaluationModel.tsx @@ -8,12 +8,9 @@ import { toast } from 'sonner'; import { Brain, Settings, - Eye, - EyeOff, CheckCircle2, XCircle, Target, - Droplet, Sun, Mountain, Building, diff --git a/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx b/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx index 8c4f40b..a1e3c98 100644 --- a/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/FactorConfiguration.tsx @@ -11,11 +11,9 @@ import { DialogContent, DialogHeader, DialogTitle, - DialogTrigger, } from '@/components/ui/dialog'; import { Settings, - Sliders, CheckCircle, XCircle, Info @@ -209,7 +207,7 @@ export default function FactorConfiguration({ state, dispatch, isDialog = false
  • • 启用/禁用因子来控制评价维度,禁用的因子不参与计算
  • • 土壤条件、气候条件、地形条件、基础设施为四大评价维度
  • • 可以根据具体需求调整各因子的权重,建议咨询农业专家
  • -
  • • 点击"重置默认"可以恢复到推荐的标准权重配置
  • +
  • • 点击“重置默认”可以恢复到推荐的标准权重配置
  • diff --git a/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx b/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx index 767fd51..357e32b 100644 --- a/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/FactorWeights.tsx @@ -11,7 +11,6 @@ import { SlidersHorizontal, TrendingUp, Target, - Droplet, Sun, Mountain, Building, diff --git a/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx b/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx index ebf035a..6441fca 100644 --- a/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/SpatialDistribution.tsx @@ -10,7 +10,9 @@ interface SpatialDistributionProps { dispatch: React.Dispatch; } -export default function SpatialDistribution({ state, dispatch }: SpatialDistributionProps) { +export default function SpatialDistribution({ state }: SpatialDistributionProps) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { dispatch } = { dispatch: null }; // 保留dispatch参数以保持接口一致性,但当前未使用 const getScoreColor = (score: number) => { if (score >= 80) return 'bg-green-500'; if (score >= 60) return 'bg-yellow-500'; diff --git a/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx b/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx index 899efa1..9ea472b 100644 --- a/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/WeightConfiguration.tsx @@ -148,7 +148,7 @@ export default function WeightConfiguration({ state, dispatch, isDialog = false

    - 权重总和不为100%,建议点击"自动平衡"调整 + 权重总和不为100%,建议点击“自动平衡”调整

    )} diff --git a/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx b/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx index a5cad72..3b8a681 100644 --- a/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx +++ b/src/app/(app)/land-information/suitability/auto/components/batchAnalysisService.tsx @@ -1,10 +1,26 @@ 'use client'; -import { SpatialAnalysisState, SpatialAnalysisAction, LandBlock, AnalysisResult, BatchAnalysisTask, EvaluationFactor } from './spatialAnalysisReducer'; +import { SpatialAnalysisAction, LandBlock, AnalysisResult, BatchAnalysisTask, EvaluationFactor } from './spatialAnalysisReducer'; + +// 因子数据类型定义 +interface FactorData { + soil: unknown; + climate: unknown; + topography: unknown; + infrastructure: unknown; +} + +// 权重配置类型 +interface WeightConfig { + soil: number; + climate: number; + topography: number; + infrastructure: number; +} // 空间分析服务接口 interface SpatialAnalysisService { - analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: any): Promise<{ + analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: WeightConfig): Promise<{ success: boolean; suitabilityIndex: number; result?: AnalysisResult; @@ -14,7 +30,7 @@ interface SpatialAnalysisService { // 模拟空间分析服务 class MockSpatialAnalysisService implements SpatialAnalysisService { - async analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: any): Promise<{ + async analyzeBlock(block: LandBlock, factors: EvaluationFactor[], weights: WeightConfig): Promise<{ success: boolean; suitabilityIndex: number; result?: AnalysisResult; @@ -33,10 +49,10 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { const factorData = await this.readFactorData(block); // 进行加权计算 - const suitabilityIndex = this.calculateWeightedScore(factorData, factors, weights); + const suitabilityIndex = this.calculateWeightedScore(factorData, weights); // 生成详细分析结果 - const result = this.generateAnalysisResult(block, factorData, suitabilityIndex, weights); + const result = this.generateAnalysisResult(block, factorData, suitabilityIndex); return { success: true, @@ -52,7 +68,7 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { } } - private async readFactorData(block: LandBlock): Promise { + private async readFactorData(block: LandBlock): Promise { // 模拟从各种数据源读取因子数据 await new Promise(resolve => setTimeout(resolve, 200)); @@ -120,45 +136,46 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { return scoreMap[irrigationType] || 0.70; } - private calculateWeightedScore(factorData: any, factors: EvaluationFactor[], weights: any): number { + private calculateWeightedScore(factorData: unknown, weights: WeightConfig): number { let totalScore = 0; let totalWeight = 0; // 土壤因子得分计算 const soilWeight = weights.soil || 0.35; - const soilScore = this.calculateSoilScore(factorData.soilFactors, factors); + const soilScore = this.calculateSoilScore((factorData as { soilFactors: unknown }).soilFactors); totalScore += soilScore * soilWeight; totalWeight += soilWeight; // 气候因子得分计算 const climateWeight = weights.climate || 0.30; - const climateScore = this.calculateClimateScore(factorData.climateFactors, factors); + const climateScore = this.calculateClimateScore((factorData as { climateFactors: unknown }).climateFactors); totalScore += climateScore * climateWeight; totalWeight += climateWeight; // 地形因子得分计算 const topographyWeight = weights.topography || 0.20; - const topographyScore = this.calculateTopographyScore(factorData.topographyFactors, factors); + const topographyScore = this.calculateTopographyScore((factorData as { topographyFactors: unknown }).topographyFactors); totalScore += topographyScore * topographyWeight; totalWeight += topographyWeight; // 基础设施因子得分计算 const infrastructureWeight = weights.infrastructure || 0.15; - const infrastructureScore = this.calculateInfrastructureScore(factorData.infrastructureFactors, factors); + const infrastructureScore = this.calculateInfrastructureScore((factorData as { infrastructureFactors: unknown }).infrastructureFactors); totalScore += infrastructureScore * infrastructureWeight; totalWeight += infrastructureWeight; return Math.round(totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0); } - private calculateSoilScore(soilFactors: any, factors: EvaluationFactor[]): number { + private calculateSoilScore(soilFactors: unknown): number { let score = 0; let count = 0; // pH值评分 - if (soilFactors.ph >= 6.0 && soilFactors.ph <= 7.5) { + const soilFactorsTyped = soilFactors as { ph: number; organicMatter: number; nitrogen: number; phosphorus: number; potassium: number }; + if (soilFactorsTyped.ph >= 6.0 && soilFactorsTyped.ph <= 7.5) { score += 90; - } else if (soilFactors.ph >= 5.5 && soilFactors.ph <= 8.0) { + } else if (soilFactorsTyped.ph >= 5.5 && soilFactorsTyped.ph <= 8.0) { score += 70; } else { score += 40; @@ -166,9 +183,9 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { count++; // 有机质评分 - if (soilFactors.organicMatter >= 2.5) { + if (soilFactorsTyped.organicMatter >= 2.5) { score += 90; - } else if (soilFactors.organicMatter >= 1.5) { + } else if (soilFactorsTyped.organicMatter >= 1.5) { score += 70; } else { score += 45; @@ -176,21 +193,22 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { count++; // 养分评分 (NPK) - const npkScore = Math.min(100, (soilFactors.nitrogen + soilFactors.phosphorus + soilFactors.potassium) / 3); + const npkScore = Math.min(100, (soilFactorsTyped.nitrogen + soilFactorsTyped.phosphorus + soilFactorsTyped.potassium) / 3); score += npkScore; count++; return count > 0 ? score / count : 50; } - private calculateClimateScore(climateFactors: any, factors: EvaluationFactor[]): number { + private calculateClimateScore(climateFactors: unknown): number { let score = 0; let count = 0; // 温度适宜性 - if (climateFactors.temperature >= 15 && climateFactors.temperature <= 25) { + const climateFactorsTyped = climateFactors as { temperature: number; rainfall: number; sunlight: number }; + if (climateFactorsTyped.temperature >= 15 && climateFactorsTyped.temperature <= 25) { score += 90; - } else if (climateFactors.temperature >= 10 && climateFactors.temperature <= 30) { + } else if (climateFactorsTyped.temperature >= 10 && climateFactorsTyped.temperature <= 30) { score += 70; } else { score += 50; @@ -198,9 +216,9 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { count++; // 降雨适宜性 - if (climateFactors.rainfall >= 500 && climateFactors.rainfall <= 1000) { + if (climateFactorsTyped.rainfall >= 500 && climateFactorsTyped.rainfall <= 1000) { score += 90; - } else if (climateFactors.rainfall >= 300 && climateFactors.rainfall <= 1500) { + } else if (climateFactorsTyped.rainfall >= 300 && climateFactorsTyped.rainfall <= 1500) { score += 70; } else { score += 45; @@ -208,9 +226,9 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { count++; // 日照充足性 - if (climateFactors.sunlight >= 2000) { + if (climateFactorsTyped.sunlight >= 2000) { score += 90; - } else if (climateFactors.sunlight >= 1600) { + } else if (climateFactorsTyped.sunlight >= 1600) { score += 75; } else { score += 60; @@ -220,16 +238,17 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { return count > 0 ? score / count : 50; } - private calculateTopographyScore(topographyFactors: any, factors: EvaluationFactor[]): number { + private calculateTopographyScore(topographyFactors: unknown): number { let score = 0; let count = 0; // 坡度评分 - if (topographyFactors.slope <= 3) { + const topographyFactorsTyped = topographyFactors as { slope: number; elevation: number; drainageIndex: number }; + if (topographyFactorsTyped.slope <= 3) { score += 95; - } else if (topographyFactors.slope <= 6) { + } else if (topographyFactorsTyped.slope <= 6) { score += 80; - } else if (topographyFactors.slope <= 15) { + } else if (topographyFactorsTyped.slope <= 15) { score += 60; } else { score += 30; @@ -237,11 +256,11 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { count++; // 海拔适宜性 - if (topographyFactors.elevation <= 100) { + if (topographyFactorsTyped.elevation <= 100) { score += 90; - } else if (topographyFactors.elevation <= 300) { + } else if (topographyFactorsTyped.elevation <= 300) { score += 80; - } else if (topographyFactors.elevation <= 600) { + } else if (topographyFactorsTyped.elevation <= 600) { score += 65; } else { score += 45; @@ -249,39 +268,51 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { count++; // 排水条件 - score += topographyFactors.drainageIndex * 100; + score += topographyFactorsTyped.drainageIndex * 100; count++; return count > 0 ? score / count : 50; } - private calculateInfrastructureScore(infrastructureFactors: any, factors: EvaluationFactor[]): number { + private calculateInfrastructureScore(infrastructureFactors: unknown): number { let score = 0; let count = 0; // 灌溉条件 - score += infrastructureFactors.irrigationScore * 100; + const infrastructureFactorsTyped = infrastructureFactors as { + irrigationScore: number; + accessibility: number; + powerSupply: boolean; + waterSupply: boolean + }; + score += infrastructureFactorsTyped.irrigationScore * 100; count++; // 交通便利性 - score += infrastructureFactors.accessibility * 100; + score += infrastructureFactorsTyped.accessibility * 100; count++; // 基础设施完善度 const infrastructureScore = - (infrastructureFactors.powerSupply ? 50 : 0) + - (infrastructureFactors.waterSupply ? 50 : 0); + (infrastructureFactorsTyped.powerSupply ? 50 : 0) + + (infrastructureFactorsTyped.waterSupply ? 50 : 0); score += infrastructureScore; count++; return count > 0 ? score / count : 50; } - private generateAnalysisResult(block: LandBlock, factorData: any, suitabilityIndex: number, weights: any): AnalysisResult { - const soilScore = this.calculateSoilScore(factorData.soilFactors, []); - const climateScore = this.calculateClimateScore(factorData.climateFactors, []); - const topographyScore = this.calculateTopographyScore(factorData.topographyFactors, []); - const infrastructureScore = this.calculateInfrastructureScore(factorData.infrastructureFactors, []); + private generateAnalysisResult(block: LandBlock, factorData: unknown, suitabilityIndex: number): AnalysisResult { + const factorDataTyped = factorData as { + soilFactors: unknown; + climateFactors: unknown; + topographyFactors: unknown; + infrastructureFactors: unknown; + }; + const soilScore = this.calculateSoilScore(factorDataTyped.soilFactors); + const climateScore = this.calculateClimateScore(factorDataTyped.climateFactors); + const topographyScore = this.calculateTopographyScore(factorDataTyped.topographyFactors); + const infrastructureScore = this.calculateInfrastructureScore(factorDataTyped.infrastructureFactors); // 生成作物推荐 const recommendedCrops = this.generateCropRecommendations(block, factorData, suitabilityIndex); @@ -308,12 +339,12 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { }; } - private generateCropRecommendations(block: LandBlock, factorData: any, suitabilityIndex: number): any[] { + private generateCropRecommendations(block: LandBlock, factorData: unknown, suitabilityIndex: number): unknown[] { // 简化的作物推荐逻辑 const crops = [ { name: '小麦', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 10 - 5) }, { name: '玉米', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 15 - 5) }, - { name: '水稻', suitabilityScore: factorData.climateFactors.rainfall > 800 ? Math.min(95, suitabilityIndex + Math.random() * 10) : Math.max(40, suitabilityIndex - 20) }, + { name: '水稻', suitabilityScore: (factorData as { climateFactors: { rainfall: number } }).climateFactors.rainfall > 800 ? Math.min(95, suitabilityIndex + Math.random() * 10) : Math.max(40, suitabilityIndex - 20) }, { name: '大豆', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 8 - 4) }, { name: '棉花', suitabilityScore: Math.min(95, suitabilityIndex + Math.random() * 12 - 6) } ]; @@ -339,37 +370,44 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { .slice(0, 3); } - private generateRiskFactors(block: LandBlock, factorData: any, suitabilityIndex: number): string[] { + private generateRiskFactors(block: LandBlock, factorData: unknown, suitabilityIndex: number): string[] { const risks = []; if (suitabilityIndex < 40) { risks.push('综合条件较差,种植风险较高'); } - if (factorData.soilFactors.ph < 6.0 || factorData.soilFactors.ph > 7.5) { + const factorDataTyped = factorData as { + soilFactors: { ph: number; organicMatter: number }; + topographyFactors: { slope: number }; + infrastructureFactors: { irrigationScore: number }; + climateFactors: { rainfall: number } + }; + + if (factorDataTyped.soilFactors.ph < 6.0 || factorDataTyped.soilFactors.ph > 7.5) { risks.push('土壤pH值偏离理想范围'); } - if (factorData.soilFactors.organicMatter < 1.5) { + if (factorDataTyped.soilFactors.organicMatter < 1.5) { risks.push('土壤有机质含量偏低'); } - if (factorData.topographyFactors.slope > 8) { + if (factorDataTyped.topographyFactors.slope > 8) { risks.push('坡度较大,存在水土流失风险'); } - if (factorData.infrastructureFactors.irrigationScore < 0.5) { + if (factorDataTyped.infrastructureFactors.irrigationScore < 0.5) { risks.push('灌溉条件不足,依赖自然降水'); } - if (factorData.climateFactors.rainfall < 400) { + if (factorDataTyped.climateFactors.rainfall < 400) { risks.push('降雨量偏少,干旱风险较高'); } return risks.length > 0 ? risks : ['无明显风险因素']; } - private generateImprovementSuggestions(block: LandBlock, factorData: any, soilScore: number, climateScore: number, topographyScore: number, infrastructureScore: number): string[] { + private generateImprovementSuggestions(block: LandBlock, factorData: unknown, soilScore: number, climateScore: number, topographyScore: number, infrastructureScore: number): string[] { const suggestions = []; if (soilScore < 70) { @@ -392,7 +430,7 @@ class MockSpatialAnalysisService implements SpatialAnalysisService { suggestions.push('完善灌溉和排水系统'); } - if (factorData.infrastructureFactors.accessibility < 0.6) { + if ((factorData as { infrastructureFactors: { accessibility: number } }).infrastructureFactors.accessibility < 0.6) { suggestions.push('改善田间道路条件'); } @@ -415,7 +453,7 @@ export class BatchAnalysisManager { async startBatchAnalysis( taskName: string, factors: EvaluationFactor[], - weightConfig: any + weightConfig: WeightConfig ): Promise { if (this.isRunning) { throw new Error('已有分析任务正在运行'); @@ -455,9 +493,6 @@ export class BatchAnalysisManager { // 循环处理所有地块 const results: AnalysisResult[] = []; - let highSuitability = 0; - let mediumSuitability = 0; - let lowSuitability = 0; for (let i = 0; i < totalFields; i++) { if (!this.isRunning) break; // 检查是否被取消 @@ -501,16 +536,6 @@ export class BatchAnalysisManager { if (analysisResult.success && analysisResult.result) { results.push(analysisResult.result); - - // 统计适宜性等级 - const score = analysisResult.suitabilityIndex; - if (score >= 80) { - highSuitability++; - } else if (score >= 60) { - mediumSuitability++; - } else { - lowSuitability++; - } } // 更新任务统计 diff --git a/src/app/(app)/land-information/suitability/auto/page.tsx b/src/app/(app)/land-information/suitability/auto/page.tsx index 11cd592..fdae187 100644 --- a/src/app/(app)/land-information/suitability/auto/page.tsx +++ b/src/app/(app)/land-information/suitability/auto/page.tsx @@ -7,7 +7,6 @@ import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Database, - Play, Download, Eye, AlertTriangle, @@ -68,7 +67,7 @@ export default function BatchEvaluationPage() { const totalWeight = factorWeights.reduce((sum, f) => sum + f.weight, 0); // 模拟从空间分析服务读取地块因子数据 - const fetchFieldFactorsFromSpatialService = (fieldId: string): EvaluationFactor[] => { + const fetchFieldFactorsFromSpatialService = (): EvaluationFactor[] => { return [ { id: 'ph', @@ -214,7 +213,7 @@ export default function BatchEvaluationPage() { const fieldId = `field-${i + 1}`; const fieldName = `地块${String.fromCharCode(65 + (i % 26))}${Math.floor(i / 26) + 1}`; - const factors = fetchFieldFactorsFromSpatialService(fieldId); + const factors = fetchFieldFactorsFromSpatialService(); const scoredFactors = factors.map(factor => ({ ...factor, score: calculateFactorScore(factor.value, factor.optimalRange) diff --git a/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx b/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx index 065728a..de20805 100644 --- a/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx +++ b/src/app/(app)/land-information/suitability/multiFactor/components/MultiFactorEvaluation.tsx @@ -5,59 +5,40 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Progress } from '@/components/ui/progress'; import { Slider } from '@/components/ui/slider'; -import { Textarea } from '@/components/ui/textarea'; import { - Leaf, TrendingUp, Award, AlertTriangle, CheckCircle2, Play, Settings, - Download, - Eye, - Calculator, - Database, - RefreshCw, Zap, - Target, - Droplet, Cloud, - Sun, - ThermometerSun, BookOpen, - Beaker, - Info, - BarChart3, - Filter + Beaker } from 'lucide-react'; import { toast } from 'sonner'; import { - EvaluationFactor, SuitabilityResult, FactorWeight, - BatchProgress, getGradeColor, getScoreColor, - getSuitabilityLevelColor, - formatDate, - MOCK_FIELDS + formatDate } from './multiFactorTypes'; import { MultiFactorService } from './multiFactorService'; import { - matchCropsForField, cropKnowledgeBase } from './cropKnowledgeBase'; @@ -65,16 +46,7 @@ export function MultiFactorEvaluation() { const [selectedField, setSelectedField] = useState('field-1'); const [showWeightConfig, setShowWeightConfig] = useState(false); const [showKnowledgeBase, setShowKnowledgeBase] = useState(false); - const [batchProgress, setBatchProgress] = useState({ - total: 0, - processed: 0, - highSuitability: 0, - mediumSuitability: 0, - lowSuitability: 0, - currentField: '', - }); - const [isBatchRunning, setIsBatchRunning] = useState(false); - const [batchAnalysisResults, setBatchAnalysisResults] = useState([]); + const [batchAnalysisResults, setBatchAnalysisResults] = useState([]); // 评价因子权重配置 const [factorWeights, setFactorWeights] = useState([ @@ -99,43 +71,6 @@ export function MultiFactorEvaluation() { evaluationResults[0]; // 批量分析处理函数 - const handleRunBatchAnalysis = async () => { - const validation = MultiFactorService.validateWeights(factorWeights); - if (!validation.isValid) { - toast.error(`权重总和必须为100%才能进行批量分析(当前:${validation.totalWeight}%)`); - return; - } - - setIsBatchRunning(true); - setBatchProgress({ - total: 68, - processed: 0, - highSuitability: 0, - mediumSuitability: 0, - lowSuitability: 0, - currentField: '', - }); - setBatchAnalysisResults([]); - - toast.success('开始批量分析,正在读取地块数据...'); - - try { - const results = await MultiFactorService.runBatchAnalysis( - factorWeights, - (progress) => { - setBatchProgress(progress); - } - ); - - setBatchAnalysisResults(results); - setIsBatchRunning(false); - toast.success(`批量分析完成!已为${results.length}个地块生成适宜性评价结果`); - } catch (error) { - setIsBatchRunning(false); - toast.error('批量分析失败'); - } - }; - const handleUpdateWeight = (id: string, newWeight: number) => { setFactorWeights(prev => MultiFactorService.updateWeight(prev, id, newWeight) @@ -174,15 +109,6 @@ export function MultiFactorEvaluation() { } }; - const exportResults = () => { - const resultsToExport = batchAnalysisResults.length > 0 ? batchAnalysisResults : evaluationResults; - toast.success('正在导出评价结果...'); - // 模拟导出功能 - setTimeout(() => { - toast.success(`已导出${resultsToExport.length}个地块的评价结果`); - }, 2000); - }; - return (
    diff --git a/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts b/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts index 28efa0b..ee5d5b0 100644 --- a/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts +++ b/src/app/(app)/land-information/suitability/multiFactor/components/cropKnowledgeBase.ts @@ -3,7 +3,7 @@ * 提供作物-环境适配数据和分析功能 */ -import { Crop, CropRecommendation, FieldFactors, MatchDetail, RiskFactor } from './multiFactorTypes'; +import { Crop, CropRecommendation, FieldFactors, MatchDetail } from './multiFactorTypes'; // 作物知识库数据 export const cropKnowledgeBase: Crop[] = [ diff --git a/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx b/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx index 695eae6..9488ffb 100644 --- a/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx +++ b/src/app/(app)/land-information/suitability/recommend/components/CropRecommendations.tsx @@ -3,10 +3,68 @@ import { Card } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Leaf, AlertTriangle, ThermometerSun, Cloud, Sun } from 'lucide-react'; -import { CropRecommendationState, SuitabilityResult } from './cropRecommendReducer'; +import { SuitabilityResult } from './cropRecommendReducer'; + +type RangeRequirement = { optimal: [number, number]; acceptable: [number, number]; }; +type SoilFactorKey = 'ph' | 'organicMatter' | 'soilDepth' | 'nitrogen' | 'phosphorus' | 'potassium' | 'drainage'; +type SoilRequirementMap = Record; + +type ClimateRequirement = { + temperature: RangeRequirement; + rainfall: RangeRequirement; + sunlight: RangeRequirement; +}; + +type YieldRange = { + high: [number, number]; + medium: [number, number]; + low: [number, number]; +}; + +type RiskSeverity = 'low' | 'medium' | 'high'; + +interface CropRiskFactor { + id: string; + name: string; + condition: string; + severity: RiskSeverity; + suggestion: string; +} + +interface CropKnowledgeEntry { + id: string; + cropName: string; + category: string; + description: string; + growthCycle: { days: number; seasons: string[] }; + soilRequirements: SoilRequirementMap; + climateRequirements: ClimateRequirement; + expectedYield: YieldRange; + riskFactors: CropRiskFactor[]; +} + +type MatchStatus = '??' | '???' | '??'; + +interface MatchDetail { + factor: string; + value: number; + score: number; + status: MatchStatus; +} + +interface RecommendationResult { + crop: CropKnowledgeEntry; + matchScore: number; + suitabilityLevel: '????' | '??' | '????' | '???'; + matchDetails: MatchDetail[]; + applicableRisks: CropRiskFactor[]; + expectedYield: [number, number]; +} + +type FieldFactors = Record; // 模拟作物知识库数据 -const cropKnowledgeBase = [ +const cropKnowledgeBase: CropKnowledgeEntry[] = [ { id: 'wheat', cropName: '小麦', @@ -40,14 +98,14 @@ const cropKnowledgeBase = [ id: 'wheat-rust', name: '锈病风险', condition: '湿度过高、温度适宜', - severity: 'medium' as const, + severity: 'medium', suggestion: '选择抗病品种,合理密植,及时防治' }, { id: 'wheat-drought', name: '干旱风险', condition: '降雨量不足400mm', - severity: 'high' as const, + severity: 'high', suggestion: '加强灌溉设施建设,选择抗旱品种' } ] @@ -85,14 +143,14 @@ const cropKnowledgeBase = [ id: 'corn-borer', name: '玉米螟', condition: '温度适宜、湿度适中', - severity: 'medium' as const, + severity: 'medium', suggestion: '生物防治与化学防治结合,适时播种' }, { id: 'corn-drought', name: '花期干旱', condition: '开花期降雨不足', - severity: 'high' as const, + severity: 'high', suggestion: '保证花期灌溉,选择耐旱品种' } ] @@ -130,7 +188,7 @@ const cropKnowledgeBase = [ id: 'soybean-disease', name: '病害风险', condition: '高温高湿环境', - severity: 'medium' as const, + severity: 'medium', suggestion: '选择抗病品种,合理轮作,加强田间管理' } ] @@ -138,11 +196,10 @@ const cropKnowledgeBase = [ ]; interface CropRecommendationsProps { - state: CropRecommendationState; currentResult: SuitabilityResult; } -export function CropRecommendations({ state, currentResult }: CropRecommendationsProps) { +export function CropRecommendations({ currentResult }: CropRecommendationsProps) { // 匹配作物推荐 const matchCropsForField = (fieldFactors: any) => { return cropKnowledgeBase.map(crop => { diff --git a/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx b/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx index 12e7e1f..95542ba 100644 --- a/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx +++ b/src/app/(app)/land-information/suitability/recommend/components/cropRecommendReducer.tsx @@ -1,7 +1,5 @@ 'use client'; -import { useReducer } from 'react'; -import { toast } from 'sonner'; export interface EvaluationFactor { id: string; diff --git a/src/app/(app)/land-information/suitability/recommend/page.tsx b/src/app/(app)/land-information/suitability/recommend/page.tsx index 21b6c4e..ad621e6 100644 --- a/src/app/(app)/land-information/suitability/recommend/page.tsx +++ b/src/app/(app)/land-information/suitability/recommend/page.tsx @@ -7,8 +7,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { BookOpen, Target } from 'lucide-react'; import { cropRecommendReducer, - initialState, - SuitabilityResult + initialState } from './components/cropRecommendReducer'; import { FieldEnvironmentOverview } from './components/FieldEnvironmentOverview'; import { CropRecommendations } from './components/CropRecommendations'; @@ -117,7 +116,7 @@ export default function CropPage() {
    {/* 智能作物推荐 */} - + {/* 知识库对话框 */}