diff --git a/.workbuddy/batch_i18n.py b/.workbuddy/batch_i18n.py new file mode 100644 index 0000000..e2b63a4 --- /dev/null +++ b/.workbuddy/batch_i18n.py @@ -0,0 +1,212 @@ +""" +Batch i18n transformation script. +Reads the zh translation file, creates a reverse mapping (Chinese text -> key), +then replaces Chinese text in all .tsx/.ts/.jsx files with t() calls. +""" +import re +import os +import json + +# Parse the zh translation file to create reverse mapping +def parse_translation_file(filepath): + """Extract all key-value pairs from the TypeScript translation object.""" + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # Remove the wrapper and export + content = re.sub(r'^const \w+ = ', '', content) + content = re.sub(r';\s*export default \w+;\s*$', '', content) + + # Parse the nested object structure + mapping = {} # Chinese text -> "namespace.key" + + def extract_keys(obj_str, prefix=''): + """Recursively extract keys from nested object string.""" + # Match key: value pairs + # Keys can be: identifier, 'string', or "string" + # Values can be: 'string', "string", or nested { } + + i = 0 + while i < len(obj_str): + # Find key + key_match = re.match(r'\s*(\w+)\s*:\s*', obj_str[i:]) + if not key_match: + # Try quoted key + key_match = re.match(r"\s*'([^']+)'\s*:\s*", obj_str[i:]) + + if not key_match: + i += 1 + continue + + key = key_match.group(1) + i += key_match.end() + + if i >= len(obj_str): + break + + # Check value type + if obj_str[i] == '{': + # Nested object - find matching brace + depth = 1 + j = i + 1 + while j < len(obj_str) and depth > 0: + if obj_str[j] == '{': + depth += 1 + elif obj_str[j] == '}': + depth -= 1 + j += 1 + + # Recurse into nested object + nested_content = obj_str[i+1:j-1] + full_prefix = f"{prefix}{key}." if prefix else f"{key}." + extract_keys(nested_content, full_prefix) + i = j + elif obj_str[i] == "'" or obj_str[i] == '"': + # String value + quote = obj_str[i] + j = i + 1 + while j < len(obj_str): + if obj_str[j] == '\\': + j += 2 + continue + if obj_str[j] == quote: + break + j += 1 + + value = obj_str[i+1:j] + full_key = f"{prefix}{key}" if prefix else key + if value and len(value) > 1: + mapping[value] = full_key + i = j + 1 + else: + # Skip non-string values + i += 1 + + extract_keys(content) + return mapping + +# Build the mapping +zh_mapping = parse_translation_file('src/locales/zh/index.ts') + +# Sort by length (longest first) to avoid partial replacements +sorted_mappings = sorted(zh_mapping.items(), key=lambda x: len(x[0]), reverse=True) + +print(f"Loaded {len(sorted_mappings)} translation keys") + +# Files to process +target_dirs = [ + 'src/pages', + 'src/components', +] + +# Files to skip +skip_files = [ + 'src/components/LanguageSwitcher.tsx', + 'src/components/Layout.tsx', +] + +def get_target_files(): + files = [] + for target_dir in target_dirs: + for root, dirs, filenames in os.walk(target_dir): + for fname in filenames: + if fname.endswith(('.tsx', '.ts', '.jsx', '.js')): + filepath = os.path.join(root, fname).replace('\\', '/') + if filepath not in skip_files: + files.append(filepath) + return files + +target_files = get_target_files() +print(f"Found {len(target_files)} files to process") + +# Statistics +total_replacements = 0 +files_modified = 0 + +for filepath in target_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + except: + continue + + original = content + file_replacements = 0 + + # Check if file already has useTranslation + has_i18n = 'useTranslation' in content + + # Do replacements + for chinese_text, key in sorted_mappings: + if chinese_text not in content: + continue + + # Pattern 1: In single-quoted strings: '中文' + old1 = f"'{chinese_text}'" + new1 = f"t('{key}')" + if old1 in content: + count = content.count(old1) + content = content.replace(old1, new1) + file_replacements += count + + # Pattern 2: In double-quoted strings: "中文" + old2 = f'"{chinese_text}"' + new2 = f"t('{key}')" + if old2 in content: + count = content.count(old2) + content = content.replace(old2, new2) + file_replacements += count + + # Pattern 3: In JSX text: >中文< + old3 = f'>{chinese_text}<' + new3 = f"{{t('{key}')}}<" + if old3 in content: + count = content.count(old3) + content = content.replace(old3, new3) + file_replacements += count + + # Pattern 4: In template literals: `中文` + old4 = f'`{chinese_text}`' + new4 = f"t('{key}')" + if old4 in content: + count = content.count(old4) + content = content.replace(old4, new4) + file_replacements += count + + if file_replacements == 0: + continue + + # Add useTranslation import if needed + if not has_i18n and file_replacements > 0: + # Find the last import line + import_pattern = r"((?:import .+?;\n)+)" + matches = list(re.finditer(import_pattern, content)) + if matches: + last_import = matches[-1] + insert_pos = last_import.end() + content = content[:insert_pos] + "import { useTranslation } from 'react-i18next';\n" + content[insert_pos:] + + # Add const { t } = useTranslation(); after component declaration + # Look for function component patterns + # Pattern 1: export default function ComponentName() { + func_pattern = r'(export default function \w+\([^)]*\)\s*\{)' + match = re.search(func_pattern, content) + if match: + insert_after = match.end() + content = content[:insert_after] + '\n\tconst { t } = useTranslation();' + content[insert_after:] + else: + # Pattern 2: const ComponentName = () => { or function ComponentName() { + func_pattern2 = r'((?:export default )?(?:const|function) \w+\s*(?:=\s*)?(?:\([^)]*\))?\s*(?:=>)?\s*\{)' + match = re.search(func_pattern2, content) + if match: + insert_after = match.end() + content = content[:insert_after] + '\n\tconst { t } = useTranslation();' + content[insert_after:] + + if content != original: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + files_modified += 1 + total_replacements += file_replacements + print(f" {filepath}: {file_replacements} replacements") + +print(f"\nDone! Modified {files_modified} files, {total_replacements} total replacements") diff --git a/.workbuddy/fix_i18n.py b/.workbuddy/fix_i18n.py new file mode 100644 index 0000000..c3d5709 --- /dev/null +++ b/.workbuddy/fix_i18n.py @@ -0,0 +1,161 @@ +""" +Fix script for i18n batch transformation issues: +1. Remove misplaced `const { t } = useTranslation();` lines (inside object literals, etc.) +2. Fix JSX attributes: `=t('...')` -> `={t('...')}` +3. Re-add the hook correctly in the main function component +""" +import re +import os + +target_dirs = ['src/pages', 'src/components'] +skip_files = [ + 'src/components/LanguageSwitcher.tsx', + 'src/components/Layout.tsx', + 'src/i18n/index.ts', + 'src/locales/zh/index.ts', + 'src/locales/en/index.ts', +] + +def get_target_files(): + files = [] + for target_dir in target_dirs: + for root, dirs, filenames in os.walk(target_dir): + for fname in filenames: + if fname.endswith(('.tsx', '.ts', '.jsx', '.js')): + filepath = os.path.join(root, fname).replace('\\', '/') + if filepath not in skip_files: + files.append(filepath) + return files + +target_files = get_target_files() +total_fixed = 0 + +for filepath in target_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + except: + continue + + original = content + fixes = 0 + + # Step 1: Remove ALL `const { t } = useTranslation();` lines + # These might be misplaced (inside object literals, etc.) + old_hook_line = '\tconst { t } = useTranslation();\n' + old_hook_line2 = '\n\tconst { t } = useTranslation();\n' + + hook_count = content.count('const { t } = useTranslation();') + if hook_count > 0: + # Remove all instances + content = content.replace(old_hook_line, '') + content = content.replace(old_hook_line2, '\n') + # Also handle variations + content = re.sub(r'\n\s*const \{ t \} = useTranslation\(\);\n', '\n', content) + fixes += hook_count + + # Step 2: Fix JSX attributes: `=t('...')` -> `={t('...')}` + # Pattern: word=t('...') where word is a JSX attribute name + # But NOT: key: t('...') (JS object property) or return t('...') or = t('...') + # JSX attributes look like: placeholder=t('key') or title=t('key') + # We need to match: identifier immediately followed by =t(' + + # Match patterns like: placeholder=t('some.key') or title=t('some.key') + # But not patterns like: :t(' or {t(' or (t(' or = t(' (with space) + def fix_jsx_attr(match): + prefix = match.group(1) + key = match.group(2) + return f'{prefix}={{t(\'{key}\')}}' + + # This regex matches: word=t('key') where word is preceded by space or < + # and NOT preceded by : or { or ( + content = re.sub( + r'(? { + # or: const ComponentName = (props) => { + match = re.search(r'(const \w+\s*=\s*(?:\([^)]*\))?\s*=>\s*\{)', content) + if match: + insert_pos = match.end() + content = content[:insert_pos] + '\n\tconst { t } = useTranslation();' + content[insert_pos:] + fixes += 1 + else: + # Pattern 4: export default () => { + match = re.search(r'(export default\s*(?:\([^)]*\))?\s*=>\s*\{)', content) + if match: + insert_pos = match.end() + content = content[:insert_pos] + '\n\tconst { t } = useTranslation();' + content[insert_pos:] + fixes += 1 + + # Step 5: Also check for files that have the import but the hook was removed + # Re-add if needed + if 'useTranslation' in content and 'import' in content and 'const { t } = useTranslation();' not in content: + if has_t_calls or re.search(r"\bt\(['\"]", content): + match = re.search(r'(export default function \w+\([^)]*\)\s*\{)', content) + if match: + insert_pos = match.end() + content = content[:insert_pos] + '\n\tconst { t } = useTranslation();' + content[insert_pos:] + fixes += 1 + else: + match = re.search(r'(function \w+\([^)]*\)\s*\{)', content) + if match: + insert_pos = match.end() + content = content[:insert_pos] + '\n\tconst { t } = useTranslation();' + content[insert_pos:] + fixes += 1 + else: + match = re.search(r'(const \w+\s*=\s*(?:\([^)]*\))?\s*=>\s*\{)', content) + if match: + insert_pos = match.end() + content = content[:insert_pos] + '\n\tconst { t } = useTranslation();' + content[insert_pos:] + fixes += 1 + + if content != original: + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + total_fixed += 1 + print(f" Fixed: {filepath} ({fixes} issues)") + +print(f"\nDone! Fixed {total_fixed} files") diff --git a/.workbuddy/fix_jsx_gt.py b/.workbuddy/fix_jsx_gt.py new file mode 100644 index 0000000..af8550a --- /dev/null +++ b/.workbuddy/fix_jsx_gt.py @@ -0,0 +1,63 @@ +""" +Fix script 2: Fix consumed '>' in JSX tags. +The batch script pattern `>Chinese<` -> `{t('key')}<` consumed the '>' from opening JSX tags. +""" +import re +import os + +target_dirs = ['src/pages', 'src/components'] +skip_files = [ + 'src/components/LanguageSwitcher.tsx', + 'src/components/Layout.tsx', + 'src/i18n/index.ts', + 'src/locales/zh/index.ts', + 'src/locales/en/index.ts', +] + +def get_target_files(): + files = [] + for target_dir in target_dirs: + for root, dirs, filenames in os.walk(target_dir): + for fname in filenames: + if fname.endswith(('.tsx', '.ts', '.jsx', '.js')): + filepath = os.path.join(root, fname).replace('\\', '/') + if filepath not in skip_files: + files.append(filepath) + return files + +target_files = get_target_files() +total_fixed = 0 + +for filepath in target_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + except: + continue + + original = content + + # Fix 1: }{t(' -> }>{t(' (consumed > after JSX expression attribute) + # Example: + content = re.sub( + r'\}\{t\(\'', + '}>{t(\'', + content + ) + + # Fix 2: "{t(' -> ">{t(' (consumed > after JSX string attribute) + # Example: + content = re.sub( + r'"\{t\(\'', + '">{t(\'', + content + ) + + if content != original: + fixes = original.count('}{t(\'') + original.count('"{t(\'') + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + total_fixed += 1 + print(f" Fixed: {filepath} ({fixes} issues)") + +print(f"\nDone! Fixed {total_fixed} files") diff --git a/.workbuddy/fix_jsx_gt2.py b/.workbuddy/fix_jsx_gt2.py new file mode 100644 index 0000000..15e1053 --- /dev/null +++ b/.workbuddy/fix_jsx_gt2.py @@ -0,0 +1,82 @@ +""" +Fix script 3: Fix self-closing tag consumed '>'. +Pattern: {t('key')} +Also fix any remaining patterns where '>' was consumed. +""" +import re +import os + +target_dirs = ['src/pages', 'src/components'] +skip_files = [ + 'src/components/LanguageSwitcher.tsx', + 'src/components/Layout.tsx', + 'src/i18n/index.ts', + 'src/locales/zh/index.ts', + 'src/locales/en/index.ts', +] + +def get_target_files(): + files = [] + for target_dir in target_dirs: + for root, dirs, filenames in os.walk(target_dir): + for fname in filenames: + if fname.endswith(('.tsx', '.ts', '.jsx', '.js')): + filepath = os.path.join(root, fname).replace('\\', '/') + if filepath not in skip_files: + files.append(filepath) + return files + +target_files = get_target_files() +total_fixed = 0 + +for filepath in target_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + except: + continue + + original = content + + # Fix 1: /{t(' -> />{t(' (self-closing tag consumed >) + content = re.sub( + r'/\{t\(\'', + '/>{t(\'', + content + ) + + # Fix 2: Also check for > that was consumed before Chinese text in + # other contexts (e.g., >text where > was consumed) + # Pattern: (letter or >){t(' -> (letter or >)>{t(' + # But we need to be careful not to break valid JSX expressions like: + #
{t('key')}
-- this is valid, > is before { + # So only fix when there's no > between the preceding char and {t( + + # Check for patterns like: d{t(' or n{t(' or />{t(' (already fixed) + # where a letter is immediately before {t(' without a > in between + # This means the > was consumed from: 中文 -> before {t(' when preceded by a letter (tag name char) + # and the {t(' is followed by )}< + content = re.sub( + r'([a-zA-Z])\{t\(\'([^\']+)\'\)\}(<)', + lambda m: f"{m.group(1)}>{m.group(2)}{m.group(3)}", + content + ) + # Wait, that removes the t() call structure. Let me fix it: + # -> {t('key')} + content = re.sub( + r'([a-zA-Z])\{t\(\'', + r'\1>{t(\'', + content + ) + + if content != original: + fixes = original.count('/{t(\'') + # Count letter before {t( pattern too + fixes += len(re.findall(r'[a-zA-Z]\{t\(\'', original)) - original.count('/{t(\'') + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + total_fixed += 1 + print(f" Fixed: {filepath}") + +print(f"\nDone! Fixed {total_fixed} files") diff --git a/.workbuddy/update_request.py b/.workbuddy/update_request.py new file mode 100644 index 0000000..1932f49 --- /dev/null +++ b/.workbuddy/update_request.py @@ -0,0 +1,29 @@ +import re + +with open('src/api/request.ts', 'r', encoding='utf-8') as f: + content = f.read() + +# Add i18n import after existing imports +content = content.replace( + "import utils from '../lib/utils';", + "import utils from '../lib/utils';\nimport i18n from '../i18n';" +) + +# Replace Chinese error messages +content = content.replace( + "message?.error('\u767b\u5f55\u5df2\u8fc7\u671f\uff0c\u8bf7\u91cd\u65b0\u767b\u5f55')", + "message?.error(i18n.t('api.loginExpired'))" +) +content = content.replace( + "return Promise.reject(new Error('\u767b\u5f55\u5df2\u8fc7\u671f'))", + "return Promise.reject(new Error(i18n.t('api.loginExpired')))" +) +content = content.replace( + "message?.error('\u7f51\u7edc\u5f02\u5e38\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5')", + "message?.error(i18n.t('api.networkError'))" +) + +with open('src/api/request.ts', 'w', encoding='utf-8') as f: + f.write(content) + +print("request.ts updated successfully") diff --git a/.workbuddy/update_router.py b/.workbuddy/update_router.py new file mode 100644 index 0000000..cd18b16 --- /dev/null +++ b/.workbuddy/update_router.py @@ -0,0 +1,95 @@ +import re + +with open('src/router/index.tsx', 'r', encoding='utf-8') as f: + content = f.read() + +# Replace routeMeta titles with i18n keys +replacements = { + "title: '\u603b\u89c8\u9996\u9875'": "title: 'router.overview'", + "title: '\u8bbe\u5907\u7ba1\u7406'": "title: 'router.devices'", + "title: '\u5728\u7ebf\u89c6\u9891'": "title: 'router.video'", + "title: '\u5b9e\u65f6\u76d1\u63a7'": "title: 'router.monitor'", + "title: '\u544a\u8b66\u4e2d\u5fc3'": "title: 'router.alerts'", + "title: 'AI\u8bca\u65ad\u5206\u6790'": "title: 'router.aiAnalysis'", + "title: '\u6e05\u6d17\u4f18\u5316'": "title: 'router.clean'", + "title: '\u5de5\u5355\u4efb\u52a1'": "title: 'router.task'", + "title: '\u62a5\u8868\u4e2d\u5fc3'": "title: 'router.table'", + "title: '\u7cfb\u7edf\u8bbe\u7f6e'": "title: 'router.systemSetting'", + "title: '\u65e5\u5fd7\u7ba1\u7406'": "title: 'router.log'", + "title: '\u89c6\u9891\u7ba1\u7406'": "title: 'router.videoManage'", + "title: '\u89c6\u9891\u4e0b\u8f7d'": "title: 'router.downloads'", + "title: '\u7528\u6237\u7ba1\u7406'": "title: 'router.users'", + "title: '\u89d2\u8272\u7ba1\u7406'": "title: 'router.roles'", + "title: '\u83dc\u5355\u7ba1\u7406'": "title: 'router.menuList'", + "title: '\u7ec4\u7ec7\u7ba1\u7406'": "title: 'router.organization'", + "title: '\u573a\u7ad9\u7ba1\u7406'": "title: 'router.station'", + "title: '\u4e2a\u4eba\u4e2d\u5fc3'": "title: 'router.profile'", +} + +for old, new in replacements.items(): + content = content.replace(old, new) + +# Replace 404 page text +content = content.replace( + '

\u6a21\u5757\u5f00\u53d1\u4e2d

', + '

{t("router.moduleDeveloping")}

' +) + +# Replace placeholder pages +content = content.replace( + '

\u901a\u77e5\u7b56\u7565\u9875\u9762

', + '

{t("router.noticePolicy")}

' +) +content = content.replace( + '

\u6570\u636e\u4e0e\u5b89\u5168\u9875\u9762

', + '

{t("router.dataSafety")}

' +) + +# Add useTranslation import and helper components +add_after_imports = """ +import { useTranslation } from 'react-i18next'; + +function ModuleDeveloping() { +\tconst { t } = useTranslation(); +\treturn ( +\t\t
+\t\t\t\ud83c\udde7 +\t\t\t

{t('router.moduleDeveloping')}

+\t\t
+\t); +} + +function NoticePolicyPage() { +\tconst { t } = useTranslation(); +\treturn

{t('router.noticePolicy')}

; +} + +function DataSafetyPage() { +\tconst { t } = useTranslation(); +\treturn

{t('router.dataSafety')}

; +} +""" + +# Insert after the Profile import line +content = re.sub( + r"(import Profile from '.*?';\n)", + r"\1" + add_after_imports, + content, + count=1 +) + +# Replace the inline 404 element with component +old_404 = '
\n\t\t\t\t\t\t\t\ud83c\udde7\n\t\t\t\t\t\t\t

{t("router.moduleDeveloping")}

\n\t\t\t\t\t\t
' +content = content.replace(old_404, '') + +# Replace the inline notice/dataSafety elements +old_notice = '

{t("router.noticePolicy")}

' +content = content.replace(old_notice, '') + +old_datasafety = '

{t("router.dataSafety")}

' +content = content.replace(old_datasafety, '') + +with open('src/router/index.tsx', 'w', encoding='utf-8') as f: + f.write(content) + +print("router/index.tsx updated successfully") diff --git a/.workbuddy/update_utils.py b/.workbuddy/update_utils.py new file mode 100644 index 0000000..b960143 --- /dev/null +++ b/.workbuddy/update_utils.py @@ -0,0 +1,119 @@ +import re + +with open('src/lib/utils.ts', 'r', encoding='utf-8') as f: + content = f.read() + +# Add i18n import at the top (after the last import line) +i18n_import = "import i18n from '../i18n';\n" +# Find the last import line and add after it +content = re.sub( + r"(import type \{ NotificationInstance \} from 'antd/es/notification/interface';\n)", + r"\1" + i18n_import, + content, + count=1 +) + +# Replace getLocationQuality labels with i18n getters +loc_replacements = { + "text: '\u65e0\u6548'": "get text() { return i18n.t('constants.locationInvalid') }", + "text: 'GPS \u5355\u70b9\u5b9a\u4f4d'": "get text() { return i18n.t('constants.locationGPS') }", + "text: 'DGPS \u4f2a\u8ddd\u5dee\u5206/SBAS'": "get text() { return i18n.t('constants.locationDGPS') }", + "text: 'RTK \u56fa\u5b9a\u89e3'": "get text() { return i18n.t('constants.locationRTKFixed') }", + "text: 'RTK \u6d6e\u70b9\u89e3'": "get text() { return i18n.t('constants.locationRTKFloat') }", + "text: '\u672a\u77e5'": "get text() { return i18n.t('constants.locationUnknown') }", +} +for old, new in loc_replacements.items(): + content = content.replace(old, new) + +# Replace errorSourceOptions labels +content = content.replace( + "{ label: '\u65e0\u4eba\u673a', value: ERROR_SOURCE.UAV }", + "{ value: ERROR_SOURCE.UAV, get label() { return i18n.t('constants.uav') } }" +) +content = content.replace( + "{ label: '\u5272\u8349\u673a', value: ERROR_SOURCE.MOWER }", + "{ value: ERROR_SOURCE.MOWER, get label() { return i18n.t('constants.mower') } }" +) +content = content.replace( + "{ label: '\u5176\u4ed6', value: ERROR_SOURCE.OTHERS }", + "{ value: ERROR_SOURCE.OTHERS, get label() { return i18n.t('constants.others') } }" +) + +# Replace errorLevelOptions labels +content = content.replace( + "{ label: '\u4fe1\u606f', value: 'INFO' }", + "{ value: 'INFO', get label() { return i18n.t('constants.infoLevel') } }" +) +content = content.replace( + "{ label: '\u8b66\u544a', value: 'WARNING' }", + "{ value: 'WARNING', get label() { return i18n.t('constants.warning') } }" +) +content = content.replace( + "{ label: '\u9519\u8bef', value: 'ERROR' }", + "{ value: 'ERROR', get label() { return i18n.t('constants.error') } }" +) + +# Replace CompareEnumOptions labels +compare_replacements = { + "label: '\u7b49\u4e8e'": "get label() { return i18n.t('constants.eq') }", + "label: '\u4e0d\u7b49\u4e8e'": "get label() { return i18n.t('constants.neq') }", + "label: '\u5927\u4e8e'": "get label() { return i18n.t('constants.gt') }", + "label: '\u5c0f\u4e8e'": "get label() { return i18n.t('constants.lt') }", + "label: '\u5927\u4e8e\u7b49\u4e8e'": "get label() { return i18n.t('constants.gte') }", + "label: '\u5c0f\u4e8e\u7b49\u4e8e'": "get label() { return i18n.t('constants.lte') }", + "label: '\u5728\u4e4b\u95f4'": "get label() { return i18n.t('constants.between') }", + "label: '\u4e0d\u5728\u4e4b\u95f4'": "get label() { return i18n.t('constants.notBetween') }", + "label: '\u5305\u542b'": "get label() { return i18n.t('constants.contain') }", + "label: '\u4e0d\u5305\u542b'": "get label() { return i18n.t('constants.notContain') }", +} +for old, new in compare_replacements.items(): + content = content.replace(old, new) + +# Replace workOrderTypeOptions labels +wo_replacements = { + 'label: "\u5272\u8349\u673a\u6545\u969c"': "get label() { return i18n.t('workOrder.mowerError') }", + 'label: "\u65e0\u4eba\u673a\u6545\u969c"': "get label() { return i18n.t('workOrder.uavError') }", + 'label: "\u5de1\u68c0"': "get label() { return i18n.t('workOrder.typeInspection') }", + 'label: "\u5149\u4f0f\u7ec4\u4ef6\u6545\u969c"': "get label() { return i18n.t('workOrder.componentDefect') }", + 'label: "\u6e05\u6d17"': "get label() { return i18n.t('workOrder.typeCleaning') }", + 'label: "\u7ef4\u62a4"': "get label() { return i18n.t('workOrder.maintainTask') }", + 'label: "\u7ef4\u4fee"': "get label() { return i18n.t('workOrder.typeRepair') }", + 'label: "\u5176\u4ed6"': "get label() { return i18n.t('workOrder.other') }", +} +for old, new in wo_replacements.items(): + content = content.replace(old, new) + +# Replace alarmHandleResultOptions labels +alarm_replacements = { + '{ value: "Handled", label: "\u5df2\u5904\u7406" }': '{ value: "Handled", get label() { return i18n.t("constants.handled") } }', + '{ value: "Restored", label: "\u5df2\u6062\u590d" }': '{ value: "Restored", get label() { return i18n.t("constants.restored") } }', + '{ value: "Ignored", label: "\u5df2\u5ffd\u7565" }': '{ value: "Ignored", get label() { return i18n.t("constants.ignored") } }', + '{ value: "UnableToHandle", label: "\u65e0\u6cd5\u5904\u7406" }': '{ value: "UnableToHandle", get label() { return i18n.t("constants.unableToHandle") } }', + '{ value: "ManufacturerHandling", label: "\u5382\u5bb6\u5904\u7406\u4e2d" }': '{ value: "ManufacturerHandling", get label() { return i18n.t("constants.manufacturerHandling") } }', + '{ value: "FalseAlarm", label: "\u8bef\u62a5" }': '{ value: "FalseAlarm", get label() { return i18n.t("constants.falseAlarm") } }', +} +for old, new in alarm_replacements.items(): + content = content.replace(old, new) + +# Replace exportTable default fileName +content = content.replace( + 'fileName = "\u5bfc\u51fa\u6570\u636e"', + 'fileName = i18n.t("constants.exportData")' +) + +# Replace getVal function +content = content.replace( + 'val == "no_rain" ? "\u65e0" : (val || \'-\')', + 'val == "no_rain" ? i18n.t("constants.none") : (val || \'-\')' +) + +# Replace weather error messages +content = content.replace( + "'[\u5929\u6c14API] \u83b7\u53d6\u5929\u6c14\u6570\u636e\u5931\u8d25\uff1a'", + "'[WeatherAPI]'" +) + +with open('src/lib/utils.ts', 'w', encoding='utf-8') as f: + f.write(content) + +print("utils.ts updated successfully") diff --git a/package-lock.json b/package-lock.json index e1ed8b8..17165d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,8 @@ "echarts": "^6.0.0", "express": "^4.21.2", "flv.js": "^1.6.2", + "i18next": "^26.3.6", + "i18next-browser-languagedetector": "^8.2.1", "js-cookie": "^3.0.5", "lodash": "^4.18.1", "lucide-react": "^0.546.0", @@ -35,6 +37,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-draggable": "^4.6.0", + "react-i18next": "^17.0.11", "react-redux": "^9.2.0", "react-router-dom": "^7.14.1", "recharts": "^3.8.1", @@ -4698,6 +4701,15 @@ "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", + "license": "MIT", + "funding": { + "url": "https://locize.com" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -4731,6 +4743,43 @@ "url": "https://opencollective.com/express" } }, + "node_modules/i18next": { + "version": "26.3.6", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz", + "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6575,6 +6624,33 @@ "react-dom": ">= 16.3.0" } }, + "node_modules/react-i18next": { + "version": "17.0.11", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.11.tgz", + "integrity": "sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^4.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", @@ -7392,7 +7468,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 125986a..9a01a41 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "echarts": "^6.0.0", "express": "^4.21.2", "flv.js": "^1.6.2", + "i18next": "^26.3.6", + "i18next-browser-languagedetector": "^8.2.1", "js-cookie": "^3.0.5", "lodash": "^4.18.1", "lucide-react": "^0.546.0", @@ -38,6 +40,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-draggable": "^4.6.0", + "react-i18next": "^17.0.11", "react-redux": "^9.2.0", "react-router-dom": "^7.14.1", "recharts": "^3.8.1", diff --git a/src/App.tsx b/src/App.tsx index efdf555..a48e140 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,13 @@ import React from 'react'; import { RouterProvider } from 'react-router-dom'; import { router } from './router'; -import { App as AntdApp } from 'antd'; +import { App as AntdApp, ConfigProvider, theme } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import enUS from 'antd/locale/en_US'; import utils from './lib/utils'; +import { useTranslation } from 'react-i18next'; +import { I18nextProvider } from 'react-i18next'; +import i18n from './i18n'; // 用于静态注入 message/modal/notification const StaticAntd = () => { @@ -13,12 +18,46 @@ const StaticAntd = () => { return null; }; -export default function App() { - +const AntdConfigWrapper = ({ children }: { children: React.ReactNode }) => { + const { i18n } = useTranslation(); + const isEn = i18n.language?.startsWith('en'); return ( - - - - + + {children} + + ); +}; + +export default function App() { + return ( + + + + + + + + ); } diff --git a/src/api/request.ts b/src/api/request.ts index 9ef2968..8268903 100644 --- a/src/api/request.ts +++ b/src/api/request.ts @@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'ax import envConfig from '../../env'; import Cookies from 'js-cookie'; import utils from '../lib/utils'; +import i18n from '../i18n'; const API_BASE_URL = envConfig.baseURL; const message = utils.message; // 使用全局 message @@ -63,7 +64,7 @@ const handleLogoutAndRedirect = () => { }); // 只提示一次 - message?.error('登录已过期,请重新登录'); + message?.error(i18n.t('api.loginExpired')); // 只跳转一次 setTimeout(() => { @@ -78,7 +79,7 @@ api.interceptors.response.use( // 业务 401 → 统一走登出 if (data?.code === 401) { handleLogoutAndRedirect(); - return Promise.reject(new Error('登录已过期')); + return Promise.reject(new Error(i18n.t('api.loginExpired'))); } return data; }, @@ -91,7 +92,7 @@ api.interceptors.response.use( // 网络异常 if (!error.response) { - message?.error('网络异常,请稍后重试'); + message?.error(i18n.t('api.networkError')); return Promise.reject(error); } diff --git a/src/components/DictManager.jsx b/src/components/DictManager.jsx index fc479d0..114040a 100644 --- a/src/components/DictManager.jsx +++ b/src/components/DictManager.jsx @@ -3,14 +3,15 @@ import { Table, Input, Button, Form, Space, Card, Modal, message, Popconfirm } from 'antd'; import { EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; const DictManager = ({ title, data, fields = [ { name: 'storeValue', label: '存储值', required: true }, - { name: 'displayName', label: '名称', required: true }, - { name: 'remark', label: '备注', required: false } + { name: 'displayName', label: t('common.name'), required: true }, + { name: 'remark', label: t('roles.remark'), required: false } ], onAdd, onUpdate, @@ -25,6 +26,7 @@ const DictManager = ({ // 打开新增 const handleAddNew = () => { + const { t } = useTranslation(); setEditing(null); form.resetFields(); setVisible(true); @@ -49,7 +51,7 @@ const DictManager = ({ } if (res?.code == 200) { - messageApi.success(editing ? "编辑成功" : '新增成功'); + messageApi.success(editing ? t('common.editSuccess') : t('common.addSuccess')); setVisible(false); form.resetFields(); setEditing(null); @@ -70,7 +72,7 @@ const DictManager = ({ key: item.name, })), { - title: '操作', + title: t('roles.actions'), key: 'action', render: (_, record) => ( @@ -80,19 +82,19 @@ const DictManager = ({ { try { const res = await onDelete(record.id); if (res?.code === 200) { - messageApi.success('删除成功'); + messageApi.success(t('common.deleteSuccess')); refresh?.(); } else { - messageApi.error(res?.msg || '删除失败'); + messageApi.error(res?.msg || t('common.deleteFail')); } } catch (err) { - messageApi.error('删除失败'); + messageApi.error(t('common.deleteFail')); } }} > @@ -123,7 +125,7 @@ const DictManager = ({ setVisible(false)} onOk={handleSubmit} confirmLoading={loading} diff --git a/src/components/Empty.tsx b/src/components/Empty.tsx index dc1eec3..3800852 100644 --- a/src/components/Empty.tsx +++ b/src/components/Empty.tsx @@ -1,5 +1,7 @@ import React from 'react'; import { Empty } from 'antd'; +import { useTranslation } from 'react-i18next'; +import { t } from 'i18next'; interface EmptyDataProps { /** 自定义文案 */ @@ -9,7 +11,7 @@ interface EmptyDataProps { } const EmptyData: React.FC = ({ - description = '暂无数据', + description = t('common.noData'), height = 220 }) => { return ( diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..dfd5fb3 --- /dev/null +++ b/src/components/LanguageSwitcher.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { Button, Dropdown } from 'antd'; +import { GlobalOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; +import type { MenuProps } from 'antd'; + +export default function LanguageSwitcher() { + const { i18n } = useTranslation(); + const currentLng = i18n.language?.startsWith('en') ? 'en' : 'zh'; + + const items: MenuProps['items'] = [ + { + key: 'zh', + label: ( + + 中文 + + ), + }, + { + key: 'en', + label: ( + + English + + ), + }, + ]; + + const handleChange: MenuProps['onClick'] = ({ key }) => { + i18n.changeLanguage(key); + localStorage.setItem('i18nextLng', key); + }; + + return ( + + + + ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 257f618..5add3f5 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,9 +1,8 @@ import React, { useState, useEffect } from 'react'; import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { - Layout, Menu, Button, Space, ConfigProvider, theme, Select, Avatar, Card, Tag + Layout, Menu, Button, Space, Select, Avatar, Card, Tag } from 'antd'; -import zhCN from 'antd/locale/zh_CN'; import { DashboardOutlined, ExperimentOutlined, VideoCameraOutlined, BellOutlined, UserOutlined, LogoutOutlined, @@ -11,6 +10,7 @@ import { CompressOutlined, ExpandOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; import logo from '../assets/logo.png'; import { useSelector } from 'react-redux'; import { RootState, useAppDispatch } from '../store'; @@ -18,17 +18,18 @@ import { logout } from '../store/userSlice'; import { setCurrentStation, clearStation } from '../store/stationSlice'; import { getMenuList } from '../api/mune'; import { getStationByUserId } from '../api/stationManage'; +import LanguageSwitcher from './LanguageSwitcher'; import envConfig from '../../env'; const { Header, Sider, Content } = Layout; export default function AppLayout() { + const { t } = useTranslation(); const [collapsed, setCollapsed] = useState(false); const [time, setTime] = useState(new Date().toLocaleString()); const [menuList, setMenuList] = useState([]); const [stationList, setStationList] = useState([]); - const [isFullScreen, setIsFullScreen] = useState(false); // 新增全屏标识 - + const [isFullScreen, setIsFullScreen] = useState(false); const navigate = useNavigate(); const location = useLocation(); @@ -38,7 +39,6 @@ export default function AppLayout() { const { currentStation, stationId } = useSelector((state: RootState) => state.station); const getStationData = () => { - getStationByUserId().then(res => { if (res.code === 200) { const list = res.data || []; @@ -49,6 +49,7 @@ export default function AppLayout() { } }); }; + const toggleFullScreen = () => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); @@ -83,7 +84,6 @@ export default function AppLayout() { const map = new Map(); const tree: any[] = []; - // 建立映射 list.forEach(item => { map.set(item.menuId, { ...item, @@ -94,10 +94,8 @@ export default function AppLayout() { }); }); - // 建树 list.forEach(item => { const node = map.get(item.menuId); - if (item.parentId === 0) { tree.push(node); } else { @@ -108,7 +106,6 @@ export default function AppLayout() { } }); - // 排序 const sortMenu = (menus: any[]) => { menus.sort((a, b) => a.orderNum - b.orderNum); menus.forEach(menu => { @@ -121,16 +118,16 @@ export default function AppLayout() { }; sortMenu(tree); - return tree; }; + const renderMenuItems = (treeList, isChild = false) => { return treeList.map(item => { const hasChild = item.children && item.children.length > 0; const key = item.path || `/menu/${item.menuId}`; return { key, - icon: isChild ? null : getIcon(item.path), // 子菜单无图标 + icon: isChild ? null : getIcon(item.path), label: item.menuName, ...(hasChild && { children: renderMenuItems(item.children, true), @@ -139,9 +136,6 @@ export default function AppLayout() { }); }; - - - const handleLogout = async () => { await dispatch(logout()); dispatch(clearStation()); @@ -176,187 +170,149 @@ export default function AppLayout() { }, []); return ( - - -
- + +
+ + - {time} - navigate('/profile')} - className="hover:opacity-80" - > - {userInfo?.avatar && ( - - )} - {userInfo?.nickName || userInfo?.username} - - - -
- - - - - {/* 菜单滚动区域:弹性占满剩余空间,超出滚动 */} -
- navigate(key)} - style={{ borderRight: 0 }} - /> -
- - {/* 底部场站卡片,固定在底部,不被菜单挤压 */} - {!collapsed && currentStation && ( - -
-
- {currentStation.siteName || '1MW 光伏电站'} -
- -
-
电站地址:{currentStation.address || '河南省林州'}
-
电站类型:{currentStation.type || '地面电站'}
-
- 运行状态: - ● - 正常运行 -
+ {!collapsed && currentStation && ( + +
+
+ {currentStation.siteName || t('layout.defaultStationName')} +
+ +
+
{t('layout.stationAddress')}:{currentStation.address || t('layout.defaultStationAddress')}
+
{t('layout.stationType')}:{currentStation.type || t('layout.groundStation')}
+
+ {t('layout.runningStatus')}: + ● + {t('layout.normalRunning')}
- - )} - - +
+
+ )} + + - - - - - + + + - + ); } diff --git a/src/components/Map.jsx b/src/components/Map.jsx index 01098a1..c01bb2e 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -9,6 +9,7 @@ import arrowImg from '../assets/images/arrow.png'; import iotImg from '../assets/images/iot.png'; import envConfig from '../../env'; import { TracePoint, TPAction, TPMode } from '../lib/TracePoint/tracePoint'; +import { useTranslation } from 'react-i18next'; const CYAN_ALARM_COLOR = new window.Cesium.Color( @@ -343,10 +344,11 @@ const CesiumMap = React.forwardRef(({ ifClear, ifClearPlanLine, ifClearNavLine, }, [markedPoints]); function showRobotTooltip(tooltip, robot, position) { + const { t } = useTranslation(); const statusMap = { 1: { text: "工作中", color: "#10b981" }, - 2: { text: "空闲", color: "#f59e0b" }, - 3: { text: "离线", color: "#f87171" } + 2: { text: t('devices.idle'), color: "#f59e0b" }, + 3: { text: t('devices.offline'), color: "#f87171" } }; let status = statusMap[robot.feStatus] || statusMap[1]; diff --git a/src/components/SystemSetting/AlarmSettingsManagement.tsx b/src/components/SystemSetting/AlarmSettingsManagement.tsx index 4ce49fa..ab1b1fa 100644 --- a/src/components/SystemSetting/AlarmSettingsManagement.tsx +++ b/src/components/SystemSetting/AlarmSettingsManagement.tsx @@ -1,336 +1,338 @@ -import React, { useState, useEffect } from 'react'; -import { - Card, - Row, - Col, - Button, - Form, - Input, - Select, - Switch, - Space, - Table, - Modal, - message, - Tag, - App -} from 'antd'; -import { - PlusOutlined, - ReloadOutlined, - EditOutlined, - DeleteOutlined -} from '@ant-design/icons'; -import { - getAlarmOrderConfigListApi, - saveAlarmOrderConfigApi, - deleteAlarmOrderConfigApi, - getErrorListAllApi, // 错误编码下拉数据源 - getWorkOrderModelListApi // 工单模型下拉数据源 -} from '../../api/systemSetting'; -import { useSelector } from 'react-redux'; -import { RootState } from '@/src/store'; -import utils, { errorSourceOptions, getErrorSourceText } from '@/lib/utils'; - -const { Option } = Select; - -// 告警模型管理子组件 -const AlarmModelManagement: React.FC = () => { - const [form] = Form.useForm(); - const [list, setList] = useState([]); - const [loading, setLoading] = useState(false); - const [modalVisible, setModalVisible] = useState(false); - const [editingItem, setEditingItem] = useState(null); - const { stationId, currentStation } = useSelector((state: RootState) => state.station); - const { modal } = App.useApp(); - - // 批量多选 - const [selectedRowKeys, setSelectedRowKeys] = useState([]); - - // 下拉数据源:错误编码列表、工单模型列表 - const [errorCodeOptions, setErrorCodeOptions] = useState([]); - const [modelOptions, setModelOptions] = useState([]); - const { userInfo } = useSelector((state: RootState) => state.user); - - - // 获取告警模型列表 - const fetchList = async () => { - setLoading(true); - const data = { siteId: stationId || '' }; - try { - const res = await getAlarmOrderConfigListApi(data); - if (res.code === 200) { - setList(res.rows || []); - setSelectedRowKeys([]); - } - } catch (e) { - utils.message.error('获取告警模型列表失败'); - setList([]); - } finally { - setLoading(false); - } - }; - - // 拉取【错误编码】下拉选项(来自错误管理接口) - const fetchErrorCodeOptions = async () => { - try { - const params = { siteId: stationId || '' }; - const res = await getErrorListAllApi(params); - if (res.code === 200 && res.rows) { - const opts = res.rows.map((item: any) => ({ - label: `${item.errorCode} - ${item.errorName}`, - value: item.errorCode - })); - setErrorCodeOptions(opts); - } - } catch (err) { - console.error('拉取错误编码失败', err); - } - }; - - // 拉取【工单模型】下拉选项(来自工单模型接口) - const fetchModelOptions = async () => { - try { - const params = { siteId: stationId || '' }; - const res = await getWorkOrderModelListApi(params); - if (res.code === 200 && res.rows) { - const opts = res.rows.map((item: any) => ({ - label: item.orderName, - value: Number(item.id) - })); - setModelOptions(opts); - } - } catch (err) { - console.error('拉取工单模型失败', err); - } - }; - - // 站点切换:刷新表格、刷新下拉数据源 - useEffect(() => { - fetchList(); - fetchErrorCodeOptions(); - fetchModelOptions(); - }, [stationId]); - - const handleAdd = () => { - setEditingItem(null); - form.resetFields(); - setModalVisible(true); - }; - - const handleEdit = (record: any) => { - setEditingItem(record); - form.setFieldsValue({ - errorCode: record.errorCode, - modelId: record.modelId, - sourceType: record.sourceType, - autoGenerateOrder: record.autoGenerateOrder - }); - setModalVisible(true); - }; - - // 单条删除 - const handleDelete = (id: string | number) => { - modal.confirm({ - title: '确认删除', - content: '您确定要删除这条告警模型吗?', - okText: '确定', - cancelText: '取消', - onOk: async () => { - try { - const res = await deleteAlarmOrderConfigApi([id]); - if (res.code === 200) { - message.success('删除成功'); - fetchList(); - } - } catch (e) { - message.error('删除失败'); - } - } - }); - }; - - // 批量删除 - const handleBatchDelete = () => { - if (!selectedRowKeys.length) { - message.warning('请先勾选要删除的数据'); - return; - } - modal.confirm({ - title: '批量删除确认', - content: `您确定要删除选中的 ${selectedRowKeys.length} 条告警模型吗?删除后不可恢复!`, - okText: '确定', - cancelText: '取消', - danger: true, - onOk: async () => { - try { - const res = await deleteAlarmOrderConfigApi(selectedRowKeys); - if (res.code === 200) { - message.success('批量删除成功'); - fetchList(); - } - } catch (e) { - message.error('批量删除失败'); - } - } - }); - }; - - // 保存:组装后端实体字段 ErrorCode / modelId / autoGenerateOrder - const handleSave = async (values: any) => { - try { - const submitParams = { - // 主键编辑时携带 - ...(editingItem ? { id: editingItem.id } : {}), - siteId: stationId, - // 后端要求字段 - errorCode: values.errorCode, - modelId: values.modelId, - autoGenerateOrder: values.autoGenerateOrder ?? false, - sourceType: values.sourceType ?? 1, - orgId: currentStation?.orgId || '', - }; - const res = await saveAlarmOrderConfigApi(submitParams); - if (res.code === 200 && res.data === true) { - message.success(editingItem ? '更新成功' : '新增成功'); - setModalVisible(false); - form.resetFields(); - fetchList(); - } - } catch (e) { - message.error('保存失败'); - } - }; - - // 表格列配置 - const columns = [ - { title: '错误编码', dataIndex: 'errorCode', key: 'errorCode' }, - { - title: '错误来源', - dataIndex: 'sourceType', - key: 'sourceType', - render: (val: string) => getErrorSourceText(val) - }, - { - title: '关联工单模型', - dataIndex: 'modelId', - key: 'modelId', - render: (val: number) => { - const target = modelOptions.find(m => m.value === val); - return target?.label || val; - } - }, - { - title: '自动生成工单', - dataIndex: 'autoGenerateOrder', - key: 'autoGenerateOrder', - render: (val: boolean) => val ? '是' : '否' - }, - { - title: '操作', - key: 'action', - render: (_: any, record: any) => ( - - - - - ) - } - ]; - - // 表格多选 - const rowSelection = { - selectedRowKeys, - onChange: (keys: (string | number)[]) => setSelectedRowKeys(keys), - }; - - return ( -
- - - - - - - - - - setModalVisible(false)} - footer={null} - width={600} - > -
- {/* 错误编码(关联错误管理) */} - - - - - -
+ + + setModalVisible(false)} + footer={null} + width={600} + > + + {/* 错误编码(关联错误管理) */} + + + + + + - + - + - + diff --git a/src/components/SystemSetting/BasicSettings.tsx b/src/components/SystemSetting/BasicSettings.tsx index 12aa62b..c6cf4b9 100644 --- a/src/components/SystemSetting/BasicSettings.tsx +++ b/src/components/SystemSetting/BasicSettings.tsx @@ -57,10 +57,12 @@ import { Lock, } from 'lucide-react'; import dayjs from 'dayjs'; +import { useTranslation } from 'react-i18next'; const { Text, Title } = Typography; const BasicSettings = () => { + const { t } = useTranslation(); const [form] = Form.useForm(); // 响应式字体大小 @@ -84,7 +86,7 @@ const BasicSettings = () => { bg: 'linear-gradient(135deg, #e6f4ff 0%, #bae0ff 100%)' }, { - title: '在线用户', + title: t('systemSetting.onlineUsers'), value: '12', trend: '↑ 2', trendColor: '#52c41a', @@ -94,14 +96,14 @@ const BasicSettings = () => { { title: '告警通道', value: '3 / 4', - trend: '正常', + trend: t('roles.normal'), trendColor: '#52c41a', icon: , bg: 'linear-gradient(135deg, #fff2f0 0%, #ffccc7 100%)' }, { title: '数据备份状态', - value: '正常', + value: t('roles.normal'), trend: '●', trendColor: '#52c41a', icon: , @@ -130,7 +132,7 @@ const BasicSettings = () => { title: '运维工程师', users: 5, perms: 18, - tags: ['设备管理', '工单管理'], + tags: [t('router.devices'), '工单管理'], color: '#52c41a', bg: '#f6ffed', icon: @@ -157,20 +159,20 @@ const BasicSettings = () => { // 设备状态 const deviceStatuses = [ - { name: '逆变器', count: '5/5', status: '正常' }, - { name: '汇流箱', count: '20/20', status: '正常' }, - { name: '气象站', count: '1/1', status: '正常' }, - { name: '摄像头', count: '12/12', status: '正常' }, - { name: '无人机', count: '2/2', status: '正常' }, - { name: '机器人', count: '5/5', status: '正常' }, - { name: 'API接口', count: '正常', status: '正常' }, - { name: 'Modbus', count: '正常', status: '正常' }, + { name: t('home.inverter'), count: '5/5', status: t('roles.normal') }, + { name: t('home.combinerBox'), count: '20/20', status: t('roles.normal') }, + { name: '气象站', count: '1/1', status: t('roles.normal') }, + { name: t('devices.camera'), count: '12/12', status: t('roles.normal') }, + { name: t('constants.uav'), count: '2/2', status: t('roles.normal') }, + { name: t('devices.robot'), count: '5/5', status: t('roles.normal') }, + { name: 'API接口', count: t('roles.normal'), status: t('roles.normal') }, + { name: 'Modbus', count: t('roles.normal'), status: t('roles.normal') }, ]; return (
- 基础设置 + {t('systemSetting.basicSettings')}
@@ -210,27 +212,27 @@ const BasicSettings = () => {
- - + + - + {t('layout.defaultStationName')} - + - + @@ -239,7 +241,7 @@ const BasicSettings = () => { @@ -342,7 +344,7 @@ const BasicSettings = () => { columns={[ { title: '用户名', dataIndex: 'name', key: 'name', fixed: 'left', width: 80 }, { - title: '角色', + title: t('users.role'), dataIndex: 'role', key: 'role', width: 100, @@ -357,22 +359,22 @@ const BasicSettings = () => { return {role}; } }, - { title: '手机号', dataIndex: 'phone', key: 'phone', width: 110 }, - { title: '邮箱', dataIndex: 'email', key: 'email', ellipsis: true }, + { title: t('users.phone'), dataIndex: 'phone', key: 'phone', width: 110 }, + { title: t('users.email'), dataIndex: 'email', key: 'email', ellipsis: true }, { - title: '状态', + title: t('roles.status'), dataIndex: 'status', key: 'status', width: 70, render: (status) => ( -
+
{status} ) }, { - title: '操作', key: 'action', width: 60, render: () => } bordered={false} style={{ borderRadius: 10, boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }} diff --git a/src/components/SystemSetting/CameraManagement.tsx b/src/components/SystemSetting/CameraManagement.tsx index ab8c29e..af65a6b 100644 --- a/src/components/SystemSetting/CameraManagement.tsx +++ b/src/components/SystemSetting/CameraManagement.tsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { Table, Input, @@ -101,6 +102,7 @@ const mockCameraList = [ ]; export default function CameraManagement() { + const { t } = useTranslation(); const { modal } = App.useApp(); const [form] = Form.useForm(); const [loading, setLoading] = useState(false); @@ -111,13 +113,13 @@ export default function CameraManagement() { const columns = [ { title: '设备编码', dataIndex: 'code', key: 'code', width: 120 }, - { title: '设备名称', dataIndex: 'name', key: 'name', width: 180 }, - { title: 'IP地址', dataIndex: 'ip', key: 'ip', width: 140 }, + { title: t('devices.deviceName'), dataIndex: 'name', key: 'name', width: 180 }, + { title: t('systemSetting.ipAddress'), dataIndex: 'ip', key: 'ip', width: 140 }, { title: '摄像头类型', dataIndex: 'type', key: 'type', width: 120 }, { title: '品牌', dataIndex: 'manufacturer', key: 'manufacturer', width: 120 }, { title: '分辨率', dataIndex: 'resolution', key: 'resolution', width: 100 }, { - title: '位置', dataIndex: 'location', key: 'location', + title: t('aiAnalysis.location'), dataIndex: 'location', key: 'location', render: (text) => ( @@ -134,13 +136,13 @@ export default function CameraManagement() { render: (nv) => nv ? 支持 : 不支持 }, { - title: '在线', dataIndex: 'online', key: 'online', width: 100, + title: t('devices.online'), dataIndex: 'online', key: 'online', width: 100, render: (text) => ( - {text === 'ONLINE' ? '在线' : '离线'} + {text === 'ONLINE' ? t('devices.online') : t('devices.offline')} ), }, { - title: '状态', dataIndex: 'status', key: 'status', width: 100, + title: t('roles.status'), dataIndex: 'status', key: 'status', width: 100, render: (text) => { const colorMap: Record = { NORMAL: 'green', @@ -148,20 +150,20 @@ export default function CameraManagement() { WARNING: 'orange', }; const textMap: Record = { - NORMAL: '正常', - FAULT: '故障', - WARNING: '警告', + NORMAL: t('roles.normal'), + FAULT: t('devices.error'), + WARNING: t('constants.warning'), }; return {textMap[text] || text}; }, }, { - title: '操作', key: 'action', width: 200, fixed: 'right' as const, + title: t('roles.actions'), key: 'action', width: 200, fixed: 'right' as const, render: (_, record: any) => ( - - + + ), }, @@ -282,7 +284,7 @@ export default function CameraManagement() {
-
在线
+
{t('devices.online')}
{cameraList.filter((c: any) => c.online === 'ONLINE').length}
@@ -337,16 +339,16 @@ export default function CameraManagement() { - + - + @@ -387,12 +389,12 @@ export default function CameraManagement() {
- + - + @@ -412,7 +414,7 @@ export default function CameraManagement() { 海康威视 大华 宇视 - 其他 + {t('constants.others')} diff --git a/src/components/SystemSetting/DeviceAccessPage.tsx b/src/components/SystemSetting/DeviceAccessPage.tsx index ba55522..08c3387 100644 --- a/src/components/SystemSetting/DeviceAccessPage.tsx +++ b/src/components/SystemSetting/DeviceAccessPage.tsx @@ -9,40 +9,11 @@ import { AppstoreOutlined, SwapOutlined, VideoCameraOutlined } from '@ant-design/icons'; import { addIotDevice, updateIotDevice, addDeviceModel, addProduct, deleteModel, deleteProduct, getConnectTypes, getDataTypesApi, getDeviceModelList, getProductPage, updateDeviceModel, updateProduct } from '../../api/deviceAccess.ts'; +import { useTranslation } from 'react-i18next'; const { Option } = Select; const { TabPane } = Tabs; -const modelDetailList: Record = { - 10: { - props: [ - { id: 1, code: 'device_online', name: '在线状态', type: 'bool', rw: 'r', isAlarm: false, unit: '', required: true, defaultValue: null }, - { id: 2, code: 'record_status', name: '录像状态', type: 'string', rw: 'r', isAlarm: false, unit: '', required: true, defaultValue: null }, - ], - events: [{ id: 1, code: 'alarm_perimeter', name: '周界入侵告警' }], - actions: [{ actionId: 'take_snapshot', actionName: '截图', params: {} }] - }, - 11: { - props: [ - { id: 1, code: 'temp', name: '温度', type: 'float', rw: 'r', isAlarm: true, unit: '℃', required: true, defaultValue: null }, - { id: 2, code: 'humi', name: '湿度', type: 'float', rw: 'r', isAlarm: true, unit: '%', required: true, defaultValue: null }, - ], - events: [{ id: 1, code: 'temp_over', name: '超温告警' }], - actions: [] - }, - 12: { - props: [ - { id: 1, code: 'battery', name: '电量', type: 'int', rw: 'r', isAlarm: true, unit: '%', required: true, defaultValue: null }, - { id: 2, code: 'voltage', name: '电压', type: 'float', rw: 'r', isAlarm: true, unit: 'V', required: true, defaultValue: null }, - ], - events: [{ id: 1, code: 'low_power', name: '低电量告警' }], - actions: [] - } -}; let mappingInitList = [ { id: 1, productId: 1, standardCode: 'device_online', sourceField: 'online_flag', convertRule: 'value==1?true:false' } @@ -51,6 +22,7 @@ let mappingInitList = [ export default function DeviceAccessPage() { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('device'); const [form] = Form.useForm(); const [visible, setVisible] = useState(false); @@ -93,6 +65,37 @@ export default function DeviceAccessPage() { pageSize: 10, total: 0, }); + const modelDetailList: Record = { + 10: { + props: [ + { id: 1, code: 'device_online', name: t('devices.onlineStatus'), type: 'bool', rw: 'r', isAlarm: false, unit: '', required: true, defaultValue: null }, + { id: 2, code: 'record_status', name: '录像状态', type: 'string', rw: 'r', isAlarm: false, unit: '', required: true, defaultValue: null }, + ], + events: [{ id: 1, code: 'alarm_perimeter', name: '周界入侵告警' }], + actions: [{ actionId: 'take_snapshot', actionName: t('video.snapshot'), params: {} }] + }, + 11: { + props: [ + { id: 1, code: 'temp', name: t('home.temperature'), type: 'float', rw: 'r', isAlarm: true, unit: '℃', required: true, defaultValue: null }, + { id: 2, code: 'humi', name: '湿度', type: 'float', rw: 'r', isAlarm: true, unit: '%', required: true, defaultValue: null }, + ], + events: [{ id: 1, code: 'temp_over', name: '超温告警' }], + actions: [] + }, + 12: { + props: [ + { id: 1, code: 'battery', name: t('devices.batteryLevel'), type: 'int', rw: 'r', isAlarm: true, unit: '%', required: true, defaultValue: null }, + { id: 2, code: 'voltage', name: '电压', type: 'float', rw: 'r', isAlarm: true, unit: 'V', required: true, defaultValue: null }, + ], + events: [{ id: 1, code: 'low_power', name: '低电量告警' }], + actions: [] + } + }; + // ====================== // 【优化】统一请求方法 @@ -155,7 +158,7 @@ export default function DeviceAccessPage() { const handleDeleteModel = async (id) => { const res = await deleteModel(id); if (res.code === 200) { - messageApi.success('删除成功'); + messageApi.success(t('common.deleteSuccess')); fetchModelPageList(); // 【优化】删除后刷新 } else { messageApi.error('删除失败:' + res.msg); @@ -166,7 +169,7 @@ export default function DeviceAccessPage() { const handleDeleteProduct = async (id) => { const res = await deleteProduct(id); if (res.code === 200) { - messageApi.success('删除成功'); + messageApi.success(t('common.deleteSuccess')); fetchProductPageList(); // 【优化】删除后刷新 } else { messageApi.error('删除失败:' + res.msg); @@ -324,7 +327,7 @@ export default function DeviceAccessPage() { }; const res = editData ? await updateDeviceModel(submitData) : await addDeviceModel(submitData); if (res.code === 200) { - messageApi.success(editData ? '编辑成功' : '新增成功'); + messageApi.success(editData ? t('common.editSuccess') : t('common.addSuccess')); success = true; } else { messageApi.error(res.msg); @@ -401,10 +404,10 @@ export default function DeviceAccessPage() { if (val.id) { const index = newList.findIndex(x => x.id === val.id); if (index >= 0) newList[index] = val; - messageApi.success('修改成功'); + messageApi.success(t('profile.modifySuccess')); } else { newList.push({ ...val, id: Date.now() }); - messageApi.success('新增成功'); + messageApi.success(t('common.addSuccess')); } setMappingList(newList); setMappingModal(false); @@ -413,7 +416,7 @@ export default function DeviceAccessPage() { const deleteMapping = (id: number) => { const newList = mappingList.filter(x => x.id !== id); setMappingList(newList); - messageApi.success('删除成功'); + messageApi.success(t('common.deleteSuccess')); }; const getAlarmProps = () => { @@ -440,7 +443,7 @@ export default function DeviceAccessPage() { // 表格列 const deviceColumns = [ - { title: '设备名称', dataIndex: 'deviceName' }, // 👈 改成 deviceName + { title: t('devices.deviceName'), dataIndex: 'deviceName' }, // 👈 改成 deviceName { title: 'SN', dataIndex: 'sn' }, { title: '产品ID', dataIndex: 'productId' }, { title: '模型ID', dataIndex: 'modelId' }, @@ -448,13 +451,13 @@ export default function DeviceAccessPage() { { title: '端口', dataIndex: 'port' }, { title: '账号', dataIndex: 'username' }, { - title: '状态', + title: t('roles.status'), render: (r) => - {r.onlineStatus ? '在线' : '离线'} + {r.onlineStatus ? t('devices.online') : t('devices.offline')} }, { - title: '操作', + title: t('roles.actions'), fixed: 'right', render: (r) => ( - handleDeleteModel(r.id)} okText="确定" cancelText="取消"> - + + handleDeleteModel(r.id)} okText={t('common.ok')} cancelText={t('common.cancel')}> + ) @@ -496,11 +499,11 @@ export default function DeviceAccessPage() {
{contextHolder}
- 设备接入 + {t('systemSetting.deviceAccess')}
- 设备管理} key="device" /> + {t('router.devices')}} key="device" /> 产品管理} key="product" /> 物模型管理} key="model" /> @@ -546,15 +549,15 @@ export default function DeviceAccessPage() { - - + - - + + + handleDeleteProduct(r.id)} - okText="确定" - cancelText="取消" + okText={t('common.ok')} + cancelText={t('common.cancel')} > - + ) @@ -616,7 +619,7 @@ export default function DeviceAccessPage() {
- + - - + + @@ -645,7 +648,7 @@ export default function DeviceAccessPage() { {modalType === 'device' && ( <> @@ -799,9 +802,9 @@ export default function DeviceAccessPage() { - + @@ -919,18 +922,18 @@ export default function DeviceAccessPage() { - + - +
))} @@ -965,19 +968,19 @@ export default function DeviceAccessPage() {
属性 - +
}, + { title: t('constants.sourceAlert'), render: r => }, { - title: '操作', render: r => ( + title: t('roles.actions'), render: r => ( - - deleteItem('props', r.id)}> + + deleteItem('props', r.id)}> ) } @@ -988,13 +991,13 @@ export default function DeviceAccessPage() { setCurrentAction({ actionId: '', actionName: '', params: {} }); setParamList([{ paramKey: '', type: 'int', desc: '', defaultValue: '', enum: [] }]); setActionModal(true); - }} className="mb-2">新增 + }} className="mb-2">{t('common.addNew')}
Object.keys(r.params || []).join(',') || '无' }, { - title: '操作', render: r => ( + title: t('roles.actions'), render: r => ( + }}>{t('roles.edit')} setModelDetail({ ...modelDetail, actions: modelDetail.actions.filter(x => x.actionId !== r.actionId) })}> - + ) @@ -1033,7 +1036,7 @@ export default function DeviceAccessPage() { - + - setParamList([{ ...paramList[0], desc: e.target.value }])} /> + setParamList([{ ...paramList[0], desc: e.target.value }])} /> setParamList([{ ...paramList[0], defaultValue: e.target.value }])} /> setParamList([{ ...paramList[0], enum: e.target.value.split(',').map(s => s.trim()) }])} /> diff --git a/src/components/SystemSetting/DeviceManagement.tsx b/src/components/SystemSetting/DeviceManagement.tsx index 5ef875d..e00b4b0 100644 --- a/src/components/SystemSetting/DeviceManagement.tsx +++ b/src/components/SystemSetting/DeviceManagement.tsx @@ -13,8 +13,10 @@ import DroneManagement from './DroneManagement'; import SmartDeviceManagement from './SmartDeviceManagement'; import AllDeviceManagement from './AllDeviceManagement'; import CameraManagement from './CameraManagement'; +import { useTranslation } from 'react-i18next'; export default function DeviceManagementPage() { + const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('all'); const [allOrgOptions, setAllOrgOptions] = useState([]); @@ -79,14 +81,14 @@ export default function DeviceManagementPage() { return (
- 设备管理 + {t('router.devices')}
state.user); const { stationId, currentStation } = useSelector((state: RootState) => state.station); @@ -233,8 +235,8 @@ export default function DroneManagement({ modal.confirm({ title: '确认解绑', content: `确定要为选中的无人机清除${typeText}绑定吗?`, - okText: '确定', - cancelText: '取消', + okText: t('common.ok'), + cancelText: t('common.cancel'), onOk: async () => { try { const res = await unbindDrone({ sns: validSns, type }); @@ -254,8 +256,8 @@ export default function DroneManagement({ }; const droneColumns = [ - { title: '设备名称', width: 150, dataIndex: 'drone_callsign', key: 'name' }, - { title: 'SN码', width: 180, dataIndex: 'device_sn', key: 'sn' }, + { title: t('devices.deviceName'), width: 150, dataIndex: 'drone_callsign', key: 'name' }, + { title: t('systemSetting.snCode'), width: 180, dataIndex: 'device_sn', key: 'sn' }, { title: '机场SN', width: 150, dataIndex: 'gateway_sn', key: 'gsn' }, { title: '所属组织', width: 150, key: 'orgName', @@ -272,18 +274,18 @@ export default function DroneManagement({ }, }, { - title: '负责人', width: 150, key: 'userName', + title: t('systemSetting.leader'), width: 150, key: 'userName', render: (_, record) => { const user = allUserOptions.find(u => u.userId == record.userId); return user ? (user.nickName || user.userName) : 未绑定; }, }, { - title: '状态', width: 150, dataIndex: 'onlineStatus', key: 'status', - render: s => {s === 1 ? '在线' : '离线'} + title: t('roles.status'), width: 150, dataIndex: 'onlineStatus', key: 'status', + render: s => {s === 1 ? t('devices.online') : t('devices.offline')} }, { - title: '操作', key: 'action', fixed: 'right' as const, width: 250, + title: t('roles.actions'), key: 'action', fixed: 'right' as const, width: 250, render: (_, record: any) => ( @@ -326,16 +328,16 @@ export default function DroneManagement({ onChange={(e) => setDroneParams({ ...droneParams, sn: e.target.value })} /> - + @@ -434,7 +436,7 @@ export default function DroneManagement({ ))} - + + + - - + + - - {errorSourceOptions.map(option => ( ))} - - {errorLevelOptions.map(option => ( ))} @@ -390,9 +386,9 @@ export default function ErrorManagement() { - - - + + + @@ -409,7 +405,7 @@ export default function ErrorManagement() { total: total, showSizeChanger: true, showQuickJumper: true, - showTotal: (t) => `共 ${t} 条`, + showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`, onChange: (page, pageSize) => { setParams({ ...params, pageNum: page, pageSize }); }, @@ -421,10 +417,10 @@ export default function ErrorManagement() { { @@ -435,15 +431,15 @@ export default function ErrorManagement() { footer={ modalType === 'view' ? [ - + ] : [
, , - + }}>{t('common.cancel')}, + ] } > @@ -457,13 +453,13 @@ export default function ErrorManagement() { {/* 第一行:基础编码名称 */}
- - + + - - + + @@ -471,8 +467,8 @@ export default function ErrorManagement() { {/* 第二行:来源、等级、校验字段 */} - - {errorSourceOptions.map(option => ( ))} @@ -480,8 +476,8 @@ export default function ErrorManagement() { - - {errorLevelOptions.map(option => ( ))} @@ -489,8 +485,8 @@ export default function ErrorManagement() { - - + + @@ -498,8 +494,8 @@ export default function ErrorManagement() { {/* 第三行:对比类型 + 对比值(自动切换单输入/区间双输入) */} - - {CompareEnumOptions.map(item => ( ))} @@ -510,33 +506,33 @@ export default function ErrorManagement() { {CompareEnumOptions.find(o => o.value === compareTypeWatch)?.range ? ( <> - - + + - - + + ) : ( - - + + )} {/* 错误描述 */} - -