fix: call real api

This commit is contained in:
zaunist committed 2025-04-07 21:38:08 +08:00
1 parent a2fdf78121
commit 2536a3aa88
17 files changed
+1176 -640

No files matched your search

+2 -1
View File
@@ -15,7 +15,8 @@
"**/*"
],
"ios": {
"supportsTablet": true
"supportsTablet": true,
"bundleIdentifier": "uk.mdzz.xugou"
},
"android": {
"adaptiveIcon": {
+292 -104
View File
@@ -9,27 +9,28 @@
"version": "0.0.1",
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.3.3",
"@react-navigation/native": "^7.0.19",
"@react-navigation/native-stack": "^7.3.3",
"async-storage": "^0.1.0",
"axios": "^1.8.4",
"expo": "~52.0.42",
"expo-dev-client": "~5.0.17",
"expo-clipboard": "~7.0.1",
"expo-dev-client": "~5.0.19",
"expo-localization": "^16.0.1",
"expo-status-bar": "~2.0.1",
"i18next": "^24.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.4.1",
"react-native": "0.76.8",
"react-native": "0.76.9",
"react-native-chart-kit": "^6.12.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-reanimated": "^3.17.1",
"react-native-safe-area-context": "^5.3.0",
"react-native-screens": "^4.10.0",
"react-native-svg": "^15.11.2",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"victory-native": "^41.16.1",
"zustand": "^5.0.3"
@@ -3226,21 +3227,21 @@
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz",
"integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==",
"version": "1.23.1",
"resolved": "https://registry.npmmirror.com/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz",
"integrity": "sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.65 <1.0"
"react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.76.8.tgz",
"integrity": "sha512-vQQi3kabQpj21Iohy2ou3/laCRE5hlW4r122ZivqCIN/UMwr5vT2/fTgPOBQoJ5X3YhZBe58BmifIstZutm0Ew==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
"integrity": "sha512-pN0Ws5xsjWOZ8P37efh0jqHHQmq+oNGKT4AyAoKRpxBDDDmlAmpaYjer9Qz7PpDKF+IUyRjF/+rBsM50a8JcUg==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -3361,13 +3362,13 @@
}
},
"node_modules/@react-native/community-cli-plugin": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.8.tgz",
"integrity": "sha512-3rs6YehAVEootGpzchmj1ln2BCSTnIDTKqAYykUggEwpplg+CNTiBrvRrnjmlLXk3nOlIE8KOw4zRCd3PpI+bg==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.9.tgz",
"integrity": "sha512-08jx8ixCjjd4jNQwNpP8yqrjrDctN2qvPPlf6ebz1OJQk8e1sbUl3wVn1zhhMvWrYcaraDnatPb5uCPq+dn3NQ==",
"license": "MIT",
"dependencies": {
"@react-native/dev-middleware": "0.76.8",
"@react-native/metro-babel-transformer": "0.76.8",
"@react-native/dev-middleware": "0.76.9",
"@react-native/metro-babel-transformer": "0.76.9",
"chalk": "^4.0.0",
"execa": "^5.1.1",
"invariant": "^2.2.4",
@@ -3390,6 +3391,47 @@
}
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/debugger-frontend/-/debugger-frontend-0.76.9.tgz",
"integrity": "sha512-0Ru72Bm066xmxFuOXhhvrryxvb57uI79yDSFf+hxRpktkC98NMuRenlJhslMrbJ6WjCu1vOe/9UjWNYyxXTRTA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/dev-middleware/-/dev-middleware-0.76.9.tgz",
"integrity": "sha512-xkd3C3dRcmZLjFTEAOvC14q3apMLouIvJViCZY/p1EfCMrNND31dgE1dYrLTiI045WAWMt5bD15i6f7dE2/QWA==",
"license": "MIT",
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.76.9",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
"debug": "^2.2.0",
"invariant": "^2.2.4",
"nullthrows": "^1.1.1",
"open": "^7.0.3",
"selfsigned": "^2.4.1",
"serve-static": "^1.13.1",
"ws": "^6.2.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz",
@@ -3446,6 +3488,12 @@
"node": ">=6"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
"node_modules/@react-native/community-cli-plugin/node_modules/npm-run-path": {
"version": "4.0.1",
"resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz",
@@ -3491,6 +3539,15 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/@react-native/community-cli-plugin/node_modules/ws": {
"version": "6.2.3",
"resolved": "https://registry.npmmirror.com/ws/-/ws-6.2.3.tgz",
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"license": "MIT",
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"node_modules/@react-native/debugger-frontend": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/debugger-frontend/-/debugger-frontend-0.76.8.tgz",
@@ -3548,31 +3605,31 @@
}
},
"node_modules/@react-native/gradle-plugin": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/gradle-plugin/-/gradle-plugin-0.76.8.tgz",
"integrity": "sha512-rgocQRydHS8IkcyUs1VFhz+Q0OjL3QYI5/A7pMLRz6nEW8GAmoFnXqwujtPHivVrXbuCLIzGBKnsl2hsZsOopg==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/gradle-plugin/-/gradle-plugin-0.76.9.tgz",
"integrity": "sha512-uGzp3dL4GfNDz+jOb8Nik1Vrfq1LHm0zESizrGhHACFiFlUSflVAnWuUAjlZlz5XfLhzGVvunG4Vdrpw8CD2ng==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/js-polyfills": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/js-polyfills/-/js-polyfills-0.76.8.tgz",
"integrity": "sha512-bZzPjpDQaWU+F//N/WZJfaglYgYga4oXl9rzXyKOJP7KcebkOKJbAsG1mo7RCOVIbHYQCKUvQXIpV3IQOWNOEg==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/js-polyfills/-/js-polyfills-0.76.9.tgz",
"integrity": "sha512-s6z6m8cK4SMjIX1hm8LT187aQ6//ujLrjzDBogqDCYXRbfjbAYovw5as/v2a2rhUIyJbS3UjokZm3W0H+Oh/RQ==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/metro-babel-transformer": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.8.tgz",
"integrity": "sha512-tEjNJo3Wwiig1OmI7H3cLOrAkAxT9lBqs1k+oanRm64o0Cay2ACH0PdXrk45sxENU3iTfxXeQ7mmY1u2XVYDpw==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.9.tgz",
"integrity": "sha512-HGq11347UHNiO/NvVbAO35hQCmH8YZRs7in7nVq7SL99pnpZK4WXwLdAXmSuwz5uYqOuwnKYDlpadz8fkE94Mg==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
"@react-native/babel-preset": "0.76.8",
"@react-native/babel-preset": "0.76.9",
"hermes-parser": "0.23.1",
"nullthrows": "^1.1.1"
},
@@ -3583,6 +3640,120 @@
"@babel/core": "*"
}
},
"node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.9.tgz",
"integrity": "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ==",
"license": "MIT",
"dependencies": {
"@react-native/codegen": "0.76.9"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/babel-preset/-/babel-preset-0.76.9.tgz",
"integrity": "sha512-TbSeCplCM6WhL3hR2MjC/E1a9cRnMLz7i767T7mP90oWkklEjyPxWl+0GGoVGnJ8FC/jLUupg/HvREKjjif6lw==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-proposal-export-default-from": "^7.24.7",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-export-default-from": "^7.24.7",
"@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-syntax-optional-chaining": "^7.8.3",
"@babel/plugin-transform-arrow-functions": "^7.24.7",
"@babel/plugin-transform-async-generator-functions": "^7.25.4",
"@babel/plugin-transform-async-to-generator": "^7.24.7",
"@babel/plugin-transform-block-scoping": "^7.25.0",
"@babel/plugin-transform-class-properties": "^7.25.4",
"@babel/plugin-transform-classes": "^7.25.4",
"@babel/plugin-transform-computed-properties": "^7.24.7",
"@babel/plugin-transform-destructuring": "^7.24.8",
"@babel/plugin-transform-flow-strip-types": "^7.25.2",
"@babel/plugin-transform-for-of": "^7.24.7",
"@babel/plugin-transform-function-name": "^7.25.1",
"@babel/plugin-transform-literals": "^7.25.2",
"@babel/plugin-transform-logical-assignment-operators": "^7.24.7",
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7",
"@babel/plugin-transform-numeric-separator": "^7.24.7",
"@babel/plugin-transform-object-rest-spread": "^7.24.7",
"@babel/plugin-transform-optional-catch-binding": "^7.24.7",
"@babel/plugin-transform-optional-chaining": "^7.24.8",
"@babel/plugin-transform-parameters": "^7.24.7",
"@babel/plugin-transform-private-methods": "^7.24.7",
"@babel/plugin-transform-private-property-in-object": "^7.24.7",
"@babel/plugin-transform-react-display-name": "^7.24.7",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/plugin-transform-react-jsx-self": "^7.24.7",
"@babel/plugin-transform-react-jsx-source": "^7.24.7",
"@babel/plugin-transform-regenerator": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/plugin-transform-shorthand-properties": "^7.24.7",
"@babel/plugin-transform-spread": "^7.24.7",
"@babel/plugin-transform-sticky-regex": "^7.24.7",
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
"@react-native/babel-plugin-codegen": "0.76.9",
"babel-plugin-syntax-hermes-parser": "^0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/codegen": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/codegen/-/codegen-0.76.9.tgz",
"integrity": "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"glob": "^7.1.1",
"hermes-parser": "0.23.1",
"invariant": "^2.2.4",
"jscodeshift": "^0.14.0",
"mkdirp": "^0.5.1",
"nullthrows": "^1.1.1",
"yargs": "^17.6.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@babel/preset-env": "^7.1.6"
}
},
"node_modules/@react-native/metro-babel-transformer/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@react-native/normalize-colors": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/normalize-colors/-/normalize-colors-0.76.8.tgz",
@@ -3590,9 +3761,9 @@
"license": "MIT"
},
"node_modules/@react-native/virtualized-lists": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/@react-native/virtualized-lists/-/virtualized-lists-0.76.8.tgz",
"integrity": "sha512-OsB+LoEFH80wL+qhQrNpJR3O3oNDmyZf0go7/MQiayZbT8IkD8aPdHmK4QIVoIwAbFWXw7RkcFdubhSWwi0wAQ==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/virtualized-lists/-/virtualized-lists-0.76.9.tgz",
"integrity": "sha512-2neUfZKuqMK2LzfS8NyOWOyWUJOWgDym5fUph6fN9qF+LNPjAvnc4Zr9+o+59qjNu/yXwQgVMWNU4+8WJuPVWw==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4",
@@ -6128,6 +6299,17 @@
"react-native": "*"
}
},
"node_modules/expo-clipboard": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/expo-clipboard/-/expo-clipboard-7.0.1.tgz",
"integrity": "sha512-rqYk0+WoqitPcPKxmMxSpLonX1E5Ije3LBYfnYMbH3xU5Gr8EAH9QnOWOi4BgahUPvcot6nbFEnx+DqARrmxKQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-constants": {
"version": "17.0.8",
"resolved": "https://registry.npmmirror.com/expo-constants/-/expo-constants-17.0.8.tgz",
@@ -6143,15 +6325,15 @@
}
},
"node_modules/expo-dev-client": {
"version": "5.0.17",
"resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-5.0.17.tgz",
"integrity": "sha512-Vgj3DmhChWJpQFGtW4eFMScrrOKkUAdMO/E/3ygp9hoIrrt4FSqiRRctk3eLc6hUzcdR0CaobJtygna0Ho73rQ==",
"version": "5.0.20",
"resolved": "https://registry.npmmirror.com/expo-dev-client/-/expo-dev-client-5.0.20.tgz",
"integrity": "sha512-bLNkHdU7V3I4UefgJbJnIDUBUL0LxIal/xYEx9BbgDd3B7wgQKY//+BpPIxBOKCQ22lkyiHY8y9tLhO903sAgg==",
"license": "MIT",
"dependencies": {
"expo-dev-launcher": "5.0.32",
"expo-dev-menu": "6.0.23",
"expo-dev-launcher": "5.0.35",
"expo-dev-menu": "6.0.25",
"expo-dev-menu-interface": "1.9.3",
"expo-manifests": "~0.15.7",
"expo-manifests": "~0.15.8",
"expo-updates-interface": "~1.0.0"
},
"peerDependencies": {
@@ -6159,36 +6341,24 @@
}
},
"node_modules/expo-dev-launcher": {
"version": "5.0.32",
"resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-5.0.32.tgz",
"integrity": "sha512-Inb8DAx5vRPd/8okx0Auu1jdUJW4f3gEzJwJKbkRl7kL4VTjFBFnkRpt4VEKTb3qAtm8BsqeLhEedW0PAVJmfA==",
"version": "5.0.35",
"resolved": "https://registry.npmmirror.com/expo-dev-launcher/-/expo-dev-launcher-5.0.35.tgz",
"integrity": "sha512-hEQr0ZREnUMxZ6wtQgfK1lzYnbb0zar3HqYZhmANzXmE6UEPbQ4GByLzhpfz/d+xxdBVQZsrHdtiV28KPG2sog==",
"license": "MIT",
"dependencies": {
"ajv": "8.11.0",
"expo-dev-menu": "6.0.22",
"expo-manifests": "~0.15.7",
"expo-dev-menu": "6.0.25",
"expo-manifests": "~0.15.8",
"resolve-from": "^5.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-launcher/node_modules/expo-dev-menu": {
"version": "6.0.22",
"resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-6.0.22.tgz",
"integrity": "sha512-3NuBgGerUzwLLrz5Y7w/fPDUZ5GfREO9ntvU/b/srPWEOmtUYMIvKTuLX+SbMJBZdQGilfF7xxo93WxIYSp/rw==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "1.9.3"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu": {
"version": "6.0.23",
"resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-6.0.23.tgz",
"integrity": "sha512-bO1FhbrQrSVGzAm36xV0SC+vAbhXFswx+w8iAF8osm+zniPNhRv1PQH/GWeet/YVorZoxyiS0juwLqP49rirEw==",
"version": "6.0.25",
"resolved": "https://registry.npmmirror.com/expo-dev-menu/-/expo-dev-menu-6.0.25.tgz",
"integrity": "sha512-K2m4z/I+CPWbMtHlDzU68lHaQs52De0v5gbsjAmA5ig8FrYh4MKZvPxSVANaiKENzgmtglu8qaFh7ua9Gt2TfA==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "1.9.3"
@@ -6262,12 +6432,12 @@
}
},
"node_modules/expo-manifests": {
"version": "0.15.7",
"resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-0.15.7.tgz",
"integrity": "sha512-IVzLcPamzUi4Br96xw6JPHaa1vjYupUnMqYyV1Mtd9VQojS5hJsf5VcVzbAMZE/cFGzWLZ1oJa6QXxYjN39Uww==",
"version": "0.15.8",
"resolved": "https://registry.npmmirror.com/expo-manifests/-/expo-manifests-0.15.8.tgz",
"integrity": "sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw==",
"license": "MIT",
"dependencies": {
"@expo/config": "~10.0.10",
"@expo/config": "~10.0.11",
"expo-json-utils": "~0.14.0"
},
"peerDependencies": {
@@ -7094,9 +7264,9 @@
}
},
"node_modules/image-size": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/image-size/-/image-size-1.2.0.tgz",
"integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==",
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
@@ -9934,19 +10104,19 @@
"license": "MIT"
},
"node_modules/react-native": {
"version": "0.76.8",
"resolved": "https://registry.npmmirror.com/react-native/-/react-native-0.76.8.tgz",
"integrity": "sha512-sVoAd/cDBdVoLjNz8Lp5DAg4l6Iag0l5AiChN2J/NGhM//PW6+MIW50TI8G1iZz4rquAgWtcCHwEp5P5FvMyDA==",
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/react-native/-/react-native-0.76.9.tgz",
"integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==",
"license": "MIT",
"dependencies": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native/assets-registry": "0.76.8",
"@react-native/codegen": "0.76.8",
"@react-native/community-cli-plugin": "0.76.8",
"@react-native/gradle-plugin": "0.76.8",
"@react-native/js-polyfills": "0.76.8",
"@react-native/normalize-colors": "0.76.8",
"@react-native/virtualized-lists": "0.76.8",
"@react-native/assets-registry": "0.76.9",
"@react-native/codegen": "0.76.9",
"@react-native/community-cli-plugin": "0.76.9",
"@react-native/gradle-plugin": "0.76.9",
"@react-native/js-polyfills": "0.76.9",
"@react-native/normalize-colors": "0.76.9",
"@react-native/virtualized-lists": "0.76.9",
"abort-controller": "^3.0.0",
"anser": "^1.4.9",
"ansi-regex": "^5.0.0",
@@ -10011,34 +10181,25 @@
}
},
"node_modules/react-native-gesture-handler": {
"version": "2.25.0",
"resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-2.25.0.tgz",
"integrity": "sha512-NPjJi6mislXxvjxQPU9IYwBjb1Uejp8GvAbE1Lhh+xMIMEvmgAvVIp5cz1P+xAbV6uYcRRArm278+tEInGOqWg==",
"version": "2.20.2",
"resolved": "https://registry.npmmirror.com/react-native-gesture-handler/-/react-native-gesture-handler-2.20.2.tgz",
"integrity": "sha512-HqzFpFczV4qCnwKlvSAvpzEXisL+Z9fsR08YV5LfJDkzuArMhBu2sOoSPUF/K62PCoAb+ObGlTC83TKHfUd0vg==",
"license": "MIT",
"dependencies": {
"@egjs/hammerjs": "^2.0.17",
"hoist-non-react-statics": "^3.3.0",
"invariant": "^2.2.4"
"invariant": "^2.2.4",
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-is-edge-to-edge": {
"version": "1.1.6",
"resolved": "https://registry.npmmirror.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.6.tgz",
"integrity": "sha512-1pHnFTlBahins6UAajXUqeCOHew9l9C2C8tErnpGC3IyLJzvxD+TpYAixnCbrVS52f7+NvMttbiSI290XfwN0w==",
"license": "MIT",
"peerDependencies": {
"react": ">=18.2.0",
"react-native": ">=0.73.0"
}
},
"node_modules/react-native-reanimated": {
"version": "3.17.1",
"resolved": "https://registry.npmmirror.com/react-native-reanimated/-/react-native-reanimated-3.17.1.tgz",
"integrity": "sha512-ECzLhLxMKLifv34a8799/MHqIazQZV9fLMNSMdixXQlzX71RyL3/ah3cz/h3ERoyhJAYRC2ySLLZho6pXSqMFQ==",
"version": "3.16.7",
"resolved": "https://registry.npmmirror.com/react-native-reanimated/-/react-native-reanimated-3.16.7.tgz",
"integrity": "sha512-qoUUQOwE1pHlmQ9cXTJ2MX9FQ9eHllopCLiWOkDkp6CER95ZWeXhJCP4cSm6AD4jigL5jHcZf/SkWrg8ttZUsw==",
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
@@ -10051,8 +10212,7 @@
"@babel/plugin-transform-unicode-regex": "^7.0.0-0",
"@babel/preset-typescript": "^7.16.7",
"convert-source-map": "^2.0.0",
"invariant": "^2.2.4",
"react-native-is-edge-to-edge": "1.1.6"
"invariant": "^2.2.4"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0",
@@ -10061,9 +10221,9 @@
}
},
"node_modules/react-native-safe-area-context": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/react-native-safe-area-context/-/react-native-safe-area-context-5.3.0.tgz",
"integrity": "sha512-glV9bwuozTjf/JDBIBm+ITnukHNaUT3nucgdeADwjtHsfEN3RL5UO6nq99vvdWv5j/O9yCZBvFncM1BBQ+UvpQ==",
"version": "4.12.0",
"resolved": "https://registry.npmmirror.com/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
"license": "MIT",
"peerDependencies": {
"react": "*",
@@ -10071,9 +10231,9 @@
}
},
"node_modules/react-native-screens": {
"version": "4.10.0",
"resolved": "https://registry.npmmirror.com/react-native-screens/-/react-native-screens-4.10.0.tgz",
"integrity": "sha512-Tw21NGuXm3PbiUGtZd0AnXirUixaAbPXDjNR0baBH7/WJDaDTTELLcQ7QRXuqAWbmr/EVCrKj1348ei1KFIr8A==",
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/react-native-screens/-/react-native-screens-4.4.0.tgz",
"integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==",
"license": "MIT",
"dependencies": {
"react-freeze": "^1.0.0",
@@ -10085,9 +10245,9 @@
}
},
"node_modules/react-native-svg": {
"version": "15.11.2",
"resolved": "https://registry.npmmirror.com/react-native-svg/-/react-native-svg-15.11.2.tgz",
"integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==",
"version": "15.8.0",
"resolved": "https://registry.npmmirror.com/react-native-svg/-/react-native-svg-15.8.0.tgz",
"integrity": "sha512-KHJzKpgOjwj1qeZzsBjxNdoIgv2zNCO9fVcoq2TEhTRsVV5DGTZ9JzUZwybd7q4giT/H3RdtqC3u44dWdO0Ffw==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
@@ -10131,6 +10291,34 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-native/node_modules/@react-native/codegen": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/codegen/-/codegen-0.76.9.tgz",
"integrity": "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.25.3",
"glob": "^7.1.1",
"hermes-parser": "0.23.1",
"invariant": "^2.2.4",
"jscodeshift": "^0.14.0",
"mkdirp": "^0.5.1",
"nullthrows": "^1.1.1",
"yargs": "^17.6.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@babel/preset-env": "^7.1.6"
}
},
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
"version": "0.76.9",
"resolved": "https://registry.npmmirror.com/@react-native/normalize-colors/-/normalize-colors-0.76.9.tgz",
"integrity": "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw==",
"license": "MIT"
},
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
"version": "0.23.1",
"resolved": "https://registry.npmmirror.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz",
+19 -10
View File
@@ -10,35 +10,44 @@
},
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.3.3",
"@react-navigation/native": "^7.0.19",
"@react-navigation/native-stack": "^7.3.3",
"async-storage": "^0.1.0",
"axios": "^1.8.4",
"expo": "~52.0.42",
"expo-dev-client": "~5.0.17",
"expo-dev-client": "~5.0.19",
"expo-localization": "^16.0.1",
"expo-status-bar": "~2.0.1",
"i18next": "^24.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.4.1",
"react-native": "0.76.8",
"react-native": "0.76.9",
"react-native-chart-kit": "^6.12.0",
"react-native-gesture-handler": "^2.25.0",
"react-native-reanimated": "^3.17.1",
"react-native-safe-area-context": "^5.3.0",
"react-native-screens": "^4.10.0",
"react-native-svg": "^15.11.2",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-web": "~0.19.13",
"victory-native": "^41.16.1",
"zustand": "^5.0.3"
"zustand": "^5.0.3",
"expo-clipboard": "~7.0.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~18.3.12",
"typescript": "^5.3.3"
},
"private": true
"private": true,
"expo": {
"doctor": {
"reactNativeDirectoryCheck": {
"exclude": ["i18next", "react-native-chart-kit"],
"listUnknownPackages": false
}
}
}
}
+15 -19
View File
@@ -3,32 +3,28 @@ import { API_ENDPOINTS } from '../config/api';
// 客户端类型
export interface Agent {
id: string;
id: string | number;
name: string;
hostname?: string;
ip_address?: string;
status: 'online' | 'offline' | 'unknown' | 'active'; // 添加'active'以兼容Dashboard
lastSeen?: string;
last_seen?: string;
os?: string;
cpu?: {
model: string;
cores: number;
usage: number;
};
memory?: {
total: number;
used: number;
free: number;
usage: number;
};
disk?: {
total: number;
used: number;
free: number;
usage: number;
};
operating_system?: string;
version?: string;
created_at: string;
updated_at: string;
updated_at?: string;
// 添加资源监控相关属性
cpu_usage?: number;
memory_usage?: number;
disk_usage?: number;
memory_total?: number;
memory_used?: number;
disk_total?: number;
disk_used?: number;
network_rx?: number;
network_tx?: number;
}
// 创建客户端参数
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

+31
View File
@@ -86,6 +86,37 @@ export default {
status_down: 'Down',
status_pending: 'Pending',
statusHistory: 'Status History',
selectStatusCode: 'Select Status Code',
enterName: 'Enter monitor name',
enterUrl: 'Enter URL or host to monitor',
headerName: 'Name',
headerValue: 'Value',
addHeader: 'Add Header',
bodyPlaceholder: 'Enter request body content, e.g. JSON data',
create: 'Create Monitor',
createError: 'Creation Error',
fieldsRequired: 'Please fill in all required fields',
createFailed: 'Failed to create monitor',
// Status code descriptions
statusCodes: {
ok: 'OK',
created: 'Created',
noContent: 'No Content',
successful: 'Successful',
movedPermanently: 'Moved Permanently',
found: 'Found',
redirection: 'Redirection',
badRequest: 'Bad Request',
unauthorized: 'Unauthorized',
forbidden: 'Forbidden',
notFound: 'Not Found',
clientError: 'Client Error',
serverError: 'Internal Server Error',
badGateway: 'Bad Gateway',
serviceUnavailable: 'Service Unavailable',
gatewayTimeout: 'Gateway Timeout',
serverErrors: 'Server Error'
},
},
// Agents
+31
View File
@@ -86,6 +86,37 @@ export default {
status_down: '异常',
status_pending: '等待中',
statusHistory: '状态历史',
selectStatusCode: '选择状态码',
enterName: '输入监控名称',
enterUrl: '输入要监控的 URL 或主机',
headerName: '名称',
headerValue: '值',
addHeader: '添加请求头',
bodyPlaceholder: '输入请求体内容,例如 JSON 数据',
create: '创建监控',
createError: '创建错误',
fieldsRequired: '请填写所有必填字段',
createFailed: '创建监控失败',
// 状态码描述
statusCodes: {
ok: '成功',
created: '已创建',
noContent: '无内容',
successful: '成功',
movedPermanently: '永久移动',
found: '已找到',
redirection: '重定向',
badRequest: '错误请求',
unauthorized: '未授权',
forbidden: '禁止访问',
notFound: '未找到',
clientError: '客户端错误',
serverError: '服务器内部错误',
badGateway: '错误网关',
serviceUnavailable: '服务不可用',
gatewayTimeout: '网关超时',
serverErrors: '服务器错误'
},
},
// 客户端
+1 -154
View File
@@ -7,8 +7,7 @@ import {
TouchableOpacity,
ActivityIndicator,
RefreshControl,
Alert,
Dimensions
Alert
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation, useRoute } from '@react-navigation/native';
@@ -91,20 +90,8 @@ const agentService = {
return Promise.resolve(history.reverse());
},
restartAgent: (id: string): Promise<{ success: boolean }> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true });
}, 2000);
});
},
deleteAgent: (id: string): Promise<{ success: boolean }> => {
return Promise.resolve({ success: true });
},
generateToken: (id: string): Promise<{ success: boolean, token: string }> => {
return Promise.resolve({ success: true, token: 'agent_' + Math.random().toString(36).substr(2, 16) });
}
};
@@ -121,9 +108,6 @@ const AgentDetailScreen: React.FC = () => {
const [resourceHistory, setResourceHistory] = useState<ResourceHistory[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [restarting, setRestarting] = useState(false);
const [showToken, setShowToken] = useState(false);
const [token, setToken] = useState<string | null>(null);
// 加载客户端详情和资源历史
const fetchData = async () => {
@@ -148,57 +132,6 @@ const AgentDetailScreen: React.FC = () => {
setRefreshing(false);
};
// 重启客户端
const handleRestart = () => {
Alert.alert(
t('agents.restartTitle', '重启客户端'),
t('agents.restartConfirmation', '您确定要重启此客户端吗?这可能会导致短暂的服务中断。'),
[
{
text: t('common.cancel', '取消'),
style: 'cancel'
},
{
text: t('agents.restart', '重启'),
style: 'destructive',
onPress: async () => {
setRestarting(true);
try {
const result = await agentService.restartAgent(agentId);
if (result.success) {
Alert.alert(
t('agents.restartSuccessTitle', '重启命令已发送'),
t('agents.restartSuccessMessage', '重启命令已成功发送到客户端。客户端将在几分钟内重启。')
);
}
} catch (error) {
console.error('重启客户端失败', error);
Alert.alert(t('common.error', '错误'), t('agents.restartFailed', '重启客户端失败'));
} finally {
setRestarting(false);
}
}
}
]
);
};
// 生成新令牌
const handleGenerateToken = async () => {
try {
const result = await agentService.generateToken(agentId);
if (result.success) {
setToken(result.token);
setShowToken(true);
}
} catch (error) {
console.error('生成令牌失败', error);
Alert.alert(t('common.error', '错误'), t('agents.generateTokenFailed', '生成新令牌失败'));
}
};
// 删除客户端
const handleDelete = () => {
Alert.alert(
@@ -229,11 +162,6 @@ const AgentDetailScreen: React.FC = () => {
);
};
// 编辑客户端
const handleEdit = () => {
navigation.navigate('EditAgent', { agentId });
};
// 获取状态颜色
const getStatusColor = (status: string) => {
return status === 'active' ? '#30c85e' : '#888';
@@ -470,58 +398,8 @@ const AgentDetailScreen: React.FC = () => {
</View>
)}
{/* 令牌信息 */}
{showToken && token && (
<View style={styles.card}>
<Text style={styles.cardTitle}>{t('agents.token', '客户端令牌')}</Text>
<View style={styles.tokenContainer}>
<Text style={styles.tokenValue}>{token}</Text>
<Text style={styles.tokenHelper}>
{t('agents.tokenHelper', '请保管好此令牌,它只会显示一次。您可以使用此令牌重新连接客户端。')}
</Text>
</View>
</View>
)}
{/* 操作按钮 */}
<View style={styles.actionsCard}>
<TouchableOpacity
style={[styles.actionButton, styles.secondaryButton]}
onPress={handleEdit}
>
<Ionicons name="create-outline" size={18} color="#0066cc" />
<Text style={[styles.actionButtonText, styles.secondaryButtonText]}>
{t('common.edit', '编辑')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.primaryButton]}
onPress={handleGenerateToken}
>
<Ionicons name="key-outline" size={18} color="#fff" />
<Text style={styles.actionButtonText}>
{t('agents.generateToken', '生成新令牌')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.warningButton]}
onPress={handleRestart}
disabled={restarting}
>
{restarting ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<>
<Ionicons name="refresh" size={18} color="#fff" />
<Text style={styles.actionButtonText}>
{t('agents.restart', '重启客户端')}
</Text>
</>
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.dangerButton]}
onPress={handleDelete}
@@ -679,23 +557,6 @@ const styles = StyleSheet.create({
fontWeight: '500',
color: '#333',
},
tokenContainer: {
backgroundColor: '#f9f9f9',
padding: 16,
borderRadius: 8,
},
tokenValue: {
fontSize: 16,
fontWeight: '500',
color: '#333',
marginBottom: 8,
textAlign: 'center',
},
tokenHelper: {
fontSize: 12,
color: '#666',
fontStyle: 'italic',
},
actionsCard: {
backgroundColor: 'white',
borderRadius: 12,
@@ -718,20 +579,6 @@ const styles = StyleSheet.create({
color: 'white',
marginLeft: 8,
},
primaryButton: {
backgroundColor: '#0066cc',
},
secondaryButton: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#0066cc',
},
secondaryButtonText: {
color: '#0066cc',
},
warningButton: {
backgroundColor: '#ffb224',
},
dangerButton: {
backgroundColor: '#f76363',
},
+1 -8
View File
@@ -130,7 +130,7 @@ const AgentsScreen: React.FC = () => {
// 实际项目中这里需要调用API删除
console.log('删除客户端:', id);
const numericId = parseInt(id, 10);
const updatedAgents = agents.filter(a => a.id !== numericId);
const updatedAgents = agents.filter(a => String(a.id) !== id);
setAgents(updatedAgents);
applyFilter(updatedAgents, filter);
}
@@ -244,13 +244,6 @@ const AgentsScreen: React.FC = () => {
</Text>
<View style={styles.actionsContainer}>
<TouchableOpacity
style={styles.actionButton}
onPress={() => navigation.navigate('EditAgent', { agentId: item.id })}
>
<Ionicons name="create-outline" size={16} color="#666" />
<Text style={styles.actionText}>{t('common.edit', '编辑')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
+338 -110
View File
@@ -1,115 +1,262 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
ScrollView,
ActivityIndicator,
Alert
Alert,
Linking
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import * as Clipboard from 'expo-clipboard';
import { API_BASE_URL } from '../../config/api';
import agentService from '../../api/agents';
const CreateAgentScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const [name, setName] = useState('');
const [hostname, setHostname] = useState('');
const [ipAddress, setIpAddress] = useState('');
const [isLoading, setIsLoading] = useState(false);
// 状态
const [isLoading, setIsLoading] = useState(true);
const [token, setToken] = useState('');
const [serverUrl, setServerUrl] = useState(API_BASE_URL);
const [selectedPlatform, setSelectedPlatform] = useState('linux');
const [selectedArch, setSelectedArch] = useState('amd64');
const [copied, setCopied] = useState(false);
const handleSubmit = async () => {
if (!name.trim()) {
Alert.alert(
t('agents.createError', '创建错误'),
t('agents.nameRequired', '请输入客户端名称')
);
return;
}
// 页面加载时自动获取注册token
useEffect(() => {
generateToken();
}, []);
// 生成注册token
const generateToken = async () => {
setIsLoading(true);
// 模拟创建客户端的 API 调用
setTimeout(() => {
try {
const tokenResult = await agentService.generateRegistrationToken();
if (!tokenResult) {
setIsLoading(false);
Alert.alert(
t('agents.tokenError', 'Token生成错误'),
t('agents.tokenFailed', '无法生成客户端注册Token')
);
return;
}
setToken(tokenResult);
setIsLoading(false);
} catch (error) {
console.error('生成Token失败', error);
setIsLoading(false);
// 生成随机令牌
const token = Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
Alert.alert(
t('agents.createSuccess', '创建成功'),
t('agents.tokenGenerated', '客户端已创建,请复制以下令牌: ') + token,
[
{
text: 'OK',
onPress: () => navigation.goBack()
}
]
t('common.error', '错误'),
t('agents.tokenFailed', '无法生成客户端注册Token')
);
}, 1000);
}
};
// 复制命令到剪贴板
const copyToClipboard = async (text: string) => {
try {
await Clipboard.setStringAsync(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('复制到剪贴板失败', error);
}
};
// 获取下载URL
const getDownloadUrl = () => {
if (!selectedPlatform || !selectedArch) return '';
const extension = selectedPlatform === 'windows' ? '.exe' : '';
return `https://dl.xugou.mdzz.uk/latest/xugou-agent-${selectedPlatform}-${selectedArch}${extension}`;
};
// 获取下载命令
const getDownloadCommand = () => {
if (!selectedPlatform || !selectedArch || selectedPlatform === 'windows') return '';
return `curl -sSL ${getDownloadUrl()} -o xugou-agent && chmod +x xugou-agent`;
};
// 获取运行命令
const getRunCommand = () => {
return `./xugou-agent start --server ${serverUrl} --token ${token} --interval 60`;
};
// 加载中状态
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#0066cc" />
<Text style={styles.loadingText}>{t('agents.generatingToken', '正在生成注册Token...')}</Text>
</View>
);
}
// 显示安装指南
return (
<ScrollView style={styles.container}>
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
>
<View style={styles.formContainer}>
<Text style={styles.label}>{t('agents.name', '名称')} *</Text>
<TextInput
style={styles.input}
value={name}
onChangeText={setName}
placeholder={t('agents.enterName', '输入客户端名称')}
/>
<Text style={styles.label}>{t('agents.hostname', '主机名')}</Text>
<TextInput
style={styles.input}
value={hostname}
onChangeText={setHostname}
placeholder={t('agents.enterHostname', '输入主机名')}
/>
<Text style={styles.label}>{t('agents.ipAddress', 'IP地址')}</Text>
<TextInput
style={styles.input}
value={ipAddress}
onChangeText={setIpAddress}
placeholder={t('agents.enterIp', '输入IP地址')}
keyboardType="numeric"
/>
<View style={styles.infoContainer}>
<Text style={styles.infoTitle}>{t('agents.instructions', '安装说明')}</Text>
<Text style={styles.infoText}>
{t('agents.installInstructions', '1. 创建客户端并获取令牌')}
</Text>
<Text style={styles.infoText}>
{t('agents.installInstructions2', '2. 在您的服务器上运行安装脚本')}
</Text>
<Text style={styles.infoText}>
{t('agents.installInstructions3', '3. 使用令牌完成客户端配置')}
</Text>
<Text style={styles.infoText}>
{t('agents.installInstructions4', '4. 客户端将自动连接到监控服务')}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('agents.addNewAgent', '添加新客户端')}</Text>
<Text style={styles.description}>
{t('agents.followGuide', '请按照以下步骤安装客户端')}
</Text>
</View>
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color="#fff" size="small" />
) : (
<Text style={styles.submitButtonText}>
{t('agents.create', '创建客户端')}
{/* 服务器地址部分 */}
<View style={styles.section}>
<Text style={styles.label}>{t('agents.serverAddress', '服务器地址')}</Text>
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{serverUrl}</Text>
</View>
<TouchableOpacity
style={styles.copyButton}
onPress={() => copyToClipboard(serverUrl)}
>
<Text style={styles.copyButtonText}>
{copied ? t('common.copied', '已复制') : t('common.copy', '复制')}
</Text>
)}
</TouchableOpacity>
</TouchableOpacity>
</View>
{/* 客户端Token部分 */}
<View style={styles.section}>
<Text style={styles.label}>{t('agents.registrationToken', '注册令牌')}</Text>
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{token}</Text>
</View>
<TouchableOpacity
style={styles.copyButton}
onPress={() => copyToClipboard(token)}
>
<Text style={styles.copyButtonText}>
{copied ? t('common.copied', '已复制') : t('common.copy', '复制')}
</Text>
</TouchableOpacity>
</View>
{/* 选择平台部分 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>{t('agents.installGuide', '安装指南')}</Text>
<Text style={styles.label}>{t('agents.selectPlatform', '1. 选择操作系统')}</Text>
<View style={styles.optionsRow}>
<TouchableOpacity
style={[styles.optionButton, selectedPlatform === 'linux' && styles.selectedOption]}
onPress={() => setSelectedPlatform('linux')}
>
<Text style={[styles.optionText, selectedPlatform === 'linux' && styles.selectedOptionText]}>Linux</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.optionButton, selectedPlatform === 'darwin' && styles.selectedOption]}
onPress={() => setSelectedPlatform('darwin')}
>
<Text style={[styles.optionText, selectedPlatform === 'darwin' && styles.selectedOptionText]}>macOS</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.optionButton, selectedPlatform === 'windows' && styles.selectedOption]}
onPress={() => setSelectedPlatform('windows')}
>
<Text style={[styles.optionText, selectedPlatform === 'windows' && styles.selectedOptionText]}>Windows</Text>
</TouchableOpacity>
</View>
{/* 选择架构部分 */}
<Text style={styles.label}>{t('agents.selectArch', '2. 选择系统架构')}</Text>
<View style={styles.optionsRow}>
<TouchableOpacity
style={[styles.optionButton, selectedArch === 'amd64' && styles.selectedOption]}
onPress={() => setSelectedArch('amd64')}
>
<Text style={[styles.optionText, selectedArch === 'amd64' && styles.selectedOptionText]}>
{selectedPlatform === 'darwin' ? 'AMD64 (Intel)' : 'AMD64 (x86_64)'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.optionButton, selectedArch === 'arm64' && styles.selectedOption]}
onPress={() => setSelectedArch('arm64')}
>
<Text style={[styles.optionText, selectedArch === 'arm64' && styles.selectedOptionText]}>
{selectedPlatform === 'darwin' ? 'ARM64 (Apple Silicon)' : 'ARM64'}
</Text>
</TouchableOpacity>
</View>
</View>
{/* 下载命令部分 */}
{selectedPlatform === 'windows' ? (
<View style={styles.section}>
<Text style={styles.label}>{t('agents.downloadInstaller', '3. 下载安装文件')}</Text>
<TouchableOpacity
style={styles.downloadButton}
onPress={() => Linking.openURL(getDownloadUrl())}
>
<Text style={styles.downloadButtonText}>
{t('agents.downloadWindows', '下载 Windows 安装程序')}
</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.section}>
<Text style={styles.label}>{t('agents.downloadRun', '3. 下载并安装')}</Text>
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{getDownloadCommand()}</Text>
</View>
<TouchableOpacity
style={styles.copyButton}
onPress={() => copyToClipboard(getDownloadCommand())}
>
<Text style={styles.copyButtonText}>
{copied ? t('common.copied', '已复制') : t('common.copy', '复制')}
</Text>
</TouchableOpacity>
</View>
)}
{/* 运行客户端部分 */}
<View style={styles.section}>
<Text style={styles.label}>{t('agents.runAgent', '4. 运行客户端')}</Text>
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{getRunCommand()}</Text>
</View>
<TouchableOpacity
style={styles.copyButton}
onPress={() => copyToClipboard(getRunCommand())}
>
<Text style={styles.copyButtonText}>
{copied ? t('common.copied', '已复制') : t('common.copy', '复制')}
</Text>
</TouchableOpacity>
</View>
{/* 返回按钮 */}
<View style={styles.buttonRow}>
<TouchableOpacity
style={styles.secondaryButton}
onPress={() => navigation.goBack()}
>
<Text style={styles.secondaryButtonText}>{t('agents.returnToList', '返回客户端列表')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.resetButton}
onPress={generateToken}
>
<Text style={styles.resetButtonText}>{t('agents.generateNewToken', '生成新Token')}</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
);
@@ -120,6 +267,20 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#f5f5f5',
},
contentContainer: {
paddingBottom: 100,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
loadingText: {
marginTop: 16,
fontSize: 16,
color: '#333',
},
formContainer: {
padding: 16,
},
@@ -129,44 +290,111 @@ const styles = StyleSheet.create({
marginBottom: 8,
marginTop: 16,
},
input: {
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 16,
section: {
marginBottom: 20,
},
infoContainer: {
backgroundColor: '#e6f3ff',
borderRadius: 8,
padding: 16,
marginTop: 24,
marginBottom: 8,
},
infoTitle: {
fontSize: 16,
sectionTitle: {
fontSize: 20,
fontWeight: '600',
marginBottom: 8,
color: '#0066cc',
},
infoText: {
fontSize: 14,
color: '#333',
marginBottom: 6,
description: {
fontSize: 16,
color: '#555',
marginBottom: 16,
},
submitButton: {
codeBlock: {
backgroundColor: '#f0f0f0',
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: '#ddd',
},
codeText: {
fontFamily: 'monospace',
fontSize: 14,
},
copyButton: {
backgroundColor: '#e6e6e6',
borderRadius: 8,
padding: 8,
alignItems: 'center',
marginTop: 8,
alignSelf: 'flex-end',
},
copyButtonText: {
color: '#333',
fontSize: 14,
fontWeight: '500',
},
optionsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
},
optionButton: {
flex: 1,
backgroundColor: '#e6e6e6',
borderRadius: 8,
padding: 10,
alignItems: 'center',
marginHorizontal: 4,
},
selectedOption: {
backgroundColor: '#0066cc',
},
optionText: {
color: '#333',
fontSize: 14,
fontWeight: '500',
},
selectedOptionText: {
color: '#fff',
},
downloadButton: {
backgroundColor: '#0066cc',
borderRadius: 8,
padding: 16,
padding: 12,
alignItems: 'center',
marginTop: 24,
marginTop: 8,
},
submitButtonText: {
downloadButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
fontWeight: '500',
},
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginTop: 30,
marginBottom: 32,
},
secondaryButton: {
backgroundColor: '#e6e6e6',
borderRadius: 8,
padding: 12,
alignItems: 'center',
flex: 1,
marginRight: 8,
},
secondaryButtonText: {
color: '#333',
fontSize: 14,
fontWeight: '500',
},
resetButton: {
backgroundColor: '#0066cc',
borderRadius: 8,
padding: 12,
alignItems: 'center',
flex: 1,
marginLeft: 8,
},
resetButtonText: {
color: '#fff',
fontSize: 14,
fontWeight: '500',
},
});
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
View,
Text,
@@ -7,20 +7,144 @@ import {
TouchableOpacity,
ScrollView,
ActivityIndicator,
Alert
Alert,
Modal,
FlatList
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { useNavigation } from '@react-navigation/native';
import { useNavigation, CommonActions } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';
import monitorService from '../../api/monitors';
// 请求头键值对类型
interface Header {
key: string;
value: string;
}
// 状态码选项类型
interface StatusCodeOption {
label: string;
value: string;
}
const CreateMonitorScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const navigation = useNavigation<any>();
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [method, setMethod] = useState<'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD'>('GET');
const [interval, setInterval] = useState('60');
const [monitorType, setMonitorType] = useState('http');
const [timeoutValue, setTimeoutValue] = useState('30');
const [expectedStatus, setExpectedStatus] = useState('200');
const [headers, setHeaders] = useState<Header[]>([{ key: '', value: '' }]);
const [body, setBody] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [shouldNavigate, setShouldNavigate] = useState(false);
const [statusModalVisible, setStatusModalVisible] = useState(false);
// HTTP方法选项
const httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD'] as const;
// 状态码选项,包括范围选择
const statusCodeOptions: StatusCodeOption[] = [
{ label: `200 - ${t('monitors.statusCodes.ok', 'OK')}`, value: '200' },
{ label: `201 - ${t('monitors.statusCodes.created', 'Created')}`, value: '201' },
{ label: `204 - ${t('monitors.statusCodes.noContent', 'No Content')}`, value: '204' },
{ label: `2xx - ${t('monitors.statusCodes.successful', 'Successful')}`, value: '2xx' },
{ label: `301 - ${t('monitors.statusCodes.movedPermanently', 'Moved Permanently')}`, value: '301' },
{ label: `302 - ${t('monitors.statusCodes.found', 'Found')}`, value: '302' },
{ label: `3xx - ${t('monitors.statusCodes.redirection', 'Redirection')}`, value: '3xx' },
{ label: `400 - ${t('monitors.statusCodes.badRequest', 'Bad Request')}`, value: '400' },
{ label: `401 - ${t('monitors.statusCodes.unauthorized', 'Unauthorized')}`, value: '401' },
{ label: `403 - ${t('monitors.statusCodes.forbidden', 'Forbidden')}`, value: '403' },
{ label: `404 - ${t('monitors.statusCodes.notFound', 'Not Found')}`, value: '404' },
{ label: `4xx - ${t('monitors.statusCodes.clientError', 'Client Error')}`, value: '4xx' },
{ label: `500 - ${t('monitors.statusCodes.serverError', 'Internal Server Error')}`, value: '500' },
{ label: `502 - ${t('monitors.statusCodes.badGateway', 'Bad Gateway')}`, value: '502' },
{ label: `503 - ${t('monitors.statusCodes.serviceUnavailable', 'Service Unavailable')}`, value: '503' },
{ label: `504 - ${t('monitors.statusCodes.gatewayTimeout', 'Gateway Timeout')}`, value: '504' },
{ label: `5xx - ${t('monitors.statusCodes.serverErrors', 'Server Error')}`, value: '5xx' },
];
// 将请求头键值对转换为JSON对象
const headersToJson = () => {
const result: Record<string, string> = {};
headers.forEach(({ key, value }) => {
if (key.trim()) {
result[key.trim()] = value;
}
});
return result;
};
// 添加新的请求头行
const addHeader = () => {
setHeaders([...headers, { key: '', value: '' }]);
};
// 删除请求头行
const removeHeader = (index: number) => {
if (headers.length > 1) {
const newHeaders = [...headers];
newHeaders.splice(index, 1);
setHeaders(newHeaders);
}
};
// 更新请求头键值对
const updateHeader = (index: number, field: keyof Header, value: string) => {
const newHeaders = [...headers];
newHeaders[index][field] = value;
// 如果最后一行有输入内容,自动添加新行
if (index === headers.length - 1 && (newHeaders[index].key || newHeaders[index].value)) {
newHeaders.push({ key: '', value: '' });
}
setHeaders(newHeaders);
};
// 获取当前选择的状态码标签
const getSelectedStatusCodeLabel = () => {
const selected = statusCodeOptions.find(option => option.value === expectedStatus);
return selected ? selected.label : expectedStatus;
};
// 处理状态码,将范围状态码(如2xx)转换为前缀数字(如2)以便与后端兼容
const processStatusCode = (statusCode: string) => {
// 检查是否为范围状态码格式(如2xx、3xx)
if (/^\d+xx$/.test(statusCode)) {
// 仅返回第一个数字
return statusCode.charAt(0);
}
return statusCode;
};
// 使用 useEffect 处理导航
useEffect(() => {
if (shouldNavigate) {
console.log('尝试使用多种方式导航到监控列表页面');
// 方式1: 基本导航
navigation.navigate('Monitors');
// 方式2: 使用CommonActions重置导航
const timerId = setTimeout(() => {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Monitors' }],
})
);
}, 100);
return () => clearTimeout(timerId);
}
}, [shouldNavigate, navigation]);
const handleSubmit = async () => {
if (!name.trim() || !url.trim()) {
@@ -33,24 +157,53 @@ const CreateMonitorScreen: React.FC = () => {
setIsLoading(true);
// 模拟创建监控的 API 调用
setTimeout(() => {
try {
// 调用真实的API服务创建监控
const result = await monitorService.createMonitor({
name,
url,
method,
interval: Number(interval),
timeout: Number(timeoutValue),
expectedStatus: processStatusCode(expectedStatus),
headers: headersToJson(),
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
type: 'http'
});
setIsLoading(false);
if (result.success) {
console.log('创建成功,准备跳转到监控列表页面');
// 通过状态触发导航
setShouldNavigate(true);
} else {
console.error('创建失败:', result.message);
// 在Web中Alert可能不显示,使用console.error记录错误
Alert.alert(
t('monitors.createError', '创建错误'),
result.message || t('monitors.createFailed', '创建监控失败')
);
}
} catch (error) {
setIsLoading(false);
console.error('创建监控失败', error);
// 在Web中Alert可能不显示,使用console.error记录错误
Alert.alert(
t('monitors.createSuccess', '创建成功'),
t('monitors.monitorCreated', '监控已成功创建'),
[
{
text: 'OK',
onPress: () => navigation.goBack()
}
]
t('monitors.createError', '创建错误'),
t('monitors.createFailed', '创建监控失败')
);
}, 1000);
}
};
// 判断是否显示请求体输入框
const showBodyField = ['POST', 'PUT', 'PATCH'].includes(method);
return (
<ScrollView style={styles.container}>
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
>
<View style={styles.formContainer}>
<Text style={styles.label}>{t('monitors.name', '名称')} *</Text>
<TextInput
@@ -68,52 +221,27 @@ const CreateMonitorScreen: React.FC = () => {
placeholder={t('monitors.enterUrl', '输入要监控的 URL 或主机')}
/>
<Text style={styles.label}>{t('monitors.type', '监控类型')}</Text>
<View style={styles.typeContainer}>
<TouchableOpacity
style={[
styles.typeButton,
monitorType === 'http' && styles.typeButtonActive
]}
onPress={() => setMonitorType('http')}
>
<Text style={[
styles.typeButtonText,
monitorType === 'http' && styles.typeButtonTextActive
]}>
HTTP
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.typeButton,
monitorType === 'tcp' && styles.typeButtonActive
]}
onPress={() => setMonitorType('tcp')}
>
<Text style={[
styles.typeButtonText,
monitorType === 'tcp' && styles.typeButtonTextActive
]}>
TCP
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.typeButton,
monitorType === 'ping' && styles.typeButtonActive
]}
onPress={() => setMonitorType('ping')}
>
<Text style={[
styles.typeButtonText,
monitorType === 'ping' && styles.typeButtonTextActive
]}>
PING
</Text>
</TouchableOpacity>
<Text style={styles.label}>{t('monitors.method', '请求方法')} *</Text>
<View style={styles.methodContainer}>
{httpMethods.map((m) => (
<TouchableOpacity
key={m}
style={[
styles.methodButton,
method === m && styles.methodButtonActive
]}
onPress={() => setMethod(m)}
>
<Text
style={[
styles.methodButtonText,
method === m && styles.methodButtonTextActive
]}
>
{m}
</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.label}>{t('monitors.interval', '检查间隔 (秒)')}</Text>
@@ -124,6 +252,68 @@ const CreateMonitorScreen: React.FC = () => {
keyboardType="numeric"
/>
<Text style={styles.label}>{t('monitors.timeout', '超时 (秒)')}</Text>
<TextInput
style={styles.input}
value={timeoutValue}
onChangeText={setTimeoutValue}
keyboardType="numeric"
/>
<Text style={styles.label}>{t('monitors.expectedStatus', '预期状态码')} *</Text>
<TouchableOpacity
style={styles.dropdown}
onPress={() => setStatusModalVisible(true)}
>
<Text style={styles.dropdownText}>
{getSelectedStatusCodeLabel()}
</Text>
<Ionicons name="chevron-down" size={20} color="#666" />
</TouchableOpacity>
<Text style={styles.label}>{t('monitors.headers', '请求头')}</Text>
<View style={styles.headerContainer}>
{headers.map((header, index) => (
<View key={index} style={styles.headerRow}>
<TextInput
style={[styles.input, styles.headerKeyInput]}
value={header.key}
onChangeText={(value) => updateHeader(index, 'key', value)}
placeholder={t('monitors.headerName', '名称')}
/>
<TextInput
style={[styles.input, styles.headerValueInput]}
value={header.value}
onChangeText={(value) => updateHeader(index, 'value', value)}
placeholder={t('monitors.headerValue', '值')}
/>
<TouchableOpacity
style={styles.headerRemoveButton}
onPress={() => removeHeader(index)}
>
<Ionicons name="trash-outline" size={20} color="#f76363" />
</TouchableOpacity>
</View>
))}
<TouchableOpacity style={styles.addHeaderButton} onPress={addHeader}>
<Ionicons name="add-circle-outline" size={20} color="#0066cc" />
<Text style={styles.addHeaderText}>{t('monitors.addHeader', '添加请求头')}</Text>
</TouchableOpacity>
</View>
{showBodyField && (
<>
<Text style={styles.label}>{t('monitors.body', '请求体')}</Text>
<TextInput
style={[styles.input, styles.bodyInput]}
value={body}
onChangeText={setBody}
placeholder={t('monitors.bodyPlaceholder', '输入请求体内容,例如 JSON 数据')}
multiline
/>
</>
)}
<TouchableOpacity
style={styles.submitButton}
onPress={handleSubmit}
@@ -138,6 +328,58 @@ const CreateMonitorScreen: React.FC = () => {
)}
</TouchableOpacity>
</View>
{/* 状态码选择模态框 */}
<Modal
visible={statusModalVisible}
transparent={true}
animationType="slide"
onRequestClose={() => setStatusModalVisible(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>{t('monitors.selectStatusCode', '选择状态码')}</Text>
<TouchableOpacity
style={styles.modalCloseButton}
onPress={() => setStatusModalVisible(false)}
>
<Ionicons name="close" size={24} color="#666" />
</TouchableOpacity>
</View>
<FlatList
data={statusCodeOptions}
keyExtractor={(item) => item.value}
renderItem={({ item }) => (
<TouchableOpacity
style={[
styles.statusOption,
expectedStatus === item.value && styles.statusOptionSelected
]}
onPress={() => {
setExpectedStatus(item.value);
setStatusModalVisible(false);
}}
>
<Text
style={[
styles.statusOptionText,
expectedStatus === item.value && styles.statusOptionTextSelected
]}
>
{item.label}
</Text>
{expectedStatus === item.value && (
<Ionicons name="checkmark" size={20} color="#0066cc" />
)}
</TouchableOpacity>
)}
style={styles.modalList}
/>
</View>
</View>
</Modal>
</ScrollView>
);
};
@@ -147,6 +389,9 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#f5f5f5',
},
contentContainer: {
paddingBottom: 100, // 添加底部间距,避免被导航栏遮挡
},
formContainer: {
padding: 16,
},
@@ -165,40 +410,139 @@ const styles = StyleSheet.create({
paddingVertical: 10,
fontSize: 16,
},
typeContainer: {
methodContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginTop: 8,
},
typeButton: {
flex: 1,
padding: 10,
methodButton: {
paddingVertical: 8,
paddingHorizontal: 16,
backgroundColor: '#f0f0f0',
borderRadius: 8,
marginRight: 8,
alignItems: 'center',
marginBottom: 8,
},
typeButtonActive: {
methodButtonActive: {
backgroundColor: '#0066cc',
},
typeButtonText: {
methodButtonText: {
fontWeight: '500',
color: '#333',
},
typeButtonTextActive: {
methodButtonTextActive: {
color: '#fff',
},
dropdown: {
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#ddd',
paddingHorizontal: 12,
paddingVertical: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
dropdownText: {
fontSize: 16,
},
headerContainer: {
marginTop: 8,
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
headerKeyInput: {
flex: 2,
marginRight: 8,
},
headerValueInput: {
flex: 3,
marginRight: 8,
},
headerRemoveButton: {
padding: 8,
},
addHeaderButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
},
addHeaderText: {
fontSize: 14,
color: '#0066cc',
marginLeft: 4,
},
bodyInput: {
minHeight: 100,
textAlignVertical: 'top',
},
submitButton: {
backgroundColor: '#0066cc',
borderRadius: 8,
padding: 16,
alignItems: 'center',
marginTop: 32,
marginBottom: 32, // 增加底部边距
},
submitButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
// 模态框样式
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: 'white',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
maxHeight: '70%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
modalTitle: {
fontSize: 18,
fontWeight: '600',
},
modalCloseButton: {
padding: 4,
},
modalList: {
maxHeight: 300,
},
statusOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0',
},
statusOptionSelected: {
backgroundColor: '#f0f7ff',
},
statusOptionText: {
fontSize: 16,
color: '#333',
},
statusOptionTextSelected: {
color: '#0066cc',
fontWeight: '500',
},
});
export default CreateMonitorScreen;
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
import { useNavigation, useRoute } from '@react-navigation/native';
import { RouteProp } from '@react-navigation/native';
import { Ionicons } from '@expo/vector-icons';
import monitorService from '../../api/monitors';
// 移除 react-native-chart-kit 依赖,使用自定义简单图表
// 简单的自定义图表组件
@@ -85,6 +86,7 @@ interface Monitor {
uptime: number;
response_time: number;
last_check?: string;
last_checked?: string;
created_at: string;
}
@@ -96,64 +98,6 @@ interface MonitorStatusHistory {
response_time?: number;
}
// 模拟服务 - 实际项目中应该使用真实的API服务
const monitorService = {
getMonitor: (id: string): Promise<Monitor> => {
return Promise.resolve({
id,
name: 'API服务',
url: 'https://api.example.com/health',
type: 'http',
method: 'GET',
interval: 60,
timeout: 30,
status: Math.random() > 0.2 ? 'up' : 'down',
active: true,
uptime: 99.8,
response_time: Math.floor(Math.random() * 300) + 50,
last_check: new Date().toISOString(),
created_at: '2023-10-01T00:00:00Z'
});
},
getMonitorHistory: (id: string): Promise<MonitorStatusHistory[]> => {
const history: MonitorStatusHistory[] = [];
const now = new Date();
// 生成过去24小时的历史数据
for (let i = 0; i < 24; i++) {
const timestamp = new Date(now.getTime() - i * 60 * 60 * 1000).toISOString();
const status = Math.random() > 0.1 ? 'up' : 'down';
const response_time = status === 'up' ? Math.floor(Math.random() * 300) + 50 : undefined;
history.push({
id: `hist-${id}-${i}`,
status,
timestamp,
response_time
});
}
return Promise.resolve(history.reverse());
},
checkNow: (id: string): Promise<{ success: boolean }> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ success: true });
}, 2000);
});
},
toggleActive: (id: string, active: boolean): Promise<{ success: boolean }> => {
return Promise.resolve({ success: true });
},
deleteMonitor: (id: string): Promise<{ success: boolean }> => {
return Promise.resolve({ success: true });
}
};
// 路由参数类型
type MonitorDetailRouteProp = RouteProp<{ MonitorDetail: { monitorId: string } }, 'MonitorDetail'>;
@@ -174,11 +118,25 @@ const MonitorDetailScreen: React.FC = () => {
// 加载监控详情和历史
const fetchData = async () => {
try {
const monitorData = await monitorService.getMonitor(monitorId);
const historyData = await monitorService.getMonitorHistory(monitorId);
// 获取监控详情
const monitorResult = await monitorService.getMonitorById(monitorId);
if (!monitorResult.success || !monitorResult.monitor) {
throw new Error(monitorResult.message || '获取监控详情失败');
}
setMonitor(monitorData);
setHistory(historyData);
// 获取监控历史
const historyData = await monitorService.getMonitorHistory(Number(monitorId));
// 转换历史数据格式以兼容界面显示
const formattedHistory: MonitorStatusHistory[] = historyData.map((item: any) => ({
id: String(item.id || `hist-${monitorId}-${Math.random()}`),
status: item.status,
timestamp: item.timestamp,
response_time: item.response_time
}));
setMonitor(monitorResult.monitor);
setHistory(formattedHistory);
} catch (error) {
console.error('获取监控详情失败', error);
Alert.alert(t('common.error', '错误'), t('monitors.fetchDetailFailed', '获取监控详情失败'));
@@ -198,7 +156,7 @@ const MonitorDetailScreen: React.FC = () => {
const handleCheckNow = async () => {
setCheckingNow(true);
try {
await monitorService.checkNow(monitorId);
await monitorService.checkMonitor(Number(monitorId));
await fetchData();
Alert.alert(t('monitors.checkNowSuccess', '检查成功'), t('monitors.checkNowSuccessMessage', '已成功发起检查,请稍后刷新查看结果'));
} catch (error) {
@@ -214,31 +172,6 @@ const MonitorDetailScreen: React.FC = () => {
navigation.navigate('EditMonitor', { monitorId });
};
// 切换激活状态
const handleToggleActive = async () => {
if (!monitor) return;
try {
await monitorService.toggleActive(monitorId, !monitor.active);
// 更新本地状态
setMonitor({
...monitor,
active: !monitor.active
});
Alert.alert(
t('common.success', '成功'),
monitor.active
? t('monitors.pauseSuccess', '已暂停监控')
: t('monitors.activateSuccess', '已激活监控')
);
} catch (error) {
console.error('切换监控状态失败', error);
Alert.alert(t('common.error', '错误'), t('monitors.toggleActiveFailed', '切换监控状态失败'));
}
};
// 删除监控
const handleDelete = () => {
Alert.alert(
@@ -254,8 +187,12 @@ const MonitorDetailScreen: React.FC = () => {
style: 'destructive',
onPress: async () => {
try {
await monitorService.deleteMonitor(monitorId);
navigation.goBack();
const result = await monitorService.deleteMonitor(monitorId);
if (result.success) {
navigation.goBack();
} else {
Alert.alert(t('common.error', '错误'), result.message || t('monitors.deleteFailed', '删除监控失败'));
}
} catch (error) {
console.error('删除监控失败', error);
Alert.alert(t('common.error', '错误'), t('monitors.deleteFailed', '删除监控失败'));
@@ -371,7 +308,7 @@ const MonitorDetailScreen: React.FC = () => {
</View>
<View style={styles.detailItem}>
<Text style={styles.detailLabel}>{t('monitors.type', '类型')}:</Text>
<Text style={styles.detailValue}>{monitor.type.toUpperCase()}</Text>
<Text style={styles.detailValue}>{monitor.type?.toUpperCase() || 'HTTP'}</Text>
</View>
<View style={styles.detailItem}>
<Text style={styles.detailLabel}>{t('monitors.method', '方法')}:</Text>
@@ -397,7 +334,7 @@ const MonitorDetailScreen: React.FC = () => {
<View style={styles.statusContainer}>
<View style={styles.statusItem}>
<Text style={styles.statusLabel}>{t('monitors.status.lastCheck', '最后检查')}</Text>
<Text style={styles.statusValue}>{formatDate(monitor.last_check)}</Text>
<Text style={styles.statusValue}>{formatDate(monitor.last_check || monitor.last_checked)}</Text>
</View>
<View style={styles.statusItem}>
<Text style={styles.statusLabel}>{t('monitors.status.uptime', '在线率')}</Text>
@@ -491,32 +428,6 @@ const MonitorDetailScreen: React.FC = () => {
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.secondaryButton]}
onPress={handleEdit}
>
<Ionicons name="create-outline" size={18} color="#0066cc" />
<Text style={[styles.actionButtonText, styles.secondaryButtonText]}>
{t('common.edit', '编辑')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, monitor.active ? styles.warningButton : styles.successButton]}
onPress={handleToggleActive}
>
<Ionicons
name={monitor.active ? "pause" : "play"}
size={18}
color={monitor.active ? "#fff" : "#fff"}
/>
<Text style={styles.actionButtonText}>
{monitor.active
? t('monitors.actions.pause', '暂停监控')
: t('monitors.actions.activate', '激活监控')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.dangerButton]}
onPress={handleDelete}
@@ -47,12 +47,6 @@ const MonitorsScreen: React.FC = () => {
case 'down':
result = data.filter(m => m.status === 'down');
break;
case 'active':
result = data.filter(m => m.active);
break;
case 'inactive':
result = data.filter(m => !m.active);
break;
default:
break;
}
@@ -151,12 +145,6 @@ const MonitorsScreen: React.FC = () => {
</View>
</View>
</View>
{!item.active && (
<View style={styles.inactiveBadge}>
<Text style={styles.inactiveText}>{t('monitors.inactive', '已禁用')}</Text>
</View>
)}
</TouchableOpacity>
);
@@ -202,24 +190,6 @@ const MonitorsScreen: React.FC = () => {
{t('monitors.status_down', '故障')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.filterButton, filter === 'active' && styles.filterButtonActive]}
onPress={() => handleFilterChange('active')}
>
<Text style={[styles.filterText, filter === 'active' && styles.filterTextActive]}>
{t('monitors.filters.active', '已激活')}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.filterButton, filter === 'inactive' && styles.filterButtonActive]}
onPress={() => handleFilterChange('inactive')}
>
<Text style={[styles.filterText, filter === 'inactive' && styles.filterTextActive]}>
{t('monitors.filters.inactive', '已暂停')}
</Text>
</TouchableOpacity>
</ScrollView>
<TouchableOpacity
@@ -357,19 +327,6 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
marginBottom: 8,
},
inactiveBadge: {
position: 'absolute',
top: 0,
right: 0,
padding: 4,
borderRadius: 12,
backgroundColor: '#f76363',
},
inactiveText: {
fontSize: 12,
fontWeight: '500',
color: 'white',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
+2 -2
View File
@@ -68,7 +68,7 @@ const StatusScreen: React.FC = () => {
}
// 如果所有监控器都正常,但有不活跃的客户端,则系统状态为部分故障
const hasInactiveAgent = agents.some(a => a.status === 'inactive');
const hasInactiveAgent = agents.some(a => a.status === 'offline' || a.status === 'unknown');
if (hasInactiveAgent) {
return 'partial';
@@ -233,7 +233,7 @@ const StatusScreen: React.FC = () => {
</View>
<View style={styles.serviceStatus}>
<View style={[styles.statusDot, { backgroundColor: getStatusColor(monitor.status) }]} />
<Text style={styles.serviceUptime}>{monitor.uptime.toFixed(1)}% {t('status.uptime', '在线率')}</Text>
<Text style={styles.serviceUptime}>{(monitor.uptime ?? 0).toFixed(1)}% {t('status.uptime', '在线率')}</Text>
</View>
</View>
))}